use rusqlite::{Connection, Result, params};
use serde::{Deserialize, Serialize};
use chrono::Utc;
use uuid::Uuid;
use std::collections::HashMap;

use crate::parser::CombatIndicator;

#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Profile {
    pub id: String,
    pub name: String,
    pub created_at: String,
}

#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct GuideRow {
    pub gid: String,
    pub name: String,
    pub data: String,
    pub last_synced_at: Option<String>,
}

pub fn get_db_path() -> String {
    let data_dir = dirs_next::data_dir()
        .unwrap_or_else(|| std::path::PathBuf::from("."))
        .join("toughligui");
    std::fs::create_dir_all(&data_dir).ok();
    data_dir.join("toughligui.db").to_string_lossy().to_string()
}

pub fn open() -> Result<Connection> {
    let path = get_db_path();
    let conn = Connection::open(&path)?;
    conn.execute_batch("PRAGMA journal_mode=WAL; PRAGMA foreign_keys=ON;")?;
    Ok(conn)
}

pub fn migrate(conn: &Connection) -> Result<()> {
    conn.execute_batch("
        CREATE TABLE IF NOT EXISTS profiles (
            id TEXT PRIMARY KEY,
            name TEXT UNIQUE NOT NULL,
            created_at TEXT NOT NULL
        );

        CREATE TABLE IF NOT EXISTS guides (
            gid TEXT PRIMARY KEY,
            name TEXT NOT NULL,
            data TEXT NOT NULL,
            last_synced_at TEXT
        );

        CREATE TABLE IF NOT EXISTS quest_completions (
            profile_id TEXT NOT NULL,
            quest_name TEXT NOT NULL,
            completed_at TEXT NOT NULL,
            PRIMARY KEY (profile_id, quest_name),
            FOREIGN KEY (profile_id) REFERENCES profiles(id) ON DELETE CASCADE
        );

        CREATE TABLE IF NOT EXISTS settings (
            key TEXT PRIMARY KEY,
            value TEXT NOT NULL
        );

        CREATE TABLE IF NOT EXISTS quest_step_progress (
            profile_id TEXT NOT NULL,
            quest_name TEXT NOT NULL,
            step_index INTEGER NOT NULL,
            PRIMARY KEY (profile_id, quest_name, step_index),
            FOREIGN KEY (profile_id) REFERENCES profiles(id) ON DELETE CASCADE
        );

        CREATE TABLE IF NOT EXISTS resource_inventory (
            profile_id TEXT NOT NULL,
            resource_name TEXT NOT NULL,
            quantity INTEGER NOT NULL DEFAULT 0,
            PRIMARY KEY (profile_id, resource_name),
            FOREIGN KEY (profile_id) REFERENCES profiles(id) ON DELETE CASCADE
        );

        CREATE TABLE IF NOT EXISTS quest_previews (
            quest_url TEXT PRIMARY KEY,
            indicators_json TEXT NOT NULL,
            cached_at TEXT NOT NULL DEFAULT (datetime('now'))
        );
    ")?;
    Ok(())
}

pub fn get_cached_previews(conn: &Connection, urls: &[String]) -> HashMap<String, Vec<CombatIndicator>> {
    if urls.is_empty() {
        return HashMap::new();
    }

    // Construit les placeholders : (?1, ?2, …)
    let placeholders: String = (1..=urls.len())
        .map(|i| format!("?{}", i))
        .collect::<Vec<_>>()
        .join(", ");
    let sql = format!(
        "SELECT quest_url, indicators_json FROM quest_previews WHERE quest_url IN ({})",
        placeholders
    );

    let mut stmt = match conn.prepare(&sql) {
        Ok(s) => s,
        Err(_) => return HashMap::new(),
    };

    // rusqlite attend &dyn ToSql — on construit un vecteur de références
    let params_vec: Vec<&dyn rusqlite::types::ToSql> = urls
        .iter()
        .map(|u| u as &dyn rusqlite::types::ToSql)
        .collect();

    let rows = match stmt.query_map(params_vec.as_slice(), |row| {
        Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
    }) {
        Ok(r) => r,
        Err(_) => return HashMap::new(),
    };

    let mut map = HashMap::new();
    for row in rows.flatten() {
        let (url, json) = row;
        if let Ok(indicators) = serde_json::from_str::<Vec<CombatIndicator>>(&json) {
            map.insert(url, indicators);
        }
    }
    map
}

pub fn upsert_preview(conn: &Connection, url: &str, indicators: &[CombatIndicator]) -> Result<()> {
    let json = serde_json::to_string(indicators).unwrap_or_else(|_| "[]".to_string());
    let now = Utc::now().to_rfc3339();
    conn.execute(
        "INSERT INTO quest_previews (quest_url, indicators_json, cached_at) VALUES (?1, ?2, ?3)
         ON CONFLICT(quest_url) DO UPDATE SET indicators_json=excluded.indicators_json, cached_at=excluded.cached_at",
        params![url, json, now],
    )?;
    Ok(())
}

/// Retourne l'ensemble des URLs déjà présentes dans quest_previews parmi celles fournies.
pub fn get_cached_urls(conn: &Connection, urls: &[String]) -> std::collections::HashSet<String> {
    if urls.is_empty() {
        return std::collections::HashSet::new();
    }

    let placeholders: String = (1..=urls.len())
        .map(|i| format!("?{}", i))
        .collect::<Vec<_>>()
        .join(", ");
    let sql = format!(
        "SELECT quest_url FROM quest_previews WHERE quest_url IN ({})",
        placeholders
    );

    let mut stmt = match conn.prepare(&sql) {
        Ok(s) => s,
        Err(_) => return std::collections::HashSet::new(),
    };

    let params_vec: Vec<&dyn rusqlite::types::ToSql> = urls
        .iter()
        .map(|u| u as &dyn rusqlite::types::ToSql)
        .collect();

    let rows = match stmt.query_map(params_vec.as_slice(), |row| row.get::<_, String>(0)) {
        Ok(r) => r,
        Err(_) => return std::collections::HashSet::new(),
    };

    rows.flatten().collect()
}

/// Charge un guide depuis la DB à partir de son gid.
pub fn get_guide(conn: &Connection, gid: &str) -> Result<Option<crate::parser::GuideData>> {
    let result = conn.query_row(
        "SELECT data FROM guides WHERE gid = ?1",
        params![gid],
        |row| row.get::<_, String>(0),
    );
    match result {
        Ok(json) => {
            let data = serde_json::from_str(&json)
                .map_err(|e| rusqlite::Error::FromSqlConversionFailure(
                    0,
                    rusqlite::types::Type::Text,
                    Box::new(e),
                ))?;
            Ok(Some(data))
        }
        Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
        Err(e) => Err(e),
    }
}

pub fn get_profiles(conn: &Connection) -> Result<Vec<Profile>> {
    let mut stmt = conn.prepare("SELECT id, name, created_at FROM profiles ORDER BY created_at ASC")?;
    let rows = stmt.query_map([], |row| {
        Ok(Profile {
            id: row.get(0)?,
            name: row.get(1)?,
            created_at: row.get(2)?,
        })
    })?;
    rows.collect()
}

pub fn create_profile(conn: &Connection, name: &str) -> Result<Profile> {
    let id = Uuid::new_v4().to_string();
    let created_at = Utc::now().to_rfc3339();
    conn.execute(
        "INSERT INTO profiles (id, name, created_at) VALUES (?1, ?2, ?3)",
        params![id, name, created_at],
    )?;
    Ok(Profile { id, name: name.to_string(), created_at })
}

pub fn delete_profile(conn: &Connection, profile_id: &str) -> Result<()> {
    conn.execute("DELETE FROM profiles WHERE id = ?1", params![profile_id])?;
    Ok(())
}

pub fn get_completed_quests(conn: &Connection, profile_id: &str) -> Result<Vec<String>> {
    let mut stmt = conn.prepare(
        "SELECT quest_name FROM quest_completions WHERE profile_id = ?1"
    )?;
    let rows = stmt.query_map(params![profile_id], |row| row.get(0))?;
    rows.collect()
}

pub fn toggle_quest(conn: &Connection, profile_id: &str, quest_name: &str) -> Result<bool> {
    let exists: bool = conn.query_row(
        "SELECT COUNT(*) FROM quest_completions WHERE profile_id = ?1 AND quest_name = ?2",
        params![profile_id, quest_name],
        |row| row.get::<_, i64>(0),
    ).map(|c| c > 0)?;

    if exists {
        conn.execute(
            "DELETE FROM quest_completions WHERE profile_id = ?1 AND quest_name = ?2",
            params![profile_id, quest_name],
        )?;
        Ok(false)
    } else {
        let now = Utc::now().to_rfc3339();
        conn.execute(
            "INSERT INTO quest_completions (profile_id, quest_name, completed_at) VALUES (?1, ?2, ?3)",
            params![profile_id, quest_name, now],
        )?;
        Ok(true)
    }
}

pub fn upsert_guide(conn: &Connection, gid: &str, name: &str, data: &str) -> Result<()> {
    let now = Utc::now().to_rfc3339();
    conn.execute(
        "INSERT INTO guides (gid, name, data, last_synced_at) VALUES (?1, ?2, ?3, ?4)
         ON CONFLICT(gid) DO UPDATE SET name=excluded.name, data=excluded.data, last_synced_at=excluded.last_synced_at",
        params![gid, name, data, now],
    )?;
    Ok(())
}

pub fn get_guides(conn: &Connection) -> Result<Vec<GuideRow>> {
    let mut stmt = conn.prepare("SELECT gid, name, data, last_synced_at FROM guides ORDER BY rowid ASC")?;
    let rows = stmt.query_map([], |row| {
        Ok(GuideRow {
            gid: row.get(0)?,
            name: row.get(1)?,
            data: row.get(2)?,
            last_synced_at: row.get(3)?,
        })
    })?;
    rows.collect()
}

pub fn get_completed_steps(conn: &Connection, profile_id: &str, quest_name: &str) -> Result<Vec<i64>> {
    let mut stmt = conn.prepare(
        "SELECT step_index FROM quest_step_progress WHERE profile_id = ?1 AND quest_name = ?2"
    )?;
    let rows = stmt.query_map(params![profile_id, quest_name], |row| row.get(0))?;
    rows.collect()
}

pub fn toggle_quest_step(conn: &Connection, profile_id: &str, quest_name: &str, step_index: i64) -> Result<bool> {
    let exists: bool = conn.query_row(
        "SELECT COUNT(*) FROM quest_step_progress WHERE profile_id = ?1 AND quest_name = ?2 AND step_index = ?3",
        params![profile_id, quest_name, step_index],
        |row| row.get::<_, i64>(0),
    ).map(|c| c > 0)?;

    if exists {
        conn.execute(
            "DELETE FROM quest_step_progress WHERE profile_id = ?1 AND quest_name = ?2 AND step_index = ?3",
            params![profile_id, quest_name, step_index],
        )?;
        Ok(false)
    } else {
        conn.execute(
            "INSERT INTO quest_step_progress (profile_id, quest_name, step_index) VALUES (?1, ?2, ?3)",
            params![profile_id, quest_name, step_index],
        )?;
        Ok(true)
    }
}

pub fn get_resource_inventory(conn: &Connection, profile_id: &str) -> Result<Vec<(String, i64)>> {
    let mut stmt = conn.prepare(
        "SELECT resource_name, quantity FROM resource_inventory WHERE profile_id = ?1"
    )?;
    let rows = stmt.query_map(params![profile_id], |row| {
        Ok((row.get::<_, String>(0)?, row.get::<_, i64>(1)?))
    })?;
    rows.collect()
}

pub fn set_resource_quantity(conn: &Connection, profile_id: &str, resource_name: &str, quantity: i64) -> Result<()> {
    if quantity <= 0 {
        conn.execute(
            "DELETE FROM resource_inventory WHERE profile_id = ?1 AND resource_name = ?2",
            params![profile_id, resource_name],
        )?;
    } else {
        conn.execute(
            "INSERT INTO resource_inventory (profile_id, resource_name, quantity) VALUES (?1, ?2, ?3)
             ON CONFLICT(profile_id, resource_name) DO UPDATE SET quantity=excluded.quantity",
            params![profile_id, resource_name, quantity],
        )?;
    }
    Ok(())
}

pub fn get_setting(conn: &Connection, key: &str) -> Option<String> {
    conn.query_row(
        "SELECT value FROM settings WHERE key = ?1",
        params![key],
        |row| row.get(0),
    ).ok()
}

pub fn set_setting(conn: &Connection, key: &str, value: &str) -> Result<()> {
    conn.execute(
        "INSERT INTO settings (key, value) VALUES (?1, ?2)
         ON CONFLICT(key) DO UPDATE SET value=excluded.value",
        params![key, value],
    )?;
    Ok(())
}

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

    fn test_db() -> Connection {
        let conn = Connection::open_in_memory().unwrap();
        migrate(&conn).unwrap();
        conn
    }

    fn make_indicator(t: &str, c: &str) -> CombatIndicator {
        CombatIndicator {
            combat_type: t.to_string(),
            count: c.to_string(),
            label: None,
            evitable: false,
        }
    }

    // ── migrate ──────────────────────────────────────────────────────────────

    #[test]
    fn test_migrate_creates_all_tables() {
        let conn = test_db();
        let expected_tables = [
            "profiles",
            "guides",
            "quest_completions",
            "settings",
            "quest_step_progress",
            "resource_inventory",
            "quest_previews",
        ];
        for table in &expected_tables {
            let count: i64 = conn
                .query_row(
                    "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name=?1",
                    params![table],
                    |row| row.get(0),
                )
                .unwrap();
            assert_eq!(count, 1, "Table '{}' manquante après migration", table);
        }
    }

    #[test]
    fn test_migrate_idempotent() {
        let conn = test_db();
        // Un second appel ne doit pas paniquer ni retourner d'erreur (IF NOT EXISTS)
        migrate(&conn).expect("Le second appel à migrate() ne doit pas échouer");
    }

    // ── settings ─────────────────────────────────────────────────────────────

    #[test]
    fn test_set_and_get_setting_roundtrip() {
        let conn = test_db();
        set_setting(&conn, "theme", "dark").unwrap();
        let value = get_setting(&conn, "theme");
        assert_eq!(value, Some("dark".to_string()));
    }

    #[test]
    fn test_get_setting_missing_key_returns_none() {
        let conn = test_db();
        let value = get_setting(&conn, "cle_inexistante");
        assert!(value.is_none());
    }

    #[test]
    fn test_set_setting_overwrites_existing_value() {
        let conn = test_db();
        set_setting(&conn, "theme", "light").unwrap();
        set_setting(&conn, "theme", "dark").unwrap();
        let value = get_setting(&conn, "theme");
        assert_eq!(value, Some("dark".to_string()));
    }

    // ── upsert_preview / get_cached_previews ─────────────────────────────────

    #[test]
    fn test_upsert_and_get_cached_previews_roundtrip() {
        let conn = test_db();
        let url = "https://example.com/quete/test".to_string();
        let indicators = vec![
            make_indicator("Monstre", "3"),
            make_indicator("Boss", "1"),
        ];

        upsert_preview(&conn, &url, &indicators).unwrap();

        let result = get_cached_previews(&conn, &[url.clone()]);
        assert!(result.contains_key(&url), "L'URL doit être présente dans le résultat");

        let retrieved = &result[&url];
        assert_eq!(retrieved.len(), 2);
        assert_eq!(retrieved[0].combat_type, "Monstre");
        assert_eq!(retrieved[0].count, "3");
        assert_eq!(retrieved[1].combat_type, "Boss");
        assert_eq!(retrieved[1].count, "1");
    }

    #[test]
    fn test_upsert_preview_idempotent() {
        let conn = test_db();
        let url = "https://example.com/quete/idempotent".to_string();
        let indicators_v1 = vec![make_indicator("Monstre", "2")];
        let indicators_v2 = vec![make_indicator("Boss", "5")];

        upsert_preview(&conn, &url, &indicators_v1).unwrap();
        // Deuxième upsert sur la même URL : pas d'erreur de contrainte UNIQUE
        upsert_preview(&conn, &url, &indicators_v2).unwrap();

        let result = get_cached_previews(&conn, &[url.clone()]);
        let retrieved = &result[&url];
        // Seul le dernier upsert doit être présent
        assert_eq!(retrieved.len(), 1);
        assert_eq!(retrieved[0].combat_type, "Boss");
        assert_eq!(retrieved[0].count, "5");
    }

    // ── get_cached_previews — cas limites ────────────────────────────────────

    #[test]
    fn test_get_cached_previews_partial_hit() {
        let conn = test_db();
        let url_cached = "https://example.com/quete/en-cache".to_string();
        let url_missing = "https://example.com/quete/absente".to_string();

        upsert_preview(&conn, &url_cached, &[make_indicator("Monstre", "1")]).unwrap();

        let result = get_cached_previews(&conn, &[url_cached.clone(), url_missing.clone()]);
        assert!(result.contains_key(&url_cached), "L'URL en cache doit être retournée");
        assert!(!result.contains_key(&url_missing), "L'URL absente ne doit pas figurer dans le résultat");
        assert_eq!(result.len(), 1);
    }

    #[test]
    fn test_get_cached_previews_empty_input() {
        let conn = test_db();
        let result = get_cached_previews(&conn, &[]);
        assert!(result.is_empty(), "Une liste vide d'URLs doit retourner un HashMap vide");
    }

    // ── get_cached_urls ───────────────────────────────────────────────────────

    #[test]
    fn test_get_cached_urls_returns_only_cached() {
        let conn = test_db();
        let cached: Vec<String> = (1..=3)
            .map(|i| format!("https://example.com/quete/{}", i))
            .collect();
        let extra: Vec<String> = (4..=5)
            .map(|i| format!("https://example.com/quete/{}", i))
            .collect();

        for url in &cached {
            upsert_preview(&conn, url, &[make_indicator("Monstre", "1")]).unwrap();
        }

        let all_urls: Vec<String> = cached.iter().chain(extra.iter()).cloned().collect();
        let result = get_cached_urls(&conn, &all_urls);

        assert_eq!(result.len(), 3, "Seules les 3 URLs en cache doivent être retournées");
        for url in &cached {
            assert!(result.contains(url), "URL '{}' attendue dans le HashSet", url);
        }
        for url in &extra {
            assert!(!result.contains(url), "URL '{}' ne doit pas être dans le HashSet", url);
        }
    }

    #[test]
    fn test_get_cached_urls_empty_input() {
        let conn = test_db();
        let result = get_cached_urls(&conn, &[]);
        assert!(result.is_empty(), "Une liste vide doit retourner un HashSet vide");
    }
}
