use std::num::TryFromIntError;
use std::str::FromStr;

use localtime::LocalTime;
use sqlite as sql;
use thiserror::Error;

use crate::git::Oid;
use crate::git::fmt::Qualified;
use crate::node::Database;
use crate::node::NodeId;
use crate::prelude::RepoId;
use crate::storage;
use crate::storage::{ReadRepository, ReadStorage, RemoteRepository, RepositoryError};

#[derive(Error, Debug)]
pub enum Error {
    /// An Internal error.
    #[error("internal error: {0}")]
    Internal(#[from] sql::Error),
    /// Timestamp error.
    #[error("invalid timestamp: {0}")]
    Timestamp(#[from] TryFromIntError),
    /// Repository error.
    #[error("repository error: {0}")]
    Repository(#[from] RepositoryError),
    /// Storage error.
    #[error("storage error: {0}")]
    Storage(#[from] storage::Error),
    /// Storage refs error.
    #[error("storage refs error: {0}")]
    Refs(#[from] storage::refs::Error),
    /// No rows returned in query result.
    #[error("no rows returned")]
    NoRows,
}

/// Refs store.
///
/// Used to cache git references.
pub trait Store {
    /// Set a reference under a remote namespace to the given [`Oid`].
    fn set(
        &mut self,
        repo: &RepoId,
        namespace: &NodeId,
        refname: &Qualified,
        oid: Oid,
        timestamp: LocalTime,
    ) -> Result<bool, Error>;
    /// Get a reference's [`Oid`] and timestamp.
    fn get(
        &self,
        repo: &RepoId,
        namespace: &NodeId,
        refname: &Qualified,
    ) -> Result<Option<(Oid, LocalTime)>, Error>;
    /// Delete a reference.
    fn delete(
        &mut self,
        repo: &RepoId,
        namespace: &NodeId,
        refname: &Qualified,
    ) -> Result<bool, Error>;
    /// Populate the database from storage.
    fn populate<S: ReadStorage>(&mut self, storage: &S) -> Result<(), Error>;
    /// Return the number of references.
    fn count(&self) -> Result<usize, Error>;
    /// Check if there are any references.
    fn is_empty(&self) -> Result<bool, Error> {
        self.count().map(|l| l == 0)
    }
}

impl Store for Database {
    fn set(
        &mut self,
        repo: &RepoId,
        namespace: &NodeId,
        refname: &Qualified,
        oid: Oid,
        timestamp: LocalTime,
    ) -> Result<bool, Error> {
        let mut stmt = self.db.prepare(
            "INSERT INTO `refs` (repo, namespace, ref, oid, timestamp)
             VALUES (?1, ?2, ?3, ?4, ?5)
             ON CONFLICT DO UPDATE
             SET oid = ?4, timestamp = ?5
             WHERE timestamp < ?5 AND oid <> ?4",
        )?;
        stmt.bind((1, repo))?;
        stmt.bind((2, namespace))?;
        stmt.bind((3, refname.to_string().as_str()))?;
        stmt.bind((4, oid.to_string().as_str()))?;
        stmt.bind((5, i64::try_from(timestamp.as_millis())?))?;
        stmt.next()?;

        Ok(self.db.change_count() > 0)
    }

    fn get(
        &self,
        repo: &RepoId,
        namespace: &NodeId,
        refname: &Qualified,
    ) -> Result<Option<(Oid, LocalTime)>, Error> {
        let mut stmt = self.db.prepare(
            "SELECT oid, timestamp FROM refs WHERE repo = ?1 AND namespace = ?2 AND ref = ?3",
        )?;

        stmt.bind((1, repo))?;
        stmt.bind((2, namespace))?;
        stmt.bind((3, refname.to_string().as_str()))?;

        if let Some(Ok(row)) = stmt.into_iter().next() {
            let oid = row.try_read::<&str, _>("oid")?;
            let oid = Oid::from_str(oid).map_err(|e| {
                Error::Internal(sql::Error {
                    code: None,
                    message: Some(format!("sql: invalid oid '{oid}': {e}")),
                })
            })?;
            let timestamp = row.try_read::<i64, _>("timestamp")?;
            let timestamp = LocalTime::from_millis(timestamp as u128);

            Ok(Some((oid, timestamp)))
        } else {
            Ok(None)
        }
    }

    fn delete(
        &mut self,
        repo: &RepoId,
        namespace: &NodeId,
        refname: &Qualified,
    ) -> Result<bool, Error> {
        let mut stmt = self
            .db
            .prepare("DELETE FROM refs WHERE repo = ?1 AND namespace = ?2 AND ref = ?3")?;

        stmt.bind((1, repo))?;
        stmt.bind((2, namespace))?;
        stmt.bind((3, refname.to_string().as_str()))?;
        stmt.next()?;

        Ok(self.db.change_count() > 0)
    }

    fn count(&self) -> Result<usize, Error> {
        let row = self
            .db
            .prepare("SELECT COUNT(*) FROM refs")?
            .into_iter()
            .next()
            .ok_or(Error::NoRows)??;
        let count = row.try_read::<i64, _>(0)? as usize;

        Ok(count)
    }

    fn populate<S: ReadStorage>(&mut self, storage: &S) -> Result<(), Error> {
        let now = LocalTime::now();

        for info in storage.repositories()? {
            let repo = storage.repository(info.rid)?;
            for refs_at in repo.remote_refs_at()? {
                self.set(&repo.id(), &refs_at.remote, refs_at.path(), refs_at.at, now)?;
            }
        }
        Ok(())
    }
}

#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod test {
    use super::*;
    use crate::git::fmt::qualified;
    use crate::test::arbitrary;
    use localtime::{LocalDuration, LocalTime};

    #[test]
    fn test_count() {
        let mut db = Database::memory().unwrap();
        let oid = arbitrary::oid();

        let repo = arbitrary::r#gen::<RepoId>(1);
        let namespace = arbitrary::r#gen::<NodeId>(1);
        let refname1 = qualified!("refs/heads/master");
        let refname2 = qualified!("refs/heads/main");
        let timestamp = LocalTime::now();

        assert!(db.is_empty().unwrap());
        assert_eq!(db.count().unwrap(), 0);

        assert!(
            db.set(&repo, &namespace, &refname1, oid, timestamp)
                .unwrap()
        );
        assert!(!db.is_empty().unwrap());
        assert_eq!(db.count().unwrap(), 1);

        assert!(
            db.set(&repo, &namespace, &refname2, oid, timestamp)
                .unwrap()
        );
        assert_eq!(db.count().unwrap(), 2);
    }

    #[test]
    fn test_set_and_delete() {
        let mut db = Database::memory().unwrap();
        let oid = arbitrary::oid();

        let repo = arbitrary::r#gen::<RepoId>(1);
        let namespace = arbitrary::r#gen::<NodeId>(1);
        let refname = qualified!("refs/heads/master");
        let timestamp = LocalTime::now();

        assert!(db.set(&repo, &namespace, &refname, oid, timestamp).unwrap());
        assert!(db.get(&repo, &namespace, &refname).unwrap().is_some());
        assert!(db.delete(&repo, &namespace, &refname).unwrap());
        assert!(db.get(&repo, &namespace, &refname).unwrap().is_none());
        assert!(!db.delete(&repo, &namespace, &refname).unwrap());
    }

    #[test]
    fn test_set_and_get() {
        let mut db = Database::memory().unwrap();
        let oid1 = arbitrary::oid();
        let oid2 = arbitrary::oid();

        assert_ne!(oid1, oid2);

        let repo = arbitrary::r#gen::<RepoId>(1);
        let namespace = arbitrary::r#gen::<NodeId>(1);
        let refname = qualified!("refs/heads/master");
        let mut timestamp = LocalTime::now();

        assert_eq!(db.get(&repo, &namespace, &refname).unwrap(), None);
        assert!(
            db.set(&repo, &namespace, &refname, oid1, timestamp)
                .unwrap()
        );
        assert_eq!(
            db.get(&repo, &namespace, &refname).unwrap(),
            Some((oid1, timestamp))
        );
        assert!(
            !db.set(&repo, &namespace, &refname, oid1, timestamp)
                .unwrap()
        );
        timestamp.elapse(LocalDuration::from_millis(1));

        assert!(
            db.set(&repo, &namespace, &refname, oid2, timestamp)
                .unwrap()
        );
        assert_eq!(
            db.get(&repo, &namespace, &refname).unwrap(),
            Some((oid2, timestamp))
        );
    }
}
