#[cfg(feature = "python")]
use numpy::{IntoPyArray, PyArray1, PyArrayMethods, PyReadonlyArray1};
#[cfg(feature = "python")]
use pyo3::exceptions::PyValueError;
#[cfg(feature = "python")]
use pyo3::prelude::*;
#[cfg(feature = "python")]
use pyo3::types::{PyDict, PyList};

#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
use serde::{Deserialize, Serialize};
#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
use wasm_bindgen::prelude::*;

use crate::utilities::data_loader::Candles;
#[cfg(all(feature = "python", feature = "cuda"))]
use crate::utilities::dlpack_cuda::export_f32_cuda_dlpack_2d;
use crate::utilities::enums::Kernel;
use crate::utilities::helpers::{
    alloc_with_nan_prefix, detect_best_batch_kernel, detect_best_kernel, init_matrix_prefixes,
    make_uninit_matrix,
};
#[cfg(feature = "python")]
use crate::utilities::kernel_validation::validate_kernel;

#[cfg(not(target_arch = "wasm32"))]
use rayon::prelude::*;

use std::convert::AsRef;
use std::error::Error;
use std::mem::MaybeUninit;
use thiserror::Error;

#[cfg(all(feature = "python", feature = "cuda"))]
use crate::cuda::moving_averages::alma_wrapper::DeviceArrayF32 as DeviceArrayF32Cuda;
#[cfg(feature = "cuda")]
use crate::cuda::{CudaCeError, CudaChandelierExit};
use crate::indicators::atr::{atr_with_kernel, AtrInput, AtrParams};
#[cfg(all(feature = "python", feature = "cuda"))]
use cust::context::Context as CudaContext;
#[cfg(all(feature = "python", feature = "cuda"))]
use cust::memory::DeviceBuffer;
#[cfg(all(feature = "python", feature = "cuda"))]
use std::sync::Arc;

#[cfg(all(feature = "python", feature = "cuda"))]
#[pyclass(module = "ta_indicators.cuda", unsendable)]
pub struct CeDeviceArrayF32Py {
    pub(crate) inner: DeviceArrayF32Cuda,
    pub(crate) _ctx: Arc<CudaContext>,
    pub(crate) device_id: u32,
}

#[cfg(all(feature = "python", feature = "cuda"))]
#[pymethods]
impl CeDeviceArrayF32Py {
    #[getter]
    fn __cuda_array_interface__<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyDict>> {
        let inner = &self.inner;
        let d = PyDict::new(py);
        let item = std::mem::size_of::<f32>();
        d.set_item("shape", (inner.rows, inner.cols))?;
        d.set_item("typestr", "<f4")?;
        d.set_item("strides", (inner.cols * item, item))?;
        let size = inner.rows.saturating_mul(inner.cols);
        let ptr_val: usize = if size == 0 {
            0
        } else {
            inner.buf.as_device_ptr().as_raw() as usize
        };
        d.set_item("data", (ptr_val, false))?;
        d.set_item("version", 3)?;
        Ok(d)
    }

    fn __dlpack_device__(&self) -> PyResult<(i32, i32)> {
        Ok((2, self.device_id as i32))
    }

    #[pyo3(signature = (stream=None, max_version=None, dl_device=None, copy=None))]
    fn __dlpack__<'py>(
        &mut self,
        py: Python<'py>,
        stream: Option<pyo3::PyObject>,
        max_version: Option<pyo3::PyObject>,
        dl_device: Option<pyo3::PyObject>,
        copy: Option<pyo3::PyObject>,
    ) -> PyResult<PyObject> {
        let (kdl, alloc_dev) = self.__dlpack_device__()?;
        if let Some(dev_obj) = dl_device.as_ref() {
            if let Ok((dev_ty, dev_id)) = dev_obj.extract::<(i32, i32)>(py) {
                if dev_ty != kdl || dev_id != alloc_dev {
                    let wants_copy = copy
                        .as_ref()
                        .and_then(|c| c.extract::<bool>(py).ok())
                        .unwrap_or(false);
                    if wants_copy {
                        return Err(PyValueError::new_err(
                            "device copy not implemented for __dlpack__",
                        ));
                    } else {
                        return Err(PyValueError::new_err("dl_device mismatch for __dlpack__"));
                    }
                }
            }
        }
        let _ = stream;

        let dummy =
            DeviceBuffer::from_slice(&[]).map_err(|e| PyValueError::new_err(e.to_string()))?;
        let inner = std::mem::replace(
            &mut self.inner,
            DeviceArrayF32Cuda {
                buf: dummy,
                rows: 0,
                cols: 0,
            },
        );

        let rows = inner.rows;
        let cols = inner.cols;
        let buf = inner.buf;

        let max_version_bound = max_version.map(|obj| obj.into_bound(py));

        export_f32_cuda_dlpack_2d(py, buf, rows, cols, alloc_dev, max_version_bound)
    }
}

impl<'a> AsRef<[f64]> for ChandelierExitInput<'a> {
    #[inline(always)]
    fn as_ref(&self) -> &[f64] {
        match &self.data {
            ChandelierExitData::Slices { close, .. } => close,
            ChandelierExitData::Candles { candles, .. } => &candles.close,
        }
    }
}

#[derive(Debug, Clone)]
pub enum ChandelierExitData<'a> {
    Candles {
        candles: &'a Candles,
    },
    Slices {
        high: &'a [f64],
        low: &'a [f64],
        close: &'a [f64],
    },
}

#[derive(Debug, Clone)]
pub struct ChandelierExitOutput {
    pub long_stop: Vec<f64>,
    pub short_stop: Vec<f64>,
}

#[derive(Debug, Clone)]
#[cfg_attr(
    all(target_arch = "wasm32", feature = "wasm"),
    derive(Serialize, Deserialize)
)]
pub struct ChandelierExitParams {
    pub period: Option<usize>,
    pub mult: Option<f64>,
    pub use_close: Option<bool>,
}

impl Default for ChandelierExitParams {
    fn default() -> Self {
        Self {
            period: Some(22),
            mult: Some(3.0),
            use_close: Some(true),
        }
    }
}

#[derive(Debug, Clone)]
pub struct ChandelierExitInput<'a> {
    pub data: ChandelierExitData<'a>,
    pub params: ChandelierExitParams,
}

impl<'a> ChandelierExitInput<'a> {
    #[inline]
    pub fn from_candles(c: &'a Candles, p: ChandelierExitParams) -> Self {
        Self {
            data: ChandelierExitData::Candles { candles: c },
            params: p,
        }
    }

    #[inline]
    pub fn from_slices(
        high: &'a [f64],
        low: &'a [f64],
        close: &'a [f64],
        p: ChandelierExitParams,
    ) -> Self {
        Self {
            data: ChandelierExitData::Slices { high, low, close },
            params: p,
        }
    }

    #[inline]
    pub fn with_default_candles(c: &'a Candles) -> Self {
        Self::from_candles(c, ChandelierExitParams::default())
    }

    #[inline]
    pub fn get_period(&self) -> usize {
        self.params.period.unwrap_or(22)
    }

    #[inline]
    pub fn get_mult(&self) -> f64 {
        self.params.mult.unwrap_or(3.0)
    }

    #[inline]
    pub fn get_use_close(&self) -> bool {
        self.params.use_close.unwrap_or(true)
    }
}

#[derive(Copy, Clone, Debug)]
pub struct ChandelierExitBuilder {
    period: Option<usize>,
    mult: Option<f64>,
    use_close: Option<bool>,
    kernel: Kernel,
}

impl Default for ChandelierExitBuilder {
    fn default() -> Self {
        Self {
            period: None,
            mult: None,
            use_close: None,
            kernel: Kernel::Auto,
        }
    }
}

impl ChandelierExitBuilder {
    #[inline(always)]
    pub fn new() -> Self {
        Self::default()
    }

    #[inline(always)]
    pub fn period(mut self, val: usize) -> Self {
        self.period = Some(val);
        self
    }

    #[inline(always)]
    pub fn mult(mut self, val: f64) -> Self {
        self.mult = Some(val);
        self
    }

    #[inline(always)]
    pub fn use_close(mut self, val: bool) -> Self {
        self.use_close = Some(val);
        self
    }

    #[inline(always)]
    pub fn kernel(mut self, k: Kernel) -> Self {
        self.kernel = k;
        self
    }

    #[inline(always)]
    pub fn build(self) -> ChandelierExitParams {
        ChandelierExitParams {
            period: self.period,
            mult: self.mult,
            use_close: self.use_close,
        }
    }

    #[inline(always)]
    pub fn apply_candles(self, c: &Candles) -> Result<ChandelierExitOutput, ChandelierExitError> {
        let p = self.build();
        let i = ChandelierExitInput::from_candles(c, p);
        chandelier_exit_with_kernel(&i, self.kernel)
    }

    #[inline(always)]
    pub fn apply_slices(
        self,
        h: &[f64],
        l: &[f64],
        c: &[f64],
    ) -> Result<ChandelierExitOutput, ChandelierExitError> {
        let p = self.build();
        let i = ChandelierExitInput::from_slices(h, l, c, p);
        chandelier_exit_with_kernel(&i, self.kernel)
    }

    #[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
    #[inline(always)]
    pub fn into_stream(self) -> Result<ChandelierExitStream, ChandelierExitError> {
        ChandelierExitStream::try_new(self.build())
    }
}

#[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
#[derive(Debug, Clone)]
pub struct ChandelierExitStream {
    period: usize,
    mult: f64,
    use_close: bool,

    i: usize,

    alpha: f64,
    atr_prev: Option<f64>,
    warm_tr_sum: f64,
    prev_close: Option<f64>,

    long_raw_prev: f64,
    short_raw_prev: f64,
    dir_prev: i8,

    cap: usize,
    mask: usize,

    dq_max_idx: Vec<usize>,
    dq_max_val: Vec<f64>,
    hmax: usize,
    tmax: usize,

    dq_min_idx: Vec<usize>,
    dq_min_val: Vec<f64>,
    hmin: usize,
    tmin: usize,
}

#[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
impl ChandelierExitStream {
    pub fn try_new(p: ChandelierExitParams) -> Result<Self, ChandelierExitError> {
        let period = p.period.unwrap_or(22);
        if period == 0 {
            return Err(ChandelierExitError::InvalidPeriod {
                period,
                data_len: 0,
            });
        }
        let mult = p.mult.unwrap_or(3.0);
        let use_close = p.use_close.unwrap_or(true);

        let cap = period.next_power_of_two();
        Ok(Self {
            period,
            mult,
            use_close,
            i: 0,

            alpha: 1.0 / (period as f64),
            atr_prev: None,
            warm_tr_sum: 0.0,
            prev_close: None,

            long_raw_prev: f64::NAN,
            short_raw_prev: f64::NAN,
            dir_prev: 1,

            cap,
            mask: cap - 1,
            dq_max_idx: vec![0usize; cap],
            dq_max_val: vec![f64::NAN; cap],
            hmax: 0,
            tmax: 0,
            dq_min_idx: vec![0usize; cap],
            dq_min_val: vec![f64::NAN; cap],
            hmin: 0,
            tmin: 0,
        })
    }

    #[inline(always)]
    pub fn get_warmup_period(&self) -> usize {
        self.period - 1
    }

    #[inline(always)]
    fn evict_old(&mut self, i: usize) {
        while self.hmax != self.tmax {
            let idx = self.dq_max_idx[self.hmax & self.mask];
            if idx + self.period <= i {
                self.hmax = self.hmax.wrapping_add(1);
            } else {
                break;
            }
        }
        while self.hmin != self.tmin {
            let idx = self.dq_min_idx[self.hmin & self.mask];
            if idx + self.period <= i {
                self.hmin = self.hmin.wrapping_add(1);
            } else {
                break;
            }
        }
    }
    #[inline(always)]
    fn push_max(&mut self, i: usize, v: f64) {
        if v.is_nan() {
            return;
        }

        while self.hmax != self.tmax {
            let back_pos = (self.tmax.wrapping_sub(1)) & self.mask;
            if self.dq_max_val[back_pos] < v {
                self.tmax = self.tmax.wrapping_sub(1);
            } else {
                break;
            }
        }
        let pos = self.tmax & self.mask;
        self.dq_max_idx[pos] = i;
        self.dq_max_val[pos] = v;
        self.tmax = self.tmax.wrapping_add(1);
    }
    #[inline(always)]
    fn push_min(&mut self, i: usize, v: f64) {
        if v.is_nan() {
            return;
        }

        while self.hmin != self.tmin {
            let back_pos = (self.tmin.wrapping_sub(1)) & self.mask;
            if self.dq_min_val[back_pos] > v {
                self.tmin = self.tmin.wrapping_sub(1);
            } else {
                break;
            }
        }
        let pos = self.tmin & self.mask;
        self.dq_min_idx[pos] = i;
        self.dq_min_val[pos] = v;
        self.tmin = self.tmin.wrapping_add(1);
    }
    #[inline(always)]
    fn front_max(&self) -> f64 {
        if self.hmax != self.tmax {
            self.dq_max_val[self.hmax & self.mask]
        } else {
            f64::NAN
        }
    }
    #[inline(always)]
    fn front_min(&self) -> f64 {
        if self.hmin != self.tmin {
            self.dq_min_val[self.hmin & self.mask]
        } else {
            f64::NAN
        }
    }

