//! Autocomplete provider for CalcPad.
//!
//! Provides completion suggestions for variables, functions, keywords, and units
//! based on the current cursor position and sheet content.
//!
//! This module is purely text-based — it does not depend on the evaluation engine.
//! It scans the sheet content for variable declarations and matches against
//! built-in registries of functions, keywords, and units.

use serde::{Deserialize, Serialize};

/// The kind of completion item.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum CompletionKind {
    /// A user-declared variable.
    Variable,
    /// A built-in math function.
    Function,
    /// An aggregator keyword (sum, total, etc.).
    Keyword,
    /// A unit suffix (kg, km, etc.).
    Unit,
}

/// A single autocomplete suggestion.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CompletionItem {
    /// Display label for the suggestion.
    pub label: String,
    /// Text to insert when the suggestion is accepted.
    pub insert_text: String,
    /// Category of the completion.
    pub kind: CompletionKind,
    /// Optional description/detail.
    pub detail: Option<String>,
}

/// Context for computing autocomplete suggestions.
pub struct CompletionContext<'a> {
    /// Current line text.
    pub line: &'a str,
    /// Cursor position within the line (0-indexed byte offset).
    pub cursor: usize,
    /// Full sheet content (all lines joined by newlines).
    pub sheet_content: &'a str,
    /// Current line number (1-indexed).
    pub line_number: usize,
}

/// Result of an autocomplete query.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CompletionResult {
    /// Matching completion items.
    pub items: Vec<CompletionItem>,
    /// The prefix being matched.
    pub prefix: String,
    /// Start position for text replacement in the line (byte offset).
    pub replace_start: usize,
    /// End position for text replacement in the line (byte offset).
    pub replace_end: usize,
}

/// Info about the extracted prefix at the cursor.
struct PrefixInfo {
    prefix: String,
    start: usize,
    end: usize,
    is_unit_context: bool,
}

// --- Built-in registries ---

fn keyword_completions() -> Vec<CompletionItem> {
    vec![
        CompletionItem {
            label: "sum".to_string(),
            insert_text: "sum".to_string(),
            kind: CompletionKind::Keyword,
            detail: Some("Sum of section values".to_string()),
        },
        CompletionItem {
            label: "total".to_string(),
            insert_text: "total".to_string(),
            kind: CompletionKind::Keyword,
            detail: Some("Total of section values".to_string()),
        },
        CompletionItem {
            label: "subtotal".to_string(),
            insert_text: "subtotal".to_string(),
            kind: CompletionKind::Keyword,
            detail: Some("Subtotal of section values".to_string()),
        },
        CompletionItem {
            label: "average".to_string(),
            insert_text: "average".to_string(),
            kind: CompletionKind::Keyword,
            detail: Some("Average of section values".to_string()),
        },
        CompletionItem {
            label: "count".to_string(),
            insert_text: "count".to_string(),
            kind: CompletionKind::Keyword,
            detail: Some("Count of section values".to_string()),
        },
        CompletionItem {
            label: "prev".to_string(),
            insert_text: "prev".to_string(),
            kind: CompletionKind::Keyword,
            detail: Some("Previous line result".to_string()),
        },
    ]
}

fn function_completions() -> Vec<CompletionItem> {
    vec![
        CompletionItem {
            label: "sqrt".to_string(),
            insert_text: "sqrt(".to_string(),
            kind: CompletionKind::Function,
            detail: Some("Square root".to_string()),
        },
        CompletionItem {
            label: "abs".to_string(),
            insert_text: "abs(".to_string(),
            kind: CompletionKind::Function,
            detail: Some("Absolute value".to_string()),
        },
        CompletionItem {
            label: "round".to_string(),
            insert_text: "round(".to_string(),
            kind: CompletionKind::Function,
            detail: Some("Round to nearest integer".to_string()),
        },
        CompletionItem {
            label: "floor".to_string(),
            insert_text: "floor(".to_string(),
            kind: CompletionKind::Function,
            detail: Some("Round down".to_string()),
        },
        CompletionItem {
            label: "ceil".to_string(),
            insert_text: "ceil(".to_string(),
            kind: CompletionKind::Function,
            detail: Some("Round up".to_string()),
        },
        CompletionItem {
            label: "log".to_string(),
            insert_text: "log(".to_string(),
            kind: CompletionKind::Function,
            detail: Some("Base-10 logarithm".to_string()),
        },
        CompletionItem {
            label: "ln".to_string(),
            insert_text: "ln(".to_string(),
            kind: CompletionKind::Function,
            detail: Some("Natural logarithm".to_string()),
        },
        CompletionItem {
            label: "sin".to_string(),
            insert_text: "sin(".to_string(),
            kind: CompletionKind::Function,
            detail: Some("Sine".to_string()),
        },
        CompletionItem {
            label: "cos".to_string(),
            insert_text: "cos(".to_string(),
            kind: CompletionKind::Function,
            detail: Some("Cosine".to_string()),
        },
        CompletionItem {
            label: "tan".to_string(),
            insert_text: "tan(".to_string(),
            kind: CompletionKind::Function,
            detail: Some("Tangent".to_string()),
        },
    ]
}

