//! `gate retro` — protection retrospective.
//!
//! Reads the JSONL stats log and prints three numbers plus a top-categories
//! breakdown. Lines that fail to parse are skipped with a warning so a
//! crash-truncated last line doesn't kill the whole report.
//!
//! Synonym keywords: `stats`, `audit`, `report` (referenced in the clap
//! help text so people grepping `--help` find this command).

use common::config::Config;
use common::patterns::map_to_tier1_category;
use common::stats::{stats_path, Event};
use std::collections::HashMap;
use std::io::{BufRead, BufReader};

pub fn run() {
    let config = Config::load().unwrap_or_default();
    if !config.stats.enabled {
        println!(
            "Stats collection is disabled in config. Set `stats.enabled: true` \
             in your gate config to enable it."
        );
        return;
    }

    let path = match stats_path() {
        Ok(p) => p,
        Err(e) => {
            eprintln!("[gate retro] cannot resolve stats path: {e}");
            return;
        }
    };

    if !path.exists() {
        print_empty();
        return;
    }

    let file = match std::fs::File::open(&path) {
        Ok(f) => f,
        Err(e) => {
            eprintln!("[gate retro] cannot read {}: {e}", path.display());
            return;
        }
    };

    let mut summary = Summary::default();
    for line in BufReader::new(file).lines().map_while(Result::ok) {
        let trimmed = line.trim();
        if trimmed.is_empty() {
            continue;
        }
        match serde_json::from_str::<Event>(trimmed) {
            Ok(ev) => summary.add(ev),
            Err(_) => summary.malformed += 1,
        }
    }

    if summary.queries == 0 {
        print_empty();
        return;
    }

    let allowlist = config.pii.effective_column_allowlist();
    print_report(&summary, &allowlist);
}

#[derive(Default, Debug, PartialEq)]
struct ToolStat {
    queries: usize,
    queries_with_pii: usize,
}

#[derive(Default)]
struct Summary {
    queries: usize,
    queries_with_pii: usize,
    fields_redacted: usize,
    type_counts: HashMap<String, usize>,
    tool_stats: HashMap<String, ToolStat>,
    /// Per-query gate overhead in microseconds. Only events that carry a
    /// recorded timing (`overhead_us > 0`) are collected; legacy events
    /// written before the field existed read as 0 and are skipped so they
    /// don't pull the percentiles toward zero.
    overhead_us: Vec<u64>,
    /// Unique low-confidence warning strings seen across all events.
    warnings: std::collections::BTreeSet<String>,
    malformed: usize,
}

impl Summary {
    fn add(&mut self, ev: Event) {
        self.queries += 1;
        let has_pii = ev.fields_redacted > 0;
        if has_pii {
            self.queries_with_pii += 1;
        }
        self.fields_redacted += ev.fields_redacted;
        if ev.overhead_us > 0 {
            self.overhead_us.push(ev.overhead_us);
        }
        let stat = self.tool_stats.entry(ev.tool).or_default();
        stat.queries += 1;
        if has_pii {
            stat.queries_with_pii += 1;
        }
        for (k, v) in ev.types {
            *self.type_counts.entry(k).or_insert(0) += v;
        }
        for w in ev.warnings {
            self.warnings.insert(w);
        }
    }
}

// All separators and table rows render to this width.
const TABLE_WIDTH: usize = 100;
const TOOL_COL: usize = 25; // For Tool Breakdown section

fn truncate_to(s: &str, max: usize) -> String {
    if s.len() <= max {
        s.to_string()
    } else {
        format!("{}...", &s[..max.saturating_sub(3)])
    }
}

fn print_empty() {
    println!(
        "No protections recorded yet. Run a query through a configured tool \
         to see results here."
    );
}