    #[inline(always)]
    fn true_range(high: f64, low: f64, prev_close: Option<f64>) -> f64 {
        if let Some(pc) = prev_close {
            let hl = (high - low).abs();
            let hc = (high - pc).abs();
            let lc = (low - pc).abs();
            hl.max(hc.max(lc))
        } else {
            (high - low).abs()
        }
    }

    #[inline(always)]
    pub fn update(&mut self, high: f64, low: f64, close: f64) -> Option<(f64, f64)> {
        let i = self.i;
        let warm = self.period - 1;

        let tr = Self::true_range(high, low, self.prev_close);

        self.evict_old(i);
        if self.use_close {
            self.push_max(i, close);
            self.push_min(i, close);
        } else {
            self.push_max(i, high);
            self.push_min(i, low);
        }

        let atr = if let Some(prev) = self.atr_prev {
            let next = (tr - prev).mul_add(self.alpha, prev);
            self.atr_prev = Some(next);
            next
        } else {
            self.warm_tr_sum += tr;
            if i < warm {
                self.prev_close = Some(close);
                self.i = i + 1;
                return None;
            }
            let seed = self.warm_tr_sum * self.alpha;
            self.atr_prev = Some(seed);
            seed
        };

        let highest = self.front_max();
        let lowest = self.front_min();

        let ls0 = (-self.mult).mul_add(atr, highest);
        let ss0 = (self.mult).mul_add(atr, lowest);

        let lsp = if self.long_raw_prev.is_nan() {
            ls0
        } else {
            self.long_raw_prev
        };
        let ssp = if self.short_raw_prev.is_nan() {
            ss0
        } else {
            self.short_raw_prev
        };

        let (ls, ss) = if i > warm {
            if let Some(pc) = self.prev_close {
                let ls = if pc > lsp { ls0.max(lsp) } else { ls0 };
                let ss = if pc < ssp { ss0.min(ssp) } else { ss0 };
                (ls, ss)
            } else {
                (ls0, ss0)
            }
        } else {
            (ls0, ss0)
        };

        let d = if close > ssp {
            1
        } else if close < lsp {
            -1
        } else {
            self.dir_prev
        };

        self.long_raw_prev = ls;
        self.short_raw_prev = ss;
        self.dir_prev = d;
        self.prev_close = Some(close);
        self.i = i + 1;

        Some((
            if d == 1 { ls } else { f64::NAN },
            if d == -1 { ss } else { f64::NAN },
        ))
    }
}

#[derive(Error, Debug)]
pub enum ChandelierExitError {
    #[error("chandelier_exit: Input data slice is empty.")]
    EmptyInputData,

    #[error("chandelier_exit: All values are NaN.")]
    AllValuesNaN,

    #[error("chandelier_exit: Invalid period: period = {period}, data length = {data_len}")]
    InvalidPeriod { period: usize, data_len: usize },

    #[error("chandelier_exit: Not enough valid data: needed = {needed}, valid = {valid}")]
    NotEnoughValidData { needed: usize, valid: usize },

    #[error("chandelier_exit: Inconsistent data lengths - high: {high_len}, low: {low_len}, close: {close_len}")]
    InconsistentDataLengths {
        high_len: usize,
        low_len: usize,
        close_len: usize,
    },

    #[error("chandelier_exit: ATR calculation error: {0}")]
    AtrError(String),

    #[error("chandelier_exit: Output length mismatch: expected {expected}, got {got}")]
    OutputLengthMismatch { expected: usize, got: usize },

    #[error("chandelier_exit: Invalid range: start={start}, end={end}, step={step}")]
    InvalidRange {
        start: String,
        end: String,
        step: String,
    },

    #[error("chandelier_exit: Invalid kernel for batch: {0:?}")]
    InvalidKernelForBatch(crate::utilities::enums::Kernel),
}

#[inline]
fn window_max(a: &[f64]) -> f64 {
    let mut m = f64::NAN;
    for &v in a {
        if v.is_nan() {
            continue;
        }
        if m.is_nan() || v > m {
            m = v;
        }
    }
    m
}

#[inline]
fn window_min(a: &[f64]) -> f64 {
    let mut m = f64::NAN;
    for &v in a {
        if v.is_nan() {
            continue;
        }
        if m.is_nan() || v < m {
            m = v;
        }
    }
    m
}

#[inline(always)]
fn ce_first_valid(
    use_close: bool,
    h: &[f64],
    l: &[f64],
    c: &[f64],
) -> Result<usize, ChandelierExitError> {
    let fc = c.iter().position(|x| !x.is_nan());
    if use_close {
        return fc.ok_or(ChandelierExitError::AllValuesNaN);
    }
    let fh = h.iter().position(|x| !x.is_nan());
    let fl = l.iter().position(|x| !x.is_nan());
    let f = match (fh, fl, fc) {
        (Some(a), Some(b), Some(d)) => Some(a.min(b).min(d)),
        _ => None,
    };
    f.ok_or(ChandelierExitError::AllValuesNaN)
}

#[inline(always)]
fn ce_prepare<'a>(
    input: &'a ChandelierExitInput,
    kern: Kernel,
) -> Result<
    (
        &'a [f64],
        &'a [f64],
        &'a [f64],
        usize,
        f64,
        bool,
        usize,
        Kernel,
    ),
    ChandelierExitError,
> {
    let (h, l, c) = match &input.data {
        ChandelierExitData::Candles { candles } => {
            if candles.close.is_empty() {
                return Err(ChandelierExitError::EmptyInputData);
            }
            (&candles.high[..], &candles.low[..], &candles.close[..])
        }
        ChandelierExitData::Slices { high, low, close } => {
            if high.len() != low.len() || low.len() != close.len() {
                return Err(ChandelierExitError::InconsistentDataLengths {
                    high_len: high.len(),
                    low_len: low.len(),
                    close_len: close.len(),
                });
            }
            if close.is_empty() {
                return Err(ChandelierExitError::EmptyInputData);
            }
            (*high, *low, *close)
        }
    };
    let len = c.len();
    let period = input.get_period();
    let mult = input.get_mult();
    let use_close = input.get_use_close();

    if period == 0 || period > len {
        return Err(ChandelierExitError::InvalidPeriod {
            period,
            data_len: len,
        });
    }

    let first = ce_first_valid(use_close, h, l, c)?;
    if len - first < period {
        return Err(ChandelierExitError::NotEnoughValidData {
            needed: period,
            valid: len - first,
        });
    }

    let chosen = match kern {
        Kernel::Auto => Kernel::Scalar,
        k => k,
    };
    Ok((h, l, c, period, mult, use_close, first, chosen))
}

#[inline(always)]
fn map_kernel_for_atr(k: Kernel) -> Kernel {
    #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
    {
        k
    }
    #[cfg(not(all(feature = "nightly-avx", target_arch = "x86_64")))]
    {
        match k {
            Kernel::Avx2 | Kernel::Avx512 | Kernel::Avx2Batch | Kernel::Avx512Batch => {
                Kernel::Scalar
            }
            _ => k,
        }
    }
}

#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
#[inline]
fn ce_avx2_fill(
    long_dst: &mut [f64],
    short_dst: &mut [f64],
    h: &[f64],
    l: &[f64],
    c: &[f64],
    atr: &[f64],
    period: usize,
    mult: f64,
    use_close: bool,
    first: usize,
) {
    let len = c.len();
    let warm = first + period - 1;

    #[inline(always)]
    fn gt(a: f64, b: f64) -> bool {
        !a.is_nan() && !b.is_nan() && a > b
    }
    #[inline(always)]
    fn lt(a: f64, b: f64) -> bool {
        !a.is_nan() && !b.is_nan() && a < b
    }

    let cap = period.next_power_of_two();
    let mask = cap - 1;
    let mut dq_max = vec![0usize; cap];
    let mut dq_min = vec![0usize; cap];
    let mut hmax = 0usize;
    let mut tmax = 0usize;
    let mut hmin = 0usize;
    let mut tmin = 0usize;

    let (src_max, src_min) = if use_close { (c, c) } else { (h, l) };

    let mut long_raw_prev = f64::NAN;
    let mut short_raw_prev = f64::NAN;
    let mut prev_dir: i8 = 1;

    unsafe {
        for i in 0..len {
            while hmax != tmax {
                let idx = *dq_max.get_unchecked(hmax & mask);
                if idx + period <= i {
                    hmax = hmax.wrapping_add(1);
                } else {
                    break;
                }
            }
            while hmin != tmin {
                let idx = *dq_min.get_unchecked(hmin & mask);
                if idx + period <= i {
                    hmin = hmin.wrapping_add(1);
                } else {
                    break;
                }
            }

            let vmax = *src_max.get_unchecked(i);
            if !vmax.is_nan() {
                while hmax != tmax {
                    let back_idx = *dq_max.get_unchecked((tmax.wrapping_sub(1)) & mask);
                    let back_v = *src_max.get_unchecked(back_idx);
                    if back_v < vmax {
                        tmax = tmax.wrapping_sub(1);
                    } else {
                        break;
                    }
                }
                *dq_max.get_unchecked_mut(tmax & mask) = i;
                tmax = tmax.wrapping_add(1);
            }
            let vmin = *src_min.get_unchecked(i);
            if !vmin.is_nan() {
                while hmin != tmin {
                    let back_idx = *dq_min.get_unchecked((tmin.wrapping_sub(1)) & mask);
                    let back_v = *src_min.get_unchecked(back_idx);
                    if back_v > vmin {
                        tmin = tmin.wrapping_sub(1);
                    } else {
                        break;
                    }
                }
                *dq_min.get_unchecked_mut(tmin & mask) = i;
                tmin = tmin.wrapping_add(1);
            }

            if i < warm {
                continue;
            }

            let highest = if hmax != tmax {
                *src_max.get_unchecked(*dq_max.get_unchecked(hmax & mask))
            } else {
                f64::NAN
            };
            let lowest = if hmin != tmin {
                *src_min.get_unchecked(*dq_min.get_unchecked(hmin & mask))
            } else {
                f64::NAN
            };

            let ai = *atr.get_unchecked(i);
            let ls0 = ai.mul_add(-mult, highest);
            let ss0 = ai.mul_add(mult, lowest);

            let lsp = if i == warm || long_raw_prev.is_nan() {
                ls0
            } else {
                long_raw_prev
            };
            let ssp = if i == warm || short_raw_prev.is_nan() {
                ss0
            } else {
                short_raw_prev
            };

            let prev_close = *c.get_unchecked(i - (i > warm) as usize);
            let ls = if i > warm && gt(prev_close, lsp) {
                ls0.max(lsp)
            } else {
                ls0
            };
            let ss = if i > warm && lt(prev_close, ssp) {
                ss0.min(ssp)
            } else {
                ss0
            };

            let d = if gt(*c.get_unchecked(i), ssp) {
                1
            } else if lt(*c.get_unchecked(i), lsp) {
                -1
            } else {
                prev_dir
            };

            long_raw_prev = ls;
            short_raw_prev = ss;
            prev_dir = d;
            *long_dst.get_unchecked_mut(i) = if d == 1 { ls } else { f64::NAN };
            *short_dst.get_unchecked_mut(i) = if d == -1 { ss } else { f64::NAN };
        }
    }
}

#[inline]
pub fn chandelier_exit(
    input: &ChandelierExitInput,
) -> Result<ChandelierExitOutput, ChandelierExitError> {
    chandelier_exit_with_kernel(input, Kernel::Auto)
}

