//! SQL text rewriter that accumulates and applies text replacements.
//!
//! The rewriter collects fix actions and applies them to the SQL string,
//! either using exact source locations or case-insensitive whole-word search.
//! Replacements are applied in reverse offset order to preserve positions.

use super::types::FixAction;

/// A SQL rewriter that accumulates text replacements.
#[derive(Debug)]
pub struct SqlRewriter {
    original: String,
    replacements: Vec<Replacement>,
}

/// A single text replacement to apply.
#[derive(Debug)]
struct Replacement {
    /// Byte offset of the start of the text to replace
    start: usize,
    /// Length in bytes of the text to replace
    length: usize,
    /// The new text to insert
    new_text: String,
}

impl SqlRewriter {
    /// Create a new rewriter for the given SQL string.
    pub fn new(sql: &str) -> Self {
        Self {
            original: sql.to_string(),
            replacements: Vec::new(),
        }
    }

    /// Schedule a replacement based on a fix action.
    ///
    /// If the action has a source location, computes the byte offset from
    /// line/column. Otherwise, does a case-insensitive whole-word text search
    /// for the original text.
    pub fn apply(&mut self, action: &FixAction) {
        if let Some(loc) = &action.location {
            // Compute byte offset from line/column (both 1-based)
            if let Some(offset) = line_col_to_offset(&self.original, loc.line, loc.column) {
                self.replacements.push(Replacement {
                    start: offset,
                    length: action.original.len(),
                    new_text: action.replacement.clone(),
                });
                return;
            }
        }

        // Fallback: case-insensitive whole-word search
        if let Some(offset) = find_whole_word(&self.original, &action.original) {
            self.replacements.push(Replacement {
                start: offset,
                length: action.original.len(),
                new_text: action.replacement.clone(),
            });
        }
    }

    /// Apply all scheduled replacements and return the fixed SQL.
    ///
    /// Applies replacements in reverse offset order to preserve positions.
    /// Skips overlapping replacements (later ones in the original text win
    /// when there's a conflict).
    pub fn finish(mut self) -> String {
        if self.replacements.is_empty() {
            return self.original;
        }

        // Sort by start offset descending so we apply from end to start
        self.replacements.sort_by(|a, b| b.start.cmp(&a.start));

        let mut result = self.original;
        // Track the lowest byte offset that has been modified so far.
        // Any replacement whose range overlaps with this is skipped.
        let mut protected_start: Option<usize> = None;

        for rep in &self.replacements {
            let rep_end = rep.start + rep.length;

            // Check for overlap with a previously applied (higher-offset) replacement
            if let Some(ps) = protected_start
                && rep_end > ps
            {
                // This replacement overlaps — skip it
                continue;
            }

            // Bounds check
            if rep_end > result.len() {
                continue;
            }

            result.replace_range(rep.start..rep_end, &rep.new_text);
            protected_start = Some(rep.start);
        }

        result
    }
}

/// Convert a 1-based line and column to a byte offset in the string.
///
/// Returns `None` if the line/column is out of bounds.
fn line_col_to_offset(s: &str, line: usize, column: usize) -> Option<usize> {
    if line == 0 || column == 0 {
        return None;
    }

    let mut current_line = 1;
    let mut line_start = 0;

    for (i, ch) in s.char_indices() {
        if current_line == line {
            let offset = line_start + (column - 1);
            if offset <= s.len() {
                return Some(offset);
            }
            return None;
        }
        if ch == '\n' {
            current_line += 1;
            line_start = i + 1;
        }
    }

    // Handle the last line (no trailing newline)
    if current_line == line {
        let offset = line_start + (column - 1);
        if offset <= s.len() {
            return Some(offset);
        }
    }

    None
}

/// Find the byte offset of a whole-word, case-insensitive match.
///
/// A "whole word" match means the character before the match (if any) is not
/// alphanumeric or underscore, and the character after the match (if any) is
/// not alphanumeric or underscore.
fn find_whole_word(haystack: &str, needle: &str) -> Option<usize> {
    if needle.is_empty() {
        return None;
    }

    let haystack_lower = haystack.to_lowercase();
    let needle_lower = needle.to_lowercase();

    let mut search_start = 0;
    while let Some(pos) = haystack_lower[search_start..].find(&needle_lower) {
        let absolute_pos = search_start + pos;
        let end_pos = absolute_pos + needle.len();

        let before_ok = if absolute_pos == 0 {
            true
        } else {
            haystack
                .as_bytes()
                .get(absolute_pos - 1)
                .is_none_or(|&b| !is_word_char(b))
        };

        let after_ok = if end_pos >= haystack.len() {
            true
        } else {
            haystack
                .as_bytes()
                .get(end_pos)
                .is_none_or(|&b| !is_word_char(b))
        };

        if before_ok && after_ok {
            return Some(absolute_pos);
        }

        // Move past this match and try again
        search_start = absolute_pos + 1;
    }

    None
}

