use std::collections::{HashMap, HashSet};

use bt_expr::{infer_expr_type, Expr, ScalarValue, ValueType};
use bt_kernel::{BtError, Result};

use crate::compiler::{Constraint, StrategyDef};

pub fn validate_strategy(
    def: &StrategyDef,
    columns: Option<&HashMap<String, ValueType>>,
) -> Result<()> {
    if def.name.trim().is_empty() {
        return Err(BtError::Strategy(
            "strategy name cannot be empty".to_string(),
        ));
    }
    if def.signals.is_empty() {
        return Err(BtError::Strategy(
            "strategy must define at least one signal".to_string(),
        ));
    }

    validate_parameter_specs(def)?;

    let column_types = columns.cloned().unwrap_or_default();
    let defaults = parameter_defaults(def);

    for (name, expr) in &def.signals {
        infer_expr_type(expr, &column_types, &defaults).map_err(|err| {
            BtError::Strategy(format!("signal '{name}' failed type validation: {err}"))
        })?;
    }
    infer_expr_type(&def.position_sizing, &column_types, &defaults).map_err(|err| {
        BtError::Strategy(format!("position_sizing failed type validation: {err}"))
    })?;

    if let Some(columns) = columns {
        let mut required = HashSet::new();
        for expr in def.signals.values() {
            collect_expr_columns(expr, &mut required);
        }
        collect_expr_columns(&def.position_sizing, &mut required);

        for constraint in &def.constraints {
            if let Constraint::RequireColumn(column) = constraint {
                required.insert(column.clone());
            }
        }

        let missing = required
            .into_iter()
            .filter(|column| !columns.contains_key(column))
            .collect::<Vec<_>>();
        if !missing.is_empty() {
            return Err(BtError::Strategy(format!(
                "strategy references unknown columns: {}",
                missing.join(", ")
            )));
        }
    }

    let mut used_parameters = HashSet::new();
    for expr in def.signals.values() {
        collect_expr_parameters(expr, &mut used_parameters);
    }
    collect_expr_parameters(&def.position_sizing, &mut used_parameters);

    let undefined_parameters = used_parameters
        .iter()
        .filter(|name| !def.parameters.contains_key(*name))
        .cloned()
        .collect::<Vec<_>>();
    if !undefined_parameters.is_empty() {
        return Err(BtError::Strategy(format!(
            "strategy uses undefined parameters: {}",
            undefined_parameters.join(", ")
        )));
    }

    Ok(())
}

