//! Auto-fix compiler warnings using machine-applicable suggestions from the compiler.
//!
//! Runs `cargo check --message-format=json` to get structured warnings with fix
//! suggestions, then converts them to Fix objects that the refactor pipeline applies.
//!
//! Supported warnings:
//! - `unused_imports`: remove the import line
//! - `unused_mut`: remove the `mut` keyword
//! - `unused_assignments`: remove the assignment
//! - `dead_code`: remove the function/method (line-range removal)

use std::path::Path;

use crate::code_audit::{AuditFinding, CodeAuditResult};
use crate::core::refactor::auto::{Fix, FixSafetyTier, Insertion, InsertionKind, SkippedFile};

/// A machine-applicable fix suggestion from the compiler.
#[derive(Debug, Clone)]
struct CompilerSuggestion {
    /// Warning code (e.g., "unused_imports", "dead_code").
    code: String,
    /// Relative file path.
    file: String,
    /// 1-indexed start line of the span to replace.
    line_start: usize,
    /// 1-indexed end line of the span to replace.
    line_end: usize,
    /// The text on the original line(s) to match for replacement.
    original_text: String,
    /// The replacement text (empty string = delete).
    replacement: String,
    /// Human-readable description.
    message: String,
}

/// Generate fixes for compiler warnings by running `cargo check` and parsing suggestions.
pub(crate) fn generate_compiler_warning_fixes(
    result: &CodeAuditResult,
    root: &Path,
    fixes: &mut Vec<Fix>,
    skipped: &mut Vec<SkippedFile>,
) {
    // Only run if there are compiler warning findings.
    let warning_count = result
        .findings
        .iter()
        .filter(|f| f.kind == AuditFinding::CompilerWarning)
        .count();

    if warning_count == 0 {
        return;
    }

    // Don't run cargo check if not a Rust project.
    if !root.join("Cargo.toml").exists() {
        return;
    }

    let suggestions = match run_cargo_check_for_suggestions(root) {
        Ok(s) => s,
        Err(e) => {
            skipped.push(SkippedFile {
                file: String::new(),
                reason: format!("Failed to run cargo check for fix suggestions: {}", e),
            });
            return;
        }
    };

    for suggestion in suggestions {
        let fix = match suggestion.code.as_str() {
            // For unused_imports: the compiler suggests removing the full line(s).
            // Use FunctionRemoval for line-range deletion.
            "unused_imports" => build_line_removal_fix(&suggestion),

            // For unused_mut, unused_assignments: use LineReplacement.
            "unused_mut" | "unused_variables" | "unused_assignments" => {
                build_line_replacement_fix(&suggestion)
            }

            // dead_code: use FunctionRemoval for the span range.
            "dead_code" => build_line_removal_fix(&suggestion),

            // Other warnings with suggestions: use LineReplacement if single-line.
            _ => {
                if suggestion.line_start == suggestion.line_end {
                    build_line_replacement_fix(&suggestion)
                } else {
                    // Multi-line replacement without specific handler — skip.
                    skipped.push(SkippedFile {
                        file: suggestion.file.clone(),
                        reason: format!(
                            "No fixer for multi-line {} warning at line {}",
                            suggestion.code, suggestion.line_start
                        ),
                    });
                    continue;
                }
            }
        };

        fixes.push(fix);
    }
}

/// Build a Fix that removes lines (for unused imports, dead code).
fn build_line_removal_fix(suggestion: &CompilerSuggestion) -> Fix {
    let mut ins = Insertion {
        kind: InsertionKind::FunctionRemoval {
            start_line: suggestion.line_start,
            end_line: suggestion.line_end,
        },
        finding: AuditFinding::CompilerWarning,
        // Compiler-suggested removals are safe — the compiler itself says this code is unused.
        safety_tier: FixSafetyTier::Safe,
        auto_apply: true,
        blocked_reason: None,
        preflight: None,
        code: String::new(),
        description: format!(
            "Remove {} (compiler: {})",
            suggestion.code, suggestion.message
        ),
    };

    // Override the default PlanOnly tier from FunctionRemoval.
    ins.safety_tier = FixSafetyTier::Safe;

    Fix {
        file: suggestion.file.clone(),
        required_methods: vec![],
        required_registrations: vec![],
        insertions: vec![ins],
        applied: false,
    }
}

