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

use atuin_common::db;
use atuin_common::db::sqlite::{Sqlite, SqliteBuilder};
use eyre::Result;
use time::OffsetDateTime;

use crate::session::CachedUsageSnapshot;
use crate::usage::UsageSnapshot;

// Database row mappings — all columns are kept even if not yet read in
// non-test code, since they're part of the schema and used in tests.
#[derive(Debug, sqlx::FromRow)]
#[allow(dead_code)]
pub struct StoredSession {
    pub id: String,
    pub head_id: Option<String>,
    pub server_session_id: Option<String>,
    pub directory: Option<String>,
    pub git_root: Option<String>,
    pub created_at: i64,
    pub updated_at: i64,
    pub archived_at: Option<i64>,
}

#[derive(Debug, sqlx::FromRow)]
#[allow(dead_code)]
pub struct StoredEvent {
    pub id: String,
    pub session_id: String,
    pub parent_id: Option<String>,
    pub invocation_id: String,
    pub event_type: String,
    pub event_data: String,
    pub created_at: i64,
}

pub struct AiSessionStore {
    sqlite: Sqlite,
}

impl AiSessionStore {
    pub async fn new(path: impl AsRef<OsStr>, timeout: Duration) -> Result<Self> {
        Self::from_builder(Sqlite::builder(path.as_ref()), timeout).await
    }

