use crate::core::config;
use crate::core::theme;

pub fn cmd_config(args: &[String]) {
    let cfg = config::Config::load();

    if args.is_empty() {
        println!("{}", cfg.show());
        return;
    }

    match args[0].as_str() {
        "init" | "create" => {
            let default = config::Config::default();
            match default.save() {
                Ok(()) => {
                    let path = config::Config::path().map_or_else(
                        || "~/.lean-ctx/config.toml".to_string(),
                        |p| p.to_string_lossy().to_string(),
                    );
                    println!("Created default config at {path}");
                }
                Err(e) => eprintln!("Error: {e}"),
            }
        }
        "set" => {
            if args.len() < 3 {
                eprintln!("Usage: lean-ctx config set <key> <value>");
                std::process::exit(1);
            }
            let mut cfg = cfg;
            let key = &args[1];
            let val = &args[2];
            match key.as_str() {
                "ultra_compact" => cfg.ultra_compact = val == "true",
                "tee_on_error" | "tee_mode" => {
                    cfg.tee_mode = match val.as_str() {
                        "true" | "failures" => config::TeeMode::Failures,
                        "always" => config::TeeMode::Always,
                        "false" | "never" => config::TeeMode::Never,
                        _ => {
                            eprintln!("Valid tee_mode values: always, failures, never");
                            std::process::exit(1);
                        }
                    };
                }
                "checkpoint_interval" => {
                    cfg.checkpoint_interval = val.parse().unwrap_or(15);
                }
                "theme" => {
                    if theme::from_preset(val).is_some() || val == "custom" {
                        cfg.theme.clone_from(val);
                    } else {
                        eprintln!(
                            "Unknown theme '{val}'. Available: {}",
                            theme::PRESET_NAMES.join(", ")
                        );
                        std::process::exit(1);
                    }
                }
                "slow_command_threshold_ms" => {
                    cfg.slow_command_threshold_ms = val.parse().unwrap_or(5000);
                }
                "passthrough_urls" => {
                    cfg.passthrough_urls = val.split(',').map(|s| s.trim().to_string()).collect();
                }
                "excluded_commands" => {
                    cfg.excluded_commands = val
                        .split(',')
                        .map(|s| s.trim().to_string())
                        .filter(|s| !s.is_empty())
                        .collect();
                }
                "rules_scope" => match val.as_str() {
                    "global" | "project" | "both" => {
                        cfg.rules_scope = Some(val.clone());
                    }
                    _ => {
                        eprintln!("Valid rules_scope values: global, project, both");
                        std::process::exit(1);
                    }
                },
                "proxy.anthropic_upstream" => {
                    cfg.proxy.anthropic_upstream = normalize_optional_upstream(val);
                }
                "proxy.openai_upstream" => {
                    cfg.proxy.openai_upstream = normalize_optional_upstream(val);
                }
                "proxy.gemini_upstream" => {
                    cfg.proxy.gemini_upstream = normalize_optional_upstream(val);
                }
                _ => {
                    eprintln!("Unknown config key: {key}");
                    std::process::exit(1);
                }
            }
            match cfg.save() {
                Ok(()) => println!("Updated {key} = {val}"),
                Err(e) => eprintln!("Error saving config: {e}"),
            }
        }
        "schema" => {
            let schema = config::schema::ConfigSchema::generate();
            println!(
                "{}",
                serde_json::to_string_pretty(&schema).unwrap_or_else(|_| "{}".to_string())
            );
        }
        "validate" => {
            cmd_validate();
        }
        _ => {
            eprintln!("Usage: lean-ctx config [init|set|schema|validate]");
            std::process::exit(1);
        }
    }
}

