#[cfg(feature = "python")]
use crate::utilities::kernel_validation::validate_kernel;
#[cfg(all(feature = "python", feature = "cuda"))]
use numpy::PyUntypedArrayMethods;
#[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;

#[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};
#[cfg(all(feature = "python", feature = "cuda"))]
use crate::utilities::dlpack_cuda::export_f32_cuda_dlpack_2d;
use crate::utilities::enums::Kernel;
use crate::utilities::helpers::{
    alloc_with_nan_prefix, detect_best_batch_kernel, detect_best_kernel, init_matrix_prefixes,
    make_uninit_matrix,
};
#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
use core::arch::x86_64::*;
#[cfg(all(feature = "python", feature = "cuda"))]
use cust::context::Context;
#[cfg(all(feature = "python", feature = "cuda"))]
use cust::memory::DeviceBuffer;
#[cfg(not(target_arch = "wasm32"))]
use rayon::prelude::*;
use std::convert::AsRef;
use std::error::Error;
#[cfg(all(feature = "python", feature = "cuda"))]
use std::sync::Arc;
use thiserror::Error;

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

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

#[derive(Debug, Clone)]
pub struct VossOutput {
    pub voss: Vec<f64>,
    pub filt: Vec<f64>,
}

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

impl Default for VossParams {
    fn default() -> Self {
        Self {
            period: Some(20),
            predict: Some(3),
            bandwidth: Some(0.25),
        }
    }
}

#[derive(Debug, Clone)]
pub struct VossInput<'a> {
    pub data: VossData<'a>,
    pub params: VossParams,
}

impl<'a> VossInput<'a> {
    #[inline]
    pub fn from_candles(c: &'a Candles, s: &'a str, p: VossParams) -> Self {
        Self {
            data: VossData::Candles {
                candles: c,
                source: s,
            },
            params: p,
        }
    }
    #[inline]
    pub fn from_slice(sl: &'a [f64], p: VossParams) -> Self {
        Self {
            data: VossData::Slice(sl),
            params: p,
        }
    }
    #[inline]
    pub fn with_default_candles(c: &'a Candles) -> Self {
        Self::from_candles(c, "close", VossParams::default())
    }
    #[inline]
    pub fn get_period(&self) -> usize {
        self.params.period.unwrap_or(20)
    }
    #[inline]
    pub fn get_predict(&self) -> usize {
        self.params.predict.unwrap_or(3)
    }
    #[inline]
    pub fn get_bandwidth(&self) -> f64 {
        self.params.bandwidth.unwrap_or(0.25)
    }
}

#[derive(Copy, Clone, Debug)]
pub struct VossBuilder {
    period: Option<usize>,
    predict: Option<usize>,
    bandwidth: Option<f64>,
    kernel: Kernel,
}

impl Default for VossBuilder {
    fn default() -> Self {
        Self {
            period: None,
            predict: None,
            bandwidth: None,
            kernel: Kernel::Auto,
        }
    }
}

impl VossBuilder {
    #[inline(always)]
    pub fn new() -> Self {
        Self::default()
    }
    #[inline(always)]
    pub fn period(mut self, n: usize) -> Self {
        self.period = Some(n);
        self
    }
    #[inline(always)]
    pub fn predict(mut self, n: usize) -> Self {
        self.predict = Some(n);
        self
    }
    #[inline(always)]
    pub fn bandwidth(mut self, b: f64) -> Self {
        self.bandwidth = Some(b);
        self
    }
    #[inline(always)]
    pub fn kernel(mut self, k: Kernel) -> Self {
        self.kernel = k;
        self
    }

    #[inline(always)]
    pub fn apply(self, c: &Candles) -> Result<VossOutput, VossError> {
        let p = VossParams {
            period: self.period,
            predict: self.predict,
            bandwidth: self.bandwidth,
        };
        let i = VossInput::from_candles(c, "close", p);
        voss_with_kernel(&i, self.kernel)
    }
    #[inline(always)]
    pub fn apply_slice(self, d: &[f64]) -> Result<VossOutput, VossError> {
        let p = VossParams {
            period: self.period,
            predict: self.predict,
            bandwidth: self.bandwidth,
        };
        let i = VossInput::from_slice(d, p);
        voss_with_kernel(&i, self.kernel)
    }
    #[inline(always)]
    pub fn into_stream(self) -> Result<VossStream, VossError> {
        let p = VossParams {
            period: self.period,
            predict: self.predict,
            bandwidth: self.bandwidth,
        };
        VossStream::try_new(p)
    }
}

#[derive(Debug, Error)]
pub enum VossError {
    #[error("voss: Input data slice is empty.")]
    EmptyInputData,
    #[error("voss: All values are NaN.")]
    AllValuesNaN,
    #[error("voss: Invalid period: period = {period}, data length = {data_len}")]
    InvalidPeriod { period: usize, data_len: usize },
    #[error("voss: Not enough valid data: needed = {needed}, valid = {valid}")]
    NotEnoughValidData { needed: usize, valid: usize },
    #[error("voss: Output length mismatch: expected = {expected}, got = {got}")]
    OutputLengthMismatch { expected: usize, got: usize },
    #[error("voss: Invalid range: start = {start}, end = {end}, step = {step}")]
    InvalidRange {
        start: String,
        end: String,
        step: String,
    },
    #[error("voss: Invalid kernel for batch: {0:?}")]
    InvalidKernelForBatch(Kernel),
}

#[inline]
pub fn voss(input: &VossInput) -> Result<VossOutput, VossError> {
    voss_with_kernel(input, Kernel::Auto)
}

#[inline(always)]
fn voss_prepare<'a>(
    input: &'a VossInput,
    kernel: Kernel,
) -> Result<(&'a [f64], usize, usize, f64, usize, usize, Kernel), VossError> {
    let data: &[f64] = match &input.data {
        VossData::Candles { candles, source } => source_type(candles, source),
        VossData::Slice(sl) => sl,
    };

    if data.is_empty() {
        return Err(VossError::EmptyInputData);
    }

    let first = data
        .iter()
        .position(|x| !x.is_nan())
        .ok_or(VossError::AllValuesNaN)?;
    let len = data.len();
    let period = input.get_period();
    let predict = input.get_predict();
    let bandwidth = input.get_bandwidth();

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

    let order = 3 * predict;
    let min_index = period.max(5).max(order);
    if (len - first) < min_index {
        return Err(VossError::NotEnoughValidData {
            needed: min_index,
            valid: len - first,
        });
    }

    let chosen = match kernel {
        Kernel::Auto => detect_best_kernel(),
        other => other,
    };

    Ok((data, period, predict, bandwidth, first, min_index, chosen))
}

#[inline(always)]
fn voss_compute_into(
    data: &[f64],
    period: usize,
    predict: usize,
    bandwidth: f64,
    first: usize,
    min_index: usize,
    kernel: Kernel,
    voss: &mut [f64],
    filt: &mut [f64],
) {
    #[cfg(all(target_arch = "wasm32", target_feature = "simd128"))]
    {
        if matches!(kernel, Kernel::Scalar | Kernel::ScalarBatch) {
            unsafe {
                voss_simd128(
                    data, period, predict, bandwidth, first, min_index, voss, filt,
                );
            }
            return;
        }
    }

    match kernel {
        Kernel::Scalar | Kernel::ScalarBatch => {
            voss_scalar(data, period, predict, bandwidth, first, voss, filt)
        }
        #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
        Kernel::Avx2 | Kernel::Avx2Batch => unsafe {
            voss_avx2(data, period, predict, bandwidth, first, voss, filt)
        },
        #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
        Kernel::Avx512 | Kernel::Avx512Batch => unsafe {
            voss_avx512(data, period, predict, bandwidth, first, voss, filt)
        },
        _ => unreachable!(),
    }
}

pub fn voss_with_kernel(input: &VossInput, kernel: Kernel) -> Result<VossOutput, VossError> {
    let (data, period, predict, bandwidth, first, min_index, chosen) = voss_prepare(input, kernel)?;

    let warmup_period = first + min_index;
    let mut voss = alloc_with_nan_prefix(data.len(), warmup_period);
    let mut filt = alloc_with_nan_prefix(data.len(), warmup_period);

    voss_compute_into(
        data, period, predict, bandwidth, first, min_index, chosen, &mut voss, &mut filt,
    );

    Ok(VossOutput { voss, filt })
}

#[inline]
pub fn voss_into_slice(
    voss_dst: &mut [f64],
    filt_dst: &mut [f64],
    input: &VossInput,
    kern: Kernel,
) -> Result<(), VossError> {
    let (data, period, predict, bandwidth, first, min_index, chosen) = voss_prepare(input, kern)?;

    if voss_dst.len() != data.len() {
        return Err(VossError::OutputLengthMismatch {
            expected: data.len(),
            got: voss_dst.len(),
        });
    }
    if filt_dst.len() != data.len() {
        return Err(VossError::OutputLengthMismatch {
            expected: data.len(),
            got: filt_dst.len(),
        });
    }

    let warmup_end = first + min_index;
    let qnan = f64::from_bits(0x7ff8_0000_0000_0000);
    let v_end = if warmup_end < voss_dst.len() {
        warmup_end
    } else {
        voss_dst.len()
    };
    for v in &mut voss_dst[..v_end] {
        *v = qnan;
    }
    let f_end = if warmup_end < filt_dst.len() {
        warmup_end
    } else {
        filt_dst.len()
    };
    for v in &mut filt_dst[..f_end] {
        *v = qnan;
    }

    voss_compute_into(
        data, period, predict, bandwidth, first, min_index, chosen, voss_dst, filt_dst,
    );

    Ok(())
}

