//! Fix applier for applying text edits to source files
//!
//! This module handles applying the fixes generated by the fixer to actual files,
//! either showing a diff (dry-run) or modifying the files in place.

use crate::commands::fixer::{Confidence, FixableIssue};
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};

/// A text edit to apply to a file
#[derive(Debug, Clone)]
pub struct TextEdit {
    /// Start byte offset
    pub start: u32,
    /// End byte offset
    pub end: u32,
    /// Replacement text
    pub replacement: String,
}

/// Result of applying fixes
#[derive(Debug, Default)]
pub struct ApplyResult {
    /// Number of files modified
    pub files_modified: usize,
    /// Number of fixes applied
    pub fixes_applied: usize,
    /// Errors that occurred
    pub errors: Vec<String>,
}

/// Applies fixes to source files
pub struct FixApplier {
    /// Edits grouped by file path
    edits_by_file: HashMap<PathBuf, Vec<TextEdit>>,
    /// File contents cache
    file_contents: HashMap<PathBuf, String>,
}

impl FixApplier {
    /// Create a new fix applier
    pub fn new() -> Self {
        Self {
            edits_by_file: HashMap::new(),
            file_contents: HashMap::new(),
        }
    }

    /// Add a fixable issue as an edit
    pub fn add_issue(
        &mut self,
        issue: &FixableIssue,
        file_path: &Path,
        source: &str,
        min_confidence: Confidence,
    ) {
        // Check confidence threshold
        if !should_apply(issue.confidence, min_confidence) {
            return;
        }

        let edit = TextEdit {
            start: issue.span.start,
            end: issue.span.end,
            replacement: issue.replacement.clone(),
        };

        self.edits_by_file
            .entry(file_path.to_path_buf())
            .or_default()
            .push(edit);

        self.file_contents
            .entry(file_path.to_path_buf())
            .or_insert_with(|| source.to_string());
    }

    /// Get the number of pending fixes
    pub fn pending_fixes(&self) -> usize {
        self.edits_by_file.values().map(|v| v.len()).sum()
    }

    /// Get the number of files with pending fixes
    pub fn pending_files(&self) -> usize {
        self.edits_by_file.len()
    }

    /// Apply all edits to files
    pub fn apply(&self) -> ApplyResult {
        let mut result = ApplyResult::default();

        for (file_path, edits) in &self.edits_by_file {
            match self.apply_edits_to_file(file_path, edits) {
                Ok(count) => {
                    result.files_modified += 1;
                    result.fixes_applied += count;
                }
                Err(e) => {
                    result
                        .errors
                        .push(format!("{}: {}", file_path.display(), e));
                }
            }
        }

        result
    }

    /// Apply edits to a single file
    fn apply_edits_to_file(&self, file_path: &Path, edits: &[TextEdit]) -> Result<usize, String> {
        let source = self
            .file_contents
            .get(file_path)
            .ok_or_else(|| "Source not found".to_string())?;

        let new_content = apply_edits_to_string(source, edits);

        fs::write(file_path, &new_content).map_err(|e| format!("Failed to write file: {}", e))?;

        Ok(edits.len())
    }

    /// Generate a diff showing what would be changed (dry-run mode)
    pub fn dry_run(&self) -> String {
        let mut output = String::new();

        for (file_path, edits) in &self.edits_by_file {
            if let Some(source) = self.file_contents.get(file_path) {
                let new_content = apply_edits_to_string(source, edits);
                let diff = generate_diff(file_path, source, &new_content);
                output.push_str(&diff);
                output.push('\n');
            }
        }

        output
    }
}

/// Check if a fix with given confidence should be applied
fn should_apply(fix_confidence: Confidence, min_confidence: Confidence) -> bool {
    match min_confidence {
        Confidence::High => fix_confidence == Confidence::High,
        Confidence::Medium => matches!(fix_confidence, Confidence::High | Confidence::Medium),
        Confidence::Low => true,
    }
}

/// Apply text edits to a string, returning the modified string
///
/// Edits are applied in reverse order (end to start) to preserve byte offsets
/// Note: SWC spans use BytePos which starts at 1, not 0, so we subtract 1
fn apply_edits_to_string(source: &str, edits: &[TextEdit]) -> String {
    let mut result = source.to_string();

    // Sort edits by start position in reverse order
    let mut sorted_edits: Vec<&TextEdit> = edits.iter().collect();
    sorted_edits.sort_by(|a, b| b.start.cmp(&a.start));

    for edit in sorted_edits {
        // SWC BytePos starts at 1, so we need to subtract 1 for 0-indexed string slicing
        let start = edit.start.saturating_sub(1) as usize;
        let end = edit.end.saturating_sub(1) as usize;

        if start <= result.len() && end <= result.len() && start <= end {
            result.replace_range(start..end, &edit.replacement);
        }
    }

    result
}