/// Build a Fix that replaces text on a single line (for unused_mut, etc.).
fn build_line_replacement_fix(suggestion: &CompilerSuggestion) -> Fix {
    let ins = Insertion {
        kind: InsertionKind::LineReplacement {
            line: suggestion.line_start,
            old_text: suggestion.original_text.clone(),
            new_text: suggestion.replacement.clone(),
        },
        finding: AuditFinding::CompilerWarning,
        safety_tier: FixSafetyTier::Safe,
        auto_apply: true,
        blocked_reason: None,
        preflight: None,
        code: String::new(),
        description: format!("Fix {} (compiler: {})", suggestion.code, suggestion.message),
    };

    Fix {
        file: suggestion.file.clone(),
        required_methods: vec![],
        required_registrations: vec![],
        insertions: vec![ins],
        applied: false,
    }
}

/// Run `cargo check --message-format=json` and extract machine-applicable suggestions.
fn run_cargo_check_for_suggestions(root: &Path) -> Result<Vec<CompilerSuggestion>, String> {
    let output = std::process::Command::new("cargo")
        .args(["check", "--message-format=json"])
        .current_dir(root)
        .output()
        .map_err(|e| format!("Failed to run cargo check: {}", e))?;

    let stdout = String::from_utf8_lossy(&output.stdout);
    Ok(parse_suggestions(&stdout, root))
}

