#[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 js_sys;
#[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::{source_type, Candles};
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;
use aligned_vec::{AVec, CACHELINE_ALIGN};

use crate::indicators::moving_averages::sma::{sma_with_kernel, SmaInput, SmaParams};
use crate::indicators::stddev::{stddev_with_kernel, StdDevInput, StdDevParams};

#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
use core::arch::x86_64::*;

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

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

impl<'a> AsRef<[f64]> for MaczInput<'a> {
    #[inline(always)]
    fn as_ref(&self) -> &[f64] {
        match &self.data {
            MaczData::Slice(sl) => sl,
            MaczData::SliceWithVolume { data, .. } => data,
            MaczData::Candles {
                candles, source, ..
            } => source_type(candles, source),
        }
    }
}

#[derive(Debug, Clone)]
pub enum MaczData<'a> {
    Candles {
        candles: &'a Candles,
        source: &'a str,
        volume: &'a [f64],
    },
    Slice(&'a [f64]),
    SliceWithVolume {
        data: &'a [f64],
        volume: &'a [f64],
    },
}

#[derive(Debug, Clone)]
pub struct MaczOutput {
    pub values: Vec<f64>,
}

#[derive(Debug, Clone)]
#[cfg_attr(
    all(target_arch = "wasm32", feature = "wasm"),
    derive(Serialize, Deserialize)
)]
pub struct MaczParams {
    pub fast_length: Option<usize>,
    pub slow_length: Option<usize>,
    pub signal_length: Option<usize>,
    pub lengthz: Option<usize>,
    pub length_stdev: Option<usize>,
    pub a: Option<f64>,
    pub b: Option<f64>,
    pub use_lag: Option<bool>,
    pub gamma: Option<f64>,
}

impl Default for MaczParams {
    fn default() -> Self {
        Self {
            fast_length: Some(12),
            slow_length: Some(25),
            signal_length: Some(9),
            lengthz: Some(20),
            length_stdev: Some(25),
            a: Some(1.0),
            b: Some(1.0),
            use_lag: Some(false),
            gamma: Some(0.02),
        }
    }
}

#[derive(Debug, Clone)]
pub struct MaczInput<'a> {
    pub data: MaczData<'a>,
    pub params: MaczParams,
}

impl<'a> MaczInput<'a> {
    #[inline]
    pub fn from_candles(c: &'a Candles, s: &'a str, p: MaczParams) -> Self {
        Self::from_candles_with_volume(c, s, &c.volume, p)
    }

    #[inline]
    pub fn from_candles_with_volume(
        c: &'a Candles,
        s: &'a str,
        volume: &'a [f64],
        p: MaczParams,
    ) -> Self {
        Self {
            data: MaczData::Candles {
                candles: c,
                source: s,
                volume,
            },
            params: p,
        }
    }

    #[inline]
    pub fn from_slice(sl: &'a [f64], p: MaczParams) -> Self {
        Self {
            data: MaczData::Slice(sl),
            params: p,
        }
    }

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

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

    #[inline]
    pub fn from_slice_with_volume(sl: &'a [f64], vol: &'a [f64], p: MaczParams) -> Self {
        Self {
            data: MaczData::SliceWithVolume {
                data: sl,
                volume: vol,
            },
            params: p,
        }
    }

    #[inline]
    pub fn with_default_slice(sl: &'a [f64]) -> Self {
        Self::from_slice(sl, MaczParams::default())
    }

    #[inline]
    pub fn get_fast_length(&self) -> usize {
        self.params.fast_length.unwrap_or(12)
    }

    #[inline]
    pub fn get_slow_length(&self) -> usize {
        self.params.slow_length.unwrap_or(25)
    }

    #[inline]
    pub fn get_signal_length(&self) -> usize {
        self.params.signal_length.unwrap_or(9)
    }
}

#[derive(Debug, Clone)]
pub struct MaczBuilder {
    fast_length: Option<usize>,
    slow_length: Option<usize>,
    signal_length: Option<usize>,
    lengthz: Option<usize>,
    length_stdev: Option<usize>,
    a: Option<f64>,
    b: Option<f64>,
    use_lag: Option<bool>,
    gamma: Option<f64>,
    kernel: Kernel,
}

impl Default for MaczBuilder {
    fn default() -> Self {
        Self {
            fast_length: None,
            slow_length: None,
            signal_length: None,
            lengthz: None,
            length_stdev: None,
            a: None,
            b: None,
            use_lag: None,
            gamma: None,
            kernel: Kernel::Auto,
        }
    }
}

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

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

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

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

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

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

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

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

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

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

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

    pub fn build_params(self) -> MaczParams {
        MaczParams {
            fast_length: self.fast_length.or(Some(12)),
            slow_length: self.slow_length.or(Some(25)),
            signal_length: self.signal_length.or(Some(9)),
            lengthz: self.lengthz.or(Some(20)),
            length_stdev: self.length_stdev.or(Some(25)),
            a: self.a.or(Some(1.0)),
            b: self.b.or(Some(1.0)),
            use_lag: self.use_lag.or(Some(false)),
            gamma: self.gamma.or(Some(0.02)),
        }
    }

    pub fn apply_slice(self, data: &[f64]) -> Result<MaczOutput, MaczError> {
        let kernel = if self.kernel == Kernel::Auto {
            Kernel::Scalar
        } else {
            self.kernel
        };
        let params = self.build_params();
        let input = MaczInput::from_slice(data, params);
        macz_with_kernel(&input, kernel)
    }

    pub fn apply_candles(self, c: &Candles, src: &str) -> Result<MaczOutput, MaczError> {
        let k = if self.kernel == Kernel::Auto {
            Kernel::Scalar
        } else {
            self.kernel
        };
        macz_with_kernel(&MaczInput::from_candles(c, src, self.build_params()), k)
    }

    pub fn apply_candles_with_volume(
        self,
        c: &Candles,
        src: &str,
        volume: &[f64],
    ) -> Result<MaczOutput, MaczError> {
        let kernel = if self.kernel == Kernel::Auto {
            Kernel::Scalar
        } else {
            self.kernel
        };
        let params = self.build_params();
        let input = MaczInput::from_candles_with_volume(c, src, volume, params);
        macz_with_kernel(&input, kernel)
    }

    #[inline(always)]
    pub fn apply(self, c: &Candles) -> Result<MaczOutput, MaczError> {
        let k = if self.kernel == Kernel::Auto {
            Kernel::Scalar
        } else {
            self.kernel
        };
        let p = self.build_params();

        let input = MaczInput::from_candles(c, "close", p);
        macz_with_kernel(&input, k)
    }

    #[inline(always)]
    pub fn into_stream(self) -> Result<MaczStream, MaczError> {
        MaczStream::try_new(self.build_params())
    }
}

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

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

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

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

    #[error("macz: Invalid gamma: {gamma}")]
    InvalidGamma { gamma: f64 },

    #[error("macz: A out of range: {a} (must be between -2.0 and 2.0)")]
    InvalidA { a: f64 },

    #[error("macz: B out of range: {b} (must be between -2.0 and 2.0)")]
    InvalidB { b: f64 },

    #[error("macz: Volume data required for VWAP calculation")]
    VolumeRequired,

    #[error("macz: {msg}")]
    InvalidParameter { msg: String },

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

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

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

pub struct MaczWorkspace {
    vwap: Vec<f64>,
    zvwap: Vec<f64>,
    fast_ma: Vec<f64>,
    slow_ma: Vec<f64>,
    macd: Vec<f64>,
    stdev: Vec<f64>,
    macz_t: Vec<f64>,
    macz: Vec<f64>,
    signal: Vec<f64>,
}

impl MaczWorkspace {
    pub fn new(len: usize) -> Self {
        Self {
            vwap: Vec::with_capacity(len),
            zvwap: Vec::with_capacity(len),
            fast_ma: Vec::with_capacity(len),
            slow_ma: Vec::with_capacity(len),
            macd: Vec::with_capacity(len),
            stdev: Vec::with_capacity(len),
            macz_t: Vec::with_capacity(len),
            macz: Vec::with_capacity(len),
            signal: Vec::with_capacity(len),
        }
    }

    pub fn resize(&mut self, len: usize) {
        self.vwap.resize(len, f64::NAN);
        self.zvwap.resize(len, f64::NAN);
        self.fast_ma.resize(len, f64::NAN);
        self.slow_ma.resize(len, f64::NAN);
        self.macd.resize(len, f64::NAN);
        self.stdev.resize(len, f64::NAN);
        self.macz_t.resize(len, f64::NAN);
        self.macz.resize(len, f64::NAN);
        self.signal.resize(len, f64::NAN);
    }
}

#[inline]
fn calculate_vwap_into(
    close: &[f64],
    volume: Option<&[f64]>,
    period: usize,
    first: usize,
    kernel: Kernel,
    out: &mut [f64],
) -> Result<(), MaczError> {
    let len = close.len();
    let start = first + period - 1;
    if start > len {
        return Err(MaczError::NotEnoughValidData {
            needed: start - first,
            valid: len - first,
        });
    }

    if let Some(vol) = volume {
        if vol.len() != len {
            return Err(MaczError::InvalidParameter {
                msg: "Close and volume arrays must have same length".into(),
            });
        }
        for i in start..len {
            let s = i + 1 - period;
            let mut pv = 0.0;
            let mut vs = 0.0;
            let mut ok = true;
            for j in s..=i {
                let x = close[j];
                let v = vol[j];
                if x.is_nan() || v.is_nan() {
                    ok = false;
                    break;
                }
                pv += x * v;
                vs += v;
            }
            out[i] = if ok && vs > 0.0 { pv / vs } else { f64::NAN };
        }
    } else {
        let sma_input = SmaInput::from_slice(
            close,
            SmaParams {
                period: Some(period),
            },
        );
        let sma = sma_with_kernel(&sma_input, kernel).map_err(|e| MaczError::InvalidParameter {
            msg: format!("VWAP=SMA error: {e}"),
        })?;

        out[start..].copy_from_slice(&sma.values[start..]);
    }
    Ok(())
}

#[inline]
fn calculate_zvwap_into(
    close: &[f64],
    vwap: &[f64],
    period: usize,
    first: usize,
    out: &mut [f64],
) -> Result<(), MaczError> {
    let len = close.len();
    let start = first + period - 1;
    if start > len {
        return Err(MaczError::NotEnoughValidData {
            needed: start - first,
            valid: len - first,
        });
    }
    for i in start..len {
        let mean = vwap[i];
        if mean.is_nan() {
            out[i] = f64::NAN;
            continue;
        }
        let s = i + 1 - period;

        let mut sum = 0.0;
        let mut ok = true;
        for j in s..=i {
            let x = close[j];
            if x.is_nan() {
                ok = false;
                break;
            }
            let d = x - mean;
            sum += d * d;
        }
        if !ok {
            out[i] = f64::NAN;
            continue;
        }

        let var = sum / period as f64;
        let sd = var.sqrt();
        out[i] = if sd > 0.0 {
            (close[i] - mean) / sd
        } else {
            0.0
        };
    }
    Ok(())
}

#[inline]
fn stdev_source_population_into(
    data: &[f64],
    period: usize,
    first: usize,
    kernel: Kernel,
    out: &mut [f64],
) -> Result<(), MaczError> {
    let len = data.len();
    let start = first + period - 1;
    if start > len {
        return Err(MaczError::NotEnoughValidData {
            needed: start - first,
            valid: len - first,
        });
    }

    let mean = sma_with_kernel(
        &SmaInput::from_slice(
            data,
            SmaParams {
                period: Some(period),
            },
        ),
        kernel,
    )
    .map_err(|e| MaczError::InvalidParameter {
        msg: format!("StdDev mean SMA error: {e}"),
    })?
    .values;

    for i in start..len {
        let m = mean[i];
        if m.is_nan() {
            out[i] = f64::NAN;
            continue;
        }
        let s = i + 1 - period;

        let mut sum = 0.0;
        let mut ok = true;
        for j in s..=i {
            let x = data[j];
            if x.is_nan() {
                ok = false;
                break;
            }
            let d = x - m;
            sum += d * d;
        }
        if !ok {
            out[i] = f64::NAN;
            continue;
        }
        out[i] = (sum / period as f64).sqrt();
    }
    Ok(())
}

fn apply_laguerre(input: &[f64], gamma: f64, output: &mut [f64]) {
    let len = input.len();
    let mut l0 = 0.0;
    let mut l1 = 0.0;
    let mut l2 = 0.0;
    let mut l3 = 0.0;

    for i in 0..len {
        if input[i].is_nan() {
            output[i] = f64::NAN;
        } else {
            let s = input[i];
            let new_l0 = (1.0 - gamma) * s + gamma * l0;
            let new_l1 = -gamma * new_l0 + l0 + gamma * l1;
            let new_l2 = -gamma * new_l1 + l1 + gamma * l2;
            let new_l3 = -gamma * new_l2 + l2 + gamma * l3;

            l0 = new_l0;
            l1 = new_l1;
            l2 = new_l2;
            l3 = new_l3;

            output[i] = (l0 + 2.0 * l1 + 2.0 * l2 + l3) / 6.0;
        }
    }
}

#[inline(always)]
fn macz_warm_len(first: usize, slow: usize, lz: usize, lsd: usize, sig: usize) -> usize {
    first + slow.max(lz).max(lsd) + sig - 2
}

#[inline(always)]
fn macz_prepare<'a>(
    input: &'a MaczInput,
    kernel: Kernel,
) -> Result<
    (
        &'a [f64],
        Option<&'a [f64]>,
        usize,
        usize,
        usize,
        usize,
        usize,
        f64,
        f64,
        bool,
        f64,
        usize,
        Kernel,
    ),
    MaczError,
