use std::collections::BTreeMap;
use std::fmt::Write as _;
use std::path::PathBuf;

use sqpack_rs::verify::{HashManifestBuildIssueKind, HashMismatch, IndexHashKey, PathMismatchKind};
use sqpack_rs::{Category, FileKind, SqPack, SqPackHashManifestBuilder, SqPackHashVerifier};

#[allow(clippy::too_many_lines)]
fn main() -> Result<(), Box<dyn std::error::Error>> {
    let install_root: PathBuf = if let Some(arg) = std::env::args().nth(1) {
        PathBuf::from(arg)
    } else {
        eprintln!(
            "usage: scan_install <install_root>\n\n\
             Pass the FFXIV game directory (the one containing sqpack/).",
        );
        std::process::exit(2);
    };

    if !install_root.join("sqpack").is_dir() {
        eprintln!(
            "No sqpack/ directory under {}; argv[1] must be the FFXIV game directory.",
            install_root.display(),
        );
        std::process::exit(1);
    }

    println!("install root: {}", install_root.display());
    println!();

    let sq = SqPack::open(&install_root)?;

    println!("=== Step 1: building hash-keyed manifest by sweeping every archive ===");
    let started_at = std::time::Instant::now();
    let mut builder = SqPackHashManifestBuilder::new(&sq);
    let build_report = builder.build()?;
    let build_elapsed = started_at.elapsed();

    println!(
        "build complete in {:?}: verified={} skipped={} cancelled={} total_entries={}",
        build_elapsed,
        build_report.verified_count,
        build_report.skipped_count,
        build_report.cancelled,
        build_report.manifest.len(),
    );

    if build_report.manifest.is_empty() {
        eprintln!("No entries in the manifest — install doesn't carry any hashable archives.");
        std::process::exit(2);
    }

    let mut by_category: BTreeMap<Category, usize> = BTreeMap::new();
    for key in build_report.manifest.entries.keys() {
        *by_category.entry(key.category()).or_default() += 1;
    }
    println!();
    println!("by_category breakdown (recorded entries):");
    for (cat, count) in &by_category {
        println!(
            "  category {} (id=0x{:02x}): {count} entries",
            category_name(*cat),
            cat.id(),
        );
    }

    let mut issue_kinds: BTreeMap<&'static str, usize> = BTreeMap::new();
    for issue in &build_report.issues {
        let label = match &issue.kind {
            HashManifestBuildIssueKind::Unverifiable {
                kind: FileKind::Empty,
            } => "Unverifiable(Empty)",
            HashManifestBuildIssueKind::Unverifiable {
                kind: FileKind::Unknown(_),
            } => "Unverifiable(Unknown)",
            HashManifestBuildIssueKind::Unverifiable {
                kind: FileKind::Model,
            } => "Unverifiable(Model)",
            HashManifestBuildIssueKind::Unverifiable {
                kind: FileKind::Texture,
            } => "Unverifiable(Texture)",
            HashManifestBuildIssueKind::Unverifiable {
                kind: FileKind::Standard,
            } => "Unverifiable(Standard)",
            HashManifestBuildIssueKind::ReadError { .. } => "ReadError",
            _ => "Other",
        };
        *issue_kinds.entry(label).or_default() += 1;
    }
    if !issue_kinds.is_empty() {
        println!();
        println!("build issues by kind:");
        for (label, count) in &issue_kinds {
            println!("  {label}: {count}");
        }
        let sample_read_errors: Vec<&sqpack_rs::verify::HashManifestBuildIssue> = build_report
            .issues
            .iter()
            .filter(|i| matches!(i.kind, HashManifestBuildIssueKind::ReadError { .. }))
            .take(5)
            .collect();
        if !sample_read_errors.is_empty() {
            println!();
            println!("sample ReadError issues:");
            for issue in sample_read_errors {
                if let HashManifestBuildIssueKind::ReadError { message } = &issue.kind {
                    println!("  - {}: {message}", describe_key(&issue.key));
                }
            }
        }
        let modtex = issue_kinds.get("Unverifiable(Model)").copied().unwrap_or(0)
            + issue_kinds
                .get("Unverifiable(Texture)")
                .copied()
                .unwrap_or(0);
        if modtex > 0 {
            eprintln!(
                "WARNING: {modtex} Model/Texture entries surfaced as Unverifiable — \
                 typed decoders should have hashed these.",
            );
        }
    }

    println!();
    println!("=== Step 2: round-trip verify against the same install ===");
    let started_at = std::time::Instant::now();
    let mut verifier = SqPackHashVerifier::new(&sq);
    let report = verifier.verify(&build_report.manifest);
    let verify_elapsed = started_at.elapsed();

    println!(
        "verify complete in {:?}: verified={} mismatches={} skipped={} extras={} cancelled={} is_clean={}",
        verify_elapsed,
        report.verified_count,
        report.mismatches.len(),
        report.skipped_count,
        report.extras.len(),
        report.cancelled,
        report.is_clean(),
    );
    if !report.mismatches.is_empty() {
        eprintln!("Round-trip verify FAILED — library produced inconsistent hashes.");
        for m in report.mismatches.iter().take(5) {
            print_mismatch(m);
        }
        std::process::exit(3);
    }
    if !report.extras.is_empty() {
        println!(
            "(extras = {} entries present in the install but skipped by the builder; \
             these are Empty / Unknown kinds and don't indicate corruption.)",
            report.extras.len(),
        );
    }

    println!();
    println!("=== Step 3: synthesize corruption by tampering with one manifest entry ===");
    let mut tampered = build_report.manifest.clone();
    let Some(victim_key) = tampered.entries.keys().next().copied() else {
        eprintln!("Tamper step requires at least one manifest entry; none recorded.");
        std::process::exit(2);
    };
    let original_hash = tampered.entries[&victim_key].hash;
    tampered.entries.get_mut(&victim_key).unwrap().hash[0] ^= 0x01;
    println!("tampered key: {victim_key:?}");
    println!(
        "  expected (post-tamper): {}",
        hex(&tampered.entries[&victim_key].hash)
    );
    println!("  actual (live install):  {}", hex(&original_hash));

    let mut verifier = SqPackHashVerifier::new(&sq);
    let report = verifier.verify(&tampered);

    println!();
    println!(
        "verify complete: verified={} mismatches={} skipped={} extras={} cancelled={} is_clean={}",
        report.verified_count,
        report.mismatches.len(),
        report.skipped_count,
        report.extras.len(),
        report.cancelled,
        report.is_clean(),
    );

    if report.mismatches.is_empty() {
        eprintln!("Expected at least one mismatch (the tampered entry); got none.");
        std::process::exit(4);
    }

    println!();
    println!("by_category breakdown of mismatches:");
    for (cat, mismatches) in report.by_category() {
        println!(
            "  category {} (id=0x{:02x}): {} mismatches",
            category_name(cat),
            cat.id(),
            mismatches.len(),
        );
        for m in mismatches.iter().take(3) {
            print_mismatch(m);
        }
    }

    let hash_only: Vec<&HashMismatch> = report
        .mismatches
        .iter()
        .filter(|m| matches!(m.kind, PathMismatchKind::HashMismatch { .. }))
        .collect();
    if !hash_only.iter().any(|m| m.key == victim_key) {
        eprintln!(
            "victim key {victim_key:?} was not among the {} HashMismatches",
            hash_only.len()
        );
        std::process::exit(5);
    }

    println!();
    println!(
        "OK — hash-keyed manifest produces a clean round-trip and identifies synthesized corruption."
    );
    Ok(())
}