#[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
#[inline]
pub fn voss_into(
    input: &VossInput,
    voss_out: &mut [f64],
    filt_out: &mut [f64],
) -> Result<(), VossError> {
    voss_into_slice(voss_out, filt_out, input, Kernel::Auto)
}

#[inline]
pub fn voss_scalar(
    data: &[f64],
    period: usize,
    predict: usize,
    bandwidth: f64,
    first: usize,
    voss: &mut [f64],
    filt: &mut [f64],
) {
    voss_row_scalar(data, first, period, predict, bandwidth, voss, filt)
}

#[cfg(all(target_arch = "wasm32", target_feature = "simd128"))]
#[inline]
unsafe fn voss_simd128(
    data: &[f64],
    period: usize,
    predict: usize,
    bandwidth: f64,
    first: usize,
    min_index: usize,
    voss: &mut [f64],
    filt: &mut [f64],
) {
    use core::arch::wasm32::*;
    use std::f64::consts::PI;

    let order = 3 * predict;
    let min_idx = period.max(5).max(order);

    let f1 = (2.0 * PI / period as f64).cos();
    let g1 = (bandwidth * 2.0 * PI / period as f64).cos();
    let s1 = 1.0 / g1 - (1.0 / (g1 * g1) - 1.0).sqrt();

    let start_idx = first + min_idx;
    if start_idx >= 2 {
        filt[start_idx - 2] = 0.0;
        filt[start_idx - 1] = 0.0;
    }

    let coeff1 = 0.5 * (1.0 - s1);
    let coeff2 = f1 * (1.0 + s1);
    let coeff3 = -s1;

    for i in start_idx..data.len() {
        let current = data[i];
        let prev_2 = data[i - 2];
        let prev_filt_1 = filt[i - 1];
        let prev_filt_2 = filt[i - 2];
        filt[i] = coeff1 * (current - prev_2) + coeff2 * prev_filt_1 + coeff3 * prev_filt_2;
    }

    let scale = (3 + order) as f64 / 2.0;
    let inv_order = 1.0 / order as f64;

    for i in start_idx..data.len() {
        let mut sum = 0.0;
        let base_idx = i - order;

        let pairs = order / 2;
        for j in 0..pairs {
            let idx1 = base_idx + j * 2;
            let idx2 = base_idx + j * 2 + 1;

            let voss_val1 = if idx1 >= first && !voss[idx1].is_nan() {
                voss[idx1]
            } else {
                0.0
            };
            let voss_val2 = if idx2 >= first && !voss[idx2].is_nan() {
                voss[idx2]
            } else {
                0.0
            };

            let v1 = v128_load64_splat(&voss_val1 as *const f64 as *const u64);
            let v2 = v128_load64_splat(&voss_val2 as *const f64 as *const u64);
            let vals = f64x2_replace_lane::<1>(v1, f64x2_extract_lane::<0>(v2));

            let w1 = (j * 2 + 1) as f64 * inv_order;
            let w2 = (j * 2 + 2) as f64 * inv_order;
            let weights = f64x2(w1, w2);

            let prod = f64x2_mul(vals, weights);
            sum += f64x2_extract_lane::<0>(prod) + f64x2_extract_lane::<1>(prod);
        }

        if order % 2 != 0 {
            let idx = base_idx + order - 1;
            let voss_val = if idx >= first && !voss[idx].is_nan() {
                voss[idx]
            } else {
                0.0
            };
            sum += order as f64 * inv_order * voss_val;
        }

        voss[i] = scale * filt[i] - sum;
    }
}

#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
#[inline]
pub unsafe fn voss_avx2(
    data: &[f64],
    period: usize,
    predict: usize,
    bandwidth: f64,
    first: usize,
    voss: &mut [f64],
    filt: &mut [f64],
) {
    voss_row_scalar_unchecked(data, first, period, predict, bandwidth, voss, filt)
}

#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
#[inline]
pub unsafe fn voss_avx512(
    data: &[f64],
    period: usize,
    predict: usize,
    bandwidth: f64,
    first: usize,
    voss: &mut [f64],
    filt: &mut [f64],
) {
    if period <= 32 {
        voss_avx512_short(data, period, predict, bandwidth, first, voss, filt)
    } else {
        voss_avx512_long(data, period, predict, bandwidth, first, voss, filt)
    }
}

#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
#[inline]
pub unsafe fn voss_avx512_short(
    data: &[f64],
    period: usize,
    predict: usize,
    bandwidth: f64,
    first: usize,
    voss: &mut [f64],
    filt: &mut [f64],
) {
    voss_row_scalar_unchecked(data, first, period, predict, bandwidth, voss, filt)
}

#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
#[inline]
pub unsafe fn voss_avx512_long(
    data: &[f64],
    period: usize,
    predict: usize,
    bandwidth: f64,
    first: usize,
    voss: &mut [f64],
    filt: &mut [f64],
) {
    voss_row_scalar_unchecked(data, first, period, predict, bandwidth, voss, filt)
}

#[derive(Debug, Clone)]
pub struct VossStream {
    period: usize,
    predict: usize,
    bandwidth: f64,

    f1: f64,
    s1: f64,
    c1: f64,
    c2: f64,
    c3: f64,
    order: usize,
    warm_left: usize,

    prev_f1: f64,
    prev_f2: f64,
    last_x1: f64,
    last_x2: f64,

    ring: Vec<f64>,
    rpos: usize,
    a_sum: f64,
    d_sum: f64,
    ord_f: f64,
    inv_order: f64,
    scale: f64,

    filled: bool,
}

impl VossStream {
    pub fn try_new(params: VossParams) -> Result<Self, VossError> {
        use std::f64::consts::PI;
        let period = params.period.unwrap_or(20);
        if period == 0 {
            return Err(VossError::InvalidPeriod {
                period,
                data_len: 0,
            });
        }
        let predict = params.predict.unwrap_or(3);
        let bandwidth = params.bandwidth.unwrap_or(0.25);

        let order = 3 * predict;
        let min_index = period.max(5).max(order);

        let w0 = 2.0 * PI / period as f64;
        let f1 = w0.cos();
        let g1 = (bandwidth * w0).cos();

        let inv_g1 = 1.0 / g1;
        let root = (inv_g1.mul_add(inv_g1, -1.0)).sqrt();
        let s1 = 1.0 / (inv_g1 + root);

        Ok(Self {
            period,
            predict,
            bandwidth,
            f1,
            s1,
            c1: 0.5 * (1.0 - s1),
            c2: f1 * (1.0 + s1),
            c3: -s1,
            order,
            warm_left: min_index,
            prev_f1: 0.0,
            prev_f2: 0.0,
            last_x1: f64::NAN,
            last_x2: f64::NAN,
            ring: if order > 0 {
                vec![0.0; order]
            } else {
                Vec::new()
            },
            rpos: 0,
            a_sum: 0.0,
            d_sum: 0.0,
            ord_f: order as f64,
            inv_order: if order > 0 { 1.0 / order as f64 } else { 0.0 },
            scale: 0.5 * (3 + order) as f64,

            filled: false,
        })
    }

    #[inline(always)]
    pub fn update(&mut self, x: f64) -> Option<(f64, f64)> {
        let x_im2 = self.last_x2;
        self.last_x2 = self.last_x1;
        self.last_x1 = x;

        let filt_val = if x_im2.is_nan() {
            0.0
        } else {
            let diff = x - x_im2;
            let t = self.c1 * diff + self.c3 * self.prev_f2;
            let f = self.c2.mul_add(self.prev_f1, t);
            self.prev_f2 = self.prev_f1;
            self.prev_f1 = f;
            f
        };

        let voss_val = if self.order == 0 {
            self.scale * filt_val
        } else {
            let sumc = self.d_sum * self.inv_order;
            let vi = self.scale.mul_add(filt_val, -sumc);

            let v_new_nz = if vi.is_nan() { 0.0 } else { vi };
            let v_old = self.ring[self.rpos];
            let a_prev = self.a_sum;
            self.a_sum = a_prev - v_old + v_new_nz;
            self.d_sum = self.ord_f.mul_add(v_new_nz, self.d_sum - a_prev);

            self.ring[self.rpos] = v_new_nz;
            self.rpos += 1;
            if self.rpos == self.order {
                self.rpos = 0;
            }

            vi
        };

        if self.warm_left > 0 {
            self.warm_left -= 1;
            return None;
        }
        self.filled = true;
        Some((voss_val, filt_val))
    }
}

#[derive(Clone, Debug)]
pub struct VossBatchRange {
    pub period: (usize, usize, usize),
    pub predict: (usize, usize, usize),
    pub bandwidth: (f64, f64, f64),
}

impl Default for VossBatchRange {
    fn default() -> Self {
        Self {
            period: (20, 269, 1),
            predict: (3, 3, 0),
            bandwidth: (0.25, 0.25, 0.0),
        }
    }
}

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

impl VossBatchBuilder {
    pub fn new() -> Self {
        Self::default()
    }
    pub fn kernel(mut self, k: Kernel) -> Self {
        self.kernel = k;
        self
    }
    pub fn period_range(mut self, start: usize, end: usize, step: usize) -> Self {
        self.range.period = (start, end, step);
        self
    }
    pub fn predict_range(mut self, start: usize, end: usize, step: usize) -> Self {
        self.range.predict = (start, end, step);
        self
    }
    pub fn bandwidth_range(mut self, start: f64, end: f64, step: f64) -> Self {
        self.range.bandwidth = (start, end, step);
        self
    }