pub fn chandelier_exit_with_kernel(
    input: &ChandelierExitInput,
    kern: Kernel,
) -> Result<ChandelierExitOutput, ChandelierExitError> {
    let (high, low, close, period, mult, use_close, first, chosen) = ce_prepare(input, kern)?;

    let atr_in = AtrInput::from_slices(
        high,
        low,
        close,
        AtrParams {
            length: Some(period),
        },
    );
    let atr = atr_with_kernel(&atr_in, map_kernel_for_atr(chosen))
        .map_err(|e| ChandelierExitError::AtrError(e.to_string()))?
        .values;

    let len = close.len();
    let warm = first + period - 1;

    let mut long_stop = alloc_with_nan_prefix(len, warm);
    let mut short_stop = alloc_with_nan_prefix(len, warm);

    #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
    if matches!(chosen, Kernel::Avx2 | Kernel::Avx512) {
        ce_avx2_fill(
            &mut long_stop,
            &mut short_stop,
            high,
            low,
            close,
            &atr,
            period,
            mult,
            use_close,
            first,
        );
    } else {
        let mut long_raw_prev = f64::NAN;
        let mut short_raw_prev = f64::NAN;
        let mut prev_dir: i8 = 1;

        let cap = period.next_power_of_two();
        let mask = cap - 1;
        let mut dq_max = vec![0usize; cap];
        let mut dq_min = vec![0usize; cap];
        let mut hmax = 0usize;
        let mut tmax = 0usize;
        let mut hmin = 0usize;
        let mut tmin = 0usize;

        let (src_max, src_min) = if use_close {
            (close, close)
        } else {
            (high, low)
        };

        for i in 0..len {
            while hmax != tmax {
                let idx = dq_max[hmax & mask];
                if idx + period <= i {
                    hmax = hmax.wrapping_add(1);
                } else {
                    break;
                }
            }
            while hmin != tmin {
                let idx = dq_min[hmin & mask];
                if idx + period <= i {
                    hmin = hmin.wrapping_add(1);
                } else {
                    break;
                }
            }

            let v_max = src_max[i];
            if !v_max.is_nan() {
                while hmax != tmax {
                    let back_pos = (tmax.wrapping_sub(1)) & mask;
                    let back_idx = dq_max[back_pos];
                    if src_max[back_idx] < v_max {
                        tmax = tmax.wrapping_sub(1);
                    } else {
                        break;
                    }
                }
                dq_max[tmax & mask] = i;
                tmax = tmax.wrapping_add(1);
            }

            let v_min = src_min[i];
            if !v_min.is_nan() {
                while hmin != tmin {
                    let back_pos = (tmin.wrapping_sub(1)) & mask;
                    let back_idx = dq_min[back_pos];
                    if src_min[back_idx] > v_min {
                        tmin = tmin.wrapping_sub(1);
                    } else {
                        break;
                    }
                }
                dq_min[tmin & mask] = i;
                tmin = tmin.wrapping_add(1);
            }

            if i < warm {
                continue;
            }

            let highest = if hmax != tmax {
                src_max[dq_max[hmax & mask]]
            } else {
                f64::NAN
            };
            let lowest = if hmin != tmin {
                src_min[dq_min[hmin & mask]]
            } else {
                f64::NAN
            };

            let ai = atr[i];

            let ls0 = ai.mul_add(-mult, highest);
            let ss0 = ai.mul_add(mult, lowest);

            let lsp = if i == warm || long_raw_prev.is_nan() {
                ls0
            } else {
                long_raw_prev
            };
            let ssp = if i == warm || short_raw_prev.is_nan() {
                ss0
            } else {
                short_raw_prev
            };

            let ls = if i > warm && close[i - 1] > lsp {
                ls0.max(lsp)
            } else {
                ls0
            };
            let ss = if i > warm && close[i - 1] < ssp {
                ss0.min(ssp)
            } else {
                ss0
            };

            let d = if close[i] > ssp {
                1
            } else if close[i] < lsp {
                -1
            } else {
                prev_dir
            };

            long_raw_prev = ls;
            short_raw_prev = ss;
            prev_dir = d;

            long_stop[i] = if d == 1 { ls } else { f64::NAN };
            short_stop[i] = if d == -1 { ss } else { f64::NAN };
        }
    }
    #[cfg(not(all(feature = "nightly-avx", target_arch = "x86_64")))]
    {
        let mut long_raw_prev = f64::NAN;
        let mut short_raw_prev = f64::NAN;
        let mut prev_dir: i8 = 1;

        let cap = period.next_power_of_two();
        let mask = cap - 1;
        let mut dq_max = vec![0usize; cap];
        let mut dq_min = vec![0usize; cap];
        let mut hmax = 0usize;
        let mut tmax = 0usize;
        let mut hmin = 0usize;
        let mut tmin = 0usize;

        let (src_max, src_min) = if use_close {
            (close, close)
        } else {
            (high, low)
        };

        for i in 0..len {
            while hmax != tmax {
                let idx = dq_max[hmax & mask];
                if idx + period <= i {
                    hmax = hmax.wrapping_add(1);
                } else {
                    break;
                }
            }
            while hmin != tmin {
                let idx = dq_min[hmin & mask];
                if idx + period <= i {
                    hmin = hmin.wrapping_add(1);
                } else {
                    break;
                }
            }

            let v_max = src_max[i];
            if !v_max.is_nan() {
                while hmax != tmax {
                    let back_pos = (tmax.wrapping_sub(1)) & mask;
                    let back_idx = dq_max[back_pos];
                    if src_max[back_idx] < v_max {
                        tmax = tmax.wrapping_sub(1);
                    } else {
                        break;
                    }
                }
                dq_max[tmax & mask] = i;
                tmax = tmax.wrapping_add(1);
            }

            let v_min = src_min[i];
            if !v_min.is_nan() {
                while hmin != tmin {
                    let back_pos = (tmin.wrapping_sub(1)) & mask;
                    let back_idx = dq_min[back_pos];
                    if src_min[back_idx] > v_min {
                        tmin = tmin.wrapping_sub(1);
                    } else {
                        break;
                    }
                }
                dq_min[tmin & mask] = i;
                tmin = tmin.wrapping_add(1);
            }

            if i < warm {
                continue;
            }

            let highest = if hmax != tmax {
                src_max[dq_max[hmax & mask]]
            } else {
                f64::NAN
            };
            let lowest = if hmin != tmin {
                src_min[dq_min[hmin & mask]]
            } else {
                f64::NAN
            };

            let ai = atr[i];
            let ls0 = ai.mul_add(-mult, highest);
            let ss0 = ai.mul_add(mult, lowest);

            let lsp = if i == warm || long_raw_prev.is_nan() {
                ls0
            } else {
                long_raw_prev
            };
            let ssp = if i == warm || short_raw_prev.is_nan() {
                ss0
            } else {
                short_raw_prev
            };

            let ls = if i > warm && close[i - 1] > lsp {
                ls0.max(lsp)
            } else {
                ls0
            };
            let ss = if i > warm && close[i - 1] < ssp {
                ss0.min(ssp)
            } else {
                ss0
            };

            let d = if close[i] > ssp {
                1
            } else if close[i] < lsp {
                -1
            } else {
                prev_dir
            };

            long_raw_prev = ls;
            short_raw_prev = ss;
            prev_dir = d;

            long_stop[i] = if d == 1 { ls } else { f64::NAN };
            short_stop[i] = if d == -1 { ss } else { f64::NAN };
        }
    }

    Ok(ChandelierExitOutput {
        long_stop,
        short_stop,
    })
}

#[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
#[inline]
pub fn chandelier_exit_into(
    input: &ChandelierExitInput,
    long_out: &mut [f64],
    short_out: &mut [f64],
) -> Result<(), ChandelierExitError> {
    chandelier_exit_into_slices(long_out, short_out, input, Kernel::Auto)
}

#[inline]
pub fn chandelier_exit_into_slices(
    long_dst: &mut [f64],
    short_dst: &mut [f64],
    input: &ChandelierExitInput,
    kern: Kernel,
) -> Result<(), ChandelierExitError> {
    let (h, l, c, period, mult, use_close, first, chosen) = ce_prepare(input, kern)?;
    let len = c.len();
    if long_dst.len() != len || short_dst.len() != len {
        return Err(ChandelierExitError::OutputLengthMismatch {
            expected: len,
            got: long_dst.len().max(short_dst.len()),
        });
    }
    let atr_in = AtrInput::from_slices(
        h,
        l,
        c,
        AtrParams {
            length: Some(period),
        },
    );
    let atr = atr_with_kernel(&atr_in, chosen)
        .map_err(|e| ChandelierExitError::AtrError(e.to_string()))?
        .values;

    let warm = first + period - 1;
    for v in &mut long_dst[..warm.min(len)] {
        *v = f64::NAN;
    }
    for v in &mut short_dst[..warm.min(len)] {
        *v = f64::NAN;
    }

    let mut long_raw_prev = f64::NAN;
    let mut short_raw_prev = f64::NAN;
    let mut prev_dir: i8 = 1;

    let cap = period.next_power_of_two();
    let mask = cap - 1;
    let mut dq_max = vec![0usize; cap];
    let mut dq_min = vec![0usize; cap];
    let mut hmax = 0usize;
    let mut tmax = 0usize;
    let mut hmin = 0usize;
    let mut tmin = 0usize;
    let (src_max, src_min) = if use_close { (c, c) } else { (h, l) };

    #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
    if matches!(chosen, Kernel::Avx2 | Kernel::Avx512) {
        ce_avx2_fill(
            long_dst, short_dst, h, l, c, &atr, period, mult, use_close, first,
        );
        return Ok(());
    }

    for i in 0..len {
        while hmax != tmax {
            let idx = dq_max[hmax & mask];
            if idx + period <= i {
                hmax = hmax.wrapping_add(1);
            } else {
                break;
            }
        }
        while hmin != tmin {
            let idx = dq_min[hmin & mask];
            if idx + period <= i {
                hmin = hmin.wrapping_add(1);
            } else {
                break;
            }
        }

        let vmax = src_max[i];
        if !vmax.is_nan() {
            while hmax != tmax {
                let back_pos = (tmax.wrapping_sub(1)) & mask;
                let back_idx = dq_max[back_pos];
                if src_max[back_idx] < vmax {
                    tmax = tmax.wrapping_sub(1);
                } else {
                    break;
                }
            }
            dq_max[tmax & mask] = i;
            tmax = tmax.wrapping_add(1);
        }
        let vmin = src_min[i];
        if !vmin.is_nan() {
            while hmin != tmin {
                let back_pos = (tmin.wrapping_sub(1)) & mask;
                let back_idx = dq_min[back_pos];
                if src_min[back_idx] > vmin {
                    tmin = tmin.wrapping_sub(1);
                } else {
                    break;
                }
            }
            dq_min[tmin & mask] = i;
            tmin = tmin.wrapping_add(1);
        }

        if i < warm {
            continue;
        }

        let highest = if hmax != tmax {
            src_max[dq_max[hmax & mask]]
        } else {
            f64::NAN
        };
        let lowest = if hmin != tmin {
            src_min[dq_min[hmin & mask]]
        } else {
            f64::NAN
        };

        let ai = atr[i];
        let ls0 = ai.mul_add(-mult, highest);
        let ss0 = ai.mul_add(mult, lowest);

        let lsp = if i == warm || long_raw_prev.is_nan() {
            ls0
        } else {
            long_raw_prev
        };
        let ssp = if i == warm || short_raw_prev.is_nan() {
            ss0
        } else {
            short_raw_prev
        };

        let ls = if i > warm && c[i - 1] > lsp {
            ls0.max(lsp)
        } else {
            ls0
        };
        let ss = if i > warm && c[i - 1] < ssp {
            ss0.min(ssp)
        } else {
            ss0
        };

        let d = if c[i] > ssp {
            1
        } else if c[i] < lsp {
            -1
        } else {
            prev_dir
        };
        long_raw_prev = ls;
        short_raw_prev = ss;
        prev_dir = d;
        long_dst[i] = if d == 1 { ls } else { f64::NAN };
        short_dst[i] = if d == -1 { ss } else { f64::NAN };
    }
    Ok(())
}

#[inline]
pub fn chandelier_exit_into_flat(
    flat_out: &mut [f64],
    input: &ChandelierExitInput,
    kern: Kernel,
) -> Result<(), ChandelierExitError> {
    let len = input.as_ref().len();
    let expected = len
        .checked_mul(2)
        .ok_or(ChandelierExitError::InvalidRange {
            start: "rows".into(),
            end: "cols".into(),
            step: "mul overflow".into(),
        })?;
    if flat_out.len() != expected {
        return Err(ChandelierExitError::OutputLengthMismatch {
            expected,
            got: flat_out.len(),
        });
    }
    let (long_dst, short_dst) = flat_out.split_at_mut(len);
    chandelier_exit_into_slices(long_dst, short_dst, input, kern)
}

#[derive(Clone, Debug)]
pub struct CeBatchRange {
    pub period: (usize, usize, usize),
    pub mult: (f64, f64, f64),
    pub use_close: (bool, bool, bool),
}

impl Default for CeBatchRange {
    fn default() -> Self {
        Self {
            period: (22, 271, 1),
            mult: (3.0, 3.0, 0.0),
            use_close: (true, true, false),
        }
    }
}

#[derive(Clone, Debug, Default)]
pub struct CeBatchBuilder {
    range: CeBatchRange,
    kernel: Kernel,
}

impl CeBatchBuilder {
    pub fn new() -> Self {
        Self::default()
    }
    pub fn kernel(mut self, k: Kernel) -> Self {
        self.kernel = k;
        self
    }
    pub fn period_range(mut self, a: usize, b: usize, s: usize) -> Self {
        self.range.period = (a, b, s);
        self
    }
    pub fn period_static(mut self, p: usize) -> Self {
        self.range.period = (p, p, 0);
        self
    }
    pub fn mult_range(mut self, a: f64, b: f64, s: f64) -> Self {
        self.range.mult = (a, b, s);
        self
    }
    pub fn mult_static(mut self, m: f64) -> Self {
        self.range.mult = (m, m, 0.0);
        self
    }
    pub fn use_close(mut self, v: bool) -> Self {
        self.range.use_close = (v, v, false);
        self
    }

    pub fn build(self) -> CeBatchRange {
        self.range
    }

