//! Simple expression evaluator using the shunting-yard algorithm.
//!
//! Supports: `+`, `-`, `*`, `/`, `()`, float literals, variable names (rule names),
//! and built-in functions `min(a,b)` and `max(a,b)`.

use std::collections::HashMap;

/// A token produced by the lexer.
#[derive(Debug, Clone, PartialEq)]
enum Token {
    Number(f64),
    Ident(String),
    Plus,
    Minus,
    Star,
    Slash,
    LParen,
    RParen,
    Comma,
    Func(String), // min, max
}

/// A compiled expression in postfix (RPN) form for fast evaluation.
#[derive(Debug, Clone)]
pub enum Op {
    Push(f64),
    PushVar(String),
    Add,
    Sub,
    Mul,
    Div,
    CallMin,
    CallMax,
}

/// Parse error for expression compilation.
#[derive(Debug, Clone)]
pub struct ExprError(pub String);

impl std::fmt::Display for ExprError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "expression error: {}", self.0)
    }
}

impl std::error::Error for ExprError {}

/// Tokenize an expression string.
fn tokenize(input: &str) -> Result<Vec<Token>, ExprError> {
    let mut tokens = Vec::new();
    let chars: Vec<char> = input.chars().collect();
    let mut i = 0;

    while i < chars.len() {
        match chars[i] {
            ' ' | '\t' | '\n' | '\r' => { i += 1; }
            '+' => { tokens.push(Token::Plus); i += 1; }
            '-' => {
                // Unary minus: if at start, after operator, after '(' or ','
                let is_unary = tokens.is_empty()
                    || matches!(tokens.last(), Some(Token::Plus | Token::Minus | Token::Star | Token::Slash | Token::LParen | Token::Comma));
                if is_unary {
                    // Parse as negative number or negate next token
                    i += 1;
                    // Skip whitespace after minus
                    while i < chars.len() && chars[i] == ' ' { i += 1; }
                    if i < chars.len() && (chars[i].is_ascii_digit() || chars[i] == '.') {
                        let start = i;
                        while i < chars.len() && (chars[i].is_ascii_digit() || chars[i] == '.') {
                            i += 1;
                        }
                        let num_str: String = chars[start..i].iter().collect();
                        let val: f64 = num_str.parse().map_err(|_| ExprError(format!("invalid number: -{}", num_str)))?;
                        tokens.push(Token::Number(-val));
                    } else {
                        // Unary minus before identifier or paren: push -1 * ...
                        tokens.push(Token::Number(-1.0));
                        tokens.push(Token::Star);
                    }
                } else {
                    tokens.push(Token::Minus);
                    i += 1;
                }
            }
            '*' => { tokens.push(Token::Star); i += 1; }
            '/' => { tokens.push(Token::Slash); i += 1; }
            '(' => { tokens.push(Token::LParen); i += 1; }
            ')' => { tokens.push(Token::RParen); i += 1; }
            ',' => { tokens.push(Token::Comma); i += 1; }
            c if c.is_ascii_digit() || c == '.' => {
                let start = i;
                while i < chars.len() && (chars[i].is_ascii_digit() || chars[i] == '.') {
                    i += 1;
                }
                let num_str: String = chars[start..i].iter().collect();
                let val: f64 = num_str.parse().map_err(|_| ExprError(format!("invalid number: {}", num_str)))?;
                tokens.push(Token::Number(val));
            }
            c if c.is_ascii_alphabetic() || c == '_' => {
                let start = i;
                while i < chars.len() && (chars[i].is_ascii_alphanumeric() || chars[i] == '_') {
                    i += 1;
                }
                let ident: String = chars[start..i].iter().collect();
                // Check if this is a function call (followed by '(')
                let mut j = i;
                while j < chars.len() && chars[j] == ' ' { j += 1; }
                if j < chars.len() && chars[j] == '(' && (ident == "min" || ident == "max") {
                    tokens.push(Token::Func(ident));
                } else {
                    tokens.push(Token::Ident(ident));
                }
            }
            c => return Err(ExprError(format!("unexpected character: '{}'", c))),
        }
    }
    Ok(tokens)
}

fn precedence(tok: &Token) -> u8 {
    match tok {
        Token::Plus | Token::Minus => 1,
        Token::Star | Token::Slash => 2,
        _ => 0,
    }
}