fn cmd_validate() {
    let schema = config::schema::ConfigSchema::generate();
    let known = schema.known_keys();

    let path = match config::Config::path() {
        Some(p) if p.exists() => p,
        _ => {
            println!("[OK] No config.toml found — using defaults.");
            return;
        }
    };

    let raw = match std::fs::read_to_string(&path) {
        Ok(s) => s,
        Err(e) => {
            eprintln!("[ERROR] Cannot read {}: {e}", path.display());
            std::process::exit(1);
        }
    };

    let table: toml::Table = match raw.parse() {
        Ok(t) => t,
        Err(e) => {
            eprintln!("[ERROR] Invalid TOML: {e}");
            std::process::exit(1);
        }
    };

    let mut warnings = 0u32;
    let mut validated = 0u32;

    fn collect_keys(table: &toml::Table, prefix: &str, out: &mut Vec<String>) {
        for (k, v) in table {
            let full = if prefix.is_empty() {
                k.clone()
            } else {
                format!("{prefix}.{k}")
            };
            match v {
                toml::Value::Table(sub) => collect_keys(sub, &full, out),
                toml::Value::Array(arr) => {
                    out.push(full.clone());
                    for item in arr {
                        if let toml::Value::Table(sub) = item {
                            for sk in sub.keys() {
                                out.push(format!("{full}[].{sk}"));
                            }
                        }
                    }
                }
                _ => out.push(full),
            }
        }
    }

    let mut user_keys = Vec::new();
    collect_keys(&table, "", &mut user_keys);

    for uk in &user_keys {
        let base = uk.split("[].").next().unwrap_or(uk);
        let field = uk.rsplit("[].").next().unwrap_or("");
        let check_key = if uk.contains("[].") {
            format!("{base}.{field}")
        } else {
            uk.clone()
        };

        if known.contains(&check_key)
            || known
                .iter()
                .any(|k| check_key.starts_with(&format!("{k}.")))
        {
            validated += 1;
        } else {
            warnings += 1;
            let suggestion = find_closest(&check_key, &known);
            if let Some(sug) = suggestion {
                eprintln!("[WARN] Unknown key '{uk}' -- did you mean '{sug}'?");
            } else {
                eprintln!("[WARN] Unknown key '{uk}' -- this field does not exist");
            }
        }
    }

    let total = validated + warnings;
    if warnings == 0 {
        println!(
            "[OK] All {total} keys validated successfully ({}).",
            path.display()
        );
    } else {
        println!(
            "[RESULT] {validated} of {total} keys validated, {warnings} unknown ({}).",
            path.display()
        );
        std::process::exit(1);
    }
}

fn find_closest(needle: &str, haystack: &[String]) -> Option<String> {
    let mut best: Option<(usize, &str)> = None;
    for candidate in haystack {
        let d = levenshtein(needle, candidate);
        if d <= 3 && (best.is_none() || d < best.unwrap().0) {
            best = Some((d, candidate));
        }
    }
    if best.is_some() {
        return best.map(|(_, s)| s.to_string());
    }
    let leaf = needle.rsplit('.').next().unwrap_or(needle);
    let mut leaf_best: Option<(usize, &str)> = None;
    for candidate in haystack {
        let cand_leaf = candidate.rsplit('.').next().unwrap_or(candidate);
        let d = levenshtein(leaf, cand_leaf);
        if d <= 2 && (leaf_best.is_none() || d < leaf_best.unwrap().0) {
            leaf_best = Some((d, candidate));
        }
    }
    leaf_best.map(|(_, s)| s.to_string())
}

fn levenshtein(a: &str, b: &str) -> usize {
    let a: Vec<char> = a.chars().collect();
    let b: Vec<char> = b.chars().collect();
    let (m, n) = (a.len(), b.len());
    let mut dp = vec![vec![0usize; n + 1]; m + 1];
    for (i, row) in dp.iter_mut().enumerate().take(m + 1) {
        row[0] = i;
    }
    for (j, val) in dp[0].iter_mut().enumerate().take(n + 1) {
        *val = j;
    }
    for i in 1..=m {
        for j in 1..=n {
            let cost = usize::from(a[i - 1] != b[j - 1]);
            dp[i][j] = (dp[i - 1][j] + 1)
                .min(dp[i][j - 1] + 1)
                .min(dp[i - 1][j - 1] + cost);
        }
    }
    dp[m][n]
}

