use crate::RingspaceError;
use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD as BASE64URL};
use crypto_box::{PublicKey, aead::OsRng};
use serde::{Deserialize, Serialize};
use serde_json::to_string;
use sqlx::{Error, FromRow, SqliteConnection, query, sqlite::SqliteQueryResult};
use time::UtcDateTime;

///
/// A vote from the single site perspective
///
#[derive(FromRow, Debug, Serialize, Deserialize, Clone)]
pub struct SiteVote {
    pub id: i64,
    pub ring_id: i64,
    pub site_id: i64,
    pub vote_subject_url: String,
    pub standing: String,
    pub description: String,
    pub timestamp: i64,
}

impl SiteVote {
    pub fn new(
        ring_id: i64,
        site_id: i64,
        vote_subject_url: String,
        standing: String,
        description: String,
    ) -> Self {
        let timestamp: i64 = UtcDateTime::now().unix_timestamp();
        SiteVote {
            id: 0,
            ring_id,
            site_id,
            vote_subject_url,
            standing,
            description,
            timestamp,
        }
    }

    ///
    /// Updates votes from member manifests.
    /// Because manifests contains only the most recent vote for a given
    /// site, this takes a single [SiteVote] arg, and compares it to
    /// what the Ring has in its database of historical votes.
    ///
    pub async fn save(self, conn: &mut SqliteConnection) -> Result<SqliteQueryResult, Error> {
        // If new Vote, add
        if self.id == 0 {
            return query(
                "
                INSERT INTO site_votes (ring_id, site_id, vote_subject_url, standing, description, timestamp)
                VALUES (?, ?, ?, ?, ?, ?)
                "
            )
            .bind(self.ring_id)
            .bind(self.site_id)
            .bind(self.vote_subject_url)
            .bind(self.standing)
            .bind(self.description)
            .bind(self.timestamp)
            .execute(&mut *conn)
            .await;
        }

        // Otherwise, update
        let new_timestamp = UtcDateTime::now().unix_timestamp();
        query(
            "
            UPDATE site_votes SET
                ring_id = ?
                site_id = ?
                vote_subject_url = ?
                standing = ?
                description = ?
                timestamp = ?
            WHERE id = ?
            ",
        )
        .bind(self.site_id)
        .bind(self.vote_subject_url)
        .bind(self.standing)
        .bind(self.description)
        .bind(new_timestamp)
        .bind(self.id)
        .execute(&mut *conn)
        .await
    }

    ///
    /// Encrypts site votes using site private enc key and ring public enc key.
    ///
    pub async fn encrypt(self, ring_public_encryption_key: &str) -> Result<String, RingspaceError> {
        let mut public_key_bytes = [0u8; 32];
        let vote_string = to_string(&self)?;
        BASE64URL
            .decode_slice(ring_public_encryption_key.as_bytes(), &mut public_key_bytes)
            .unwrap();
        let pubkey = PublicKey::from(public_key_bytes);
        let ciphertext = pubkey.seal(&mut OsRng, vote_string.as_bytes())?;
        Ok(BASE64URL.encode(&ciphertext))
    }
}