pub fn collect_expr_columns(expr: &Expr, out: &mut HashSet<String>) {
    match expr {
        Expr::Column(name) => {
            out.insert(name.clone());
        }
        Expr::Literal(_) | Expr::Parameter(_) => {}

        Expr::Add(lhs, rhs)
        | Expr::Sub(lhs, rhs)
        | Expr::Mul(lhs, rhs)
        | Expr::Div(lhs, rhs)
        | Expr::Gt(lhs, rhs)
        | Expr::Lt(lhs, rhs)
        | Expr::Eq(lhs, rhs)
        | Expr::And(lhs, rhs)
        | Expr::Or(lhs, rhs) => {
            collect_expr_columns(lhs, out);
            collect_expr_columns(rhs, out);
        }
        Expr::Not(inner)
        | Expr::Lag(inner, _)
        | Expr::Lead(inner, _)
        | Expr::RollingMean(inner, _)
        | Expr::RollingStd(inner, _)
        | Expr::RollingSum(inner, _)
        | Expr::RollingMin(inner, _)
        | Expr::RollingMax(inner, _)
        | Expr::EwmMean(inner, _)
        | Expr::Diff(inner, _)
        | Expr::PctChange(inner, _)
        | Expr::CumSum(inner)
        | Expr::CumProd(inner)
        | Expr::Rank(inner)
        | Expr::ZScore(inner, _)
        | Expr::Rsi(inner, _)
        | Expr::LinRegSlope(inner, _)
        | Expr::LinRegValue(inner, _)
        | Expr::LinRegR2(inner, _)
        | Expr::Dema(inner, _)
        | Expr::Tema(inner, _)
        | Expr::Wma(inner, _)
        | Expr::Hma(inner, _)
        | Expr::Kama(inner, _)
        | Expr::Roc(inner, _)
        | Expr::Macd(inner, _, _)
        | Expr::MacdSignal(inner, _, _, _)
        | Expr::MacdHist(inner, _, _, _)
        | Expr::BollingerUpper(inner, _, _)
        | Expr::BollingerLower(inner, _, _)
        | Expr::BollingerWidth(inner, _, _)
        | Expr::RollingMedian(inner, _)
        | Expr::CrossSectionalMean(inner)
        | Expr::CrossSectionalRank(inner)
        | Expr::Hour(inner)
        | Expr::Minute(inner)
        | Expr::DayOfWeek(inner)
        | Expr::Month(inner)
        | Expr::DayOfMonth(inner) => collect_expr_columns(inner, out),

        Expr::CrossAbove(a, b) | Expr::CrossBelow(a, b) | Expr::Obv(a, b) => {
            collect_expr_columns(a, out);
            collect_expr_columns(b, out);
        }

        // SymbolRef pulls from another symbol's env — do NOT collect the
        // inner column as a required column for the current symbol.
        Expr::SymbolRef(_symbol, _inner) => {}

        Expr::Atr(high, low, close, _)
        | Expr::TrueRange(high, low, close)
        | Expr::Natr(high, low, close, _)
        | Expr::StochK(high, low, close, _)
        | Expr::WilliamsR(high, low, close, _)
        | Expr::Cci(high, low, close, _)
        | Expr::Adx(high, low, close, _)
        | Expr::SuperTrend(high, low, close, _, _)
        | Expr::KeltnerUpper(high, low, close, _, _)
        | Expr::KeltnerLower(high, low, close, _, _) => {
            collect_expr_columns(high, out);
            collect_expr_columns(low, out);
            collect_expr_columns(close, out);
        }

        Expr::Vwap(h, l, c, v) | Expr::AdLine(h, l, c, v) | Expr::Mfi(h, l, c, v, _) => {
            collect_expr_columns(h, out);
            collect_expr_columns(l, out);
            collect_expr_columns(c, out);
            collect_expr_columns(v, out);
        }

        Expr::ParabolicSar(h, l, _, _) => {
            collect_expr_columns(h, out);
            collect_expr_columns(l, out);
        }

        Expr::IfElse(condition, then_value, else_value) => {
            collect_expr_columns(condition, out);
            collect_expr_columns(then_value, out);
            collect_expr_columns(else_value, out);
        }

        Expr::Function(_, args) => {
            for arg in args {
                collect_expr_columns(arg, out);
            }
        }

        Expr::Scan {
            init_exprs,
            update_exprs,
            ..
        } => {
            for ie in init_exprs {
                collect_expr_columns(ie, out);
            }
            for ue in update_exprs {
                collect_scan_body_columns(ue, out);
            }
        }
        Expr::ScanPrev(_) | Expr::ScanVar(_) => {}
    }
}

/// Collect columns from inside a scan body (skips ScanPrev/ScanVar).
fn collect_scan_body_columns(expr: &Expr, out: &mut HashSet<String>) {
    match expr {
        Expr::ScanPrev(_) | Expr::ScanVar(_) => {}
        _ => collect_expr_columns(expr, out),
    }
}