/// Generate a simple diff between old and new content
fn generate_diff(file_path: &Path, old: &str, new: &str) -> String {
    let mut output = String::new();
    output.push_str(&format!("--- {}\n", file_path.display()));
    output.push_str(&format!("+++ {} (fixed)\n", file_path.display()));

    let old_lines: Vec<&str> = old.lines().collect();
    let new_lines: Vec<&str> = new.lines().collect();

    // Simple line-by-line diff
    let mut i = 0;
    let mut j = 0;

    while i < old_lines.len() || j < new_lines.len() {
        let old_line = old_lines.get(i);
        let new_line = new_lines.get(j);

        match (old_line, new_line) {
            (Some(old), Some(new)) if old == new => {
                // Lines are the same - show context around changes
                i += 1;
                j += 1;
            }
            (Some(old), Some(new)) => {
                // Lines differ
                output.push_str(&format!("@@ -{},{} +{},{} @@\n", i + 1, 1, j + 1, 1));
                output.push_str(&format!("-{}\n", old));
                output.push_str(&format!("+{}\n", new));
                i += 1;
                j += 1;
            }
            (Some(old), None) => {
                // Line removed
                output.push_str(&format!("@@ -{},{} +{},{} @@\n", i + 1, 1, j + 1, 0));
                output.push_str(&format!("-{}\n", old));
                i += 1;
            }
            (None, Some(new)) => {
                // Line added
                output.push_str(&format!("@@ -{},{} +{},{} @@\n", i + 1, 0, j + 1, 1));
                output.push_str(&format!("+{}\n", new));
                j += 1;
            }
            (None, None) => break,
        }
    }

    output
}

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

    #[test]
    fn test_apply_single_edit() {
        // Note: SWC uses 1-indexed BytePos, so positions are +1 from string index
        // "let x: any = 5;" - "any" is at string indices 7..10, so BytePos is 8..11
        let source = "let x: any = 5;";
        let edits = vec![TextEdit {
            start: 8, // BytePos for string index 7
            end: 11,  // BytePos for string index 10
            replacement: "unknown".to_string(),
        }];
        let result = apply_edits_to_string(source, &edits);
        assert_eq!(result, "let x: unknown = 5;");
    }

    #[test]
    fn test_apply_multiple_edits() {
        // "let x: any = 5;\nlet y: any = 10;"
        // First "any" at indices 7..10 (BytePos 8..11)
        // Second "any" at indices 23..26 (BytePos 24..27)
        let source = "let x: any = 5;\nlet y: any = 10;";
        let edits = vec![
            TextEdit {
                start: 8,
                end: 11,
                replacement: "number".to_string(),
            },
            TextEdit {
                start: 24,
                end: 27,
                replacement: "number".to_string(),
            },
        ];
        let result = apply_edits_to_string(source, &edits);
        assert_eq!(result, "let x: number = 5;\nlet y: number = 10;");
    }

    #[test]
    fn test_apply_edits_reverse_order() {
        // Edits should work regardless of order they're provided
        // "any any any"
        // First "any" at indices 0..3 (BytePos 1..4)
        // Second "any" at indices 4..7 (BytePos 5..8)
        // Third "any" at indices 8..11 (BytePos 9..12)
        let source = "any any any";
        let edits = vec![
            TextEdit {
                start: 1,
                end: 4,
                replacement: "unknown".to_string(),
            },
            TextEdit {
                start: 9,
                end: 12,
                replacement: "unknown".to_string(),
            },
            TextEdit {
                start: 5,
                end: 8,
                replacement: "unknown".to_string(),
            },
        ];
        let result = apply_edits_to_string(source, &edits);
        assert_eq!(result, "unknown unknown unknown");
    }

    #[test]
    fn test_confidence_filtering() {
        assert!(should_apply(Confidence::High, Confidence::High));
        assert!(!should_apply(Confidence::Medium, Confidence::High));
        assert!(!should_apply(Confidence::Low, Confidence::High));

        assert!(should_apply(Confidence::High, Confidence::Medium));
        assert!(should_apply(Confidence::Medium, Confidence::Medium));
        assert!(!should_apply(Confidence::Low, Confidence::Medium));

        assert!(should_apply(Confidence::High, Confidence::Low));
        assert!(should_apply(Confidence::Medium, Confidence::Low));
        assert!(should_apply(Confidence::Low, Confidence::Low));
    }
}