fn unit_completions() -> Vec<CompletionItem> {
    vec![
        // Mass
        CompletionItem { label: "kg".to_string(), insert_text: "kg".to_string(), kind: CompletionKind::Unit, detail: Some("Kilograms".to_string()) },
        CompletionItem { label: "lb".to_string(), insert_text: "lb".to_string(), kind: CompletionKind::Unit, detail: Some("Pounds".to_string()) },
        CompletionItem { label: "oz".to_string(), insert_text: "oz".to_string(), kind: CompletionKind::Unit, detail: Some("Ounces".to_string()) },
        CompletionItem { label: "mg".to_string(), insert_text: "mg".to_string(), kind: CompletionKind::Unit, detail: Some("Milligrams".to_string()) },
        // Length
        CompletionItem { label: "km".to_string(), insert_text: "km".to_string(), kind: CompletionKind::Unit, detail: Some("Kilometers".to_string()) },
        CompletionItem { label: "mm".to_string(), insert_text: "mm".to_string(), kind: CompletionKind::Unit, detail: Some("Millimeters".to_string()) },
        CompletionItem { label: "cm".to_string(), insert_text: "cm".to_string(), kind: CompletionKind::Unit, detail: Some("Centimeters".to_string()) },
        CompletionItem { label: "ft".to_string(), insert_text: "ft".to_string(), kind: CompletionKind::Unit, detail: Some("Feet".to_string()) },
        CompletionItem { label: "in".to_string(), insert_text: "in".to_string(), kind: CompletionKind::Unit, detail: Some("Inches".to_string()) },
        // Volume
        CompletionItem { label: "ml".to_string(), insert_text: "ml".to_string(), kind: CompletionKind::Unit, detail: Some("Milliliters".to_string()) },
        // Data
        CompletionItem { label: "kB".to_string(), insert_text: "kB".to_string(), kind: CompletionKind::Unit, detail: Some("Kilobytes".to_string()) },
        CompletionItem { label: "MB".to_string(), insert_text: "MB".to_string(), kind: CompletionKind::Unit, detail: Some("Megabytes".to_string()) },
        CompletionItem { label: "GB".to_string(), insert_text: "GB".to_string(), kind: CompletionKind::Unit, detail: Some("Gigabytes".to_string()) },
        CompletionItem { label: "TB".to_string(), insert_text: "TB".to_string(), kind: CompletionKind::Unit, detail: Some("Terabytes".to_string()) },
        // Time
        CompletionItem { label: "ms".to_string(), insert_text: "ms".to_string(), kind: CompletionKind::Unit, detail: Some("Milliseconds".to_string()) },
        CompletionItem { label: "hr".to_string(), insert_text: "hr".to_string(), kind: CompletionKind::Unit, detail: Some("Hours".to_string()) },
    ]
}

// --- Prefix extraction ---

/// Extract the identifier prefix at the cursor position.
///
/// Handles unit context detection: when letters immediately follow digits
/// (e.g., "50km"), the prefix is the letter portion ("km") and is_unit_context is true.
fn extract_prefix(line: &str, cursor: usize) -> Option<PrefixInfo> {
    if cursor == 0 || cursor > line.len() {
        return None;
    }

    let bytes = line.as_bytes();

    // Walk backwards collecting word characters (alphanumeric + underscore)
    let mut start = cursor;
    while start > 0 && is_word_char(bytes[start - 1]) {
        start -= 1;
    }

    if start == cursor {
        return None;
    }

    let full_word = &line[start..cursor];

    // If the word starts with a letter or underscore, it's a normal identifier prefix
    if full_word.as_bytes()[0].is_ascii_alphabetic() || full_word.as_bytes()[0] == b'_' {
        return Some(PrefixInfo {
            prefix: full_word.to_string(),
            start,
            end: cursor,
            is_unit_context: false,
        });
    }

    // Word starts with digits — find where letters begin for unit context
    let letter_start = full_word
        .bytes()
        .position(|b| b.is_ascii_alphabetic() || b == b'_');

    match letter_start {
        Some(offset) => {
            let prefix = full_word[offset..].to_string();
            let abs_start = start + offset;
            Some(PrefixInfo {
                prefix,
                start: abs_start,
                end: cursor,
                is_unit_context: true,
            })
        }
        None => None, // All digits, no completion
    }
}

