//! Trigger system (IFTTT-style) for conditional actions.
//!
//! Pure, stateless trigger evaluation. All mutable state (last fire time,
//! armed flag, previous values) managed by Python closure.

use pyo3::prelude::*;

/// Condition type for trigger evaluation.
#[pyclass(eq, eq_int, frozen, hash)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum TriggerCondition {
    Above = 0,
    Below = 1,
    CrossAbove = 2,
    CrossBelow = 3,
    PercentChange = 4,
    AbsoluteChange = 5,
    Between = 6,
    Outside = 7,
}

#[pymethods]
impl TriggerCondition {
    fn __repr__(&self) -> String {
        match self {
            Self::Above => "TriggerCondition.Above",
            Self::Below => "TriggerCondition.Below",
            Self::CrossAbove => "TriggerCondition.CrossAbove",
            Self::CrossBelow => "TriggerCondition.CrossBelow",
            Self::PercentChange => "TriggerCondition.PercentChange",
            Self::AbsoluteChange => "TriggerCondition.AbsoluteChange",
            Self::Between => "TriggerCondition.Between",
            Self::Outside => "TriggerCondition.Outside",
        }
        .to_string()
    }
    fn __str__(&self) -> String { self.__repr__() }
}

/// Action type when trigger fires.
#[pyclass(eq, eq_int, frozen, hash)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum TriggerActionType {
    Alert = 0,
    Webhook = 1,
    Order = 2,
    Hedge = 3,
    KillSwitch = 4,
}

#[pymethods]
impl TriggerActionType {
    fn __repr__(&self) -> String {
        match self {
            Self::Alert => "TriggerActionType.Alert",
            Self::Webhook => "TriggerActionType.Webhook",
            Self::Order => "TriggerActionType.Order",
            Self::Hedge => "TriggerActionType.Hedge",
            Self::KillSwitch => "TriggerActionType.KillSwitch",
        }
        .to_string()
    }
    fn __str__(&self) -> String { self.__repr__() }
}

/// A trigger rule definition.
#[pyclass(frozen, get_all)]
#[derive(Debug, Clone)]
pub struct TriggerRule {
    pub name: String,
    pub condition: TriggerCondition,
    pub threshold: f64,
    pub threshold_upper: f64,
    pub action_type: TriggerActionType,
    pub cooldown_secs: f64,
    pub hysteresis: f64,
}

#[pymethods]
impl TriggerRule {
    #[new]
    #[pyo3(signature = (name, condition, threshold, action_type, threshold_upper=0.0, cooldown_secs=60.0, hysteresis=0.0))]
    fn new(
        name: String,
        condition: TriggerCondition,
        threshold: f64,
        action_type: TriggerActionType,
        threshold_upper: f64,
        cooldown_secs: f64,
        hysteresis: f64,
    ) -> Self {
        Self { name, condition, threshold, threshold_upper, action_type, cooldown_secs, hysteresis }
    }

    fn __repr__(&self) -> String {
        format!("TriggerRule(name='{}', condition={:?}, threshold={:.4})",
            self.name, self.condition, self.threshold)
    }
}

/// Result of trigger evaluation.
#[pyclass(frozen, get_all)]
#[derive(Debug, Clone)]
pub struct TriggerResult {
    pub rule_name: String,
    pub fired: bool,
    pub action_type: TriggerActionType,
    pub current_value: f64,
    pub threshold: f64,
    pub reason: String,
}

#[pymethods]
impl TriggerResult {
    fn __repr__(&self) -> String {
        format!("TriggerResult(rule='{}', fired={}, reason='{}')",
            self.rule_name, self.fired, self.reason)
    }
}