/// Compile an expression string into a postfix operation list using shunting-yard.
pub fn compile_expression(input: &str) -> Result<Vec<Op>, ExprError> {
    if input.trim().is_empty() {
        return Err(ExprError("empty expression".to_string()));
    }

    let tokens = tokenize(input)?;
    let mut output: Vec<Op> = Vec::new();
    let mut op_stack: Vec<Token> = Vec::new();

    for token in tokens {
        match token {
            Token::Number(val) => output.push(Op::Push(val)),
            Token::Ident(name) => output.push(Op::PushVar(name)),
            Token::Func(_) => op_stack.push(token),
            Token::Comma => {
                // Pop until we hit a left paren (function arg separator)
                while let Some(top) = op_stack.last() {
                    if *top == Token::LParen {
                        break;
                    }
                    let op = op_stack.pop().unwrap();
                    push_operator(&op, &mut output)?;
                }
            }
            Token::Plus | Token::Minus | Token::Star | Token::Slash => {
                while let Some(top) = op_stack.last() {
                    if *top == Token::LParen || matches!(top, Token::Func(_)) {
                        break;
                    }
                    if precedence(top) >= precedence(&token) {
                        let op = op_stack.pop().unwrap();
                        push_operator(&op, &mut output)?;
                    } else {
                        break;
                    }
                }
                op_stack.push(token);
            }
            Token::LParen => op_stack.push(token),
            Token::RParen => {
                let mut found_paren = false;
                while let Some(top) = op_stack.pop() {
                    if top == Token::LParen {
                        found_paren = true;
                        break;
                    }
                    push_operator(&top, &mut output)?;
                }
                if !found_paren {
                    return Err(ExprError("mismatched parentheses".to_string()));
                }
                // If top of stack is a function, pop it
                if let Some(Token::Func(name)) = op_stack.last() {
                    let op = match name.as_str() {
                        "min" => Op::CallMin,
                        "max" => Op::CallMax,
                        _ => return Err(ExprError(format!("unknown function: {}", name))),
                    };
                    op_stack.pop();
                    output.push(op);
                }
            }
        }
    }

    // Pop remaining operators
    while let Some(top) = op_stack.pop() {
        if top == Token::LParen {
            return Err(ExprError("mismatched parentheses".to_string()));
        }
        push_operator(&top, &mut output)?;
    }

    if output.is_empty() {
        return Err(ExprError("empty expression".to_string()));
    }

    Ok(output)
}

fn push_operator(tok: &Token, output: &mut Vec<Op>) -> Result<(), ExprError> {
    match tok {
        Token::Plus => output.push(Op::Add),
        Token::Minus => output.push(Op::Sub),
        Token::Star => output.push(Op::Mul),
        Token::Slash => output.push(Op::Div),
        _ => return Err(ExprError(format!("unexpected operator: {:?}", tok))),
    }
    Ok(())
}