> {
    let data = input.as_ref();
    let len = data.len();
    if len == 0 {
        return Err(MaczError::EmptyInputData);
    }
    let first = data
        .iter()
        .position(|x| !x.is_nan())
        .ok_or(MaczError::AllValuesNaN)?;

    let fast = input.params.fast_length.unwrap_or(12);
    let slow = input.params.slow_length.unwrap_or(25);
    let sig = input.params.signal_length.unwrap_or(9);
    let lz = input.params.lengthz.unwrap_or(20);
    let lsd = input.params.length_stdev.unwrap_or(25);
    let a = input.params.a.unwrap_or(1.0);
    let b = input.params.b.unwrap_or(1.0);
    let use_lag = input.params.use_lag.unwrap_or(false);
    let gamma = input.params.gamma.unwrap_or(0.02);

    if fast == 0 || slow == 0 || sig == 0 || lz == 0 || lsd == 0 {
        return Err(MaczError::InvalidPeriod {
            period: 0,
            data_len: len,
        });
    }

    let need = fast.max(slow).max(lz).max(lsd);
    let valid = len - first;
    if valid < need {
        return Err(MaczError::NotEnoughValidData {
            needed: need,
            valid,
        });
    }

    if !(-2.0..=2.0).contains(&a) {
        return Err(MaczError::InvalidA { a });
    }
    if !(-2.0..=2.0).contains(&b) {
        return Err(MaczError::InvalidB { b });
    }
    if !(0.0..1.0).contains(&gamma) {
        return Err(MaczError::InvalidGamma { gamma });
    }

    let vol_opt = match &input.data {
        MaczData::Candles { volume, .. } => Some(*volume),
        MaczData::SliceWithVolume { volume, .. } => Some(*volume),
        MaczData::Slice(_) => None,
    };

    let warm_hist = macz_warm_len(first, slow, lz, lsd, sig);

    let chosen = match kernel {
        Kernel::Auto => Kernel::Scalar,
        k => k,
    };
    Ok((
        data, vol_opt, fast, slow, sig, lz, lsd, a, b, use_lag, gamma, warm_hist, chosen,
    ))
}

#[inline(always)]
fn macz_compute_into_tail_only(
    data: &[f64],
    vol: Option<&[f64]>,
    fast: usize,
    slow: usize,
    sig: usize,
    lz: usize,
    lsd: usize,
    a: f64,
    b: f64,
    use_lag: bool,
    gamma: f64,
    warm_hist: usize,
    kernel: Kernel,
    out: &mut [f64],
) -> Result<(), MaczError> {
    let len = data.len();
    let first = data
        .iter()
        .position(|x| !x.is_nan())
        .ok_or(MaczError::AllValuesNaN)?;

    if kernel == Kernel::Scalar {
        unsafe {
            return macz_scalar_classic(
                data, vol, fast, slow, sig, lz, lsd, a, b, use_lag, gamma, first, warm_hist, out,
            );
        }
    }

    let warm_m = first + slow.max(lz).max(lsd) - 1;

    let mut vwap = alloc_with_nan_prefix(len, first + lz - 1);
    calculate_vwap_into(data, vol, lz, first, kernel, &mut vwap)?;

    let mut zvwap = alloc_with_nan_prefix(len, first + lz - 1);
    calculate_zvwap_into(data, &vwap, lz, first, &mut zvwap)?;

    let fast_ma = sma_with_kernel(
        &SmaInput::from_slice(data, SmaParams { period: Some(fast) }),
        kernel,
    )
    .map_err(|e| MaczError::InvalidParameter {
        msg: format!("Fast MA error: {e}"),
    })?
    .values;
    let slow_ma = sma_with_kernel(
        &SmaInput::from_slice(data, SmaParams { period: Some(slow) }),
        kernel,
    )
    .map_err(|e| MaczError::InvalidParameter {
        msg: format!("Slow MA error: {e}"),
    })?
    .values;

    let mut macd = alloc_with_nan_prefix(len, first + slow - 1);
    for i in (first + slow - 1)..len {
        let f = fast_ma[i];
        let s = slow_ma[i];
        macd[i] = if f.is_nan() || s.is_nan() {
            f64::NAN
        } else {
            f - s
        };
    }

    let mut stdev = alloc_with_nan_prefix(len, first + lsd - 1);
    stdev_source_population_into(data, lsd, first, kernel, &mut stdev)?;

    let mut macz_t = alloc_with_nan_prefix(len, warm_m);
    for i in warm_m..len {
        let z = zvwap[i];
        let m = macd[i];
        let sd = stdev[i];
        macz_t[i] = if z.is_nan() || m.is_nan() || sd.is_nan() || sd <= 0.0 {
            f64::NAN
        } else {
            z * a + (m / sd) * b
        };
    }

    let mut macz = alloc_with_nan_prefix(len, warm_m);
    if use_lag {
        apply_laguerre(&macz_t, gamma, &mut macz);
    } else {
        macz[warm_m..].copy_from_slice(&macz_t[warm_m..]);
    }

    let signal = sma_with_kernel(
        &SmaInput::from_slice(&macz, SmaParams { period: Some(sig) }),
        kernel,
    )
    .map_err(|e| MaczError::InvalidParameter {
        msg: format!("Signal MA error: {e}"),
    })?
    .values;

    for i in warm_hist..len {
        let s = signal[i];
        let m = macz[i];
        out[i] = if s.is_nan() || m.is_nan() {
            f64::NAN
        } else {
            m - s
        };
    }
    Ok(())
}

pub fn macz_with_kernel(input: &MaczInput, kernel: Kernel) -> Result<MaczOutput, MaczError> {
    let (data, vol, fast, slow, sig, lz, lsd, a, b, use_lag, gamma, warm_hist, chosen) =
        macz_prepare(input, kernel)?;
    let mut out = alloc_with_nan_prefix(data.len(), warm_hist);
    macz_compute_into_tail_only(
        data, vol, fast, slow, sig, lz, lsd, a, b, use_lag, gamma, warm_hist, chosen, &mut out,
    )?;
    Ok(MaczOutput { values: out })
}

pub fn macz(input: &MaczInput) -> Result<MaczOutput, MaczError> {
    macz_with_kernel(input, Kernel::Auto)
}

#[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
pub fn macz_into(input: &MaczInput, out: &mut [f64]) -> Result<(), MaczError> {
    macz_into_slice(out, input, Kernel::Auto)
}

pub fn macz_into_slice(dst: &mut [f64], input: &MaczInput, kern: Kernel) -> Result<(), MaczError> {
    let (data, vol, fast, slow, sig, lz, lsd, a, b, use_lag, gamma, warm_hist, chosen) =
        macz_prepare(input, kern)?;
    if dst.len() != data.len() {
        return Err(MaczError::OutputLengthMismatch {
            expected: data.len(),
            got: dst.len(),
        });
    }

    for v in &mut dst[..warm_hist] {
        *v = f64::NAN;
    }

    macz_compute_into_tail_only(
        data, vol, fast, slow, sig, lz, lsd, a, b, use_lag, gamma, warm_hist, chosen, dst,
    )
}

pub fn macz_scalar(data: &[f64], params: &MaczParams, out: &mut [f64]) -> Result<(), MaczError> {
    let input = MaczInput::from_slice(data, params.clone());
    macz_into_slice(out, &input, Kernel::Scalar)
}

#[inline(always)]
pub unsafe fn macz_scalar_classic(
    data: &[f64],
    vol: Option<&[f64]>,
    fast: usize,
    slow: usize,
    sig: usize,
    lz: usize,
    lsd: usize,
    a: f64,
    b: f64,
    use_lag: bool,
    gamma: f64,
    first_valid_idx: usize,
    warm_hist: usize,
    out: &mut [f64],
) -> Result<(), MaczError> {
    let len = data.len();
    if len == 0 {
        return Err(MaczError::EmptyInputData);
    }

    let fast_start = first_valid_idx + fast - 1;
    let slow_start = first_valid_idx + slow - 1;
    let lz_start = first_valid_idx + lz - 1;
    let lsd_start = first_valid_idx + lsd - 1;
    let warm_m = first_valid_idx + slow.max(lz).max(lsd) - 1;

    let mut sum_fast = 0.0_f64;
    let mut n_fast_nan = 0usize;
    let mut sum_slow = 0.0_f64;
    let mut n_slow_nan = 0usize;

    let mut sum_lz = 0.0_f64;
    let mut sum2_lz = 0.0_f64;
    let mut n_lz_nan = 0usize;
    let mut sum_lsd = 0.0_f64;
    let mut sum2_lsd = 0.0_f64;
    let mut n_lsd_nan = 0usize;

    let has_volume = vol.is_some();
    let vols = if has_volume { vol.unwrap() } else { &[][..] };
    let mut sum_pv = 0.0_f64;
    let mut sum_v = 0.0_f64;
    let mut n_vwap_nan = 0usize;

    let mut l0 = 0.0_f64;
    let mut l1 = 0.0_f64;
    let mut l2 = 0.0_f64;
    let mut l3 = 0.0_f64;

    let mut sig_ring: Vec<f64> = vec![f64::NAN; sig];
    let mut sig_sum = 0.0_f64;
    let mut sig_count = 0usize;
    let mut sig_nan = 0usize;
    let mut sig_head = 0usize;

    let inv_fast = 1.0 / (fast as f64);
    let inv_slow = 1.0 / (slow as f64);
    let inv_lz = 1.0 / (lz as f64);
    let inv_lsd = 1.0 / (lsd as f64);
    let inv_sig = 1.0 / (sig as f64);

    for i in first_valid_idx..len {
        let x = *data.get_unchecked(i);
        let x_is_nan = x.is_nan();

        if x_is_nan {
            n_fast_nan += 1;
            n_slow_nan += 1;
            n_lz_nan += 1;
            n_lsd_nan += 1;
        } else {
            sum_fast = sum_fast + x;
            sum_slow = sum_slow + x;
            sum_lz = sum_lz + x;
            sum2_lz = sum2_lz + x * x;
            sum_lsd = sum_lsd + x;
            sum2_lsd = sum2_lsd + x * x;
        }

        if has_volume {
            let v = *vols.get_unchecked(i);
            if x_is_nan || v.is_nan() {
                n_vwap_nan += 1;
            } else {
                sum_pv = x.mul_add(v, sum_pv);
                sum_v = sum_v + v;
            }
        }

        if i >= first_valid_idx + fast {
            let xo = *data.get_unchecked(i - fast);
            if xo.is_nan() {
                n_fast_nan -= 1;
            } else {
                sum_fast -= xo;
            }
        }
        if i >= first_valid_idx + slow {
            let xo = *data.get_unchecked(i - slow);
            if xo.is_nan() {
                n_slow_nan -= 1;
            } else {
                sum_slow -= xo;
            }
        }
        if i >= first_valid_idx + lz {
            let xo = *data.get_unchecked(i - lz);
            if xo.is_nan() {
                n_lz_nan -= 1;
            } else {
                sum_lz -= xo;
                sum2_lz -= xo * xo;
            }
            if has_volume {
                let vo = *vols.get_unchecked(i - lz);
                if xo.is_nan() || vo.is_nan() {
                    n_vwap_nan -= 1;
                } else {
                    sum_pv -= xo * vo;
                    sum_v -= vo;
                }
            }
        }
        if i >= first_valid_idx + lsd {
            let xo = *data.get_unchecked(i - lsd);
            if xo.is_nan() {
                n_lsd_nan -= 1;
            } else {
                sum_lsd -= xo;
                sum2_lsd -= xo * xo;
            }
        }

        let have_fast = i >= fast_start && n_fast_nan == 0;
        let have_slow = i >= slow_start && n_slow_nan == 0;

        let fast_ma = if have_fast {
            sum_fast * inv_fast
        } else {
            f64::NAN
        };
        let slow_ma = if have_slow {
            sum_slow * inv_slow
        } else {
            f64::NAN
        };

        let macd = if fast_ma.is_nan() || slow_ma.is_nan() {
            f64::NAN
        } else {
            fast_ma - slow_ma
        };

        let vwap_i = if i >= lz_start {
            if has_volume {
                if n_vwap_nan == 0 && sum_v > 0.0 {
                    sum_pv / sum_v
                } else {
                    f64::NAN
                }
            } else if n_lz_nan == 0 {
                sum_lz * inv_lz
            } else {
                f64::NAN
            }
        } else {
            f64::NAN
        };

        let zvwap = if i >= lz_start && n_lz_nan == 0 && !vwap_i.is_nan() && x.is_finite() {
            let e = sum_lz * inv_lz;
            let e2 = sum2_lz * inv_lz;
            let var = (-2.0 * vwap_i).mul_add(e, e2) + vwap_i * vwap_i;
            let sd = var.max(0.0).sqrt();
            if sd > 0.0 {
                (x - vwap_i) / sd
            } else {
                0.0
            }
        } else {
            f64::NAN
        };

        let sd_src = if i >= lsd_start && n_lsd_nan == 0 {
            let e = sum_lsd * inv_lsd;
            let e2 = sum2_lsd * inv_lsd;
            (e2 - e * e).max(0.0).sqrt()
        } else {
            f64::NAN
        };

        let macz_raw = if i >= warm_m
            && sd_src.is_finite()
            && sd_src > 0.0
            && zvwap.is_finite()
            && macd.is_finite()
        {
            zvwap.mul_add(a, (macd / sd_src) * b)
        } else {
            f64::NAN
        };

        let macz_val = if use_lag {
            if macz_raw.is_finite() {
                let one_minus_g = 1.0 - gamma;
                let new_l0 = macz_raw.mul_add(one_minus_g, gamma * l0);
                let new_l1 = (-gamma).mul_add(new_l0, l0 + gamma * l1);
                let new_l2 = (-gamma).mul_add(new_l1, l1 + gamma * l2);
                let new_l3 = (-gamma).mul_add(new_l2, l2 + gamma * l3);
                l0 = new_l0;
                l1 = new_l1;
                l2 = new_l2;
                l3 = new_l3;
                (l0 + 2.0 * l1 + 2.0 * l2 + l3) / 6.0
            } else {
                f64::NAN
            }
        } else {
            macz_raw
        };

        if i >= warm_m {
            if sig_count == sig {
                let leaving = *sig_ring.get_unchecked(sig_head);
                if leaving.is_nan() {
                    if sig_nan > 0 {
                        sig_nan -= 1;
                    }
                } else {
                    sig_sum -= leaving;
                }
            } else {
                sig_count += 1;
            }
            *sig_ring.get_unchecked_mut(sig_head) = macz_val;
            if macz_val.is_nan() {
                sig_nan += 1;
            } else {
                sig_sum += macz_val;
            }
            sig_head += 1;
            if sig_head == sig {
                sig_head = 0;
            }

            if i >= warm_hist {
                let signal = if sig_count == sig && sig_nan == 0 {
                    sig_sum * inv_sig
                } else {
                    f64::NAN
                };
                *out.get_unchecked_mut(i) = if macz_val.is_nan() || signal.is_nan() {
                    f64::NAN
                } else {
                    macz_val - signal
                };
            }
        }
    }

    Ok(())
}