fn hex(bytes: &[u8]) -> String {
    let mut s = String::with_capacity(bytes.len() * 2);
    for b in bytes {
        let _ = write!(s, "{b:02x}");
    }
    s
}

fn category_name(c: Category) -> &'static str {
    for (name, id) in KNOWN_NAMES {
        if c.id() == *id {
            return name;
        }
    }
    "<unknown>"
}

const KNOWN_NAMES: &[(&str, u8)] = &[
    ("common", 0x00),
    ("bgcommon", 0x01),
    ("bg", 0x02),
    ("cut", 0x03),
    ("chara", 0x04),
    ("shader", 0x05),
    ("ui", 0x06),
    ("sound", 0x07),
    ("vfx", 0x08),
    ("ui_script", 0x09),
    ("exd", 0x0a),
    ("game_script", 0x0b),
    ("music", 0x0c),
    ("sqpack_test", 0x12),
    ("debug", 0x13),
];

fn print_mismatch(m: &HashMismatch) {
    let key_repr = describe_key(&m.key);
    let path_repr = m.path.as_deref().unwrap_or("<no path>");
    match &m.kind {
        PathMismatchKind::HashMismatch { expected, actual } => {
            println!("    {key_repr} [{path_repr}] HashMismatch");
            println!("      expected: {}", hex(expected));
            println!("      actual:   {}", hex(actual));
        }
        PathMismatchKind::SizeMismatch { expected, actual } => {
            println!(
                "    {key_repr} [{path_repr}] SizeMismatch (expected={expected} actual={actual})",
            );
        }
        PathMismatchKind::Missing => println!("    {key_repr} [{path_repr}] Missing"),
        PathMismatchKind::Unverifiable { kind } => {
            println!("    {key_repr} [{path_repr}] Unverifiable (kind={kind:?})");
        }
        PathMismatchKind::ReadError { message, .. } => {
            println!("    {key_repr} [{path_repr}] ReadError: {message}");
        }
        other => println!("    {key_repr} [{path_repr}] {other:?}"),
    }
}

fn describe_key(key: &IndexHashKey) -> String {
    match key {
        IndexHashKey::V1 { triple, hash } => format!(
            "v1 {}/{}/{} @ {:#018x}",
            category_name(triple.category),
            triple.expansion,
            triple.chunk,
            hash,
        ),
        IndexHashKey::V2 { triple, hash } => format!(
            "v2 {}/{}/{} @ {:#010x}",
            category_name(triple.category),
            triple.expansion,
            triple.chunk,
            hash,
        ),
        other => format!("{other:?}"),
    }
}
