//! Command handlers for `aphoria verify`.

use std::path::PathBuf;
use std::process::ExitCode;

use aphoria::claims_file::ClaimsFile;
use aphoria::extractors::ExtractorRegistry;
use aphoria::report::{format_verify_json, format_verify_table};
use aphoria::verify;
use aphoria::AphoriaConfig;

use crate::cli::VerifyCommands;

/// Dispatch a verify subcommand.
pub async fn handle_verify_command(command: VerifyCommands, config: &AphoriaConfig) -> ExitCode {
    match command {
        VerifyCommands::Run {
            path,
            format,
            exit_code,
            changed_only,
            show_unclaimed,
            claim,
            category,
        } => {
            handle_verify_run(
                path,
                format,
                exit_code,
                changed_only,
                show_unclaimed,
                claim,
                category,
                config,
            )
            .await
        }
        VerifyCommands::Map { path } => handle_verify_map(path, config).await,
    }
}

/// Run verification: extract observations, compare against claims.
#[allow(clippy::too_many_arguments)]
async fn handle_verify_run(
    path: PathBuf,
    format: String,
    exit_code: bool,
    changed_only: bool,
    show_unclaimed: bool,
    claim_filter: Vec<String>,
    category_filter: Option<String>,
    config: &AphoriaConfig,
) -> ExitCode {
    let project_root = path.canonicalize().unwrap_or(path);

    // 1. Load claims from .aphoria/claims.toml
    let claims_path = ClaimsFile::default_path(&project_root);
    let claims_file = match ClaimsFile::load(&claims_path) {
        Ok(cf) => cf,
        Err(e) => {
            eprintln!("Error loading claims file: {e}");
            return ExitCode::from(3);
        }
    };

    if claims_file.is_empty() {
        eprintln!("No claims found in {}", claims_path.display());
        eprintln!("Create claims with: aphoria claims create");
        return ExitCode::SUCCESS;
    }

    // 2. Filter claims by ID or category if specified
    let mut claims: Vec<aphoria::AuthoredClaim> = claims_file.claims;

    if !claim_filter.is_empty() {
        claims.retain(|c| claim_filter.contains(&c.id));
    }
    if let Some(ref cat) = category_filter {
        claims.retain(|c| c.category == *cat);
    }

    // 3. Walk the project and extract observations
    let files = if changed_only {
        match aphoria::walker::walk_staged_files(&project_root, config) {
            Ok(f) => f,
            Err(e) => {
                eprintln!("Error walking staged files: {e}");
                return ExitCode::from(3);
            }
        }
    } else {
        match aphoria::walker::walk_project(&project_root, config) {
            Ok(f) => f,
            Err(e) => {
                eprintln!("Error walking project: {e}");
                return ExitCode::from(3);
            }
        }
    };

    let registry = ExtractorRegistry::new(config);
    let mut all_observations = Vec::new();
    for file in &files {
        let content = match std::fs::read_to_string(&file.path) {
            Ok(c) => c,
            Err(_) => continue,
        };
        let obs =
            registry.extract_all(&file.path_segments, &content, file.language, &file.relative_path);
        all_observations.extend(obs);
    }

    // 4. Run verification
    let report = verify::verify_claims(&claims, &all_observations);

    // 5. Format and output
    let project_name = project_root.file_name().and_then(|n| n.to_str()).unwrap_or("project");

    let output = match format.as_str() {
        "json" => format_verify_json(&report, show_unclaimed),
        _ => format_verify_table(&report, project_name, show_unclaimed),
    };

    println!("{output}");

    // 6. Exit codes: 0=pass, 1=missing/unclaimed, 2=conflicts, 3=error
    if !exit_code {
        return ExitCode::SUCCESS;
    }

    if report.summary.conflict > 0 {
        ExitCode::from(2)
    } else if report.summary.missing > 0 {
        ExitCode::from(1)
    } else {
        ExitCode::SUCCESS
    }
}

/// Show the mapping between extractors and claims.
async fn handle_verify_map(path: PathBuf, config: &AphoriaConfig) -> ExitCode {
    let project_root = path.canonicalize().unwrap_or(path);

    // Load claims
    let claims_path = ClaimsFile::default_path(&project_root);
    let claims_file = match ClaimsFile::load(&claims_path) {
        Ok(cf) => cf,
        Err(e) => {
            eprintln!("Error loading claims file: {e}");
            return ExitCode::from(3);
        }
    };

    let registry = ExtractorRegistry::new(config);

    println!("Extractor → Claim Mapping");
    println!("{}", "=".repeat(60));
    println!();

    if claims_file.is_empty() {
        println!("No authored claims found.");
        println!();
        println!("Registered Extractors ({}):", registry.extractor_names().len());
        for name in &registry.extractor_names() {
            let preds = registry
                .extractors()
                .iter()
                .find(|e| e.name() == *name)
                .map(|e| e.verifiable_predicates())
                .unwrap_or_default();
            if preds.is_empty() {
                println!("  {name}  (no declared predicates)");
            } else {
                let pred_strs: Vec<String> =
                    preds.iter().map(|(tp, p)| format!("{tp}::{p}")).collect();
                println!("  {name}  [{}]", pred_strs.join(", "));
            }
        }
    } else {
        let map = verify::compute_extractor_claim_map(&claims_file.claims, registry.extractors());

        // Show per-claim coverage
        println!("Claim Coverage ({} active claims):", map.claim_mappings.len());
        println!();

        let mut covered = 0usize;
        for mapping in &map.claim_mappings {
            if mapping.covering_extractors.is_empty() {
                println!("  {} ({})  -> NO EXTRACTOR", mapping.claim_id, mapping.claim_tail_path);
            } else {
                println!(
                    "  {} ({})  -> {}",
                    mapping.claim_id,
                    mapping.claim_tail_path,
                    mapping.covering_extractors.join(", ")
                );
                covered += 1;
            }
        }

        println!();
        let total = map.claim_mappings.len();
        println!(
            "Coverage: {covered}/{total} claims have covering extractors ({:.0}%)",
            if total > 0 { (covered as f64 / total as f64) * 100.0 } else { 0.0 }
        );

        // Show extractors that declare predicates but have no matching claims
        if !map.unmatched_extractors.is_empty() {
            println!();
            println!(
                "Extractors with declared predicates but no matching claims ({}):",
                map.unmatched_extractors.len()
            );
            for ext in &map.unmatched_extractors {
                let pred_strs: Vec<String> =
                    ext.predicates.iter().map(|(tp, p)| format!("{tp}::{p}")).collect();
                println!("  {}  [{}]", ext.name, pred_strs.join(", "));
            }
        }
    }

    ExitCode::SUCCESS
}