/// Evaluate a compiled expression with the given variable bindings.
///
/// Variables not found in the map default to 0.0.
pub fn evaluate(ops: &[Op], vars: &HashMap<String, f64>) -> Result<f64, ExprError> {
    let mut stack: Vec<f64> = Vec::new();

    for op in ops {
        match op {
            Op::Push(val) => stack.push(*val),
            Op::PushVar(name) => {
                let val = vars.get(name).copied().unwrap_or(0.0);
                stack.push(val);
            }
            Op::Add => {
                let b = stack.pop().ok_or_else(|| ExprError("stack underflow".to_string()))?;
                let a = stack.pop().ok_or_else(|| ExprError("stack underflow".to_string()))?;
                stack.push(a + b);
            }
            Op::Sub => {
                let b = stack.pop().ok_or_else(|| ExprError("stack underflow".to_string()))?;
                let a = stack.pop().ok_or_else(|| ExprError("stack underflow".to_string()))?;
                stack.push(a - b);
            }
            Op::Mul => {
                let b = stack.pop().ok_or_else(|| ExprError("stack underflow".to_string()))?;
                let a = stack.pop().ok_or_else(|| ExprError("stack underflow".to_string()))?;
                stack.push(a * b);
            }
            Op::Div => {
                let b = stack.pop().ok_or_else(|| ExprError("stack underflow".to_string()))?;
                let a = stack.pop().ok_or_else(|| ExprError("stack underflow".to_string()))?;
                if b == 0.0 {
                    stack.push(0.0); // Safe division: 0/0 = 0
                } else {
                    stack.push(a / b);
                }
            }
            Op::CallMin => {
                let b = stack.pop().ok_or_else(|| ExprError("stack underflow".to_string()))?;
                let a = stack.pop().ok_or_else(|| ExprError("stack underflow".to_string()))?;
                stack.push(a.min(b));
            }
            Op::CallMax => {
                let b = stack.pop().ok_or_else(|| ExprError("stack underflow".to_string()))?;
                let a = stack.pop().ok_or_else(|| ExprError("stack underflow".to_string()))?;
                stack.push(a.max(b));
            }
        }
    }

    if stack.len() != 1 {
        return Err(ExprError(format!("expression did not reduce to single value (stack has {} items)", stack.len())));
    }

    Ok(stack[0])
}

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

    #[test]
    fn test_simple_addition() {
        let ops = compile_expression("1.0 + 2.0").unwrap();
        let result = evaluate(&ops, &HashMap::new()).unwrap();
        assert!((result - 3.0).abs() < f64::EPSILON);
    }

    #[test]
    fn test_operator_precedence() {
        let ops = compile_expression("2.0 + 3.0 * 4.0").unwrap();
        let result = evaluate(&ops, &HashMap::new()).unwrap();
        assert!((result - 14.0).abs() < f64::EPSILON);
    }

    #[test]
    fn test_parentheses() {
        let ops = compile_expression("(2.0 + 3.0) * 4.0").unwrap();
        let result = evaluate(&ops, &HashMap::new()).unwrap();
        assert!((result - 20.0).abs() < f64::EPSILON);
    }

    #[test]
    fn test_variables() {
        let ops = compile_expression("email_exact * 0.5 + name_fuzzy * 0.3").unwrap();
        let mut vars = HashMap::new();
        vars.insert("email_exact".to_string(), 1.0);
        vars.insert("name_fuzzy".to_string(), 0.8);
        let result = evaluate(&ops, &vars).unwrap();
        assert!((result - 0.74).abs() < 1e-10);
    }

    #[test]
    fn test_missing_variable_defaults_to_zero() {
        let ops = compile_expression("missing_rule * 5.0").unwrap();
        let result = evaluate(&ops, &HashMap::new()).unwrap();
        assert!((result - 0.0).abs() < f64::EPSILON);
    }

    #[test]
    fn test_min_function() {
        let ops = compile_expression("min(0.8, 0.3)").unwrap();
        let result = evaluate(&ops, &HashMap::new()).unwrap();
        assert!((result - 0.3).abs() < f64::EPSILON);
    }

    #[test]
    fn test_max_function() {
        let ops = compile_expression("max(0.2, 0.9)").unwrap();
        let result = evaluate(&ops, &HashMap::new()).unwrap();
        assert!((result - 0.9).abs() < f64::EPSILON);
    }

    #[test]
    fn test_nested_functions() {
        let ops = compile_expression("max(min(0.8, 0.3), 0.5)").unwrap();
        let result = evaluate(&ops, &HashMap::new()).unwrap();
        assert!((result - 0.5).abs() < f64::EPSILON);
    }

    #[test]
    fn test_complex_expression() {
        let ops = compile_expression("(email_exact * 0.5) + (name_fuzzy * phone_exact * 0.3)").unwrap();
        let mut vars = HashMap::new();
        vars.insert("email_exact".to_string(), 1.0);
        vars.insert("name_fuzzy".to_string(), 0.9);
        vars.insert("phone_exact".to_string(), 1.0);
        let result = evaluate(&ops, &vars).unwrap();
        assert!((result - 0.77).abs() < 1e-10);
    }

    #[test]
    fn test_division_by_zero() {
        let ops = compile_expression("1.0 / 0.0").unwrap();
        let result = evaluate(&ops, &HashMap::new()).unwrap();
        assert!((result - 0.0).abs() < f64::EPSILON);
    }

    #[test]
    fn test_unary_minus() {
        let ops = compile_expression("-1.0 + 2.0").unwrap();
        let result = evaluate(&ops, &HashMap::new()).unwrap();
        assert!((result - 1.0).abs() < f64::EPSILON);
    }

    #[test]
    fn test_subtraction() {
        let ops = compile_expression("1.0 - 0.3").unwrap();
        let result = evaluate(&ops, &HashMap::new()).unwrap();
        assert!((result - 0.7).abs() < 1e-10);
    }

    #[test]
    fn test_empty_expression_error() {
        let result = compile_expression("");
        assert!(result.is_err());
    }

    #[test]
    fn test_mismatched_parens_error() {
        let result = compile_expression("(1.0 + 2.0");
        assert!(result.is_err());
    }
}
