//! Span-safe replacement engine with atomic writes and validation gates.
//!
//! This module provides byte-exact patching with:
//! - Atomic file replacement (write temp + fsync + rename)
//! - File hash validation (before/after)
//! - Tree-sitter reparse gate (multi-language)
//! - Compiler validation gate (multi-language)
//! - Optional rust-analyzer gate (Rust only)
//! - Automatic rollback on any failure

mod backup;
mod batch_loader;
pub mod pattern;

use crate::error::{Diagnostic, DiagnosticLevel, Result, SpliceError};
use crate::symbol::Language as SymbolLanguage;
use crate::validate::{self, AnalyzerMode};
use crate::verify;
use ropey::Rope;
use serde::Serialize;
use sha2::{Digest, Sha256};
use std::collections::BTreeMap;
use std::ffi::OsStr;
use std::fs::{self, File};
use std::io::Write;
use std::path::{Path, PathBuf};
use tempfile::TempDir;

pub use backup::{restore_from_manifest, BackupManifest, BackupWriter};
pub use batch_loader::load_batches_from_file;
pub use pattern::{
    apply_pattern_replace, find_pattern_in_files, PatternReplaceConfig, PatternReplaceResult,
};

/// Replacement to apply within a specific file.
#[derive(Debug, Clone, Serialize)]
pub struct SpanReplacement {
    /// Absolute or workspace-relative file path.
    pub file: PathBuf,
    /// Start byte offset (inclusive).
    pub start: usize,
    /// End byte offset (exclusive).
    pub end: usize,
    /// Replacement contents.
    pub content: String,
}

impl SpanReplacement {
    /// Create a new span replacement.
    pub fn new(file: PathBuf, start: usize, end: usize, content: String) -> Self {
        Self {
            file,
            start,
            end,
            content,
        }
    }
}

/// Collection of replacements that must succeed atomically.
#[derive(Debug, Clone)]
pub struct SpanBatch {
    replacements: Vec<SpanReplacement>,
}

impl SpanBatch {
    /// Create a batch from raw replacements.
    pub fn new(replacements: Vec<SpanReplacement>) -> Self {
        Self { replacements }
    }

    /// Borrow the replacements for inspection.
    pub fn replacements(&self) -> &[SpanReplacement] {
        &self.replacements
    }

    /// Add a replacement to the batch.
    pub fn push(&mut self, replacement: SpanReplacement) {
        self.replacements.push(replacement);
    }

    /// Returns true when the batch contains no work.
    pub fn is_empty(&self) -> bool {
        self.replacements.is_empty()
    }
}

/// Result summary for a patched file.
#[derive(Debug, Clone, Serialize)]
pub struct FilePatchSummary {
    /// Path of the patched file.
    pub file: PathBuf,
    /// SHA-256 before patching.
    pub before_hash: String,
    /// SHA-256 after patching.
    pub after_hash: String,
}

/// Preview metadata describing the diff produced by a patch.
#[derive(Debug, Clone, Serialize)]
pub struct PreviewReport {
    /// The file that would be patched.
    pub file: String,
    /// 1-based line number where the change begins.
    pub line_start: usize,
    /// 1-based line number where the change ends.
    pub line_end: usize,
    /// Number of lines added by the patch.
    pub lines_added: usize,
    /// Number of lines removed by the patch.
    pub lines_removed: usize,
    /// Number of bytes inserted.
    pub bytes_added: usize,
    /// Number of bytes removed.
    pub bytes_removed: usize,
}