pub fn collect_expr_parameters(expr: &Expr, out: &mut HashSet<String>) {
    match expr {
        Expr::Parameter(name) => {
            out.insert(name.clone());
        }
        Expr::Column(_) | Expr::Literal(_) => {}

        Expr::Add(lhs, rhs)
        | Expr::Sub(lhs, rhs)
        | Expr::Mul(lhs, rhs)
        | Expr::Div(lhs, rhs)
        | Expr::Gt(lhs, rhs)
        | Expr::Lt(lhs, rhs)
        | Expr::Eq(lhs, rhs)
        | Expr::And(lhs, rhs)
        | Expr::Or(lhs, rhs) => {
            collect_expr_parameters(lhs, out);
            collect_expr_parameters(rhs, out);
        }
        Expr::Not(inner)
        | Expr::Lag(inner, _)
        | Expr::Lead(inner, _)
        | Expr::RollingMean(inner, _)
        | Expr::RollingStd(inner, _)
        | Expr::RollingSum(inner, _)
        | Expr::RollingMin(inner, _)
        | Expr::RollingMax(inner, _)
        | Expr::EwmMean(inner, _)
        | Expr::Diff(inner, _)
        | Expr::PctChange(inner, _)
        | Expr::CumSum(inner)
        | Expr::CumProd(inner)
        | Expr::Rank(inner)
        | Expr::ZScore(inner, _)
        | Expr::Rsi(inner, _)
        | Expr::LinRegSlope(inner, _)
        | Expr::LinRegValue(inner, _)
        | Expr::LinRegR2(inner, _)
        | Expr::Dema(inner, _)
        | Expr::Tema(inner, _)
        | Expr::Wma(inner, _)
        | Expr::Hma(inner, _)
        | Expr::Kama(inner, _)
        | Expr::Roc(inner, _)
        | Expr::Macd(inner, _, _)
        | Expr::MacdSignal(inner, _, _, _)
        | Expr::MacdHist(inner, _, _, _)
        | Expr::BollingerUpper(inner, _, _)
        | Expr::BollingerLower(inner, _, _)
        | Expr::BollingerWidth(inner, _, _)
        | Expr::RollingMedian(inner, _)
        | Expr::CrossSectionalMean(inner)
        | Expr::CrossSectionalRank(inner)
        | Expr::Hour(inner)
        | Expr::Minute(inner)
        | Expr::DayOfWeek(inner)
        | Expr::Month(inner)
        | Expr::DayOfMonth(inner) => collect_expr_parameters(inner, out),

        Expr::CrossAbove(a, b) | Expr::CrossBelow(a, b) | Expr::Obv(a, b) => {
            collect_expr_parameters(a, out);
            collect_expr_parameters(b, out);
        }

        Expr::SymbolRef(_symbol, inner) => collect_expr_parameters(inner, out),

        Expr::Atr(high, low, close, _)
        | Expr::TrueRange(high, low, close)
        | Expr::Natr(high, low, close, _)
        | Expr::StochK(high, low, close, _)
        | Expr::WilliamsR(high, low, close, _)
        | Expr::Cci(high, low, close, _)
        | Expr::Adx(high, low, close, _)
        | Expr::SuperTrend(high, low, close, _, _)
        | Expr::KeltnerUpper(high, low, close, _, _)
        | Expr::KeltnerLower(high, low, close, _, _) => {
            collect_expr_parameters(high, out);
            collect_expr_parameters(low, out);
            collect_expr_parameters(close, out);
        }

        Expr::Vwap(h, l, c, v) | Expr::AdLine(h, l, c, v) | Expr::Mfi(h, l, c, v, _) => {
            collect_expr_parameters(h, out);
            collect_expr_parameters(l, out);
            collect_expr_parameters(c, out);
            collect_expr_parameters(v, out);
        }

        Expr::ParabolicSar(h, l, _, _) => {
            collect_expr_parameters(h, out);
            collect_expr_parameters(l, out);
        }

        Expr::IfElse(condition, then_value, else_value) => {
            collect_expr_parameters(condition, out);
            collect_expr_parameters(then_value, out);
            collect_expr_parameters(else_value, out);
        }

        Expr::Function(_, args) => {
            for arg in args {
                collect_expr_parameters(arg, out);
            }
        }

        Expr::Scan {
            init_exprs,
            update_exprs,
            ..
        } => {
            for ie in init_exprs {
                collect_expr_parameters(ie, out);
            }
            for ue in update_exprs {
                collect_scan_body_parameters(ue, out);
            }
        }
        Expr::ScanPrev(_) | Expr::ScanVar(_) => {}
    }
}

/// Collect parameters from inside a scan body (skips ScanPrev/ScanVar).
fn collect_scan_body_parameters(expr: &Expr, out: &mut HashSet<String>) {
    match expr {
        Expr::ScanPrev(_) | Expr::ScanVar(_) => {}
        _ => collect_expr_parameters(expr, out),
    }
}

fn parameter_defaults(def: &StrategyDef) -> HashMap<String, ScalarValue> {
    let mut defaults = HashMap::new();
    for (name, spec) in &def.parameters {
        defaults.insert(name.clone(), spec.default.clone());
    }
    defaults
}

fn validate_parameter_specs(def: &StrategyDef) -> Result<()> {
    for (name, spec) in &def.parameters {
        if spec.name != *name {
            return Err(BtError::Strategy(format!(
                "parameter key '{name}' must match spec.name '{}'",
                spec.name
            )));
        }

        if let Some((min, max)) = &spec.range {
            match (min.as_f64(), max.as_f64()) {
                (Some(min), Some(max)) => {
                    if min > max {
                        return Err(BtError::Strategy(format!(
                            "parameter '{name}' range is invalid: min {min} > max {max}"
                        )));
                    }
                }
                _ => {
                    return Err(BtError::Strategy(format!(
                        "parameter '{name}' range must be numeric"
                    )))
                }
            }
        }
    }

    Ok(())
}
