use std::ffi::OsStr;
use std::str::FromStr;
use std::time::Duration;

use atuin_common::db;
use atuin_common::db::sqlite::{Journaling, Sqlite, SqliteBuilder};
use atuin_domain::record::HostId;
use eyre::{Result, eyre};
use time::OffsetDateTime;
use time::format_description::well_known::Rfc3339;
use tokio::sync::OnceCell;
use uuid::Uuid;

// Filenames for the legacy plain-text files that we migrate from.
const LEGACY_HOST_ID_FILENAME: &str = "host_id";
const LEGACY_LAST_SYNC_FILENAME: &str = "last_sync_time";
const LEGACY_LAST_VERSION_CHECK_FILENAME: &str = "last_version_check_time";
const LEGACY_LATEST_VERSION_FILENAME: &str = "latest_version";
const LEGACY_SESSION_FILENAME: &str = "session";

const KEY_HOST_ID: &str = "host_id";
const KEY_LAST_SYNC: &str = "last_sync_time";
const KEY_LAST_VERSION_CHECK: &str = "last_version_check_time";
const KEY_LATEST_VERSION: &str = "latest_version";
const KEY_SESSION: &str = "session";
const KEY_HUB_SESSION: &str = "hub_session";
const KEY_FILES_MIGRATED: &str = "files_migrated";

pub struct MetaStore {
    sqlite: Sqlite,
    cached_host_id: OnceCell<HostId>,
}

impl MetaStore {
    pub async fn new(path: impl AsRef<OsStr>, timeout: Duration) -> Result<Self> {
        let path = path.as_ref();
        debug!("opening meta sqlite database at {path:?}");

        let builder = Sqlite::builder(path);
        let ephemeral = builder.is_memory();

        let store = Self::from_builder(builder, timeout).await?;

        if !ephemeral {
            store.migrate_files().await?;
        }

        Ok(store)
    }

    pub async fn in_memory(timeout: Duration) -> Result<Self> {
        Self::from_builder(Sqlite::builder_in_memory(), timeout).await
    }

    async fn from_builder(builder: SqliteBuilder<'_>, timeout: Duration) -> Result<Self> {
        let sqlite = builder
            .timeout(timeout)
            .journal(Some(Journaling::Delete))
            .foreign_keys(false)
            .restrict_permissions()
            .open()
            .await?;

        db::migrate!(sqlite.pool(), "./meta-migrations").await?;

        Ok(Self {
            sqlite,
            cached_host_id: OnceCell::const_new(),
        })
    }

    // Generic key-value operations

    pub async fn get(&self, key: &str) -> Result<Option<String>> {
        let row: Option<(String,)> = db::query_as("SELECT value FROM meta WHERE key = ?1")
            .bind(key)
            .fetch_optional(self.sqlite.pool())
            .await?;

        Ok(row.map(|r| r.0))
    }

    pub async fn set(&self, key: &str, value: &str) -> Result<()> {
        db::query(
            "INSERT INTO meta (key, value, updated_at) VALUES (?1, ?2, strftime('%s', 'now'))
             ON CONFLICT(key) DO UPDATE SET value = ?2, updated_at = strftime('%s', 'now')",
        )
        .bind(key)
        .bind(value)
        .execute(self.sqlite.pool())
        .await?;

        Ok(())
    }

    pub async fn delete(&self, key: &str) -> Result<()> {
        db::query("DELETE FROM meta WHERE key = ?1").bind(key).execute(self.sqlite.pool()).await?;

        Ok(())
    }

    // Typed accessors

    pub async fn host_id(&self) -> Result<HostId> {
        self.cached_host_id
            .get_or_try_init(|| async {
                if let Some(id) = self.get(KEY_HOST_ID).await? {
                    let parsed = Uuid::from_str(id.as_str())
                        .map_err(|e| eyre!("failed to parse host ID: {e}"))?;
                    return Ok(HostId(parsed));
                }

                let uuid = atuin_common::utils::uuid_v7();
                self.set(KEY_HOST_ID, uuid.as_simple().to_string().as_ref()).await?;

                Ok(HostId(uuid))
            })
            .await
            .copied()
    }