fn is_word_char(b: u8) -> bool {
    b.is_ascii_alphanumeric() || b == b'_'
}

// --- Variable extraction ---

/// Extract declared variable names from the sheet content.
/// Scans each line for `identifier = expression` patterns.
/// Excludes the current line to avoid self-reference.
fn extract_variables(sheet_content: &str, current_line_number: usize) -> Vec<CompletionItem> {
    let mut seen = std::collections::HashSet::new();
    let mut items = Vec::new();

    for (i, line) in sheet_content.lines().enumerate() {
        let line_num = i + 1; // 1-indexed
        if line_num == current_line_number {
            continue;
        }

        let trimmed = line.trim();
        if let Some(name) = extract_variable_name(trimmed) {
            if !seen.contains(&name) {
                seen.insert(name.clone());
                items.push(CompletionItem {
                    label: name.clone(),
                    insert_text: name,
                    kind: CompletionKind::Variable,
                    detail: Some(format!("Variable (line {})", line_num)),
                });
            }
        }
    }

    items
}

/// Extract the variable name from an assignment line.
/// Returns Some(name) if the line matches `identifier = ...`.
fn extract_variable_name(line: &str) -> Option<String> {
    let bytes = line.as_bytes();
    if bytes.is_empty() || (!bytes[0].is_ascii_alphabetic() && bytes[0] != b'_') {
        return None;
    }

    let mut i = 0;
    while i < bytes.len() && is_word_char(bytes[i]) {
        i += 1;
    }
    let name = &line[..i];

    // Skip whitespace
    while i < bytes.len() && bytes[i].is_ascii_whitespace() {
        i += 1;
    }

    // Must be followed by '=' but not '=='
    if i < bytes.len() && bytes[i] == b'=' {
        if i + 1 < bytes.len() && bytes[i + 1] == b'=' {
            return None; // comparison, not assignment
        }
        return Some(name.to_string());
    }

    None
}

// --- Core completion function ---

