use anyhow::Result;
use std::collections::HashMap;
use std::path::PathBuf;
use weave::mesh::registry::FilterMesh;

// ── Domain tables ─────────────────────────────────────────────────────────────

/// Average compression ratio per program, derived from integration test data.
fn avg_compression_pct(program: &str) -> u64 {
    match program {
        "git" => 84,
        "cargo" => 79,
        "kubectl" => 68,
        "npm" | "pnpm" | "yarn" | "bun" => 71,
        "docker" | "docker-compose" => 65,
        "terraform" | "tofu" => 60,
        "aws" => 63,
        "go" => 72,
        "make" | "cmake" | "bazel" => 58,
        "pytest" | "jest" | "vitest" => 75,
        "ruff" | "mypy" | "eslint" | "clippy" => 77,
        "psql" | "mysql" | "prisma" => 62,
        "helm" | "ansible" => 60,
        _ => 55,
    }
}

/// Average tokens produced per invocation. Conservative estimates.
fn avg_tokens_per_run(program: &str) -> u64 {
    match program {
        "git" => 2_000,
        "cargo" => 6_000,
        "kubectl" => 3_000,
        "npm" | "pnpm" | "yarn" | "bun" => 4_000,
        "docker" | "docker-compose" => 3_500,
        "terraform" | "tofu" => 4_000,
        "aws" => 3_000,
        "go" => 3_500,
        "make" | "cmake" | "bazel" => 5_000,
        "pytest" | "jest" | "vitest" => 5_000,
        "ruff" | "mypy" | "eslint" | "clippy" => 2_500,
        "psql" | "mysql" | "prisma" => 2_000,
        _ => 1_500,
    }
}

/// Compute estimated missed tokens for `count` invocations of `program`.
pub fn missed_tokens_for(program: &str, count: u64) -> u64 {
    count * avg_tokens_per_run(program) * avg_compression_pct(program) / 100
}

// ── History parsing ───────────────────────────────────────────────────────────

/// Shell builtins and bctx itself — never report these as uncompressed.
pub fn is_builtin(program: &str) -> bool {
    matches!(
        program,
        "cd" | "ls"
            | "echo"
            | "export"
            | "source"
            | "."
            | "bctx"
            | "exit"
            | "history"
            | "alias"
            | "unset"
            | "set"
            | "eval"
            | "exec"
            | "pwd"
            | "type"
            | "which"
            | "man"
    )
}

/// Parse one raw history line into `(program, subcommand)`.
///
/// Returns `None` for blank lines, comments, builtins, or entries older than
/// `cutoff` (Unix seconds; only enforced when a timestamp is present).
pub fn parse_history_line(
    line: &str,
    is_zsh_extended: bool,
    cutoff: u64,
) -> Option<(String, String)> {
    let line = line.trim();
    if line.is_empty() || line.starts_with('#') {
        return None;
    }

    let (ts, cmd): (u64, &str) = if is_zsh_extended && line.starts_with(": ") {
        // `: 1715000000:0;git log --oneline -20`
        let semi = line.find(';')?;
        let ts_val: u64 = line[2..semi]
            .split(':')
            .next()
            .unwrap_or("0")
            .trim()
            .parse()
            .unwrap_or(0);
        (ts_val, line[semi + 1..].trim())
    } else if let Some(rest) = line.strip_prefix("- cmd: ") {
        // Fish: `- cmd: git log`
        (0, rest.trim())
    } else {
        // Plain bash history
        (0, line)
    };

    // Drop entries older than the cutoff (only when timestamp is known)
    if ts > 0 && ts < cutoff {
        return None;
    }

    let mut parts = cmd.split_whitespace();
    let program = parts.next()?;
    if is_builtin(program) {
        return None;
    }
    let sub = parts.next().unwrap_or("").to_string();
    Some((program.to_string(), sub))
}

/// Parse all lines from a history file content string.
pub fn parse_history_content(
    content: &str,
    is_zsh_extended: bool,
    cutoff: u64,
) -> Vec<(String, String)> {
    content
        .lines()
        .filter_map(|l| parse_history_line(l, is_zsh_extended, cutoff))
        .collect()
}

// ── FilterMesh classification ─────────────────────────────────────────────────

/// Count how many times each program appeared in `entries` AND is supported
/// by the FilterMesh. Unsupported programs are silently ignored.
pub fn count_supported(entries: &[(String, String)], mesh: &FilterMesh) -> HashMap<String, u64> {
    let mut counts: HashMap<String, u64> = HashMap::new();
    for (program, sub) in entries {
        let args: Vec<String> = if sub.is_empty() {
            vec![]
        } else {
            vec![sub.clone()]
        };
        if mesh.find(program, &args).is_some() {
            *counts.entry(program.clone()).or_insert(0) += 1;
        }
    }
    counts
}