    pub fn apply_slices(
        self,
        h: &[f64],
        l: &[f64],
        c: &[f64],
    ) -> Result<CeBatchOutput, ChandelierExitError> {
        ce_batch_with_kernel(h, l, c, &self.range, self.kernel)
    }
    pub fn apply_candles(self, candles: &Candles) -> Result<CeBatchOutput, ChandelierExitError> {
        self.apply_slices(&candles.high, &candles.low, &candles.close)
    }

    pub fn with_default_candles(
        c: &Candles,
        k: Kernel,
    ) -> Result<CeBatchOutput, ChandelierExitError> {
        CeBatchBuilder::new().kernel(k).apply_candles(c)
    }
}

#[derive(Clone, Debug)]
pub struct CeBatchOutput {
    pub values: Vec<f64>,
    pub combos: Vec<ChandelierExitParams>,
    pub rows: usize,
    pub cols: usize,
}

impl CeBatchOutput {
    #[inline]
    pub fn row_pair_for(&self, p: &ChandelierExitParams) -> Option<(usize, usize)> {
        self.combos
            .iter()
            .position(|q| {
                q.period.unwrap_or(22) == p.period.unwrap_or(22)
                    && (q.mult.unwrap_or(3.0) - p.mult.unwrap_or(3.0)).abs() < 1e-12
                    && q.use_close.unwrap_or(true) == p.use_close.unwrap_or(true)
            })
            .map(|r| (2 * r, 2 * r + 1))
    }

    #[inline]
    pub fn values_for(&self, p: &ChandelierExitParams) -> Option<(&[f64], &[f64])> {
        self.row_pair_for(p).map(|(r_long, r_short)| {
            let a = &self.values[r_long * self.cols..(r_long + 1) * self.cols];
            let b = &self.values[r_short * self.cols..(r_short + 1) * self.cols];
            (a, b)
        })
    }
}

#[inline(always)]
fn expand_ce_checked(r: &CeBatchRange) -> Result<Vec<ChandelierExitParams>, ChandelierExitError> {
    fn axis_usize(t: (usize, usize, usize)) -> Result<Vec<usize>, ChandelierExitError> {
        if t.2 == 0 || t.0 == t.1 {
            return Ok(vec![t.0]);
        }
        let (start, end, step) = (t.0, t.1, t.2);
        let mut v = Vec::new();
        if start < end {
            let mut x = start;
            while x <= end {
                v.push(x);
                match x.checked_add(step) {
                    Some(nx) => x = nx,
                    None => {
                        return Err(ChandelierExitError::InvalidRange {
                            start: start.to_string(),
                            end: end.to_string(),
                            step: step.to_string(),
                        })
                    }
                }
            }
        } else {
            let mut x = start;
            while x >= end {
                v.push(x);
                if x < step {
                    break;
                }
                x -= step;
                if x == usize::MAX {
                    return Err(ChandelierExitError::InvalidRange {
                        start: start.to_string(),
                        end: end.to_string(),
                        step: step.to_string(),
                    });
                }
            }
        }
        if v.is_empty() {
            return Err(ChandelierExitError::InvalidRange {
                start: start.to_string(),
                end: end.to_string(),
                step: step.to_string(),
            });
        }
        Ok(v)
    }
    fn axis_f64(t: (f64, f64, f64)) -> Result<Vec<f64>, ChandelierExitError> {
        let (start, end, step) = (t.0, t.1, t.2);
        if step.abs() < 1e-12 || (start - end).abs() < 1e-12 {
            return Ok(vec![start]);
        }
        let mut v = Vec::new();

        let s = if step > 0.0 {
            if start <= end {
                step
            } else {
                -step
            }
        } else {
            step
        };
        let mut x = start;

        let mut iters = 0usize;
        while iters < 1_000_000 {
            if (s > 0.0 && x > end + 1e-12) || (s < 0.0 && x < end - 1e-12) {
                break;
            }
            v.push(x);
            x += s;
            iters += 1;
        }
        if v.is_empty() {
            return Err(ChandelierExitError::InvalidRange {
                start: start.to_string(),
                end: end.to_string(),
                step: step.to_string(),
            });
        }
        Ok(v)
    }
    let periods = axis_usize(r.period)?;
    let mults = axis_f64(r.mult)?;
    let uses = vec![r.use_close.0];
    let cap = periods
        .len()
        .checked_mul(mults.len())
        .and_then(|x| x.checked_mul(uses.len()))
        .ok_or(ChandelierExitError::InvalidRange {
            start: "periods".into(),
            end: "mults".into(),
            step: "cap overflow".into(),
        })?;
    let mut out = Vec::with_capacity(cap);
    for &p in &periods {
        for &m in &mults {
            for &u in &uses {
                out.push(ChandelierExitParams {
                    period: Some(p),
                    mult: Some(m),
                    use_close: Some(u),
                });
            }
        }
    }
    if out.is_empty() {
        return Err(ChandelierExitError::InvalidRange {
            start: r.period.0.to_string(),
            end: r.period.1.to_string(),
            step: r.period.2.to_string(),
        });
    }
    Ok(out)
}

#[inline]
pub fn ce_batch_slice(
    h: &[f64],
    l: &[f64],
    c: &[f64],
    sweep: &CeBatchRange,
    kern: Kernel,
) -> Result<CeBatchOutput, ChandelierExitError> {
    ce_batch_inner(h, l, c, sweep, kern, false)
}

#[inline]
pub fn ce_batch_par_slice(
    h: &[f64],
    l: &[f64],
    c: &[f64],
    sweep: &CeBatchRange,
    kern: Kernel,
) -> Result<CeBatchOutput, ChandelierExitError> {
    ce_batch_inner(h, l, c, sweep, kern, true)
}

pub fn ce_batch_with_kernel(
    h: &[f64],
    l: &[f64],
    c: &[f64],
    sweep: &CeBatchRange,
    k: Kernel,
) -> Result<CeBatchOutput, ChandelierExitError> {
    let kernel = match k {
        Kernel::Auto => detect_best_batch_kernel(),
        other if other.is_batch() => other,
        _ => return Err(ChandelierExitError::InvalidKernelForBatch(k)),
    };

    let simd = match kernel {
        Kernel::Avx512Batch => Kernel::Avx512,
        Kernel::Avx2Batch => Kernel::Avx2,
        Kernel::ScalarBatch => Kernel::Scalar,
        _ => unreachable!(),
    };
    ce_batch_par_slice(h, l, c, sweep, simd)
}

#[inline(always)]
fn ce_batch_inner(
    h: &[f64],
    l: &[f64],
    c: &[f64],
    sweep: &CeBatchRange,
    kern: Kernel,
    _parallel: bool,
) -> Result<CeBatchOutput, ChandelierExitError> {
    if h.len() != l.len() || l.len() != c.len() {
        return Err(ChandelierExitError::InconsistentDataLengths {
            high_len: h.len(),
            low_len: l.len(),
            close_len: c.len(),
        });
    }
    let combos = expand_ce_checked(sweep)?;
    let cols = c.len();
    if cols == 0 {
        return Err(ChandelierExitError::EmptyInputData);
    }

    let warms: Vec<usize> = {
        let mut w = Vec::with_capacity(2 * combos.len());
        for prm in &combos {
            let first = ce_first_valid(prm.use_close.unwrap(), h, l, c)?;
            w.push(first + prm.period.unwrap() - 1);
            w.push(first + prm.period.unwrap() - 1);
        }
        w
    };

    let rows = combos
        .len()
        .checked_mul(2)
        .ok_or(ChandelierExitError::InvalidRange {
            start: "combos".into(),
            end: "2".into(),
            step: "mul overflow".into(),
        })?;

    let _ = rows
        .checked_mul(cols)
        .ok_or(ChandelierExitError::InvalidRange {
            start: rows.to_string(),
            end: cols.to_string(),
            step: "mul overflow".into(),
        })?;
    let mut buf_mu = make_uninit_matrix(rows, cols);
    init_matrix_prefixes(&mut buf_mu, cols, &warms);

    let mut guard = core::mem::ManuallyDrop::new(buf_mu);
    let out: &mut [f64] =
        unsafe { core::slice::from_raw_parts_mut(guard.as_mut_ptr() as *mut f64, guard.len()) };

    ce_batch_inner_into(h, l, c, &combos, kern, out)?;

    let values = unsafe {
        Vec::from_raw_parts(
            guard.as_mut_ptr() as *mut f64,
            guard.len(),
            guard.capacity(),
        )
    };

    Ok(CeBatchOutput {
        values,
        combos,
        rows,
        cols,
    })
}

#[inline(always)]
fn ce_batch_inner_into(
    h: &[f64],
    l: &[f64],
    c: &[f64],
    combos: &[ChandelierExitParams],
    k: Kernel,
    out: &mut [f64],
) -> Result<(), ChandelierExitError> {
    let len = c.len();
    let cols = len;
    let chosen = match k {
        Kernel::Auto => detect_best_batch_kernel(),
        x => x,
    };
    let mut row = 0usize;

    for prm in combos {
        let period = prm.period.unwrap();
        let mult = prm.mult.unwrap();
        let use_close = prm.use_close.unwrap();

        let first = ce_first_valid(use_close, h, l, c).unwrap_or(0);
        if len - first < period {
            return Err(ChandelierExitError::NotEnoughValidData {
                needed: period,
                valid: len - first,
            });
        }

        let atr_in = AtrInput::from_slices(
            h,
            l,
            c,
            AtrParams {
                length: Some(period),
            },
        );
        let atr = atr_with_kernel(
            &atr_in,
            map_kernel_for_atr(match chosen {
                Kernel::Avx512Batch => Kernel::Avx512,
                Kernel::Avx2Batch => Kernel::Avx2,
                Kernel::ScalarBatch => Kernel::Scalar,
                other => other,
            }),
        )
        .map_err(|e| ChandelierExitError::AtrError(e.to_string()))?
        .values;

        let warm = first + period - 1;

        let (long_dst, short_dst) = {
            let start = row * cols;
            let mid = (row + 1) * cols;
            let end = (row + 2) * cols;
            let (a, b) = out[start..end].split_at_mut(cols);
            (a, b)
        };

        for v in &mut long_dst[..warm] {
            *v = f64::NAN;
        }
        for v in &mut short_dst[..warm] {
            *v = f64::NAN;
        }

        #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
        if matches!(
            chosen,
            Kernel::Avx2 | Kernel::Avx512 | Kernel::Avx2Batch | Kernel::Avx512Batch
        ) {
            ce_avx2_fill(
                long_dst, short_dst, h, l, c, &atr, period, mult, use_close, first,
            );
        } else {
            let mut long_raw_prev = f64::NAN;
            let mut short_raw_prev = f64::NAN;
            let mut prev_dir: i8 = 1;

            let cap = period.next_power_of_two();
            let mask = cap - 1;
            let mut dq_max = vec![0usize; cap];
            let mut dq_min = vec![0usize; cap];
            let mut hmax = 0usize;
            let mut tmax = 0usize;
            let mut hmin = 0usize;
            let mut tmin = 0usize;
            let (src_max, src_min) = if use_close { (c, c) } else { (h, l) };

            for i in 0..len {
                while hmax != tmax {
                    let idx = dq_max[hmax & mask];
                    if idx + period <= i {
                        hmax = hmax.wrapping_add(1);
                    } else {
                        break;
                    }
                }
                while hmin != tmin {
                    let idx = dq_min[hmin & mask];
                    if idx + period <= i {
                        hmin = hmin.wrapping_add(1);
                    } else {
                        break;
                    }
                }
                let vmax = src_max[i];
                if !vmax.is_nan() {
                    while hmax != tmax {
                        let back_pos = (tmax.wrapping_sub(1)) & mask;
                        let back_idx = dq_max[back_pos];
                        if src_max[back_idx] < vmax {
                            tmax = tmax.wrapping_sub(1);
                        } else {
                            break;
                        }
                    }
                    dq_max[tmax & mask] = i;
                    tmax = tmax.wrapping_add(1);
                }
                let vmin = src_min[i];
                if !vmin.is_nan() {
                    while hmin != tmin {
                        let back_pos = (tmin.wrapping_sub(1)) & mask;
                        let back_idx = dq_min[back_pos];
                        if src_min[back_idx] > vmin {
                            tmin = tmin.wrapping_sub(1);
                        } else {
                            break;
                        }
                    }
                    dq_min[tmin & mask] = i;
                    tmin = tmin.wrapping_add(1);
                }
                if i < warm {
                    continue;
                }
                let highest = if hmax != tmax {
                    src_max[dq_max[hmax & mask]]
                } else {
                    f64::NAN
                };
                let lowest = if hmin != tmin {
                    src_min[dq_min[hmin & mask]]
                } else {
                    f64::NAN
                };
                let ai = atr[i];
                let ls0 = ai.mul_add(-mult, highest);
                let ss0 = ai.mul_add(mult, lowest);
                let lsp = if i == warm || long_raw_prev.is_nan() {
                    ls0
                } else {
                    long_raw_prev
                };
                let ssp = if i == warm || short_raw_prev.is_nan() {
                    ss0
                } else {
                    short_raw_prev
                };
                let ls = if i > warm && c[i - 1] > lsp {
                    ls0.max(lsp)
                } else {
                    ls0
                };
                let ss = if i > warm && c[i - 1] < ssp {
                    ss0.min(ssp)
                } else {
                    ss0
                };
                let d = if c[i] > ssp {
                    1
                } else if c[i] < lsp {
                    -1
                } else {
                    prev_dir
                };
                long_raw_prev = ls;
                short_raw_prev = ss;
                prev_dir = d;
                long_dst[i] = if d == 1 { ls } else { f64::NAN };
                short_dst[i] = if d == -1 { ss } else { f64::NAN };
            }
        }
        #[cfg(not(all(feature = "nightly-avx", target_arch = "x86_64")))]
        {
            let mut long_raw_prev = f64::NAN;
            let mut short_raw_prev = f64::NAN;
            let mut prev_dir: i8 = 1;

            let cap = period.next_power_of_two();
            let mask = cap - 1;
            let mut dq_max = vec![0usize; cap];
            let mut dq_min = vec![0usize; cap];
            let mut hmax = 0usize;
            let mut tmax = 0usize;
            let mut hmin = 0usize;
            let mut tmin = 0usize;
            let (src_max, src_min) = if use_close { (c, c) } else { (h, l) };

            for i in 0..len {
                while hmax != tmax {
                    let idx = dq_max[hmax & mask];
                    if idx + period <= i {
                        hmax = hmax.wrapping_add(1);
                    } else {
                        break;
                    }
                }
                while hmin != tmin {
                    let idx = dq_min[hmin & mask];
                    if idx + period <= i {
                        hmin = hmin.wrapping_add(1);
                    } else {
                        break;
                    }
                }
                let vmax = src_max[i];
                if !vmax.is_nan() {
                    while hmax != tmax {
                        let back_pos = (tmax.wrapping_sub(1)) & mask;
                        let back_idx = dq_max[back_pos];
                        if src_max[back_idx] < vmax {
                            tmax = tmax.wrapping_sub(1);
                        } else {
                            break;
                        }
                    }
                    dq_max[tmax & mask] = i;
                    tmax = tmax.wrapping_add(1);
                }
                let vmin = src_min[i];
                if !vmin.is_nan() {
                    while hmin != tmin {
                        let back_pos = (tmin.wrapping_sub(1)) & mask;
                        let back_idx = dq_min[back_pos];
                        if src_min[back_idx] > vmin {
                            tmin = tmin.wrapping_sub(1);
                        } else {
                            break;
                        }
                    }
                    dq_min[tmin & mask] = i;
                    tmin = tmin.wrapping_add(1);
                }
                if i < warm {
                    continue;
                }
                let highest = if hmax != tmax {
                    src_max[dq_max[hmax & mask]]
                } else {
                    f64::NAN
                };
                let lowest = if hmin != tmin {
                    src_min[dq_min[hmin & mask]]
                } else {
                    f64::NAN
                };
                let ai = atr[i];
                let ls0 = ai.mul_add(-mult, highest);
                let ss0 = ai.mul_add(mult, lowest);
                let lsp = if i == warm || long_raw_prev.is_nan() {
                    ls0
                } else {
                    long_raw_prev
                };
                let ssp = if i == warm || short_raw_prev.is_nan() {
                    ss0
                } else {
                    short_raw_prev
                };
                let ls = if i > warm && c[i - 1] > lsp {
                    ls0.max(lsp)
                } else {
                    ls0
                };
                let ss = if i > warm && c[i - 1] < ssp {
                    ss0.min(ssp)
                } else {
                    ss0
                };
                let d = if c[i] > ssp {
                    1
                } else if c[i] < lsp {
                    -1
                } else {
                    prev_dir
                };
                long_raw_prev = ls;
                short_raw_prev = ss;
                prev_dir = d;
                long_dst[i] = if d == 1 { ls } else { f64::NAN };
                short_dst[i] = if d == -1 { ss } else { f64::NAN };
            }
        }

        row += 2;
    }
    Ok(())
}