/// Get autocomplete suggestions for the current cursor position.
///
/// Returns `None` if:
/// - The prefix is less than 2 characters
/// - No completions match the prefix
pub fn get_completions(context: &CompletionContext) -> Option<CompletionResult> {
    let prefix_info = extract_prefix(context.line, context.cursor)?;

    // Enforce 2+ character minimum threshold
    if prefix_info.prefix.len() < 2 {
        return None;
    }

    let lower_prefix = prefix_info.prefix.to_lowercase();

    let candidates = if prefix_info.is_unit_context {
        // Unit context: only suggest units
        unit_completions()
    } else {
        // General context: suggest variables, functions, and keywords
        let variables = extract_variables(context.sheet_content, context.line_number);
        let mut all = variables;
        all.extend(function_completions());
        all.extend(keyword_completions());
        all
    };

    // Filter by case-insensitive prefix match
    let mut filtered: Vec<CompletionItem> = candidates
        .into_iter()
        .filter(|item| item.label.to_lowercase().starts_with(&lower_prefix))
        .collect();

    if filtered.is_empty() {
        return None;
    }

    // Sort: exact case match first, then alphabetical
    let prefix_clone = prefix_info.prefix.clone();
    filtered.sort_by(|a, b| {
        let a_exact = if a.label.starts_with(&prefix_clone) { 0 } else { 1 };
        let b_exact = if b.label.starts_with(&prefix_clone) { 0 } else { 1 };
        if a_exact != b_exact {
            return a_exact.cmp(&b_exact);
        }
        a.label.cmp(&b.label)
    });

    Some(CompletionResult {
        items: filtered,
        prefix: prefix_info.prefix,
        replace_start: prefix_info.start,
        replace_end: prefix_info.end,
    })
}

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

    fn make_context<'a>(
        line: &'a str,
        cursor: usize,
        sheet: &'a str,
        line_number: usize,
    ) -> CompletionContext<'a> {
        CompletionContext {
            line,
            cursor,
            sheet_content: sheet,
            line_number,
        }
    }

    // --- AC1: Variable suggestions for 2+ character prefix ---

    #[test]
    fn test_variable_suggestions() {
        let sheet = "monthly_rent = 1250\nmonthly_insurance = 200\nmortgage_payment = 800\n";
        let ctx = make_context("mo", 2, sheet, 4);
        let result = get_completions(&ctx).unwrap();
        assert_eq!(result.prefix, "mo");
        assert!(result.items.len() >= 2);
        let labels: Vec<&str> = result.items.iter().map(|i| i.label.as_str()).collect();
        assert!(labels.contains(&"monthly_rent"));
        assert!(labels.contains(&"monthly_insurance"));
        assert!(labels.contains(&"mortgage_payment"));
    }

    // --- AC4: Built-in function suggestions ---

    #[test]
    fn test_function_suggestions_sq() {
        let ctx = make_context("sq", 2, "", 1);
        let result = get_completions(&ctx).unwrap();
        let labels: Vec<&str> = result.items.iter().map(|i| i.label.as_str()).collect();
        assert!(labels.contains(&"sqrt"));
    }

    // --- AC5: No suggestions for single character ---

    #[test]
    fn test_no_suggestions_single_char() {
        let ctx = make_context("m", 1, "", 1);
        let result = get_completions(&ctx);
        assert!(result.is_none());
    }

    // --- AC6: No suggestions when nothing matches ---

    #[test]
    fn test_no_suggestions_no_match() {
        let ctx = make_context("zzzz", 4, "", 1);
        let result = get_completions(&ctx);
        assert!(result.is_none());
    }

    // --- AC7: Unit context after number ---

    #[test]
    fn test_unit_context_after_number() {
        let ctx = make_context("50km", 4, "", 1);
        let result = get_completions(&ctx).unwrap();
        assert!(result.items.iter().all(|i| i.kind == CompletionKind::Unit));
        let labels: Vec<&str> = result.items.iter().map(|i| i.label.as_str()).collect();
        assert!(labels.contains(&"km"));
    }

    #[test]
    fn test_unit_context_kg() {
        let ctx = make_context("50kg", 4, "", 1);
        let result = get_completions(&ctx).unwrap();
        let labels: Vec<&str> = result.items.iter().map(|i| i.label.as_str()).collect();
        assert!(labels.contains(&"kg"));
    }

    // --- Edge cases ---

    #[test]
    fn test_empty_line() {
        let ctx = make_context("", 0, "", 1);
        let result = get_completions(&ctx);
        assert!(result.is_none());
    }

    #[test]
    fn test_cursor_at_start() {
        let ctx = make_context("sum", 0, "", 1);
        let result = get_completions(&ctx);
        assert!(result.is_none());
    }

    #[test]
    fn test_excludes_current_line_variables() {
        let sheet = "my_var = 10\nmy_other = 20";
        // Cursor is on line 1 typing "my" — should not suggest my_var from line 1
        let ctx = make_context("my", 2, sheet, 1);
        let result = get_completions(&ctx).unwrap();
        let labels: Vec<&str> = result.items.iter().map(|i| i.label.as_str()).collect();
        assert!(!labels.contains(&"my_var")); // excluded: same line
        assert!(labels.contains(&"my_other")); // included: different line
    }

    #[test]
    fn test_keyword_suggestions() {
        let ctx = make_context("su", 2, "", 1);
        let result = get_completions(&ctx).unwrap();
        let labels: Vec<&str> = result.items.iter().map(|i| i.label.as_str()).collect();
        assert!(labels.contains(&"sum"));
        assert!(labels.contains(&"subtotal"));
    }

    #[test]
    fn test_replace_range() {
        let ctx = make_context("x + sq", 6, "", 1);
        let result = get_completions(&ctx).unwrap();
        assert_eq!(result.prefix, "sq");
        assert_eq!(result.replace_start, 4);
        assert_eq!(result.replace_end, 6);
    }

    #[test]
    fn test_prev_suggestion() {
        let ctx = make_context("pr", 2, "", 1);
        let result = get_completions(&ctx).unwrap();
        let labels: Vec<&str> = result.items.iter().map(|i| i.label.as_str()).collect();
        assert!(labels.contains(&"prev"));
    }

    #[test]
    fn test_case_insensitive_matching() {
        let ctx = make_context("SU", 2, "", 1);
        let result = get_completions(&ctx).unwrap();
        let labels: Vec<&str> = result.items.iter().map(|i| i.label.as_str()).collect();
        assert!(labels.contains(&"sum"));
        assert!(labels.contains(&"subtotal"));
    }

    // --- Variable extraction ---

    #[test]
    fn test_extract_variable_name_valid() {
        assert_eq!(extract_variable_name("x = 5"), Some("x".to_string()));
        assert_eq!(
            extract_variable_name("tax_rate = 0.15"),
            Some("tax_rate".to_string())
        );
        assert_eq!(
            extract_variable_name("_temp = 100"),
            Some("_temp".to_string())
        );
        assert_eq!(
            extract_variable_name("item1 = 42"),
            Some("item1".to_string())
        );
    }

    #[test]
    fn test_extract_variable_name_invalid() {
        assert_eq!(extract_variable_name("5 + 3"), None);
        assert_eq!(extract_variable_name("== 5"), None);
        assert_eq!(extract_variable_name(""), None);
        assert_eq!(extract_variable_name("x == 5"), None);
    }
}