/// Apply a patch with comprehensive validation and automatic rollback.
///
/// This function:
/// 1. Pre-verification (file state, workspace resources, graph sync)
/// 2. Computes hash of original file
/// 3. Replaces [start..end] byte span with new_content
/// 4. Writes to temp file, fsyncs, atomic rename
/// 5. Runs tree-sitter reparse gate (language-specific)
/// 6. Runs compiler validation gate (language-specific)
/// 7. Runs rust-analyzer gate (if enabled and Rust)
/// 8. On any failure, rolls back atomically
///
/// # Rollback Behavior
///
/// If any validation gate fails after patching, the original content
/// is restored atomically. The rope mutation (remove + insert) happens
/// in memory first, then the result is written to a temp file. If
/// validation fails, we restore the original content.
///
/// # State Tracking
///
/// - `before_hash`: Content hash before patching
/// - `replaced`: Original bytes for rollback
/// - `after_hash`: Content hash after patching (for verification)
///
/// # Arguments
/// * `file_path` - Path to the file to patch
/// * `start` - Start byte offset (inclusive)
/// * `end` - End byte offset (exclusive)
/// * `new_content` - Replacement content
/// * `workspace_dir` - Directory containing project config for validation
/// * `language` - Programming language for validation gates
/// * `analyzer_mode` - rust-analyzer mode (off/path/explicit, Rust only)
///
/// # Returns
/// * `Ok((before_hash, after_hash))` - SHA-256 hashes before/after patch
/// * `Err(SpliceError)` - Validation failure with automatic rollback
pub fn apply_patch_with_validation(
    file_path: &Path,
    start: usize,
    end: usize,
    new_content: &str,
    workspace_dir: &Path,
    language: SymbolLanguage,
    analyzer_mode: AnalyzerMode,
    strict: bool,
    skip: bool,
) -> Result<(String, String)> {
    // Step 0: Pre-verification before reading file
    // Note: skip=true for patch operations since they don't require a code graph database
    // (graph DB is only needed for query/get commands using Magellan)
    // strict and skip flags now passed from CLI
    let db_path = workspace_dir.join(".magellan/magellan.db"); // Updated to use Magellan convention
    let pre_checks =
        verify::pre_verify_patch(file_path, None, workspace_dir, &db_path, strict, skip)?;

    // Check for blocking failures
    for check in &pre_checks {
        if check.is_blocking() {
            return Err(SpliceError::PreVerificationFailed {
                check: format!("{:?}", check),
            });
        }
    }

    // Log warnings but don't fail
    for check in &pre_checks {
        if check.is_warning() {
            log::warn!("Pre-verification warning: {:?}", check);
        }
    }

    // Step 0.5: Check CFG complexity if Mirage available (non-blocking)
    // This uses Mirage's 4D spatial coordinates to assess function complexity
    if let Some(function_name) = extract_function_name_from_patch(new_content) {
        if let Ok(complexity) =
            crate::cfg_analysis::check_function_complexity(&db_path, &function_name, file_path)
        {
            match complexity.risk_level {
                crate::cfg_analysis::RiskLevel::VeryHigh => {
                    log::warn!(
                        "VERY HIGH COMPLEXITY: Function '{}' has branch distance={}, dominator depth={}, loop nesting={}. \
                        Consider manual review before automated refactoring.",
                        function_name,
                        complexity.max_branch_distance,
                        complexity.max_dominator_depth,
                        complexity.max_loop_nesting
                    );
                }
                crate::cfg_analysis::RiskLevel::High => {
                    log::warn!(
                        "HIGH COMPLEXITY: Function '{}' has branch distance={}, dominator depth={}. \
                        Automated refactoring may be risky.",
                        function_name,
                        complexity.max_branch_distance,
                        complexity.max_dominator_depth
                    );
                }
                crate::cfg_analysis::RiskLevel::Medium => {
                    log::info!(
                        "Medium complexity: Function '{}' (branch distance={}, dominator depth={})",
                        function_name,
                        complexity.max_branch_distance,
                        complexity.max_dominator_depth
                    );
                }
                crate::cfg_analysis::RiskLevel::Low => {
                    log::debug!(
                        "Low complexity: Function '{}' (branch distance={})",
                        function_name,
                        complexity.max_branch_distance
                    );
                }
            }
        }
        // Don't fail if Mirage unavailable - just log and continue
    }

    // Step 1: Read original file and compute hash
    let replaced = std::fs::read(file_path)?;
    let before_hash = compute_hash(&replaced);

    // Step 2: Validate span bounds
    if start > end || end > replaced.len() {
        return Err(SpliceError::InvalidSpan {
            file: file_path.to_path_buf(),
            start,
            end,
            file_size: replaced.len(),
        });
    }

    // Step 3: Validate UTF-8 boundaries
    std::str::from_utf8(&replaced[start..end]).map_err(|_| SpliceError::InvalidSpan {
        file: file_path.to_path_buf(),
        start,
        end,
        file_size: replaced.len(),
    })?;

    // Step 4: Apply byte-exact replacement using ropey
    // Note: rope.remove() and rope.insert() are in-memory operations.
    // If validation fails (Step 7), we rollback by restoring the original content.
    let mut rope = Rope::from_str(std::str::from_utf8(&replaced)?);
    let start_char = rope.byte_to_char(start);
    let end_char = rope.byte_to_char(end);

    // Mutate rope: remove old content, insert new content
    rope.remove(start_char..end_char);
    rope.insert(start_char, new_content);

    let patched_content = rope.to_string();

    // Step 5: Write to temp file in same directory (for atomic rename)
    let patched_bytes = patched_content.into_bytes();
    write_atomic(file_path, &patched_bytes, "patch")?;

    // Step 7: Run validation gates
    match run_validation_gates(file_path, workspace_dir, language, analyzer_mode.clone()) {
        Ok(_) => {}
        Err(e) => {
            log::warn!("Validation failed, rolling back patch: {:?}", e);

            if let Err(rollback_err) = write_atomic(file_path, &replaced, "rollback") {
                log::error!(
                    "Failed to restore {} during rollback: {}",
                    file_path.display(),
                    rollback_err
                );
            }
            return Err(e);
        }
    }

    // Step 8: Compute after hash
    let refreshed_bytes = std::fs::read(file_path)?;
    let after_hash = compute_hash(&refreshed_bytes);

    // Step 9: Run post-verification to confirm expected changes
    let mut post_verify =
        verify::verify_after_patch(file_path, workspace_dir, &before_hash, analyzer_mode)?;

    // Step 9.1: Verify localized change (no unintended modifications)
    let localized = verify::verify_localized_change(file_path, &replaced, (start, end));

    match &localized {
        Ok(true) => {
            log::info!("Localized change verification passed");
        }
        Ok(false) => {
            log::warn!("Localized change verification detected modifications outside target span");
            post_verify.add_warning("File modified outside target span");
        }
        Err(e) => {
            log::warn!("Localized change verification failed: {}", e);
            post_verify.add_warning(format!("Could not verify localized change: {}", e));
        }
    }

    // Log warnings for user visibility
    for warning in &post_verify.warnings {
        log::warn!("Post-verification warning: {}", warning);
    }

    // Log errors (non-blocking, advisory)
    for error in &post_verify.errors {
        log::error!("Post-verification error: {}", error);
    }

    // Log post-verification status
    log::info!(
        "Post-verification: syntax={}, compiler={}, semantic={}, changed={}",
        post_verify.syntax_ok,
        post_verify.compiler_ok,
        post_verify.semantic_ok,
        post_verify.file_changed(),
    );

    Ok((before_hash, after_hash))
}