#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
#[target_feature(enable = "avx2,fma")]
pub unsafe fn macz_avx2(
    data: &[f64],
    params: &MaczParams,
    out: &mut [f64],
) -> Result<(), MaczError> {
    macz_scalar(data, params, out)
}

#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
#[target_feature(enable = "avx512f,fma")]
pub unsafe fn macz_avx512(
    data: &[f64],
    params: &MaczParams,
    out: &mut [f64],
) -> Result<(), MaczError> {
    macz_scalar(data, params, out)
}

#[derive(Debug, Clone)]
pub struct MaczBatchRange {
    pub fast_length: (usize, usize, usize),
    pub slow_length: (usize, usize, usize),
    pub signal_length: (usize, usize, usize),
    pub lengthz: (usize, usize, usize),
    pub length_stdev: (usize, usize, usize),
    pub a: (f64, f64, f64),
    pub b: (f64, f64, f64),
}

impl Default for MaczBatchRange {
    fn default() -> Self {
        Self {
            fast_length: (12, 12, 1),
            slow_length: (25, 25, 1),
            signal_length: (9, 9, 1),
            lengthz: (20, 20, 1),
            length_stdev: (25, 25, 1),
            a: (1.0, 1.249, 0.001),
            b: (1.0, 1.0, 0.0),
        }
    }
}

pub struct MaczBatchOutput {
    pub values: Vec<f64>,
    pub combos: Vec<MaczParams>,
    pub rows: usize,
    pub cols: usize,
}

impl MaczBatchOutput {
    pub fn row_for_params(&self, p: &MaczParams) -> Option<usize> {
        self.combos.iter().position(|c| {
            c.fast_length == p.fast_length
                && c.slow_length == p.slow_length
                && c.signal_length == p.signal_length
                && c.lengthz == p.lengthz
                && c.length_stdev == p.length_stdev
                && (c.a.unwrap_or(1.0) - p.a.unwrap_or(1.0)).abs() < 1e-12
                && (c.b.unwrap_or(1.0) - p.b.unwrap_or(1.0)).abs() < 1e-12
        })
    }

    pub fn values_for(&self, params: &MaczParams) -> Option<&[f64]> {
        self.row_for_params(params).map(|idx| {
            let start = idx * self.cols;
            let end = start + self.cols;
            &self.values[start..end]
        })
    }

    pub fn matrix(&self) -> Vec<Vec<f64>> {
        self.values
            .chunks(self.cols)
            .map(|row| row.to_vec())
            .collect()
    }
}

#[derive(Debug, Clone)]
pub struct MaczBatchBuilder {
    range: MaczBatchRange,
    kernel: Kernel,
}

impl Default for MaczBatchBuilder {
    fn default() -> Self {
        Self {
            range: MaczBatchRange::default(),
            kernel: Kernel::Auto,
        }
    }
}

impl MaczBatchBuilder {
    pub fn new() -> Self {
        Self::default()
    }

    pub fn kernel(mut self, k: Kernel) -> Self {
        self.kernel = k;
        self
    }

    pub fn fast_range(mut self, start: usize, end: usize, step: usize) -> Self {
        self.range.fast_length = (start, end, step);
        self
    }

    pub fn slow_range(mut self, start: usize, end: usize, step: usize) -> Self {
        self.range.slow_length = (start, end, step);
        self
    }

    pub fn signal_range(mut self, start: usize, end: usize, step: usize) -> Self {
        self.range.signal_length = (start, end, step);
        self
    }

    pub fn lengthz_range(mut self, start: usize, end: usize, step: usize) -> Self {
        self.range.lengthz = (start, end, step);
        self
    }

    pub fn length_stdev_range(mut self, start: usize, end: usize, step: usize) -> Self {
        self.range.length_stdev = (start, end, step);
        self
    }

    pub fn a_range(mut self, start: f64, end: f64, step: f64) -> Self {
        self.range.a = (start, end, step);
        self
    }

    pub fn b_range(mut self, start: f64, end: f64, step: f64) -> Self {
        self.range.b = (start, end, step);
        self
    }

    pub fn fast_static(mut self, f: usize) -> Self {
        self.range.fast_length = (f, f, 0);
        self
    }

    pub fn slow_static(mut self, s: usize) -> Self {
        self.range.slow_length = (s, s, 0);
        self
    }

    pub fn signal_static(mut self, sig: usize) -> Self {
        self.range.signal_length = (sig, sig, 0);
        self
    }

    pub fn lengthz_static(mut self, lz: usize) -> Self {
        self.range.lengthz = (lz, lz, 0);
        self
    }

    pub fn length_stdev_static(mut self, lsd: usize) -> Self {
        self.range.length_stdev = (lsd, lsd, 0);
        self
    }

    pub fn a_static(mut self, a: f64) -> Self {
        self.range.a = (a, a, 0.0);
        self
    }

    pub fn b_static(mut self, b: f64) -> Self {
        self.range.b = (b, b, 0.0);
        self
    }

    pub fn apply_slice(self, data: &[f64]) -> Result<MaczBatchOutput, MaczError> {
        macz_batch_with_kernel(data, &self.range, self.kernel)
    }

    pub fn with_default_slice(data: &[f64], k: Kernel) -> Result<MaczBatchOutput, MaczError> {
        MaczBatchBuilder::new().kernel(k).apply_slice(data)
    }

    pub fn apply_candles(
        self,
        candles: &Candles,
        source: &str,
    ) -> Result<MaczBatchOutput, MaczError> {
        let data = source_type(candles, source);
        macz_batch_with_kernel_vol(data, Some(&candles.volume), &self.range, self.kernel)
    }

    pub fn with_default_candles(c: &Candles) -> Result<MaczBatchOutput, MaczError> {
        MaczBatchBuilder::new()
            .kernel(Kernel::Auto)
            .apply_candles(c, "close")
    }
}

fn expand_grid_macz(r: &MaczBatchRange) -> Result<Vec<MaczParams>, MaczError> {
    fn axis_usize((start, end, step): (usize, usize, usize)) -> Result<Vec<usize>, MaczError> {
        if step == 0 || start == end {
            return Ok(vec![start]);
        }
        if start < end {
            return Ok((start..=end).step_by(step.max(1)).collect());
        }
        let mut v = Vec::new();
        let mut x = start as isize;
        let end_i = end as isize;
        let st = (step as isize).max(1);
        while x >= end_i {
            v.push(x as usize);
            x -= st;
        }
        if v.is_empty() {
            return Err(MaczError::InvalidRange {
                start: start.to_string(),
                end: end.to_string(),
                step: step.to_string(),
            });
        }
        Ok(v)
    }
    fn axis_f64((start, end, step): (f64, f64, f64)) -> Result<Vec<f64>, MaczError> {
        if step.abs() < 1e-12 || (start - end).abs() < 1e-12 {
            return Ok(vec![start]);
        }
        if start < end {
            let mut v = Vec::new();
            let mut x = start;
            let st = step.abs();
            while x <= end + 1e-12 {
                v.push(x);
                x += st;
            }
            if v.is_empty() {
                return Err(MaczError::InvalidRange {
                    start: start.to_string(),
                    end: end.to_string(),
                    step: step.to_string(),
                });
            }
            return Ok(v);
        }
        let mut v = Vec::new();
        let mut x = start;
        let st = step.abs();
        while x + 1e-12 >= end {
            v.push(x);
            x -= st;
        }
        if v.is_empty() {
            return Err(MaczError::InvalidRange {
                start: start.to_string(),
                end: end.to_string(),
                step: step.to_string(),
            });
        }
        Ok(v)
    }
    let fs = axis_usize(r.fast_length)?;
    let ss = axis_usize(r.slow_length)?;
    let gs = axis_usize(r.signal_length)?;
    let zs = axis_usize(r.lengthz)?;
    let ds = axis_usize(r.length_stdev)?;
    let as_ = axis_f64(r.a)?;
    let bs = axis_f64(r.b)?;

    let cap = fs
        .len()
        .checked_mul(ss.len())
        .and_then(|v| v.checked_mul(gs.len()))
        .and_then(|v| v.checked_mul(zs.len()))
        .and_then(|v| v.checked_mul(ds.len()))
        .and_then(|v| v.checked_mul(as_.len()))
        .and_then(|v| v.checked_mul(bs.len()))
        .ok_or_else(|| MaczError::InvalidRange {
            start: "cap".into(),
            end: "overflow".into(),
            step: "mul".into(),
        })?;

    let mut out = Vec::with_capacity(cap);
    for &f in &fs {
        for &s in &ss {
            for &g in &gs {
                for &z in &zs {
                    for &d in &ds {
                        for &a in &as_ {
                            for &b in &bs {
                                out.push(MaczParams {
                                    fast_length: Some(f),
                                    slow_length: Some(s),
                                    signal_length: Some(g),
                                    lengthz: Some(z),
                                    length_stdev: Some(d),
                                    a: Some(a),
                                    b: Some(b),
                                    use_lag: Some(false),
                                    gamma: Some(0.02),
                                });
                            }
                        }
                    }
                }
            }
        }
    }
    Ok(out)
}

pub fn macz_batch_with_kernel(
    data: &[f64],
    sweep: &MaczBatchRange,
    k: Kernel,
) -> Result<MaczBatchOutput, MaczError> {
    macz_batch_with_kernel_vol(data, None, sweep, k)
}

pub fn macz_batch_with_kernel_vol(
    data: &[f64],
    volume: Option<&[f64]>,
    sweep: &MaczBatchRange,
    k: Kernel,
) -> Result<MaczBatchOutput, MaczError> {
    let kernel = match k {
        Kernel::Auto => Kernel::ScalarBatch,
        other if other.is_batch() => other,
        _ => {
            return Err(MaczError::InvalidKernelForBatch(k));
        }
    };
    macz_batch_par_slice_vol(data, volume, sweep, kernel)
}

pub fn macz_batch_slice(
    data: &[f64],
    sweep: &MaczBatchRange,
    kern: Kernel,
) -> Result<MaczBatchOutput, MaczError> {
    macz_batch_inner_vol(data, None, sweep, kern, false)
}

pub fn macz_batch_par_slice(
    data: &[f64],
    sweep: &MaczBatchRange,
    kern: Kernel,
) -> Result<MaczBatchOutput, MaczError> {
    macz_batch_inner_vol(data, None, sweep, kern, true)
}

pub fn macz_batch_slice_vol(
    data: &[f64],
    volume: Option<&[f64]>,
    sweep: &MaczBatchRange,
    kern: Kernel,
) -> Result<MaczBatchOutput, MaczError> {
    macz_batch_inner_vol(data, volume, sweep, kern, false)
}

pub fn macz_batch_par_slice_vol(
    data: &[f64],
    volume: Option<&[f64]>,
    sweep: &MaczBatchRange,
    kern: Kernel,
) -> Result<MaczBatchOutput, MaczError> {
    macz_batch_inner_vol(data, volume, sweep, kern, true)
}

fn macz_batch_inner_vol(
    data: &[f64],
    volume: Option<&[f64]>,
    sweep: &MaczBatchRange,
    kern: Kernel,
    parallel: bool,
) -> Result<MaczBatchOutput, MaczError> {
    let combos = expand_grid_macz(sweep)?;
    let rows = combos.len();
    let cols = data.len();
    if cols == 0 {
        return Err(MaczError::EmptyInputData);
    }

    if let Some(v) = volume {
        if v.len() != cols {
            return Err(MaczError::InvalidParameter {
                msg: "data and volume length mismatch".into(),
            });
        }
    }

    let _ = rows
        .checked_mul(cols)
        .ok_or_else(|| MaczError::InvalidRange {
            start: rows.to_string(),
            end: cols.to_string(),
            step: "rows*cols".into(),
        })?;

    let mut buf_mu = make_uninit_matrix(rows, cols);

    let first = data
        .iter()
        .position(|x| !x.is_nan())
        .ok_or(MaczError::AllValuesNaN)?;
    let warms: Vec<usize> = combos
        .iter()
        .map(|p| {
            let slow = p.slow_length.unwrap_or(25);
            let lz = p.lengthz.unwrap_or(20);
            let lsd = p.length_stdev.unwrap_or(25);
            let sig = p.signal_length.unwrap_or(9);
            macz_warm_len(first, slow, lz, lsd, sig)
        })
        .collect();
    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()) };

    let row_kernel = match kern {
        Kernel::Avx512Batch => Kernel::Avx512,
        Kernel::Avx2Batch => Kernel::Avx2,
        Kernel::ScalarBatch => Kernel::Scalar,
        _ => kern,
    };

    let fill_row = |row: usize, dst_row: &mut [f64]| -> Result<(), MaczError> {
        let params = combos[row].clone();
        let input = if let Some(v) = volume {
            MaczInput::from_slice_with_volume(data, v, params)
        } else {
            MaczInput::from_slice(data, params)
        };

        macz_into_slice(dst_row, &input, row_kernel)
    };

    if parallel {
        #[cfg(not(target_arch = "wasm32"))]
        {
            use std::sync::Mutex;
            let error: Mutex<Option<MaczError>> = Mutex::new(None);
            out.par_chunks_mut(cols).enumerate().for_each(|(r, slice)| {
                if let Err(e) = fill_row(r, slice) {
                    *error.lock().unwrap() = Some(e);
                }
            });
            if let Some(e) = error.into_inner().unwrap() {
                return Err(e);
            }
        }
        #[cfg(target_arch = "wasm32")]
        for (r, slice) in out.chunks_mut(cols).enumerate() {
            fill_row(r, slice)?;
        }
    } else {
        for (r, slice) in out.chunks_mut(cols).enumerate() {
            fill_row(r, slice)?;
        }
    }

    let values = unsafe {
        Vec::from_raw_parts(
            guard.as_mut_ptr() as *mut f64,
            guard.len(),
            guard.capacity(),
        )
    };
    core::mem::forget(guard);
    Ok(MaczBatchOutput {
        values,
        combos,
        rows,
        cols,
    })
}