#[cfg(feature = "python")]
#[pyfunction(name = "chandelier_exit")]
#[pyo3(signature = (high, low, close, period=None, mult=None, use_close=None, kernel=None))]
pub fn chandelier_exit_py<'py>(
    py: Python<'py>,
    high: PyReadonlyArray1<'py, f64>,
    low: PyReadonlyArray1<'py, f64>,
    close: PyReadonlyArray1<'py, f64>,
    period: Option<usize>,
    mult: Option<f64>,
    use_close: Option<bool>,
    kernel: Option<&str>,
) -> PyResult<(Bound<'py, PyArray1<f64>>, Bound<'py, PyArray1<f64>>)> {
    let h = high.as_slice()?;
    let l = low.as_slice()?;
    let c = close.as_slice()?;
    let params = ChandelierExitParams {
        period,
        mult,
        use_close,
    };
    let input = ChandelierExitInput::from_slices(h, l, c, params);
    let kern = validate_kernel(kernel, false)?;
    let (long_vec, short_vec) = py
        .allow_threads(|| {
            chandelier_exit_with_kernel(&input, kern).map(|o| (o.long_stop, o.short_stop))
        })
        .map_err(|e| PyValueError::new_err(e.to_string()))?;
    Ok((long_vec.into_pyarray(py), short_vec.into_pyarray(py)))
}

#[cfg(feature = "python")]
#[pyfunction(name = "chandelier_exit_batch")]
#[pyo3(signature = (high, low, close, period_range, mult_range, use_close=true, kernel=None))]
pub fn chandelier_exit_batch_py<'py>(
    py: Python<'py>,
    high: PyReadonlyArray1<'py, f64>,
    low: PyReadonlyArray1<'py, f64>,
    close: PyReadonlyArray1<'py, f64>,
    period_range: (usize, usize, usize),
    mult_range: (f64, f64, f64),
    use_close: bool,
    kernel: Option<&str>,
) -> PyResult<Bound<'py, PyDict>> {
    use numpy::{IntoPyArray, PyArray1, PyArrayMethods};
    let h = high.as_slice()?;
    let l = low.as_slice()?;
    let c = close.as_slice()?;

    let sweep = CeBatchRange {
        period: period_range,
        mult: mult_range,
        use_close: (use_close, use_close, false),
    };

    let combos = expand_ce_checked(&sweep).map_err(|e| PyValueError::new_err(e.to_string()))?;
    let rows = combos
        .len()
        .checked_mul(2)
        .ok_or_else(|| PyValueError::new_err("rows*2 overflow in chandelier_exit_batch_py"))?;
    let cols = c.len();
    let total = rows
        .checked_mul(cols)
        .ok_or_else(|| PyValueError::new_err("rows*cols overflow in chandelier_exit_batch_py"))?;

    let out_arr = unsafe { PyArray1::<f64>::new(py, [total], false) };
    let slice_out = unsafe { out_arr.as_slice_mut()? };

    let kern = validate_kernel(kernel, true)?;

    py.allow_threads(|| {
        let simd = match kern {
            Kernel::Auto => detect_best_batch_kernel(),
            Kernel::Avx512Batch => Kernel::Avx512,
            Kernel::Avx2Batch => Kernel::Avx2,
            Kernel::ScalarBatch => Kernel::Scalar,
            other => other,
        };
        ce_batch_inner_into(h, l, c, &combos, simd, slice_out)
    })
    .map_err(|e| PyValueError::new_err(e.to_string()))?;

    let d = PyDict::new(py);
    d.set_item("values", out_arr.reshape((rows, cols))?)?;
    d.set_item(
        "periods",
        combos
            .iter()
            .map(|p| p.period.unwrap() as u64)
            .collect::<Vec<_>>()
            .into_pyarray(py),
    )?;
    d.set_item(
        "mults",
        combos
            .iter()
            .map(|p| p.mult.unwrap())
            .collect::<Vec<_>>()
            .into_pyarray(py),
    )?;
    d.set_item(
        "use_close",
        combos
            .iter()
            .map(|p| p.use_close.unwrap())
            .collect::<Vec<_>>()
            .into_pyarray(py),
    )?;
    Ok(d)
}

#[cfg(feature = "python")]
#[pyclass]
pub struct ChandelierExitStreamPy {
    high_buffer: Vec<f64>,
    low_buffer: Vec<f64>,
    close_buffer: Vec<f64>,
    period: usize,
    mult: f64,
    use_close: bool,
    kernel: Kernel,

    prev_close: Option<f64>,
    atr_prev: Option<f64>,
    long_stop_prev: Option<f64>,
    short_stop_prev: Option<f64>,
    dir_prev: i8,
    warm_tr_sum: f64,
    count: usize,
}

#[cfg(feature = "python")]
#[pymethods]
impl ChandelierExitStreamPy {
    #[new]
    #[pyo3(signature = (period=None, mult=None, use_close=None, kernel=None))]
    fn new(
        period: Option<usize>,
        mult: Option<f64>,
        use_close: Option<bool>,
        kernel: Option<String>,
    ) -> PyResult<Self> {
        let kernel = validate_kernel(kernel.as_deref(), false)?;
        Ok(Self {
            high_buffer: Vec::new(),
            low_buffer: Vec::new(),
            close_buffer: Vec::new(),
            period: period.unwrap_or(22),
            mult: mult.unwrap_or(3.0),
            use_close: use_close.unwrap_or(true),
            kernel,
            prev_close: None,
            atr_prev: None,
            long_stop_prev: None,
            short_stop_prev: None,
            dir_prev: 1,
            warm_tr_sum: 0.0,
            count: 0,
        })
    }

    fn update(&mut self, high: f64, low: f64, close: f64) -> PyResult<Option<(f64, f64)>> {
        self.high_buffer.push(high);
        self.low_buffer.push(low);
        self.close_buffer.push(close);

        let tr = if let Some(pc) = self.prev_close {
            let hl = (high - low).abs();
            let hc = (high - pc).abs();
            let lc = (low - pc).abs();
            hl.max(hc.max(lc))
        } else {
            (high - low).abs()
        };

        let atr = if self.atr_prev.is_none() {
            self.warm_tr_sum += tr;
            self.count += 1;
            if self.count < self.period {
                self.prev_close = Some(close);
                return Ok(None);
            }
            let seed = self.warm_tr_sum / self.period as f64;
            self.atr_prev = Some(seed);
            seed
        } else {
            let prev = self.atr_prev.unwrap();
            let n = self.period as f64;
            let next = (prev * (n - 1.0) + tr) / n;
            self.atr_prev = Some(next);
            next
        };

        if self.high_buffer.len() > self.period {
            self.high_buffer.remove(0);
            self.low_buffer.remove(0);
            self.close_buffer.remove(0);
        }

        let (highest, lowest) = if self.use_close {
            (
                window_max(&self.close_buffer),
                window_min(&self.close_buffer),
            )
        } else {
            (window_max(&self.high_buffer), window_min(&self.low_buffer))
        };

        let long_stop_val = highest - self.mult * atr;
        let short_stop_val = lowest + self.mult * atr;

        let lsp = self.long_stop_prev.unwrap_or(long_stop_val);
        let ssp = self.short_stop_prev.unwrap_or(short_stop_val);

        let ls = if let Some(pc) = self.prev_close {
            if pc > lsp {
                long_stop_val.max(lsp)
            } else {
                long_stop_val
            }
        } else {
            long_stop_val
        };
        let ss = if let Some(pc) = self.prev_close {
            if pc < ssp {
                short_stop_val.min(ssp)
            } else {
                short_stop_val
            }
        } else {
            short_stop_val
        };

        let d = if close > ssp {
            1
        } else if close < lsp {
            -1
        } else {
            self.dir_prev
        };

        self.long_stop_prev = Some(ls);
        self.short_stop_prev = Some(ss);
        self.dir_prev = d;
        self.prev_close = Some(close);

        let out_long = if d == 1 { ls } else { f64::NAN };
        let out_short = if d == -1 { ss } else { f64::NAN };
        Ok(Some((out_long, out_short)))
    }

    fn reset(&mut self) {
        self.high_buffer.clear();
        self.low_buffer.clear();
        self.close_buffer.clear();
        self.prev_close = None;
        self.atr_prev = None;
        self.long_stop_prev = None;
        self.short_stop_prev = None;
        self.dir_prev = 1;
        self.warm_tr_sum = 0.0;
        self.count = 0;
    }
}

#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
#[derive(Serialize, Deserialize)]
pub struct CeResult {
    pub values: Vec<f64>,
    pub rows: usize,
    pub cols: usize,
}

#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
#[derive(Serialize, Deserialize)]
pub struct CeBatchJsOutput {
    pub values: Vec<f64>,
    pub combos: Vec<ChandelierExitParams>,
    pub rows: usize,
    pub cols: usize,
}