    pub fn apply_slice(self, data: &[f64]) -> Result<VossBatchOutput, VossError> {
        voss_batch_with_kernel(data, &self.range, self.kernel)
    }

    pub fn apply_candles(self, c: &Candles, src: &str) -> Result<VossBatchOutput, VossError> {
        let slice = source_type(c, src);
        self.apply_slice(slice)
    }
}

pub fn voss_batch_with_kernel(
    data: &[f64],
    sweep: &VossBatchRange,
    k: Kernel,
) -> Result<VossBatchOutput, VossError> {
    let kernel = match k {
        Kernel::Auto => detect_best_batch_kernel(),
        other if other.is_batch() => other,
        other => return Err(VossError::InvalidKernelForBatch(other)),
    };
    let simd = match kernel {
        Kernel::Avx512Batch => Kernel::Avx512,
        Kernel::Avx2Batch => Kernel::Avx2,
        Kernel::ScalarBatch => Kernel::Scalar,
        _ => unreachable!(),
    };
    voss_batch_par_slice(data, sweep, simd)
}

#[derive(Clone, Debug)]
pub struct VossBatchOutput {
    pub voss: Vec<f64>,
    pub filt: Vec<f64>,
    pub combos: Vec<VossParams>,
    pub rows: usize,
    pub cols: usize,
}
impl VossBatchOutput {
    pub fn row_for_params(&self, p: &VossParams) -> Option<usize> {
        self.combos.iter().position(|c| {
            c.period.unwrap_or(20) == p.period.unwrap_or(20)
                && c.predict.unwrap_or(3) == p.predict.unwrap_or(3)
                && (c.bandwidth.unwrap_or(0.25) - p.bandwidth.unwrap_or(0.25)).abs() < 1e-12
        })
    }
    pub fn voss_for(&self, p: &VossParams) -> Option<&[f64]> {
        self.row_for_params(p).map(|row| {
            let start = row * self.cols;
            &self.voss[start..start + self.cols]
        })
    }
    pub fn filt_for(&self, p: &VossParams) -> Option<&[f64]> {
        self.row_for_params(p).map(|row| {
            let start = row * self.cols;
            &self.filt[start..start + self.cols]
        })
    }
}

#[inline(always)]
fn expand_grid(r: &VossBatchRange) -> Result<Vec<VossParams>, VossError> {
    fn axis_usize((start, end, step): (usize, usize, usize)) -> Result<Vec<usize>, VossError> {
        if step == 0 || start == end {
            return Ok(vec![start]);
        }
        let mut out = Vec::new();
        if start < end {
            let mut v = start;
            while v <= end {
                out.push(v);
                match v.checked_add(step) {
                    Some(next) => {
                        if next == v {
                            break;
                        }
                        v = next;
                    }
                    None => break,
                }
            }
        } else {
            let mut v = start as isize;
            let end_i = end as isize;
            let st = (step as isize).max(1);
            while v >= end_i {
                out.push(v as usize);
                v -= st;
            }
        }
        if out.is_empty() {
            return Err(VossError::InvalidRange {
                start: start.to_string(),
                end: end.to_string(),
                step: step.to_string(),
            });
        }
        Ok(out)
    }
    fn axis_f64((start, end, step): (f64, f64, f64)) -> Result<Vec<f64>, VossError> {
        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(VossError::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(VossError::InvalidRange {
                start: start.to_string(),
                end: end.to_string(),
                step: step.to_string(),
            });
        }
        Ok(v)
    }
    let periods = axis_usize(r.period)?;
    let predicts = axis_usize(r.predict)?;
    let bandwidths = axis_f64(r.bandwidth)?;
    let cap = periods
        .len()
        .checked_mul(predicts.len())
        .and_then(|x| x.checked_mul(bandwidths.len()))
        .ok_or_else(|| VossError::InvalidRange {
            start: "cap".into(),
            end: "overflow".into(),
            step: "mul".into(),
        })?;
    let mut out = Vec::with_capacity(cap);
    for &p in &periods {
        for &q in &predicts {
            for &b in &bandwidths {
                out.push(VossParams {
                    period: Some(p),
                    predict: Some(q),
                    bandwidth: Some(b),
                });
            }
        }
    }
    if out.is_empty() {
        return Err(VossError::InvalidRange {
            start: "combos".into(),
            end: "empty".into(),
            step: "voss".into(),
        });
    }
    Ok(out)
}

#[inline(always)]
pub fn voss_batch_slice(
    data: &[f64],
    sweep: &VossBatchRange,
    kern: Kernel,
) -> Result<VossBatchOutput, VossError> {
    voss_batch_inner(data, sweep, kern, false)
}

#[inline(always)]
pub fn voss_batch_par_slice(
    data: &[f64],
    sweep: &VossBatchRange,
    kern: Kernel,
) -> Result<VossBatchOutput, VossError> {
    voss_batch_inner(data, sweep, kern, true)
}

#[inline(always)]
pub fn voss_batch_inner_into(
    data: &[f64],
    sweep: &VossBatchRange,
    kern: Kernel,
    parallel: bool,
    voss_out: &mut [f64],
    filt_out: &mut [f64],
) -> Result<Vec<VossParams>, VossError> {
    let combos = expand_grid(sweep)?;

    let first = data
        .iter()
        .position(|x| !x.is_nan())
        .ok_or(VossError::AllValuesNaN)?;
    let rows = combos.len();
    let cols = data.len();

    let expected = rows
        .checked_mul(cols)
        .ok_or_else(|| VossError::InvalidRange {
            start: rows.to_string(),
            end: cols.to_string(),
            step: "rows*cols".into(),
        })?;
    if voss_out.len() != expected {
        return Err(VossError::OutputLengthMismatch {
            expected,
            got: voss_out.len(),
        });
    }
    if filt_out.len() != expected {
        return Err(VossError::OutputLengthMismatch {
            expected,
            got: filt_out.len(),
        });
    }

    let do_row = |row: usize, vo: &mut [f64], fo: &mut [f64]| {
        let p = combos[row].period.unwrap_or(20);
        let q = combos[row].predict.unwrap_or(3);
        let order = 3 * q;
        let min_index = p.max(5).max(order);
        let warm = first + min_index;
        for x in &mut vo[..warm.min(cols)] {
            *x = f64::NAN;
        }
        for x in &mut fo[..warm.min(cols)] {
            *x = f64::NAN;
        }

        match kern {
            Kernel::Scalar | Kernel::ScalarBatch => voss_scalar(
                data,
                p,
                q,
                combos[row].bandwidth.unwrap_or(0.25),
                first,
                vo,
                fo,
            ),
            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
            Kernel::Avx2 | Kernel::Avx2Batch => unsafe {
                voss_avx2(
                    data,
                    p,
                    q,
                    combos[row].bandwidth.unwrap_or(0.25),
                    first,
                    vo,
                    fo,
                )
            },
            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
            Kernel::Avx512 | Kernel::Avx512Batch => unsafe {
                voss_avx512(
                    data,
                    p,
                    q,
                    combos[row].bandwidth.unwrap_or(0.25),
                    first,
                    vo,
                    fo,
                )
            },
            Kernel::Auto => unreachable!(),
            _ => unreachable!(),
        }
    };

    if parallel {
        #[cfg(not(target_arch = "wasm32"))]
        {
            use rayon::prelude::*;
            voss_out
                .par_chunks_mut(cols)
                .zip(filt_out.par_chunks_mut(cols))
                .enumerate()
                .for_each(|(row, (vo, fo))| do_row(row, vo, fo));
        }
        #[cfg(target_arch = "wasm32")]
        {
            for (row, (vo, fo)) in voss_out
                .chunks_mut(cols)
                .zip(filt_out.chunks_mut(cols))
                .enumerate()
            {
                do_row(row, vo, fo);
            }
        }
    } else {
        for (row, (vo, fo)) in voss_out
            .chunks_mut(cols)
            .zip(filt_out.chunks_mut(cols))
            .enumerate()
        {
            do_row(row, vo, fo);
        }
    }

    Ok(combos)
}