    #[cfg(test)]
    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).foreign_keys(false).restrict_permissions().open().await?;

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

        Ok(Self { sqlite })
    }

    pub async fn create_session(
        &self,
        id: &str,
        directory: Option<&str>,
        git_root: Option<&str>,
    ) -> Result<StoredSession> {
        let now = OffsetDateTime::now_utc().unix_timestamp();

        db::query(
            "INSERT INTO sessions (id, directory, git_root, created_at, updated_at)
             VALUES (?1, ?2, ?3, ?4, ?4)",
        )
        .bind(id)
        .bind(directory)
        .bind(git_root)
        .bind(now)
        .execute(self.sqlite.pool())
        .await?;

        Ok(StoredSession {
            id: id.to_string(),
            head_id: None,
            server_session_id: None,
            directory: directory.map(String::from),
            git_root: git_root.map(String::from),
            created_at: now,
            updated_at: now,
            archived_at: None,
        })
    }

    #[allow(dead_code)] // used in tests; will be used by daemon service
    pub async fn get_session(&self, id: &str) -> Result<Option<StoredSession>> {
        let session = db::query_as::<_, StoredSession>(
            "SELECT id, head_id, server_session_id, directory, git_root,
                    created_at, updated_at, archived_at
             FROM sessions WHERE id = ?1",
        )
        .bind(id)
        .fetch_optional(self.sqlite.pool())
        .await?;

        Ok(session)
    }

    /// Find the most recent non-archived session matching the given directory or git
    /// root, updated within `max_age_secs` seconds.
    pub async fn find_resumable_session(
        &self,
        directory: Option<&str>,
        git_root: Option<&str>,
        max_age_secs: i64,
    ) -> Result<Option<StoredSession>> {
        let cutoff = OffsetDateTime::now_utc().unix_timestamp() - max_age_secs;

        let session = db::query_as::<_, StoredSession>(
            "SELECT id, head_id, server_session_id, directory, git_root,
                    created_at, updated_at, archived_at
             FROM sessions
             WHERE archived_at IS NULL
               AND updated_at > ?1
               AND (directory = ?2 OR (git_root IS NOT NULL AND git_root = ?3))
             ORDER BY updated_at DESC
             LIMIT 1",
        )
        .bind(cutoff)
        .bind(directory)
        .bind(git_root)
        .fetch_optional(self.sqlite.pool())
        .await?;

        Ok(session)
    }

    /// Append a single event and update the session's `head_id` and `updated_at`.
    pub async fn append_event(
        &self,
        session_id: &str,
        event_id: &str,
        parent_id: Option<&str>,
        invocation_id: &str,
        event_type: &str,
        event_data: &str,
    ) -> Result<()> {
        let now = OffsetDateTime::now_utc().unix_timestamp();

        let mut tx = self.sqlite.pool().begin().await?;

        db::query(
            "INSERT INTO session_events (id, session_id, parent_id, invocation_id, event_type, \
             event_data, created_at)
             VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
        )
        .bind(event_id)
        .bind(session_id)
        .bind(parent_id)
        .bind(invocation_id)
        .bind(event_type)
        .bind(event_data)
        .bind(now)
        .execute(&mut *tx)
        .await?;

        db::query("UPDATE sessions SET head_id = ?1, updated_at = ?2 WHERE id = ?3")
            .bind(event_id)
            .bind(now)
            .bind(session_id)
            .execute(&mut *tx)
            .await?;

        tx.commit().await?;
        Ok(())
    }

    /// Load all events for a session, ordered chronologically.
    pub async fn load_events(&self, session_id: &str) -> Result<Vec<StoredEvent>> {
        let events = db::query_as::<_, StoredEvent>(
            "SELECT id, session_id, parent_id, invocation_id, event_type, event_data, created_at
                 FROM session_events
                 WHERE session_id = ?1
                 ORDER BY created_at ASC, rowid ASC",
        )
        .bind(session_id)
        .fetch_all(self.sqlite.pool())
        .await?;

        Ok(events)
    }

    pub async fn update_server_session_id(
        &self,
        session_id: &str,
        server_session_id: &str,
    ) -> Result<()> {
        db::query("UPDATE sessions SET server_session_id = ?1 WHERE id = ?2")
            .bind(server_session_id)
            .bind(session_id)
            .execute(self.sqlite.pool())
            .await?;
        Ok(())
    }

    pub async fn archive_session(&self, session_id: &str) -> Result<()> {
        let now = OffsetDateTime::now_utc().unix_timestamp();
        db::query("UPDATE sessions SET archived_at = ?1 WHERE id = ?2")
            .bind(now)
            .bind(session_id)
            .execute(self.sqlite.pool())
            .await?;
        Ok(())
    }

    // ── Session metadata (key-value per session) ──

    /// Read a metadata value for a session. Returns `None` if the key doesn't
    /// exist or the session hasn't been persisted yet.
    pub async fn get_metadata(&self, session_id: &str, key: &str) -> Result<Option<String>> {
        let row: Option<(String,)> =
            db::query_as("SELECT value FROM session_metadata WHERE session_id = ?1 AND key = ?2")
                .bind(session_id)
                .bind(key)
                .fetch_optional(self.sqlite.pool())
                .await?;

        Ok(row.map(|(v,)| v))
    }

    /// Write a metadata value for a session (upsert).
    pub async fn set_metadata(&self, session_id: &str, key: &str, value: &str) -> Result<()> {
        let now = OffsetDateTime::now_utc().unix_timestamp();
        db::query(
            "INSERT INTO session_metadata (session_id, key, value, updated_at)
             VALUES (?1, ?2, ?3, ?4)
             ON CONFLICT (session_id, key) DO UPDATE SET value = ?3, updated_at = ?4",
        )
        .bind(session_id)
        .bind(key)
        .bind(value)
        .bind(now)
        .execute(self.sqlite.pool())
        .await?;
        Ok(())
    }

    // ── Usage cache (server credit totals, one row per user key) ──

    /// Read the cached usage snapshot for a user key. Returns the snapshot
    /// JSON and the unix timestamp it was written at.
    pub async fn get_usage(&self, user_key: &str) -> Result<Option<CachedUsageSnapshot>> {
        let row: Option<(String, i64)> =
            db::query_as("SELECT snapshot, updated_at FROM usage WHERE user_key = ?1")
                .bind(user_key)
                .fetch_optional(self.sqlite.pool())
                .await?;

        let Some((snapshot, written_at)) = row else {
            return Ok(None);
        };

        let snapshot = serde_json::from_str(&snapshot)?;
        Ok(Some(CachedUsageSnapshot {
            snapshot,
            written_at,
        }))
    }

    /// Write the usage snapshot for a user key (upsert).
    pub async fn set_usage(&self, user_key: &str, snapshot: &UsageSnapshot) -> Result<()> {
        let snapshot_json = serde_json::to_string(snapshot)?;
        let now = OffsetDateTime::now_utc().unix_timestamp();
        db::query(
            "INSERT INTO usage (user_key, snapshot, updated_at)
             VALUES (?1, ?2, ?3)
             ON CONFLICT (user_key) DO UPDATE SET snapshot = ?2, updated_at = ?3",
        )
        .bind(user_key)
        .bind(snapshot_json)
        .bind(now)
        .execute(self.sqlite.pool())
        .await?;
        Ok(())
    }
}

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

    use super::*;
    use crate::usage::UsageSnapshot;

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

    #[fixture]
    async fn store_with_s1(#[future] store: AiSessionStore) -> AiSessionStore {
        let store = store.await;
        store.create_session("s1", Some("/tmp"), None).await.unwrap();
        store
    }

    #[rstest]
    #[tokio::test]
    async fn test_create_and_get_session(#[future] store: AiSessionStore) {
        let store = store.await;

        let session = store
            .create_session("s1", Some("/home/user/project"), Some("/home/user/project"))
            .await
            .unwrap();
        assert_eq!(session.id, "s1");
        assert!(session.head_id.is_none());
        assert!(session.archived_at.is_none());

        let loaded = store.get_session("s1").await.unwrap().unwrap();
        assert_eq!(loaded.id, "s1");
        assert_eq!(loaded.directory.as_deref(), Some("/home/user/project"));
    }

    #[rstest]
    #[tokio::test]
    async fn test_get_nonexistent_session(#[future] store: AiSessionStore) {
        let store = store.await;
        assert!(store.get_session("nope").await.unwrap().is_none());
    }

    #[rstest]
    #[tokio::test]
    async fn test_append_and_load_events(#[future] store_with_s1: AiSessionStore) {
        let store = store_with_s1.await;

        store
            .append_event("s1", "e1", None, "inv1", "user_message", r#"{"content":"hello"}"#)
            .await
            .unwrap();
        store
            .append_event("s1", "e2", Some("e1"), "inv1", "text", r#"{"content":"hi there"}"#)
            .await
            .unwrap();

        let events = store.load_events("s1").await.unwrap();
        assert_eq!(events.len(), 2);
        assert_eq!(events[0].id, "e1");
        assert!(events[0].parent_id.is_none());
        assert_eq!(events[0].invocation_id, "inv1");
        assert_eq!(events[1].id, "e2");
        assert_eq!(events[1].parent_id.as_deref(), Some("e1"));

        let session = store.get_session("s1").await.unwrap().unwrap();
        assert_eq!(session.head_id.as_deref(), Some("e2"));
    }

    #[rstest]
    #[tokio::test]
    async fn test_find_resumable_session(#[future] store: AiSessionStore) {
        let store = store.await;
        store.create_session("s1", Some("/home/user/project"), None).await.unwrap();

        let found =
            store.find_resumable_session(Some("/home/user/project"), None, 3600).await.unwrap();
        assert!(found.is_some());
        assert_eq!(found.unwrap().id, "s1");
    }

    #[rstest]
    #[tokio::test]
    async fn test_find_resumable_by_git_root(#[future] store: AiSessionStore) {
        let store = store.await;
        store
            .create_session("s1", Some("/home/user/project/sub"), Some("/home/user/project"))
            .await
            .unwrap();

        let found = store
            .find_resumable_session(Some("/different/dir"), Some("/home/user/project"), 3600)
            .await
            .unwrap();
        assert!(found.is_some());
        assert_eq!(found.unwrap().id, "s1");
    }

    #[rstest]
    #[tokio::test]
    async fn test_find_resumable_skips_archived(#[future] store: AiSessionStore) {
        let store = store.await;
        store.create_session("s1", Some("/tmp"), None).await.unwrap();
        store.archive_session("s1").await.unwrap();

        let found = store.find_resumable_session(Some("/tmp"), None, 3600).await.unwrap();
        assert!(found.is_none());
    }

    #[rstest]
    #[tokio::test]
    async fn test_find_resumable_no_match_different_dir(#[future] store: AiSessionStore) {
        let store = store.await;
        store.create_session("s1", Some("/home/user/project"), None).await.unwrap();

        let found = store.find_resumable_session(Some("/other/dir"), None, 3600).await.unwrap();
        assert!(found.is_none());
    }

    #[rstest]
    #[tokio::test]
    async fn test_archive_session(#[future] store_with_s1: AiSessionStore) {
        let store = store_with_s1.await;

        store.archive_session("s1").await.unwrap();

        let session = store.get_session("s1").await.unwrap().unwrap();
        assert!(session.archived_at.is_some());
    }

    #[rstest]
    #[tokio::test]
    async fn test_update_server_session_id(#[future] store_with_s1: AiSessionStore) {
        let store = store_with_s1.await;

        store.update_server_session_id("s1", "server-abc").await.unwrap();

        let session = store.get_session("s1").await.unwrap().unwrap();
        assert_eq!(session.server_session_id.as_deref(), Some("server-abc"));
    }

    #[rstest]
    #[tokio::test]
    async fn test_find_resumable_does_not_mutate(#[future] store_with_s1: AiSessionStore) {
        let store = store_with_s1.await;

        let before = store.get_session("s1").await.unwrap().unwrap();
        store.find_resumable_session(Some("/tmp"), None, 3600).await.unwrap().unwrap();
        let after = store.get_session("s1").await.unwrap().unwrap();

        assert_eq!(before.updated_at, after.updated_at);
    }

    #[rstest]
    #[tokio::test]
    async fn test_usage_cache_roundtrip(#[future] store: AiSessionStore) {
        use crate::usage::UsageBucket;

        let store = store.await;

        assert!(store.get_usage("key-a").await.unwrap().is_none());

        let snapshot = UsageSnapshot {
            period: "calendar_monthly".into(),
            resets_at: "2026-08-01T00:00:00Z".into(),
            requests: UsageBucket { used: 1, limit: 10 },
            input: UsageBucket { used: 2, limit: 20 },
            output: UsageBucket { used: 3, limit: 0 },
        };
        store.set_usage("key-a", &snapshot).await.unwrap();

        let cached = store.get_usage("key-a").await.unwrap().unwrap();
        assert!(cached.written_at > 0);
        assert_eq!(cached.snapshot, snapshot);

        // Upsert replaces the snapshot for the same key
        let updated = UsageSnapshot {
            requests: UsageBucket { used: 4, limit: 10 },
            ..snapshot
        };
        store.set_usage("key-a", &updated).await.unwrap();

        let cached = store.get_usage("key-a").await.unwrap().unwrap();
        assert_eq!(cached.snapshot, updated);

        // Other keys are independent
        assert!(store.get_usage("key-b").await.unwrap().is_none());
    }

    #[rstest]
    #[tokio::test]
    async fn test_events_ordered_chronologically(#[future] store_with_s1: AiSessionStore) {
        let store = store_with_s1.await;

        store.append_event("s1", "e1", None, "inv1", "user_message", "{}").await.unwrap();
        store.append_event("s1", "e2", Some("e1"), "inv1", "text", "{}").await.unwrap();
        store.append_event("s1", "e3", Some("e2"), "inv2", "user_message", "{}").await.unwrap();

        let events = store.load_events("s1").await.unwrap();
        assert_eq!(events.len(), 3);
        assert!(events[0].created_at <= events[1].created_at);
        assert!(events[1].created_at <= events[2].created_at);
        assert_eq!(events[2].invocation_id, "inv2");
    }
}