/// Apply multiple span replacements atomically across files.
///
/// All replacements are made durable before running validation gates. Any tree-sitter,
/// compiler, or analyzer failure restores every file to its original bytes before returning
/// the error.
pub fn apply_batch_with_validation(
    batches: &[SpanBatch],
    workspace_dir: &Path,
    language: SymbolLanguage,
    analyzer_mode: AnalyzerMode,
) -> Result<Vec<FilePatchSummary>> {
    if batches.is_empty() {
        return Ok(Vec::new());
    }

    let mut grouped: BTreeMap<PathBuf, Vec<SpanReplacement>> = BTreeMap::new();
    for batch in batches {
        for replacement in batch.replacements() {
            grouped
                .entry(replacement.file.clone())
                .or_default()
                .push(replacement.clone());
        }
    }

    let mut applied = Vec::new();

    for (file_path, mut replacements) in grouped {
        if replacements.is_empty() {
            continue;
        }

        // Pre-verify each file
        let pre_check = verify::verify_file_ready(&file_path, None, workspace_dir);
        if pre_check.is_blocking() {
            log::warn!(
                "Skipping {:?}: pre-verification failed: {:?}",
                file_path,
                pre_check
            );
            continue;
        }

        replacements.sort_by_key(|r| std::cmp::Reverse(r.start));
        let (replaced, before_hash) = read_with_hash(&file_path)?;
        validate_replacements(&file_path, &replacements, &replaced)?;
        let patched_bytes = apply_replacements(&replaced, &replacements)?;
        let after_hash = compute_hash(&patched_bytes);

        if let Err(write_err) = write_atomic(&file_path, &patched_bytes, "batch") {
            rollback_files(&applied);
            return Err(write_err);
        }

        applied.push(AppliedFile {
            file: file_path,
            replaced,
            before_hash,
            after_hash,
        });
    }

    let validation = run_batch_validations(&applied, workspace_dir, language, analyzer_mode);
    if let Err(err) = validation {
        rollback_files(&applied);
        return Err(err);
    }

    Ok(applied
        .into_iter()
        .map(|file| FilePatchSummary {
            file: file.file,
            before_hash: file.before_hash,
            after_hash: file.after_hash,
        })
        .collect())
}

/// Preview a patch by cloning the workspace, applying the change, and validating there.
pub fn preview_patch(
    file_path: &Path,
    start: usize,
    end: usize,
    new_content: &str,
    workspace_root: &Path,
    language: SymbolLanguage,
    analyzer_mode: AnalyzerMode,
) -> Result<(FilePatchSummary, PreviewReport)> {
    let preview_workspace = clone_workspace_for_preview(workspace_root)?;
    let relative = file_path
        .strip_prefix(workspace_root)
        .map_err(|_| SpliceError::Other("File not under workspace root".to_string()))?;
    let preview_file = preview_workspace.path().join(relative);

    let (before_hash, after_hash) = apply_patch_with_validation(
        &preview_file,
        start,
        end,
        new_content,
        preview_workspace.path(),
        language,
        analyzer_mode,
        false, // strict: preview mode doesn't need strict validation
        true,  // skip: preview mode also doesn't need graph DB
    )?;

    let preview_report = compute_preview_report(&file_path, start, end, new_content)?;

    Ok((
        FilePatchSummary {
            file: file_path.to_path_buf(),
            before_hash,
            after_hash,
        },
        preview_report,
    ))
}

/// Preview a patch and return before/after content for diff generation.
///
/// This extends the `preview_patch()` functionality by also returning the original
/// and patched file contents, enabling unified diff generation in dry-run mode.
///
/// # Arguments
/// Same parameters as `preview_patch()`:
/// * `file_path` - Path to the file to preview
/// * `start` - Start byte offset (inclusive)
/// * `end` - End byte offset (exclusive)
/// * `new_content` - Replacement content
/// * `workspace_root` - Root directory of the workspace
/// * `language` - Programming language for validation gates
/// * `analyzer_mode` - rust-analyzer mode (off/path/explicit, Rust only)
///
/// # Returns
/// Result containing a tuple of:
/// * `FilePatchSummary` - Hash information
/// * `PreviewReport` - Line/byte change statistics
/// * `String` - Original file content (before patch)
/// * `String` - Patched file content (after patch)
///
/// # Examples
/// ```ignore
/// use splice::patch::preview_patch_with_content;
///
/// let (summary, report, before, after) = preview_patch_with_content(
///     &file_path,
///     start,
///     end,
///     new_content,
///     &workspace_root,
///     language,
///     analyzer_mode,
/// )?;
/// ```
pub fn preview_patch_with_content(
    file_path: &Path,
    start: usize,
    end: usize,
    new_content: &str,
    workspace_root: &Path,
    language: SymbolLanguage,
    analyzer_mode: AnalyzerMode,
) -> Result<(FilePatchSummary, PreviewReport, String, String)> {
    // Ensure both paths are absolute for consistent prefix stripping
    let file_path = std::fs::canonicalize(file_path).map_err(|e| SpliceError::Io {
        path: file_path.to_path_buf(),
        source: e,
    })?;
    let workspace_root = std::fs::canonicalize(workspace_root).map_err(|e| SpliceError::Io {
        path: workspace_root.to_path_buf(),
        source: e,
    })?;

    let preview_workspace = clone_workspace_for_preview(&workspace_root)?;
    let relative = file_path.strip_prefix(&workspace_root).map_err(|_| {
        SpliceError::Other(format!(
            "File {} not under workspace root {}",
            file_path.display(),
            workspace_root.display()
        ))
    })?;
    let preview_file = preview_workspace.path().join(relative);

    // Read original file content before patching
    let before_content = std::fs::read_to_string(&preview_file)?;

    let (before_hash, after_hash) = apply_patch_with_validation(
        &preview_file,
        start,
        end,
        new_content,
        preview_workspace.path(),
        language,
        analyzer_mode,
        false, // strict: preview mode doesn't need strict validation
        true,  // skip: preview mode also doesn't need graph DB
    )?;

    // Read patched file content
    let after_content = std::fs::read_to_string(&preview_file)?;

    let preview_report = compute_preview_report(&file_path, start, end, new_content)?;

    Ok((
        FilePatchSummary {
            file: file_path.to_path_buf(),
            before_hash,
            after_hash,
        },
        preview_report,
        before_content,
        after_content,
    ))
}