#[inline(always)]
fn voss_batch_inner(
    data: &[f64],
    sweep: &VossBatchRange,
    kern: Kernel,
    parallel: bool,
) -> Result<VossBatchOutput, VossError> {
    let combos = expand_grid(sweep)?;

    let first = data
        .iter()
        .position(|x| !x.is_nan())
        .ok_or(VossError::AllValuesNaN)?;
    let max_p = combos.iter().map(|c| c.period.unwrap()).max().unwrap();
    if data.len() - first < max_p {
        return Err(VossError::NotEnoughValidData {
            needed: max_p,
            valid: data.len() - first,
        });
    }

    let rows = combos.len();
    let cols = data.len();
    let _total = rows
        .checked_mul(cols)
        .ok_or_else(|| VossError::InvalidRange {
            start: rows.to_string(),
            end: cols.to_string(),
            step: "rows*cols".into(),
        })?;

    let warmup_periods: Vec<usize> = combos
        .iter()
        .map(|c| {
            let period = c.period.unwrap();
            let predict = c.predict.unwrap();
            let order = 3 * predict;
            let min_index = period.max(5).max(order);
            first + min_index
        })
        .collect();

    let mut voss_mu = make_uninit_matrix(rows, cols);
    let mut filt_mu = make_uninit_matrix(rows, cols);
    init_matrix_prefixes(&mut voss_mu, cols, &warmup_periods);
    init_matrix_prefixes(&mut filt_mu, cols, &warmup_periods);

    let mut voss_guard = core::mem::ManuallyDrop::new(voss_mu);
    let mut filt_guard = core::mem::ManuallyDrop::new(filt_mu);
    let voss_slice: &mut [f64] = unsafe {
        core::slice::from_raw_parts_mut(voss_guard.as_mut_ptr() as *mut f64, voss_guard.len())
    };
    let filt_slice: &mut [f64] = unsafe {
        core::slice::from_raw_parts_mut(filt_guard.as_mut_ptr() as *mut f64, filt_guard.len())
    };

    let do_row = |row: usize, out_voss: &mut [f64], out_filt: &mut [f64]| {
        let prm = &combos[row];
        let period = prm.period.unwrap();
        let predict = prm.predict.unwrap();
        let bandwidth = prm.bandwidth.unwrap();
        match kern {
            Kernel::Scalar => {
                voss_row_scalar(data, first, period, predict, bandwidth, out_voss, out_filt)
            }
            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
            Kernel::Avx2 => unsafe {
                voss_row_avx2(data, first, period, predict, bandwidth, out_voss, out_filt)
            },
            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
            Kernel::Avx512 => unsafe {
                voss_row_avx512(data, first, period, predict, bandwidth, out_voss, out_filt)
            },
            _ => unreachable!(),
        }
    };

    if parallel {
        #[cfg(not(target_arch = "wasm32"))]
        {
            voss_slice
                .par_chunks_mut(cols)
                .zip(filt_slice.par_chunks_mut(cols))
                .enumerate()
                .for_each(|(row, (vo, fo))| do_row(row, vo, fo));
        }

        #[cfg(target_arch = "wasm32")]
        {
            for (row, (vo, fo)) in voss_slice
                .chunks_mut(cols)
                .zip(filt_slice.chunks_mut(cols))
                .enumerate()
            {
                do_row(row, vo, fo);
            }
        }
    } else {
        for (row, (vo, fo)) in voss_slice
            .chunks_mut(cols)
            .zip(filt_slice.chunks_mut(cols))
            .enumerate()
        {
            do_row(row, vo, fo);
        }
    }

    let voss_vec = unsafe {
        let raw_vec = Vec::from_raw_parts(
            voss_guard.as_mut_ptr() as *mut f64,
            voss_guard.len(),
            voss_guard.capacity(),
        );
        core::mem::forget(voss_guard);
        raw_vec
    };
    let filt_vec = unsafe {
        let raw_vec = Vec::from_raw_parts(
            filt_guard.as_mut_ptr() as *mut f64,
            filt_guard.len(),
            filt_guard.capacity(),
        );
        core::mem::forget(filt_guard);
        raw_vec
    };

    Ok(VossBatchOutput {
        voss: voss_vec,
        filt: filt_vec,
        combos,
        rows,
        cols,
    })
}

#[inline(always)]
fn voss_row_scalar(
    data: &[f64],
    first: usize,
    period: usize,
    predict: usize,
    bandwidth: f64,
    voss: &mut [f64],
    filt: &mut [f64],
) {
    use std::f64::consts::PI;

    let order = 3 * predict;
    let min_index = period.max(5).max(order);
    let start = first + min_index;
    if start >= data.len() {
        return;
    }

    let w0 = 2.0 * PI / period as f64;
    let f1 = w0.cos();
    let g1 = (bandwidth * w0).cos();
    let s1 = 1.0 / g1 - (1.0 / (g1 * g1) - 1.0).sqrt();
    let c1 = 0.5 * (1.0 - s1);
    let c2 = f1 * (1.0 + s1);
    let c3 = -s1;

    if start >= 2 {
        filt[start - 2] = 0.0;
        filt[start - 1] = 0.0;
    }

    let mut prev_f1 = 0.0f64;
    let mut prev_f2 = 0.0f64;
    let scale = 0.5 * (3 + order) as f64;

    if order == 0 {
        for i in start..data.len() {
            let diff = data[i] - data[i - 2];
            let t = c3.mul_add(prev_f2, c1 * diff);
            let f = c2.mul_add(prev_f1, t);
            filt[i] = f;
            prev_f2 = prev_f1;
            prev_f1 = f;
            voss[i] = scale * f;
        }
        return;
    }

    let ord_f = order as f64;
    let inv_order = 1.0 / ord_f;
    let mut a_sum = 0.0f64;
    let mut d_sum = 0.0f64;
    let mut ring = vec![0.0f64; order];
    let mut rpos = 0usize;

    for i in start..data.len() {
        let diff = data[i] - data[i - 2];
        let t = c3.mul_add(prev_f2, c1 * diff);
        let f = c2.mul_add(prev_f1, t);
        filt[i] = f;
        prev_f2 = prev_f1;
        prev_f1 = f;

        let sumc = d_sum * inv_order;
        let vi = scale.mul_add(f, -sumc);
        voss[i] = vi;

        let v_new_nz = if vi.is_nan() { 0.0 } else { vi };
        let v_old = ring[rpos];

        let a_prev = a_sum;
        a_sum = a_prev - v_old + v_new_nz;
        d_sum = ord_f.mul_add(v_new_nz, d_sum - a_prev);

        ring[rpos] = v_new_nz;
        rpos += 1;
        if rpos == order {
            rpos = 0;
        }
    }
}

#[inline(always)]
unsafe fn voss_row_scalar_unchecked(
    data: &[f64],
    first: usize,
    period: usize,
    predict: usize,
    bandwidth: f64,
    voss: &mut [f64],
    filt: &mut [f64],
) {
    use std::f64::consts::PI;

    let order = 3 * predict;
    let min_index = period.max(5).max(order);
    let start = first + min_index;
    let len = data.len();
    if start >= len {
        return;
    }

    let w0 = 2.0 * PI / period as f64;
    let f1 = w0.cos();
    let g1 = (bandwidth * w0).cos();

    let s1 = 1.0 / g1 - (1.0 / (g1 * g1) - 1.0).sqrt();
    let c1 = 0.5 * (1.0 - s1);
    let c2 = f1 * (1.0 + s1);
    let c3 = -s1;

    if start >= 2 {
        *filt.get_unchecked_mut(start - 2) = 0.0;
        *filt.get_unchecked_mut(start - 1) = 0.0;
    }

    let mut prev_f1 = 0.0f64;
    let mut prev_f2 = 0.0f64;
    let scale = 0.5 * (3 + order) as f64;

    if order == 0 {
        for i in start..len {
            let xi = *data.get_unchecked(i);
            let xim2 = *data.get_unchecked(i - 2);
            let diff = xi - xim2;
            let t = c3.mul_add(prev_f2, c1 * diff);
            let f = c2.mul_add(prev_f1, t);
            *filt.get_unchecked_mut(i) = f;
            *voss.get_unchecked_mut(i) = scale * f;
            prev_f2 = prev_f1;
            prev_f1 = f;
        }
        return;
    }

    let ord_f = order as f64;
    let inv_order = 1.0 / ord_f;
    let mut a_sum = 0.0f64;
    let mut d_sum = 0.0f64;
    let mut ring = vec![0.0f64; order];
    let mut rpos = 0usize;

    for i in start..len {
        let xi = *data.get_unchecked(i);
        let xim2 = *data.get_unchecked(i - 2);
        let diff = xi - xim2;
        let t = c3.mul_add(prev_f2, c1 * diff);
        let f = c2.mul_add(prev_f1, t);
        *filt.get_unchecked_mut(i) = f;
        prev_f2 = prev_f1;
        prev_f1 = f;

        let sumc = d_sum * inv_order;
        let vi = scale.mul_add(f, -sumc);
        *voss.get_unchecked_mut(i) = vi;

        let v_new_nz = if vi.is_nan() { 0.0 } else { vi };
        let v_old = *ring.get_unchecked(rpos);

        let a_prev = a_sum;
        a_sum = a_prev - v_old + v_new_nz;
        d_sum = ord_f.mul_add(v_new_nz, d_sum - a_prev);

        *ring.get_unchecked_mut(rpos) = v_new_nz;
        rpos += 1;
        if rpos == order {
            rpos = 0;
        }
    }
}

#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
#[inline(always)]
unsafe fn voss_row_avx2(
    data: &[f64],
    first: usize,
    period: usize,
    predict: usize,
    bandwidth: f64,
    voss: &mut [f64],
    filt: &mut [f64],
) {
    voss_avx2(data, period, predict, bandwidth, first, voss, filt)
}

#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
#[inline(always)]
unsafe fn voss_row_avx512(
    data: &[f64],
    first: usize,
    period: usize,
    predict: usize,
    bandwidth: f64,
    voss: &mut [f64],
    filt: &mut [f64],
) {
    voss_avx512(data, period, predict, bandwidth, first, voss, filt)
}

#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
#[inline(always)]
unsafe fn voss_row_avx512_short(
    data: &[f64],
    first: usize,
    period: usize,
    predict: usize,
    bandwidth: f64,
    voss: &mut [f64],
    filt: &mut [f64],
) {
    voss_avx512_short(data, period, predict, bandwidth, first, voss, filt)
}

#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
#[inline(always)]
unsafe fn voss_row_avx512_long(
    data: &[f64],
    first: usize,
    period: usize,
    predict: usize,
    bandwidth: f64,
    voss: &mut [f64],
    filt: &mut [f64],
) {
    voss_avx512_long(data, period, predict, bandwidth, first, voss, filt)
}

#[inline(always)]
pub fn expand_grid_voss(r: &VossBatchRange) -> Result<Vec<VossParams>, VossError> {
    expand_grid(r)
}

