//! Token-optimized output (toon) for AI agent consumption.
//!
//! Design principles from the AI's perspective:
//!
//! 1. **I never need all N matches.** I need aggregate counts and the ability
//!    to drill into specifics on demand. Sending 108,010 match records when I
//!    only report the top 20 evidence types is a 5,400:1 waste.
//!
//! 2. **Repeated field names are pure waste.** JSON repeats "evidence_type",
//!    "detection_method", "line_number" etc. 108,010 times. Positional or
//!    tagged formats eliminate this entirely.
//!
//! 3. **I think in hierarchies.** Evidence grouped by type > by file > by line.
//!    Not the reverse. Flat lists of matches require me to mentally GROUP BY
//!    before I can answer any question.
//!
//! 4. **I need risk classification built in.** Don't make me know that MD5,
//!    DES, RC4, SHA-1 are weak. Tag them. I should be able to say "3 weak
//!    algorithms found" without consulting my training data.
//!
//! 5. **Context lines are almost never needed.** 6 lines of context per match
//!    (3 before + 3 after) accounts for ~70% of output volume. Only include
//!    them in drill-down mode.
//!
//! 6. **Paths should be relative and prefix-stripped.** "/home/ops/Project/
//!    windriver-offerings/kindi/src/main.rs" should be "src/main.rs".
//!
//! Format:
//! ```text
//! @kindi pkg=linux files=93188 hits=108010 matched=4887 t=0.934s verif=5647fd86
//! #by_type count=20
//! generic 73637 1823f
//! AES 10804 412f drivers/crypto/inside-secure/safexcel_cipher.c:1010 drivers/crypto/axis/artpec6_crypto.c:940
//! SHA2 3851 301f crypto/sha256_generic.c:42
//! ...
//! #weak
//! DES 2948 266f DEPRECATED:NIST-2023
//! MD5 1193 189f BROKEN:collision-attacks
//! SHA1 1364 201f DEPRECATED:NIST-2030
//! RC4 87 12f BROKEN:biased-keystream
//! #libs
//! OpenSSL 197 call=171 type=248 inc=29
//! Crypto++ 249
//! #hot top=15
//! 1010 drivers/crypto/inside-secure/safexcel_cipher.c AES,DES,SHA2
//! 940 drivers/crypto/axis/artpec6_crypto.c AES,SHA2,HMAC
//! ...
//! #methods
//! keyword 107562
//! api 448
//! ```

use std::collections::{HashMap, HashSet};
use std::fmt::Write as _;
use std::path::Path;

use crate::detect::ScanResult;

/// Algorithms/protocols considered weak or deprecated.
const WEAK_EVIDENCE: &[(&str, &str)] = &[
    ("algorithm/symmetric/block-cipher/DES", "DEPRECATED:NIST-withdrawn"),
    ("algorithm/symmetric/block-cipher/3DES", "DEPRECATED:NIST-2023"),
    ("algorithm/hash/MD5", "BROKEN:collision-attacks"),
    ("algorithm/hash/MD4", "BROKEN:collision-attacks"),
    ("algorithm/hash/MD2", "BROKEN:collision-attacks"),
    ("algorithm/hash/SHA1", "DEPRECATED:NIST-2030"),
    ("algorithm/symmetric/stream-cipher/RC4", "BROKEN:biased-keystream"),
    ("algorithm/symmetric/stream-cipher/RC2", "DEPRECATED:weak-key-schedule"),
    ("algorithm/symmetric/block-cipher/Blowfish", "WEAK:64-bit-block"),
    ("algorithm/symmetric/block-cipher/IDEA", "DEPRECATED:patent-expired-weak"),
];

/// Evidence type entry in the toon output.
struct EvidenceAgg {
    count: usize,
    files: HashSet<String>,
    /// Top files by hit count: (count, path)
    top_files: Vec<(usize, String)>,
}