// ── Ranking ───────────────────────────────────────────────────────────────────

/// Build a ranked list `(program, run_count, missed_tokens)` sorted by
/// missed_tokens descending. Capped at top 10.
pub fn rank_missed(counts: HashMap<String, u64>) -> Vec<(String, u64, u64)> {
    let mut ranked: Vec<(String, u64, u64)> = counts
        .into_iter()
        .map(|(prog, count)| {
            let missed = missed_tokens_for(&prog, count);
            (prog, count, missed)
        })
        .collect();
    ranked.sort_by_key(|b| std::cmp::Reverse(b.2));
    ranked.truncate(10);
    ranked
}

// ── I/O ───────────────────────────────────────────────────────────────────────

fn read_shell_history(days: u32) -> Vec<(String, String)> {
    let home = super::home_dir();
    let cutoff = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map(|d| d.as_secs())
        .unwrap_or(0)
        .saturating_sub(days as u64 * 86_400);

    let sources: Vec<(PathBuf, bool)> = vec![
        (PathBuf::from(&home).join(".zsh_history"), true),
        (PathBuf::from(&home).join(".bash_history"), false),
        (
            PathBuf::from(&home).join(".local/share/fish/fish_history"),
            false,
        ),
    ];

    let mut entries = Vec::new();
    for (path, is_zsh_extended) in sources {
        let Ok(content) = std::fs::read_to_string(&path) else {
            continue;
        };
        entries.extend(parse_history_content(&content, is_zsh_extended, cutoff));
    }
    entries
}

pub fn handle(days: u32) -> Result<()> {
    let mesh = FilterMesh::new();
    let history = read_shell_history(days);

    if history.is_empty() {
        println!();
        println!("  No shell history found.");
        println!("  Checked: ~/.zsh_history  ~/.bash_history  ~/.local/share/fish/fish_history");
        println!();
        return Ok(());
    }

    let counts = count_supported(&history, &mesh);

    if counts.is_empty() {
        println!();
        println!("  No uncompressed commands found in the last {days} days.");
        let home = super::home_dir();
        let hook_active = [
            format!("{home}/.zshrc"),
            format!("{home}/.bashrc"),
            format!("{home}/.bash_profile"),
        ]
        .iter()
        .any(|rc| {
            std::fs::read_to_string(rc)
                .map(|c| c.contains("# bctx"))
                .unwrap_or(false)
        });
        if hook_active {
            println!("  Shell hook is active — all recent commands ran through bctx.");
        } else {
            println!("  Run `bctx init` to wire the shell hook and start saving tokens.");
        }
        println!();
        return Ok(());
    }

    let ranked = rank_missed(counts);
    let total_missed: u64 = ranked.iter().map(|(_, _, t)| t).sum();
    let cost = total_missed as f64 * 0.000_015;
    let max_missed = ranked.iter().map(|(_, _, t)| *t).max().unwrap_or(1);

    let sep = "  ─────────────────────────────────────────────────────";
    println!();
    println!("  UNCOMPRESSED COMMANDS  (last {days} days)");
    println!("{sep}");
    for (prog, count, missed_tokens) in &ranked {
        let bar = savings_bar(*missed_tokens, max_missed, 14);
        println!(
            "  {:<12}  {}  ran {:>4}×  ·  est. missed  ~{}",
            prog,
            bar,
            count,
            fmt_tokens(*missed_tokens),
        );
    }
    println!("{sep}");
    println!(
        "  Total estimated missed savings: ~{} tokens (~${:.2})",
        fmt_tokens(total_missed),
        cost,
    );
    println!();
    println!("  These commands ran without bctx interception.");
    println!("  Run `bctx init` to wire the shell hook and capture them automatically.");
    println!();
    Ok(())
}

// ── Formatting helpers ────────────────────────────────────────────────────────

fn fmt_tokens(n: u64) -> String {
    if n >= 1_000_000 {
        format!("{:.1}M", n as f64 / 1_000_000.0)
    } else if n >= 1_000 {
        format!("{:.0}K", n as f64 / 1_000.0)
    } else {
        n.to_string()
    }
}

