use crate::ast::*;
use crate::context::EvalContext;
use crate::span::Span;
use crate::types::*;
use chrono::{Duration, NaiveDate};

/// Internal intermediate value used during evaluation.
#[derive(Debug, Clone)]
enum Value {
    Number(f64),
    UnitValue { value: f64, unit: String },
    CurrencyValue { amount: f64, currency: String },
    DateTime(NaiveDate),
    TimeDelta { days: i64, description: String },
    Boolean(bool),
}

/// Evaluate an AST expression within the given context.
pub fn evaluate(expr: &Expr, ctx: &mut EvalContext) -> CalcResult {
    let span = expr.span;
    match eval_inner(expr, ctx) {
        Ok(val) => value_to_result(val, span),
        Err(msg) => CalcResult::error(&msg, span),
    }
}

fn eval_inner(expr: &Expr, ctx: &mut EvalContext) -> Result<Value, String> {
    match &expr.node {
        ExprKind::Number(n) => Ok(Value::Number(*n)),

        ExprKind::Boolean(b) => Ok(Value::Boolean(*b)),

        ExprKind::UnitNumber { value, unit } => Ok(Value::UnitValue {
            value: *value,
            unit: unit.clone(),
        }),

        ExprKind::CurrencyValue { amount, currency } => Ok(Value::CurrencyValue {
            amount: *amount,
            currency: currency.clone(),
        }),

        ExprKind::DateLiteral { year, month, day } => {
            let date = NaiveDate::from_ymd_opt(*year, *month, *day)
                .ok_or_else(|| format!("Invalid date: {}-{}-{}", year, month, day))?;
            Ok(Value::DateTime(date))
        }

        ExprKind::Today => Ok(Value::DateTime(ctx.today)),

        ExprKind::Duration { value, unit } => {
            let days = duration_to_days(*value, *unit);
            let desc = format_duration(*value, *unit);
            Ok(Value::TimeDelta {
                days,
                description: desc,
            })
        }

        ExprKind::BinaryOp { op, left, right } => {
            let lval = eval_inner(left, ctx)?;
            let rval = eval_inner(right, ctx)?;
            eval_binary(*op, lval, rval, ctx)
        }

        ExprKind::UnaryNeg(inner) => {
            let val = eval_inner(inner, ctx)?;
            match val {
                Value::Number(n) => Ok(Value::Number(-n)),
                Value::UnitValue { value, unit } => Ok(Value::UnitValue {
                    value: -value,
                    unit,
                }),
                Value::CurrencyValue { amount, currency } => Ok(Value::CurrencyValue {
                    amount: -amount,
                    currency,
                }),
                _ => Err("cannot negate this value".to_string()),
            }
        }

        ExprKind::PercentOp {
            op,
            base,
            percentage,
        } => {
            let base_val = eval_inner(base, ctx)?;
            eval_percent(*op, base_val, *percentage)
        }

        ExprKind::Conversion {
            expr,
            target_currency,
        } => {
            let val = eval_inner(expr, ctx)?;
            eval_conversion(val, target_currency, ctx)
        }

        ExprKind::DateRange { from, to } => {
            let from_val = eval_inner(from, ctx)?;
            let to_val = eval_inner(to, ctx)?;
            match (from_val, to_val) {
                (Value::DateTime(d1), Value::DateTime(d2)) => {
                    let days = (d2 - d1).num_days();
                    Ok(Value::TimeDelta {
                        days,
                        description: format!("{} days", days),
                    })
                }
                _ => Err("date range requires two dates".to_string()),
            }
        }

        ExprKind::Comparison { op, left, right } => {
            let lval = eval_inner(left, ctx)?;
            let rval = eval_inner(right, ctx)?;
            eval_comparison(*op, lval, rval)
        }

        ExprKind::Identifier(name) => {
            if let Some(result) = ctx.get_variable(name) {
                result_to_value(result)
            } else {
                Err(format!("undefined variable: {}", name))
            }
        }

        ExprKind::Assignment { name, value } => {
            let val = eval_inner(value, ctx)?;
            let result = value_to_result(val.clone(), value.span);
            ctx.set_variable(name, result);
            Ok(val)
        }

        ExprKind::LineRef(line_num) => {
            let key = format!("__line_{}", line_num);
            if let Some(result) = ctx.get_variable(&key) {
                result_to_value(result)
            } else {
                Err(format!("invalid line reference: line {}", line_num))
            }
        }

        ExprKind::PrevRef => {
            if let Some(result) = ctx.get_variable("__prev") {
                result_to_value(result)
            } else {
                Err("no previous line result".to_string())
            }
        }

        ExprKind::FunctionCall { name, args } => {
            eval_function_call(name, args, ctx)
        }
    }
}