/// Build the toon output from a ScanResult.
pub fn to_toon(result: &ScanResult, elapsed_secs: f64, top_n: usize) -> String {
    let mut out = String::with_capacity(4096);

    let root = result.package_name.as_str();

    // Build aggregates
    let mut by_type: HashMap<String, EvidenceAgg> = HashMap::new();
    let mut by_file: HashMap<String, (usize, HashSet<String>)> = HashMap::new(); // path -> (count, evidence_types)
    let mut libs: HashMap<String, LibAgg> = HashMap::new();
    let mut method_counts: HashMap<String, usize> = HashMap::new();

    for fr in &result.file_results {
        // Strip common root prefix for shorter paths
        let short_path = strip_prefix(&fr.path, root);

        let file_entry = by_file
            .entry(short_path.clone())
            .or_insert_with(|| (0, HashSet::new()));

        for m in &fr.matches {
            let etype = &m.inner.evidence_type;
            let entry = by_type.entry(etype.clone()).or_insert_with(|| EvidenceAgg {
                count: 0,
                files: HashSet::new(),
                top_files: Vec::new(),
            });
            entry.count += 1;
            entry.files.insert(short_path.clone());

            file_entry.0 += 1;
            file_entry.1.insert(shorten_evidence_type(etype));

            *method_counts.entry(m.detection_method.clone()).or_default() += 1;

            if let Some(ref lib) = m.inner.encryption_library {
                let le = libs.entry(lib.clone()).or_insert_with(|| LibAgg {
                    count: 0,
                    calls: 0,
                    types: 0,
                    includes: 0,
                });
                le.count += 1;
                match m.inner.encryption_api_usage.as_deref() {
                    Some("call") => le.calls += 1,
                    Some("data_type") => le.types += 1,
                    Some("include") => le.includes += 1,
                    _ => {}
                }
            }
        }
    }

    // Compute top files per evidence type
    for (etype, agg) in &mut by_type {
        let mut file_counts: HashMap<String, usize> = HashMap::new();
        for fr in &result.file_results {
            let c = fr
                .matches
                .iter()
                .filter(|m| &m.inner.evidence_type == etype)
                .count();
            if c > 0 {
                let short = strip_prefix(&fr.path, root);
                *file_counts.entry(short).or_default() += c;
            }
        }
        let mut sorted: Vec<_> = file_counts.into_iter().map(|(p, c)| (c, p)).collect();
        sorted.sort_by(|a, b| b.0.cmp(&a.0));
        agg.top_files = sorted.into_iter().take(3).collect();
    }

    // Sort evidence types by count descending
    let mut sorted_types: Vec<_> = by_type.iter().collect();
    sorted_types.sort_by(|a, b| b.1.count.cmp(&a.1.count));

    // Sort files by hit count descending
    let mut sorted_files: Vec<_> = by_file.iter().collect();
    sorted_files.sort_by(|a, b| (b.1).0.cmp(&(a.1).0));

    // Build weak crypto lookup
    let weak_map: HashMap<&str, &str> = WEAK_EVIDENCE.iter().copied().collect();

    // === Header ===
    let _ = writeln!(
        out,
        "@kindi pkg={} files={} hits={} matched={} t={:.3}s",
        root, result.files_scanned, result.total_matches, result.files_with_matches, elapsed_secs
    );

    // === Evidence by type ===
    let type_count = sorted_types.len().min(top_n);
    let _ = writeln!(out, "#by_type count={}", type_count);
    for (etype, agg) in sorted_types.iter().take(top_n) {
        let short = shorten_evidence_type(etype);
        let top_str: String = agg
            .top_files
            .iter()
            .take(2)
            .map(|(c, p)| format!(" {}:{}", p, c))
            .collect();
        let _ = writeln!(out, "{} {} {}f{}", short, agg.count, agg.files.len(), top_str);
    }

    // === Weak crypto ===
    let weak_found: Vec<_> = sorted_types
        .iter()
        .filter(|(etype, _)| weak_map.contains_key(etype.as_str()))
        .collect();
    if !weak_found.is_empty() {
        let _ = writeln!(out, "#weak count={}", weak_found.len());
        for (etype, agg) in &weak_found {
            let reason = weak_map.get(etype.as_str()).unwrap_or(&"REVIEW");
            let short = shorten_evidence_type(etype);
            let _ = writeln!(out, "{} {} {}f {}", short, agg.count, agg.files.len(), reason);
        }
    }

    // === Libraries ===
    if !libs.is_empty() {
        let _ = writeln!(out, "#libs");
        let mut sorted_libs: Vec<_> = libs.iter().collect();
        sorted_libs.sort_by(|a, b| b.1.count.cmp(&a.1.count));
        for (name, la) in sorted_libs {
            let mut detail = format!("{} {}", name, la.count);
            if la.calls > 0 {
                let _ = write!(detail, " call={}", la.calls);
            }
            if la.types > 0 {
                let _ = write!(detail, " type={}", la.types);
            }
            if la.includes > 0 {
                let _ = write!(detail, " inc={}", la.includes);
            }
            let _ = writeln!(out, "{}", detail);
        }
    }

    // === Hot files ===
    let hot_count = sorted_files.len().min(top_n);
    let _ = writeln!(out, "#hot top={}", hot_count);
    for (path, (count, etypes)) in sorted_files.iter().take(top_n) {
        let mut types_vec: Vec<_> = etypes.iter().cloned().collect();
        types_vec.sort();
        types_vec.truncate(5);
        let _ = writeln!(out, "{} {} {}", count, path, types_vec.join(","));
    }

    // === Methods ===
    let _ = writeln!(out, "#methods");
    let mut sorted_methods: Vec<_> = method_counts.iter().collect();
    sorted_methods.sort_by(|a, b| b.1.cmp(a.1));
    for (method, count) in sorted_methods {
        let _ = writeln!(out, "{} {}", method, count);
    }

    out
}