/// Run all validation gates in sequence.
///
/// Gates are executed in order:
/// 1. Tree-sitter reparse (syntax validation, language-specific)
/// 2. Compiler validation (language-specific)
/// 3. rust-analyzer (optional, Rust only)
///
/// If any gate fails, returns error immediately.
fn run_validation_gates(
    file_path: &Path,
    workspace_dir: &Path,
    language: SymbolLanguage,
    analyzer_mode: AnalyzerMode,
) -> Result<()> {
    // Gate 1: Tree-sitter reparse (language-specific)
    gate_tree_sitter_reparse(file_path, language)?;

    // Gate 2: Compiler validation (language-specific)
    gate_compiler_validation(file_path, workspace_dir, language)?;

    // Gate 3: rust-analyzer (Rust only, optional)
    if language == SymbolLanguage::Rust {
        use crate::validate::gate_rust_analyzer;
        gate_rust_analyzer(workspace_dir, analyzer_mode)?;
    }

    Ok(())
}

/// Tree-sitter reparse gate (language-specific).
///
/// Validates that the patched file can be parsed as valid syntax
/// for the given programming language.
fn gate_tree_sitter_reparse(file_path: &Path, language: SymbolLanguage) -> Result<()> {
    let source = std::fs::read(file_path)?;

    let mut parser = tree_sitter::Parser::new();
    let tree_sitter_lang = get_tree_sitter_language(language);

    parser
        .set_language(&tree_sitter_lang)
        .map_err(|e| SpliceError::Parse {
            file: file_path.to_path_buf(),
            message: format!("Failed to set language: {:?}", e),
        })?;

    let tree = parser
        .parse(&source, None)
        .ok_or_else(|| SpliceError::ParseValidationFailed {
            file: file_path.to_path_buf(),
            message: "Parse failed - no tree returned".to_string(),
        })?;

    // Check for parse errors
    if tree.root_node().has_error() {
        return Err(SpliceError::ParseValidationFailed {
            file: file_path.to_path_buf(),
            message: format!(
                "Tree-sitter detected syntax errors in patched {} file",
                language.as_str()
            ),
        });
    }

    Ok(())
}

/// Get the appropriate tree-sitter language for the given SymbolLanguage.
fn get_tree_sitter_language(language: SymbolLanguage) -> tree_sitter::Language {
    match language {
        SymbolLanguage::Rust => tree_sitter_rust::language(),
        SymbolLanguage::Python => tree_sitter_python::language(),
        SymbolLanguage::C => tree_sitter_c::language(),
        SymbolLanguage::Cpp => tree_sitter_cpp::language(),
        SymbolLanguage::Java => tree_sitter_java::language(),
        SymbolLanguage::JavaScript => tree_sitter_javascript::language(),
        SymbolLanguage::TypeScript => tree_sitter_typescript::language_typescript(),
    }
}

/// Compiler validation gate (language-specific).
///
/// Validates that the patched file compiles using the appropriate
/// compiler for each language (via validate::gates::validate_file).
fn gate_compiler_validation(
    file_path: &Path,
    workspace_dir: &Path,
    language: SymbolLanguage,
) -> Result<()> {
    match language {
        SymbolLanguage::Rust => {
            // Rust: Use cargo check from workspace directory
            gate_cargo_check(workspace_dir)?;
        }
        _ => {
            // Other languages: Use validate_file which auto-detects language
            use crate::validate::gates::validate_file;

            let outcome = validate_file(file_path)?;
            let tool_metadata = tool_invocation_for_language(language)
                .map(|inv| validate::collect_tool_metadata(inv.binary, inv.version_args));

            if !outcome.is_valid {
                if !outcome.tool_available {
                    // Tool not available is a soft failure - we can't validate
                    // For now, we treat this as success but log a warning
                    log::warn!(
                        "Compiler validation tool not available for {}, skipping validation",
                        language.as_str()
                    );
                    return Ok(());
                }

                // Tool is available but validation failed
                let mut diagnostics = Vec::new();
                let tool_name = format!("{}-compiler", language.as_str());

                for err in outcome.errors {
                    let remediation = err
                        .code
                        .as_deref()
                        .and_then(validate::remediation_link_for_code);
                    diagnostics.push(
                        Diagnostic::new(&tool_name, DiagnosticLevel::Error, err.message)
                            .with_file(file_for_diagnostic(&err.file, file_path))
                            .with_position(nonzero(err.line), nonzero(err.column))
                            .with_code(err.code.clone())
                            .with_note(err.note.clone())
                            .with_tool_metadata(tool_metadata.as_ref())
                            .with_remediation(remediation),
                    );
                }

                for warn in outcome.warnings {
                    let remediation = warn
                        .code
                        .as_deref()
                        .and_then(validate::remediation_link_for_code);
                    diagnostics.push(
                        Diagnostic::new(&tool_name, DiagnosticLevel::Warning, warn.message)
                            .with_file(file_for_diagnostic(&warn.file, file_path))
                            .with_position(nonzero(warn.line), nonzero(warn.column))
                            .with_code(warn.code.clone())
                            .with_note(warn.note.clone())
                            .with_tool_metadata(tool_metadata.as_ref())
                            .with_remediation(remediation),
                    );
                }

                return Err(SpliceError::CompilerValidationFailed {
                    file: file_path.to_path_buf(),
                    language: language.as_str().to_string(),
                    diagnostics,
                });
            }
        }
    }

    Ok(())
}

fn file_for_diagnostic(reported: &str, fallback: &Path) -> PathBuf {
    if reported.is_empty() {
        fallback.to_path_buf()
    } else {
        PathBuf::from(reported)
    }
}

fn nonzero(value: usize) -> Option<usize> {
    if value == 0 {
        None
    } else {
        Some(value)
    }
}

struct ToolInvocation {
    binary: &'static str,
    version_args: &'static [&'static str],
}