fn normalize_optional_upstream(value: &str) -> Option<String> {
    use crate::core::config::normalize_url_opt;
    let trimmed = value.trim();
    if trimmed.is_empty() || trimmed.eq_ignore_ascii_case("default") {
        None
    } else {
        normalize_url_opt(trimmed)
    }
}

pub fn cmd_benchmark(args: &[String]) {
    use crate::core::benchmark;

    let action = args.first().map_or("run", std::string::String::as_str);

    match action {
        "--help" | "-h" => {
            println!("Usage: lean-ctx benchmark run [path] [--json]");
            println!("       lean-ctx benchmark report [path]");
        }
        "run" => {
            let path = args.get(1).map_or(".", std::string::String::as_str);
            let is_json = args.iter().any(|a| a == "--json");

            let result = benchmark::run_project_benchmark(path);
            if is_json {
                println!("{}", benchmark::format_json(&result));
            } else {
                println!("{}", benchmark::format_terminal(&result));
            }
        }
        "report" => {
            let path = args.get(1).map_or(".", std::string::String::as_str);
            let result = benchmark::run_project_benchmark(path);
            println!("{}", benchmark::format_markdown(&result));
        }
        _ => {
            if std::path::Path::new(action).exists() {
                let result = benchmark::run_project_benchmark(action);
                println!("{}", benchmark::format_terminal(&result));
            } else {
                eprintln!("Usage: lean-ctx benchmark run [path] [--json]");
                eprintln!("       lean-ctx benchmark report [path]");
                std::process::exit(1);
            }
        }
    }
}

pub fn cmd_stats(args: &[String]) {
    match args.first().map(std::string::String::as_str) {
        Some("reset-cep") => {
            crate::core::stats::reset_cep();
            println!("CEP stats reset. Shell hook data preserved.");
        }
        Some("json") => {
            let store = crate::core::stats::load();
            println!(
                "{}",
                serde_json::to_string_pretty(&store).unwrap_or_else(|_| "{}".to_string())
            );
        }
        _ => {
            let store = crate::core::stats::load();
            let input_saved = store
                .total_input_tokens
                .saturating_sub(store.total_output_tokens);
            let pct = if store.total_input_tokens > 0 {
                input_saved as f64 / store.total_input_tokens as f64 * 100.0
            } else {
                0.0
            };
            println!("Commands:    {}", store.total_commands);
            println!("Input:       {} tokens", store.total_input_tokens);
            println!("Output:      {} tokens", store.total_output_tokens);
            println!("Saved:       {input_saved} tokens ({pct:.1}%)");
            println!();
            println!("CEP sessions:  {}", store.cep.sessions);
            println!(
                "CEP tokens:    {} → {}",
                store.cep.total_tokens_original, store.cep.total_tokens_compressed
            );
            println!();
            println!("Subcommands: stats reset-cep | stats json");
        }
    }
}

pub fn cmd_cache(args: &[String]) {
    use crate::core::cli_cache;
    match args.first().map(std::string::String::as_str) {
        Some("clear") => {
            let count = cli_cache::clear();
            println!("Cleared {count} cached entries.");
        }
        Some("reset") => {
            let project_flag = args.get(1).map(std::string::String::as_str) == Some("--project");
            if project_flag {
                let root =
                    crate::core::session::SessionState::load_latest().and_then(|s| s.project_root);
                if let Some(root) = root {
                    let count = cli_cache::clear_project(&root);
                    println!("Reset {count} cache entries for project: {root}");
                } else {
                    eprintln!("No active project root found. Start a session first.");
                    std::process::exit(1);
                }
            } else {
                let count = cli_cache::clear();
                println!("Reset all {count} cache entries.");
            }
        }
        Some("stats") => {
            let (hits, reads, entries) = cli_cache::stats();
            let rate = if reads > 0 {
                (hits as f64 / reads as f64 * 100.0).round() as u32
            } else {
                0
            };
            println!("CLI Cache Stats:");
            println!("  Entries:   {entries}");
            println!("  Reads:     {reads}");
            println!("  Hits:      {hits}");
            println!("  Hit Rate:  {rate}%");
        }
        Some("invalidate") => {
            if args.len() < 2 {
                eprintln!("Usage: lean-ctx cache invalidate <path>");
                std::process::exit(1);
            }
            cli_cache::invalidate(&args[1]);
            println!("Invalidated cache for {}", args[1]);
        }
        Some("prune") => {
            let result = prune_bm25_caches();
            println!(
                "Pruned {} entries, freed {:.1} MB",
                result.removed,
                result.bytes_freed as f64 / 1_048_576.0
            );
        }
        _ => {
            let (hits, reads, entries) = cli_cache::stats();
            let rate = if reads > 0 {
                (hits as f64 / reads as f64 * 100.0).round() as u32
            } else {
                0
            };
            println!("CLI File Cache: {entries} entries, {hits}/{reads} hits ({rate}%)");
            println!();
            println!("Subcommands:");
            println!("  cache stats       Show detailed stats");
            println!("  cache clear       Clear all cached entries");
            println!("  cache reset       Reset all cache (or --project for current project only)");
            println!("  cache invalidate  Remove specific file from cache");
            println!(
                "  cache prune       Remove oversized, quarantined, and orphaned BM25 indexes"
            );
        }
    }
}