#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
#[wasm_bindgen]
pub fn ce_js(
    high: &[f64],
    low: &[f64],
    close: &[f64],
    period: usize,
    mult: f64,
    use_close: bool,
) -> Result<JsValue, JsValue> {
    let p = ChandelierExitParams {
        period: Some(period),
        mult: Some(mult),
        use_close: Some(use_close),
    };
    let i = ChandelierExitInput::from_slices(high, low, close, p);
    let out = chandelier_exit_with_kernel(&i, Kernel::Auto)
        .map_err(|e| JsValue::from_str(&e.to_string()))?;
    let rows = 2usize;
    let cols = close.len();
    let total = rows
        .checked_mul(cols)
        .ok_or_else(|| JsValue::from_str("rows*cols overflow in ce_js"))?;
    let mut values = vec![f64::NAN; total];
    values[..cols].copy_from_slice(&out.long_stop);
    values[cols..].copy_from_slice(&out.short_stop);
    serde_wasm_bindgen::to_value(&CeResult { values, rows, cols })
        .map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))
}

#[cfg(all(feature = "python", feature = "cuda"))]
#[pyfunction(name = "chandelier_exit_cuda_batch_dev")]
#[pyo3(signature = (high_f32, low_f32, close_f32, period_range, mult_range=(3.0,3.0,0.0), use_close=true, device_id=0))]
pub fn chandelier_exit_cuda_batch_dev_py<'py>(
    py: Python<'py>,
    high_f32: PyReadonlyArray1<'py, f32>,
    low_f32: PyReadonlyArray1<'py, f32>,
    close_f32: PyReadonlyArray1<'py, f32>,
    period_range: (usize, usize, usize),
    mult_range: (f64, f64, f64),
    use_close: bool,
    device_id: usize,
) -> PyResult<(CeDeviceArrayF32Py, Bound<'py, PyDict>)> {
    use crate::cuda::cuda_available;
    if !cuda_available() {
        return Err(PyValueError::new_err("CUDA not available"));
    }
    let h = high_f32.as_slice()?;
    let l = low_f32.as_slice()?;
    let c = close_f32.as_slice()?;

    let sweep = CeBatchRange {
        period: period_range,
        mult: mult_range,
        use_close: (use_close, use_close, false),
    };
    let (inner, combos, ctx, dev_id) = py.allow_threads(|| {
        let cuda =
            CudaChandelierExit::new(device_id).map_err(|e| PyValueError::new_err(e.to_string()))?;
        let ctx = cuda.context_arc();
        let dev_id = cuda.device_id();
        cuda.chandelier_exit_batch_dev(h, l, c, &sweep)
            .map(|(a, b)| (a, b, ctx, dev_id))
            .map_err(|e| PyValueError::new_err(e.to_string()))
    })?;

    let d = PyDict::new(py);
    d.set_item(
        "periods",
        combos
            .iter()
            .map(|p| p.period.unwrap() as u64)
            .collect::<Vec<_>>()
            .into_pyarray(py),
    )?;
    d.set_item(
        "mults",
        combos
            .iter()
            .map(|p| p.mult.unwrap())
            .collect::<Vec<_>>()
            .into_pyarray(py),
    )?;
    d.set_item(
        "use_close",
        combos
            .iter()
            .map(|p| p.use_close.unwrap())
            .collect::<Vec<_>>()
            .into_pyarray(py),
    )?;
    Ok((
        CeDeviceArrayF32Py {
            inner,
            _ctx: ctx,
            device_id: dev_id,
        },
        d,
    ))
}

#[cfg(all(feature = "python", feature = "cuda"))]
#[pyfunction(name = "chandelier_exit_cuda_many_series_one_param_dev")]
#[pyo3(signature = (high_tm_f32, low_tm_f32, close_tm_f32, cols, rows, period, mult, use_close=true, device_id=0))]
pub fn chandelier_exit_cuda_many_series_one_param_dev_py<'py>(
    py: Python<'py>,
    high_tm_f32: PyReadonlyArray1<'py, f32>,
    low_tm_f32: PyReadonlyArray1<'py, f32>,
    close_tm_f32: PyReadonlyArray1<'py, f32>,
    cols: usize,
    rows: usize,
    period: usize,
    mult: f64,
    use_close: bool,
    device_id: usize,
) -> PyResult<CeDeviceArrayF32Py> {
    use crate::cuda::cuda_available;
    if !cuda_available() {
        return Err(PyValueError::new_err("CUDA not available"));
    }
    let h = high_tm_f32.as_slice()?;
    let l = low_tm_f32.as_slice()?;
    let c = close_tm_f32.as_slice()?;
    let (inner, ctx, dev_id) = py.allow_threads(|| {
        let cuda =
            CudaChandelierExit::new(device_id).map_err(|e| PyValueError::new_err(e.to_string()))?;
        let ctx = cuda.context_arc();
        let dev_id = cuda.device_id();
        cuda.chandelier_exit_many_series_one_param_time_major_dev(
            h,
            l,
            c,
            cols,
            rows,
            period,
            mult as f32,
            use_close,
        )
        .map(|a| (a, ctx, dev_id))
        .map_err(|e| PyValueError::new_err(e.to_string()))
    })?;
    Ok(CeDeviceArrayF32Py {
        inner,
        _ctx: ctx,
        device_id: dev_id,
    })
}

#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
#[wasm_bindgen]
pub fn ce_alloc(len: usize) -> *mut f64 {
    let mut v = Vec::<f64>::with_capacity(len);
    let p = v.as_mut_ptr();
    std::mem::forget(v);
    p
}

#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
#[wasm_bindgen]
pub fn ce_free(ptr: *mut f64, len: usize) {
    unsafe {
        let _ = Vec::from_raw_parts(ptr, len, len);
    }
}

#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
#[wasm_bindgen]
pub fn ce_into(
    high_ptr: *const f64,
    low_ptr: *const f64,
    close_ptr: *const f64,
    out_ptr: *mut f64,
    len: usize,
    period: usize,
    mult: f64,
    use_close: bool,
) -> Result<(), JsValue> {
    if [
        high_ptr as usize,
        low_ptr as usize,
        close_ptr as usize,
        out_ptr as usize,
    ]
    .iter()
    .any(|&p| p == 0)
    {
        return Err(JsValue::from_str("null pointer to ce_into"));
    }
    unsafe {
        let h = std::slice::from_raw_parts(high_ptr, len);
        let l = std::slice::from_raw_parts(low_ptr, len);
        let c = std::slice::from_raw_parts(close_ptr, len);
        let total = len
            .checked_mul(2)
            .ok_or_else(|| JsValue::from_str("2*len overflow in ce_into"))?;

        let alias = out_ptr == high_ptr as *mut f64
            || out_ptr == low_ptr as *mut f64
            || out_ptr == close_ptr as *mut f64;
        if alias {
            let mut tmp = vec![f64::NAN; total];
            let params = ChandelierExitParams {
                period: Some(period),
                mult: Some(mult),
                use_close: Some(use_close),
            };
            let input = ChandelierExitInput::from_slices(h, l, c, params);
            let result = chandelier_exit_with_kernel(&input, Kernel::Auto)
                .map_err(|e| JsValue::from_str(&e.to_string()))?;
            tmp[..len].copy_from_slice(&result.long_stop);
            tmp[len..].copy_from_slice(&result.short_stop);
            std::ptr::copy_nonoverlapping(tmp.as_ptr(), out_ptr, total);
            return Ok(());
        }

        let out = std::slice::from_raw_parts_mut(out_ptr, total);
        let params = ChandelierExitParams {
            period: Some(period),
            mult: Some(mult),
            use_close: Some(use_close),
        };
        let input = ChandelierExitInput::from_slices(h, l, c, params);
        let (long_stop, short_stop) = match chandelier_exit_with_kernel(&input, Kernel::Auto) {
            Ok(o) => (o.long_stop, o.short_stop),
            Err(e) => return Err(JsValue::from_str(&e.to_string())),
        };
        out[..len].copy_from_slice(&long_stop);
        out[len..].copy_from_slice(&short_stop);
    }
    Ok(())
}

#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
#[wasm_bindgen]
pub fn ce_batch_into(
    high_ptr: *const f64,
    low_ptr: *const f64,
    close_ptr: *const f64,
    len: usize,
    out_ptr: *mut f64,
    period_start: usize,
    period_end: usize,
    period_step: usize,
    mult_start: f64,
    mult_end: f64,
    mult_step: f64,
    use_close: bool,
) -> Result<usize, JsValue> {
    if [
        high_ptr as usize,
        low_ptr as usize,
        close_ptr as usize,
        out_ptr as usize,
    ]
    .iter()
    .any(|&p| p == 0)
    {
        return Err(JsValue::from_str("null pointer to ce_batch_into"));
    }
    unsafe {
        let h = std::slice::from_raw_parts(high_ptr, len);
        let l = std::slice::from_raw_parts(low_ptr, len);
        let c = std::slice::from_raw_parts(close_ptr, len);
        let sweep = CeBatchRange {
            period: (period_start, period_end, period_step),
            mult: (mult_start, mult_end, mult_step),
            use_close: (use_close, use_close, false),
        };
        let combos = expand_ce_checked(&sweep).map_err(|e| JsValue::from_str(&e.to_string()))?;
        let rows = combos
            .len()
            .checked_mul(2)
            .ok_or_else(|| JsValue::from_str("rows*2 overflow in ce_batch_into"))?;
        let cols = len;
        let total = rows
            .checked_mul(cols)
            .ok_or_else(|| JsValue::from_str("rows*cols overflow in ce_batch_into"))?;
        let out = std::slice::from_raw_parts_mut(out_ptr, total);
        ce_batch_inner_into(h, l, c, &combos, detect_best_kernel(), out)
            .map_err(|e| JsValue::from_str(&e.to_string()))?;
        Ok(rows)
    }
}

#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
#[wasm_bindgen(js_name = ce_batch)]
pub fn ce_batch_unified_js(
    high: &[f64],
    low: &[f64],
    close: &[f64],
    config: JsValue,
) -> Result<JsValue, JsValue> {
    #[derive(Deserialize)]
    struct BatchConfig {
        period_range: (usize, usize, usize),
        mult_range: (f64, f64, f64),
        use_close: bool,
    }

    let cfg: BatchConfig = serde_wasm_bindgen::from_value(config)
        .map_err(|e| JsValue::from_str(&format!("Invalid config: {}", e)))?;

    let sweep = CeBatchRange {
        period: cfg.period_range,
        mult: cfg.mult_range,
        use_close: (cfg.use_close, cfg.use_close, false),
    };
    let combos = expand_ce_checked(&sweep).map_err(|e| JsValue::from_str(&e.to_string()))?;
    let rows = combos
        .len()
        .checked_mul(2)
        .ok_or_else(|| JsValue::from_str("rows*2 overflow in ce_batch_unified_js"))?;
    let cols = close.len();
    let total = rows
        .checked_mul(cols)
        .ok_or_else(|| JsValue::from_str("rows*cols overflow in ce_batch_unified_js"))?;
    let mut values = vec![f64::NAN; total];
    ce_batch_inner_into(high, low, close, &combos, detect_best_kernel(), &mut values)
        .map_err(|e| JsValue::from_str(&e.to_string()))?;

    serde_wasm_bindgen::to_value(&CeBatchJsOutput {
        values,
        combos,
        rows,
        cols,
    })
    .map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))
}

#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
#[wasm_bindgen]
pub fn chandelier_exit_wasm(
    high: &[f64],
    low: &[f64],
    close: &[f64],
    period: Option<usize>,
    mult: Option<f64>,
    use_close: Option<bool>,
) -> Result<JsValue, JsValue> {
    let p = period.unwrap_or(22);
    let m = mult.unwrap_or(3.0);
    let u = use_close.unwrap_or(true);

    let params = ChandelierExitParams {
        period: Some(p),
        mult: Some(m),
        use_close: Some(u),
    };
    let input = ChandelierExitInput::from_slices(high, low, close, params);
    let out = chandelier_exit_with_kernel(&input, Kernel::Auto)
        .map_err(|e| JsValue::from_str(&e.to_string()))?;

    #[derive(Serialize)]
    struct OldFormatResult {
        long_stop: Vec<f64>,
        short_stop: Vec<f64>,
    }

    serde_wasm_bindgen::to_value(&OldFormatResult {
        long_stop: out.long_stop,
        short_stop: out.short_stop,
    })
    .map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))
}

#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
#[wasm_bindgen]
pub struct ChandelierExitStreamWasm {
    high_buffer: Vec<f64>,
    low_buffer: Vec<f64>,
    close_buffer: Vec<f64>,
    period: usize,
    mult: f64,
    use_close: bool,

    prev_close: Option<f64>,
    atr_prev: Option<f64>,
    long_stop_prev: Option<f64>,
    short_stop_prev: Option<f64>,
    dir_prev: i8,
    warm_tr_sum: f64,
    count: usize,
}