/// Parse cargo JSON output for machine-applicable suggestions.
fn parse_suggestions(stdout: &str, root: &Path) -> Vec<CompilerSuggestion> {
    let root_str = root.to_string_lossy();
    let mut suggestions = Vec::new();

    for line in stdout.lines() {
        let Ok(msg) = serde_json::from_str::<serde_json::Value>(line) else {
            continue;
        };

        if msg.get("reason").and_then(|v| v.as_str()) != Some("compiler-message") {
            continue;
        }

        let Some(message) = msg.get("message") else {
            continue;
        };

        if message.get("level").and_then(|v| v.as_str()) != Some("warning") {
            continue;
        }

        let code = message
            .get("code")
            .and_then(|c| c.get("code"))
            .and_then(|c| c.as_str())
            .unwrap_or("unknown")
            .to_string();

        let text = message
            .get("message")
            .and_then(|m| m.as_str())
            .unwrap_or("")
            .to_string();

        // Look for machine-applicable suggestions in children.
        for child in message
            .get("children")
            .and_then(|c| c.as_array())
            .into_iter()
            .flatten()
        {
            for span in child
                .get("spans")
                .and_then(|s| s.as_array())
                .into_iter()
                .flatten()
            {
                let Some(replacement) = span.get("suggested_replacement").and_then(|r| r.as_str())
                else {
                    continue;
                };

                let file_name = span
                    .get("file_name")
                    .and_then(|f| f.as_str())
                    .unwrap_or("")
                    .to_string();

                // Make path relative to root.
                let relative = file_name
                    .strip_prefix(&*root_str)
                    .map(|s| s.trim_start_matches('/').to_string())
                    .unwrap_or(file_name);

                // Skip external files.
                if relative.is_empty() || relative.starts_with('/') || relative.contains("/.cargo/")
                {
                    continue;
                }

                let line_start =
                    span.get("line_start").and_then(|l| l.as_u64()).unwrap_or(1) as usize;
                let line_end = span
                    .get("line_end")
                    .and_then(|l| l.as_u64())
                    .unwrap_or(line_start as u64) as usize;

                // Extract the original text from the span for LineReplacement matching.
                let original_text = span
                    .get("text")
                    .and_then(|t| t.as_array())
                    .and_then(|arr| arr.first())
                    .and_then(|t| t.get("text"))
                    .and_then(|t| t.as_str())
                    .unwrap_or("")
                    .to_string();

                // For single-line replacements, extract just the portion being replaced.
                let col_start = span
                    .get("column_start")
                    .and_then(|c| c.as_u64())
                    .unwrap_or(1) as usize;
                let col_end = span
                    .get("column_end")
                    .and_then(|c| c.as_u64())
                    .unwrap_or(col_start as u64) as usize;

                let old_text = if line_start == line_end && !original_text.is_empty() {
                    // Extract just the replaced portion from the line.
                    let start = col_start.saturating_sub(1);
                    let end = col_end.saturating_sub(1);
                    if start < original_text.len() && end <= original_text.len() {
                        original_text[start..end].to_string()
                    } else {
                        original_text.clone()
                    }
                } else {
                    original_text
                };

                suggestions.push(CompilerSuggestion {
                    code: code.clone(),
                    file: relative,
                    line_start,
                    line_end,
                    original_text: old_text,
                    replacement: replacement.to_string(),
                    message: text.clone(),
                });
            }
        }
    }

    // Deduplicate.
    suggestions
        .sort_by(|a, b| (&a.file, a.line_start, &a.code).cmp(&(&b.file, b.line_start, &b.code)));
    suggestions
        .dedup_by(|a, b| a.file == b.file && a.line_start == b.line_start && a.code == b.code);

    suggestions
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn parse_suggestions_extracts_unused_import() {
        let json_line = r#"{"reason":"compiler-message","package_id":"foo 0.1.0","message":{"rendered":"warning: unused import","level":"warning","code":{"code":"unused_imports","explanation":null},"message":"unused import: `std::collections::HashMap`","spans":[{"file_name":"src/lib.rs","byte_start":0,"byte_end":31,"line_start":1,"line_end":2,"column_start":1,"column_end":1,"is_primary":true,"text":[{"text":"use std::collections::HashMap;","highlight_start":1,"highlight_end":31}]}],"children":[{"message":"remove the whole `use` item","code":null,"level":"help","spans":[{"file_name":"src/lib.rs","byte_start":0,"byte_end":31,"line_start":1,"line_end":2,"column_start":1,"column_end":1,"is_primary":true,"text":[{"text":"use std::collections::HashMap;","highlight_start":1,"highlight_end":31}],"suggested_replacement":""}],"children":[],"rendered":null}]}}"#;

        let root = Path::new("/project");
        let suggestions = parse_suggestions(json_line, root);

        assert_eq!(suggestions.len(), 1);
        assert_eq!(suggestions[0].code, "unused_imports");
        assert_eq!(suggestions[0].file, "src/lib.rs");
        assert_eq!(suggestions[0].line_start, 1);
        assert_eq!(suggestions[0].replacement, "");
    }

    #[test]
    fn parse_suggestions_extracts_unused_mut() {
        let json_line = r#"{"reason":"compiler-message","package_id":"foo 0.1.0","message":{"rendered":"warning: unused mut","level":"warning","code":{"code":"unused_mut","explanation":null},"message":"variable does not need to be mutable","spans":[{"file_name":"src/lib.rs","byte_start":90,"byte_end":94,"line_start":6,"line_end":6,"column_start":9,"column_end":13,"is_primary":true,"text":[{"text":"    let mut x = 5;","highlight_start":9,"highlight_end":13}]}],"children":[{"message":"remove this `mut`","code":null,"level":"help","spans":[{"file_name":"src/lib.rs","byte_start":90,"byte_end":94,"line_start":6,"line_end":6,"column_start":9,"column_end":13,"is_primary":true,"text":[{"text":"    let mut x = 5;","highlight_start":9,"highlight_end":13}],"suggested_replacement":""}],"children":[],"rendered":null}]}}"#;

        let root = Path::new("/project");
        let suggestions = parse_suggestions(json_line, root);

        assert_eq!(suggestions.len(), 1);
        assert_eq!(suggestions[0].code, "unused_mut");
        assert_eq!(suggestions[0].file, "src/lib.rs");
        assert_eq!(suggestions[0].line_start, 6);
        assert_eq!(suggestions[0].original_text, "mut ");
        assert_eq!(suggestions[0].replacement, "");
    }

    #[test]
    fn build_line_removal_fix_creates_function_removal() {
        let suggestion = CompilerSuggestion {
            code: "unused_imports".to_string(),
            file: "src/lib.rs".to_string(),
            line_start: 1,
            line_end: 1,
            original_text: "use std::collections::HashMap;".to_string(),
            replacement: String::new(),
            message: "unused import: `std::collections::HashMap`".to_string(),
        };

        let fix = build_line_removal_fix(&suggestion);
        assert_eq!(fix.file, "src/lib.rs");
        assert_eq!(fix.insertions.len(), 1);
        assert_eq!(fix.insertions[0].safety_tier, FixSafetyTier::Safe);
        assert!(matches!(
            fix.insertions[0].kind,
            InsertionKind::FunctionRemoval {
                start_line: 1,
                end_line: 1
            }
        ));
    }

    #[test]
    fn build_line_replacement_fix_creates_replacement() {
        let suggestion = CompilerSuggestion {
            code: "unused_mut".to_string(),
            file: "src/lib.rs".to_string(),
            line_start: 6,
            line_end: 6,
            original_text: "mut ".to_string(),
            replacement: String::new(),
            message: "variable does not need to be mutable".to_string(),
        };

        let fix = build_line_replacement_fix(&suggestion);
        assert_eq!(fix.file, "src/lib.rs");
        assert_eq!(fix.insertions.len(), 1);
        assert_eq!(fix.insertions[0].safety_tier, FixSafetyTier::Safe);
        assert!(matches!(
            fix.insertions[0].kind,
            InsertionKind::LineReplacement { .. }
        ));
    }
}
