//! Naive vs DiffSift comparison benchmark.
//!
//! Loads multi-file fixtures across multiple scenarios, classifies all hunks,
//! generates both naive and DiffSift-targeted prompts, and reports token/cost metrics.
//!
//! Run all scenarios (prompt-only mode):
//!   cargo run --example comparison
//!
//! Run a single scenario:
//!   cargo run --example comparison -- --scenario small_bugfix
//!
//! Run with LLM comparison (auto-detects provider from env):
//!   ANTHROPIC_API_KEY=sk-... cargo run --example comparison
//!   OPENAI_API_KEY=sk-... cargo run --example comparison
//!   GEMINI_API_KEY=... cargo run --example comparison
//!
//! Force a specific provider:
//!   DIFFSIFT_PROVIDER=openai OPENAI_API_KEY=sk-... cargo run --example comparison

use std::path::PathBuf;

use diffsift_core::classifier::{Classifier, CrossHunkContext};
use diffsift_core::differ::{self, DiffFile, FileStatus};
use diffsift_core::languages;
use diffsift_core::mapper::{self, MappedFile};
use diffsift_core::rules;
use diffsift_core::taxonomy::{
    ChangeCategory, ClassifiedFile as TaxClassifiedFile, ClassifiedHunk, Priority,
};

use diffsift_llm::anthropic::AnthropicProvider;
use diffsift_llm::openai::OpenAiProvider;
use diffsift_llm::provider::ReviewProvider;
use diffsift_llm::TemplateEngine;

// ── Manifest types ──────────────────────────────────────────────────────────

#[derive(serde::Deserialize)]
struct Manifest {
    name: String,
    #[allow(dead_code)]
    description: String,
    #[allow(dead_code)]
    expected_noise_ratio: String,
    files: Vec<String>,
    bugs: Vec<BugDef>,
}

#[derive(serde::Deserialize)]
#[allow(dead_code)]
struct BugDef {
    id: String,
    file: String,
    description: String,
    expected_rule: String,
    expected_category: String,
    search_keywords: Vec<String>,
}

// ── Scenario loading ────────────────────────────────────────────────────────

fn scenarios_dir() -> PathBuf {
    PathBuf::from(env!("CARGO_MANIFEST_DIR"))
        .parent()
        .unwrap()
        .join("diffsift-core/tests/comparison/scenarios")
}

fn list_scenarios() -> Vec<String> {
    let dir = scenarios_dir();
    let mut scenarios: Vec<String> = std::fs::read_dir(&dir)
        .unwrap_or_else(|e| panic!("read scenarios dir {}: {e}", dir.display()))
        .filter_map(|entry| {
            let entry = entry.ok()?;
            if entry.file_type().ok()?.is_dir() {
                Some(entry.file_name().to_string_lossy().to_string())
            } else {
                None
            }
        })
        .collect();
    // Stable ordering: small, medium, large, language-specific, pure — grouped by tier
    let order = [
        "small_bugfix",
        "small_security",
        "small_api_break",
        "medium_feature",
        "medium_security",
        "medium_api_change",
        "large_refactor",
        "large_migration",
        "go_concurrency_cache",
        "python_test_refactor",
        "infra_config_update",
        "rust_async_refactor",
        "java_payment_service",
        "ts_dashboard_refactor",
        "ruby_ecommerce",
        "csharp_api_service",
        "csharp_dotnet_patterns",
        "php_cms_plugin",
        "kotlin_android_app",
        "elixir_genserver",
        "swift_ios_app",
        "bash_deploy_script",
        "powershell_infra",
        "javascript_node_api",
        "realworld_goto_fail",
        "realworld_heartbleed",
        "realworld_log4shell",
        "ai_hallucinated_deps",
        "forgot_precondition",
        "ai_incomplete_stub",
        "ai_silent_swallow",
        "semantic_vs_rename",
        "taint_injection_chain",
        "structural_refactor_noise",
        "auth_permission_escalation",
        "control_flow_error_handling",
        "type_safety_regression",
        "data_flow_mutations",
        "pure_rename",
        "pure_formatting",
    ];
    scenarios.sort_by_key(|s| order.iter().position(|o| o == s).unwrap_or(usize::MAX));
    scenarios
}