#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
#[wasm_bindgen]
impl ChandelierExitStreamWasm {
    #[wasm_bindgen(constructor)]
    pub fn new(period: Option<usize>, mult: Option<f64>, use_close: Option<bool>) -> Self {
        Self {
            high_buffer: Vec::new(),
            low_buffer: Vec::new(),
            close_buffer: Vec::new(),
            period: period.unwrap_or(22),
            mult: mult.unwrap_or(3.0),
            use_close: use_close.unwrap_or(true),
            prev_close: None,
            atr_prev: None,
            long_stop_prev: None,
            short_stop_prev: None,
            dir_prev: 1,
            warm_tr_sum: 0.0,
            count: 0,
        }
    }

    pub fn update(&mut self, high: f64, low: f64, close: f64) -> Result<JsValue, JsValue> {
        self.high_buffer.push(high);
        self.low_buffer.push(low);
        self.close_buffer.push(close);

        let tr = if let Some(pc) = self.prev_close {
            let hl = (high - low).abs();
            let hc = (high - pc).abs();
            let lc = (low - pc).abs();
            hl.max(hc.max(lc))
        } else {
            (high - low).abs()
        };

        let atr = if self.atr_prev.is_none() {
            self.warm_tr_sum += tr;
            self.count += 1;
            if self.count < self.period {
                self.prev_close = Some(close);
                return Ok(JsValue::NULL);
            }
            let seed = self.warm_tr_sum / self.period as f64;
            self.atr_prev = Some(seed);
            seed
        } else {
            let prev = self.atr_prev.unwrap();
            let n = self.period as f64;
            let next = (prev * (n - 1.0) + tr) / n;
            self.atr_prev = Some(next);
            next
        };

        if self.high_buffer.len() > self.period {
            self.high_buffer.remove(0);
            self.low_buffer.remove(0);
            self.close_buffer.remove(0);
        }

        let (highest, lowest) = if self.use_close {
            (
                window_max(&self.close_buffer),
                window_min(&self.close_buffer),
            )
        } else {
            (window_max(&self.high_buffer), window_min(&self.low_buffer))
        };

        let long_stop_val = highest - self.mult * atr;
        let short_stop_val = lowest + self.mult * atr;

        let lsp = self.long_stop_prev.unwrap_or(long_stop_val);
        let ssp = self.short_stop_prev.unwrap_or(short_stop_val);

        let ls = if let Some(pc) = self.prev_close {
            if pc > lsp {
                long_stop_val.max(lsp)
            } else {
                long_stop_val
            }
        } else {
            long_stop_val
        };
        let ss = if let Some(pc) = self.prev_close {
            if pc < ssp {
                short_stop_val.min(ssp)
            } else {
                short_stop_val
            }
        } else {
            short_stop_val
        };

        let d = if close > ssp {
            1
        } else if close < lsp {
            -1
        } else {
            self.dir_prev
        };

        self.long_stop_prev = Some(ls);
        self.short_stop_prev = Some(ss);
        self.dir_prev = d;
        self.prev_close = Some(close);

        let out_long = if d == 1 { ls } else { f64::NAN };
        let out_short = if d == -1 { ss } else { f64::NAN };

        let result = serde_json::json!({
            "long_stop": out_long,
            "short_stop": out_short,
        });
        serde_wasm_bindgen::to_value(&result)
            .map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))
    }

    pub fn reset(&mut self) {
        self.high_buffer.clear();
        self.low_buffer.clear();
        self.close_buffer.clear();
        self.prev_close = None;
        self.atr_prev = None;
        self.long_stop_prev = None;
        self.short_stop_prev = None;
        self.dir_prev = 1;
        self.warm_tr_sum = 0.0;
        self.count = 0;
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::skip_if_unsupported;
    use crate::utilities::data_loader::read_candles_from_csv;
    #[cfg(feature = "proptest")]
    use proptest::prelude::*;
    use std::error::Error;

    fn check_chandelier_exit_partial_params(
        test_name: &str,
        kernel: Kernel,
    ) -> Result<(), Box<dyn Error>> {
        skip_if_unsupported!(kernel, test_name);
        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
        let candles = read_candles_from_csv(file_path)?;

        let default_params = ChandelierExitParams {
            period: None,
            mult: None,
            use_close: None,
        };
        let input = ChandelierExitInput::from_candles(&candles, default_params);
        let output = chandelier_exit_with_kernel(&input, kernel)?;
        assert_eq!(output.long_stop.len(), candles.close.len());
        assert_eq!(output.short_stop.len(), candles.close.len());

        Ok(())
    }

    fn check_chandelier_exit_accuracy(
        test_name: &str,
        kernel: Kernel,
    ) -> Result<(), Box<dyn Error>> {
        skip_if_unsupported!(kernel, test_name);
        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
        let candles = read_candles_from_csv(file_path)?;

        let params = ChandelierExitParams {
            period: Some(22),
            mult: Some(3.0),
            use_close: Some(true),
        };
        let input = ChandelierExitInput::from_candles(&candles, params);
        let result = chandelier_exit_with_kernel(&input, kernel)?;

        let expected_indices = [15386, 15387, 15388, 15389, 15390];
        let expected_short_stops = [
            68719.23648167,
            68705.54391432,
            68244.42828185,
            67599.49972358,
            66883.02246342,
        ];

        for (i, &idx) in expected_indices.iter().enumerate() {
            if idx < result.short_stop.len() {
                let actual = result.short_stop[idx];
                let expected = expected_short_stops[i];
                let diff = (actual - expected).abs();
                assert!(
                    diff < 1e-5,
                    "[{}] CE {:?} short_stop[{}] mismatch: expected {:.8}, got {:.8}, diff {:.8}",
                    test_name,
                    kernel,
                    idx,
                    expected,
                    actual,
                    diff
                );
            }
        }

        for i in 0..21 {
            assert!(
                result.long_stop[i].is_nan(),
                "[{}] CE {:?} long_stop should be NaN at idx {}",
                test_name,
                kernel,
                i
            );
            assert!(
                result.short_stop[i].is_nan(),
                "[{}] CE {:?} short_stop should be NaN at idx {}",
                test_name,
                kernel,
                i
            );
        }

        let has_valid_long = result.long_stop.iter().skip(21).any(|&v| !v.is_nan());
        let has_valid_short = result.short_stop.iter().skip(21).any(|&v| !v.is_nan());
        assert!(
            has_valid_long || has_valid_short,
            "[{}] CE {:?} should have valid values after warmup",
            test_name,
            kernel
        );

        Ok(())
    }

    fn check_chandelier_exit_default_candles(
        test_name: &str,
        kernel: Kernel,
    ) -> Result<(), Box<dyn Error>> {
        skip_if_unsupported!(kernel, test_name);
        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
        let candles = read_candles_from_csv(file_path)?;

        let input = ChandelierExitInput::with_default_candles(&candles);
        let result = chandelier_exit_with_kernel(&input, kernel)?;

        assert_eq!(result.long_stop.len(), candles.close.len());
        assert_eq!(result.short_stop.len(), candles.close.len());

        Ok(())
    }

    fn check_chandelier_exit_zero_period(
        test_name: &str,
        kernel: Kernel,
    ) -> Result<(), Box<dyn Error>> {
        skip_if_unsupported!(kernel, test_name);
        let data = vec![1.0; 10];
        let params = ChandelierExitParams {
            period: Some(0),
            mult: Some(3.0),
            use_close: Some(true),
        };
        let input = ChandelierExitInput::from_slices(&data, &data, &data, params);
        let res = chandelier_exit_with_kernel(&input, kernel);
        assert!(
            res.is_err(),
            "[{}] CE should fail with zero period",
            test_name
        );
        Ok(())
    }

    fn check_chandelier_exit_period_exceeds_length(
        test_name: &str,
        kernel: Kernel,
    ) -> Result<(), Box<dyn Error>> {
        skip_if_unsupported!(kernel, test_name);
        let data = vec![1.0; 10];
        let params = ChandelierExitParams {
            period: Some(20),
            mult: Some(3.0),
            use_close: Some(true),
        };
        let input = ChandelierExitInput::from_slices(&data, &data, &data, params);
        let res = chandelier_exit_with_kernel(&input, kernel);
        assert!(
            res.is_err(),
            "[{}] CE should fail when period exceeds data length",
            test_name
        );
        Ok(())
    }

    fn check_chandelier_exit_very_small_dataset(
        test_name: &str,
        kernel: Kernel,
    ) -> Result<(), Box<dyn Error>> {
        skip_if_unsupported!(kernel, test_name);
        let data = vec![1.0; 2];
        let params = ChandelierExitParams {
            period: Some(22),
            mult: Some(3.0),
            use_close: Some(true),
        };
        let input = ChandelierExitInput::from_slices(&data, &data, &data, params);
        let res = chandelier_exit_with_kernel(&input, kernel);
        assert!(
            res.is_err(),
            "[{}] CE should fail with insufficient data",
            test_name
        );
        Ok(())
    }

    fn check_chandelier_exit_empty_input(
        test_name: &str,
        kernel: Kernel,
    ) -> Result<(), Box<dyn Error>> {
        skip_if_unsupported!(kernel, test_name);
        let empty: Vec<f64> = vec![];
        let params = ChandelierExitParams::default();
        let input = ChandelierExitInput::from_slices(&empty, &empty, &empty, params);
        let res = chandelier_exit_with_kernel(&input, kernel);
        assert!(
            res.is_err(),
            "[{}] CE should return error for empty input",
            test_name
        );
        Ok(())
    }

    fn check_chandelier_exit_invalid_mult(
        test_name: &str,
        kernel: Kernel,
    ) -> Result<(), Box<dyn Error>> {
        skip_if_unsupported!(kernel, test_name);
        let data = vec![1.0; 30];

        let params = ChandelierExitParams {
            period: Some(10),
            mult: Some(-2.0),
            use_close: Some(true),
        };
        let input = ChandelierExitInput::from_slices(&data, &data, &data, params);
        let res = chandelier_exit_with_kernel(&input, kernel);

        assert!(
            res.is_ok(),
            "[{}] CE should handle negative multiplier",
            test_name
        );

        let params_zero = ChandelierExitParams {
            period: Some(10),
            mult: Some(0.0),
            use_close: Some(true),
        };
        let input_zero = ChandelierExitInput::from_slices(&data, &data, &data, params_zero);
        let res_zero = chandelier_exit_with_kernel(&input_zero, kernel);
        assert!(
            res_zero.is_ok(),
            "[{}] CE should handle zero multiplier",
            test_name
        );

        Ok(())
    }

    fn check_chandelier_exit_reinput(
        test_name: &str,
        kernel: Kernel,
    ) -> Result<(), Box<dyn Error>> {
        skip_if_unsupported!(kernel, test_name);
        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
        let candles = read_candles_from_csv(file_path)?;

        let params = ChandelierExitParams {
            period: Some(14),
            mult: Some(2.5),
            use_close: Some(false),
        };
        let input1 = ChandelierExitInput::from_candles(&candles, params.clone());
        let output1 = chandelier_exit_with_kernel(&input1, kernel)?;

        let input2 = ChandelierExitInput::from_slices(
            &output1.long_stop,
            &output1.long_stop,
            &output1.long_stop,
            params,
        );
        let output2 = chandelier_exit_with_kernel(&input2, kernel)?;

        assert_eq!(output1.long_stop.len(), output2.long_stop.len());

        let mut has_diff = false;
        for i in 14..output1.long_stop.len() {
            if !output1.long_stop[i].is_nan() && !output2.long_stop[i].is_nan() {
                if (output1.long_stop[i] - output2.long_stop[i]).abs() > 1e-10 {
                    has_diff = true;
                    break;
                }
            }
        }
        assert!(
            has_diff,
            "[{}] CE reinput should produce different results",
            test_name
        );

        Ok(())
    }

    fn check_chandelier_exit_nan_handling(
        test_name: &str,
        kernel: Kernel,
    ) -> Result<(), Box<dyn Error>> {
        skip_if_unsupported!(kernel, test_name);
        let mut high = vec![10.0; 50];
        let mut low = vec![5.0; 50];
        let mut close = vec![7.5; 50];

        high[10] = f64::NAN;
        low[20] = f64::NAN;
        close[30] = f64::NAN;

        let params = ChandelierExitParams {
            period: Some(10),
            mult: Some(2.0),
            use_close: Some(true),
        };
        let input = ChandelierExitInput::from_slices(&high, &low, &close, params);
        let res = chandelier_exit_with_kernel(&input, kernel)?;

        assert_eq!(res.long_stop.len(), 50);
        assert_eq!(res.short_stop.len(), 50);

        Ok(())
    }

    fn check_chandelier_exit_streaming(
        test_name: &str,
        kernel: Kernel,
    ) -> Result<(), Box<dyn Error>> {
        skip_if_unsupported!(kernel, test_name);
        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
        let candles = read_candles_from_csv(file_path)?;

        let period = 22;
        let mult = 3.0;
        let use_close = true;

        let params = ChandelierExitParams {
            period: Some(period),
            mult: Some(mult),
            use_close: Some(use_close),
        };
        let input = ChandelierExitInput::from_candles(&candles, params);
        let batch_output = chandelier_exit_with_kernel(&input, kernel)?;

        let mut stream_long: Vec<f64> = Vec::with_capacity(candles.close.len());
        let mut stream_short: Vec<f64> = Vec::with_capacity(candles.close.len());

        for i in 0..candles.close.len() {
            if i < period - 1 {
                assert!(batch_output.long_stop[i].is_nan());
                assert!(batch_output.short_stop[i].is_nan());
            }
        }

        Ok(())
    }

    #[cfg(debug_assertions)]
    fn check_chandelier_exit_no_poison(
        test_name: &str,
        kernel: Kernel,
    ) -> Result<(), Box<dyn Error>> {
        skip_if_unsupported!(kernel, test_name);

        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
        let candles = read_candles_from_csv(file_path)?;

        let test_params = vec![
            ChandelierExitParams::default(),
            ChandelierExitParams {
                period: Some(10),
                mult: Some(1.5),
                use_close: Some(false),
            },
            ChandelierExitParams {
                period: Some(30),
                mult: Some(4.0),
                use_close: Some(true),
            },
        ];

        for params in test_params.iter() {
            let input = ChandelierExitInput::from_candles(&candles, params.clone());
            let output = chandelier_exit_with_kernel(&input, kernel)?;

            for (i, &val) in output.long_stop.iter().enumerate() {
                if val.is_nan() {
                    continue;
                }

                let bits = val.to_bits();

                if bits == 0x11111111_11111111 {
                    panic!(
                        "[{}] Found alloc_with_nan_prefix poison value {} (0x{:016X}) at index {} \
                        with params: period={}, mult={}, use_close={}",
                        test_name,
                        val,
                        bits,
                        i,
                        params.period.unwrap_or(22),
                        params.mult.unwrap_or(3.0),
                        params.use_close.unwrap_or(true)
                    );
                }
            }

            for (i, &val) in output.short_stop.iter().enumerate() {
                if val.is_nan() {
                    continue;
                }

                let bits = val.to_bits();

                if bits == 0x11111111_11111111 {
                    panic!(
                        "[{}] Found alloc_with_nan_prefix poison value {} (0x{:016X}) at index {} \
                        with params: period={}, mult={}, use_close={}",
                        test_name,
                        val,
                        bits,
                        i,
                        params.period.unwrap_or(22),
                        params.mult.unwrap_or(3.0),
                        params.use_close.unwrap_or(true)
                    );
                }
            }
        }

        Ok(())
    }

    #[cfg(not(debug_assertions))]
    fn check_chandelier_exit_no_poison(
        _test_name: &str,
        _kernel: Kernel,
    ) -> Result<(), Box<dyn Error>> {
        Ok(())
    }

    #[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
    fn check_ce_streaming_vs_batch(test: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
        skip_if_unsupported!(kernel, test);
        let c = read_candles_from_csv("src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv")?;
        let p = ChandelierExitParams::default();
        let batch =
            chandelier_exit_with_kernel(&ChandelierExitInput::from_candles(&c, p.clone()), kernel)?;
        let mut s = ChandelierExitStream::try_new(p)?;
        let mut ls = Vec::with_capacity(c.close.len());
        let mut ss = Vec::with_capacity(c.close.len());
        for i in 0..c.close.len() {
            match s.update(c.high[i], c.low[i], c.close[i]) {
                Some((a, b)) => {
                    ls.push(a);
                    ss.push(b);
                }
                None => {
                    ls.push(f64::NAN);
                    ss.push(f64::NAN);
                }
            }
        }
        assert_eq!(ls.len(), batch.long_stop.len());
        let mut max_diff: f64 = 0.0;
        for i in 0..ls.len() {
            let ls_nan = ls[i].is_nan();
            let bs_nan = batch.long_stop[i].is_nan();

            if ls_nan && bs_nan {
                continue;
            }

            if ls_nan != bs_nan {
                continue;
            }

            let diff = (ls[i] - batch.long_stop[i]).abs();
            max_diff = max_diff.max(diff);
            assert!(
                diff < 1e-9,
                "[{test}] long idx {i}: streaming={} vs batch={}, diff={}",
                ls[i],
                batch.long_stop[i],
                diff
            );
        }

        for i in 0..ss.len() {
            let ss_nan = ss[i].is_nan();
            let bs_nan = batch.short_stop[i].is_nan();

            if ss_nan && bs_nan {
                continue;
            }

            if ss_nan != bs_nan {
                continue;
            }

            let diff = (ss[i] - batch.short_stop[i]).abs();
            max_diff = max_diff.max(diff);
            assert!(
                diff < 1e-9,
                "[{test}] short idx {i}: streaming={} vs batch={}, diff={}",
                ss[i],
                batch.short_stop[i],
                diff
            );
        }

        Ok(())
    }

    #[cfg(all(target_arch = "wasm32", feature = "wasm"))]
    fn check_ce_streaming_vs_batch(_test: &str, _kernel: Kernel) -> Result<(), Box<dyn Error>> {
        Ok(())
    }

    fn check_ce_batch_no_poison(test: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
        skip_if_unsupported!(kernel, test);
        let c = read_candles_from_csv("src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv")?;
        let out = CeBatchBuilder::new()
            .period_range(10, 12, 1)
            .mult_range(2.0, 3.0, 0.5)
            .use_close(true)
            .kernel(kernel)
            .apply_candles(&c)?;
        for (idx, &v) in out.values.iter().enumerate() {
            if v.is_nan() {
                continue;
            }
            let b = v.to_bits();
            assert!(
                b != 0x11111111_11111111 && b != 0x22222222_22222222 && b != 0x33333333_33333333,
                "[{test}] poison at flat idx {idx}"
            );
        }
        Ok(())
    }

    #[cfg(feature = "proptest")]
    #[allow(clippy::float_cmp)]
    fn check_chandelier_exit_property(
        test_name: &str,
        kernel: Kernel,
    ) -> Result<(), Box<dyn std::error::Error>> {
        use proptest::prelude::*;
        skip_if_unsupported!(kernel, test_name);

        let strat = (1usize..=50).prop_flat_map(|period| {
            (
                prop::collection::vec(
                    (-1e6f64..1e6f64).prop_filter("finite", |x| x.is_finite()),
                    period..400,
                ),
                prop::collection::vec(
                    (-1e6f64..1e6f64).prop_filter("finite", |x| x.is_finite()),
                    period..400,
                ),
                prop::collection::vec(
                    (-1e6f64..1e6f64).prop_filter("finite", |x| x.is_finite()),
                    period..400,
                ),
                Just(period),
                1.0f64..5.0f64,
                any::<bool>(),
            )
        });

        proptest::test_runner::TestRunner::default()
            .run(&strat, |(high, low, close, period, mult, use_close)| {
                let mut high_fixed = high.clone();
                let mut low_fixed = low.clone();
                for i in 0..high.len().min(low.len()) {
                    if high_fixed[i] < low_fixed[i] {
                        std::mem::swap(&mut high_fixed[i], &mut low_fixed[i]);
                    }
                }

                let params = ChandelierExitParams {
                    period: Some(period),
                    mult: Some(mult),
                    use_close: Some(use_close),
                };
                let input =
                    ChandelierExitInput::from_slices(&high_fixed, &low_fixed, &close, params);

                let out = chandelier_exit_with_kernel(&input, kernel);

                if let Ok(output) = out {
                    prop_assert_eq!(output.long_stop.len(), close.len());
                    prop_assert_eq!(output.short_stop.len(), close.len());

                    for i in 0..output.long_stop.len() {
                        let long_active = !output.long_stop[i].is_nan();
                        let short_active = !output.short_stop[i].is_nan();
                        prop_assert!(
                            !(long_active && short_active),
                            "Both stops active at index {}: long={}, short={}",
                            i,
                            output.long_stop[i],
                            output.short_stop[i]
                        );
                    }
                }
                Ok(())
            })
            .unwrap();

        Ok(())
    }

    macro_rules! generate_all_chandelier_exit_tests {
        ($($test_fn:ident),*) => {
            paste::paste! {
                $(
                    #[test]
                    fn [<$test_fn _scalar_f64>]() {
                        let _ = $test_fn(stringify!([<$test_fn _scalar_f64>]), Kernel::Scalar);
                    }
                )*
                #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
                $(
                    #[test]
                    fn [<$test_fn _avx2_f64>]() {
                        let _ = $test_fn(stringify!([<$test_fn _avx2_f64>]), Kernel::Avx2);
                    }
                    #[test]
                    fn [<$test_fn _avx512_f64>]() {
                        let _ = $test_fn(stringify!([<$test_fn _avx512_f64>]), Kernel::Avx512);
                    }
                )*
                #[cfg(all(target_arch = "wasm32", target_feature = "simd128"))]
                $(
                    #[test]
                    fn [<$test_fn _simd128_f64>]() {
                        let _ = $test_fn(stringify!([<$test_fn _simd128_f64>]), Kernel::Scalar);
                    }
                )*
            }
        }
    }

    generate_all_chandelier_exit_tests!(
        check_chandelier_exit_partial_params,
        check_chandelier_exit_accuracy,
        check_chandelier_exit_default_candles,
        check_chandelier_exit_zero_period,
        check_chandelier_exit_period_exceeds_length,
        check_chandelier_exit_very_small_dataset,
        check_chandelier_exit_empty_input,
        check_chandelier_exit_invalid_mult,
        check_chandelier_exit_reinput,
        check_chandelier_exit_nan_handling,
        check_chandelier_exit_streaming,
        check_chandelier_exit_no_poison,
        check_ce_streaming_vs_batch,
        check_ce_batch_no_poison
    );

    #[cfg(feature = "proptest")]
    generate_all_chandelier_exit_tests!(check_chandelier_exit_property);

    #[test]
    fn ce_no_poison() {
        use crate::utilities::data_loader::read_candles_from_csv;
        let c = read_candles_from_csv("src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv").unwrap();
        let out = ChandelierExitBuilder::new().apply_candles(&c).unwrap();
        for &v in out.long_stop.iter().chain(out.short_stop.iter()) {
            if v.is_nan() {
                continue;
            }
            let b = v.to_bits();
            assert!(
                b != 0x11111111_11111111 && b != 0x22222222_22222222 && b != 0x33333333_33333333
            );
        }
    }

    #[test]
    fn ce_streaming_consistency() {
        use crate::utilities::data_loader::read_candles_from_csv;
        let c = read_candles_from_csv("src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv").unwrap();
        let subset = 100;
        let high = &c.high[..subset];
        let low = &c.low[..subset];
        let close = &c.close[..subset];

        let batch_out = ChandelierExitBuilder::new()
            .period(22)
            .mult(3.0)
            .use_close(true)
            .apply_slices(high, low, close)
            .unwrap();

        assert_eq!(batch_out.long_stop.len(), subset);
        assert_eq!(batch_out.short_stop.len(), subset);

        for i in 0..21 {
            assert!(batch_out.long_stop[i].is_nan());
            assert!(batch_out.short_stop[i].is_nan());
        }
    }

    #[test]
    fn ce_batch_shapes() {
        use crate::utilities::data_loader::read_candles_from_csv;
        let c = read_candles_from_csv("src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv").unwrap();
        let out = CeBatchBuilder::new()
            .period_range(10, 12, 1)
            .mult_range(2.5, 3.5, 0.5)
            .use_close(true)
            .apply_candles(&c)
            .unwrap();
        assert_eq!(out.rows, 2 * out.combos.len());
        assert_eq!(out.cols, c.close.len());
    }

    #[test]
    fn test_chandelier_exit_into_matches_api() -> Result<(), Box<dyn std::error::Error>> {
        let len = 256usize;
        let mut high = Vec::with_capacity(len);
        let mut low = Vec::with_capacity(len);
        let mut close = Vec::with_capacity(len);
        for i in 0..len {
            let base = 100.0 + (i as f64) * 0.01 + ((i % 7) as f64 - 3.0);
            let c = base;
            let h = c + 0.5 + 0.05 * ((i % 3) as f64);
            let l = c - 0.5 - 0.05 * ((i % 2) as f64);
            high.push(h);
            low.push(l);
            close.push(c);
        }

        let params = ChandelierExitParams::default();
        let input = ChandelierExitInput::from_slices(&high, &low, &close, params);

        let baseline = chandelier_exit(&input)?;

        let mut out_long = vec![0.0; len];
        let mut out_short = vec![0.0; len];
        chandelier_exit_into(&input, &mut out_long, &mut out_short)?;

        assert_eq!(out_long.len(), baseline.long_stop.len());
        assert_eq!(out_short.len(), baseline.short_stop.len());

        fn eq_or_both_nan(a: f64, b: f64) -> bool {
            (a.is_nan() && b.is_nan()) || (a == b)
        }

        for i in 0..len {
            assert!(
                eq_or_both_nan(out_long[i], baseline.long_stop[i]),
                "long_stop mismatch at {}: into={} api={}",
                i,
                out_long[i],
                baseline.long_stop[i]
            );
            assert!(
                eq_or_both_nan(out_short[i], baseline.short_stop[i]),
                "short_stop mismatch at {}: into={} api={}",
                i,
                out_short[i],
                baseline.short_stop[i]
            );
        }

        Ok(())
    }
}
