//! Stop-rule nodes — emit a protective-stop *distance* (price units, ≥ 0,
//! direction-agnostic). The stop DEFINES the risk unit R (1R = the loss if stopped);
//! position-management latches the entry-cycle distance as the frozen R-denominator.
//!
//! `FixedStop` and `VolTfStop` (`vol_tf_stop.rs`) are the fused stop-rule
//! PRIMITIVES: `FixedStop` is a constant distance gated on its price input (a
//! source-less `Const` has no firing trigger in the push model, so the
//! triggered-constant shape is the honest primitive); `VolTfStop` folds
//! `k · √EMA(Δ², length)` over completed timescale-matched buckets. The
//! per-cycle volatility stop remains NOT a primitive — it is a COMPOSITION of
//! primitives, `k · Sqrt(Ema(Mul(Δ,Δ), length))` with
//! `Δ = Sub(price, Delay(price,1))` (a rolling EWMA standard deviation), built
//! with `GraphBuilder` — see `crates/aura-engine/tests/vol_stop_composite.rs`.
//! True-range ATR (a richer stop) is deferred — it needs OHLC.
use aura_core::{
    Cell, Ctx, FieldSpec, Firing, Node, NodeSchema, ParamSpec, PortSpec, PrimitiveBuilder,
    ScalarKind,
};

/// Constant stop distance, gated on a price input. `distance` must be > 0.
pub struct FixedStop {
    distance: f64,
    out: [Cell; 1],
}
impl FixedStop {
    pub fn new(distance: f64) -> Self {
        assert!(distance > 0.0, "FixedStop distance must be > 0");
        Self { distance, out: [Cell::from_f64(distance)] }
    }
    pub fn builder() -> PrimitiveBuilder {
        PrimitiveBuilder::new(
            "FixedStop",
            NodeSchema {
                inputs: vec![PortSpec { kind: ScalarKind::F64, firing: Firing::Any, name: "price".into() }],
                output: vec![FieldSpec { name: "stop_distance".into(), kind: ScalarKind::F64 }],
                params: vec![ParamSpec { name: "distance".into(), kind: ScalarKind::F64 }],
            },
            |p| Box::new(FixedStop::new(p[0].f64())),
        )
    }
}
impl Node for FixedStop {
    fn lookbacks(&self) -> Vec<usize> {
        vec![1]
    }
    fn eval(&mut self, ctx: Ctx<'_>) -> Option<&[Cell]> {
        if ctx.f64_in(0).is_empty() {
            return None; // fire with price (warm-up filter)
        }
        self.out[0] = Cell::from_f64(self.distance);
        Some(&self.out)
    }
    fn label(&self) -> String {
        format!("FixedStop({})", self.distance)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use aura_core::{AnyColumn, Scalar, Timestamp};
    fn feed(node: &mut dyn Node, prices: &[f64]) -> Vec<Option<f64>> {
        let mut col = vec![AnyColumn::with_capacity(ScalarKind::F64, 1)];
        let mut out = vec![];
        for &p in prices {
            col[0].push(Scalar::f64(p)).unwrap();
            out.push(node.eval(Ctx::new(&col, Timestamp(0))).map(|c| c[0].f64()));
        }
        out
    }
    #[test]
    fn fixed_stop_is_constant_after_first_price() {
        let mut s = FixedStop::new(2.5);
        assert_eq!(feed(&mut s, &[100.0, 110.0, 90.0]), vec![Some(2.5), Some(2.5), Some(2.5)]);
    }
}