pub struct PruneResult {
    pub scanned: u32,
    pub removed: u32,
    pub bytes_freed: u64,
}

pub fn prune_bm25_caches() -> PruneResult {
    let mut result = PruneResult {
        scanned: 0,
        removed: 0,
        bytes_freed: 0,
    };

    let Ok(data_dir) = crate::core::data_dir::lean_ctx_data_dir() else {
        return result;
    };
    let vectors_dir = data_dir.join("vectors");
    let Ok(entries) = std::fs::read_dir(&vectors_dir) else {
        return result;
    };

    let max_bytes = crate::core::config::Config::load().bm25_max_cache_mb * 1024 * 1024;

    for entry in entries.flatten() {
        let dir = entry.path();
        if !dir.is_dir() {
            continue;
        }
        result.scanned += 1;

        for q_name in &[
            "bm25_index.json.quarantined",
            "bm25_index.bin.quarantined",
            "bm25_index.bin.zst.quarantined",
        ] {
            let quarantined = dir.join(q_name);
            if quarantined.exists() {
                if let Ok(meta) = std::fs::metadata(&quarantined) {
                    result.bytes_freed += meta.len();
                }
                let _ = std::fs::remove_file(&quarantined);
                result.removed += 1;
                println!("  Removed quarantined: {}", quarantined.display());
            }
        }

        let index_path = if dir.join("bm25_index.bin.zst").exists() {
            dir.join("bm25_index.bin.zst")
        } else if dir.join("bm25_index.bin").exists() {
            dir.join("bm25_index.bin")
        } else {
            dir.join("bm25_index.json")
        };
        if let Ok(meta) = std::fs::metadata(&index_path) {
            if meta.len() > max_bytes {
                result.bytes_freed += meta.len();
                let _ = std::fs::remove_file(&index_path);
                result.removed += 1;
                println!(
                    "  Removed oversized ({:.1} MB): {}",
                    meta.len() as f64 / 1_048_576.0,
                    index_path.display()
                );
            }
        }

        let marker = dir.join("project_root.txt");
        if let Ok(root_str) = std::fs::read_to_string(&marker) {
            let root_path = std::path::Path::new(root_str.trim());
            if !root_path.exists() {
                let freed = dir_size(&dir);
                result.bytes_freed += freed;
                let _ = std::fs::remove_dir_all(&dir);
                result.removed += 1;
                println!(
                    "  Removed orphaned ({:.1} MB, project gone: {}): {}",
                    freed as f64 / 1_048_576.0,
                    root_str.trim(),
                    dir.display()
                );
            }
        }
    }

    result
}

fn dir_size(path: &std::path::Path) -> u64 {
    let mut total = 0u64;
    if let Ok(entries) = std::fs::read_dir(path) {
        for entry in entries.flatten() {
            let p = entry.path();
            if p.is_file() {
                total += std::fs::metadata(&p).map_or(0, |m| m.len());
            } else if p.is_dir() {
                total += dir_size(&p);
            }
        }
    }
    total
}
