use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::LazyLock;

use crate::parser;
use crate::plugins::{
    PluginManager, PluginReturnKind, PluginSignature, extract_plugin_call_name,
    plugin_signature_map,
};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AssertionTypeMismatch {
    pub rule_id: String,
    pub line: usize,
    pub expression: String,
    pub message: String,
    pub expected: String,
    pub actual: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UnknownPluginCall {
    pub rule_id: String,
    pub line: usize,
    pub expression: String,
    pub plugin_name: String,
    pub message: String,
    pub suggestion: Option<String>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ValueKind {
    Boolean,
    Number,
    String,
    Array,
    Object,
    Unknown,
}

#[derive(Debug, Clone, Copy)]
enum OperatorContractKind {
    SameKnownType,
    BothNumber,
    BothString,
    Contains,
}

#[derive(Debug, Clone, Copy)]
struct OperatorContract {
    rule_id: &'static str,
    kind: OperatorContractKind,
}

impl ValueKind {
    fn as_str(self) -> &'static str {
        match self {
            Self::Boolean => "boolean",
            Self::Number => "number",
            Self::String => "string",
            Self::Array => "array",
            Self::Object => "object",
            Self::Unknown => "unknown",
        }
    }
}

fn operator_from_expression(expr: &str) -> Option<(&'static str, usize, usize)> {
    for op in ["==", "!=", ">=", "<=", ">", "<"] {
        if let Some(idx) = expr.find(op) {
            return Some((op, idx, op.len()));
        }
    }

    for op in ["contains", "matches", "startsWith", "endsWith"] {
        let token = format!(" {} ", op);
        if let Some(idx) = expr.find(&token) {
            return Some((op, idx, token.len()));
        }
    }

    None
}

fn operator_contract(op: &str) -> Option<OperatorContract> {
    match op {
        "==" | "!=" => Some(OperatorContract {
            rule_id: "SEM_T001",
            kind: OperatorContractKind::SameKnownType,
        }),
        ">" | "<" | ">=" | "<=" => Some(OperatorContract {
            rule_id: "SEM_T002",
            kind: OperatorContractKind::BothNumber,
        }),
        "matches" | "startsWith" | "endsWith" => Some(OperatorContract {
            rule_id: "SEM_T003",
            kind: OperatorContractKind::BothString,
        }),
        "contains" => Some(OperatorContract {
            rule_id: "SEM_T004",
            kind: OperatorContractKind::Contains,
        }),
        _ => None,
    }
}

fn plugin_signatures() -> &'static HashMap<String, PluginSignature> {
    static PLUGIN_SIGNATURES: LazyLock<HashMap<String, PluginSignature>> =
        LazyLock::new(plugin_signature_map);
    &PLUGIN_SIGNATURES
}

fn section_content_line(start_line: usize, idx: usize) -> usize {
    start_line + idx + 2
}

fn extract_plugin_calls(expr: &str) -> Vec<String> {
    let chars: Vec<char> = expr.chars().collect();
    let mut calls = Vec::new();
    let mut i = 0;

    while i < chars.len() {
        if chars[i] != '@' {
            i += 1;
            continue;
        }

        let start = i + 1;
        let mut end = start;
        while end < chars.len() && (chars[end].is_ascii_alphanumeric() || chars[end] == '_') {
            end += 1;
        }

        if end == start {
            i += 1;
            continue;
        }

        let mut cursor = end;
        while cursor < chars.len() && chars[cursor].is_whitespace() {
            cursor += 1;
        }

        if cursor < chars.len() && chars[cursor] == '(' {
            let name: String = chars[start..end].iter().collect();
            calls.push(name);
        }

        i = end;
    }

    calls
}

fn best_plugin_suggestion(unknown: &str, known_plugins: &[String]) -> Option<String> {
    fn common_prefix_len(a: &str, b: &str) -> usize {
        a.chars().zip(b.chars()).take_while(|(x, y)| x == y).count()
    }

    let mut best: Option<(&str, usize, usize)> = None;
    for candidate in known_plugins {
        let prefix = common_prefix_len(unknown, candidate);
        let len_diff = unknown.len().abs_diff(candidate.len());

        match best {
            None => best = Some((candidate.as_str(), prefix, len_diff)),
            Some((_, best_prefix, best_len_diff)) => {
                if prefix > best_prefix || (prefix == best_prefix && len_diff < best_len_diff) {
                    best = Some((candidate.as_str(), prefix, len_diff));
                }
            }
        }
    }

    best.and_then(|(name, prefix, _)| {
        if prefix >= 3 {
            Some(name.to_string())
        } else {
            None
        }
    })
}

