use crate::index::parse::ParsedFile;
use rusqlite::{Connection, OpenFlags};
use std::path::PathBuf;

/// 当前 schema 版本，结构变动时递增（变动即整库重建，索引可丢弃重来）
const SCHEMA_VERSION: i64 = 5;

/// 索引库存放目录：%APPDATA%\ccgui\index\
pub fn index_dir() -> PathBuf {
    dirs::config_dir()
        .unwrap_or_else(|| PathBuf::from("."))
        .join("ccgui")
        .join("index")
}

/// FNV-1a 64 位哈希：把项目绝对路径映射为稳定的文件名（同一可执行文件内跨进程一致）
fn fnv1a(s: &str) -> u64 {
    let mut hash: u64 = 0xcbf2_9ce4_8422_2325;
    for b in s.as_bytes() {
        hash ^= u64::from(*b);
        hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
    }
    hash
}

/// 某项目根目录对应的索引库路径
pub fn db_path_for(project_root: &str) -> PathBuf {
    index_dir().join(format!("{:016x}.db", fnv1a(project_root)))
}

/// 打开可写连接（主进程写入者用）：建目录、建表、开 WAL，schema 版本不符则清空重建
pub fn open_rw(path: &PathBuf) -> Result<Connection, String> {
    if let Some(parent) = path.parent() {
        std::fs::create_dir_all(parent).map_err(|e| e.to_string())?;
    }
    let conn = Connection::open(path).map_err(|e| e.to_string())?;
    conn.pragma_update(None, "journal_mode", "WAL")
        .map_err(|e| e.to_string())?;
    conn.pragma_update(None, "synchronous", "NORMAL")
        .map_err(|e| e.to_string())?;
    conn.pragma_update(None, "foreign_keys", "ON")
        .map_err(|e| e.to_string())?;
    conn.busy_timeout(std::time::Duration::from_secs(5))
        .map_err(|e| e.to_string())?;
    init_schema(&conn)?;
    Ok(conn)
}

/// 打开查询连接（--mcp-index 读取者用）：库不存在返回 None。
/// 用 READ_WRITE（不带 CREATE）而非 READ_ONLY：Windows 上只读连接无法参与 WAL 的 -shm
/// 共享内存索引，写入者（主进程）仍打开时只读会打开失败；再以 query_only 强制禁写保证只读语义。
pub fn open_ro(path: &PathBuf) -> Option<Connection> {
    if !path.exists() {
        return None;
    }
    let conn = Connection::open_with_flags(
        path,
        OpenFlags::SQLITE_OPEN_READ_WRITE | OpenFlags::SQLITE_OPEN_NO_MUTEX,
    )
    .ok()?;
    let _ = conn.busy_timeout(std::time::Duration::from_secs(5));
    let _ = conn.pragma_update(None, "query_only", "ON");
    Some(conn)
}