fn result_to_value(result: &CalcResult) -> Result<Value, String> {
    match &result.value {
        CalcValue::Number { value } => Ok(Value::Number(*value)),
        CalcValue::UnitValue { value, unit } => Ok(Value::UnitValue {
            value: *value,
            unit: unit.clone(),
        }),
        CalcValue::CurrencyValue { amount, currency } => Ok(Value::CurrencyValue {
            amount: *amount,
            currency: currency.clone(),
        }),
        CalcValue::Boolean { value } => Ok(Value::Boolean(*value)),
        CalcValue::DateTime { date } => {
            let d = NaiveDate::parse_from_str(date, "%Y-%m-%d")
                .map_err(|e| format!("invalid date: {}", e))?;
            Ok(Value::DateTime(d))
        }
        CalcValue::TimeDelta { days, description } => Ok(Value::TimeDelta {
            days: *days,
            description: description.clone(),
        }),
        CalcValue::Error { message, .. } => Err(message.clone()),
    }
}

fn eval_binary(
    op: BinOp,
    lval: Value,
    rval: Value,
    ctx: &EvalContext,
) -> Result<Value, String> {
    match (lval, rval) {
        // Number op Number
        (Value::Number(a), Value::Number(b)) => {
            let result = match op {
                BinOp::Add => a + b,
                BinOp::Sub => a - b,
                BinOp::Mul => a * b,
                BinOp::Div => {
                    if b == 0.0 {
                        return Err("division by zero".to_string());
                    }
                    a / b
                }
                BinOp::Pow => a.powf(b),
                BinOp::Mod => {
                    if b == 0.0 {
                        return Err("modulo by zero".to_string());
                    }
                    a % b
                }
            };
            Ok(Value::Number(result))
        }

        // UnitValue op Number (scalar operations)
        (Value::UnitValue { value, unit }, Value::Number(b)) => {
            let result = match op {
                BinOp::Mul => b * value,
                BinOp::Div => {
                    if b == 0.0 {
                        return Err("division by zero".to_string());
                    }
                    value / b
                }
                BinOp::Add => value + b,
                BinOp::Sub => value - b,
                _ => return Err(format!("unsupported operation: unit {} number", op)),
            };
            Ok(Value::UnitValue {
                value: result,
                unit,
            })
        }

        // Number op UnitValue
        (Value::Number(a), Value::UnitValue { value, unit }) => {
            let result = match op {
                BinOp::Mul => a * value,
                _ => return Err(format!("unsupported operation: number {} unit", op)),
            };
            Ok(Value::UnitValue {
                value: result,
                unit,
            })
        }

        // UnitValue op UnitValue (same unit)
        (
            Value::UnitValue {
                value: a,
                unit: u1,
            },
            Value::UnitValue {
                value: b,
                unit: u2,
            },
        ) => {
            if u1 != u2 {
                // Try unit conversion
                if let Some(converted) = convert_units(b, &u2, &u1) {
                    let result = match op {
                        BinOp::Add => a + converted,
                        BinOp::Sub => a - converted,
                        _ => {
                            return Err(format!(
                                "unsupported operation between {} and {}",
                                u1, u2
                            ))
                        }
                    };
                    return Ok(Value::UnitValue {
                        value: result,
                        unit: u1,
                    });
                }
                return Err(format!("incompatible units: {} and {}", u1, u2));
            }
            let result = match op {
                BinOp::Add => a + b,
                BinOp::Sub => a - b,
                _ => return Err(format!("unsupported operation between units: {}", op)),
            };
            Ok(Value::UnitValue {
                value: result,
                unit: u1,
            })
        }

        // Currency op Currency
        (
            Value::CurrencyValue {
                amount: a,
                currency: c1,
            },
            Value::CurrencyValue {
                amount: b,
                currency: c2,
            },
        ) => {
            if c1 != c2 {
                if let Some(converted) = ctx.convert_currency(b, &c2, &c1) {
                    let result = match op {
                        BinOp::Add => a + converted,
                        BinOp::Sub => a - converted,
                        _ => return Err("unsupported currency operation".to_string()),
                    };
                    return Ok(Value::CurrencyValue {
                        amount: result,
                        currency: c1,
                    });
                }
                return Err(format!("cannot convert between {} and {}", c1, c2));
            }
            let result = match op {
                BinOp::Add => a + b,
                BinOp::Sub => a - b,
                _ => return Err("unsupported currency operation".to_string()),
            };
            Ok(Value::CurrencyValue {
                amount: result,
                currency: c1,
            })
        }

        // Currency op Number
        (Value::CurrencyValue { amount, currency }, Value::Number(b)) => {
            let result = match op {
                BinOp::Mul => amount * b,
                BinOp::Div => {
                    if b == 0.0 {
                        return Err("division by zero".to_string());
                    }
                    amount / b
                }
                BinOp::Add => amount + b,
                BinOp::Sub => amount - b,
                _ => return Err("unsupported currency operation".to_string()),
            };
            Ok(Value::CurrencyValue {
                amount: result,
                currency,
            })
        }

        // Number op Currency
        (Value::Number(a), Value::CurrencyValue { amount, currency }) => {
            let result = match op {
                BinOp::Mul => a * amount,
                _ => return Err("unsupported currency operation".to_string()),
            };
            Ok(Value::CurrencyValue {
                amount: result,
                currency,
            })
        }

        // DateTime +/- Duration
        (Value::DateTime(date), Value::TimeDelta { days, .. }) => match op {
            BinOp::Add => {
                let new_date = date + Duration::days(days);
                Ok(Value::DateTime(new_date))
            }
            BinOp::Sub => {
                let new_date = date - Duration::days(days);
                Ok(Value::DateTime(new_date))
            }
            _ => Err("unsupported date operation".to_string()),
        },

        // DateTime - DateTime
        (Value::DateTime(d1), Value::DateTime(d2)) => match op {
            BinOp::Sub => {
                let days = (d1 - d2).num_days();
                Ok(Value::TimeDelta {
                    days,
                    description: format!("{} days", days),
                })
            }
            _ => Err("unsupported date operation".to_string()),
        },

        _ => Err(format!("unsupported binary operation: {}", op)),
    }
}