fn tool_invocation_for_language(language: SymbolLanguage) -> Option<ToolInvocation> {
    match language {
        SymbolLanguage::Python => Some(ToolInvocation {
            binary: "python",
            version_args: &["--version"],
        }),
        SymbolLanguage::C => Some(ToolInvocation {
            binary: "gcc",
            version_args: &["--version"],
        }),
        SymbolLanguage::Cpp => Some(ToolInvocation {
            binary: "g++",
            version_args: &["--version"],
        }),
        SymbolLanguage::Java => Some(ToolInvocation {
            binary: "javac",
            version_args: &["-version"],
        }),
        SymbolLanguage::JavaScript => Some(ToolInvocation {
            binary: "node",
            version_args: &["--version"],
        }),
        SymbolLanguage::TypeScript => Some(ToolInvocation {
            binary: "tsc",
            version_args: &["--version"],
        }),
        _ => None,
    }
}

/// Cargo check gate (Rust-specific).
///
/// Validates that the workspace compiles after the patch.
/// Uses a 60-second timeout to prevent hanging on large projects.
fn gate_cargo_check(workspace_dir: &Path) -> Result<()> {
    use std::process::Command;
    use std::thread;
    use std::time::Duration;

    // Spawn cargo check in a separate thread to allow timeout
    let workspace_path = workspace_dir.to_path_buf();
    let (tx, rx) = std::sync::mpsc::channel();

    thread::spawn(move || {
        let output = Command::new("cargo")
            .args(["check", "--color=never"])
            .current_dir(&workspace_path)
            .output();
        let _ = tx.send(output);
    });

    // Wait for completion with 120-second timeout (cargo check can take 50+ seconds on large projects)
    let output = match rx.recv_timeout(Duration::from_secs(120)) {
        Ok(result) => result?,
        Err(_) => {
            return Err(SpliceError::Other(
                "cargo check timed out after 120 seconds".to_string(),
            ));
        }
    };

    let stderr = String::from_utf8_lossy(&output.stderr);
    let stdout = String::from_utf8_lossy(&output.stdout);

    let combined = format!("{}{}", stderr, stdout);

    if output.status.success() {
        return Ok(());
    }

    let compiler_errors = validate::parse_cargo_output(&stderr);
    let mut diagnostics = Vec::new();
    let cargo_meta = validate::collect_tool_metadata("cargo", &["--version"]);

    if compiler_errors.is_empty() {
        diagnostics.push(
            Diagnostic::new("cargo-check", DiagnosticLevel::Error, combined.clone())
                .with_file(workspace_dir.to_path_buf())
                .with_tool_metadata(Some(&cargo_meta)),
        );
    } else {
        for err in compiler_errors {
            let remediation = err
                .code
                .as_deref()
                .and_then(validate::remediation_link_for_code);
            diagnostics.push(
                Diagnostic::new("cargo-check", DiagnosticLevel::from(err.level), err.message)
                    .with_file(PathBuf::from(err.file))
                    .with_position(nonzero(err.line), nonzero(err.column))
                    .with_code(err.code.clone())
                    .with_note(err.note.clone())
                    .with_tool_metadata(Some(&cargo_meta))
                    .with_remediation(remediation),
            );
        }
    }

    Err(SpliceError::CargoCheckFailed {
        workspace: workspace_dir.to_path_buf(),
        output: combined,
        diagnostics,
    })
}

/// Compute SHA-256 hash of file contents.
fn compute_hash(bytes: &[u8]) -> String {
    let mut hasher = Sha256::new();
    hasher.update(bytes);
    let result = hasher.finalize();
    format!("{:x}", result)
}

/// Replace byte span without validation (legacy method for backward compatibility).
///
/// This is a simple span replacement without validation gates.
/// Prefer `apply_patch_with_validation` for all new code.
pub fn replace_span(file_path: &Path, start: usize, end: usize, new_content: &str) -> Result<()> {
    let replaced = std::fs::read_to_string(file_path)?;
    let file_size = replaced.len();

    if start > end || end > file_size {
        return Err(SpliceError::InvalidSpan {
            file: file_path.to_path_buf(),
            start,
            end,
            file_size,
        });
    }

    // Validate that the span is within bounds
    if end > file_size || start > end {
        return Err(SpliceError::InvalidSpan {
            file: file_path.to_path_buf(),
            start,
            end,
            file_size,
        });
    }

    let mut rope = Rope::from_str(&replaced);
    let start_char = rope.byte_to_char(start);
    let end_char = rope.byte_to_char(end);

    rope.remove(start_char..end_char);
    rope.insert(start_char, new_content);

    std::fs::write(file_path, rope.to_string())?;

    Ok(())
}

fn run_batch_validations(
    files: &[AppliedFile],
    workspace_dir: &Path,
    language: SymbolLanguage,
    analyzer_mode: AnalyzerMode,
) -> Result<()> {
    if files.is_empty() {
        return Ok(());
    }

    let mut requires_rust_validation = false;
    for file in files {
        gate_tree_sitter_reparse(&file.file, language)?;
        if language == SymbolLanguage::Rust {
            requires_rust_validation = true;
        } else {
            gate_compiler_validation(&file.file, workspace_dir, language)?;
        }
    }

    if requires_rust_validation {
        gate_cargo_check(workspace_dir)?;
        if language == SymbolLanguage::Rust {
            if analyzer_mode != AnalyzerMode::Off {
                use crate::validate::gate_rust_analyzer;
                gate_rust_analyzer(workspace_dir, analyzer_mode)?;
            }
        }
    }

    Ok(())
}

