use anyhow::Result;
use clap::Args;
use frontend_core::fix::FixSource;
use std::path::PathBuf;

use frontend_fix_engine::engine as fix_engine;
use frontend_fix_engine::goose_client;
use frontend_fix_engine::llm_client;
use frontend_fix_engine::registry::FixContextRegistry;
use patternfly_fix_context::PatternFlyV5ToV6Context;

#[derive(Args)]
pub struct FixOpts {
    /// Path to the project to fix.
    pub project: PathBuf,

    /// Path to Konveyor analysis output (YAML or JSON).
    #[arg(short, long)]
    pub input: PathBuf,

    /// Preview changes without writing (default behavior).
    #[arg(long)]
    pub dry_run: bool,

    /// Apply changes to disk.
    #[arg(long)]
    pub apply: bool,

    /// LLM provider for AI-assisted fixes: "goose" (local goose CLI) or "openai" (remote endpoint).
    #[arg(long)]
    pub llm_provider: Option<String>,

    /// LLM endpoint URL (required when --llm-provider=openai).
    #[arg(long)]
    pub llm_endpoint: Option<String>,

    /// Only process fixes for specific rule IDs (comma-separated).
    #[arg(long)]
    pub rules: Option<String>,

    /// Path to external fix strategies JSON file.
    /// Generated by semver-analyzer's `konveyor` command as `fix-strategies.json`.
    /// When provided, these strategies override any rule-adjacent strategies.
    #[arg(long)]
    pub strategies: Option<PathBuf>,

    /// Path to rule-adjacent fix strategies JSON file.
    /// Bundled alongside the rule YAML files (e.g., rules/patternfly-v5-to-v6/fix-strategies.json).
    /// Maps handwritten rule IDs to their fix strategies.
    #[arg(long)]
    pub rules_strategies: Option<PathBuf>,

    /// Directory to save goose prompts and responses for debugging.
    #[arg(long)]
    pub log_dir: Option<PathBuf>,

    /// Show detailed output.
    #[arg(short, long)]
    pub verbose: bool,
}