#[cfg(feature = "python")]
pub fn register_voss_module(m: &Bound<'_, pyo3::types::PyModule>) -> PyResult<()> {
    m.add_function(wrap_pyfunction!(voss_py, m)?)?;
    m.add_function(wrap_pyfunction!(voss_batch_py, m)?)?;
    m.add_class::<VossStreamPy>()?;
    #[cfg(all(feature = "python", feature = "cuda"))]
    {
        m.add_class::<VossDeviceArrayF32Py>()?;
    }
    Ok(())
}

#[cfg(all(feature = "python", feature = "cuda"))]
#[pyclass(module = "ta_indicators.cuda", unsendable)]
pub struct VossDeviceArrayF32Py {
    pub(crate) buf: Option<DeviceBuffer<f32>>,
    pub(crate) rows: usize,
    pub(crate) cols: usize,
    pub(crate) _ctx: Arc<Context>,
    pub(crate) device_id: u32,
}

#[cfg(all(feature = "python", feature = "cuda"))]
#[pymethods]
impl VossDeviceArrayF32Py {
    #[getter]
    fn __cuda_array_interface__<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyDict>> {
        let d = PyDict::new(py);
        d.set_item("shape", (self.rows, self.cols))?;
        d.set_item("typestr", "<f4")?;
        d.set_item(
            "strides",
            (
                self.cols * std::mem::size_of::<f32>(),
                std::mem::size_of::<f32>(),
            ),
        )?;
        let ptr = self
            .buf
            .as_ref()
            .ok_or_else(|| PyValueError::new_err("buffer already exported via __dlpack__"))?
            .as_device_ptr()
            .as_raw() as usize;
        d.set_item("data", (ptr, false))?;

        d.set_item("version", 3)?;
        Ok(d)
    }

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

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

        let buf = self
            .buf
            .take()
            .ok_or_else(|| PyValueError::new_err("__dlpack__ may only be called once"))?;

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

        export_f32_cuda_dlpack_2d(py, buf, rows, cols, alloc_dev, max_version_bound)
    }
}
#[cfg(all(feature = "python", feature = "cuda"))]
#[pyfunction(name = "voss_cuda_batch_dev")]
#[pyo3(signature = (data_f32, period=(20,20,0), predict=(3,3,0), bandwidth=(0.25,0.25,0.0), device_id=0))]
pub fn voss_cuda_batch_dev_py<'py>(
    py: Python<'py>,
    data_f32: numpy::PyReadonlyArray1<'py, f32>,
    period: (usize, usize, usize),
    predict: (usize, usize, usize),
    bandwidth: (f64, f64, f64),
    device_id: usize,
) -> PyResult<Bound<'py, PyDict>> {
    use crate::cuda::cuda_available;
    if !cuda_available() {
        return Err(PyValueError::new_err("CUDA not available"));
    }
    let slice_in = data_f32.as_slice()?;
    let sweep = VossBatchRange {
        period,
        predict,
        bandwidth,
    };
    let (voss_dev, filt_dev, combos, ctx, dev_id) = py.allow_threads(|| -> PyResult<_> {
        let cuda = crate::cuda::CudaVoss::new(device_id)
            .map_err(|e| PyValueError::new_err(e.to_string()))?;
        let (voss_dev, filt_dev, combos) = cuda
            .voss_batch_dev(slice_in, &sweep)
            .map_err(|e| PyValueError::new_err(e.to_string()))?;
        Ok((
            voss_dev,
            filt_dev,
            combos,
            cuda.context_arc(),
            cuda.device_id(),
        ))
    })?;
    let dict = PyDict::new(py);
    let crate::cuda::moving_averages::DeviceArrayF32 { buf, rows, cols } = voss_dev;
    dict.set_item(
        "voss",
        Py::new(
            py,
            VossDeviceArrayF32Py {
                buf: Some(buf),
                rows,
                cols,
                _ctx: ctx.clone(),
                device_id: dev_id,
            },
        )?,
    )?;
    let crate::cuda::moving_averages::DeviceArrayF32 { buf, rows, cols } = filt_dev;
    dict.set_item(
        "filt",
        Py::new(
            py,
            VossDeviceArrayF32Py {
                buf: Some(buf),
                rows,
                cols,
                _ctx: ctx,
                device_id: dev_id,
            },
        )?,
    )?;
    dict.set_item("rows", combos.len())?;
    dict.set_item("cols", slice_in.len())?;
    use numpy::IntoPyArray;
    dict.set_item(
        "periods",
        combos
            .iter()
            .map(|c| c.period.unwrap_or(20) as u64)
            .collect::<Vec<_>>()
            .into_pyarray(py),
    )?;
    dict.set_item(
        "predicts",
        combos
            .iter()
            .map(|c| c.predict.unwrap_or(3) as u64)
            .collect::<Vec<_>>()
            .into_pyarray(py),
    )?;
    dict.set_item(
        "bandwidths",
        combos
            .iter()
            .map(|c| c.bandwidth.unwrap_or(0.25))
            .collect::<Vec<_>>()
            .into_pyarray(py),
    )?;
    Ok(dict)
}