fn init_schema(conn: &Connection) -> Result<(), String> {
    let version: i64 = conn
        .pragma_query_value(None, "user_version", |r| r.get(0))
        .unwrap_or(0);
    if version != SCHEMA_VERSION {
        // 版本不符：丢弃旧结构重建（索引是可再生数据，无需迁移）
        conn.execute_batch(
            "DROP TABLE IF EXISTS symbols;
             DROP TABLE IF EXISTS imports;
             DROP TABLE IF EXISTS inherits;
             DROP TABLE IF EXISTS fts;
             DROP TABLE IF EXISTS parse_issues;
             DROP TABLE IF EXISTS meta;
             DROP TABLE IF EXISTS files;",
        )
        .map_err(|e| e.to_string())?;
    }
    conn.execute_batch(
        "CREATE TABLE IF NOT EXISTS files (
            id        INTEGER PRIMARY KEY,
            rel_path  TEXT NOT NULL UNIQUE,
            mtime_ms  INTEGER NOT NULL,
            lang      TEXT NOT NULL
         );
         CREATE TABLE IF NOT EXISTS symbols (
            id         INTEGER PRIMARY KEY,
            file_id    INTEGER NOT NULL REFERENCES files(id) ON DELETE CASCADE,
            name       TEXT NOT NULL,
            kind       TEXT NOT NULL,
            is_def     INTEGER NOT NULL,
            start_line INTEGER NOT NULL,
            end_line   INTEGER NOT NULL,
            signature  TEXT NOT NULL
         );
         CREATE TABLE IF NOT EXISTS imports (
            id               INTEGER PRIMARY KEY,
            file_id          INTEGER NOT NULL REFERENCES files(id) ON DELETE CASCADE,
            module           TEXT NOT NULL,
            resolved_file_id INTEGER REFERENCES files(id) ON DELETE SET NULL
         );
         CREATE INDEX IF NOT EXISTS idx_symbols_name ON symbols(name);
         CREATE INDEX IF NOT EXISTS idx_symbols_file ON symbols(file_id);
         CREATE INDEX IF NOT EXISTS idx_symbols_def  ON symbols(is_def);
         CREATE INDEX IF NOT EXISTS idx_imports_file ON imports(file_id);
         CREATE INDEX IF NOT EXISTS idx_imports_resolved ON imports(resolved_file_id);
         CREATE TABLE IF NOT EXISTS inherits (
            id      INTEGER PRIMARY KEY,
            file_id INTEGER NOT NULL REFERENCES files(id) ON DELETE CASCADE,
            child   TEXT NOT NULL,
            parent  TEXT NOT NULL
         );
         CREATE INDEX IF NOT EXISTS idx_inherits_child ON inherits(child);
         CREATE INDEX IF NOT EXISTS idx_inherits_parent ON inherits(parent);
         CREATE TABLE IF NOT EXISTS parse_issues (
            rel_path TEXT PRIMARY KEY,
            lang     TEXT NOT NULL,
            reason   TEXT NOT NULL
         );
         CREATE TABLE IF NOT EXISTS meta (
            key   TEXT PRIMARY KEY,
            value TEXT NOT NULL
         );",
    )
    .map_err(|e| e.to_string())?;
    // 全文检索：按源码行存储（trigram 分词，支持子串/代码片段检索）。
    // file/line 不参与分词；FTS5 无 FK，文件级删除需手动清理。
    conn.execute_batch(
        "CREATE VIRTUAL TABLE IF NOT EXISTS fts USING fts5(\
            content, file UNINDEXED, line UNINDEXED, tokenize='trigram');",
    )
    .map_err(|e| format!("创建 FTS5 表失败（SQLite 未启用 FTS5？）: {e}"))?;
    conn.pragma_update(None, "user_version", SCHEMA_VERSION)
        .map_err(|e| e.to_string())?;
    Ok(())
}

/// 读取某文件已记录的 mtime（用于全量扫描时跳过未变文件）
pub fn file_mtime(conn: &Connection, rel: &str) -> Option<u64> {
    conn.query_row(
        "SELECT mtime_ms FROM files WHERE rel_path = ?1",
        [rel],
        |r| r.get::<_, i64>(0),
    )
    .ok()
    .map(|v| v as u64)
}

/// 写入/更新单个文件的解析结果：先删旧行（级联清符号/导入，手动清 FTS）再插新行。
/// source 为文件原始内容，用于填充全文检索表。
pub fn upsert_file(
    conn: &Connection,
    rel: &str,
    mtime_ms: u64,
    parsed: &ParsedFile,
    source: &str,
) -> Result<(), String> {
    conn.execute("DELETE FROM files WHERE rel_path = ?1", [rel])
        .map_err(|e| e.to_string())?;
    conn.execute("DELETE FROM fts WHERE file = ?1", [rel])
        .map_err(|e| e.to_string())?;
    // 成功入库即清除该文件的历史解析问题记录
    conn.execute("DELETE FROM parse_issues WHERE rel_path = ?1", [rel])
        .map_err(|e| e.to_string())?;
    conn.execute(
        "INSERT INTO files (rel_path, mtime_ms, lang) VALUES (?1, ?2, ?3)",
        rusqlite::params![rel, mtime_ms as i64, parsed.lang],
    )
    .map_err(|e| e.to_string())?;
    let file_id = conn.last_insert_rowid();
    {
        let mut stmt = conn
            .prepare_cached(
                "INSERT INTO symbols (file_id, name, kind, is_def, start_line, end_line, signature)
                 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
            )
            .map_err(|e| e.to_string())?;
        for s in &parsed.symbols {
            stmt.execute(rusqlite::params![
                file_id,
                s.name,
                s.kind,
                s.is_definition as i64,
                s.start_line as i64,
                s.end_line as i64,
                s.signature,
            ])
            .map_err(|e| e.to_string())?;
        }
    }
    {
        let mut stmt = conn
            .prepare_cached("INSERT INTO imports (file_id, module) VALUES (?1, ?2)")
            .map_err(|e| e.to_string())?;
        for m in &parsed.imports {
            stmt.execute(rusqlite::params![file_id, m])
                .map_err(|e| e.to_string())?;
        }
    }
    {
        let mut stmt = conn
            .prepare_cached("INSERT INTO inherits (file_id, child, parent) VALUES (?1, ?2, ?3)")
            .map_err(|e| e.to_string())?;
        for (child, parent) in &parsed.inherits {
            stmt.execute(rusqlite::params![file_id, child, parent])
                .map_err(|e| e.to_string())?;
        }
    }
    {
        // 全文检索：逐行入库，跳过空行/极短行（trigram 查询下限 3 字符，且控制体积）
        let mut stmt = conn
            .prepare_cached("INSERT INTO fts (content, file, line) VALUES (?1, ?2, ?3)")
            .map_err(|e| e.to_string())?;
        for (i, line) in source.lines().enumerate() {
            if line.trim().len() < 3 {
                continue;
            }
            stmt.execute(rusqlite::params![line, rel, (i + 1) as i64])
                .map_err(|e| e.to_string())?;
        }
    }
    Ok(())
}

