use crate::llm::AppState;
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
use tauri::Manager;
use tauri_plugin_store::StoreExt;

// ponytail: the data-dir pref store + key are defined in lib.rs; mirror them
// here so this module is self-contained for reads/writes.
const PREFS_STORE: &str = "dm-pal-prefs.json";
const DATA_DIR_KEY: &str = "dataDir";

#[derive(Debug, Serialize)]
pub struct DataDirInfo {
    /// Absolute path currently in use (may differ from `configured` if it was
    /// unset/fell back to the default).
    pub current: String,
    /// Absolute path that will be used after the next restart, if different.
    pub configured: Option<String>,
    /// The OS-default location ($APPDATA/dm-toolkit).
    pub default: String,
}

/// Return the current data dir + the configured (pending-restart) value, so
/// Settings can show "using X — restart to apply Y".
#[tauri::command]
pub fn get_data_dir(
    app: tauri::AppHandle,
    state: tauri::State<'_, AppState>,
) -> Result<DataDirInfo, String> {
    let default = app
        .path()
        .app_data_dir()
        .map_err(|e| e.to_string())?
        .join("dm-toolkit");
    let configured = app
        .store(PREFS_STORE)
        .map_err(|e| e.to_string())?
        .get(DATA_DIR_KEY)
        .and_then(|v| v.as_str().map(|s| s.to_string()));
    Ok(DataDirInfo {
        current: state.data_dir.display().to_string(),
        configured,
        default: default.display().to_string(),
    })
}

/// Subdirs/files that make up the campaign data and must move together.
const DATA_RELATIVE: &[&str] = &["lore/lore.db", "generations.db", "images"];

/// Recursively copy `src` to `dst`, but never overwrite an existing file —
/// a re-locate must not clobber data already at the destination, and never
/// deletes the source (the old location stays as a backup until the DM deletes it).
/// When both src and dst are directories, recurse so new files inside an
/// existing destination tree still get copied.
fn copy_if_absent(src: &Path, dst: &Path) -> std::io::Result<()> {
    if !src.exists() {
        return Ok(());
    }
    if src.is_dir() {
        std::fs::create_dir_all(dst)?;
        for entry in std::fs::read_dir(src)? {
            let entry = entry?;
            copy_if_absent(&entry.path(), &dst.join(entry.file_name()))?;
        }
        return Ok(());
    }
    // leaf file: skip if the destination already has it (no overwrite).
    if dst.exists() {
        return Ok(());
    }
    if let Some(parent) = dst.parent() {
        std::fs::create_dir_all(parent)?;
    }
    std::fs::copy(src, dst)?;
    Ok(())
}

/// Set the data dir to `new_dir`: create it, copy existing campaign data
/// (lore.db, generations.db, images/) into it, and persist the preference so
/// the next restart uses it. The currently-open SQLite connections stay on the
/// old paths until restart — the caller must prompt the user to relaunch.
#[tauri::command]
pub fn set_data_dir(
    app: tauri::AppHandle,
    state: tauri::State<'_, AppState>,
    new_dir: String,
) -> Result<DataDirInfo, String> {
    let new_path = PathBuf::from(new_dir.trim());
    if !new_path.is_absolute() {
        return Err("Choose an absolute folder path".into());
    }
    std::fs::create_dir_all(&new_path).map_err(|e| format!("can't create dir: {e}"))?;

    // Copy existing data into the new location (no overwrite; old data stays put).
    for rel in DATA_RELATIVE {
        let src = state.data_dir.join(rel);
        let dst = new_path.join(rel);
        if src.exists() {
            copy_if_absent(&src, &dst).map_err(|e| format!("migrate {rel}: {e}"))?;
        }
    }

    // Persist the preference for the next startup.
    let store = app.store(PREFS_STORE).map_err(|e| e.to_string())?;
    store.set(DATA_DIR_KEY, serde_json::Value::String(new_path.display().to_string()));
    store.save().map_err(|e| e.to_string())?;

    Ok(DataDirInfo {
        current: state.data_dir.display().to_string(),
        configured: Some(new_path.display().to_string()),
        default: app
            .path()
            .app_data_dir()
            .map_err(|e| e.to_string())?
            .join("dm-toolkit")
            .display()
            .to_string(),
    })
}

/// Clear the preference so the next restart uses the OS default. Does not
/// delete any files — the DM re-points, then restarts.
#[tauri::command]
pub fn reset_data_dir(app: tauri::AppHandle) -> Result<(), String> {
    let store = app.store(PREFS_STORE).map_err(|e| e.to_string())?;
    store.delete(DATA_DIR_KEY);
    store.save().map_err(|e| e.to_string())?;
    Ok(())
}

// ─── SQLite viewer (read-only) ─────────────────────────────────

#[derive(Debug, Deserialize)]
pub struct SqlQueryReq {
    /// "lore" or "generations".
    pub db: String,
    pub sql: String,
}

#[derive(Debug, Serialize)]
pub struct SqlQueryResult {
    pub columns: Vec<String>,
    pub rows: Vec<Vec<serde_json::Value>>,
}

/// Run a read-only SQL query against one of the two SQLite DBs so a DM can
/// inspect the backend from Settings → Advanced. Only SELECT/PRAGMA/WITH.
#[tauri::command]
pub fn sql_query(state: tauri::State<'_, AppState>, req: SqlQueryReq) -> Result<SqlQueryResult, String> {
    let res = match req.db.as_str() {
        "lore" => state.rag.raw_query(&req.sql),
        "generations" => state.gen.raw_query(&req.sql),
        other => Err(anyhow::anyhow!("unknown db: {other}")),
    }
    .map_err(|e| e.to_string())?;
    Ok(SqlQueryResult { columns: res.0, rows: res.1 })
}

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

    #[test]
    fn copy_if_absent_does_not_overwrite() {
        let tmp = std::env::temp_dir().join(format!("dm-pal-data-test-{}", std::process::id()));
        let _ = std::fs::remove_dir_all(&tmp);
        let src = tmp.join("src");
        let dst = tmp.join("dst");
        std::fs::create_dir_all(src.join("images")).unwrap();
        std::fs::write(src.join("images/a.png"), b"png").unwrap();
        std::fs::create_dir_all(src.join("lore")).unwrap();
        std::fs::write(src.join("lore/lore.db"), b"db").unwrap();

        // first copy creates dst with the files
        copy_if_absent(&src, &dst).unwrap();
        assert!(dst.join("images/a.png").exists());
        assert!(dst.join("lore/lore.db").exists());

        // mutate src; second copy must NOT overwrite existing dst files
        std::fs::write(src.join("images/a.png"), b"changed").unwrap();
        copy_if_absent(&src, &dst).unwrap();
        assert_eq!(std::fs::read(dst.join("images/a.png")).unwrap(), b"png");

        // a new file in src does get copied (absent in dst)
        std::fs::write(src.join("images/b.png"), b"png2").unwrap();
        copy_if_absent(&src, &dst).unwrap();
        assert!(dst.join("images/b.png").exists());

        let _ = std::fs::remove_dir_all(&tmp);
    }

    #[test]
    fn copy_if_absent_missing_src_is_noop() {
        let tmp = std::env::temp_dir().join(format!("dm-pal-data-test2-{}", std::process::id()));
        let _ = std::fs::remove_dir_all(&tmp);
        copy_if_absent(&tmp.join("nope"), &tmp.join("dst")).unwrap();
        assert!(!tmp.join("dst").exists());
        let _ = std::fs::remove_dir_all(&tmp);
    }
}