#[cfg(all(feature = "python", feature = "cuda"))]
#[pyfunction(name = "voss_cuda_many_series_one_param_dev")]
#[pyo3(signature = (data_tm_f32, period=20, predict=3, bandwidth=0.25, device_id=0))]
pub fn voss_cuda_many_series_one_param_dev_py<'py>(
    py: Python<'py>,
    data_tm_f32: numpy::PyReadonlyArray2<'py, f32>,
    period: usize,
    predict: usize,
    bandwidth: f64,
    device_id: usize,
) -> PyResult<(VossDeviceArrayF32Py, VossDeviceArrayF32Py)> {
    use crate::cuda::cuda_available;
    if !cuda_available() {
        return Err(PyValueError::new_err("CUDA not available"));
    }
    let rows = data_tm_f32.shape()[0];
    let cols = data_tm_f32.shape()[1];
    let flat = data_tm_f32.as_slice()?;
    let params = VossParams {
        period: Some(period),
        predict: Some(predict),
        bandwidth: Some(bandwidth),
    };
    let (voss_dev, filt_dev, ctx, dev_id) = py.allow_threads(|| -> PyResult<_> {
        let cuda = crate::cuda::CudaVoss::new(device_id)
            .map_err(|e| PyValueError::new_err(e.to_string()))?;
        let (voss_dev, filt_dev) = cuda
            .voss_many_series_one_param_time_major_dev(flat, cols, rows, &params)
            .map_err(|e| PyValueError::new_err(e.to_string()))?;
        Ok((voss_dev, filt_dev, cuda.context_arc(), cuda.device_id()))
    })?;
    let crate::cuda::moving_averages::DeviceArrayF32 { buf, rows, cols } = voss_dev;
    let voss_py = VossDeviceArrayF32Py {
        buf: Some(buf),
        rows,
        cols,
        _ctx: ctx.clone(),
        device_id: dev_id,
    };
    let crate::cuda::moving_averages::DeviceArrayF32 { buf, rows, cols } = filt_dev;
    let filt_py = VossDeviceArrayF32Py {
        buf: Some(buf),
        rows,
        cols,
        _ctx: ctx,
        device_id: dev_id,
    };
    Ok((voss_py, filt_py))
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::skip_if_unsupported;
    use crate::utilities::data_loader::read_candles_from_csv;

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

    #[test]
    fn test_voss_into_matches_api() -> Result<(), Box<dyn Error>> {
        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
        let candles = read_candles_from_csv(file_path)?;
        let input = VossInput::with_default_candles(&candles);

        let base = voss(&input)?;

        let len = candles.close.len();
        let mut out_voss = vec![0.0f64; len];
        let mut out_filt = vec![0.0f64; len];
        #[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
        {
            voss_into(&input, &mut out_voss, &mut out_filt)?;
        }
        #[cfg(all(target_arch = "wasm32", feature = "wasm"))]
        {
            voss_into_slice(&mut out_voss, &mut out_filt, &input, Kernel::Auto)?;
        }

        assert_eq!(base.voss.len(), out_voss.len());
        assert_eq!(base.filt.len(), out_filt.len());

        for i in 0..len {
            assert!(
                eq_or_both_nan(base.voss[i], out_voss[i]),
                "voss mismatch at {}: got {}, expected {}",
                i,
                out_voss[i],
                base.voss[i]
            );
            assert!(
                eq_or_both_nan(base.filt[i], out_filt[i]),
                "filt mismatch at {}: got {}, expected {}",
                i,
                out_filt[i],
                base.filt[i]
            );
        }

        Ok(())
    }

    fn check_voss_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 = VossParams {
            period: None,
            predict: Some(2),
            bandwidth: None,
        };
        let input = VossInput::from_candles(&candles, "close", params);
        let output = voss_with_kernel(&input, kernel)?;
        assert_eq!(output.voss.len(), candles.close.len());
        assert_eq!(output.filt.len(), candles.close.len());
        Ok(())
    }

    fn check_voss_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 = VossParams {
            period: Some(20),
            predict: Some(3),
            bandwidth: Some(0.25),
        };
        let input = VossInput::from_candles(&candles, "close", params);
        let output = voss_with_kernel(&input, kernel)?;

        let expected_voss_last_five = [
            -290.430249544605,
            -269.74949153549596,
            -241.08179139844515,
            -149.2113276943419,
            -138.60361772412466,
        ];
        let expected_filt_last_five = [
            -228.0283989610523,
            -257.79056527053103,
            -270.3220395771822,
            -257.4282859799144,
            -235.78021136041997,
        ];

        let start = output.voss.len() - 5;
        for (i, &val) in output.voss[start..].iter().enumerate() {
            let expected = expected_voss_last_five[i];
            assert!(
                (val - expected).abs() < 1e-6,
                "[{}] VOSS mismatch at idx {}: got {}, expected {}",
                test_name,
                i,
                val,
                expected
            );
        }
        for (i, &val) in output.filt[start..].iter().enumerate() {
            let expected = expected_filt_last_five[i];
            assert!(
                (val - expected).abs() < 1e-6,
                "[{}] Filt mismatch at idx {}: got {}, expected {}",
                test_name,
                i,
                val,
                expected
            );
        }
        Ok(())
    }

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

        let input = VossInput::with_default_candles(&candles);
        match input.data {
            VossData::Candles { source, .. } => assert_eq!(source, "close"),
            _ => panic!("Expected VossData::Candles"),
        }
        let output = voss_with_kernel(&input, kernel)?;
        assert_eq!(output.voss.len(), candles.close.len());
        Ok(())
    }

    fn check_voss_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 = VossParams {
            period: Some(0),
            predict: None,
            bandwidth: None,
        };
        let input = VossInput::from_slice(&input_data, params);
        let res = voss_with_kernel(&input, kernel);
        assert!(
            res.is_err(),
            "[{}] VOSS should fail with zero period",
            test_name
        );
        Ok(())
    }

    fn check_voss_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 = VossParams {
            period: Some(10),
            predict: None,
            bandwidth: None,
        };
        let input = VossInput::from_slice(&data_small, params);
        let res = voss_with_kernel(&input, kernel);
        assert!(
            res.is_err(),
            "[{}] VOSS should fail with period exceeding length",
            test_name
        );
        Ok(())
    }

    fn check_voss_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 = VossParams {
            period: Some(20),
            predict: None,
            bandwidth: None,
        };
        let input = VossInput::from_slice(&single_point, params);
        let res = voss_with_kernel(&input, kernel);
        assert!(
            res.is_err(),
            "[{}] VOSS should fail with insufficient data",
            test_name
        );
        Ok(())
    }

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

        let first_params = VossParams {
            period: Some(10),
            predict: Some(2),
            bandwidth: Some(0.2),
        };
        let first_input = VossInput::from_candles(&candles, "close", first_params);
        let first_result = voss_with_kernel(&first_input, kernel)?;

        let second_params = VossParams {
            period: Some(10),
            predict: Some(2),
            bandwidth: Some(0.2),
        };
        let second_input = VossInput::from_slice(&first_result.voss, second_params);
        let second_result = voss_with_kernel(&second_input, kernel)?;

        assert_eq!(second_result.voss.len(), first_result.voss.len());
        Ok(())
    }

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

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

        let test_params = vec![
            VossParams::default(),
            VossParams {
                period: Some(2),
                predict: Some(1),
                bandwidth: Some(0.1),
            },
            VossParams {
                period: Some(5),
                predict: Some(2),
                bandwidth: Some(0.2),
            },
            VossParams {
                period: Some(10),
                predict: Some(3),
                bandwidth: Some(0.3),
            },
            VossParams {
                period: Some(15),
                predict: Some(4),
                bandwidth: Some(0.4),
            },
            VossParams {
                period: Some(30),
                predict: Some(5),
                bandwidth: Some(0.5),
            },
            VossParams {
                period: Some(50),
                predict: Some(8),
                bandwidth: Some(0.6),
            },
            VossParams {
                period: Some(100),
                predict: Some(10),
                bandwidth: Some(0.75),
            },
            VossParams {
                period: Some(20),
                predict: Some(1),
                bandwidth: Some(0.1),
            },
            VossParams {
                period: Some(20),
                predict: Some(10),
                bandwidth: Some(0.9),
            },
            VossParams {
                period: Some(20),
                predict: Some(3),
                bandwidth: Some(0.05),
            },
            VossParams {
                period: Some(20),
                predict: Some(3),
                bandwidth: Some(1.0),
            },
            VossParams {
                period: Some(3),
                predict: Some(1),
                bandwidth: Some(0.15),
            },
            VossParams {
                period: Some(7),
                predict: Some(2),
                bandwidth: Some(0.35),
            },
            VossParams {
                period: Some(13),
                predict: Some(3),
                bandwidth: Some(0.45),
            },
        ];

        for (param_idx, params) in test_params.iter().enumerate() {
            let input = VossInput::from_candles(&candles, "close", params.clone());
            let output = voss_with_kernel(&input, kernel)?;

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

                let bits = val.to_bits();

                if bits == 0x11111111_11111111 {
                    panic!(
                        "[{}] Found alloc_with_nan_prefix poison value {} (0x{:016X}) at index {} \
						 in voss output with params: period={}, predict={}, bandwidth={} (param set {})",
                        test_name,
                        val,
                        bits,
                        i,
                        params.period.unwrap_or(20),
                        params.predict.unwrap_or(3),
                        params.bandwidth.unwrap_or(0.25),
                        param_idx
                    );
                }

                if bits == 0x22222222_22222222 {
                    panic!(
                        "[{}] Found init_matrix_prefixes poison value {} (0x{:016X}) at index {} \
						 in voss output with params: period={}, predict={}, bandwidth={} (param set {})",
                        test_name,
                        val,
                        bits,
                        i,
                        params.period.unwrap_or(20),
                        params.predict.unwrap_or(3),
                        params.bandwidth.unwrap_or(0.25),
                        param_idx
                    );
                }

                if bits == 0x33333333_33333333 {
                    panic!(
                        "[{}] Found make_uninit_matrix poison value {} (0x{:016X}) at index {} \
						 in voss output with params: period={}, predict={}, bandwidth={} (param set {})",
                        test_name,
                        val,
                        bits,
                        i,
                        params.period.unwrap_or(20),
                        params.predict.unwrap_or(3),
                        params.bandwidth.unwrap_or(0.25),
                        param_idx
                    );
                }
            }

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

                let bits = val.to_bits();

                if bits == 0x11111111_11111111 {
                    panic!(
                        "[{}] Found alloc_with_nan_prefix poison value {} (0x{:016X}) at index {} \
						 in filt output with params: period={}, predict={}, bandwidth={} (param set {})",
                        test_name,
                        val,
                        bits,
                        i,
                        params.period.unwrap_or(20),
                        params.predict.unwrap_or(3),
                        params.bandwidth.unwrap_or(0.25),
                        param_idx
                    );
                }

                if bits == 0x22222222_22222222 {
                    panic!(
                        "[{}] Found init_matrix_prefixes poison value {} (0x{:016X}) at index {} \
						 in filt output with params: period={}, predict={}, bandwidth={} (param set {})",
                        test_name,
                        val,
                        bits,
                        i,
                        params.period.unwrap_or(20),
                        params.predict.unwrap_or(3),
                        params.bandwidth.unwrap_or(0.25),
                        param_idx
                    );
                }

                if bits == 0x33333333_33333333 {
                    panic!(
                        "[{}] Found make_uninit_matrix poison value {} (0x{:016X}) at index {} \
						 in filt output with params: period={}, predict={}, bandwidth={} (param set {})",
                        test_name,
                        val,
                        bits,
                        i,
                        params.period.unwrap_or(20),
                        params.predict.unwrap_or(3),
                        params.bandwidth.unwrap_or(0.25),
                        param_idx
                    );
                }
            }
        }

        Ok(())
    }

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

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

        let strat = (10usize..=20).prop_flat_map(|period| {
            (
                prop::collection::vec((100.0f64..10000.0f64), 100..200),
                Just(period),
                Just(1usize),
                Just(0.5f64),
            )
        });

        proptest::test_runner::TestRunner::default()
            .run(&strat, |(mut data, period, predict, bandwidth)| {
                for (i, val) in data.iter_mut().enumerate() {
                    *val += (i as f64).sin() * 100.0;
                }

                let params = VossParams {
                    period: Some(period),
                    predict: Some(predict),
                    bandwidth: Some(bandwidth),
                };
                let input = VossInput::from_slice(&data, params);

                let output = voss_with_kernel(&input, kernel).unwrap();
                let ref_output = voss_with_kernel(&input, Kernel::Scalar).unwrap();

                let order = 3 * predict;
                let min_index = period.max(5).max(order);
                let first = data.iter().position(|x| !x.is_nan()).unwrap_or(0);
                let warmup_end = first + min_index;

                prop_assert_eq!(
                    output.voss.len(),
                    data.len(),
                    "[{}] VOSS length mismatch",
                    test_name
                );
                prop_assert_eq!(
                    output.filt.len(),
                    data.len(),
                    "[{}] Filt length mismatch",
                    test_name
                );

                for i in 0..warmup_end.min(5) {
                    prop_assert!(
                        output.voss[i].is_nan(),
                        "[{}] Expected NaN in voss during warmup at idx {}, got {}",
                        test_name,
                        i,
                        output.voss[i]
                    );
                    prop_assert!(
                        output.filt[i].is_nan(),
                        "[{}] Expected NaN in filt during warmup at idx {}, got {}",
                        test_name,
                        i,
                        output.filt[i]
                    );
                }

                for i in 0..data.len() {
                    let voss_val = output.voss[i];
                    let voss_ref = ref_output.voss[i];
                    let filt_val = output.filt[i];
                    let filt_ref = ref_output.filt[i];

                    prop_assert!(
                        voss_val.is_nan() == voss_ref.is_nan(),
                        "[{}] VOSS NaN pattern mismatch at idx {}: {} vs {}",
                        test_name,
                        i,
                        voss_val,
                        voss_ref
                    );
                    prop_assert!(
                        filt_val.is_nan() == filt_ref.is_nan(),
                        "[{}] Filt NaN pattern mismatch at idx {}: {} vs {}",
                        test_name,
                        i,
                        filt_val,
                        filt_ref
                    );

                    if filt_val.is_finite() && filt_ref.is_finite() {
                        let diff = (filt_val - filt_ref).abs();
                        let scale = filt_val.abs().max(filt_ref.abs()).max(1.0);
                        prop_assert!(
                            diff / scale < 1e-4,
                            "[{}] Filt kernel value mismatch at idx {}: {} vs {} (rel diff: {})",
                            test_name,
                            i,
                            filt_val,
                            filt_ref,
                            diff / scale
                        );
                    }
                }

                Ok(())
            })
            .unwrap();
        Ok(())
    }

    macro_rules! generate_all_voss_tests {
        ($($test_fn:ident),*) => {
            paste::paste! {
                $(
                    #[test]
                    fn [<$test_fn _scalar_f64>]() {
                        let _ = $test_fn(stringify!([<$test_fn _scalar_f64>]), Kernel::Scalar);
                    }
                )*
                #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
                $(
                    #[test]
                    fn [<$test_fn _avx2_f64>]() {
                        let _ = $test_fn(stringify!([<$test_fn _avx2_f64>]), Kernel::Avx2);
                    }
                    #[test]
                    fn [<$test_fn _avx512_f64>]() {
                        let _ = $test_fn(stringify!([<$test_fn _avx512_f64>]), Kernel::Avx512);
                    }
                )*
            }
        }
    }
    generate_all_voss_tests!(
        check_voss_partial_params,
        check_voss_accuracy,
        check_voss_default_candles,
        check_voss_zero_period,
        check_voss_period_exceeds_length,
        check_voss_very_small_dataset,
        check_voss_reinput,
        check_voss_no_poison
    );

    #[cfg(feature = "proptest")]
    generate_all_voss_tests!(check_voss_property);

    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 c = read_candles_from_csv(file)?;

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

        let def = VossParams::default();
        let row = output.voss_for(&def).expect("default row missing");

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

        for (i, &v) in row.iter().enumerate() {
            if i < def.period.unwrap() {
                assert!(
                    v.is_nan() || v.abs() < 1e-8,
                    "[{test_name}] expected NaN or 0 at idx {i}, got {v}"
                );
            }
        }
        Ok(())
    }

    fn check_batch_param_grid(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 c = read_candles_from_csv(file)?;

        let builder = VossBatchBuilder::new()
            .kernel(kernel)
            .period_range(10, 14, 2)
            .predict_range(2, 4, 1)
            .bandwidth_range(0.1, 0.2, 0.1);

        let output = builder.apply_candles(&c, "close")?;
        let expected_param_count = ((14 - 10) / 2 + 1) * (4 - 2 + 1) * 2;
        assert_eq!(
            output.combos.len(),
            expected_param_count,
            "[{test_name}] Unexpected grid size: got {}, expected {}",
            output.combos.len(),
            expected_param_count
        );

        for p in &output.combos {
            let row = output.voss_for(p).unwrap();
            assert_eq!(row.len(), c.close.len());
        }
        Ok(())
    }

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

        let data = [f64::NAN, f64::NAN, 1.0, 2.0, 3.0, 4.0];
        let range = VossBatchRange {
            period: (3, 3, 0),
            predict: (2, 2, 0),
            bandwidth: (0.1, 0.1, 0.0),
        };
        let output = VossBatchBuilder::new().kernel(kernel).apply_slice(&data)?;
        for row in 0..output.rows {
            let v = &output.voss[row * output.cols..][..output.cols];
            assert!(
                v.iter().any(|&x| x.is_nan()),
                "[{test_name}] No NaNs found in output row"
            );
        }
        Ok(())
    }

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

        let data = [1.0, 2.0, 3.0];
        let range = VossBatchRange {
            period: (10, 10, 0),
            predict: (3, 3, 0),
            bandwidth: (0.25, 0.25, 0.0),
        };
        let result = VossBatchBuilder::new().kernel(kernel).apply_slice(&data);
        assert!(
            result.is_err(),
            "[{test_name}] Expected error for invalid batch range"
        );
        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 test_configs = vec![
            (2, 10, 2, 1, 3, 1, 0.1, 0.3, 0.1),
            (5, 25, 5, 2, 4, 1, 0.2, 0.4, 0.1),
            (30, 60, 15, 5, 8, 1, 0.3, 0.6, 0.15),
            (2, 5, 1, 1, 2, 1, 0.1, 0.2, 0.05),
            (10, 30, 10, 3, 5, 1, 0.25, 0.5, 0.25),
            (50, 100, 25, 8, 10, 1, 0.5, 0.75, 0.25),
        ];

        for (cfg_idx, config) in test_configs.iter().enumerate() {
            let output = VossBatchBuilder::new()
                .kernel(kernel)
                .period_range(config.0, config.1, config.2)
                .predict_range(config.3, config.4, config.5)
                .bandwidth_range(config.6, config.7, config.8)
                .apply_candles(&c, "close")?;

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

                let bits = val.to_bits();
                let row = idx / output.cols;
                let col = idx % output.cols;
                let combo = &output.combos[row];

                if bits == 0x11111111_11111111 {
                    panic!(
						"[{}] Config {}: Found alloc_with_nan_prefix poison value {} (0x{:016X}) \
						 at row {} col {} (flat index {}) in voss output with params: period={}, predict={}, bandwidth={}",
						test, cfg_idx, val, bits, row, col, idx,
						combo.period.unwrap_or(20),
						combo.predict.unwrap_or(3),
						combo.bandwidth.unwrap_or(0.25)
					);
                }

                if bits == 0x22222222_22222222 {
                    panic!(
						"[{}] Config {}: Found init_matrix_prefixes poison value {} (0x{:016X}) \
						 at row {} col {} (flat index {}) in voss output with params: period={}, predict={}, bandwidth={}",
						test, cfg_idx, val, bits, row, col, idx,
						combo.period.unwrap_or(20),
						combo.predict.unwrap_or(3),
						combo.bandwidth.unwrap_or(0.25)
					);
                }

                if bits == 0x33333333_33333333 {
                    panic!(
						"[{}] Config {}: Found make_uninit_matrix poison value {} (0x{:016X}) \
						 at row {} col {} (flat index {}) in voss output with params: period={}, predict={}, bandwidth={}",
						test, cfg_idx, val, bits, row, col, idx,
						combo.period.unwrap_or(20),
						combo.predict.unwrap_or(3),
						combo.bandwidth.unwrap_or(0.25)
					);
                }
            }

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

                let bits = val.to_bits();
                let row = idx / output.cols;
                let col = idx % output.cols;
                let combo = &output.combos[row];

                if bits == 0x11111111_11111111 {
                    panic!(
						"[{}] Config {}: Found alloc_with_nan_prefix poison value {} (0x{:016X}) \
						 at row {} col {} (flat index {}) in filt output with params: period={}, predict={}, bandwidth={}",
						test, cfg_idx, val, bits, row, col, idx,
						combo.period.unwrap_or(20),
						combo.predict.unwrap_or(3),
						combo.bandwidth.unwrap_or(0.25)
					);
                }

                if bits == 0x22222222_22222222 {
                    panic!(
						"[{}] Config {}: Found init_matrix_prefixes poison value {} (0x{:016X}) \
						 at row {} col {} (flat index {}) in filt output with params: period={}, predict={}, bandwidth={}",
						test, cfg_idx, val, bits, row, col, idx,
						combo.period.unwrap_or(20),
						combo.predict.unwrap_or(3),
						combo.bandwidth.unwrap_or(0.25)
					);
                }

                if bits == 0x33333333_33333333 {
                    panic!(
						"[{}] Config {}: Found make_uninit_matrix poison value {} (0x{:016X}) \
						 at row {} col {} (flat index {}) in filt output with params: period={}, predict={}, bandwidth={}",
						test, cfg_idx, val, bits, row, col, idx,
						combo.period.unwrap_or(20),
						combo.predict.unwrap_or(3),
						combo.bandwidth.unwrap_or(0.25)
					);
                }
            }
        }

        Ok(())
    }

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

    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_param_grid);
    gen_batch_tests!(check_batch_nan_propagation);
    gen_batch_tests!(check_batch_invalid_range);
    gen_batch_tests!(check_batch_no_poison);
}