fn load_scenario(name: &str) -> (Vec<DiffFile>, Manifest, PathBuf) {
    let dir = scenarios_dir().join(name);
    let scenario_root = dir.clone();
    let manifest_path = dir.join("manifest.json");
    let json = std::fs::read_to_string(&manifest_path)
        .unwrap_or_else(|e| panic!("read {}: {e}", manifest_path.display()));
    let manifest: Manifest = serde_json::from_str(&json)
        .unwrap_or_else(|e| panic!("parse {}: {e}", manifest_path.display()));

    let diff_files: Vec<DiffFile> = manifest
        .files
        .iter()
        .map(|filename| {
            let base = std::fs::read(dir.join("base").join(filename))
                .unwrap_or_else(|e| panic!("read base/{filename}: {e}"));
            let head = std::fs::read(dir.join("head").join(filename))
                .unwrap_or_else(|e| panic!("read head/{filename}: {e}"));

            let changed_ranges = differ::diff_contents(&base, &head);
            let diff_ranges = differ::diff_contents_extended(&base, &head);
            let diff_text = differ::build_diff_text(&base, &head);

            DiffFile {
                path: PathBuf::from(filename),
                head_content: head,
                base_content: base,
                changed_line_ranges: changed_ranges,
                diff_ranges,
                diff_text,
                status: FileStatus::Modified,
            }
        })
        .collect();

    (diff_files, manifest, scenario_root)
}

// ── CLI arg parsing ─────────────────────────────────────────────────────────

fn parse_scenario_arg() -> Option<String> {
    let args: Vec<String> = std::env::args().collect();
    for i in 0..args.len() {
        if args[i] == "--scenario" {
            return args.get(i + 1).cloned();
        }
    }
    None
}

// ── Classification ──────────────────────────────────────────────────────────

struct ClassifiedFile {
    filename: String,
    diff_file: DiffFile,
    hunks: Vec<ClassifiedHunk>,
}

fn classify_all(diff_files: Vec<DiffFile>, repo_root: &std::path::Path) -> Vec<ClassifiedFile> {
    let rules = rules::all_rules();
    let classifier = Classifier::new(rules);

    // Phase 1: per-hunk classification, retaining mapped files for the cross-hunk pass.
    let mut mapped_files: Vec<MappedFile> = Vec::with_capacity(diff_files.len());
    let mut per_file_hunks: Vec<Vec<ClassifiedHunk>> = Vec::with_capacity(diff_files.len());
    for df in &diff_files {
        let mapped = match languages::detect_language(&df.path) {
            Some(syntax) => mapper::map_diff_to_ast(df, syntax.as_ref()).expect("map_diff_to_ast"),
            None => mapper::map_diff_without_ast(df),
        };
        let hunks: Vec<ClassifiedHunk> = mapped
            .hunks
            .iter()
            .map(|h| classifier.classify_hunk(h))
            .collect();
        per_file_hunks.push(hunks);
        mapped_files.push(mapped);
    }

    // Build a TaxClassifiedFile view for the cross-hunk context.
    let tax_files: Vec<TaxClassifiedFile> = diff_files
        .iter()
        .zip(per_file_hunks.iter())
        .zip(mapped_files.iter())
        .map(|((df, hunks), mapped)| TaxClassifiedFile {
            path: df.path.display().to_string(),
            language: mapped.language.clone(),
            hunks: hunks.clone(),
            max_priority: hunks
                .iter()
                .map(|h| h.priority)
                .max()
                .unwrap_or(Priority::Filtered),
            package: None,
        })
        .collect();

    // Phase 2: cross-hunk analysis.
    let cross_rules = rules::cross_hunk::all_cross_hunk_rules();
    let cross_ctx = CrossHunkContext {
        mapped_files: &mapped_files,
        classified_files: &tax_files,
        diff_files: &diff_files,
        repo_root: Some(repo_root),
    };
    for rule in &cross_rules {
        for finding in rule.check(&cross_ctx) {
            if let Some(idx) = diff_files
                .iter()
                .position(|df| df.path.display().to_string() == finding.target_file_path)
            {
                per_file_hunks[idx].push(finding.hunk);
            }
        }
    }

    diff_files
        .into_iter()
        .zip(per_file_hunks)
        .map(|(df, hunks)| {
            let filename = df.path.display().to_string();
            ClassifiedFile {
                filename,
                diff_file: df,
                hunks,
            }
        })
        .collect()
}

// ── Hunk merging ────────────────────────────────────────────────────────────