/// Evaluate a trigger condition (pure, stateless).
///
/// `current_value`: the current feed/metric value.
/// `previous_value`: the previous value (for cross/change conditions).
/// `last_fire_time`: Unix timestamp of last fire (for cooldown).
/// `now`: current Unix timestamp.
/// `armed`: whether the trigger is currently armed (for hysteresis).
#[pyfunction]
#[pyo3(signature = (rule, current_value, previous_value=0.0, last_fire_time=0.0, now=0.0, armed=true))]
pub fn evaluate_trigger(
    rule: &TriggerRule,
    current_value: f64,
    previous_value: f64,
    last_fire_time: f64,
    now: f64,
    armed: bool,
) -> TriggerResult {
    // Cooldown check
    if last_fire_time > 0.0 && now > 0.0 && (now - last_fire_time) < rule.cooldown_secs {
        return TriggerResult {
            rule_name: rule.name.clone(),
            fired: false,
            action_type: rule.action_type,
            current_value,
            threshold: rule.threshold,
            reason: "cooldown active".to_string(),
        };
    }

    if !armed {
        return TriggerResult {
            rule_name: rule.name.clone(),
            fired: false,
            action_type: rule.action_type,
            current_value,
            threshold: rule.threshold,
            reason: "not armed".to_string(),
        };
    }

    let (fired, reason) = match rule.condition {
        TriggerCondition::Above => {
            let thresh = if rule.hysteresis > 0.0 && current_value > rule.threshold {
                rule.threshold - rule.hysteresis
            } else {
                rule.threshold
            };
            (current_value > thresh, format!("{:.4} > {:.4}", current_value, thresh))
        }
        TriggerCondition::Below => {
            let thresh = if rule.hysteresis > 0.0 && current_value < rule.threshold {
                rule.threshold + rule.hysteresis
            } else {
                rule.threshold
            };
            (current_value < thresh, format!("{:.4} < {:.4}", current_value, thresh))
        }
        TriggerCondition::CrossAbove => {
            let crossed = previous_value <= rule.threshold && current_value > rule.threshold;
            (crossed, format!("{:.4} crossed above {:.4}", current_value, rule.threshold))
        }
        TriggerCondition::CrossBelow => {
            let crossed = previous_value >= rule.threshold && current_value < rule.threshold;
            (crossed, format!("{:.4} crossed below {:.4}", current_value, rule.threshold))
        }
        TriggerCondition::PercentChange => {
            let pct = if previous_value.abs() > 1e-10 {
                ((current_value - previous_value) / previous_value).abs()
            } else {
                0.0
            };
            (pct > rule.threshold, format!("{:.2}% change > {:.2}%", pct * 100.0, rule.threshold * 100.0))
        }
        TriggerCondition::AbsoluteChange => {
            let abs_change = (current_value - previous_value).abs();
            (abs_change > rule.threshold, format!("{:.4} abs change > {:.4}", abs_change, rule.threshold))
        }
        TriggerCondition::Between => {
            let inside = current_value >= rule.threshold && current_value <= rule.threshold_upper;
            (inside, format!("{:.4} in [{:.4}, {:.4}]", current_value, rule.threshold, rule.threshold_upper))
        }
        TriggerCondition::Outside => {
            let outside = current_value < rule.threshold || current_value > rule.threshold_upper;
            (outside, format!("{:.4} outside [{:.4}, {:.4}]", current_value, rule.threshold, rule.threshold_upper))
        }
    };

    TriggerResult {
        rule_name: rule.name.clone(),
        fired,
        action_type: rule.action_type,
        current_value,
        threshold: rule.threshold,
        reason,
    }
}

/// Convenience: create a TriggerRule from parameters.
#[pyfunction]
#[pyo3(signature = (name, condition, threshold, action_type, threshold_upper=0.0, cooldown_secs=60.0, hysteresis=0.0))]
pub fn create_trigger(
    name: String,
    condition: TriggerCondition,
    threshold: f64,
    action_type: TriggerActionType,
    threshold_upper: f64,
    cooldown_secs: f64,
    hysteresis: f64,
) -> TriggerRule {
    TriggerRule { name, condition, threshold, threshold_upper, action_type, cooldown_secs, hysteresis }
}

pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> {
    m.add_class::<TriggerCondition>()?;
    m.add_class::<TriggerActionType>()?;
    m.add_class::<TriggerRule>()?;
    m.add_class::<TriggerResult>()?;
    m.add_function(wrap_pyfunction!(evaluate_trigger, m)?)?;
    m.add_function(wrap_pyfunction!(create_trigger, m)?)?;
    Ok(())
}

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

    fn make_rule(condition: TriggerCondition, threshold: f64) -> TriggerRule {
        TriggerRule {
            name: "test".to_string(),
            condition,
            threshold,
            threshold_upper: 0.0,
            action_type: TriggerActionType::Alert,
            cooldown_secs: 0.0,
            hysteresis: 0.0,
        }
    }

    #[test]
    fn test_above() {
        let rule = make_rule(TriggerCondition::Above, 0.5);
        let r = evaluate_trigger(&rule, 0.6, 0.0, 0.0, 0.0, true);
        assert!(r.fired);
    }

    #[test]
    fn test_above_not_fired() {
        let rule = make_rule(TriggerCondition::Above, 0.5);
        let r = evaluate_trigger(&rule, 0.4, 0.0, 0.0, 0.0, true);
        assert!(!r.fired);
    }

    #[test]
    fn test_below() {
        let rule = make_rule(TriggerCondition::Below, 0.5);
        let r = evaluate_trigger(&rule, 0.3, 0.0, 0.0, 0.0, true);
        assert!(r.fired);
    }

    #[test]
    fn test_cross_above() {
        let rule = make_rule(TriggerCondition::CrossAbove, 0.5);
        let r = evaluate_trigger(&rule, 0.6, 0.4, 0.0, 0.0, true);
        assert!(r.fired);
    }

    #[test]
    fn test_cross_above_already_above() {
        let rule = make_rule(TriggerCondition::CrossAbove, 0.5);
        let r = evaluate_trigger(&rule, 0.6, 0.55, 0.0, 0.0, true);
        assert!(!r.fired);
    }

    #[test]
    fn test_cross_below() {
        let rule = make_rule(TriggerCondition::CrossBelow, 0.5);
        let r = evaluate_trigger(&rule, 0.4, 0.6, 0.0, 0.0, true);
        assert!(r.fired);
    }

    #[test]
    fn test_percent_change() {
        let rule = make_rule(TriggerCondition::PercentChange, 0.05);
        let r = evaluate_trigger(&rule, 1.1, 1.0, 0.0, 0.0, true);
        assert!(r.fired); // 10% > 5%
    }

    #[test]
    fn test_absolute_change() {
        let rule = make_rule(TriggerCondition::AbsoluteChange, 0.01);
        let r = evaluate_trigger(&rule, 0.52, 0.50, 0.0, 0.0, true);
        assert!(r.fired); // 0.02 > 0.01
    }

    #[test]
    fn test_between() {
        let mut rule = make_rule(TriggerCondition::Between, 0.3);
        rule = TriggerRule { threshold_upper: 0.7, ..rule };
        let r = evaluate_trigger(&rule, 0.5, 0.0, 0.0, 0.0, true);
        assert!(r.fired);
    }

    #[test]
    fn test_outside() {
        let mut rule = make_rule(TriggerCondition::Outside, 0.3);
        rule = TriggerRule { threshold_upper: 0.7, ..rule };
        let r = evaluate_trigger(&rule, 0.1, 0.0, 0.0, 0.0, true);
        assert!(r.fired);
    }

    #[test]
    fn test_cooldown() {
        let mut rule = make_rule(TriggerCondition::Above, 0.5);
        rule = TriggerRule { cooldown_secs: 60.0, ..rule };
        let r = evaluate_trigger(&rule, 0.6, 0.0, 100.0, 130.0, true);
        assert!(!r.fired);
        assert!(r.reason.contains("cooldown"));
    }

    #[test]
    fn test_not_armed() {
        let rule = make_rule(TriggerCondition::Above, 0.5);
        let r = evaluate_trigger(&rule, 0.6, 0.0, 0.0, 0.0, false);
        assert!(!r.fired);
        assert!(r.reason.contains("not armed"));
    }

    #[test]
    fn test_hysteresis() {
        let mut rule = make_rule(TriggerCondition::Above, 0.5);
        rule = TriggerRule { hysteresis: 0.05, ..rule };
        // Value above threshold; hysteresis lowers effective threshold
        let r = evaluate_trigger(&rule, 0.52, 0.0, 0.0, 0.0, true);
        assert!(r.fired); // 0.52 > 0.45 (0.5 - 0.05)
    }

    #[test]
    fn test_create_trigger() {
        let rule = create_trigger(
            "my_trigger".to_string(),
            TriggerCondition::Above,
            0.5,
            TriggerActionType::Alert,
            0.0,
            60.0,
            0.01,
        );
        assert_eq!(rule.name, "my_trigger");
        assert_eq!(rule.condition, TriggerCondition::Above);
    }
}