pub async fn run(opts: FixOpts) -> Result<()> {
    let project = opts.project.canonicalize()?;
    let input_content = std::fs::read_to_string(&opts.input)?;

    // Parse the Konveyor output (try JSON first, then YAML)
    let output: Vec<konveyor_core::report::RuleSet> = {
        let trimmed = input_content.trim_start();
        if trimmed.starts_with('[') || trimmed.starts_with('{') {
            serde_json::from_str(&input_content)?
        } else {
            // Parse YAML using yaml_serde (pure-Rust yaml-rust2 backend).
            yaml_serde::from_str::<Vec<konveyor_core::report::RuleSet>>(&input_content)?
        }
    };

    // Build fix context registry with framework-specific LLM guidance
    let mut context_registry = FixContextRegistry::new();
    context_registry.register(Box::new(PatternFlyV5ToV6Context::new()));

    // Resolve the fix context from the first ruleset name in the analysis output.
    // All rulesets in a single analysis run typically share the same framework.
    let ruleset_name = output.first().map(|rs| rs.name.as_str()).unwrap_or("");
    let fix_context = context_registry.get(ruleset_name);

    let total_violations: usize = output.iter().map(|rs| rs.violations.len()).sum();
    let total_incidents: usize = output
        .iter()
        .flat_map(|rs| rs.violations.values())
        .map(|v| v.incidents.len())
        .sum();
    let total_errors: usize = output.iter().map(|rs| rs.errors.len()).sum();

    eprintln!(
        "Loaded {} violations with {} incidents",
        total_violations, total_incidents
    );

    // Surface provider errors (e.g., files that could not be parsed).
    // These are populated by kantra from the provider's EvaluateResponse.error field.
    if total_errors > 0 {
        eprintln!("\n── Provider errors ({} rules affected) ──", total_errors);
        // Deduplicate error messages since many rules may report the same parse errors
        let mut seen_errors = std::collections::HashSet::new();
        for rs in &output {
            for error_msg in rs.errors.values() {
                if seen_errors.insert(error_msg.clone()) {
                    eprintln!("  {}", error_msg);
                }
            }
        }
        eprintln!();
    }

    // Load and merge fix strategies from all sources.
    // Order: rules-adjacent strategies first, then external strategies override.
    let mut merged_strategies = std::collections::BTreeMap::new();

    // 1. Load rule-adjacent strategies (bundled with rules)
    if let Some(ref rules_strategies_path) = opts.rules_strategies {
        eprintln!(
            "Loading rule strategies from {}",
            rules_strategies_path.display()
        );
        match frontend_core::fix::load_strategies_from_json(rules_strategies_path) {
            Ok(strats) => {
                eprintln!("  Loaded {} rule strategies", strats.len());
                merged_strategies.extend(strats);
            }
            Err(e) => {
                eprintln!("  WARNING: Failed to load rule strategies: {}", e);
            }
        }
    }

    // 2. Load external strategies (--strategies flag, from semver-analyzer)
    //    These override rule-adjacent strategies for the same rule ID.
    if let Some(ref strategies_path) = opts.strategies {
        eprintln!(
            "Loading external strategies from {}",
            strategies_path.display()
        );
        match frontend_core::fix::load_strategies_from_json(strategies_path) {
            Ok(strats) => {
                eprintln!("  Loaded {} external strategies", strats.len());
                merged_strategies.extend(strats);
            }
            Err(e) => {
                eprintln!("  WARNING: Failed to load external strategies: {}", e);
            }
        }
    }

    if !merged_strategies.is_empty() {
        eprintln!("  Total merged strategies: {}", merged_strategies.len());
    }

    // Phase 1: Plan fixes
    eprintln!("Planning fixes...");
    let mut plan = fix_engine::plan_fixes(&output, &project, &merged_strategies)?;

    let pattern_fix_count: usize = plan
        .files
        .values()
        .flat_map(|fixes| fixes.iter())
        .filter(|f| f.source == FixSource::Pattern)
        .count();
    let pattern_edit_count: usize = plan
        .files
        .values()
        .flat_map(|fixes| fixes.iter())
        .filter(|f| f.source == FixSource::Pattern)
        .flat_map(|f| f.edits.iter())
        .count();

    eprintln!(
        "  Pattern-based: {} fixes ({} edits) across {} files",
        pattern_fix_count,
        pattern_edit_count,
        plan.files.len()
    );
    eprintln!("  Manual review: {} incidents", plan.manual.len());
    eprintln!("  LLM-assisted:  {} incidents", plan.pending_llm.len());

    // Phase 1b: Apply pattern-based fixes first (so LLM sees already-renamed code)
    if opts.apply && !plan.files.is_empty() {
        eprintln!("\nApplying pattern-based fixes...");
        let result = fix_engine::apply_fixes(&plan)?;
        eprintln!("  Files modified: {}", result.files_modified);
        eprintln!("  Edits applied:  {}", result.edits_applied);
        eprintln!("  Edits skipped:  {}", result.edits_skipped);
        if !result.errors.is_empty() {
            eprintln!("  Errors:");
            for err in &result.errors {
                eprintln!("    {}", err);
            }
        }
    } else if !opts.apply {
        // Preview mode — show diff for pattern-based fixes
        let diff = fix_engine::preview_fixes(&plan)?;
        if diff.is_empty() {
            eprintln!("\nNo pattern-based auto-fixable changes found.");
        } else {
            eprintln!("\nPlanned pattern-based changes (use --apply to write):\n");
            println!("{}", diff);
        }
    }

    // Phase 2: LLM-assisted fixes
    let llm_provider = opts.llm_provider.as_deref().or_else(|| {
        // Legacy: if --llm-endpoint is set without --llm-provider, assume openai
        if opts.llm_endpoint.is_some() {
            Some("openai")
        } else {
            None
        }
    });

    if !plan.pending_llm.is_empty() {
        match llm_provider {
            Some("goose") => {
                eprintln!(
                    "\nUsing goose for {} LLM-assisted fixes...",
                    plan.pending_llm.len()
                );

                if !opts.apply {
                    eprintln!("  (dry run — use --apply to let goose edit files directly)");
                    // In dry-run mode, just show what would be sent to goose
                    for req in &plan.pending_llm {
                        let file_name = req
                            .file_path
                            .file_name()
                            .map(|n| n.to_string_lossy().to_string())
                            .unwrap_or_default();
                        eprintln!(
                            "  Would fix: {} line {} [{}]",
                            file_name, req.line, req.rule_id
                        );
                    }
                } else {
                    let pending = std::mem::take(&mut plan.pending_llm);
                    let results = goose_client::run_all_goose_fixes(
                        &pending,
                        fix_context,
                        opts.verbose,
                        opts.log_dir.as_deref(),
                    );

                    let succeeded = results.iter().filter(|r| r.success).count();
                    let failed = results.iter().filter(|r| !r.success).count();
                    eprintln!(
                        "\n  Goose fixes: {} succeeded, {} failed",
                        succeeded, failed
                    );

                    // Move failures to manual review
                    for (result, requests) in results.iter().zip(
                        // Re-group by file to match results
                        {
                            let mut by_file: std::collections::BTreeMap<
                                PathBuf,
                                Vec<&frontend_core::fix::LlmFixRequest>,
                            > = std::collections::BTreeMap::new();
                            for req in &pending {
                                by_file.entry(req.file_path.clone()).or_default().push(req);
                            }
                            by_file.into_values().collect::<Vec<_>>()
                        },
                    ) {
                        if !result.success {
                            for req in requests {
                                plan.manual.push(frontend_core::fix::ManualFixItem {
                                    rule_id: req.rule_id.clone(),
                                    file_uri: req.file_uri.clone(),
                                    line: req.line,
                                    message: req.message.clone(),
                                    code_snip: req.code_snip.clone(),
                                });
                            }
                        }
                    }
                }
            }
            Some("openai") => {
                let endpoint = opts.llm_endpoint.as_deref().ok_or_else(|| {
                    anyhow::anyhow!("--llm-endpoint is required when using --llm-provider=openai")
                })?;

                if !plan.pending_llm.is_empty() {
                    eprintln!(
                        "\nSending {} incidents to LLM endpoint: {}",
                        plan.pending_llm.len(),
                        endpoint
                    );

                    let pending = std::mem::take(&mut plan.pending_llm);
                    let mut llm_fixes = 0;
                    let mut llm_errors = 0;

                    for request in &pending {
                        match llm_client::request_llm_fix(endpoint, request, fix_context).await {
                            Ok(fixes) => {
                                for fix in fixes {
                                    if !fix.edits.is_empty() {
                                        llm_fixes += 1;
                                        plan.files
                                            .entry(request.file_path.clone())
                                            .or_default()
                                            .push(fix);
                                    }
                                }
                            }
                            Err(e) => {
                                llm_errors += 1;
                                if opts.verbose {
                                    eprintln!(
                                        "  LLM error for {}:{} — {}",
                                        request.file_path.display(),
                                        request.line,
                                        e
                                    );
                                }
                                plan.manual.push(frontend_core::fix::ManualFixItem {
                                    rule_id: request.rule_id.clone(),
                                    file_uri: request.file_uri.clone(),
                                    line: request.line,
                                    message: request.message.clone(),
                                    code_snip: request.code_snip.clone(),
                                });
                            }
                        }
                    }

                    eprintln!("  LLM generated {} fixes, {} errors", llm_fixes, llm_errors);

                    // Apply LLM-generated edits
                    if opts.apply && !plan.files.is_empty() {
                        let result = fix_engine::apply_fixes(&plan)?;
                        eprintln!("  Applied {} LLM edits", result.edits_applied);
                    }
                }
            }
            Some(other) => {
                anyhow::bail!("Unknown LLM provider '{}'. Use 'goose' or 'openai'.", other);
            }
            None => {
                eprintln!(
                    "\n  {} incidents need LLM-assisted fixes.",
                    plan.pending_llm.len()
                );
                eprintln!("  Use --llm-provider goose to fix with local goose.");
                eprintln!("  Use --llm-provider openai --llm-endpoint <url> for remote LLM.");

                // Move to manual
                for request in std::mem::take(&mut plan.pending_llm) {
                    plan.manual.push(frontend_core::fix::ManualFixItem {
                        rule_id: request.rule_id,
                        file_uri: request.file_uri,
                        line: request.line,
                        message: request.message,
                        code_snip: request.code_snip,
                    });
                }
            }
        }
    }

    // Show manual review items
    if !plan.manual.is_empty() {
        eprintln!("\n── Manual review required ({}) ──", plan.manual.len());
        for item in &plan.manual {
            eprintln!(
                "  {} line {} [{}]",
                item.file_uri
                    .strip_prefix("file://")
                    .unwrap_or(&item.file_uri)
                    .split('/')
                    .next_back()
                    .unwrap_or("?"),
                item.line,
                item.rule_id,
            );
            if opts.verbose {
                eprintln!("    {}", item.message);
            }
        }
    }

    Ok(())
}