/// Adjacent flagged hunks with the same winning rule merged into one entry.
struct MergedHunk {
    start_line: u32,
    end_line: u32,
    category: ChangeCategory,
    priority: Priority,
    rule_id: String,
    rule_name: String,
    diff_text: String,
    enclosing: Option<String>,
}

/// Filter and merge consecutive hunks that share the same winning rule
/// and are within 5 lines of each other (truly adjacent, not just same rule).
fn merge_adjacent_hunks(hunks: &[ClassifiedHunk]) -> (Vec<MergedHunk>, usize) {
    let mut merged: Vec<MergedHunk> = Vec::new();
    let mut skipped = 0;

    for hunk in hunks {
        if hunk.priority < Priority::Moderate || hunk.patterns.is_empty() {
            skipped += 1;
            continue;
        }

        let pat = &hunk.patterns[0];

        // Merge into previous group if same winning rule AND within 5 lines
        if let Some(last) = merged.last_mut() {
            if last.rule_id == pat.pattern_id && hunk.start_line <= last.end_line + 5 {
                last.end_line = hunk.end_line;
                last.diff_text.push_str(&hunk.diff_text);
                last.diff_text.push('\n');
                continue;
            }
        }

        // Start a new group
        let enclosing = hunk
            .enclosing_symbol
            .clone()
            .or_else(|| hunk.enclosing_node.clone());
        let mut diff_text = hunk.diff_text.clone();
        diff_text.push('\n');

        merged.push(MergedHunk {
            start_line: hunk.start_line,
            end_line: hunk.end_line,
            category: hunk.category,
            priority: hunk.priority,
            rule_id: pat.pattern_id.clone(),
            rule_name: pat.pattern_name.clone(),
            diff_text,
            enclosing,
        });
    }

    (merged, skipped)
}

// ── Prompt generation ───────────────────────────────────────────────────────

fn build_naive_prompt(classified_files: &[ClassifiedFile]) -> String {
    let mut prompt = String::from(
        "You are an expert code reviewer. Review the following diff for bugs, security issues, \
         and correctness problems. For each issue found, specify the file, line number, and \
         a clear description of the problem.\n\n",
    );

    for cf in classified_files {
        prompt.push_str(&format!(
            "## {}\n```diff\n{}\n```\n\n",
            cf.filename, cf.diff_file.diff_text
        ));
    }

    prompt.push_str("Identify all bugs and issues. Be specific about file names and line numbers.");
    prompt
}

struct DiffSiftPrompt {
    system: String,
    user_prompt: String,
    flagged_hunk_count: usize,
    skipped_hunk_count: usize,
}

struct SummaryJsonPrompt {
    system: String,
    user_prompt: String,
    flagged_hunk_count: usize,
    skipped_hunk_count: usize,
}

fn build_diffsift_prompt(classified_files: &[ClassifiedFile]) -> DiffSiftPrompt {
    let engine = TemplateEngine::new();
    let system = engine.render_system().expect("render system prompt");

    let mut flagged_count = 0;
    let mut skipped_count = 0;

    // Build focus areas grouped by file, merging adjacent hunks with same rule
    let mut focus_section = String::new();
    let mut file_merged: Vec<(&str, Vec<MergedHunk>)> = Vec::new();
    for cf in classified_files {
        let (merged, skipped) = merge_adjacent_hunks(&cf.hunks);
        skipped_count += skipped;
        flagged_count += merged.len();
        if !merged.is_empty() {
            file_merged.push((&cf.filename, merged));
        }
    }

    for (filename, merged) in &file_merged {
        focus_section.push_str(&format!("### {}\n", filename));
        for mh in merged {
            focus_section.push_str(&format!(
                "- L{}-{} [{:?}/{:?}] {}: {}\n",
                mh.start_line, mh.end_line, mh.category, mh.priority, mh.rule_id, mh.rule_name,
            ));
        }
        focus_section.push('\n');
    }

    // Build prompt: focus areas grouped by file + merged per-hunk diffs
    let mut user_prompt = String::new();
    user_prompt
        .push_str("Review this multi-file diff. Static analysis has flagged specific areas.\n\n");

    user_prompt.push_str(&format!(
        "## Focus areas ({} flagged, {} skipped as low-priority)\n\n",
        flagged_count, skipped_count,
    ));
    user_prompt.push_str(&focus_section);

    user_prompt.push_str("## Diff\n\n");
    for (filename, merged) in &file_merged {
        let focused_diff: String = merged.iter().map(|mh| mh.diff_text.as_str()).collect();
        user_prompt.push_str(&format!(
            "### {}\n```diff\n{}\n```\n\n",
            filename, focused_diff
        ));
    }

    user_prompt.push_str(
        "For each focus area, identify specific bugs or correctness issues. \
        If a flagged area is fine, say \"LGTM\". Be specific about file names and line numbers.",
    );

    DiffSiftPrompt {
        system,
        user_prompt,
        flagged_hunk_count: flagged_count,
        skipped_hunk_count: skipped_count,
    }
}

