use crate::{Standing, consts::MIN_VOTE_INTERVAL, error::RingspaceError, managed_ring, member};
use serde::{Deserialize, Serialize};
use sqlx::{FromRow, Sqlite, SqliteConnection, query, query_as, sqlite::SqliteQueryResult};
use time::{Duration, UtcDateTime};

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

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

    ///
    /// Adds new [MemberVote]s to the database. Also performs safety checks
    /// for rapid voting, based on [consts::MIN_VOTE_INTERVAL].
    ///
    /// This will most commonly be used when parsing votes from manifests.
    ///  
    pub async fn save(
        &self,
        conn: &mut SqliteConnection,
    ) -> Result<SqliteQueryResult, RingspaceError> {
        // Notify if first vote from member
        // Only perform check on new votes
        if self.id == 0 {
            let voting_member = member::get_by_id(conn, self.member_id).await?;
            let member_votes = voting_member.votes(&mut *conn).await?;
            // If no prior votes, notify
            if member_votes.is_empty() {
                println!(
                    "First vote for {} from {}",
                    self.vote_subject_url, voting_member.url
                );
            }
        }

        // Here we check for a previous vote and compare its timestamp to this
        // one. This helps prevent spamming the audit log.

        if let Some(last_vote) = query_as::<Sqlite, MemberVote>(
            "
            SELECT * FROM member_votes
            WHERE member_id = ?
            AND ring_id = ?
            AND vote_subject_url = ?
            ORDER BY timestamp DESC
            ",
        )
        .bind(self.member_id)
        .bind(self.ring_id)
        .bind(&self.vote_subject_url)
        .fetch_optional(&mut *conn)
        .await?
        {
            let last_vote_time = UtcDateTime::from_unix_timestamp(last_vote.timestamp).unwrap();
            let new_vote_time = UtcDateTime::from_unix_timestamp(self.timestamp).unwrap();
            let now = UtcDateTime::now();
            let diff: Duration = new_vote_time - last_vote_time;

            // If the interval between votes is too small
            // or the new time is greater than the present, move on
            if diff.whole_seconds() < MIN_VOTE_INTERVAL || new_vote_time >= now {
                return Err(RingspaceError {
                    msg: format!(
                        "Vote for {} too soon after previous vote",
                        &self.vote_subject_url
                    ),
                });
            }
        }

        match query(
            "
            INSERT INTO member_votes
                (ring_id, member_id, standing, vote_subject_url, description, timestamp)
            VALUES
                (?, ?, ?, ?, ?, ?);
            ",
        )
        .bind(self.ring_id)
        .bind(self.member_id)
        .bind(self.standing.clone())
        .bind(self.vote_subject_url.clone())
        .bind(self.description.clone())
        .bind(self.timestamp)
        .execute(&mut *conn)
        .await
        {
            Ok(r) => Ok(r),
            Err(e) => Err(RingspaceError::from(e)),
        }
    }
    ///
    /// Determines if a MemberVote is valid
    ///
    pub async fn validate(self, conn: &mut SqliteConnection) -> Result<bool, RingspaceError> {
        let ring = managed_ring::get_by_id(conn, self.ring_id).await?;
        let members = ring.members(conn).await?;
        let member_urls: Vec<String> = members.iter().map(|m| m.url.clone()).collect();

        // Is the vote for a URL in the ring?
        if !member_urls.contains(&self.vote_subject_url) {
            Ok(false)
        } else {
            // Is it a real standing?
            Ok(Standing::try_from(self.standing).is_ok())
        }
    }
}