/// Check if a byte is a word character (alphanumeric or underscore).
fn is_word_char(b: u8) -> bool {
    b.is_ascii_alphanumeric() || b == b'_'
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::fixer::types::{FixAction, FixActionType};

    fn make_action(original: &str, replacement: &str) -> FixAction {
        FixAction {
            action: FixActionType::ReplaceTable,
            original: original.to_string(),
            replacement: replacement.to_string(),
            confidence: 0.95,
            explanation: "test".to_string(),
            location: None,
        }
    }

    #[test]
    fn test_simple_replacement() {
        let mut rewriter = SqlRewriter::new("SELECT * FROM usrs");
        rewriter.apply(&make_action("usrs", "users"));
        let result = rewriter.finish();
        assert_eq!(result, "SELECT * FROM users");
    }

    #[test]
    fn test_case_insensitive_match() {
        let mut rewriter = SqlRewriter::new("SELECT * FROM USRS");
        rewriter.apply(&make_action("USRS", "users"));
        let result = rewriter.finish();
        assert_eq!(result, "SELECT * FROM users");
    }

    #[test]
    fn test_whole_word_boundary() {
        // Should NOT replace "user" inside "user_id"
        let mut rewriter = SqlRewriter::new("SELECT user_id FROM user");
        rewriter.apply(&make_action("user", "users"));
        let result = rewriter.finish();
        assert_eq!(result, "SELECT user_id FROM users");
    }

    #[test]
    fn test_multiple_replacements() {
        let mut rewriter = SqlRewriter::new("SELECT emal FROM usrs");
        rewriter.apply(&make_action("usrs", "users"));
        rewriter.apply(&FixAction {
            action: FixActionType::ReplaceColumn,
            original: "emal".to_string(),
            replacement: "email".to_string(),
            confidence: 0.9,
            explanation: "test".to_string(),
            location: None,
        });
        let result = rewriter.finish();
        assert_eq!(result, "SELECT email FROM users");
    }

    #[test]
    fn test_no_match_preserves_original() {
        let mut rewriter = SqlRewriter::new("SELECT * FROM users");
        rewriter.apply(&make_action("nonexistent", "replacement"));
        let result = rewriter.finish();
        assert_eq!(result, "SELECT * FROM users");
    }

    #[test]
    fn test_empty_replacements() {
        let rewriter = SqlRewriter::new("SELECT * FROM users");
        let result = rewriter.finish();
        assert_eq!(result, "SELECT * FROM users");
    }

    #[test]
    fn test_line_col_to_offset_single_line() {
        let s = "SELECT * FROM users";
        assert_eq!(line_col_to_offset(s, 1, 1), Some(0));
        assert_eq!(line_col_to_offset(s, 1, 15), Some(14)); // 'u' in users
    }

    #[test]
    fn test_line_col_to_offset_multi_line() {
        let s = "SELECT *\nFROM users";
        assert_eq!(line_col_to_offset(s, 2, 1), Some(9)); // 'F' in FROM
        assert_eq!(line_col_to_offset(s, 2, 6), Some(14)); // 'u' in users
    }

    #[test]
    fn test_line_col_to_offset_out_of_bounds() {
        let s = "SELECT";
        assert_eq!(line_col_to_offset(s, 0, 1), None);
        assert_eq!(line_col_to_offset(s, 1, 0), None);
        assert_eq!(line_col_to_offset(s, 5, 1), None);
    }

    #[test]
    fn test_find_whole_word_basic() {
        assert_eq!(find_whole_word("SELECT * FROM users", "users"), Some(14));
        assert_eq!(find_whole_word("SELECT * FROM users", "USERS"), Some(14));
    }

    #[test]
    fn test_find_whole_word_no_partial_match() {
        // "user" should not match inside "user_id"
        assert_eq!(find_whole_word("SELECT user_id FROM t", "user"), None);
    }

    #[test]
    fn test_find_whole_word_at_boundaries() {
        assert_eq!(find_whole_word("user FROM t", "user"), Some(0));
        assert_eq!(find_whole_word("SELECT user", "user"), Some(7));
    }

    #[test]
    fn test_find_whole_word_empty_needle() {
        assert_eq!(find_whole_word("SELECT * FROM users", ""), None);
    }
}