fn savings_bar(value: u64, max: u64, width: usize) -> String {
    use std::io::IsTerminal;
    let filled = if max == 0 {
        0
    } else {
        ((value as usize) * width / (max as usize)).min(width)
    };
    let empty = width - filled;
    let blocks = "█".repeat(filled);
    let shade = "░".repeat(empty);
    if std::io::stdout().is_terminal() {
        format!("\x1b[33m{blocks}\x1b[2m{shade}\x1b[0m")
    } else {
        format!("{blocks}{shade}")
    }
}

// ── Tests ─────────────────────────────────────────────────────────────────────

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

    // ── fmt_tokens ────────────────────────────────────────────────────────────

    #[test]
    fn fmt_tokens_millions() {
        assert_eq!(fmt_tokens(2_400_000), "2.4M");
    }

    #[test]
    fn fmt_tokens_thousands() {
        assert_eq!(fmt_tokens(94_000), "94K");
    }

    #[test]
    fn fmt_tokens_small() {
        assert_eq!(fmt_tokens(0), "0");
        assert_eq!(fmt_tokens(42), "42");
        assert_eq!(fmt_tokens(999), "999");
    }

    // ── savings_bar ───────────────────────────────────────────────────────────

    #[test]
    fn savings_bar_full() {
        let b = savings_bar(16, 16, 8);
        assert!(b.contains("████████"));
    }

    #[test]
    fn savings_bar_zero_max_no_panic() {
        let b = savings_bar(0, 0, 8);
        assert!(b.contains("░"));
    }

    #[test]
    fn savings_bar_half() {
        let b = savings_bar(4, 8, 8);
        assert!(b.contains("████"));
        assert!(b.contains("░░░░"));
    }

    // ── avg_compression_pct ───────────────────────────────────────────────────

    #[test]
    fn avg_compression_pct_known() {
        assert_eq!(avg_compression_pct("git"), 84);
        assert_eq!(avg_compression_pct("cargo"), 79);
        assert_eq!(avg_compression_pct("kubectl"), 68);
    }

    #[test]
    fn avg_compression_pct_fallback() {
        assert_eq!(avg_compression_pct("some_unknown_tool"), 55);
    }

    // ── is_builtin ────────────────────────────────────────────────────────────

    #[test]
    fn builtins_are_filtered() {
        for b in &["cd", "ls", "echo", "export", "source", "exit", "bctx"] {
            assert!(is_builtin(b), "{b} should be a builtin");
        }
    }

    #[test]
    fn supported_programs_are_not_builtins() {
        for p in &["git", "cargo", "kubectl", "npm", "docker", "terraform"] {
            assert!(!is_builtin(p), "{p} should not be a builtin");
        }
    }

    // ── parse_history_line ────────────────────────────────────────────────────

    #[test]
    fn parse_plain_bash_line() {
        let result = parse_history_line("git log --oneline -20", false, 0);
        assert_eq!(result, Some(("git".into(), "log".into())));
    }

    #[test]
    fn parse_plain_bash_no_subcommand() {
        let result = parse_history_line("docker", false, 0);
        assert_eq!(result, Some(("docker".into(), "".into())));
    }

    #[test]
    fn parse_zsh_extended_line() {
        let result = parse_history_line(": 1715000000:0;cargo build --release", true, 0);
        assert_eq!(result, Some(("cargo".into(), "build".into())));
    }

    #[test]
    fn parse_zsh_extended_old_entry_filtered_by_cutoff() {
        // timestamp 1000 < cutoff 9999999999 → should be dropped
        let result = parse_history_line(": 1000:0;git status", true, 9_999_999_999);
        assert_eq!(result, None);
    }

    #[test]
    fn parse_zsh_extended_recent_entry_kept() {
        // timestamp far in the future relative to cutoff 0
        let result = parse_history_line(": 9999999999:0;git status", true, 0);
        assert_eq!(result, Some(("git".into(), "status".into())));
    }

    #[test]
    fn parse_fish_line() {
        let result = parse_history_line("- cmd: kubectl get pods", false, 0);
        assert_eq!(result, Some(("kubectl".into(), "get".into())));
    }

    #[test]
    fn parse_empty_line_returns_none() {
        assert_eq!(parse_history_line("", false, 0), None);
        assert_eq!(parse_history_line("   ", false, 0), None);
    }

    #[test]
    fn parse_comment_line_returns_none() {
        assert_eq!(parse_history_line("# this is a comment", false, 0), None);
    }

    #[test]
    fn parse_builtin_returns_none() {
        assert_eq!(parse_history_line("cd /home/user", false, 0), None);
        assert_eq!(parse_history_line("bctx git log", false, 0), None);
        assert_eq!(parse_history_line("echo hello", false, 0), None);
    }

    // ── parse_history_content ─────────────────────────────────────────────────

    #[test]
    fn parse_history_content_bash_multiline() {
        let content = "git log\ncargo test\ncd /tmp\ngit status\n";
        let result = parse_history_content(content, false, 0);
        assert_eq!(result.len(), 3); // cd filtered
        assert!(result.iter().any(|(p, _)| p == "git"));
        assert!(result.iter().any(|(p, _)| p == "cargo"));
        assert!(!result.iter().any(|(p, _)| p == "cd"));
    }

    #[test]
    fn parse_history_content_zsh_filters_old_entries() {
        let content = ": 100:0;git log\n: 9999999999:0;cargo build\n";
        let result = parse_history_content(content, true, 1_000_000);
        assert_eq!(result.len(), 1);
        assert_eq!(result[0].0, "cargo");
    }

    #[test]
    fn parse_history_content_skips_blank_and_comments() {
        let content = "\n# comment\ngit status\n\n";
        let result = parse_history_content(content, false, 0);
        assert_eq!(result.len(), 1);
        assert_eq!(result[0].0, "git");
    }

    // ── count_supported ───────────────────────────────────────────────────────

    #[test]
    fn count_supported_ignores_unknown_programs() {
        let mesh = FilterMesh::default_mesh();
        let entries = vec![
            ("my_obscure_tool_xyz".into(), "".into()),
            ("another_unknown_999".into(), "run".into()),
        ];
        let counts = count_supported(&entries, &mesh);
        assert!(counts.is_empty(), "unknown tools must not appear in counts");
    }

    #[test]
    fn count_supported_counts_git_occurrences() {
        let mesh = FilterMesh::default_mesh();
        let entries = vec![
            ("git".into(), "log".into()),
            ("git".into(), "status".into()),
            ("git".into(), "diff".into()),
        ];
        let counts = count_supported(&entries, &mesh);
        assert_eq!(counts.get("git"), Some(&3));
    }

    #[test]
    fn count_supported_counts_multiple_programs() {
        let mesh = FilterMesh::default_mesh();
        let entries = vec![
            ("git".into(), "log".into()),
            ("cargo".into(), "build".into()),
            ("git".into(), "status".into()),
        ];
        let counts = count_supported(&entries, &mesh);
        assert_eq!(counts.get("git"), Some(&2));
        assert_eq!(counts.get("cargo"), Some(&1));
    }

    #[test]
    fn count_supported_mixed_known_and_unknown() {
        let mesh = FilterMesh::default_mesh();
        let entries = vec![
            ("git".into(), "log".into()),
            ("totally_fake_tool_xyz".into(), "run".into()),
            ("cargo".into(), "test".into()),
        ];
        let counts = count_supported(&entries, &mesh);
        assert_eq!(counts.len(), 2);
        assert!(!counts.contains_key("totally_fake_tool_xyz"));
    }

    // ── rank_missed ───────────────────────────────────────────────────────────

    #[test]
    fn rank_missed_sorted_by_tokens_descending() {
        let mut counts = HashMap::new();
        counts.insert("git".into(), 1u64); // 2000 × 84% = 1680
        counts.insert("cargo".into(), 1u64); // 6000 × 79% = 4740
        let ranked = rank_missed(counts);
        assert_eq!(ranked[0].0, "cargo");
        assert_eq!(ranked[1].0, "git");
    }

    #[test]
    fn rank_missed_capped_at_ten() {
        let counts: HashMap<String, u64> = (0..15).map(|i| (format!("tool_{i}"), 1u64)).collect();
        let ranked = rank_missed(counts);
        assert!(ranked.len() <= 10);
    }

    #[test]
    fn rank_missed_token_estimate_correct() {
        let mut counts = HashMap::new();
        counts.insert("git".into(), 2u64); // 2 × 2000 × 84% = 3360
        let ranked = rank_missed(counts);
        assert_eq!(ranked[0].2, 3_360);
    }

    // ── missed_tokens_for ─────────────────────────────────────────────────────

    #[test]
    fn missed_tokens_for_git_single_run() {
        // 1 × 2000 tokens × 84% = 1680
        assert_eq!(missed_tokens_for("git", 1), 1_680);
    }

    #[test]
    fn missed_tokens_for_cargo_multiple_runs() {
        // 3 × 6000 × 79% = 14220
        assert_eq!(missed_tokens_for("cargo", 3), 14_220);
    }

    #[test]
    fn missed_tokens_for_zero_runs() {
        assert_eq!(missed_tokens_for("git", 0), 0);
    }
}
