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

use bt_core::{StrategyMetadata, StrategyPlan};
use bt_expr::{
    compile_expr, has_dynamic_periods, resolve_dynamic_periods, Expr, ScalarValue, ValueType,
};
use bt_kernel::{BtError, Result};
use serde::{Deserialize, Serialize};

use crate::validate::{collect_expr_columns, validate_strategy};

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct StrategyDef {
    pub name: String,
    pub signals: HashMap<String, Expr>,
    pub position_sizing: Expr,
    pub parameters: HashMap<String, ParamSpec>,
    pub constraints: Vec<Constraint>,
    pub metadata: StrategyMetadata,
}

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct ParamSpec {
    pub name: String,
    pub default: ScalarValue,
    pub range: Option<(ScalarValue, ScalarValue)>,
    pub description: String,
}

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub enum Constraint {
    RequireColumn(String),
    MaxAbsPosition(f64),
    MinHistory(usize),
}

#[tracing::instrument(level = "info", skip_all)]
pub fn compile_strategy(def: &StrategyDef) -> Result<StrategyPlan> {
    compile_strategy_with_columns(def, None)
}

pub fn compile_strategy_with_columns(
    def: &StrategyDef,
    columns: Option<&HashMap<String, ValueType>>,
) -> Result<StrategyPlan> {
    validate_strategy(def, columns)?;

    // Detect dynamic periods (param() in indicator period arguments)
    let has_dyn =
        def.signals.values().any(has_dynamic_periods) || has_dynamic_periods(&def.position_sizing);

    let mut parameters = HashMap::new();
    for (name, spec) in &def.parameters {
        parameters.insert(name.clone(), spec.default.clone());
    }

    // If dynamic periods exist, resolve with defaults before compiling
    let (signals_to_compile, sizing_to_compile) = if has_dyn {
        let mut resolved_signals = HashMap::new();
        for (name, expr) in &def.signals {
            resolved_signals.insert(name.clone(), resolve_dynamic_periods(expr, &parameters)?);
        }
        let resolved_sizing = resolve_dynamic_periods(&def.position_sizing, &parameters)?;
        (resolved_signals, resolved_sizing)
    } else {
        (def.signals.clone(), def.position_sizing.clone())
    };

    let mut signal_names = signals_to_compile.keys().cloned().collect::<Vec<_>>();
    signal_names.sort();

    let mut compiled_signals = HashMap::new();
    for name in signal_names {
        let expr = signals_to_compile
            .get(&name)
            .ok_or_else(|| BtError::Strategy(format!("missing signal expression for '{name}'")))?;
        compiled_signals.insert(name, compile_expr(expr.clone())?);
    }

    let position_sizing = compile_expr(sizing_to_compile)?;

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

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

    let mut required_columns = required_columns.into_iter().collect::<Vec<_>>();
    required_columns.sort();

    tracing::info!(name = %def.name, has_dynamic_periods = has_dyn, "compiled strategy");
    Ok(StrategyPlan {
        name: def.name.clone(),
        signals: compiled_signals,
        position_sizing,
        required_columns,
        required_features: Vec::new(),
        parameters,
        metadata: def.metadata.clone(),
        raw_signals: if has_dyn {
            Some(def.signals.clone())
        } else {
            None
        },
        raw_position_sizing: if has_dyn {
            Some(def.position_sizing.clone())
        } else {
            None
        },
        has_dynamic_periods: has_dyn,
    })
}