fn build_summary_json_prompt(classified_files: &[ClassifiedFile]) -> SummaryJsonPrompt {
    let engine = TemplateEngine::new();
    let system = engine.render_system().expect("render system prompt");

    let mut flagged_count = 0;
    let mut skipped_count = 0;

    // Merge adjacent hunks per file, then build JSON focus entries
    let mut file_merged: Vec<(&str, Vec<MergedHunk>)> = Vec::new();
    for cf in classified_files {
        let (merged, skipped) = merge_adjacent_hunks(&cf.hunks);
        skipped_count += skipped;
        flagged_count += merged.len();
        if !merged.is_empty() {
            file_merged.push((&cf.filename, merged));
        }
    }

    let mut file_sections = Vec::new();
    for (filename, merged) in &file_merged {
        let entries: Vec<String> = merged
            .iter()
            .map(|mh| {
                let mut entry = format!(
                    r#"    {{"lines":[{},{}],"category":"{:?}","priority":"{:?}","rule":"{}: {}""#,
                    mh.start_line, mh.end_line, mh.category, mh.priority, mh.rule_id, mh.rule_name,
                );
                if let Some(sym) = &mh.enclosing {
                    entry.push_str(&format!(r#","in":"{}""#, sym));
                }
                entry.push('}');
                entry
            })
            .collect();
        file_sections.push(format!(
            "  \"{}\":[\n{}\n  ]",
            filename,
            entries.join(",\n"),
        ));
    }

    let mut user_prompt = String::new();
    user_prompt.push_str(
        "Review this diff. Static analysis has identified focus areas listed below as JSON.\n\n",
    );
    user_prompt.push_str(&format!(
        "## Focus areas ({} flagged, {} skipped as low-priority)\n\n```json\n{{\n{}\n}}\n```\n\n",
        flagged_count,
        skipped_count,
        file_sections.join(",\n"),
    ));

    user_prompt.push_str("## Diff\n\n");
    for (filename, merged) in &file_merged {
        let focused_diff: String = merged.iter().map(|mh| mh.diff_text.as_str()).collect();
        user_prompt.push_str(&format!(
            "### {}\n```diff\n{}\n```\n\n",
            filename, focused_diff
        ));
    }

    user_prompt.push_str(
        "For each focus area, identify specific bugs or correctness issues. \
         If a flagged area is fine, say \"LGTM\". Be specific about file names and line numbers.",
    );

    SummaryJsonPrompt {
        system,
        user_prompt,
        flagged_hunk_count: flagged_count,
        skipped_hunk_count: skipped_count,
    }
}

// ── Token estimation ────────────────────────────────────────────────────────

fn estimate_tokens(text: &str) -> usize {
    text.len() / 4 // chars/4 heuristic
}

fn estimate_cost_usd(input_tokens: usize, output_tokens: usize) -> f64 {
    // Sonnet pricing: $3/M input, $15/M output
    let input_cost = (input_tokens as f64 / 1_000_000.0) * 3.0;
    let output_cost = (output_tokens as f64 / 1_000_000.0) * 15.0;
    input_cost + output_cost
}

// ── LLM provider detection ─────────────────────────────────────────────────

struct ProviderConfig {
    provider: Box<dyn ReviewProvider>,
    model: String,
    name: String,
}

fn detect_provider() -> Option<ProviderConfig> {
    // Allow explicit override via DIFFSIFT_PROVIDER
    let forced = std::env::var("DIFFSIFT_PROVIDER").ok();

    // Try providers in priority order (Anthropic first)
    let providers: Vec<(&str, &str, &str)> = vec![
        // (name, env_var, default_model)
        ("anthropic", "ANTHROPIC_API_KEY", "claude-sonnet-4-20250514"),
        ("openai", "OPENAI_API_KEY", "gpt-4o"),
        ("gemini", "GEMINI_API_KEY", "gemini-2.0-flash"),
        ("deepseek", "DEEPSEEK_API_KEY", "deepseek-chat"),
        ("mistral", "MISTRAL_API_KEY", "mistral-large-latest"),
    ];

    for (name, env_var, default_model) in &providers {
        if let Some(ref forced_name) = forced {
            if forced_name != name {
                continue;
            }
        }

        if let Ok(api_key) = std::env::var(env_var) {
            let model =
                std::env::var("DIFFSIFT_MODEL").unwrap_or_else(|_| default_model.to_string());

            let provider: Box<dyn ReviewProvider> = match *name {
                "anthropic" => Box::new(AnthropicProvider::new(api_key, None)),
                "openai" => Box::new(OpenAiProvider::new(api_key, None)),
                "gemini" => Box::new(OpenAiProvider::new(
                    api_key,
                    Some("https://generativelanguage.googleapis.com/v1beta/openai".to_string()),
                )),
                "deepseek" => Box::new(OpenAiProvider::new(
                    api_key,
                    Some("https://api.deepseek.com".to_string()),
                )),
                "mistral" => Box::new(OpenAiProvider::new(
                    api_key,
                    Some("https://api.mistral.ai".to_string()),
                )),
                _ => continue,
            };

            return Some(ProviderConfig {
                provider,
                model,
                name: name.to_string(),
            });
        }
    }

    None
}

// ── LLM comparison ─────────────────────────────────────────────────────────

async fn run_llm_comparison(
    naive_prompt: &str,
    diffsift: &DiffSiftPrompt,
    summary_json: &SummaryJsonPrompt,
    manifest: &Manifest,
    config: &ProviderConfig,
) {
    let provider = &config.provider;
    let model = &config.model;

    println!("\n{}", "=".repeat(60));
    println!(
        "  LLM COMPARISON (provider: {}, model: {})",
        config.name, model
    );
    println!("{}\n", "=".repeat(60));

    // ── Naive approach ──────────────────────────────────────────────────
    println!("Running naive approach...");
    let naive_system = "You are an expert code reviewer. Review code diffs for bugs and issues.";
    let naive_response = provider
        .review(naive_system, naive_prompt, model)
        .await
        .expect("naive LLM call");

    println!(
        "  Naive response: {} tokens in, {} tokens out",
        naive_response.input_tokens, naive_response.output_tokens
    );

    let naive_bugs_found = score_response(&naive_response.content, &manifest.bugs);
    println!(
        "  Bugs found: {}/{}",
        naive_bugs_found.len(),
        manifest.bugs.len()
    );
    for bug in &manifest.bugs {
        let found = naive_bugs_found.contains(&bug.id);
        println!(
            "    {} {}: {}",
            if found { "[x]" } else { "[ ]" },
            bug.id,
            bug.description
        );
    }

    // ── DiffSift approach ───────────────────────────────────────────────
    println!(
        "\nRunning DiffSift approach (single prompt, {} flagged hunks)...",
        diffsift.flagged_hunk_count
    );

    let diffsift_response = provider
        .review(&diffsift.system, &diffsift.user_prompt, model)
        .await
        .expect("DiffSift LLM call");

    println!(
        "  DiffSift response: {} tokens in, {} tokens out",
        diffsift_response.input_tokens, diffsift_response.output_tokens
    );

    let diffsift_bugs_found = score_response(&diffsift_response.content, &manifest.bugs);
    println!(
        "  Bugs found: {}/{}",
        diffsift_bugs_found.len(),
        manifest.bugs.len()
    );
    for bug in &manifest.bugs {
        let found = diffsift_bugs_found.contains(&bug.id);
        println!(
            "    {} {}: {}",
            if found { "[x]" } else { "[ ]" },
            bug.id,
            bug.description
        );
    }

    // ── Summary-JSON approach ───────────────────────────────────────────
    println!(
        "\nRunning summary-json approach (compact focus, {} flagged hunks)...",
        summary_json.flagged_hunk_count
    );

    let summary_json_response = provider
        .review(&summary_json.system, &summary_json.user_prompt, model)
        .await
        .expect("summary-json LLM call");

    println!(
        "  Summary-json response: {} tokens in, {} tokens out",
        summary_json_response.input_tokens, summary_json_response.output_tokens
    );

    let summary_json_bugs_found = score_response(&summary_json_response.content, &manifest.bugs);
    println!(
        "  Bugs found: {}/{}",
        summary_json_bugs_found.len(),
        manifest.bugs.len()
    );
    for bug in &manifest.bugs {
        let found = summary_json_bugs_found.contains(&bug.id);
        println!(
            "    {} {}: {}",
            if found { "[x]" } else { "[ ]" },
            bug.id,
            bug.description
        );
    }

    // ── Summary ─────────────────────────────────────────────────────────
    let naive_total = naive_response.input_tokens + naive_response.output_tokens;
    let diffsift_total = diffsift_response.input_tokens + diffsift_response.output_tokens;
    let summary_json_total =
        summary_json_response.input_tokens + summary_json_response.output_tokens;

    let naive_cost = estimate_cost_usd(
        naive_response.input_tokens as usize,
        naive_response.output_tokens as usize,
    );
    let diffsift_cost = estimate_cost_usd(
        diffsift_response.input_tokens as usize,
        diffsift_response.output_tokens as usize,
    );
    let summary_json_cost = estimate_cost_usd(
        summary_json_response.input_tokens as usize,
        summary_json_response.output_tokens as usize,
    );

    println!("\n--- LLM COMPARISON SUMMARY ---");
    println!("                 Naive      DiffSift   Summary-JSON");
    println!(
        "  Tokens:        {:<10} {:<10} {}",
        naive_total, diffsift_total, summary_json_total
    );
    println!(
        "  Est. cost:     ${:<9.4} ${:<9.4} ${:.4}",
        naive_cost, diffsift_cost, summary_json_cost
    );
    println!(
        "  Bugs found:    {}/{}        {}/{}        {}/{}",
        naive_bugs_found.len(),
        manifest.bugs.len(),
        diffsift_bugs_found.len(),
        manifest.bugs.len(),
        summary_json_bugs_found.len(),
        manifest.bugs.len(),
    );
}

/// Score LLM response by checking which bugs' keywords appear in the text.
/// A bug is "found" if at least 2 of its keywords appear.
fn score_response(response: &str, bugs: &[BugDef]) -> Vec<String> {
    let response_lower = response.to_lowercase();
    bugs.iter()
        .filter(|bug| {
            let matches = bug
                .search_keywords
                .iter()
                .filter(|kw| response_lower.contains(&kw.to_lowercase()))
                .count();
            matches >= 2
        })
        .map(|bug| bug.id.clone())
        .collect()
}

// ── Per-scenario results ────────────────────────────────────────────────────

#[allow(dead_code)]
struct ScenarioResult {
    name: String,
    total_hunks: usize,
    high_hunks: usize,
    moderate_hunks: usize,
    low_hunks: usize,
    filtered_hunks: usize,
    naive_tokens: usize,
    diffsift_tokens: usize,
    summary_json_tokens: usize,
    total_bugs: usize,
    detected_bugs: usize,
}

impl ScenarioResult {
    fn noise_pct(&self) -> f64 {
        if self.total_hunks == 0 {
            return 0.0;
        }
        let noise = self.low_hunks + self.filtered_hunks;
        (noise as f64 / self.total_hunks as f64) * 100.0
    }

    fn token_savings_pct(&self) -> f64 {
        if self.naive_tokens == 0 {
            return 0.0;
        }
        (1.0 - self.diffsift_tokens as f64 / self.naive_tokens as f64) * 100.0
    }
}

// ── Main ────────────────────────────────────────────────────────────────────

#[tokio::main]
async fn main() {
    println!("DiffSift Comparison Benchmark");
    println!("=============================\n");

    let all_scenarios = list_scenarios();
    let selected = parse_scenario_arg();

    let scenarios: Vec<&str> = match &selected {
        Some(name) => {
            if !all_scenarios.contains(name) {
                eprintln!(
                    "Unknown scenario '{}'. Available: {}",
                    name,
                    all_scenarios.join(", ")
                );
                std::process::exit(1);
            }
            vec![name.as_str()]
        }
        None => all_scenarios.iter().map(|s| s.as_str()).collect(),
    };

    println!(
        "Scenarios: {} ({})\n",
        scenarios.len(),
        scenarios.join(", ")
    );

    let mut results: Vec<ScenarioResult> = Vec::new();

    for scenario_name in &scenarios {
        println!("{}", "=".repeat(70));
        println!("  SCENARIO: {}", scenario_name);
        println!("{}\n", "=".repeat(70));

        // 1. Load and classify
        println!("Loading fixtures...");
        let (diff_files, manifest, scenario_root) = load_scenario(scenario_name);

        println!(
            "Classifying {} files for \"{}\"...",
            diff_files.len(),
            manifest.name
        );
        let classified = classify_all(diff_files, &scenario_root);

        // 2. Print classification summary
        println!("\n--- CLASSIFICATION SUMMARY ---\n");

        let mut total_hunks = 0;
        let mut high_hunks = 0;
        let mut moderate_hunks = 0;
        let mut low_hunks = 0;
        let mut filtered_hunks = 0;

        for cf in &classified {
            println!("  {} ({} hunks)", cf.filename, cf.hunks.len());
            for hunk in &cf.hunks {
                let patterns: Vec<&str> = hunk
                    .patterns
                    .iter()
                    .map(|p| p.pattern_id.as_str())
                    .collect();
                println!(
                    "    L{}-{}: {:?} ({:?}) [{}]",
                    hunk.start_line,
                    hunk.end_line,
                    hunk.category,
                    hunk.priority,
                    patterns.join(", ")
                );
                total_hunks += 1;
                match hunk.priority {
                    Priority::High => high_hunks += 1,
                    Priority::Moderate => moderate_hunks += 1,
                    Priority::Low => low_hunks += 1,
                    Priority::Filtered => filtered_hunks += 1,
                }
            }
            println!();
        }

        println!("  Total: {} hunks", total_hunks);
        println!("    High:     {}", high_hunks);
        println!("    Moderate: {}", moderate_hunks);
        println!("    Low:      {}", low_hunks);
        println!("    Filtered: {}", filtered_hunks);

        // 3. Generate prompts
        println!("\n--- PROMPT COMPARISON ---\n");

        let naive_prompt = build_naive_prompt(&classified);
        let diffsift = build_diffsift_prompt(&classified);
        let summary_json = build_summary_json_prompt(&classified);

        let naive_tokens = estimate_tokens(&naive_prompt);
        let diffsift_tokens =
            estimate_tokens(&diffsift.user_prompt) + estimate_tokens(&diffsift.system);
        let summary_json_tokens =
            estimate_tokens(&summary_json.user_prompt) + estimate_tokens(&summary_json.system);
        let naive_output_est = 500;
        let diffsift_output_est = 800; // more structured output since we ask per-hunk
        let summary_json_output_est = 800;

        let naive_cost = estimate_cost_usd(naive_tokens, naive_output_est);
        let diffsift_cost = estimate_cost_usd(diffsift_tokens, diffsift_output_est);
        let summary_json_cost = estimate_cost_usd(summary_json_tokens, summary_json_output_est);

        println!("  Naive approach:");
        println!(
            "    Single prompt: {} chars, ~{} tokens",
            naive_prompt.len(),
            naive_tokens
        );
        println!("    Estimated cost: ${:.4}", naive_cost);
        println!();
        println!("  DiffSift approach:");
        println!(
            "    Single prompt: {} chars, ~{} tokens",
            diffsift.user_prompt.len(),
            diffsift_tokens
        );
        println!(
            "    Flagged hunks: {} (HIGH + MODERATE)",
            diffsift.flagged_hunk_count
        );
        println!(
            "    Skipped hunks: {} (LOW + Filtered)",
            diffsift.skipped_hunk_count
        );
        println!("    Estimated cost: ${:.4}", diffsift_cost);
        println!();
        println!("  Summary-JSON approach:");
        println!(
            "    Single prompt: {} chars, ~{} tokens",
            summary_json.user_prompt.len(),
            summary_json_tokens
        );
        println!(
            "    Flagged hunks: {} (HIGH + MODERATE)",
            summary_json.flagged_hunk_count
        );
        println!(
            "    Skipped hunks: {} (LOW + Filtered)",
            summary_json.skipped_hunk_count
        );
        println!("    Estimated cost: ${:.4}", summary_json_cost);

        // Measure full --format json size by serializing all hunks
        let all_hunks: Vec<&ClassifiedHunk> = classified.iter().flat_map(|cf| &cf.hunks).collect();
        let full_json = serde_json::to_string_pretty(&all_hunks).expect("serialize hunks");
        let full_json_tokens = estimate_tokens(&full_json);
        println!(
            "    Savings vs --format json: ~{:.0}% ({} vs {} tokens)",
            (1.0 - summary_json_tokens as f64 / full_json_tokens as f64) * 100.0,
            summary_json_tokens,
            full_json_tokens,
        );

        // 4. Bug detection ground truth
        println!("\n--- GROUND TRUTH ---\n");
        let bug_count = manifest.bugs.len();
        let file_count = manifest.files.len();
        println!("  {} bugs planted across {} files:", bug_count, file_count);
        let mut detected_count = 0;
        for bug in &manifest.bugs {
            // Check if this bug's file has a HIGH/MODERATE hunk that matches
            let detected = classified.iter().any(|cf| {
                cf.filename == bug.file
                    && cf.hunks.iter().any(|h| {
                        h.priority >= Priority::Moderate
                            && h.patterns.iter().any(|p| p.pattern_id == bug.expected_rule)
                    })
            });
            if detected {
                detected_count += 1;
            }
            let status = if detected { "DETECTED" } else { "MISSED" };
            println!(
                "    {} [{}] {} in {} — expected {}",
                status, bug.id, bug.description, bug.file, bug.expected_rule
            );
        }

        // Collect result for summary table
        results.push(ScenarioResult {
            name: scenario_name.to_string(),
            total_hunks,
            high_hunks,
            moderate_hunks,
            low_hunks,
            filtered_hunks,
            naive_tokens,
            diffsift_tokens,
            summary_json_tokens,
            total_bugs: bug_count,
            detected_bugs: detected_count,
        });

        // 5. Optional LLM comparison
        if let Some(config) = detect_provider() {
            println!(
                "\n  Using provider: {} (model: {})",
                config.name, config.model
            );
            run_llm_comparison(&naive_prompt, &diffsift, &summary_json, &manifest, &config).await;
        } else if scenarios.len() == 1 {
            // Only print LLM skip message for single-scenario runs
            println!("\n--- LLM COMPARISON SKIPPED ---");
            println!("  Set one of these env vars to run LLM comparison:");
            println!("    ANTHROPIC_API_KEY  (default, uses claude-sonnet-4-20250514)");
            println!("    OPENAI_API_KEY     (uses gpt-4o)");
            println!("    GEMINI_API_KEY     (uses gemini-2.0-flash)");
            println!("    DEEPSEEK_API_KEY   (uses deepseek-chat)");
            println!("    MISTRAL_API_KEY    (uses mistral-large-latest)");
            println!();
            println!("  Override provider: DIFFSIFT_PROVIDER=openai");
            println!("  Override model:    DIFFSIFT_MODEL=gpt-4-turbo");
        }

        println!();
    }

    // ── Cross-scenario summary table ────────────────────────────────────
    if results.len() > 1 {
        println!("{}", "=".repeat(70));
        println!("  CROSS-SCENARIO SUMMARY");
        println!("{}\n", "=".repeat(70));

        // Table header
        println!(
            "  {:<20} {:>5} {:>8} {:>14} {:>7}",
            "Scenario", "Hunks", "Noise%", "Token Savings", "Bugs"
        );
        println!("  {}", "-".repeat(58));

        for r in &results {
            let bug_str = format!("{}/{}", r.detected_bugs, r.total_bugs);
            println!(
                "  {:<20} {:>5} {:>7.0}% {:>13.0}% {:>7}",
                r.name,
                r.total_hunks,
                r.noise_pct(),
                r.token_savings_pct(),
                bug_str,
            );
        }

        println!();

        // Averages
        let avg_noise: f64 =
            results.iter().map(|r| r.noise_pct()).sum::<f64>() / results.len() as f64;
        let avg_savings: f64 =
            results.iter().map(|r| r.token_savings_pct()).sum::<f64>() / results.len() as f64;
        let total_bugs_detected: usize = results.iter().map(|r| r.detected_bugs).sum();
        let total_bugs: usize = results.iter().map(|r| r.total_bugs).sum();

        println!(
            "  {:<20} {:>5} {:>7.0}% {:>13.0}% {:>4}/{:<3}",
            "AVERAGE/TOTAL", "", avg_noise, avg_savings, total_bugs_detected, total_bugs,
        );

        if detect_provider().is_none() {
            println!(
                "\n  LLM comparison skipped — set ANTHROPIC_API_KEY or OPENAI_API_KEY to enable."
            );
        }
    }
}