fn infer_value_kind(expr: &str, signatures: &HashMap<String, PluginSignature>) -> ValueKind {
    let trimmed = expr.trim();

    if trimmed == "true" || trimmed == "false" {
        return ValueKind::Boolean;
    }

    if trimmed.starts_with('"') && trimmed.ends_with('"') && trimmed.len() >= 2 {
        return ValueKind::String;
    }

    if trimmed.starts_with('[') && trimmed.ends_with(']') {
        return ValueKind::Array;
    }

    if trimmed.starts_with('{') && trimmed.ends_with('}') {
        return ValueKind::Object;
    }

    if trimmed.parse::<f64>().is_ok() {
        return ValueKind::Number;
    }

    if let Some(plugin_name) = extract_plugin_call_name(trimmed) {
        return match signatures.get(plugin_name.as_str()).map(|s| s.return_kind) {
            Some(PluginReturnKind::Boolean) => ValueKind::Boolean,
            Some(PluginReturnKind::Number) => ValueKind::Number,
            Some(PluginReturnKind::String) => ValueKind::String,
            _ => ValueKind::Unknown,
        };
    }

    ValueKind::Unknown
}

fn detect_type_mismatch(
    expr: &str,
    signatures: &HashMap<String, PluginSignature>,
) -> Option<AssertionTypeMismatch> {
    let (op, op_idx, op_len) = operator_from_expression(expr)?;
    let contract = operator_contract(op)?;
    let lhs = expr[..op_idx].trim();
    let rhs = expr[op_idx + op_len..].trim();
    if lhs.is_empty() || rhs.is_empty() {
        return None;
    }

    let lhs_kind = infer_value_kind(lhs, signatures);
    let rhs_kind = infer_value_kind(rhs, signatures);

    match contract.kind {
        OperatorContractKind::SameKnownType => {
            if lhs_kind != ValueKind::Unknown
                && rhs_kind != ValueKind::Unknown
                && lhs_kind != rhs_kind
            {
                return Some(AssertionTypeMismatch {
                    rule_id: contract.rule_id.to_string(),
                    line: 0,
                    expression: expr.to_string(),
                    message: format!(
                        "Type-incompatible comparison: {} is {}, but {} is {}",
                        lhs,
                        lhs_kind.as_str(),
                        rhs,
                        rhs_kind.as_str()
                    ),
                    expected: lhs_kind.as_str().to_string(),
                    actual: rhs_kind.as_str().to_string(),
                });
            }
        }
        OperatorContractKind::BothNumber => {
            for (side_expr, side_kind) in [(lhs, lhs_kind), (rhs, rhs_kind)] {
                if side_kind != ValueKind::Unknown && side_kind != ValueKind::Number {
                    return Some(AssertionTypeMismatch {
                        rule_id: contract.rule_id.to_string(),
                        line: 0,
                        expression: expr.to_string(),
                        message: format!(
                            "Ordering operator '{}' requires numbers, but {} is {}",
                            op,
                            side_expr,
                            side_kind.as_str()
                        ),
                        expected: "number".to_string(),
                        actual: side_kind.as_str().to_string(),
                    });
                }
            }
        }
        OperatorContractKind::BothString => {
            for (side_expr, side_kind) in [(lhs, lhs_kind), (rhs, rhs_kind)] {
                if side_kind != ValueKind::Unknown && side_kind != ValueKind::String {
                    return Some(AssertionTypeMismatch {
                        rule_id: contract.rule_id.to_string(),
                        line: 0,
                        expression: expr.to_string(),
                        message: format!(
                            "Operator '{}' requires strings, but {} is {}",
                            op,
                            side_expr,
                            side_kind.as_str()
                        ),
                        expected: "string".to_string(),
                        actual: side_kind.as_str().to_string(),
                    });
                }
            }
        }
        OperatorContractKind::Contains => {
            if lhs_kind == ValueKind::String
                && rhs_kind != ValueKind::Unknown
                && rhs_kind != ValueKind::String
            {
                return Some(AssertionTypeMismatch {
                    rule_id: contract.rule_id.to_string(),
                    line: 0,
                    expression: expr.to_string(),
                    message: format!(
                        "Operator 'contains' with string LHS requires string RHS, but {} is {}",
                        rhs,
                        rhs_kind.as_str()
                    ),
                    expected: "string".to_string(),
                    actual: rhs_kind.as_str().to_string(),
                });
            }
        }
    }

    None
}

pub fn validate_plugin_semantics_completeness() -> Vec<String> {
    let mut issues = Vec::new();
    for plugin in PluginManager::new().list() {
        let name = plugin.name().to_string();
        let sig = plugin.signature();

        if sig.return_kind == PluginReturnKind::Unknown {
            issues.push(format!("{}: return_kind is Unknown", name));
        }
        let _ = sig.arg_names;
    }
    issues
}