#[cfg(feature = "python")]
#[pyfunction(name = "voss")]
#[pyo3(signature = (data, period=20, predict=3, bandwidth=0.25, kernel=None))]
pub fn voss_py<'py>(
    py: Python<'py>,
    data: PyReadonlyArray1<'py, f64>,
    period: usize,
    predict: usize,
    bandwidth: f64,
    kernel: Option<&str>,
) -> PyResult<(Bound<'py, PyArray1<f64>>, Bound<'py, PyArray1<f64>>)> {
    let slice_in = data.as_slice()?;
    let kern = validate_kernel(kernel, false)?;

    let params = VossParams {
        period: Some(period),
        predict: Some(predict),
        bandwidth: Some(bandwidth),
    };
    let input = VossInput::from_slice(slice_in, params);

    let (voss_vec, filt_vec) = py
        .allow_threads(|| voss_with_kernel(&input, kern).map(|o| (o.voss, o.filt)))
        .map_err(|e| PyValueError::new_err(e.to_string()))?;

    Ok((voss_vec.into_pyarray(py), filt_vec.into_pyarray(py)))
}

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

#[cfg(feature = "python")]
#[pymethods]
impl VossStreamPy {
    #[new]
    #[pyo3(signature = (period=20, predict=3, bandwidth=0.25))]
    fn new(period: usize, predict: usize, bandwidth: f64) -> PyResult<Self> {
        let params = VossParams {
            period: Some(period),
            predict: Some(predict),
            bandwidth: Some(bandwidth),
        };
        let stream =
            VossStream::try_new(params).map_err(|e| PyValueError::new_err(e.to_string()))?;
        Ok(VossStreamPy { stream })
    }

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

#[cfg(feature = "python")]
#[pyfunction(name = "voss_batch")]
#[pyo3(signature = (data, period_range=(20, 100, 1), predict_range=(3, 3, 0), bandwidth_range=(0.25, 0.25, 0.0), kernel=None))]
pub fn voss_batch_py<'py>(
    py: Python<'py>,
    data: PyReadonlyArray1<'py, f64>,
    period_range: (usize, usize, usize),
    predict_range: (usize, usize, usize),
    bandwidth_range: (f64, f64, f64),
    kernel: Option<&str>,
) -> PyResult<Bound<'py, PyDict>> {
    use numpy::PyArray1;
    let slice_in = data.as_slice()?;

    let sweep = VossBatchRange {
        period: period_range,
        predict: predict_range,
        bandwidth: bandwidth_range,
    };

    let combos = expand_grid(&sweep).map_err(|e| PyValueError::new_err(e.to_string()))?;
    let rows = combos.len();
    let cols = slice_in.len();
    let total = rows
        .checked_mul(cols)
        .ok_or_else(|| PyValueError::new_err("rows*cols overflow in voss_batch_py"))?;

    let voss_arr = unsafe { PyArray1::<f64>::new(py, [total], false) };
    let filt_arr = unsafe { PyArray1::<f64>::new(py, [total], false) };
    let voss_slice = unsafe { voss_arr.as_slice_mut()? };
    let filt_slice = unsafe { filt_arr.as_slice_mut()? };

    let kern = validate_kernel(kernel, true)?;

    let combos = py
        .allow_threads(|| {
            let k = match kern {
                Kernel::Auto => detect_best_batch_kernel(),
                k => k,
            };
            let simd = match k {
                Kernel::Avx512Batch => Kernel::Avx512,
                Kernel::Avx2Batch => Kernel::Avx2,
                Kernel::ScalarBatch => Kernel::Scalar,
                _ => k,
            };
            voss_batch_inner_into(slice_in, &sweep, simd, true, voss_slice, filt_slice)
        })
        .map_err(|e| PyValueError::new_err(e.to_string()))?;

    let dict = PyDict::new(py);
    dict.set_item("voss", voss_arr.reshape((rows, cols))?)?;
    dict.set_item("filt", filt_arr.reshape((rows, cols))?)?;
    dict.set_item(
        "periods",
        combos
            .iter()
            .map(|p| p.period.unwrap_or(20) as u64)
            .collect::<Vec<_>>()
            .into_pyarray(py),
    )?;
    dict.set_item(
        "predicts",
        combos
            .iter()
            .map(|p| p.predict.unwrap_or(3) as u64)
            .collect::<Vec<_>>()
            .into_pyarray(py),
    )?;
    dict.set_item(
        "bandwidths",
        combos
            .iter()
            .map(|p| p.bandwidth.unwrap_or(0.25))
            .collect::<Vec<_>>()
            .into_pyarray(py),
    )?;

    Ok(dict)
}