fn print_report(s: &Summary, allowlist: &[String]) {
    let (hdr, reset, dim) = if crate::color::supports_color() {
        ("\x1b[1;96m", "\x1b[0m", "\x1b[2m")
    } else {
        ("", "", "")
    };
    println!();
    println!("{hdr}Gate Retro — all time{reset}");
    println!("{}", "─".repeat(TABLE_WIDTH));
    let hit_pct = if s.queries > 0 {
        (s.queries_with_pii as f64 / s.queries as f64) * 100.0
    } else {
        0.0
    };
    let bar_width = 24usize;
    let filled = ((hit_pct / 100.0) * bar_width as f64).round() as usize;
    let bar = format!(
        "{}{}",
        "█".repeat(filled.min(bar_width)),
        "░".repeat(bar_width - filled.min(bar_width))
    );

    println!(
        "{:<TOOL_COL$}  {:>24}",
        "PII fields redacted:", s.fields_redacted
    );
    println!("{:<TOOL_COL$}  {:>24}", "Queries protected:", s.queries);
    println!(
        "{:<TOOL_COL$}  {:>24}",
        "Queries with PII:", s.queries_with_pii
    );
    println!();
    println!("{:<TOOL_COL$}  {}    {:.1}%", "Hit rate:", bar, hit_pct);
    println!();

    if !s.overhead_us.is_empty() {
        let avg_ms = s.overhead_us.iter().sum::<u64>() as f64 / s.overhead_us.len() as f64 / 1000.0;

        println!("{hdr}Gate Overhead — added latency per query{reset}");
        println!("{}", "─".repeat(TABLE_WIDTH));
        println!("{:<TOOL_COL$}  {:>21.2} ms", "Average:", avg_ms);
        println!();
        println!(
            "{dim}Gate added an average of {:.2} ms per query (n={}). \
             The wrapped tool's own runtime is not counted.{reset}",
            avg_ms,
            s.overhead_us.len(),
        );
        println!();
    }

    if !s.tool_stats.is_empty() {
        let mut rows: Vec<(&String, &ToolStat)> = s.tool_stats.iter().collect();
        rows.sort_by(|a, b| b.1.queries.cmp(&a.1.queries).then(a.0.cmp(b.0)));

        const H_PROT: &str = "Queries protected";
        const H_PII: &str = "Queries with PII";
        const H_HR: &str = "Hit rate";

        println!("{hdr}Tool Breakdown{reset}");
        println!("{}", "─".repeat(TABLE_WIDTH));
        println!(
            "{:<TOOL_COL$}  {:>25}  {:>25}  {:>12}",
            "Tool", H_PROT, H_PII, H_HR,
        );
        println!("{}", "─".repeat(TABLE_WIDTH));
        for (name, stat) in &rows {
            let hit_rate = format!(
                "{:.1}%",
                (stat.queries_with_pii as f64 / stat.queries as f64) * 100.0
            );
            println!(
                "{:<TOOL_COL$}  {:>25}  {:>25}  {:>12}",
                truncate_to(name, TOOL_COL),
                stat.queries,
                stat.queries_with_pii,
                hit_rate,
            );
        }
        println!();
    }

    if !s.type_counts.is_empty() {
        let mut by_count: Vec<(&String, &usize)> = s.type_counts.iter().collect();
        by_count.sort_by(|a, b| b.1.cmp(a.1).then(a.0.cmp(b.0)));
        let total_redacted: usize = s.type_counts.values().sum();

        let mut grouped: HashMap<&str, Vec<(&String, &usize)>> = HashMap::new();
        for (name, count) in &by_count {
            let tier1 = map_to_tier1_category(name);
            grouped.entry(tier1).or_default().push((name, count));
        }

        let mut tier1_sorted: Vec<&str> = grouped.keys().copied().collect();
        tier1_sorted.sort_by_key(|tier1| grouped[tier1].iter().map(|(_, c)| *c).sum::<usize>());
        tier1_sorted.reverse();

        const H_CAT: &str = "Category";
        const H_SUBCAT: &str = "Sub Category";
        const H_REDACTED: &str = "PII fields redacted";
        const H_PCT: &str = "Percentage";

        println!("{hdr}PII Breakdown{reset}");
        println!("{}", "─".repeat(TABLE_WIDTH));
        println!(
            "{:<24}  {:<22}  {:>20}        {:>16}",
            H_CAT, H_SUBCAT, H_REDACTED, H_PCT,
        );
        println!("{}", "─".repeat(TABLE_WIDTH));

        let mut row_count = 0;
        for tier1 in tier1_sorted {
            let items = &grouped[tier1];
            for (i, (name, count)) in items.iter().enumerate() {
                if row_count >= 10 {
                    break;
                }
                let pct = if total_redacted > 0 {
                    (**count as f64 / total_redacted as f64) * 100.0
                } else {
                    0.0
                };
                let cat_display = if i == 0 { tier1 } else { "" };
                println!(
                    "{:<24}  {:<22}  {:>20}        {:>15.1}%",
                    cat_display,
                    truncate_to(name, 22),
                    count,
                    pct,
                );
                row_count += 1;
            }
            if row_count >= 10 {
                break;
            }
        }
        println!();
    }

    println!(
        "{dim}{} sensitive fields never reached the model.{reset}",
        s.fields_redacted
    );
    println!();

    // Only show false-positive warnings (low-confidence matches).
    // Other warning types (e.g., SELECT * policy notices) are not actionable
    // for the user and should not appear in retro.
    // Filter out columns already in the allowlist — the user has already
    // acted on those; showing them again would just be noise.
    let pending_warnings: Vec<&String> = s
        .warnings
        .iter()
        .filter(|w| {
            // Only include low-confidence match warnings
            if !w.starts_with("low-confidence match:") {
                return false;
            }
            let key = w
                .split("key=")
                .nth(1)
                .and_then(|s| s.split_whitespace().next())
                .unwrap_or("?");
            !allowlist.iter().any(|a| a == key)
        })
        .collect();

    if !pending_warnings.is_empty() {
        println!("{hdr}Low-confidence redactions — review for false positives{reset}");
        println!("{}", "─".repeat(TABLE_WIDTH));
        for w in pending_warnings {
            let key = w
                .split("key=")
                .nth(1)
                .and_then(|s| s.split_whitespace().next())
                .unwrap_or("?");
            println!("  {w}");
            println!(
                "  {dim}→ run 'gate allowlist add {key}' to suppress if false positive{reset}"
            );
            println!();
        }
    }

    if s.malformed > 0 {
        eprintln!(
            "[gate retro] note: skipped {} malformed line(s) in stats log",
            s.malformed
        );
    }
}

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

    fn ev(tool: &str, fields: usize, types: &[(&str, usize)]) -> Event {
        let mut m = HashMap::new();
        for (k, v) in types {
            m.insert(k.to_string(), *v);
        }
        Event {
            ts: 0,
            path: "bash".to_string(),
            tool: tool.to_string(),
            fields_redacted: fields,
            overhead_us: 0,
            types: m,
            warnings: vec![],
        }
    }

    fn ev_timed(tool: &str, fields: usize, overhead_us: u64) -> Event {
        Event {
            ts: 0,
            path: "bash".to_string(),
            tool: tool.to_string(),
            fields_redacted: fields,
            overhead_us,
            types: HashMap::new(),
            warnings: vec![],
        }
    }

    #[test]
    fn summary_aggregates_per_type_counts_across_events() {
        let mut s = Summary::default();
        s.add(ev("tkpsql", 5, &[("email", 3), ("ssn", 2)]));
        s.add(ev("tkpsql", 4, &[("email", 4)]));
        s.add(ev("postgres", 2, &[("phone", 2)]));
        assert_eq!(s.queries, 3);
        assert_eq!(s.queries_with_pii, 3);
        assert_eq!(s.fields_redacted, 11);
        assert_eq!(s.type_counts.get("email"), Some(&7));
        assert_eq!(s.type_counts.get("ssn"), Some(&2));
        assert_eq!(s.type_counts.get("phone"), Some(&2));
    }

    #[test]
    fn hit_rate_excludes_clean_queries() {
        let mut s = Summary::default();
        s.add(ev("tkpsql", 3, &[("email", 3)]));
        s.add(ev("tkpsql", 0, &[]));
        s.add(ev("tkpsql", 0, &[]));
        assert_eq!(s.queries, 3);
        assert_eq!(s.queries_with_pii, 1);
    }

    #[test]
    fn summary_counts_per_tool() {
        let mut s = Summary::default();
        s.add(ev("tkpsql", 3, &[("email", 3)]));
        s.add(ev("tkpsql", 0, &[]));
        s.add(ev("postgres", 1, &[("ssn", 1)]));
        assert_eq!(s.tool_stats.get("tkpsql").map(|t| t.queries), Some(2));
        assert_eq!(
            s.tool_stats.get("tkpsql").map(|t| t.queries_with_pii),
            Some(1)
        );
        assert_eq!(s.tool_stats.get("postgres").map(|t| t.queries), Some(1));
        assert_eq!(
            s.tool_stats.get("postgres").map(|t| t.queries_with_pii),
            Some(1)
        );
        assert_eq!(s.tool_stats.get("mcp"), None);
    }

    #[test]
    fn summary_collects_only_nonzero_overhead() {
        let mut s = Summary::default();
        s.add(ev_timed("tkpsql", 1, 500));
        s.add(ev_timed("tkpsql", 0, 0)); // legacy event: no timing recorded
        s.add(ev_timed("tkpsql", 2, 1500));
        assert_eq!(s.overhead_us, vec![500, 1500]);
    }

    #[test]
    fn retro_filters_to_low_confidence_warnings_only() {
        let mut s = Summary::default();
        // Add a SELECT * warning (should be filtered out)
        let mut ev1 = ev("tkpsql", 0, &[]);
        ev1.warnings = vec![
            "SELECT * encountered; wildcard_policy=warn, Gate 2 is the safety net".to_string(),
        ];
        s.add(ev1);

        // Add a low-confidence warning (should be included)
        let mut ev2 = ev("tkpsql", 1, &[("email", 1)]);
        ev2.warnings = vec![
            "low-confidence match: key=user_info pattern=email_pattern score=0.65".to_string(),
        ];
        s.add(ev2);

        // Add another low-confidence warning (should be included)
        let mut ev3 = ev("tkpsql", 1, &[("ssn", 1)]);
        ev3.warnings = vec![
            "low-confidence match: key=personal_data pattern=ssn_pattern score=0.72".to_string(),
        ];
        s.add(ev3);

        assert_eq!(s.warnings.len(), 3);
        // Manually filter to simulate what print_report does
        let pending: Vec<&String> = s
            .warnings
            .iter()
            .filter(|w| w.starts_with("low-confidence match:"))
            .collect();
        assert_eq!(
            pending.len(),
            2,
            "Should only include low-confidence warnings, not SELECT * warnings"
        );
    }
}