pub fn collect_assertion_type_mismatches(doc: &parser::GctfDocument) -> Vec<AssertionTypeMismatch> {
    let signatures = plugin_signatures();
    let mut mismatches = Vec::new();

    for section in &doc.sections {
        if section.section_type != parser::ast::SectionType::Asserts {
            continue;
        }

        for (idx, line) in section.raw_content.lines().enumerate() {
            let trimmed = line.trim();
            if trimmed.is_empty() || trimmed.starts_with('#') || trimmed.starts_with("//") {
                continue;
            }

            if let Some(mut mismatch) = detect_type_mismatch(trimmed, signatures) {
                mismatch.line = section_content_line(section.start_line, idx);
                mismatches.push(mismatch);
            }
        }
    }

    mismatches
}

pub fn collect_unknown_plugin_calls(doc: &parser::GctfDocument) -> Vec<UnknownPluginCall> {
    let signatures = plugin_signatures();
    let mut known_plugins: Vec<String> = signatures.keys().cloned().collect();
    known_plugins.sort();

    let mut unknown = Vec::new();

    for section in &doc.sections {
        if section.section_type != parser::ast::SectionType::Asserts {
            continue;
        }

        for (idx, line) in section.raw_content.lines().enumerate() {
            let trimmed = line.trim();
            if trimmed.is_empty() || trimmed.starts_with('#') || trimmed.starts_with("//") {
                continue;
            }

            for plugin_name in extract_plugin_calls(trimmed) {
                if signatures.contains_key(plugin_name.as_str()) {
                    continue;
                }

                let suggestion =
                    best_plugin_suggestion(&plugin_name, &known_plugins).map(|s| format!("@{}", s));
                let message = match &suggestion {
                    Some(s) => format!(
                        "Unknown assertion plugin '@{}'. Did you mean {}?",
                        plugin_name, s
                    ),
                    None => format!("Unknown assertion plugin '@{}'", plugin_name),
                };

                unknown.push(UnknownPluginCall {
                    rule_id: "SEM_F001".to_string(),
                    line: section_content_line(section.start_line, idx),
                    expression: trimmed.to_string(),
                    plugin_name,
                    message,
                    suggestion,
                });
            }
        }
    }

    unknown
}

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

    #[test]
    fn test_semantics_detects_boolean_vs_number() {
        let content = r#"--- ENDPOINT ---
test.Service/Method

--- ASSERTS ---
@len(.names) == true
"#;

        let doc = parser::parse_gctf_from_str(content, "test.gctf").unwrap();
        let mismatches = collect_assertion_type_mismatches(&doc);
        assert_eq!(mismatches.len(), 1);
        assert_eq!(mismatches[0].rule_id, "SEM_T001");
    }

    #[test]
    fn test_semantics_allows_boolean_compare() {
        let content = r#"--- ENDPOINT ---
test.Service/Method

--- ASSERTS ---
@has_header("x-request-id") == true
"#;

        let doc = parser::parse_gctf_from_str(content, "test.gctf").unwrap();
        let mismatches = collect_assertion_type_mismatches(&doc);
        assert!(mismatches.is_empty());
    }

    #[test]
    fn test_semantics_detects_startswith_non_string() {
        let content = r#"--- ENDPOINT ---
test.Service/Method

--- ASSERTS ---
@len(.names) startsWith "a"
"#;

        let doc = parser::parse_gctf_from_str(content, "test.gctf").unwrap();
        let mismatches = collect_assertion_type_mismatches(&doc);
        assert_eq!(mismatches.len(), 1);
        assert_eq!(mismatches[0].rule_id, "SEM_T003");
    }

    #[test]
    fn test_plugin_semantics_completeness() {
        let issues = validate_plugin_semantics_completeness();
        assert!(issues.is_empty(), "Incomplete plugin semantics: {issues:?}");
    }

    #[test]
    fn test_semantics_detects_unknown_plugin_calls() {
        let content = r#"--- ENDPOINT ---
test.Service/Method

--- ASSERTS ---
@regexp(.name, "^a") == true
"#;

        let doc = parser::parse_gctf_from_str(content, "test.gctf").unwrap();
        let unknown = collect_unknown_plugin_calls(&doc);
        assert_eq!(unknown.len(), 1);
        assert_eq!(unknown[0].rule_id, "SEM_F001");
        assert_eq!(unknown[0].plugin_name, "regexp");
        assert_eq!(unknown[0].suggestion.as_deref(), Some("@regex"));
    }

    #[test]
    fn test_semantics_allows_known_plugin_calls() {
        let content = r#"--- ENDPOINT ---
test.Service/Method

--- ASSERTS ---
@regex(.name, "^a") == true
"#;

        let doc = parser::parse_gctf_from_str(content, "test.gctf").unwrap();
        let unknown = collect_unknown_plugin_calls(&doc);
        assert!(unknown.is_empty());
    }
}