fn validate_replacements(
    file_path: &Path,
    replacements: &[SpanReplacement],
    replaced: &[u8],
) -> Result<()> {
    if replacements.is_empty() {
        return Ok(());
    }
    let file_len = replaced.len();

    let mut sorted = replacements.to_vec();
    sorted.sort_by_key(|r| r.start);

    let mut previous_end: Option<usize> = None;
    for replacement in &sorted {
        if replacement.start > replacement.end || replacement.end > file_len {
            return Err(SpliceError::InvalidSpan {
                file: file_path.to_path_buf(),
                start: replacement.start,
                end: replacement.end,
                file_size: file_len,
            });
        }

        std::str::from_utf8(&replaced[replacement.start..replacement.end]).map_err(|_| {
            SpliceError::InvalidSpan {
                file: file_path.to_path_buf(),
                start: replacement.start,
                end: replacement.end,
                file_size: file_len,
            }
        })?;

        if let Some(prev_end) = previous_end {
            if replacement.start < prev_end {
                return Err(SpliceError::Other(format!(
                    "Overlapping replacements detected in {}",
                    file_path.display()
                )));
            }
        }
        previous_end = Some(replacement.end);
    }

    Ok(())
}

fn apply_replacements(replaced: &[u8], replacements: &[SpanReplacement]) -> Result<Vec<u8>> {
    let content = std::str::from_utf8(replaced)?;
    let mut rope = Rope::from_str(content);

    for replacement in replacements {
        let start_char = rope.byte_to_char(replacement.start);
        let end_char = rope.byte_to_char(replacement.end);
        rope.remove(start_char..end_char);
        rope.insert(start_char, &replacement.content);
    }

    Ok(rope.to_string().into_bytes())
}

fn read_with_hash(path: &Path) -> Result<(Vec<u8>, String)> {
    let data = std::fs::read(path)?;
    let hash = compute_hash(&data);
    Ok((data, hash))
}

fn rollback_files(files: &[AppliedFile]) {
    for file in files.iter().rev() {
        if let Err(err) = write_atomic(&file.file, &file.replaced, "rollback") {
            log::error!(
                "Rollback failed for {}: {}",
                file.file.display(),
                err.to_string()
            );
        }
    }
}

fn write_atomic(file_path: &Path, content: &[u8], suffix: &str) -> Result<()> {
    let temp_path = temp_path_for(file_path, suffix)?;
    let mut temp_file = File::create(&temp_path)?;
    temp_file.write_all(content)?;
    temp_file.sync_all()?;
    std::fs::rename(&temp_path, file_path)?;
    Ok(())
}

fn temp_path_for(file_path: &Path, suffix: &str) -> Result<PathBuf> {
    let file_dir = file_path
        .parent()
        .ok_or_else(|| SpliceError::Other("File has no parent directory".to_string()))?;
    let file_name = file_path
        .file_name()
        .and_then(|n| n.to_str())
        .unwrap_or("tmp");
    Ok(file_dir.join(format!(".{}.{}.tmp", file_name, suffix)))
}

struct AppliedFile {
    file: PathBuf,
    replaced: Vec<u8>,
    before_hash: String,
    after_hash: String,
}

/// Clone workspace to a temporary directory for preview operations.
///
/// Creates a temporary directory and recursively copies the workspace.
/// Also copies any local path dependencies (sibling directories referenced
/// in Cargo.toml) to ensure cargo check works in the preview environment.
///
/// If copying fails, the temp directory is automatically cleaned up by Drop.
///
/// # Returns
///
/// Returns `Ok(TempDir)` which will be cleaned up when dropped.
fn clone_workspace_for_preview(workspace_root: &Path) -> Result<TempDir> {
    let preview_dir = TempDir::new()?;
    let preview_path = preview_dir.path();

    // First, copy the workspace itself
    // Note: If copy_dir_recursive fails, preview_dir is dropped here
    // and automatically cleans up the temp directory
    copy_dir_recursive(workspace_root, preview_path)?;

    // Handle local path dependencies from Cargo.toml
    // Projects with dependencies like `llmgrep = { path = "../llmgrep" }`
    // need those sibling directories copied to the preview workspace parent
    if let Ok(local_deps) = extract_local_path_dependencies(workspace_root) {
        let preview_parent = preview_path.parent().unwrap_or(preview_path);

        for dep_path in local_deps {
            // For nested paths like ../sqlitegraph/sqlitegraph, we need to copy
            // the parent directory (sqlitegraph) so the relative path resolves.
            let (source_path, target_name) = if let Some(dep_parent) = dep_path.parent() {
                // Check if this is a nested path (grandparent is workspace's parent)
                if let Some(grandparent) = dep_parent.parent() {
                    if let Some(workspace_parent) = workspace_root.parent() {
                        if grandparent == workspace_parent {
                            // Nested path: use parent directory as source
                            let parent_name = dep_parent
                                .file_name()
                                .and_then(|n| n.to_str())
                                .ok_or_else(|| {
                                    SpliceError::Other(format!(
                                        "Invalid dependency parent path: {:?}",
                                        dep_parent
                                    ))
                                })?;
                            (dep_parent.to_path_buf(), parent_name.to_string())
                        } else {
                            // Normal path: use dep_path as-is
                            let dep_name = dep_path
                                .file_name()
                                .and_then(|n| n.to_str())
                                .ok_or_else(|| {
                                    SpliceError::Other(format!(
                                        "Invalid dependency path: {:?}",
                                        dep_path
                                    ))
                                })?;
                            (dep_path.clone(), dep_name.to_string())
                        }
                    } else {
                        // Can't determine workspace parent, use dep_path as-is
                        let dep_name =
                            dep_path
                                .file_name()
                                .and_then(|n| n.to_str())
                                .ok_or_else(|| {
                                    SpliceError::Other(format!(
                                        "Invalid dependency path: {:?}",
                                        dep_path
                                    ))
                                })?;
                        (dep_path.clone(), dep_name.to_string())
                    }
                } else {
                    // No grandparent, use dep_path as-is
                    let dep_name =
                        dep_path
                            .file_name()
                            .and_then(|n| n.to_str())
                            .ok_or_else(|| {
                                SpliceError::Other(format!(
                                    "Invalid dependency path: {:?}",
                                    dep_path
                                ))
                            })?;
                    (dep_path.clone(), dep_name.to_string())
                }
            } else {
                // No parent, use dep_path as-is
                let dep_name = dep_path
                    .file_name()
                    .and_then(|n| n.to_str())
                    .ok_or_else(|| {
                        SpliceError::Other(format!("Invalid dependency path: {:?}", dep_path))
                    })?;
                (dep_path.clone(), dep_name.to_string())
            };

            let dep_dest = preview_parent.join(&target_name);

            // Skip if already exists or is the same as workspace
            if dep_dest.exists() || source_path == workspace_root {
                continue;
            }

            // Copy the dependency directory
            if let Err(e) = copy_dir_recursive(&source_path, &dep_dest) {
                log::warn!(
                    "Failed to copy local dependency {:?} to {:?}: {}",
                    source_path,
                    dep_dest,
                    e
                );
                // Non-fatal: preview may still work if dependency isn't used
            }
        }
    }

    Ok(preview_dir)
}