/// Write toon output to a file.
pub fn write_toon(result: &ScanResult, elapsed_secs: f64, top_n: usize, path: &Path) -> std::io::Result<()> {
    let content = to_toon(result, elapsed_secs, top_n);
    std::fs::write(path, content)
}

/// Write toon to stdout.
pub fn print_toon(result: &ScanResult, elapsed_secs: f64, top_n: usize) {
    let content = to_toon(result, elapsed_secs, top_n);
    print!("{}", content);
}

struct LibAgg {
    count: usize,
    calls: usize,
    types: usize,
    includes: usize,
}

/// Shorten an evidence type for compact display.
/// "algorithm/symmetric/block-cipher/AES" -> "AES"
/// "algorithm/hash/SHA2" -> "SHA2"
/// "protocol/TLS" -> "TLS"
/// "library/OpenSSL" -> "OpenSSL"
fn shorten_evidence_type(etype: &str) -> String {
    etype
        .rsplit('/')
        .next()
        .unwrap_or(etype)
        .to_string()
}

/// Strip a common prefix from a path for shorter display.
fn strip_prefix(path: &str, root: &str) -> String {
    // Try stripping the package root
    if let Some(rest) = path.strip_prefix(root) {
        return rest.trim_start_matches('/').to_string();
    }
    // Try stripping everything up to and including the package name
    if let Some(pos) = path.find(root) {
        return path[pos + root.len()..].trim_start_matches('/').to_string();
    }
    path.to_string()
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::detect::{AnnotatedMatch, FileResult, ScanResult};
    use crate::pattern::Match;

    fn make_match(evidence_type: &str, matched_text: &str, method: &str) -> AnnotatedMatch {
        AnnotatedMatch {
            inner: Match {
                matched_text: matched_text.to_string(),
                evidence_type: evidence_type.to_string(),
                line_number: 1,
                file_index_begin: 0,
                file_index_end: matched_text.len(),
                line_index_begin: 0,
                line_index_end: matched_text.len(),
                line_text: matched_text.to_string(),
                encryption_library: None,
                encryption_api_usage: None,
                context_before: [String::new(), String::new(), String::new()],
                context_after: [String::new(), String::new(), String::new()],
            },
            detection_method: method.to_string(),
        }
    }

    #[test]
    fn test_toon_basic() {
        let result = ScanResult {
            package_name: "testpkg".to_string(),
            files_scanned: 100,
            files_with_matches: 2,
            total_matches: 5,
            file_results: vec![
                FileResult {
                    path: "testpkg/src/crypto.c".to_string(),
                    sha1: "aaaa".to_string(),
                    is_source_code: true,
                    language: "c".to_string(),
                    matches: vec![
                        make_match("algorithm/hash/SHA2", "SHA256", "keyword"),
                        make_match("algorithm/hash/SHA2", "SHA256", "keyword"),
                        make_match("algorithm/hash/MD5", "MD5_Init", "api"),
                    ],
                },
                FileResult {
                    path: "testpkg/src/legacy.c".to_string(),
                    sha1: "bbbb".to_string(),
                    is_source_code: true,
                    language: "c".to_string(),
                    matches: vec![
                        make_match("algorithm/symmetric/block-cipher/DES", "DES_encrypt", "keyword"),
                        make_match("algorithm/hash/MD5", "MD5_Final", "keyword"),
                    ],
                },
            ],
        };

        let toon = to_toon(&result, 0.042, 20);

        // Header must be present
        assert!(toon.starts_with("@kindi pkg=testpkg files=100 hits=5 matched=2"));

        // Must contain evidence types
        assert!(toon.contains("SHA2 2"));
        assert!(toon.contains("MD5 2"));
        assert!(toon.contains("DES 1"));

        // Must flag weak crypto
        assert!(toon.contains("#weak"));
        assert!(toon.contains("BROKEN:collision-attacks"));
        assert!(toon.contains("DEPRECATED:NIST-withdrawn"));

        // Must contain methods
        assert!(toon.contains("#methods"));
        assert!(toon.contains("keyword"));
        assert!(toon.contains("api"));

        // Must be small
        assert!(toon.len() < 1000, "toon is {} bytes, should be <1KB", toon.len());
    }

    #[test]
    fn test_shorten_evidence_type() {
        assert_eq!(shorten_evidence_type("algorithm/symmetric/block-cipher/AES"), "AES");
        assert_eq!(shorten_evidence_type("algorithm/hash/SHA2"), "SHA2");
        assert_eq!(shorten_evidence_type("protocol/TLS"), "TLS");
        assert_eq!(shorten_evidence_type("generic"), "generic");
    }

    #[test]
    fn test_strip_prefix() {
        assert_eq!(strip_prefix("/tmp/linux/drivers/crypto/aes.c", "linux"), "drivers/crypto/aes.c");
        assert_eq!(strip_prefix("pkg/src/main.rs", "pkg"), "src/main.rs");
    }
}