fn eval_percent(op: PercentOp, base: Value, percentage: f64) -> Result<Value, String> {
    let factor = percentage / 100.0;
    match base {
        Value::Number(n) => {
            let result = match op {
                PercentOp::Add => n + n * factor,
                PercentOp::Sub => n - n * factor,
            };
            Ok(Value::Number(result))
        }
        Value::CurrencyValue { amount, currency } => {
            let result = match op {
                PercentOp::Add => amount + amount * factor,
                PercentOp::Sub => amount - amount * factor,
            };
            Ok(Value::CurrencyValue {
                amount: result,
                currency,
            })
        }
        Value::UnitValue { value, unit } => {
            let result = match op {
                PercentOp::Add => value + value * factor,
                PercentOp::Sub => value - value * factor,
            };
            Ok(Value::UnitValue {
                value: result,
                unit,
            })
        }
        _ => Err("percentage operation requires numeric value".to_string()),
    }
}

fn eval_conversion(
    val: Value,
    target: &str,
    ctx: &EvalContext,
) -> Result<Value, String> {
    match val {
        Value::CurrencyValue { amount, currency } => {
            if let Some(converted) = ctx.convert_currency(amount, &currency, target) {
                Ok(Value::CurrencyValue {
                    amount: converted,
                    currency: target.to_string(),
                })
            } else {
                Err(format!(
                    "no exchange rate available for {} to {}",
                    currency, target
                ))
            }
        }
        Value::UnitValue { value, unit } => {
            if let Some(converted) = convert_units(value, &unit, target) {
                Ok(Value::UnitValue {
                    value: converted,
                    unit: target.to_string(),
                })
            } else {
                Err(format!("cannot convert {} to {}", unit, target))
            }
        }
        _ => Err("conversion requires a currency or unit value".to_string()),
    }
}