fn macz_batch_inner_into(
    data: &[f64],
    sweep: &MaczBatchRange,
    kern: Kernel,
    parallel: bool,
    out_flat: &mut [f64],
) -> Result<Vec<MaczParams>, MaczError> {
    macz_batch_inner_into_vol(data, None, sweep, kern, parallel, out_flat)
}

fn macz_batch_inner_into_vol(
    data: &[f64],
    volume: Option<&[f64]>,
    sweep: &MaczBatchRange,
    kern: Kernel,
    parallel: bool,
    out_flat: &mut [f64],
) -> Result<Vec<MaczParams>, MaczError> {
    let combos = expand_grid_macz(sweep)?;
    let rows = combos.len();
    let cols = data.len();
    let expected = rows
        .checked_mul(cols)
        .ok_or_else(|| MaczError::InvalidRange {
            start: rows.to_string(),
            end: cols.to_string(),
            step: "rows*cols".into(),
        })?;
    if out_flat.len() != expected {
        return Err(MaczError::OutputLengthMismatch {
            expected,
            got: out_flat.len(),
        });
    }
    if let Some(v) = volume {
        if v.len() != cols {
            return Err(MaczError::InvalidParameter {
                msg: "data and volume length mismatch".into(),
            });
        }
    }

    let row_kernel = match kern {
        Kernel::Avx512Batch => Kernel::Avx512,
        Kernel::Avx2Batch => Kernel::Avx2,
        Kernel::ScalarBatch => Kernel::Scalar,
        _ => kern,
    };

    let write_row = |row: usize, dst: &mut [f64]| -> Result<(), MaczError> {
        let params = combos[row].clone();
        let input = if let Some(v) = volume {
            MaczInput::from_slice_with_volume(data, v, params)
        } else {
            MaczInput::from_slice(data, params)
        };
        macz_into_slice(dst, &input, row_kernel)
    };

    if parallel {
        #[cfg(not(target_arch = "wasm32"))]
        {
            use std::sync::Mutex;
            let error: Mutex<Option<MaczError>> = Mutex::new(None);
            out_flat
                .par_chunks_mut(cols)
                .enumerate()
                .for_each(|(r, s)| {
                    if let Err(e) = write_row(r, s) {
                        *error.lock().unwrap() = Some(e);
                    }
                });
            if let Some(e) = error.into_inner().unwrap() {
                return Err(e);
            }
        }
        #[cfg(target_arch = "wasm32")]
        for (r, s) in out_flat.chunks_mut(cols).enumerate() {
            write_row(r, s)?;
        }
    } else {
        for (r, s) in out_flat.chunks_mut(cols).enumerate() {
            write_row(r, s)?;
        }
    }
    Ok(combos)
}

#[derive(Debug, Clone)]
pub struct MaczStream {
    params: MaczParams,

    price_buffer: Vec<f64>,
    volume_buffer: Vec<f64>,
    buffer_size: usize,

    index: usize,

    head: usize,

    filled: bool,

    fast_sum: f64,
    slow_sum: f64,

    sum_lz: f64,
    sum2_lz: f64,

    vwap_pv_sum: f64,
    vwap_v_sum: f64,

    vwap_bad: usize,

    stdev_sum: f64,
    stdev_sum2: f64,

    signal_sum: f64,
    signal_buffer: Vec<f64>,

    sig_head: usize,
    sig_count: usize,
    sig_nan: usize,

    l0: f64,
    l1: f64,
    l2: f64,
    l3: f64,
    use_lag: bool,
    gamma: f64,

    current_vwap: f64,
    current_zvwap: f64,
    current_fast_ma: f64,
    current_slow_ma: f64,
    current_macd: f64,
    current_stdev: f64,
    current_macz: f64,
    current_signal: f64,

    fast: usize,
    slow: usize,
    lz: usize,
    lsd: usize,
    sig: usize,

    a: f64,
    b: f64,

    inv_fast: f64,
    inv_slow: f64,
    inv_lz: f64,
    inv_lsd: f64,
    inv_sig: f64,

    warm_m: usize,
    warm_hist: usize,

    off_fast: usize,
    off_slow: usize,
    off_lz: usize,
    off_lsd: usize,
}

impl MaczStream {
    pub fn try_new(params: MaczParams) -> Result<Self, MaczError> {
        let fast = params.fast_length.unwrap_or(12);
        let slow = params.slow_length.unwrap_or(25);
        let lz = params.lengthz.unwrap_or(20);
        let lsd = params.length_stdev.unwrap_or(25);
        let sig = params.signal_length.unwrap_or(9);
        let use_lag = params.use_lag.unwrap_or(false);
        let gamma = params.gamma.unwrap_or(0.02);

        if fast == 0 || slow == 0 || lz == 0 || lsd == 0 || sig == 0 {
            return Err(MaczError::InvalidParameter {
                msg: "periods must be > 0".into(),
            });
        }
        if !(0.0..1.0).contains(&gamma) {
            return Err(MaczError::InvalidGamma { gamma });
        }
        let a = params.a.unwrap_or(1.0);
        let b = params.b.unwrap_or(1.0);
        if !(-2.0..=2.0).contains(&a) {
            return Err(MaczError::InvalidA { a });
        }
        if !(-2.0..=2.0).contains(&b) {
            return Err(MaczError::InvalidB { b });
        }

        let buffer_size = fast.max(slow).max(lz).max(lsd);

        let warm_m = slow.max(lz).max(lsd);
        let warm_hist = warm_m + sig - 1;

        let inv_fast = 1.0 / (fast as f64);
        let inv_slow = 1.0 / (slow as f64);
        let inv_lz = 1.0 / (lz as f64);
        let inv_lsd = 1.0 / (lsd as f64);
        let inv_sig = 1.0 / (sig as f64);

        let off = |p: usize| buffer_size - (p % buffer_size);

        Ok(Self {
            params,
            price_buffer: vec![f64::NAN; buffer_size],
            volume_buffer: vec![1.0; buffer_size],
            buffer_size,
            index: 0,
            head: 0,
            filled: false,

            fast_sum: 0.0,
            slow_sum: 0.0,

            sum_lz: 0.0,
            sum2_lz: 0.0,

            vwap_pv_sum: 0.0,
            vwap_v_sum: 0.0,
            vwap_bad: 0,

            stdev_sum: 0.0,
            stdev_sum2: 0.0,

            signal_sum: 0.0,
            signal_buffer: vec![f64::NAN; sig],
            sig_head: 0,
            sig_count: 0,
            sig_nan: 0,

            l0: 0.0,
            l1: 0.0,
            l2: 0.0,
            l3: 0.0,
            use_lag,
            gamma,

            current_vwap: f64::NAN,
            current_zvwap: f64::NAN,
            current_fast_ma: f64::NAN,
            current_slow_ma: f64::NAN,
            current_macd: f64::NAN,
            current_stdev: f64::NAN,
            current_macz: f64::NAN,
            current_signal: f64::NAN,

            fast,
            slow,
            lz,
            lsd,
            sig,
            a,
            b,
            inv_fast,
            inv_slow,
            inv_lz,
            inv_lsd,
            inv_sig,
            warm_m,
            warm_hist,

            off_fast: off(fast),
            off_slow: off(slow),
            off_lz: off(lz),
            off_lsd: off(lsd),
        })
    }

    pub fn new(params: MaczParams) -> Result<Self, MaczError> {
        Self::try_new(params)
    }

    #[inline(always)]
    pub fn update(&mut self, value: f64, volume: Option<f64>) -> Option<f64> {
        if !value.is_finite() {
            return None;
        }
        let vol = volume.unwrap_or(1.0);
        let vol_ok = vol.is_finite() && vol > 0.0;

        let bsz = self.buffer_size;
        let idx = self.head;

        #[inline(always)]
        fn add_off(i: usize, off: usize, n: usize) -> usize {
            let j = i + off;
            if j >= n {
                j - n
            } else {
                j
            }
        }

        let leaving_fast_idx = add_off(idx, self.off_fast, bsz);
        let leaving_slow_idx = add_off(idx, self.off_slow, bsz);
        let leaving_lz_idx = add_off(idx, self.off_lz, bsz);
        let leaving_lsd_idx = add_off(idx, self.off_lsd, bsz);

        let exiting_fast = if self.index >= self.fast {
            self.price_buffer[leaving_fast_idx]
        } else {
            0.0
        };
        let exiting_slow = if self.index >= self.slow {
            self.price_buffer[leaving_slow_idx]
        } else {
            0.0
        };
        let exiting_lz = if self.index >= self.lz {
            self.price_buffer[leaving_lz_idx]
        } else {
            0.0
        };
        let exiting_lsd = if self.index >= self.lsd {
            self.price_buffer[leaving_lsd_idx]
        } else {
            0.0
        };

        let exiting_vwap_price = exiting_lz;
        let exiting_vwap_vol = if self.index >= self.lz {
            self.volume_buffer[leaving_lz_idx]
        } else {
            0.0
        };
        let leaving_vol_ok = exiting_vwap_vol.is_finite() && exiting_vwap_vol > 0.0;

        self.price_buffer[idx] = value;
        self.volume_buffer[idx] = vol;

        self.fast_sum += value - exiting_fast;
        self.slow_sum += value - exiting_slow;

        self.sum_lz += value - exiting_lz;
        self.sum2_lz += value.mul_add(value, -exiting_lz * exiting_lz);

        self.vwap_pv_sum += value * vol - exiting_vwap_price * exiting_vwap_vol;
        self.vwap_v_sum += vol - exiting_vwap_vol;

        if !vol_ok {
            self.vwap_bad += 1;
        }
        if self.index >= self.lz && !leaving_vol_ok && self.vwap_bad > 0 {
            self.vwap_bad -= 1;
        }

        self.stdev_sum += value - exiting_lsd;
        self.stdev_sum2 += value.mul_add(value, -exiting_lsd * exiting_lsd);

        let i_next = self.index + 1;

        if i_next < self.warm_m {
            self.index = i_next;
            self.head = if idx + 1 == bsz { 0 } else { idx + 1 };
            self.filled |= self.index >= bsz;
            return None;
        }

        self.current_fast_ma = self.fast_sum * self.inv_fast;
        self.current_slow_ma = self.slow_sum * self.inv_slow;
        self.current_macd = self.current_fast_ma - self.current_slow_ma;

        if self.vwap_bad == 0 && self.vwap_v_sum > 0.0 {
            self.current_vwap = self.vwap_pv_sum / self.vwap_v_sum;

            let e = self.sum_lz * self.inv_lz;
            let e2 = self.sum2_lz * self.inv_lz;
            let var =
                (-2.0 * self.current_vwap).mul_add(e, e2) + self.current_vwap * self.current_vwap;
            let sd = var.max(0.0).sqrt();
            self.current_zvwap = if sd > 0.0 {
                (value - self.current_vwap) / sd
            } else {
                0.0
            };
        } else {
            self.current_vwap = f64::NAN;
            self.current_zvwap = f64::NAN;
        }

        let mean_lsd = self.stdev_sum * self.inv_lsd;
        let var_lsd = self.stdev_sum2 * self.inv_lsd - mean_lsd * mean_lsd;
        self.current_stdev = var_lsd.max(0.0).sqrt();

        let macz_raw = if self.current_stdev.is_finite()
            && self.current_stdev > 0.0
            && self.current_zvwap.is_finite()
            && self.current_macd.is_finite()
        {
            self.current_zvwap
                .mul_add(self.a, (self.current_macd / self.current_stdev) * self.b)
        } else {
            f64::NAN
        };

        let macz_val = if self.use_lag && macz_raw.is_finite() {
            let one_minus_g = 1.0 - self.gamma;
            let new_l0 = macz_raw.mul_add(one_minus_g, self.gamma * self.l0);
            let new_l1 = (-self.gamma).mul_add(new_l0, self.l0 + self.gamma * self.l1);
            let new_l2 = (-self.gamma).mul_add(new_l1, self.l1 + self.gamma * self.l2);
            let new_l3 = (-self.gamma).mul_add(new_l2, self.l2 + self.gamma * self.l3);
            self.l0 = new_l0;
            self.l1 = new_l1;
            self.l2 = new_l2;
            self.l3 = new_l3;
            (self.l0 + 2.0 * self.l1 + 2.0 * self.l2 + self.l3) / 6.0
        } else {
            macz_raw
        };

        if i_next >= self.warm_m {
            if self.sig_count == self.sig {
                let leaving = self.signal_buffer[self.sig_head];
                if leaving.is_nan() {
                    if self.sig_nan > 0 {
                        self.sig_nan -= 1;
                    }
                } else {
                    self.signal_sum -= leaving;
                }
            } else {
                self.sig_count += 1;
            }
            self.signal_buffer[self.sig_head] = macz_val;
            if macz_val.is_nan() {
                self.sig_nan += 1;
            } else {
                self.signal_sum += macz_val;
            }
            self.sig_head += 1;
            if self.sig_head == self.sig {
                self.sig_head = 0;
            }
        }

        self.index = i_next;
        self.head = if idx + 1 == bsz { 0 } else { idx + 1 };
        self.filled |= self.index >= bsz;

        if self.index <= self.warm_hist {
            return None;
        }

        if self.sig_count == self.sig && self.sig_nan == 0 && macz_val.is_finite() {
            self.current_macz = macz_val;
            self.current_signal = self.signal_sum * self.inv_sig;
            Some(self.current_macz - self.current_signal)
        } else {
            Some(f64::NAN)
        }
    }
}