    pub async fn last_sync(&self) -> Result<OffsetDateTime> {
        match self.get(KEY_LAST_SYNC).await? {
            Some(v) => Ok(OffsetDateTime::parse(v.as_str(), &Rfc3339)?),
            None => Ok(OffsetDateTime::UNIX_EPOCH),
        }
    }

    pub async fn save_sync_time(&self) -> Result<()> {
        self.set(KEY_LAST_SYNC, OffsetDateTime::now_utc().format(&Rfc3339)?.as_str()).await
    }

    pub async fn last_version_check(&self) -> Result<OffsetDateTime> {
        match self.get(KEY_LAST_VERSION_CHECK).await? {
            Some(v) => Ok(OffsetDateTime::parse(v.as_str(), &Rfc3339)?),
            None => Ok(OffsetDateTime::UNIX_EPOCH),
        }
    }

    pub async fn save_version_check_time(&self) -> Result<()> {
        self.set(KEY_LAST_VERSION_CHECK, OffsetDateTime::now_utc().format(&Rfc3339)?.as_str()).await
    }

    pub async fn latest_version(&self) -> Result<Option<String>> {
        self.get(KEY_LATEST_VERSION).await
    }

    pub async fn save_latest_version(&self, version: &str) -> Result<()> {
        self.set(KEY_LATEST_VERSION, version).await
    }

    pub async fn session_token(&self) -> Result<Option<String>> {
        self.get(KEY_SESSION).await
    }

    pub async fn save_session(&self, token: &str) -> Result<()> {
        self.set(KEY_SESSION, token).await
    }

    pub async fn delete_session(&self) -> Result<()> {
        self.delete(KEY_SESSION).await
    }

    pub async fn logged_in(&self) -> Result<bool> {
        Ok(self.session_token().await?.is_some() || self.hub_session_token().await?.is_some())
    }

    // Hub session methods (separate from sync session, used for Hub-specific features like AI)

    pub async fn hub_session_token(&self) -> Result<Option<String>> {
        self.get(KEY_HUB_SESSION).await
    }

    pub async fn save_hub_session(&self, token: &str) -> Result<()> {
        self.set(KEY_HUB_SESSION, token).await
    }

    pub async fn delete_hub_session(&self) -> Result<()> {
        self.delete(KEY_HUB_SESSION).await
    }

    pub async fn hub_logged_in(&self) -> Result<bool> {
        Ok(self.hub_session_token().await?.is_some())
    }

    // File migration: on first open, migrate old plain-text files into the database.
    // Old files are left in place for safe downgrades.