fn eval_comparison(op: CmpOp, lval: Value, rval: Value) -> Result<Value, String> {
    let (a, b) = match (lval, rval) {
        (Value::Number(a), Value::Number(b)) => (a, b),
        _ => return Err("comparison requires numeric values".to_string()),
    };
    let result = match op {
        CmpOp::Gt => a > b,
        CmpOp::Lt => a < b,
        CmpOp::Gte => a >= b,
        CmpOp::Lte => a <= b,
        CmpOp::Eq => (a - b).abs() < f64::EPSILON,
        CmpOp::Neq => (a - b).abs() >= f64::EPSILON,
    };
    Ok(Value::Boolean(result))
}

fn value_to_result(val: Value, span: Span) -> CalcResult {
    match val {
        Value::Number(n) => CalcResult::number(n, span),
        Value::UnitValue { value, unit } => CalcResult::unit_value(value, &unit, span),
        Value::CurrencyValue { amount, currency } => {
            CalcResult::currency_value(amount, &currency, span)
        }
        Value::DateTime(date) => CalcResult::datetime(date, span),
        Value::TimeDelta { days, description } => CalcResult::time_delta(days, &description, span),
        Value::Boolean(b) => CalcResult::boolean(b, span),
    }
}

fn duration_to_days(value: f64, unit: DurationUnit) -> i64 {
    let days = match unit {
        DurationUnit::Days => value,
        DurationUnit::Weeks => value * 7.0,
        DurationUnit::Months => value * 30.0,
        DurationUnit::Years => value * 365.0,
        DurationUnit::Hours => value / 24.0,
        DurationUnit::Minutes => value / (24.0 * 60.0),
        DurationUnit::Seconds => value / (24.0 * 60.0 * 60.0),
    };
    days as i64
}

fn format_duration(value: f64, unit: DurationUnit) -> String {
    let unit_str = match unit {
        DurationUnit::Days => "days",
        DurationUnit::Weeks => "weeks",
        DurationUnit::Months => "months",
        DurationUnit::Years => "years",
        DurationUnit::Hours => "hours",
        DurationUnit::Minutes => "minutes",
        DurationUnit::Seconds => "seconds",
    };
    if value == 1.0 {
        format!("1 {}", unit_str.trim_end_matches('s'))
    } else {
        format!("{} {}", value, unit_str)
    }
}