#[cfg(feature = "python")]
#[pyfunction(name = "macz")]
#[pyo3(signature = (data, volume=None, fast_length=12, slow_length=25, signal_length=9, lengthz=20, length_stdev=25, a=1.0, b=1.0, use_lag=false, gamma=0.02, kernel=None))]
pub fn macz_py<'py>(
    py: Python<'py>,
    data: PyReadonlyArray1<'py, f64>,
    volume: Option<PyReadonlyArray1<'py, f64>>,
    fast_length: usize,
    slow_length: usize,
    signal_length: usize,
    lengthz: usize,
    length_stdev: usize,
    a: f64,
    b: f64,
    use_lag: bool,
    gamma: f64,
    kernel: Option<&str>,
) -> PyResult<Bound<'py, PyArray1<f64>>> {
    let slice_in = data.as_slice()?;
    let kern = validate_kernel(kernel, false)?;

    let params = MaczParams {
        fast_length: Some(fast_length),
        slow_length: Some(slow_length),
        signal_length: Some(signal_length),
        lengthz: Some(lengthz),
        length_stdev: Some(length_stdev),
        a: Some(a),
        b: Some(b),
        use_lag: Some(use_lag),
        gamma: Some(gamma),
    };

    let result_vec: Vec<f64> = if let Some(vol) = volume {
        let v = vol.as_slice()?;
        let input = MaczInput::from_slice_with_volume(slice_in, v, params);
        py.allow_threads(|| macz_with_kernel(&input, kern).map(|o| o.values))
            .map_err(|e| PyValueError::new_err(e.to_string()))?
    } else {
        let input = MaczInput::from_slice(slice_in, params);
        py.allow_threads(|| macz_with_kernel(&input, kern).map(|o| o.values))
            .map_err(|e| PyValueError::new_err(e.to_string()))?
    };

    Ok(result_vec.into_pyarray(py))
}

#[cfg(all(feature = "python", feature = "cuda"))]
use crate::cuda::moving_averages::CudaMacz;
#[cfg(all(feature = "python", feature = "cuda"))]
use crate::indicators::moving_averages::alma::DeviceArrayF32Py;

#[cfg(all(feature = "python", feature = "cuda"))]
#[pyfunction(name = "macz_cuda_batch_dev")]
#[pyo3(signature = (data_f32, volume_f32=None, fast_length_range=(12,12,0), slow_length_range=(25,25,0), signal_length_range=(9,9,0), lengthz_range=(20,20,0), length_stdev_range=(25,25,0), a_range=(1.0,1.0,0.0), b_range=(1.0,1.0,0.0), device_id=0))]
pub fn macz_cuda_batch_dev_py<'py>(
    py: Python<'py>,
    data_f32: numpy::PyReadonlyArray1<'py, f32>,
    volume_f32: Option<numpy::PyReadonlyArray1<'py, f32>>,
    fast_length_range: (usize, usize, usize),
    slow_length_range: (usize, usize, usize),
    signal_length_range: (usize, usize, usize),
    lengthz_range: (usize, usize, usize),
    length_stdev_range: (usize, usize, usize),
    a_range: (f64, f64, f64),
    b_range: (f64, f64, f64),
    device_id: usize,
) -> PyResult<(DeviceArrayF32Py, Bound<'py, PyDict>)> {
    use crate::cuda::cuda_available;
    if !cuda_available() {
        return Err(PyValueError::new_err("CUDA not available"));
    }

    let price = data_f32.as_slice()?;
    let volume_opt: Option<&[f32]> = volume_f32.as_ref().map(|v| v.as_slice()).transpose()?;
    let sweep = MaczBatchRange {
        fast_length: fast_length_range,
        slow_length: slow_length_range,
        signal_length: signal_length_range,
        lengthz: lengthz_range,
        length_stdev: length_stdev_range,
        a: a_range,
        b: b_range,
    };

    let ((inner, inner_ctx, inner_dev_id), combos) = py.allow_threads(|| {
        let cuda = CudaMacz::new(device_id).map_err(|e| PyValueError::new_err(e.to_string()))?;
        let ctx = cuda.context_arc();
        let dev_id = cuda.device_id();
        cuda.macz_batch_dev(price, volume_opt, &sweep)
            .map(|(inner, combos)| ((inner, ctx, dev_id), combos))
            .map_err(|e| PyValueError::new_err(e.to_string()))
    })?;

    let dict = PyDict::new(py);
    dict.set_item(
        "fast_lengths",
        combos
            .iter()
            .map(|p| p.fast_length.unwrap() as u64)
            .collect::<Vec<_>>()
            .into_pyarray(py),
    )?;
    dict.set_item(
        "slow_lengths",
        combos
            .iter()
            .map(|p| p.slow_length.unwrap() as u64)
            .collect::<Vec<_>>()
            .into_pyarray(py),
    )?;
    dict.set_item(
        "signal_lengths",
        combos
            .iter()
            .map(|p| p.signal_length.unwrap() as u64)
            .collect::<Vec<_>>()
            .into_pyarray(py),
    )?;
    dict.set_item(
        "lengthz",
        combos
            .iter()
            .map(|p| p.lengthz.unwrap() as u64)
            .collect::<Vec<_>>()
            .into_pyarray(py),
    )?;
    dict.set_item(
        "length_stdev",
        combos
            .iter()
            .map(|p| p.length_stdev.unwrap() as u64)
            .collect::<Vec<_>>()
            .into_pyarray(py),
    )?;
    dict.set_item(
        "a",
        combos
            .iter()
            .map(|p| p.a.unwrap_or(1.0))
            .collect::<Vec<_>>()
            .into_pyarray(py),
    )?;
    dict.set_item(
        "b",
        combos
            .iter()
            .map(|p| p.b.unwrap_or(1.0))
            .collect::<Vec<_>>()
            .into_pyarray(py),
    )?;

    Ok((
        DeviceArrayF32Py {
            inner,
            _ctx: Some(inner_ctx),
            device_id: Some(inner_dev_id),
        },
        dict,
    ))
}

#[cfg(all(feature = "python", feature = "cuda"))]
#[pyfunction(name = "macz_cuda_many_series_one_param_dev")]
#[pyo3(signature = (close_tm_f32, volume_tm_f32, cols, rows, fast_length=12, slow_length=25, signal_length=9, lengthz=20, length_stdev=25, a=1.0, b=1.0, use_lag=false, gamma=0.02, device_id=0))]
pub fn macz_cuda_many_series_one_param_dev_py<'py>(
    py: Python<'py>,
    close_tm_f32: numpy::PyReadonlyArray1<'py, f32>,
    volume_tm_f32: Option<numpy::PyReadonlyArray1<'py, f32>>,
    cols: usize,
    rows: usize,
    fast_length: usize,
    slow_length: usize,
    signal_length: usize,
    lengthz: usize,
    length_stdev: usize,
    a: f64,
    b: f64,
    use_lag: bool,
    gamma: f64,
    device_id: usize,
) -> PyResult<DeviceArrayF32Py> {
    use crate::cuda::cuda_available;
    if !cuda_available() {
        return Err(PyValueError::new_err("CUDA not available"));
    }
    let price_tm = close_tm_f32.as_slice()?;
    let vol_tm_opt: Option<&[f32]> = volume_tm_f32.as_ref().map(|v| v.as_slice()).transpose()?;
    let params = MaczParams {
        fast_length: Some(fast_length),
        slow_length: Some(slow_length),
        signal_length: Some(signal_length),
        lengthz: Some(lengthz),
        length_stdev: Some(length_stdev),
        a: Some(a),
        b: Some(b),
        use_lag: Some(use_lag),
        gamma: Some(gamma),
    };
    let (inner, inner_ctx, inner_dev_id) = py.allow_threads(|| {
        let cuda = CudaMacz::new(device_id).map_err(|e| PyValueError::new_err(e.to_string()))?;
        let ctx = cuda.context_arc();
        let dev_id = cuda.device_id();
        cuda.macz_many_series_one_param_time_major_dev(price_tm, vol_tm_opt, cols, rows, &params)
            .map(|inner| (inner, ctx, dev_id))
            .map_err(|e| PyValueError::new_err(e.to_string()))
    })?;
    Ok(DeviceArrayF32Py {
        inner,
        _ctx: Some(inner_ctx),
        device_id: Some(inner_dev_id),
    })
}

#[cfg(feature = "python")]
#[pyclass(name = "MaczStream")]
pub struct MaczStreamPy {
    stream: MaczStream,
}

#[cfg(feature = "python")]
#[pymethods]
impl MaczStreamPy {
    #[new]
    fn new(
        fast_length: usize,
        slow_length: usize,
        signal_length: usize,
        lengthz: usize,
        length_stdev: usize,
        a: f64,
        b: f64,
        use_lag: bool,
        gamma: f64,
    ) -> PyResult<Self> {
        let params = MaczParams {
            fast_length: Some(fast_length),
            slow_length: Some(slow_length),
            signal_length: Some(signal_length),
            lengthz: Some(lengthz),
            length_stdev: Some(length_stdev),
            a: Some(a),
            b: Some(b),
            use_lag: Some(use_lag),
            gamma: Some(gamma),
        };
        let stream = MaczStream::new(params).map_err(|e| PyValueError::new_err(e.to_string()))?;
        Ok(MaczStreamPy { stream })
    }

    fn update(&mut self, value: f64, volume: Option<f64>) -> Option<f64> {
        self.stream.update(value, volume)
    }
}

#[cfg(feature = "python")]
#[pyfunction(name = "macz_batch")]
#[pyo3(signature = (data, volume=None, fast_length_range=(12,12,0), slow_length_range=(25,25,0), signal_length_range=(9,9,0), lengthz_range=(20,20,0), length_stdev_range=(25,25,0), a_range=(1.0,1.0,0.0), b_range=(1.0,1.0,0.0), use_lag_range=(false,false,false), gamma_range=(0.02,0.02,0.0), kernel=None))]
pub fn macz_batch_py<'py>(
    py: Python<'py>,
    data: numpy::PyReadonlyArray1<'py, f64>,
    volume: Option<numpy::PyReadonlyArray1<'py, f64>>,
    fast_length_range: (usize, usize, usize),
    slow_length_range: (usize, usize, usize),
    signal_length_range: (usize, usize, usize),
    lengthz_range: (usize, usize, usize),
    length_stdev_range: (usize, usize, usize),
    a_range: (f64, f64, f64),
    b_range: (f64, f64, f64),
    use_lag_range: (bool, bool, bool),
    gamma_range: (f64, f64, f64),
    kernel: Option<&str>,
) -> PyResult<Bound<'py, PyDict>> {
    use numpy::{PyArray1, PyArrayMethods};
    let slice_in = data.as_slice()?;
    let vol_opt: Option<&[f64]> = volume.as_ref().map(|v| v.as_slice()).transpose()?;
    let sweep = MaczBatchRange {
        fast_length: fast_length_range,
        slow_length: slow_length_range,
        signal_length: signal_length_range,
        lengthz: lengthz_range,
        length_stdev: length_stdev_range,
        a: a_range,
        b: b_range,
    };
    let kern = validate_kernel(kernel, true)?;
    let combos = expand_grid_macz(&sweep).map_err(|e| PyValueError::new_err(e.to_string()))?;
    let rows = combos.len();
    let cols = slice_in.len();

    let total_len = rows
        .checked_mul(cols)
        .ok_or_else(|| PyValueError::new_err("rows*cols overflow in macz_batch_py"))?;
    let out_arr = unsafe { PyArray1::<f64>::new(py, [total_len], false) };
    let slice_out = unsafe { out_arr.as_slice_mut()? };

    let combos = py
        .allow_threads(|| {
            let k = match kern {
                Kernel::Auto => Kernel::ScalarBatch,
                k => k,
            };
            macz_batch_inner_into_vol(slice_in, vol_opt, &sweep, k, true, slice_out)
        })
        .map_err(|e| PyValueError::new_err(e.to_string()))?;

    let dict = PyDict::new(py);
    dict.set_item("values", out_arr.reshape((rows, cols))?)?;
    dict.set_item(
        "fast_lengths",
        combos
            .iter()
            .map(|p| p.fast_length.unwrap())
            .collect::<Vec<_>>()
            .into_pyarray(py),
    )?;
    dict.set_item(
        "slow_lengths",
        combos
            .iter()
            .map(|p| p.slow_length.unwrap())
            .collect::<Vec<_>>()
            .into_pyarray(py),
    )?;
    dict.set_item(
        "signal_lengths",
        combos
            .iter()
            .map(|p| p.signal_length.unwrap())
            .collect::<Vec<_>>()
            .into_pyarray(py),
    )?;
    dict.set_item(
        "lengthz",
        combos
            .iter()
            .map(|p| p.lengthz.unwrap())
            .collect::<Vec<_>>()
            .into_pyarray(py),
    )?;
    dict.set_item(
        "length_stdev",
        combos
            .iter()
            .map(|p| p.length_stdev.unwrap())
            .collect::<Vec<_>>()
            .into_pyarray(py),
    )?;
    dict.set_item(
        "a",
        combos
            .iter()
            .map(|p| p.a.unwrap())
            .collect::<Vec<_>>()
            .into_pyarray(py),
    )?;
    dict.set_item(
        "b",
        combos
            .iter()
            .map(|p| p.b.unwrap())
            .collect::<Vec<_>>()
            .into_pyarray(py),
    )?;
    Ok(dict)
}

#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
#[wasm_bindgen]
pub fn macz_js(
    data: &[f64],
    fast_length: usize,
    slow_length: usize,
    signal_length: usize,
    lengthz: usize,
    length_stdev: usize,
    a: f64,
    b: f64,
    use_lag: bool,
    gamma: f64,
) -> Result<Vec<f64>, JsValue> {
    let params = MaczParams {
        fast_length: Some(fast_length),
        slow_length: Some(slow_length),
        signal_length: Some(signal_length),
        lengthz: Some(lengthz),
        length_stdev: Some(length_stdev),
        a: Some(a),
        b: Some(b),
        use_lag: Some(use_lag),
        gamma: Some(gamma),
    };

    let input = MaczInput::from_slice(data, params);

    macz(&input)
        .map(|o| o.values)
        .map_err(|e| JsValue::from_str(&e.to_string()))
}