    async fn migrate_files(&self) -> Result<()> {
        if self.get(KEY_FILES_MIGRATED).await?.is_some() {
            return Ok(());
        }

        let data_dir = crate::settings::Settings::effective_data_dir();

        // host_id — validate as UUID
        let host_id_path = data_dir.join(LEGACY_HOST_ID_FILENAME);
        if host_id_path.exists()
            && let Ok(value) = fs_err::read_to_string(&host_id_path)
        {
            let value = value.trim();
            if !value.is_empty() {
                if Uuid::from_str(value).is_ok() {
                    self.set(KEY_HOST_ID, value).await?;
                } else {
                    warn!("skipping migration of host_id: invalid UUID {value:?}");
                }
            }
        }

        // last_sync_time — validate as RFC3339
        let sync_path = data_dir.join(LEGACY_LAST_SYNC_FILENAME);
        if sync_path.exists()
            && let Ok(value) = fs_err::read_to_string(&sync_path)
        {
            let value = value.trim();
            if !value.is_empty() {
                if OffsetDateTime::parse(value, &Rfc3339).is_ok() {
                    self.set(KEY_LAST_SYNC, value).await?;
                } else {
                    warn!("skipping migration of last_sync_time: invalid RFC3339 {value:?}");
                }
            }
        }

        // last_version_check_time — validate as RFC3339
        let version_check_path = data_dir.join(LEGACY_LAST_VERSION_CHECK_FILENAME);
        if version_check_path.exists()
            && let Ok(value) = fs_err::read_to_string(&version_check_path)
        {
            let value = value.trim();
            if !value.is_empty() {
                if OffsetDateTime::parse(value, &Rfc3339).is_ok() {
                    self.set(KEY_LAST_VERSION_CHECK, value).await?;
                } else {
                    warn!(
                        "skipping migration of last_version_check_time: invalid RFC3339 {value:?}"
                    );
                }
            }
        }

        // latest_version — no strict validation, just non-empty
        let latest_version_path = data_dir.join(LEGACY_LATEST_VERSION_FILENAME);
        if latest_version_path.exists()
            && let Ok(value) = fs_err::read_to_string(&latest_version_path)
        {
            let value = value.trim();
            if !value.is_empty() {
                self.set(KEY_LATEST_VERSION, value).await?;
            }
        }

        // session token — no strict validation, just non-empty
        let session_path = data_dir.join(LEGACY_SESSION_FILENAME);
        if session_path.exists()
            && let Ok(value) = fs_err::read_to_string(&session_path)
        {
            let value = value.trim();
            if !value.is_empty() {
                self.set(KEY_SESSION, value).await?;
            }
        }

        self.set(KEY_FILES_MIGRATED, "true").await?;

        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use rstest::*;

    use super::*;

    #[fixture]
    async fn store() -> MetaStore {
        MetaStore::in_memory(Duration::from_secs(2)).await.unwrap()
    }

    #[rstest]
    #[tokio::test]
    async fn test_get_set_delete(#[future(awt)] store: MetaStore) {
        assert_eq!(store.get("foo").await.unwrap(), None);

        store.set("foo", "bar").await.unwrap();
        assert_eq!(store.get("foo").await.unwrap(), Some("bar".to_string()));

        store.set("foo", "baz").await.unwrap();
        assert_eq!(store.get("foo").await.unwrap(), Some("baz".to_string()));

        store.delete("foo").await.unwrap();
        assert_eq!(store.get("foo").await.unwrap(), None);
    }

    #[rstest]
    #[tokio::test]
    async fn test_host_id_generation_and_stability(#[future(awt)] store: MetaStore) {
        let id1 = store.host_id().await.unwrap();
        let id2 = store.host_id().await.unwrap();

        assert_eq!(id1, id2, "host_id should be stable across calls");
    }

    #[rstest]
    #[tokio::test]
    async fn test_sync_time(#[future(awt)] store: MetaStore) {
        let t = store.last_sync().await.unwrap();
        assert_eq!(t, OffsetDateTime::UNIX_EPOCH);

        store.save_sync_time().await.unwrap();
        let t = store.last_sync().await.unwrap();
        assert!(t > OffsetDateTime::UNIX_EPOCH);
    }

    #[rstest]
    #[tokio::test]
    async fn test_version_check_time(#[future(awt)] store: MetaStore) {
        let t = store.last_version_check().await.unwrap();
        assert_eq!(t, OffsetDateTime::UNIX_EPOCH);

        store.save_version_check_time().await.unwrap();
        let t = store.last_version_check().await.unwrap();
        assert!(t > OffsetDateTime::UNIX_EPOCH);
    }

    #[rstest]
    #[tokio::test]
    async fn test_session_crud(#[future(awt)] store: MetaStore) {
        assert!(!store.logged_in().await.unwrap());
        assert_eq!(store.session_token().await.unwrap(), None);

        store.save_session("tok123").await.unwrap();
        assert!(store.logged_in().await.unwrap());
        assert_eq!(store.session_token().await.unwrap(), Some("tok123".to_string()));

        store.delete_session().await.unwrap();
        assert!(!store.logged_in().await.unwrap());
    }

    #[rstest]
    #[tokio::test]
    async fn test_latest_version(#[future(awt)] store: MetaStore) {
        assert_eq!(store.latest_version().await.unwrap(), None);

        store.save_latest_version("1.2.3").await.unwrap();
        assert_eq!(store.latest_version().await.unwrap(), Some("1.2.3".to_string()));
    }

    #[tokio::test]
    async fn memory_store_skips_file_migration() {
        let store = MetaStore::new(":memory:", Duration::from_secs(2)).await.unwrap();

        assert_eq!(store.get(KEY_FILES_MIGRATED).await.unwrap(), None);
    }
}
