use rusqlite::{params, params_from_iter, types::Value, Connection};
use serde::Serialize;
use std::sync::Mutex;

// ponytail: a separate SQLite file from lore.db so a corrupt generations
// store can't lose the user's world bible. Same `bundled` rusqlite feature.
// Generations are append-only in v1: no schema migrations planned.

const SCHEMA: &str = "CREATE TABLE IF NOT EXISTS generations (
    id          INTEGER PRIMARY KEY AUTOINCREMENT,
    kind        TEXT NOT NULL,
    title       TEXT NOT NULL,
    data        TEXT NOT NULL,
    source      TEXT,
    created_at  INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_gen_kind ON generations(kind);
CREATE INDEX IF NOT EXISTS idx_gen_created ON generations(created_at DESC);";

#[derive(Debug, Serialize, Clone)]
pub struct Generation {
    pub id: i64,
    pub kind: String,
    pub title: String,
    pub data: String,
    pub source: Option<String>,
    pub created_at: i64,
}

#[derive(Debug, Serialize)]
pub struct GenerationSummary {
    pub id: i64,
    pub kind: String,
    pub title: String,
    /// Epoch ms — for relative-time display in the UI.
    pub created_at: i64,
}

pub struct GenerationStore {
    db: Mutex<Connection>,
}

impl GenerationStore {
    pub fn open(dir: &std::path::Path) -> anyhow::Result<Self> {
        std::fs::create_dir_all(dir)?;
        let path = dir.join("generations.db");
        let db = Connection::open(path)?;
        db.execute_batch(SCHEMA)?;
        Ok(Self { db: Mutex::new(db) })
    }

    /// Append a generation. `data` is the raw JSON string from the LLM
    /// (already-validated by the caller), `source` is the human-readable
    /// input (theme, name, etc.) so the history list can show context.
    pub fn add(
        &self,
        kind: &str,
        title: &str,
        data: &str,
        source: Option<&str>,
    ) -> anyhow::Result<i64> {
        let now = std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .map(|d| d.as_millis() as i64)
            .unwrap_or(0);
        let db = self.db.lock().map_err(|e| anyhow::anyhow!("db lock: {e}"))?;
        db.execute(
            "INSERT INTO generations (kind, title, data, source, created_at) VALUES (?, ?, ?, ?, ?)",
            params![kind, title, data, source, now],
        )?;
        Ok(db.last_insert_rowid())
    }

    /// All generations, newest first, optionally filtered by kind.
    pub fn list(&self, kind: Option<&str>) -> anyhow::Result<Vec<GenerationSummary>> {
        let db = self.db.lock().map_err(|e| anyhow::anyhow!("db lock: {e}"))?;
        let (sql, params): (&str, Vec<Value>) = match kind {
            Some(k) => (
                "SELECT id, kind, title, created_at FROM generations WHERE kind = ?1 ORDER BY created_at DESC",
                vec![Value::from(k.to_string())],
            ),
            None => (
                "SELECT id, kind, title, created_at FROM generations ORDER BY created_at DESC",
                vec![],
            ),
        };
        let mut stmt = db.prepare(sql)?;
        let rows = stmt.query_map(params_from_iter(params.iter()), |r| {
            Ok(GenerationSummary {
                id: r.get(0)?,
                kind: r.get(1)?,
                title: r.get(2)?,
                created_at: r.get(3)?,
            })
        })?;
        let mut out = Vec::new();
        for r in rows {
            out.push(r?);
        }
        Ok(out)
    }

    /// Full row including data + source. Used when rehydrating into a tool.
    pub fn get(&self, id: i64) -> anyhow::Result<Option<Generation>> {
        let db = self.db.lock().map_err(|e| anyhow::anyhow!("db lock: {e}"))?;
        let mut stmt = db.prepare(
            "SELECT id, kind, title, data, source, created_at FROM generations WHERE id = ?",
        )?;
        let mut rows = stmt.query(params![id])?;
        if let Some(r) = rows.next()? {
            Ok(Some(Generation {
                id: r.get(0)?,
                kind: r.get(1)?,
                title: r.get(2)?,
                data: r.get(3)?,
                source: r.get(4)?,
                created_at: r.get(5)?,
            }))
        } else {
            Ok(None)
        }
    }

    /// Delete one row, or all rows if id is None.
    pub fn delete(&self, id: Option<i64>) -> anyhow::Result<usize> {
        let db = self.db.lock().map_err(|e| anyhow::anyhow!("db lock: {e}"))?;
        let n = match id {
            Some(i) => db.execute("DELETE FROM generations WHERE id = ?", params![i])?,
            None => db.execute("DELETE FROM generations", [])?,
        };
        Ok(n)
    }

    /// Count by kind — for the dashboard "X NPCs, Y encounters" pill.
    pub fn counts(&self) -> anyhow::Result<Vec<(String, i64)>> {
        let db = self.db.lock().map_err(|e| anyhow::anyhow!("db lock: {e}"))?;
        let mut stmt = db.prepare(
            "SELECT kind, COUNT(*) FROM generations GROUP BY kind ORDER BY kind",
        )?;
        let rows = stmt.query_map([], |r| Ok((r.get::<_, String>(0)?, r.get::<_, i64>(1)?)))?;
        let mut out = Vec::new();
        for r in rows {
            out.push(r?);
        }
        Ok(out)
    }

    /// Read-only SQL pass-through for the advanced SQLite viewer.
    pub fn raw_query(
        &self,
        sql: &str,
    ) -> anyhow::Result<(Vec<String>, Vec<Vec<serde_json::Value>>)> {
        let db = self.db.lock().map_err(|e| anyhow::anyhow!("db lock: {e}"))?;
        crate::sql_viewer::run_readonly_query(&db, sql)
    }
}

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

    fn tmp() -> std::path::PathBuf {
        let dir = std::env::temp_dir().join(format!("dm-pal-test-{}", std::process::id()));
        let _ = std::fs::remove_dir_all(&dir);
        dir
    }

    #[test]
    fn round_trip() {
        let dir = tmp();
        let store = GenerationStore::open(&dir).unwrap();
        let id = store
            .add("npc", "Thorin Stonefist", r#"{"name":"Thorin"}"#, Some("Dwarf Fighter"))
            .unwrap();
        let g = store.get(id).unwrap().unwrap();
        assert_eq!(g.kind, "npc");
        assert_eq!(g.title, "Thorin Stonefist");
        assert_eq!(g.source.as_deref(), Some("Dwarf Fighter"));
        assert!(g.created_at > 0);

        let list = store.list(None).unwrap();
        assert_eq!(list.len(), 1);
        assert_eq!(list[0].id, id);

        let npcs = store.list(Some("npc")).unwrap();
        assert_eq!(npcs.len(), 1);
        let items = store.list(Some("item")).unwrap();
        assert_eq!(items.len(), 0);

        let counts = store.counts().unwrap();
        assert_eq!(counts, vec![("npc".to_string(), 1)]);

        assert_eq!(store.delete(Some(id)).unwrap(), 1);
        assert!(store.get(id).unwrap().is_none());
    }

    #[test]
    fn clear_all() {
        let dir = tmp();
        let store = GenerationStore::open(&dir).unwrap();
        store.add("a", "x", "{}", None).unwrap();
        store.add("b", "y", "{}", None).unwrap();
        assert_eq!(store.delete(None).unwrap(), 2);
        assert_eq!(store.list(None).unwrap().len(), 0);
    }
}