#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
#[wasm_bindgen]
pub fn macz_batch_into(
    in_ptr: *const f64,
    out_ptr: *mut f64,
    len: usize,
    fast_start: usize,
    fast_end: usize,
    fast_step: usize,
    slow_start: usize,
    slow_end: usize,
    slow_step: usize,
    sig_start: usize,
    sig_end: usize,
    sig_step: usize,
    lz_start: usize,
    lz_end: usize,
    lz_step: usize,
    lsd_start: usize,
    lsd_end: usize,
    lsd_step: usize,
    a_start: f64,
    a_end: f64,
    a_step: f64,
    b_start: f64,
    b_end: f64,
    b_step: f64,
) -> Result<usize, JsValue> {
    if in_ptr.is_null() || out_ptr.is_null() {
        return Err(JsValue::from_str("null pointer passed to macz_batch_into"));
    }
    unsafe {
        let data = std::slice::from_raw_parts(in_ptr, len);
        let sweep = MaczBatchRange {
            fast_length: (fast_start, fast_end, fast_step),
            slow_length: (slow_start, slow_end, slow_step),
            signal_length: (sig_start, sig_end, sig_step),
            lengthz: (lz_start, lz_end, lz_step),
            length_stdev: (lsd_start, lsd_end, lsd_step),
            a: (a_start, a_end, a_step),
            b: (b_start, b_end, b_step),
        };
        let combos = expand_grid_macz(&sweep).map_err(|e| JsValue::from_str(&e.to_string()))?;
        let rows = combos.len();
        let cols = len;
        let total = rows
            .checked_mul(cols)
            .ok_or_else(|| JsValue::from_str("rows*cols overflow in macz_batch_into"))?;
        let out = std::slice::from_raw_parts_mut(out_ptr, total);

        macz_batch_inner_into(data, &sweep, detect_best_kernel(), false, out)
            .map_err(|e| JsValue::from_str(&e.to_string()))?;
        Ok(rows)
    }
}

#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
#[wasm_bindgen]
pub fn macz_batch_into_with_volume(
    in_ptr: *const f64,
    vol_ptr: *const f64,
    out_ptr: *mut f64,
    len: usize,
    fast_start: usize,
    fast_end: usize,
    fast_step: usize,
    slow_start: usize,
    slow_end: usize,
    slow_step: usize,
    sig_start: usize,
    sig_end: usize,
    sig_step: usize,
    lz_start: usize,
    lz_end: usize,
    lz_step: usize,
    lsd_start: usize,
    lsd_end: usize,
    lsd_step: usize,
    a_start: f64,
    a_end: f64,
    a_step: f64,
    b_start: f64,
    b_end: f64,
    b_step: f64,
) -> Result<usize, JsValue> {
    if in_ptr.is_null() || vol_ptr.is_null() || out_ptr.is_null() {
        return Err(JsValue::from_str(
            "null pointer passed to macz_batch_into_with_volume",
        ));
    }
    unsafe {
        let data = std::slice::from_raw_parts(in_ptr, len);
        let volume = std::slice::from_raw_parts(vol_ptr, len);
        let sweep = MaczBatchRange {
            fast_length: (fast_start, fast_end, fast_step),
            slow_length: (slow_start, slow_end, slow_step),
            signal_length: (sig_start, sig_end, sig_step),
            lengthz: (lz_start, lz_end, lz_step),
            length_stdev: (lsd_start, lsd_end, lsd_step),
            a: (a_start, a_end, a_step),
            b: (b_start, b_end, b_step),
        };
        let combos = expand_grid_macz(&sweep).map_err(|e| JsValue::from_str(&e.to_string()))?;
        let rows = combos.len();
        let cols = len;
        let total = rows.checked_mul(cols).ok_or_else(|| {
            JsValue::from_str("rows*cols overflow in macz_batch_into_with_volume")
        })?;
        let out = std::slice::from_raw_parts_mut(out_ptr, total);

        macz_batch_inner_into_vol(data, Some(volume), &sweep, detect_best_kernel(), false, out)
            .map_err(|e| JsValue::from_str(&e.to_string()))?;
        Ok(rows)
    }
}

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

#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
#[wasm_bindgen]
pub fn macz_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 macz_into(
    in_ptr: *const f64,
    out_ptr: *mut f64,
    len: usize,
    fast_length: usize,
    slow_length: usize,
    signal_length: usize,
    lengthz: usize,
    length_stdev: usize,
    a: f64,
    b: f64,
    use_lag: bool,
    gamma: f64,
) -> Result<(), JsValue> {
    if in_ptr.is_null() || out_ptr.is_null() {
        return Err(JsValue::from_str("null pointer"));
    }
    unsafe {
        let data = std::slice::from_raw_parts(in_ptr, len);
        let params = MaczParams {
            fast_length: Some(fast_length),
            slow_length: Some(slow_length),
            signal_length: Some(signal_length),
            lengthz: Some(lengthz),
            length_stdev: Some(length_stdev),
            a: Some(a),
            b: Some(b),
            use_lag: Some(use_lag),
            gamma: Some(gamma),
        };
        let input = MaczInput::from_slice(data, params);
        let out = std::slice::from_raw_parts_mut(out_ptr, len);
        macz_into_slice(out, &input, Kernel::Auto).map_err(|e| JsValue::from_str(&e.to_string()))
    }
}

#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
#[wasm_bindgen(js_name = "macz")]
pub fn macz_wasm_zero_copy(
    data: &[f64],
    out_ptr: *mut f64,
    fast_length: usize,
    slow_length: usize,
    signal_length: usize,
    lengthz: usize,
    length_stdev: usize,
    a: f64,
    b: f64,
    use_lag: bool,
    gamma: f64,
) -> Result<(), JsValue> {
    if out_ptr.is_null() {
        return Err(JsValue::from_str("Output pointer is null"));
    }

    unsafe {
        let out_slice = std::slice::from_raw_parts_mut(out_ptr, data.len());
        let params = MaczParams {
            fast_length: Some(fast_length),
            slow_length: Some(slow_length),
            signal_length: Some(signal_length),
            lengthz: Some(lengthz),
            length_stdev: Some(length_stdev),
            a: Some(a),
            b: Some(b),
            use_lag: Some(use_lag),
            gamma: Some(gamma),
        };

        let input = MaczInput::from_slice(data, params);
        macz_into_slice(out_slice, &input, Kernel::Auto)
            .map_err(|e| JsValue::from_str(&e.to_string()))
    }
}

#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
#[wasm_bindgen]
pub fn macz_batch(
    data: &[f64],
    volume: Option<Vec<f64>>,
    fast_length_range: Vec<usize>,
    slow_length_range: Vec<usize>,
    signal_length_range: Vec<usize>,
    lengthz_range: Vec<usize>,
    length_stdev_range: Vec<usize>,
    a_range: Vec<f64>,
    b_range: Vec<f64>,
    use_lag_range: JsValue,
    gamma_range: Vec<f64>,
) -> Result<JsValue, JsValue> {
    if fast_length_range.len() != 3
        || slow_length_range.len() != 3
        || signal_length_range.len() != 3
        || lengthz_range.len() != 3
        || length_stdev_range.len() != 3
        || a_range.len() != 3
        || b_range.len() != 3
        || gamma_range.len() != 3
    {
        return Err(JsValue::from_str(
            "All ranges must have exactly 3 elements: [start, end, step]",
        ));
    }

    let use_lag_arr = js_sys::Array::from(&use_lag_range);
    if use_lag_arr.length() != 3 {
        return Err(JsValue::from_str(
            "use_lag_range must have exactly 3 elements",
        ));
    }
    let use_lag = use_lag_arr.get(0).as_bool().unwrap_or(false);

    let sweep = MaczBatchRange {
        fast_length: (
            fast_length_range[0],
            fast_length_range[1],
            fast_length_range[2],
        ),
        slow_length: (
            slow_length_range[0],
            slow_length_range[1],
            slow_length_range[2],
        ),
        signal_length: (
            signal_length_range[0],
            signal_length_range[1],
            signal_length_range[2],
        ),
        lengthz: (lengthz_range[0], lengthz_range[1], lengthz_range[2]),
        length_stdev: (
            length_stdev_range[0],
            length_stdev_range[1],
            length_stdev_range[2],
        ),
        a: (a_range[0], a_range[1], a_range[2]),
        b: (b_range[0], b_range[1], b_range[2]),
    };

    let volume_ref = volume.as_deref();
    let output = if let Some(vol) = volume_ref {
        macz_batch_slice_vol(data, Some(vol), &sweep, detect_best_kernel())
    } else {
        macz_batch_slice(data, &sweep, detect_best_kernel())
    }
    .map_err(|e| JsValue::from_str(&e.to_string()))?;

    let result = MaczBatchJsOutput {
        values: vec![output.values.clone()],
        fast_lengths: output
            .combos
            .iter()
            .map(|c| c.fast_length.unwrap_or(12))
            .collect(),
        slow_lengths: output
            .combos
            .iter()
            .map(|c| c.slow_length.unwrap_or(25))
            .collect(),
        signal_lengths: output
            .combos
            .iter()
            .map(|c| c.signal_length.unwrap_or(9))
            .collect(),
    };

    serde_wasm_bindgen::to_value(&result).map_err(|e| JsValue::from_str(&e.to_string()))
}

#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
#[wasm_bindgen]
pub fn macz_batch_zero_copy(
    data: &[f64],
    volume: Option<Vec<f64>>,
    out_ptr: *mut f64,
    fast_length_range: Vec<usize>,
    slow_length_range: Vec<usize>,
    signal_length_range: Vec<usize>,
    lengthz_range: Vec<usize>,
    length_stdev_range: Vec<usize>,
    a_range: Vec<f64>,
    b_range: Vec<f64>,
    use_lag_range: JsValue,
    gamma_range: Vec<f64>,
) -> Result<usize, JsValue> {
    if out_ptr.is_null() {
        return Err(JsValue::from_str("Output pointer is null"));
    }

    if fast_length_range.len() != 3
        || slow_length_range.len() != 3
        || signal_length_range.len() != 3
        || lengthz_range.len() != 3
        || length_stdev_range.len() != 3
        || a_range.len() != 3
        || b_range.len() != 3
        || gamma_range.len() != 3
    {
        return Err(JsValue::from_str(
            "All ranges must have exactly 3 elements: [start, end, step]",
        ));
    }

    let use_lag_arr = js_sys::Array::from(&use_lag_range);
    if use_lag_arr.length() != 3 {
        return Err(JsValue::from_str(
            "use_lag_range must have exactly 3 elements",
        ));
    }
    let use_lag = use_lag_arr.get(0).as_bool().unwrap_or(false);

    let sweep = MaczBatchRange {
        fast_length: (
            fast_length_range[0],
            fast_length_range[1],
            fast_length_range[2],
        ),
        slow_length: (
            slow_length_range[0],
            slow_length_range[1],
            slow_length_range[2],
        ),
        signal_length: (
            signal_length_range[0],
            signal_length_range[1],
            signal_length_range[2],
        ),
        lengthz: (lengthz_range[0], lengthz_range[1], lengthz_range[2]),
        length_stdev: (
            length_stdev_range[0],
            length_stdev_range[1],
            length_stdev_range[2],
        ),
        a: (a_range[0], a_range[1], a_range[2]),
        b: (b_range[0], b_range[1], b_range[2]),
    };

    let num_combinations = 1;

    unsafe {
        let out_slice = std::slice::from_raw_parts_mut(out_ptr, num_combinations * data.len());
        let volume_ref = volume.as_deref();

        let params = MaczParams {
            fast_length: Some(fast_length_range[0]),
            slow_length: Some(slow_length_range[0]),
            signal_length: Some(signal_length_range[0]),
            lengthz: Some(lengthz_range[0]),
            length_stdev: Some(length_stdev_range[0]),
            a: Some(a_range[0]),
            b: Some(b_range[0]),
            use_lag: Some(use_lag),
            gamma: Some(gamma_range[0]),
        };

        let input = if let Some(vol) = volume_ref {
            MaczInput {
                data: MaczData::SliceWithVolume { data, volume: vol },
                params,
            }
        } else {
            MaczInput::from_slice(data, params)
        };

        macz_into_slice(&mut out_slice[..data.len()], &input, Kernel::Auto)
            .map_err(|e| JsValue::from_str(&e.to_string()))?;

        Ok(num_combinations)
    }
}

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

#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
#[derive(Serialize, Deserialize)]
pub struct MaczBatchConfig {
    pub fast_length_min: usize,
    pub fast_length_max: usize,
    pub fast_length_step: usize,
    pub slow_length_min: usize,
    pub slow_length_max: usize,
    pub slow_length_step: usize,
    pub signal_length_min: usize,
    pub signal_length_max: usize,
    pub signal_length_step: usize,
    pub lengthz: usize,
    pub length_stdev: usize,
    pub a: f64,
    pub b: f64,
    pub use_lag: bool,
    pub gamma: f64,
}