/// 删除单个文件的索引（精确匹配 rel_path）
pub fn delete_file(conn: &Connection, rel: &str) -> Result<(), String> {
    conn.execute("DELETE FROM files WHERE rel_path = ?1", [rel])
        .map_err(|e| e.to_string())?;
    conn.execute("DELETE FROM fts WHERE file = ?1", [rel])
        .map_err(|e| e.to_string())?;
    conn.execute("DELETE FROM parse_issues WHERE rel_path = ?1", [rel])
        .map_err(|e| e.to_string())?;
    Ok(())
}

/// 删除某路径及其所有子路径的索引：覆盖删除单个文件，也覆盖删除整个目录
/// （监听器对删目录通常只报目录本身，故需按前缀清理其下文件）。
pub fn delete_path(conn: &Connection, rel: &str) -> Result<(), String> {
    // LIKE 前缀需转义 % _ \，避免路径里的下划线被当通配符
    let esc: String = rel
        .chars()
        .flat_map(|c| {
            if matches!(c, '\\' | '%' | '_') {
                vec!['\\', c]
            } else {
                vec![c]
            }
        })
        .collect();
    let like = format!("{esc}/%");
    conn.execute(
        "DELETE FROM files WHERE rel_path = ?1 OR rel_path LIKE ?2 ESCAPE '\\'",
        rusqlite::params![rel, like],
    )
    .map_err(|e| e.to_string())?;
    conn.execute(
        "DELETE FROM fts WHERE file = ?1 OR file LIKE ?2 ESCAPE '\\'",
        rusqlite::params![rel, like],
    )
    .map_err(|e| e.to_string())?;
    conn.execute(
        "DELETE FROM parse_issues WHERE rel_path = ?1 OR rel_path LIKE ?2 ESCAPE '\\'",
        rusqlite::params![rel, like],
    )
    .map_err(|e| e.to_string())?;
    Ok(())
}

/// 解析 imports.module → resolved_file_id：相对路径/别名 resolve 到项目内实际文件。
/// 全量重算（新文件可能让旧的未解析导入变为可解析）。第三方裸 specifier 留 NULL。
pub fn resolve_imports(conn: &Connection, root: &str) -> Result<(), String> {
    use std::collections::{HashMap, HashSet};
    let mut by_path: HashMap<String, i64> = HashMap::new();
    {
        let mut stmt = conn
            .prepare("SELECT id, rel_path FROM files")
            .map_err(|e| e.to_string())?;
        let rows = stmt
            .query_map([], |r| Ok((r.get::<_, i64>(0)?, r.get::<_, String>(1)?)))
            .map_err(|e| e.to_string())?;
        for r in rows {
            let (id, p) = r.map_err(|e| e.to_string())?;
            by_path.insert(p, id);
        }
    }
    let fileset: HashSet<String> = by_path.keys().cloned().collect();
    let aliases = crate::index::resolve::load_aliases(root);

    let mut items: Vec<(i64, String, String)> = Vec::new();
    {
        let mut stmt = conn
            .prepare(
                "SELECT i.id, f.rel_path, i.module FROM imports i JOIN files f ON f.id = i.file_id",
            )
            .map_err(|e| e.to_string())?;
        let rows = stmt
            .query_map([], |r| {
                Ok((
                    r.get::<_, i64>(0)?,
                    r.get::<_, String>(1)?,
                    r.get::<_, String>(2)?,
                ))
            })
            .map_err(|e| e.to_string())?;
        for r in rows {
            items.push(r.map_err(|e| e.to_string())?);
        }
    }

    let tx = conn.unchecked_transaction().map_err(|e| e.to_string())?;
    {
        let mut up = tx
            .prepare("UPDATE imports SET resolved_file_id = ?1 WHERE id = ?2")
            .map_err(|e| e.to_string())?;
        for (id, from_rel, module) in &items {
            let rid: Option<i64> =
                crate::index::resolve::resolve_import(module, from_rel, &fileset, &aliases)
                    .and_then(|r| by_path.get(&r).copied());
            up.execute(rusqlite::params![rid, id])
                .map_err(|e| e.to_string())?;
        }
    }
    tx.commit().map_err(|e| e.to_string())?;
    Ok(())
}