fn eval_function_call(
    name: &str,
    args: &[Expr],
    ctx: &mut EvalContext,
) -> Result<Value, String> {
    if args.len() != 1 {
        return Err(format!(
            "function '{}' expects 1 argument, got {}",
            name,
            args.len()
        ));
    }
    let arg_val = eval_inner(&args[0], ctx)?;
    let n = match &arg_val {
        Value::Number(v) => *v,
        Value::UnitValue { value, .. } => *value,
        Value::CurrencyValue { amount, .. } => *amount,
        _ => return Err(format!("function '{}' requires a numeric argument", name)),
    };
    let result = match name.to_lowercase().as_str() {
        "sqrt" => {
            if n < 0.0 {
                return Err("sqrt of negative number".to_string());
            }
            n.sqrt()
        }
        "abs" => n.abs(),
        "round" => n.round(),
        "floor" => n.floor(),
        "ceil" => n.ceil(),
        "log" | "log10" => {
            if n <= 0.0 {
                return Err("log of non-positive number".to_string());
            }
            n.log10()
        }
        "ln" => {
            if n <= 0.0 {
                return Err("ln of non-positive number".to_string());
            }
            n.ln()
        }
        "sin" => n.sin(),
        "cos" => n.cos(),
        "tan" => n.tan(),
        _ => return Err(format!("unknown function: {}", name)),
    };
    // Preserve unit/currency context through functions
    match arg_val {
        Value::UnitValue { unit, .. } => Ok(Value::UnitValue {
            value: result,
            unit,
        }),
        Value::CurrencyValue { currency, .. } => Ok(Value::CurrencyValue {
            amount: result,
            currency,
        }),
        _ => Ok(Value::Number(result)),
    }
}

fn convert_units(value: f64, from: &str, to: &str) -> Option<f64> {
    // Normalize to base unit, then convert to target
    let (base_value, base_unit) = to_base_unit(value, from)?;
    from_base_unit(base_value, &base_unit, to)
}

fn to_base_unit(value: f64, unit: &str) -> Option<(f64, String)> {
    match unit {
        // Mass → grams
        "kg" => Some((value * 1000.0, "g".to_string())),
        "g" => Some((value, "g".to_string())),
        "mg" => Some((value / 1000.0, "g".to_string())),
        "lb" => Some((value * 453.592, "g".to_string())),
        "oz" => Some((value * 28.3495, "g".to_string())),
        // Length → meters
        "m" => Some((value, "m".to_string())),
        "cm" => Some((value / 100.0, "m".to_string())),
        "mm" => Some((value / 1000.0, "m".to_string())),
        "km" => Some((value * 1000.0, "m".to_string())),
        "ft" => Some((value * 0.3048, "m".to_string())),
        "in" => Some((value * 0.0254, "m".to_string())),
        // Volume → liters
        "l" => Some((value, "l".to_string())),
        "ml" => Some((value / 1000.0, "l".to_string())),
        // Time → seconds
        "s" => Some((value, "s".to_string())),
        "min" => Some((value * 60.0, "s".to_string())),
        "h" => Some((value * 3600.0, "s".to_string())),
        _ => None,
    }
}

fn from_base_unit(base_value: f64, base_unit: &str, target: &str) -> Option<f64> {
    match (base_unit, target) {
        // Mass (base: grams)
        ("g", "kg") => Some(base_value / 1000.0),
        ("g", "g") => Some(base_value),
        ("g", "mg") => Some(base_value * 1000.0),
        ("g", "lb") => Some(base_value / 453.592),
        ("g", "oz") => Some(base_value / 28.3495),
        // Length (base: meters)
        ("m", "m") => Some(base_value),
        ("m", "cm") => Some(base_value * 100.0),
        ("m", "mm") => Some(base_value * 1000.0),
        ("m", "km") => Some(base_value / 1000.0),
        ("m", "ft") => Some(base_value / 0.3048),
        ("m", "in") => Some(base_value / 0.0254),
        // Volume (base: liters)
        ("l", "l") => Some(base_value),
        ("l", "ml") => Some(base_value * 1000.0),
        // Time (base: seconds)
        ("s", "s") => Some(base_value),
        ("s", "min") => Some(base_value / 60.0),
        ("s", "h") => Some(base_value / 3600.0),
        _ => None,
    }
}