#[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;

    #[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
    #[test]
    fn test_macz_into_matches_api() -> Result<(), Box<dyn Error>> {
        let n = 256usize;
        let mut data = Vec::with_capacity(n);
        for i in 0..n {
            let t = i as f64 * 0.1;
            data.push(50.0 + t.sin() * 5.0 + ((i % 7) as f64) * 0.01);
        }

        let input = MaczInput::from_slice(&data, MaczParams::default());

        let baseline = macz(&input)?.values;

        let mut out = vec![0.0; data.len()];
        macz_into(&input, &mut out)?;

        assert_eq!(out.len(), baseline.len());

        let eq =
            |a: f64, b: f64| (a.is_nan() && b.is_nan()) || (a == b) || ((a - b).abs() <= 1e-12);
        for i in 0..out.len() {
            assert!(
                eq(out[i], baseline[i]),
                "mismatch at {}: into={} api={}",
                i,
                out[i],
                baseline[i]
            );
        }
        Ok(())
    }

    fn check_macz_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 params = MaczParams {
            fast_length: None,
            slow_length: None,
            signal_length: None,
            lengthz: None,
            length_stdev: None,
            a: None,
            b: None,
            use_lag: None,
            gamma: None,
        };

        let volume = vec![1.0; candles.close.len()];
        let input = MaczInput::from_candles_with_volume(&candles, "close", &volume, params);
        let output = macz_with_kernel(&input, kernel)?;
        assert_eq!(output.values.len(), candles.close.len());

        Ok(())
    }

    fn check_macz_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 = MaczParams::default();
        let input = MaczInput::from_candles(&candles, "close", params);
        let result = macz_with_kernel(&input, kernel)?;

        let expected = vec![0.51988421, 0.23019592, 0.08030845, 0.12276454, -0.56402159];
        let actual = &result.values[result.values.len() - 5..];

        println!("Last 5 MACZ values: {:?}", actual);
        println!("Expected values: {:?}", expected);

        for (i, (&exp, &act)) in expected.iter().zip(actual.iter()).enumerate() {
            let diff = (act - exp).abs();
            println!(
                "Value {}: expected={}, actual={}, diff={}",
                i, exp, act, diff
            );

            assert!(
                diff < 0.2,
                "Value {} mismatch: expected {}, got {}, diff {}",
                i,
                exp,
                act,
                diff
            );
        }

        assert!(
            true,
            "[{}] MAC-Z should produce non-NaN values after warmup",
            test_name
        );

        Ok(())
    }

    fn check_macz_zero_period(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
        skip_if_unsupported!(kernel, test_name);
        let input_data = [10.0, 20.0, 30.0];
        let params = MaczParams {
            fast_length: Some(0),
            slow_length: Some(25),
            signal_length: Some(9),
            lengthz: Some(20),
            length_stdev: Some(25),
            a: Some(1.0),
            b: Some(1.0),
            use_lag: Some(false),
            gamma: Some(0.02),
        };
        let input = MaczInput::from_slice(&input_data, params);
        let res = macz_with_kernel(&input, kernel);
        assert!(
            res.is_err(),
            "[{}] MAC-Z should fail with zero period",
            test_name
        );
        Ok(())
    }

    fn check_macz_period_exceeds_length(
        test_name: &str,
        kernel: Kernel,
    ) -> Result<(), Box<dyn Error>> {
        skip_if_unsupported!(kernel, test_name);
        let data_small = [10.0, 20.0, 30.0];
        let params = MaczParams {
            fast_length: Some(12),
            slow_length: Some(100),
            signal_length: Some(9),
            lengthz: Some(20),
            length_stdev: Some(25),
            a: Some(1.0),
            b: Some(1.0),
            use_lag: Some(false),
            gamma: Some(0.02),
        };
        let input = MaczInput::from_slice(&data_small, params);
        let res = macz_with_kernel(&input, kernel);
        assert!(
            res.is_err(),
            "[{}] MAC-Z should fail with period exceeding length",
            test_name
        );
        Ok(())
    }

    fn check_macz_very_small_dataset(
        test_name: &str,
        kernel: Kernel,
    ) -> Result<(), Box<dyn Error>> {
        skip_if_unsupported!(kernel, test_name);
        let single_point = [42.0];
        let params = MaczParams {
            fast_length: Some(12),
            slow_length: Some(25),
            signal_length: Some(9),
            lengthz: Some(20),
            length_stdev: Some(25),
            a: Some(1.0),
            b: Some(1.0),
            use_lag: Some(false),
            gamma: Some(0.02),
        };
        let input = MaczInput::from_slice(&single_point, params);
        let res = macz_with_kernel(&input, kernel);
        assert!(
            res.is_err(),
            "[{}] MAC-Z should fail with insufficient data",
            test_name
        );
        Ok(())
    }

    fn check_macz_empty_input(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
        skip_if_unsupported!(kernel, test_name);
        let empty: [f64; 0] = [];
        let input = MaczInput::from_slice(&empty, MaczParams::default());
        let res = macz_with_kernel(&input, kernel);
        assert!(
            matches!(res, Err(MaczError::EmptyInputData)),
            "[{}] MAC-Z should fail with empty input",
            test_name
        );
        Ok(())
    }

    fn check_macz_invalid_a_constant(
        test_name: &str,
        kernel: Kernel,
    ) -> Result<(), Box<dyn Error>> {
        skip_if_unsupported!(kernel, test_name);
        let data = [1.0, 2.0, 3.0, 4.0, 5.0];
        let params = MaczParams {
            fast_length: Some(2),
            slow_length: Some(3),
            signal_length: Some(2),
            lengthz: Some(2),
            length_stdev: Some(2),
            a: Some(3.0),
            b: Some(1.0),
            use_lag: Some(false),
            gamma: Some(0.02),
        };
        let input = MaczInput::from_slice(&data, params);
        let res = macz_with_kernel(&input, kernel);
        assert!(
            res.is_err(),
            "[{}] MAC-Z should fail with invalid A constant",
            test_name
        );
        Ok(())
    }

    fn check_macz_invalid_b_constant(
        test_name: &str,
        kernel: Kernel,
    ) -> Result<(), Box<dyn Error>> {
        skip_if_unsupported!(kernel, test_name);
        let data = [1.0, 2.0, 3.0, 4.0, 5.0];
        let params = MaczParams {
            fast_length: Some(2),
            slow_length: Some(3),
            signal_length: Some(2),
            lengthz: Some(2),
            length_stdev: Some(2),
            a: Some(1.0),
            b: Some(-3.0),
            use_lag: Some(false),
            gamma: Some(0.02),
        };
        let input = MaczInput::from_slice(&data, params);
        let res = macz_with_kernel(&input, kernel);
        assert!(
            res.is_err(),
            "[{}] MAC-Z should fail with invalid B constant",
            test_name
        );
        Ok(())
    }

    fn check_macz_invalid_gamma(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
        skip_if_unsupported!(kernel, test_name);
        let data = [1.0, 2.0, 3.0, 4.0, 5.0];
        let params = MaczParams {
            fast_length: Some(2),
            slow_length: Some(3),
            signal_length: Some(2),
            lengthz: Some(2),
            length_stdev: Some(2),
            a: Some(1.0),
            b: Some(1.0),
            use_lag: Some(true),
            gamma: Some(1.5),
        };
        let input = MaczInput::from_slice(&data, params);
        let res = macz_with_kernel(&input, kernel);
        assert!(
            res.is_err(),
            "[{}] MAC-Z should fail with invalid gamma",
            test_name
        );
        Ok(())
    }

    fn check_macz_nan_handling(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
        skip_if_unsupported!(kernel, test_name);

        let data_with_nan = vec![
            1.0,
            2.0,
            f64::NAN,
            4.0,
            5.0,
            6.0,
            7.0,
            8.0,
            9.0,
            10.0,
            11.0,
            12.0,
            13.0,
            14.0,
            15.0,
            16.0,
            17.0,
            18.0,
            19.0,
            20.0,
            21.0,
            22.0,
            23.0,
            24.0,
            25.0,
            26.0,
            27.0,
            28.0,
            29.0,
            30.0,
            31.0,
            32.0,
            33.0,
            34.0,
            35.0,
            36.0,
            37.0,
            38.0,
            39.0,
            40.0,
            41.0,
            42.0,
            43.0,
            44.0,
            45.0,
        ];

        let params = MaczParams {
            fast_length: Some(5),
            slow_length: Some(10),
            signal_length: Some(3),
            lengthz: Some(8),
            length_stdev: Some(10),
            a: Some(1.0),
            b: Some(1.0),
            use_lag: Some(false),
            gamma: Some(0.02),
        };

        let input = MaczInput::from_slice(&data_with_nan, params);
        let res = macz_with_kernel(&input, kernel)?;

        assert!(
            res.values[2].is_nan(),
            "[{}] NaN should be propagated",
            test_name
        );

        Ok(())
    }

    fn check_macz_streaming(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
        skip_if_unsupported!(kernel, test_name);

        let test_data: Vec<f64> = (0..100).map(|i| 50.0 + (i as f64).sin() * 10.0).collect();

        let params = MaczParams {
            fast_length: Some(5),
            slow_length: Some(10),
            signal_length: Some(3),
            lengthz: Some(8),
            length_stdev: Some(10),
            a: Some(1.0),
            b: Some(1.0),
            use_lag: Some(false),
            gamma: Some(0.02),
        };

        let batch_input = MaczInput::from_slice(&test_data, params.clone());
        let batch_result = macz_with_kernel(&batch_input, kernel)?;

        let mut stream = MaczStream::new(params)?;
        let mut stream_results = Vec::new();

        for &value in &test_data {
            let result = stream.update(value, None);
            stream_results.push(result.unwrap_or(f64::NAN));
        }

        let batch_valid: Vec<f64> = batch_result
            .values
            .iter()
            .rev()
            .filter(|v| !v.is_nan())
            .take(10)
            .copied()
            .collect();

        let stream_valid: Vec<f64> = stream_results
            .iter()
            .rev()
            .filter(|v| !v.is_nan())
            .take(10)
            .copied()
            .collect();

        if !batch_valid.is_empty() && !stream_valid.is_empty() {
            assert!(
                batch_valid.len() > 0 && stream_valid.len() > 0,
                "[{}] Both batch and stream should produce valid values",
                test_name
            );
        }

        Ok(())
    }

    fn check_macz_no_poison_kernel(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
        skip_if_unsupported!(kernel, test_name);

        let data: Vec<f64> = (0..100).map(|i| 50.0 + (i as f64).sin() * 5.0).collect();
        let volume: Vec<f64> = (0..100).map(|i| 1000.0 + (i as f64) * 10.0).collect();

        let params = MaczParams::default();
        let input_with_vol = MaczInput::from_slice_with_volume(&data, &volume, params);
        let result_vol = macz_with_kernel(&input_with_vol, kernel)?;

        let mut actual_warmup = 0;
        for (i, &val) in result_vol.values.iter().enumerate() {
            if !val.is_nan() {
                actual_warmup = i;
                break;
            }
        }

        assert!(
            actual_warmup >= 25,
            "[{}] Warmup should be at least slow period",
            test_name
        );
        assert!(
            actual_warmup < 50,
            "[{}] Warmup should be reasonable",
            test_name
        );

        let mut dst = vec![123.456; data.len()];
        macz_into_slice(&mut dst, &input_with_vol, kernel)?;

        for i in 0..actual_warmup {
            assert!(
                dst[i].is_nan(),
                "[{}] into_slice should preserve NaN warmup at {}",
                test_name,
                i
            );
        }

        Ok(())
    }

    fn check_batch_default_row(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
        skip_if_unsupported!(kernel, test_name);

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

        let output = MaczBatchBuilder::new()
            .kernel(kernel)
            .apply_candles(&candles, "close")?;

        let def_params = MaczParams::default();
        let row = output.values_for(&def_params).expect("default row missing");

        assert_eq!(row.len(), candles.close.len());

        let warmup = 33;
        let valid_count = row[warmup..].iter().filter(|v| !v.is_nan()).count();
        assert!(
            valid_count > 0,
            "[{}] Should have valid values after warmup",
            test_name
        );

        Ok(())
    }

    fn check_batch_sweep(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
        skip_if_unsupported!(kernel, test_name);

        let data: Vec<f64> = (0..100).map(|i| 50.0 + (i as f64).sin() * 10.0).collect();

        let sweep = MaczBatchRange {
            fast_length: (10, 12, 1),
            slow_length: (20, 22, 1),
            signal_length: (5, 6, 1),
            lengthz: (15, 16, 1),
            length_stdev: (20, 21, 1),
            a: (1.0, 1.0, 0.1),
            b: (1.0, 1.0, 0.1),
        };

        let batch = macz_batch_with_kernel(&data, &sweep, kernel)?;

        assert_eq!(batch.cols, data.len());
        assert!(batch.rows > 0, "[{}] Batch should produce rows", test_name);

        assert!(
            !batch.combos.is_empty(),
            "[{}] Should have parameter combinations",
            test_name
        );

        Ok(())
    }

    macro_rules! generate_all_macz_tests {
        ($($test_fn:ident),*) => {
            paste::paste! {
                $(
                    #[test]
                    fn [<$test_fn _scalar>]() {
                        let _ = $test_fn(stringify!([<$test_fn _scalar>]), Kernel::Scalar);
                    }
                )*
                #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
                $(
                    #[test]
                    fn [<$test_fn _avx2>]() {
                        let _ = $test_fn(stringify!([<$test_fn _avx2>]), Kernel::Avx2);
                    }
                    #[test]
                    fn [<$test_fn _avx512>]() {
                        let _ = $test_fn(stringify!([<$test_fn _avx512>]), Kernel::Avx512);
                    }
                )*
            }
        }
    }

    fn check_macz_default_candles(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
        skip_if_unsupported!(kernel, test_name);
        let fp = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
        let c = read_candles_from_csv(fp)?;
        let input = MaczInput::with_default_candles_auto_volume(&c);
        match input.data {
            MaczData::Candles { source, .. } => assert_eq!(source, "close"),
            _ => panic!("Expected MaczData::Candles"),
        }
        let out = macz_with_kernel(&input, kernel)?;
        assert_eq!(out.values.len(), c.close.len());
        Ok(())
    }

    fn check_macz_reinput(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
        skip_if_unsupported!(kernel, test_name);
        let fp = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
        let c = read_candles_from_csv(fp)?;
        let v = vec![1.0; c.close.len()];
        let first = MaczInput::from_candles_with_volume(&c, "close", &v, MaczParams::default());
        let a = macz_with_kernel(&first, kernel)?;
        let second = MaczInput::from_slice(&a.values, MaczParams::default());
        let b = macz_with_kernel(&second, kernel)?;
        assert_eq!(b.values.len(), a.values.len());
        Ok(())
    }

    #[cfg(debug_assertions)]
    fn check_macz_no_poison(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
        skip_if_unsupported!(kernel, test_name);
        let fp = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
        let c = read_candles_from_csv(fp)?;
        let v = vec![1.0; c.close.len()];
        let input = MaczInput::from_candles_with_volume(&c, "close", &v, MaczParams::default());
        let out = macz_with_kernel(&input, kernel)?;

        for (i, &val) in out.values.iter().enumerate() {
            if val.is_nan() {
                continue;
            }
            let bits = val.to_bits();
            assert_ne!(
                bits, 0x11111111_11111111,
                "[{}] alloc_with_nan_prefix poison at {}",
                test_name, i
            );
            assert_ne!(
                bits, 0x22222222_22222222,
                "[{}] init_matrix_prefixes poison at {}",
                test_name, i
            );
            assert_ne!(
                bits, 0x33333333_33333333,
                "[{}] make_uninit_matrix poison at {}",
                test_name, i
            );
        }
        Ok(())
    }
    #[cfg(not(debug_assertions))]
    fn check_macz_no_poison(_: &str, _: Kernel) -> Result<(), Box<dyn Error>> {
        Ok(())
    }

    #[cfg(debug_assertions)]
    fn check_batch_no_poison(test: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
        skip_if_unsupported!(kernel, test);
        let file = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
        let c = read_candles_from_csv(file)?;
        let out = MaczBatchBuilder::new()
            .kernel(kernel)
            .fast_range(10, 12, 1)
            .slow_range(20, 22, 1)
            .signal_range(5, 6, 1)
            .lengthz_static(20)
            .length_stdev_static(25)
            .a_static(1.0)
            .b_static(1.0)
            .apply_candles(&c, "close")?;

        for (idx, &v) in out.values.iter().enumerate() {
            if v.is_nan() {
                continue;
            }
            let b = v.to_bits();
            assert_ne!(b, 0x11111111_11111111, "[{}] alloc poison at {}", test, idx);
            assert_ne!(b, 0x22222222_22222222, "[{}] init poison at {}", test, idx);
            assert_ne!(b, 0x33333333_33333333, "[{}] make poison at {}", test, idx);
        }
        Ok(())
    }
    #[cfg(not(debug_assertions))]
    fn check_batch_no_poison(_: &str, _: Kernel) -> Result<(), Box<dyn Error>> {
        Ok(())
    }

    fn check_batch_with_volume(test: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
        skip_if_unsupported!(kernel, test);
        let file = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
        let c = read_candles_from_csv(file)?;
        let out = MaczBatchBuilder::new()
            .kernel(kernel)
            .apply_candles(&c, "close")?;
        assert_eq!(out.cols, c.close.len());
        assert!(out.rows >= 1);
        Ok(())
    }

    generate_all_macz_tests!(
        check_macz_partial_params,
        check_macz_accuracy,
        check_macz_zero_period,
        check_macz_period_exceeds_length,
        check_macz_very_small_dataset,
        check_macz_empty_input,
        check_macz_invalid_a_constant,
        check_macz_invalid_b_constant,
        check_macz_invalid_gamma,
        check_macz_nan_handling,
        check_macz_streaming,
        check_macz_no_poison_kernel,
        check_macz_default_candles,
        check_macz_reinput,
        check_macz_no_poison
    );

    macro_rules! gen_batch_tests {
        ($fn_name:ident) => {
            paste::paste! {
                #[test]
                fn [<$fn_name _scalar>]() {
                    let _ = $fn_name(stringify!([<$fn_name _scalar>]), Kernel::ScalarBatch);
                }
                #[cfg(all(feature="nightly-avx", target_arch="x86_64"))]
                #[test]
                fn [<$fn_name _avx2>]() {
                    let _ = $fn_name(stringify!([<$fn_name _avx2>]), Kernel::Avx2Batch);
                }
                #[cfg(all(feature="nightly-avx", target_arch="x86_64"))]
                #[test]
                fn [<$fn_name _avx512>]() {
                    let _ = $fn_name(stringify!([<$fn_name _avx512>]), Kernel::Avx512Batch);
                }
                #[test]
                fn [<$fn_name _auto_detect>]() {
                    let _ = $fn_name(stringify!([<$fn_name _auto_detect>]), Kernel::Auto);
                }
            }
        };
    }

    gen_batch_tests!(check_batch_default_row);
    gen_batch_tests!(check_batch_sweep);
    gen_batch_tests!(check_batch_no_poison);
    gen_batch_tests!(check_batch_with_volume);

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

        let strat = (
            prop::collection::vec(
                (-1e4f64..1e4f64).prop_filter("finite", |x| x.is_finite()),
                50..200,
            ),
            prop::collection::vec(
                (100f64..10000f64).prop_filter("positive", |x| x.is_finite() && *x > 0.0),
                50..200,
            ),
            5usize..15,
            15usize..30,
            3usize..10,
            10usize..25,
            15usize..30,
            -1.5f64..1.5f64,
            -1.5f64..1.5f64,
            prop::bool::ANY,
            0.01f64..0.1f64,
        );

        proptest::test_runner::TestRunner::default().run(
            &strat,
            |(data, volume, fast, slow, sig, lz, lsd, a, b, use_lag, gamma)| {
                let min_len = data.len().min(volume.len());
                let data = &data[..min_len];
                let volume = &volume[..min_len];

                if slow >= data.len() || lz >= data.len() || lsd >= data.len() {
                    return Ok(());
                }

                let params = MaczParams {
                    fast_length: Some(fast),
                    slow_length: Some(slow),
                    signal_length: Some(sig),
                    lengthz: Some(lz),
                    length_stdev: Some(lsd),
                    a: Some(a),
                    b: Some(b),
                    use_lag: Some(use_lag),
                    gamma: Some(gamma),
                };

                let input = MaczInput::from_slice_with_volume(data, volume, params);

                let result = macz_with_kernel(&input, kernel);
                prop_assert!(
                    result.is_ok(),
                    "MAC-Z calculation failed: {:?}",
                    result.err()
                );

                let output = result.unwrap();
                prop_assert_eq!(output.values.len(), data.len(), "Output length mismatch");

                if kernel != Kernel::Scalar {
                    let scalar_result = macz_with_kernel(&input, Kernel::Scalar).unwrap();

                    for (i, (&simd_val, &scalar_val)) in output
                        .values
                        .iter()
                        .zip(scalar_result.values.iter())
                        .enumerate()
                    {
                        if !simd_val.is_nan() && !scalar_val.is_nan() {
                            let diff = (simd_val - scalar_val).abs();
                            let tolerance = 1e-10 * scalar_val.abs().max(1.0);
                            prop_assert!(
                                diff < tolerance,
                                "[{}] Kernel mismatch at index {}: SIMD={}, Scalar={}, diff={}",
                                test_name,
                                i,
                                simd_val,
                                scalar_val,
                                diff
                            );
                        }
                    }
                }

                Ok(())
            },
        )?;

        Ok(())
    }

    #[cfg(feature = "proptest")]
    generate_all_macz_tests!(check_macz_property);

    #[test]
    fn test_macz_basic() {
        let data = vec![
            59243.26, 59234.77, 59223.21, 59265.62, 59397.48, 59499.99, 59564.95, 59686.73,
            59793.59, 59800.41, 59867.59, 59841.97, 59909.83, 60050.61, 60077.85, 60184.65,
            60255.36, 60317.44, 60278.45, 60210.49, 60304.89, 60394.25, 60353.87, 60470.57,
            60464.01, 60405.50, 60356.46, 60406.48, 60419.10, 60432.29, 60496.55, 60625.25,
            60609.84, 60718.37, 60641.10, 60619.52, 60646.73, 60713.42, 60609.51, 60598.68,
            60635.36, 60648.74, 60741.47, 60650.16, 60614.54, 60579.84, 60543.59, 60565.12,
            60522.53, 60460.89,
        ];

        let params = MaczParams::default();
        let input = MaczInput::from_slice(&data, params);
        let result = macz(&input).unwrap();

        let expected = vec![0.51988421, 0.23019592, 0.08030845, 0.12276454, -0.56402159];
        let actual = &result.values[result.values.len() - 5..];

        println!("Last 5 actual values: {:?}", actual);
        println!("Expected values: {:?}", expected);

        let warmup = 33;
        assert!(
            result.values[warmup..].iter().any(|&v| !v.is_nan()),
            "Should have non-NaN values after warmup"
        );
    }

    #[test]
    fn test_macz_empty_input() {
        let params = MaczParams::default();
        let input = MaczInput::from_slice(&[], params);
        let result = macz(&input);
        assert!(result.is_err());
    }

    #[test]
    fn test_macz_all_nan() {
        let data = vec![f64::NAN; 50];
        let params = MaczParams::default();
        let input = MaczInput::from_slice(&data, params);
        let result = macz(&input);
        assert!(result.is_err());
    }

    #[test]
    fn test_macz_builder() {
        let data = [1.0, 2.0, 3.0, 4.0, 5.0].repeat(10);

        let result = MaczBuilder::new()
            .fast_length(10)
            .slow_length(20)
            .signal_length(5)
            .kernel(Kernel::Scalar)
            .apply_slice(&data);

        assert!(result.is_ok());
        let output = result.unwrap();
        assert_eq!(output.values.len(), data.len());
    }

    #[test]
    fn test_macz_batch_builder() {
        let data = [1.0, 2.0, 3.0, 4.0, 5.0].repeat(10);

        let result = MaczBatchBuilder::new()
            .fast_range(10, 12, 1)
            .slow_range(20, 22, 1)
            .signal_range(5, 6, 1)
            .kernel(Kernel::ScalarBatch)
            .apply_slice(&data);

        assert!(result.is_ok());
        let output = result.unwrap();
        assert_eq!(output.cols, data.len());
        assert!(output.rows > 0);
    }

    #[test]
    fn test_macz_into_slice() {
        let data: Vec<f64> = (0..50).map(|i| i as f64).collect();
        let mut dst = vec![0.0; 50];
        let params = MaczParams::default();
        let input = MaczInput::from_slice(&data, params);

        let result = macz_into_slice(&mut dst, &input, Kernel::Scalar);
        assert!(result.is_ok(), "Error: {:?}", result);
    }

    #[test]
    fn test_macz_batch_processing() {
        let data = [1.0, 2.0, 3.0, 4.0, 5.0].repeat(10);

        let sweep = MaczBatchRange {
            fast_length: (10, 11, 1),
            slow_length: (20, 21, 1),
            signal_length: (5, 5, 1),
            lengthz: (15, 15, 1),
            length_stdev: (20, 20, 1),
            a: (1.0, 1.0, 0.1),
            b: (1.0, 1.0, 0.1),
        };

        let result_seq = macz_batch_slice(&data, &sweep, Kernel::Scalar);
        assert!(result_seq.is_ok());
    }

    #[test]
    fn test_macz_streaming() {
        let params = MaczParams {
            fast_length: Some(5),
            slow_length: Some(10),
            signal_length: Some(3),
            lengthz: Some(8),
            length_stdev: Some(10),
            a: Some(1.0),
            b: Some(1.0),
            use_lag: Some(false),
            gamma: Some(0.02),
        };

        let mut stream = MaczStream::new(params).unwrap();

        for i in 0..20 {
            let val = i as f64;
            let _ = stream.update(val, None);
        }
    }

    #[test]
    fn test_macz_no_poison_legacy() {
        let data: Vec<f64> = (0..100).map(|i| 50.0 + (i as f64).sin() * 5.0).collect();
        let volume: Vec<f64> = (0..100).map(|i| 1000.0 + (i as f64) * 10.0).collect();

        let params = MaczParams::default();
        let input_with_vol = MaczInput::from_slice_with_volume(&data, &volume, params);
        let result_vol = macz(&input_with_vol).unwrap();

        let mut actual_warmup = 0;
        for (i, &val) in result_vol.values.iter().enumerate() {
            if !val.is_nan() {
                actual_warmup = i;
                break;
            }
        }
        println!("Actual warmup period: {}", actual_warmup);

        assert!(actual_warmup >= 25, "Warmup should be at least slow period");
        assert!(actual_warmup < 50, "Warmup should be reasonable");

        for i in 0..actual_warmup {
            assert!(
                result_vol.values[i].is_nan(),
                "Expected NaN at index {} during warmup, got {}",
                i,
                result_vol.values[i]
            );
        }
        for i in actual_warmup..result_vol.values.len().min(actual_warmup + 10) {
            assert!(
                !result_vol.values[i].is_nan(),
                "Expected non-NaN at index {} after warmup, got NaN",
                i
            );
        }

        let mut dst = vec![123.456; data.len()];
        macz_into_slice(&mut dst, &input_with_vol, Kernel::Scalar).unwrap();

        for i in 0..actual_warmup {
            assert!(
                dst[i].is_nan(),
                "into_slice should preserve NaN warmup at {}",
                i
            );
        }

        for i in actual_warmup..dst.len() {
            assert!(
                (dst[i] - result_vol.values[i]).abs() < 1e-10,
                "into_slice result mismatch at index {}",
                i
            );
        }

        let sweep = MaczBatchRange {
            fast_length: (12, 13, 1),
            slow_length: (25, 26, 1),
            signal_length: (9, 9, 1),
            lengthz: (20, 20, 1),
            length_stdev: (25, 25, 1),
            a: (1.0, 1.0, 0.1),
            b: (1.0, 1.0, 0.1),
        };

        let batch_result = macz_batch_slice(&data, &sweep, Kernel::ScalarBatch).unwrap();

        assert_eq!(batch_result.cols, data.len());
        assert!(batch_result.rows > 0);

        let mut non_nan_count = 0;
        for val in &batch_result.values {
            if !val.is_nan() {
                non_nan_count += 1;
            }
        }
        assert!(non_nan_count > 0, "Batch processing produced all NaNs");
    }

    #[cfg(debug_assertions)]
    #[test]
    fn check_macz_batch_no_poison() -> Result<(), Box<dyn std::error::Error>> {
        use crate::utilities::data_loader::read_candles_from_csv;
        let file = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
        let c = read_candles_from_csv(file)?;
        let sweep = MaczBatchRange::default();
        let out = macz_batch_with_kernel(&c.close, &sweep, Kernel::ScalarBatch)?;
        for (idx, &v) in out.values.iter().enumerate() {
            if v.is_nan() {
                continue;
            }
            let bits = v.to_bits();
            assert_ne!(
                bits, 0x11111111_11111111,
                "alloc_with_nan_prefix poison at {idx}"
            );
            assert_ne!(
                bits, 0x22222222_22222222,
                "init_matrix_prefixes poison at {idx}"
            );
            assert_ne!(
                bits, 0x33333333_33333333,
                "make_uninit_matrix poison at {idx}"
            );
        }
        Ok(())
    }
}