/// Extract local path dependencies from Cargo.toml.
///
/// Returns paths to sibling directories that are local dependencies,
/// e.g., `../llmgrep` from `llmgrep = { path = "../llmgrep" }`.
fn extract_local_path_dependencies(workspace_root: &Path) -> Result<Vec<PathBuf>> {
    let cargo_toml_path = workspace_root.join("Cargo.toml");
    let cargo_content = fs::read_to_string(&cargo_toml_path)?;

    let mut local_deps = Vec::new();
    let mut seen_deps = std::collections::HashSet::new();

    // Simple string-based parsing for path dependencies
    // Match patterns like: `dep_name = { path = "../something" }`
    for line in cargo_content.lines() {
        let line = line.trim();
        // Look for inline table dependencies with path
        if line.contains("{") && line.contains("path") {
            // Extract the path value
            if let Some(start) = line.find("path = \"") {
                let start_idx = start + 8; // "path = \"".len()
                if let Some(end) = line[start_idx..].find('"') {
                    let rel_path = &line[start_idx..start_idx + end];
                    if rel_path.starts_with("..") {
                        let dep_path = workspace_root.join(rel_path);
                        if dep_path.exists() {
                            // Get the canonical path and track what we've seen
                            if let Ok(canonical) = dep_path.canonicalize() {
                                if !seen_deps.contains(&canonical) {
                                    seen_deps.insert(canonical.clone());
                                    local_deps.push(canonical);
                                }
                            }
                        }
                    }
                }
            }
        }
    }

    // Also check for workspace members and their dependencies
    if let Some(parent) = workspace_root.parent() {
        // Check for a workspace Cargo.toml in parent directory
        let workspace_cargo = parent.join("Cargo.toml");
        if workspace_cargo.exists() {
            if let Ok(ws_content) = fs::read_to_string(&workspace_cargo) {
                // Find workspace members
                if let Some(start) = ws_content.find("members = [") {
                    let members_start = start + 11;
                    if let Some(end) = ws_content[members_start..].find(']') {
                        let members_str = &ws_content[members_start..members_start + end];
                        for member in members_str.split(',') {
                            let member = member.trim().trim_matches('"').trim_matches('\'');
                            let member_path = parent.join(member);
                            if member_path.exists() && member_path != workspace_root {
                                if let Ok(canonical) = member_path.canonicalize() {
                                    if !seen_deps.contains(&canonical) {
                                        seen_deps.insert(canonical.clone());
                                        local_deps.push(canonical);
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }

    Ok(local_deps)
}

fn copy_dir_recursive(src: &Path, dst: &Path) -> Result<()> {
    fs::create_dir_all(dst)?;

    for entry in fs::read_dir(src)? {
        let entry = entry?;
        if should_skip_entry(&entry.file_name()) {
            continue;
        }

        let dest = dst.join(entry.file_name());
        let file_type = entry.file_type()?;

        if file_type.is_dir() {
            copy_dir_recursive(&entry.path(), &dest)?;
        } else if file_type.is_file() {
            if let Some(parent) = dest.parent() {
                fs::create_dir_all(parent)?;
            }
            fs::copy(entry.path(), &dest)?;
        }
    }

    Ok(())
}

fn should_skip_entry(name: &OsStr) -> bool {
    matches!(
        name.to_string_lossy().as_ref(),
        ".git"
            | ".splice-backup"
            | "target"
            | "node_modules"
            | ".splice_graph.db"
            | ".splice_graph.db-shm"
            | ".splice_graph.db-wal"
            | "codegraph.db"
            | "magellan.db"
            | "operations.db"
            | "splice_map.db"
            | "syncore_code_graph.db"
            | "syncore_code_graph.db-shm"
            | "syncore_code_graph.db-wal"
    )
}

/// Calculate line counts for a patch operation.
///
/// This is a public function so it can be reused in different contexts
/// (preview mode, actual patch, JSON output, etc.).
///
/// # Arguments
/// * `file_path` - Path to the file
/// * `start` - Byte offset where the replacement starts
/// * `end` - Byte offset where the replacement ends
/// * `new_content` - The new content to insert
///
/// # Returns
/// * `PreviewReport` - Contains line counts, byte counts, and line numbers
pub fn compute_preview_report(
    file_path: &Path,
    start: usize,
    end: usize,
    new_content: &str,
) -> Result<PreviewReport> {
    let replaced = fs::read(file_path)?;
    let source = std::str::from_utf8(&replaced)?;
    let rope = Rope::from_str(source);

    let start_line = rope.byte_to_line(start);
    let end_line = if end == start {
        start_line
    } else if end == replaced.len() {
        rope.len_lines().saturating_sub(1)
    } else {
        rope.byte_to_line(end)
    };

    let lines_removed = if end > start {
        (&source[start..end]).lines().count()
    } else {
        0
    };
    let lines_added = if new_content.is_empty() {
        0
    } else {
        new_content.lines().count()
    };

    let bytes_removed = end.saturating_sub(start);
    let bytes_added = new_content.as_bytes().len();

    Ok(PreviewReport {
        file: file_path.to_string_lossy().into_owned(),
        line_start: start_line + 1,
        line_end: if lines_removed == 0 {
            start_line + 1
        } else {
            end_line + 1
        },
        lines_added,
        lines_removed,
        bytes_added,
        bytes_removed,
    })
}

/// Validate that a span aligns with UTF-8 boundaries.
pub fn validate_utf8_span(source: &str, start: usize, end: usize) -> Result<()> {
    let file_size = source.len();

    // Validate that the span is within bounds
    if end > file_size || start > end {
        return Err(SpliceError::InvalidSpan {
            file: std::path::PathBuf::from("<unknown>"),
            start,
            end,
            file_size,
        });
    }
    // If source is valid UTF-8, any slice of it is also valid UTF-8
    let _ = &source[start..end];
    Ok(())
}

/// Extract function name from patch content for CFG complexity analysis
///
/// This is a simple heuristic that looks for common function declaration patterns.
/// It's used to query Mirage for CFG complexity metrics before applying a patch.
///
/// # Arguments
/// * `patch_content` - The new content being patched in
///
/// # Returns
/// * `Some(function_name)` - If a function declaration is found
/// * `None` - If no function declaration detected
///
/// # Notes
/// - Uses simple regex patterns (not full parsing)
/// - Supports `fn name`, `pub fn name`, `async fn name`, etc.
/// - Falls back gracefully if not found (Mirage check is optional)
fn extract_function_name_from_patch(patch_content: &str) -> Option<String> {
    // Look for Rust function declarations
    // Patterns: "fn name(", "pub fn name(", "async fn name(", etc.
    use regex::Regex;

    // Lazy-init regex to avoid compiling on every call
    let fn_regex =
        Regex::new(r"(?m)^(?:pub\s+)?(?:async\s+)?(?:unsafe\s+)?fn\s+(\w+)\s*\(").ok()?;

    fn_regex
        .captures(patch_content)
        .map(|caps| caps[1].to_string())
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::io::Write;
    use tempfile::NamedTempFile;

    #[test]
    fn test_compute_preview_report_line_counts() {
        // Create a test file with known content
        let mut temp_file = NamedTempFile::new().unwrap();
        writeln!(temp_file, "line 1").unwrap();
        writeln!(temp_file, "line 2").unwrap();
        writeln!(temp_file, "line 3").unwrap();
        writeln!(temp_file, "line 4").unwrap();
        temp_file.flush().unwrap();

        // Test replacing 2 lines with 1 line
        let source = std::fs::read_to_string(temp_file.path()).unwrap();
        let start = source.find("line 2\n").unwrap();
        let end = source.find("line 4").unwrap();
        let new_content = "NEW LINE\n";

        let report = compute_preview_report(temp_file.path(), start, end, new_content).unwrap();

        // We removed "line 2\nline 3\n" (2 lines) and added "NEW LINE\n" (1 line)
        assert_eq!(report.lines_removed, 2, "Should count 2 lines removed");
        assert_eq!(report.lines_added, 1, "Should count 1 line added");
    }

    #[test]
    fn test_compute_preview_report_empty_replacement() {
        // Test replacing content with empty string (deletion)
        let mut temp_file = NamedTempFile::new().unwrap();
        writeln!(temp_file, "line 1").unwrap();
        writeln!(temp_file, "line 2").unwrap();
        temp_file.flush().unwrap();

        let source = std::fs::read_to_string(temp_file.path()).unwrap();
        let start = source.find("line 1").unwrap();
        let end = source.len();

        let report = compute_preview_report(temp_file.path(), start, end, "").unwrap();

        assert_eq!(report.lines_removed, 2, "Should count 2 lines removed");
        assert_eq!(report.lines_added, 0, "Empty content = 0 lines added");
    }

    #[test]
    fn test_compute_preview_report_add_only() {
        // Test inserting at position (no removal)
        let mut temp_file = NamedTempFile::new().unwrap();
        writeln!(temp_file, "line 1").unwrap();
        temp_file.flush().unwrap();

        let source = std::fs::read_to_string(temp_file.path()).unwrap();
        let start = source.len(); // Insert at end
        let end = start;

        let report =
            compute_preview_report(temp_file.path(), start, end, "NEW LINE 1\nNEW LINE 2\n")
                .unwrap();

        assert_eq!(report.lines_removed, 0, "No lines removed when start==end");
        assert_eq!(report.lines_added, 2, "Should count 2 new lines");
    }

    // TDD test for strict/skip flags being passed through
    #[test]
    fn test_apply_patch_accepts_strict_and_skip_flags() {
        use std::io::Write;
        use tempfile::TempDir;

        // Create a temporary workspace
        let workspace = TempDir::new().unwrap();
        let file_path = workspace.path().join("lib.rs");

        // Create a simple valid Rust file
        {
            let mut file = std::fs::File::create(&file_path).unwrap();
            writeln!(file, "pub fn old() {{ }}").unwrap();
        }

        // Create a minimal Cargo.toml to make it a valid package
        {
            let cargo_toml = workspace.path().join("Cargo.toml");
            let mut file = std::fs::File::create(&cargo_toml).unwrap();
            writeln!(
                file,
                r#"[package]
name = "test"
version = "0.1.0"
edition = "2021"

[lib]
path = "lib.rs"
"#
            )
            .unwrap();
        }

        // Test that apply_patch_with_validation can accept strict and skip parameters
        // This test verifies the function signature includes these parameters
        let content = std::fs::read_to_string(&file_path).unwrap();
        let start = content.find("old()").unwrap();
        let end = start + "old()".len();

        // Mock call - we're testing the function exists and has the right parameters
        // For now, this just verifies the compiles with the correct parameters
        // The actual integration test would verify behavior

        // Note: This test documents the intended behavior
        // strict=true should treat warnings as errors
        // skip=true should bypass pre-verification
        let _ = (start, end);
    }
}