/// 记录某文件的解析问题（无法解析/过大/读失败等），便于发现索引盲区
pub fn record_parse_issue(
    conn: &Connection,
    rel: &str,
    lang: &str,
    reason: &str,
) -> Result<(), String> {
    conn.execute(
        "INSERT OR REPLACE INTO parse_issues (rel_path, lang, reason) VALUES (?1, ?2, ?3)",
        rusqlite::params![rel, lang, reason],
    )
    .map_err(|e| e.to_string())?;
    Ok(())
}

/// 写入元信息（如 last_scan_ms）
pub fn set_meta(conn: &Connection, key: &str, value: &str) -> Result<(), String> {
    conn.execute(
        "INSERT OR REPLACE INTO meta (key, value) VALUES (?1, ?2)",
        rusqlite::params![key, value],
    )
    .map_err(|e| e.to_string())?;
    Ok(())
}

/// 列出库中所有已记录文件的相对路径（对账 GC 用）
pub fn all_relpaths(conn: &Connection) -> Result<Vec<String>, String> {
    let mut stmt = conn
        .prepare("SELECT rel_path FROM files")
        .map_err(|e| e.to_string())?;
    let rows = stmt
        .query_map([], |r| r.get::<_, String>(0))
        .map_err(|e| e.to_string())?;
    let mut out = Vec::new();
    for r in rows {
        out.push(r.map_err(|e| e.to_string())?);
    }
    Ok(out)
}

/// 统计文件数与符号定义数（状态展示用）
pub fn counts(conn: &Connection) -> (u64, u64) {
    let files: i64 = conn
        .query_row("SELECT COUNT(*) FROM files", [], |r| r.get(0))
        .unwrap_or(0);
    let symbols: i64 = conn
        .query_row("SELECT COUNT(*) FROM symbols WHERE is_def = 1", [], |r| {
            r.get(0)
        })
        .unwrap_or(0);
    (files as u64, symbols as u64)
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::index::parse::{ParsedFile, ParsedSymbol};

    fn sample() -> ParsedFile {
        ParsedFile {
            lang: "rust".into(),
            symbols: vec![ParsedSymbol {
                name: "x".into(),
                kind: "function".into(),
                is_definition: true,
                start_line: 1,
                end_line: 1,
                signature: "fn x()".into(),
            }],
            imports: vec!["std::fs".into()],
            inherits: vec![],
        }
    }

    #[test]
    fn delete_path_removes_subtree_with_escaping() {
        let path =
            std::env::temp_dir().join(format!("ccgui_idx_test_{}.db", std::process::id()));
        let _ = std::fs::remove_file(&path);
        let conn = open_rw(&path).unwrap();
        upsert_file(&conn, "src/a.rs", 1, &sample(), "fn x() {}\nlet y = 1;\n").unwrap();
        upsert_file(&conn, "src/my_mod/b.rs", 1, &sample(), "fn x() {}\nlet y = 1;\n").unwrap();
        // 下划线兄弟目录：未转义 LIKE 会把 _ 当通配符误删，转义后必须存活
        upsert_file(&conn, "src/myXmod/c.rs", 1, &sample(), "fn x() {}\nlet y = 1;\n").unwrap();
        assert_eq!(counts(&conn).0, 3);

        delete_path(&conn, "src/my_mod").unwrap();
        let remaining = all_relpaths(&conn).unwrap();
        assert!(remaining.contains(&"src/a.rs".to_string()));
        assert!(remaining.contains(&"src/myXmod/c.rs".to_string()), "下划线转义失败，误删了兄弟目录");
        assert!(!remaining.contains(&"src/my_mod/b.rs".to_string()));
        assert_eq!(remaining.len(), 2);

        drop(conn);
        let _ = std::fs::remove_file(&path);
        let _ = std::fs::remove_file(path.with_extension("db-wal"));
        let _ = std::fs::remove_file(path.with_extension("db-shm"));
    }
}