#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
#[wasm_bindgen]
pub fn voss_js(
    data: &[f64],
    period: usize,
    predict: usize,
    bandwidth: f64,
) -> Result<JsValue, JsValue> {
    let params = VossParams {
        period: Some(period),
        predict: Some(predict),
        bandwidth: Some(bandwidth),
    };
    let input = VossInput::from_slice(data, params);
    let out = voss_with_kernel(&input, detect_best_kernel())
        .map_err(|e| JsValue::from_str(&e.to_string()))?;

    #[derive(Serialize)]
    struct Out {
        voss: Vec<f64>,
        filt: Vec<f64>,
    }

    serde_wasm_bindgen::to_value(&Out {
        voss: out.voss,
        filt: out.filt,
    })
    .map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))
}

#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
#[wasm_bindgen]
pub fn voss_into(
    in_ptr: *const f64,
    voss_ptr: *mut f64,
    filt_ptr: *mut f64,
    len: usize,
    period: usize,
    predict: usize,
    bandwidth: f64,
) -> Result<(), JsValue> {
    if in_ptr.is_null() || voss_ptr.is_null() || filt_ptr.is_null() {
        return Err(JsValue::from_str("Null pointer provided"));
    }

    unsafe {
        let data = std::slice::from_raw_parts(in_ptr, len);
        let params = VossParams {
            period: Some(period),
            predict: Some(predict),
            bandwidth: Some(bandwidth),
        };
        let input = VossInput::from_slice(data, params);

        let in_aliased = in_ptr == voss_ptr as *const f64 || in_ptr == filt_ptr as *const f64;
        let out_aliased = voss_ptr == filt_ptr;

        if in_aliased || out_aliased {
            let mut temp_voss = vec![0.0; len];
            let mut temp_filt = vec![0.0; len];
            voss_into_slice(&mut temp_voss, &mut temp_filt, &input, Kernel::Auto)
                .map_err(|e| JsValue::from_str(&e.to_string()))?;

            let voss_out = std::slice::from_raw_parts_mut(voss_ptr, len);
            let filt_out = std::slice::from_raw_parts_mut(filt_ptr, len);
            voss_out.copy_from_slice(&temp_voss);
            filt_out.copy_from_slice(&temp_filt);
        } else {
            let voss_out = std::slice::from_raw_parts_mut(voss_ptr, len);
            let filt_out = std::slice::from_raw_parts_mut(filt_ptr, len);
            voss_into_slice(voss_out, filt_out, &input, Kernel::Auto)
                .map_err(|e| JsValue::from_str(&e.to_string()))?;
        }
        Ok(())
    }
}

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

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

#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
#[derive(Serialize, Deserialize)]
pub struct VossBatchConfig {
    pub period_range: (usize, usize, usize),
    pub predict_range: (usize, usize, usize),
    pub bandwidth_range: (f64, f64, f64),
}

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

#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
#[wasm_bindgen(js_name = "voss_batch")]
pub fn voss_batch_unified_js(data: &[f64], config: JsValue) -> Result<JsValue, JsValue> {
    let cfg: VossBatchConfig = serde_wasm_bindgen::from_value(config)
        .map_err(|e| JsValue::from_str(&format!("Invalid config: {}", e)))?;

    let sweep = VossBatchRange {
        period: cfg.period_range,
        predict: cfg.predict_range,
        bandwidth: cfg.bandwidth_range,
    };

    let out = voss_batch_inner(data, &sweep, detect_best_kernel(), false)
        .map_err(|e| JsValue::from_str(&e.to_string()))?;

    let js = VossBatchJsOutput {
        voss: out.voss,
        filt: out.filt,
        combos: out.combos,
        rows: out.rows,
        cols: out.cols,
    };

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

#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
#[wasm_bindgen]
pub fn voss_batch_metadata_js(
    period_start: usize,
    period_end: usize,
    period_step: usize,
    predict_start: usize,
    predict_end: usize,
    predict_step: usize,
    bandwidth_start: f64,
    bandwidth_end: f64,
    bandwidth_step: f64,
) -> Result<Vec<f64>, JsValue> {
    let sweep = VossBatchRange {
        period: (period_start, period_end, period_step),
        predict: (predict_start, predict_end, predict_step),
        bandwidth: (bandwidth_start, bandwidth_end, bandwidth_step),
    };

    let combos = expand_grid(&sweep).map_err(|e| JsValue::from_str(&e.to_string()))?;
    let cap = combos.len().checked_mul(3).unwrap_or(0);
    let mut metadata = Vec::with_capacity(cap);

    for combo in combos {
        metadata.push(combo.period.unwrap_or(20) as f64);
        metadata.push(combo.predict.unwrap_or(3) as f64);
        metadata.push(combo.bandwidth.unwrap_or(0.25));
    }

    Ok(metadata)
}

#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
#[wasm_bindgen]
pub fn voss_batch_into(
    in_ptr: *const f64,
    voss_ptr: *mut f64,
    filt_ptr: *mut f64,
    len: usize,
    period_start: usize,
    period_end: usize,
    period_step: usize,
    predict_start: usize,
    predict_end: usize,
    predict_step: usize,
    bandwidth_start: f64,
    bandwidth_end: f64,
    bandwidth_step: f64,
) -> Result<(), JsValue> {
    if in_ptr.is_null() || voss_ptr.is_null() || filt_ptr.is_null() {
        return Err(JsValue::from_str("Null pointer provided"));
    }

    unsafe {
        let data = std::slice::from_raw_parts(in_ptr, len);
        let sweep = VossBatchRange {
            period: (period_start, period_end, period_step),
            predict: (predict_start, predict_end, predict_step),
            bandwidth: (bandwidth_start, bandwidth_end, bandwidth_step),
        };

        let output = voss_batch_par_slice(data, &sweep, Kernel::Auto)
            .map_err(|e| JsValue::from_str(&e.to_string()))?;

        let total_size = output
            .rows
            .checked_mul(output.cols)
            .ok_or_else(|| JsValue::from_str("rows*cols overflow in voss_batch_into"))?;
        if total_size > 0 {
            let voss_out = std::slice::from_raw_parts_mut(voss_ptr, total_size);
            let filt_out = std::slice::from_raw_parts_mut(filt_ptr, total_size);
            voss_out.copy_from_slice(&output.voss);
            filt_out.copy_from_slice(&output.filt);
        }

        Ok(())
    }
}
