#[cfg(all(feature = "python", feature = "cuda"))]
use crate::cuda::cuda_available;
#[cfg(all(feature = "python", feature = "cuda"))]
use crate::cuda::moving_averages::trima_wrapper::DeviceArrayF32Trima;
#[cfg(all(feature = "python", feature = "cuda"))]
use crate::cuda::moving_averages::CudaTrima;
use crate::indicators::sma::{sma, SmaData, SmaInput, SmaParams};
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(feature = "python")]
use crate::utilities::kernel_validation::validate_kernel;
#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
use core::arch::x86_64::*;
use paste::paste;
#[cfg(not(target_arch = "wasm32"))]
use rayon::prelude::*;
use std::convert::AsRef;
use std::mem::MaybeUninit;
use thiserror::Error;

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

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

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

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

impl Default for TrimaParams {
    fn default() -> Self {
        Self { period: Some(30) }
    }
}

#[derive(Debug, Clone)]
pub struct TrimaInput<'a> {
    pub data: TrimaData<'a>,
    pub params: TrimaParams,
}

impl<'a> TrimaInput<'a> {
    #[inline]
    pub fn from_candles(c: &'a Candles, s: &'a str, p: TrimaParams) -> Self {
        Self {
            data: TrimaData::Candles {
                candles: c,
                source: s,
            },
            params: p,
        }
    }
    #[inline]
    pub fn from_slice(sl: &'a [f64], p: TrimaParams) -> Self {
        Self {
            data: TrimaData::Slice(sl),
            params: p,
        }
    }
    #[inline]
    pub fn with_default_candles(c: &'a Candles) -> Self {
        Self::from_candles(c, "close", TrimaParams::default())
    }
    #[inline]
    pub fn get_period(&self) -> usize {
        self.params.period.unwrap_or(30)
    }
}

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

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

impl TrimaBuilder {
    #[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 kernel(mut self, k: Kernel) -> Self {
        self.kernel = k;
        self
    }
    #[inline(always)]
    pub fn apply(self, c: &Candles) -> Result<TrimaOutput, TrimaError> {
        let p = TrimaParams {
            period: self.period,
        };
        let i = TrimaInput::from_candles(c, "close", p);
        trima_with_kernel(&i, self.kernel)
    }
    #[inline(always)]
    pub fn apply_slice(self, d: &[f64]) -> Result<TrimaOutput, TrimaError> {
        let p = TrimaParams {
            period: self.period,
        };
        let i = TrimaInput::from_slice(d, p);
        trima_with_kernel(&i, self.kernel)
    }
    #[inline(always)]
    pub fn into_stream(self) -> Result<TrimaStream, TrimaError> {
        let p = TrimaParams {
            period: self.period,
        };
        TrimaStream::try_new(p)
    }
}

#[derive(Debug, Error)]
pub enum TrimaError {
    #[error("trima: No data provided (input data slice is empty).")]
    EmptyInputData,
    #[error("trima: All values are NaN.")]
    AllValuesNaN,

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

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

    #[error("trima: Period too small: {period}")]
    PeriodTooSmall { period: usize },

    #[error("trima: No data provided.")]
    NoData,

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

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

    #[error("trima: Invalid kernel for batch path: {0:?}")]
    InvalidKernelForBatch(Kernel),
}

#[inline]
pub fn trima(input: &TrimaInput) -> Result<TrimaOutput, TrimaError> {
    trima_with_kernel(input, Kernel::Auto)
}

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

    if period == 0 || period > len {
        return Err(TrimaError::InvalidPeriod {
            period,
            data_len: len,
        });
    }
    if period <= 3 {
        return Err(TrimaError::PeriodTooSmall { period });
    }
    if (len - first) < period {
        return Err(TrimaError::NotEnoughValidData {
            needed: period,
            valid: len - first,
        });
    }

    let m1 = (period + 1) / 2;
    let m2 = period - m1 + 1;

    let chosen = match kernel {
        Kernel::Auto => Kernel::Scalar,
        k => k,
    };

    Ok((data, period, m1, m2, first, chosen))
}

#[inline(always)]
fn trima_compute_into(
    data: &[f64],
    period: usize,
    m1: usize,
    m2: usize,
    first: usize,
    kernel: Kernel,
    out: &mut [f64],
) {
    unsafe {
        #[cfg(all(target_arch = "wasm32", target_feature = "simd128"))]
        {
            if matches!(kernel, Kernel::Scalar | Kernel::ScalarBatch) {
                trima_simd128(data, m1, m2, first, out);
                return;
            }
        }

        match kernel {
            Kernel::Scalar | Kernel::ScalarBatch => {
                trima_scalar_optimized(data, period, m1, m2, first, out)
            }
            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
            Kernel::Avx2 | Kernel::Avx2Batch => {
                trima_avx2(data, period, first, out);
            }
            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
            Kernel::Avx512 | Kernel::Avx512Batch => {
                trima_avx512(data, period, first, out);
            }
            #[cfg(not(all(feature = "nightly-avx", target_arch = "x86_64")))]
            Kernel::Avx2 | Kernel::Avx2Batch | Kernel::Avx512 | Kernel::Avx512Batch => {
                trima_scalar_optimized(data, period, m1, m2, first, out)
            }
            Kernel::Auto => trima_scalar_optimized(data, period, m1, m2, first, out),
        }
    }
}

#[inline(always)]
unsafe fn trima_scalar_optimized(
    data: &[f64],
    period: usize,
    m1: usize,
    m2: usize,
    first: usize,
    out: &mut [f64],
) {
    debug_assert_eq!(data.len(), out.len());
    let n = data.len();
    if n == 0 {
        return;
    }
    let warm = first + period - 1;
    if warm >= n {
        return;
    }

    let inv_m1 = 1.0 / (m1 as f64);
    let inv_m2 = 1.0 / (m2 as f64);

    let base = data.as_ptr().add(first);
    let mut sum1 = 0.0;
    {
        let mut j = 0usize;
        let end_unroll = m1 & !3usize;
        while j < end_unroll {
            sum1 += *base.add(j) + *base.add(j + 1) + *base.add(j + 2) + *base.add(j + 3);
            j += 4;
        }
        while j < m1 {
            sum1 += *base.add(j);
            j += 1;
        }
    }

    let mut ring: Vec<f64> = Vec::with_capacity(m2);
    let mut sum2 = 0.0;

    let mut t = first + m1 - 1;

    let mut p_new = data.as_ptr().add(first + m1);
    let mut p_old = data.as_ptr().add(first);

    {
        let s1 = sum1 * inv_m1;
        ring.push(s1);
        sum2 += s1;
    }

    while ring.len() < m2 {
        t += 1;

        sum1 += *p_new - *p_old;
        p_new = p_new.add(1);
        p_old = p_old.add(1);

        let s1 = sum1 * inv_m1;
        ring.push(s1);
        sum2 += s1;
    }

    *out.get_unchecked_mut(warm) = sum2 * inv_m2;

    let mut head = 0usize;
    t += 1;
    while t < n {
        sum1 += *p_new - *p_old;
        p_new = p_new.add(1);
        p_old = p_old.add(1);

        let new_s1 = sum1 * inv_m1;
        let old_s1 = *ring.get_unchecked(head);
        sum2 += new_s1 - old_s1;
        *ring.get_unchecked_mut(head) = new_s1;

        head += 1;
        if head == m2 {
            head = 0;
        }

        *out.get_unchecked_mut(t) = sum2 * inv_m2;

        t += 1;
    }
}

#[cfg(all(target_arch = "wasm32", target_feature = "simd128"))]
#[inline]
unsafe fn trima_simd128(data: &[f64], m1: usize, m2: usize, first: usize, out: &mut [f64]) {
    use core::arch::wasm32::*;

    const STEP: usize = 2;
    let n = data.len();

    let mut sma1 = vec![f64::NAN; n];

    if first + m1 <= n {
        let chunks = m1 / STEP;
        let tail = m1 % STEP;

        let mut acc = f64x2_splat(0.0);
        for i in 0..chunks {
            let idx = first + i * STEP;
            let d = v128_load(data.as_ptr().add(idx) as *const v128);
            acc = f64x2_add(acc, d);
        }

        let mut sum = f64x2_extract_lane::<0>(acc) + f64x2_extract_lane::<1>(acc);
        if tail != 0 {
            sum += data[first + chunks * STEP];
        }

        sma1[first + m1 - 1] = sum / m1 as f64;

        for i in (first + m1)..n {
            sum += data[i] - data[i - m1];
            sma1[i] = sum / m1 as f64;
        }
    }

    if first + m1 + m2 - 1 <= n {
        let sma1_first = first + m1 - 1;

        let chunks2 = m2 / STEP;
        let tail2 = m2 % STEP;

        let mut acc2 = f64x2_splat(0.0);
        for i in 0..chunks2 {
            let idx = sma1_first + i * STEP;
            let d = v128_load(sma1.as_ptr().add(idx) as *const v128);
            acc2 = f64x2_add(acc2, d);
        }

        let mut sum2 = f64x2_extract_lane::<0>(acc2) + f64x2_extract_lane::<1>(acc2);
        if tail2 != 0 {
            sum2 += sma1[sma1_first + chunks2 * STEP];
        }

        out[sma1_first + m2 - 1] = sum2 / m2 as f64;

        for i in (sma1_first + m2)..n {
            sum2 += sma1[i] - sma1[i - m2];
            out[i] = sum2 / m2 as f64;
        }
    }
}

pub fn trima_with_kernel(input: &TrimaInput, kernel: Kernel) -> Result<TrimaOutput, TrimaError> {
    let (data, period, m1, m2, first, chosen) = trima_prepare(input, kernel)?;
    let len = data.len();
    let warm = first + period - 1;
    let mut out = alloc_with_nan_prefix(len, warm);
    trima_compute_into(data, period, m1, m2, first, chosen, &mut out);
    Ok(TrimaOutput { values: out })
}

#[inline]
pub fn trima_into_slice(
    output: &mut [f64],
    input: &TrimaInput,
    kernel: Kernel,
) -> Result<(), TrimaError> {
    let (data, period, m1, m2, first, chosen) = trima_prepare(input, kernel)?;

    if output.len() != data.len() {
        return Err(TrimaError::OutputLengthMismatch {
            expected: data.len(),
            got: output.len(),
        });
    }

    trima_compute_into(data, period, m1, m2, first, chosen, output);

    let warmup = first + period - 1;
    for i in 0..warmup.min(output.len()) {
        output[i] = f64::NAN;
    }

    Ok(())
}

#[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
#[inline(always)]
pub fn trima_into(input: &TrimaInput, out: &mut [f64]) -> Result<(), TrimaError> {
    let (data, period, m1, m2, first, chosen) = trima_prepare(input, Kernel::Auto)?;

    if out.len() != data.len() {
        return Err(TrimaError::OutputLengthMismatch {
            expected: data.len(),
            got: out.len(),
        });
    }

    let warm = first + period - 1;
    let end = warm.min(out.len());
    let qnan = f64::from_bits(0x7ff8_0000_0000_0000);
    for v in &mut out[..end] {
        *v = qnan;
    }

    trima_compute_into(data, period, m1, m2, first, chosen, out);
    Ok(())
}

#[inline]

pub fn trima_scalar_classic(data: &[f64], period: usize, first: usize, out: &mut [f64]) {
    let n = data.len();
    let m1 = (period + 1) / 2;
    let m2 = period - m1 + 1;

    let mut sma1 = vec![f64::NAN; n];

    if first + m1 <= n {
        let mut sum1 = 0.0;
        for j in 0..m1 {
            sum1 += data[first + j];
        }
        sma1[first + m1 - 1] = sum1 / m1 as f64;

        for i in (first + m1)..n {
            sum1 += data[i] - data[i - m1];
            sma1[i] = sum1 / m1 as f64;
        }
    }

    let warmup_end = first + period - 1;
    if warmup_end < n {
        let first_valid_sma1 = first + m1 - 1;
        let first_valid_sma2 = first_valid_sma1 + m2 - 1;

        if first_valid_sma2 < n {
            let mut sum2 = 0.0;
            for j in 0..m2 {
                sum2 += sma1[first_valid_sma1 + j];
            }

            if warmup_end < n {
                out[warmup_end] = sum2 / m2 as f64;
            }

            for i in (warmup_end + 1)..n {
                let old_idx = i - m2;
                if old_idx >= first_valid_sma1 {
                    sum2 += sma1[i] - sma1[old_idx];
                    out[i] = sum2 / m2 as f64;
                }
            }
        }
    }
}

pub fn trima_scalar(data: &[f64], period: usize, first: usize, out: &mut [f64]) {
    let n = data.len();
    let m1 = (period + 1) / 2;
    let m2 = period - m1 + 1;

    let sma1_in = SmaInput {
        data: SmaData::Slice(data),
        params: SmaParams { period: Some(m1) },
    };

    let pass1 = sma(&sma1_in).unwrap();

    let sma2_in = SmaInput {
        data: SmaData::Slice(&pass1.values),
        params: SmaParams { period: Some(m2) },
    };
    let pass2 = sma(&sma2_in).unwrap();

    let warmup_end = first + period - 1;
    for i in warmup_end..n {
        out[i] = pass2.values[i];
    }
}

#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
#[inline]
pub fn trima_avx512(data: &[f64], period: usize, first: usize, out: &mut [f64]) {
    if period <= 32 {
        unsafe { trima_avx512_short(data, period, first, out) }
    } else {
        unsafe { trima_avx512_long(data, period, first, out) }
    }
}
#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
#[inline]
pub unsafe fn trima_avx2(data: &[f64], period: usize, first: usize, out: &mut [f64]) {
    debug_assert_eq!(data.len(), out.len());
    let n = data.len();
    if n == 0 {
        return;
    }

    let m1 = (period + 1) / 2;
    let m2 = period - m1 + 1;
    let warm = first + period - 1;
    if warm >= n {
        return;
    }

    let inv_m1 = 1.0 / (m1 as f64);
    let inv_m2 = 1.0 / (m2 as f64);

    let mut sum1 = sum_u_avx2(data.as_ptr().add(first), m1);

    let mut ring: Vec<f64> = Vec::with_capacity(m2);
    let mut sum2 = 0.0;

    let mut t = first + m1 - 1;
    let mut p_new = data.as_ptr().add(first + m1);
    let mut p_old = data.as_ptr().add(first);

    {
        let s1 = sum1 * inv_m1;
        ring.push(s1);
        sum2 += s1;
    }

    while ring.len() < m2 {
        t += 1;
        sum1 += *p_new - *p_old;
        p_new = p_new.add(1);
        p_old = p_old.add(1);

        let s1 = sum1 * inv_m1;
        ring.push(s1);
        sum2 += s1;
    }

    *out.get_unchecked_mut(warm) = sum2 * inv_m2;

    let mut head = 0usize;
    t += 1;
    while t < n {
        sum1 += *p_new - *p_old;
        p_new = p_new.add(1);
        p_old = p_old.add(1);

        let new_s1 = sum1 * inv_m1;
        let old_s1 = *ring.get_unchecked(head);
        sum2 += new_s1 - old_s1;
        *ring.get_unchecked_mut(head) = new_s1;

        head += 1;
        if head == m2 {
            head = 0;
        }

        *out.get_unchecked_mut(t) = sum2 * inv_m2;
        t += 1;
    }
}
#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
#[inline]
pub unsafe fn trima_avx512_short(data: &[f64], period: usize, first: usize, out: &mut [f64]) {
    debug_assert_eq!(data.len(), out.len());
    let n = data.len();
    if n == 0 {
        return;
    }

    let m1 = (period + 1) / 2;
    let m2 = period - m1 + 1;
    let warm = first + period - 1;
    if warm >= n {
        return;
    }

    let inv_m1 = 1.0 / (m1 as f64);
    let inv_m2 = 1.0 / (m2 as f64);

    let mut sum1 = sum_u_avx2(data.as_ptr().add(first), m1);

    let mut ring: Vec<f64> = Vec::with_capacity(m2);
    let mut sum2 = 0.0;

    let mut t = first + m1 - 1;
    let mut p_new = data.as_ptr().add(first + m1);
    let mut p_old = data.as_ptr().add(first);

    let s1 = sum1 * inv_m1;
    ring.push(s1);
    sum2 += s1;

    while ring.len() < m2 {
        t += 1;
        sum1 += *p_new - *p_old;
        p_new = p_new.add(1);
        p_old = p_old.add(1);

        let s1 = sum1 * inv_m1;
        ring.push(s1);
        sum2 += s1;
    }

    *out.get_unchecked_mut(warm) = sum2 * inv_m2;

    let mut head = 0usize;
    t += 1;
    while t < n {
        sum1 += *p_new - *p_old;
        p_new = p_new.add(1);
        p_old = p_old.add(1);

        let new_s1 = sum1 * inv_m1;
        let old_s1 = *ring.get_unchecked(head);
        sum2 += new_s1 - old_s1;
        *ring.get_unchecked_mut(head) = new_s1;

        head += 1;
        if head == m2 {
            head = 0;
        }

        *out.get_unchecked_mut(t) = sum2 * inv_m2;
        t += 1;
    }
}
#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
#[inline]
pub unsafe fn trima_avx512_long(data: &[f64], period: usize, first: usize, out: &mut [f64]) {
    trima_avx512_short(data, period, first, out)
}

#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
#[inline(always)]
unsafe fn hsum256d(v: __m256d) -> f64 {
    let hi = _mm256_extractf128_pd(v, 1);
    let lo = _mm256_castpd256_pd128(v);
    let sum128 = _mm_add_pd(lo, hi);
    let shuffled = _mm_unpackhi_pd(sum128, sum128);
    _mm_cvtsd_f64(_mm_add_sd(sum128, shuffled))
}

#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
#[inline(always)]
unsafe fn sum_u_avx2(ptr: *const f64, len: usize) -> f64 {
    let mut acc = _mm256_setzero_pd();
    let mut p = ptr;
    let chunks = len / 4;
    for _ in 0..chunks {
        acc = _mm256_add_pd(acc, _mm256_loadu_pd(p));
        p = p.add(4);
    }
    let mut s = hsum256d(acc);
    for i in 0..(len & 3) {
        s += *p.add(i);
    }
    s
}

#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
#[inline(always)]
unsafe fn hsum512d(v: __m512d) -> f64 {
    let v4 = _mm256_add_pd(_mm512_castpd512_pd256(v), _mm512_extractf64x4_pd(v, 1));
    let v2 = _mm_add_pd(_mm256_castpd256_pd128(v4), _mm256_extractf128_pd(v4, 1));
    let hi = _mm_unpackhi_pd(v2, v2);
    _mm_cvtsd_f64(_mm_add_sd(v2, hi))
}

#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
#[inline(always)]
unsafe fn sum_u_avx512(ptr: *const f64, len: usize) -> f64 {
    let mut acc = _mm512_setzero_pd();
    let mut p = ptr;
    let chunks = len / 8;
    for _ in 0..chunks {
        acc = _mm512_add_pd(acc, _mm512_loadu_pd(p));
        p = p.add(8);
    }
    let mut s = hsum512d(acc);
    for i in 0..(len & 7) {
        s += *p.add(i);
    }
    s
}

#[inline(always)]
pub fn trima_row_scalar(
    data: &[f64],
    first: usize,
    period: usize,
    _stride: usize,
    _w_ptr: *const f64,
    _inv_n: f64,
    out: &mut [f64],
) {
    let m1 = (period + 1) / 2;
    let m2 = period - m1 + 1;
    unsafe { trima_scalar_optimized(data, period, m1, m2, first, out) }
}

#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
#[inline(always)]
pub unsafe fn trima_row_avx2(
    data: &[f64],
    first: usize,
    period: usize,
    _stride: usize,
    _w_ptr: *const f64,
    _inv_n: f64,
    out: &mut [f64],
) {
    trima_avx2(data, period, first, out)
}

#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
#[inline(always)]
pub unsafe fn trima_row_avx512(
    data: &[f64],
    first: usize,
    period: usize,
    _stride: usize,
    _w_ptr: *const f64,
    _inv_n: f64,
    out: &mut [f64],
) {
    trima_avx512(data, period, first, out)
}
#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
#[inline(always)]
pub unsafe fn trima_row_avx512_short(
    data: &[f64],
    first: usize,
    period: usize,
    _stride: usize,
    _w_ptr: *const f64,
    _inv_n: f64,
    out: &mut [f64],
) {
    trima_avx512_short(data, period, first, out)
}
#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
#[inline(always)]
pub unsafe fn trima_row_avx512_long(
    data: &[f64],
    first: usize,
    period: usize,
    _stride: usize,
    _w_ptr: *const f64,
    _inv_n: f64,
    out: &mut [f64],
) {
    trima_avx512_long(data, period, first, out)
}

#[derive(Debug, Clone)]
pub struct TrimaStream {
    period: usize,
    m1: usize,
    m2: usize,

    inv_m1: f64,
    inv_m2: f64,

    buf1: Box<[f64]>,
    sum1: f64,
    head1: usize,
    filled1: bool,

    buf2: Box<[f64]>,
    sum2: f64,
    head2: usize,
    filled2: bool,
}

impl TrimaStream {
    #[inline]
    pub fn try_new(params: TrimaParams) -> Result<Self, TrimaError> {
        let period = params.period.unwrap_or(30);
        if period <= 3 {
            return Err(TrimaError::PeriodTooSmall { period });
        }

        let m1 = (period + 1) / 2;
        let m2 = period - m1 + 1;

        Ok(Self {
            period,
            m1,
            m2,
            inv_m1: 1.0 / (m1 as f64),
            inv_m2: 1.0 / (m2 as f64),

            buf1: vec![f64::NAN; m1].into_boxed_slice(),
            sum1: 0.0,
            head1: 0,
            filled1: false,

            buf2: vec![f64::NAN; m2].into_boxed_slice(),
            sum2: 0.0,
            head2: 0,
            filled2: false,
        })
    }

    #[inline(always)]
    pub fn update(&mut self, x: f64) -> Option<f64> {
        let old1 = self.buf1[self.head1];
        self.buf1[self.head1] = x;
        self.head1 += 1;
        if self.head1 == self.m1 {
            self.head1 = 0;
            self.filled1 = true;
        }

        if !old1.is_nan() {
            self.sum1 -= old1;
        }
        if !x.is_nan() {
            self.sum1 += x;
        }

        if !self.filled1 {
            return None;
        }

        let s1 = self.sum1 * self.inv_m1;

        let old2 = self.buf2[self.head2];
        self.buf2[self.head2] = s1;
        self.head2 += 1;
        if self.head2 == self.m2 {
            self.head2 = 0;
            self.filled2 = true;
        }

        if !old2.is_nan() {
            self.sum2 -= old2;
        }

        self.sum2 += s1;

        if self.filled2 {
            Some(self.sum2 * self.inv_m2)
        } else {
            None
        }
    }
}

#[derive(Clone, Debug)]
pub struct TrimaBatchRange {
    pub period: (usize, usize, usize),
}

impl Default for TrimaBatchRange {
    fn default() -> Self {
        Self {
            period: (14, 263, 1),
        }
    }
}

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

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

    #[inline]
    pub fn period_range(mut self, start: usize, end: usize, step: usize) -> Self {
        self.range.period = (start, end, step);
        self
    }
    #[inline]
    pub fn period_static(mut self, p: usize) -> Self {
        self.range.period = (p, p, 0);
        self
    }

    pub fn apply_slice(self, data: &[f64]) -> Result<TrimaBatchOutput, TrimaError> {
        trima_batch_with_kernel(data, &self.range, self.kernel)
    }

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

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

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

pub fn trima_batch_with_kernel(
    data: &[f64],
    sweep: &TrimaBatchRange,
    k: Kernel,
) -> Result<TrimaBatchOutput, TrimaError> {
    let kernel = match k {
        Kernel::Auto => detect_best_batch_kernel(),
        other if other.is_batch() => other,
        other => return Err(TrimaError::InvalidKernelForBatch(other)),
    };

    let simd = match kernel {
        Kernel::Avx512Batch => Kernel::Avx512,
        Kernel::Avx2Batch => Kernel::Avx2,
        Kernel::ScalarBatch => Kernel::Scalar,
        Kernel::Avx512 | Kernel::Avx2 | Kernel::Scalar => kernel,
        _ => Kernel::Scalar,
    };

    trima_batch_par_slice(data, sweep, simd)
}

#[derive(Clone, Debug)]
pub struct TrimaBatchOutput {
    pub values: Vec<f64>,
    pub combos: Vec<TrimaParams>,
    pub rows: usize,
    pub cols: usize,
}
impl TrimaBatchOutput {
    pub fn row_for_params(&self, p: &TrimaParams) -> Option<usize> {
        self.combos
            .iter()
            .position(|c| c.period.unwrap_or(14) == p.period.unwrap_or(14))
    }

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

#[inline(always)]
fn expand_grid(r: &TrimaBatchRange) -> Vec<TrimaParams> {
    fn axis_usize((start, end, step): (usize, usize, usize)) -> Result<Vec<usize>, TrimaError> {
        if step == 0 || start == end {
            return Ok(vec![start]);
        }
        let (lo, hi) = if start <= end {
            (start, end)
        } else {
            (end, start)
        };
        let mut v = Vec::new();
        let mut cur = lo;
        while cur <= hi {
            v.push(cur);
            cur = cur
                .checked_add(step)
                .ok_or(TrimaError::InvalidRange { start, end, step })?;
            if cur == *v.last().unwrap() {
                break;
            }
        }
        if v.is_empty() {
            return Err(TrimaError::InvalidRange { start, end, step });
        }
        Ok(v)
    }

    let periods = match axis_usize(r.period) {
        Ok(v) => v,
        Err(_) => return Vec::new(),
    };
    let mut out = Vec::with_capacity(periods.len());
    for &p in &periods {
        out.push(TrimaParams { period: Some(p) });
    }
    out
}

#[inline(always)]
pub fn trima_batch_slice(
    data: &[f64],
    sweep: &TrimaBatchRange,
    kern: Kernel,
) -> Result<TrimaBatchOutput, TrimaError> {
    trima_batch_inner(data, sweep, kern, false)
}

#[inline(always)]
pub fn trima_batch_par_slice(
    data: &[f64],
    sweep: &TrimaBatchRange,
    kern: Kernel,
) -> Result<TrimaBatchOutput, TrimaError> {
    trima_batch_inner(data, sweep, kern, true)
}

#[inline(always)]
fn trima_batch_inner(
    data: &[f64],
    sweep: &TrimaBatchRange,
    kern: Kernel,
    parallel: bool,
) -> Result<TrimaBatchOutput, TrimaError> {
    let combos = expand_grid(sweep);
    if combos.is_empty() {
        return Err(TrimaError::InvalidRange {
            start: sweep.period.0,
            end: sweep.period.1,
            step: sweep.period.2,
        });
    }

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

    let rows = combos.len();
    let cols = data.len();
    let warm: Vec<usize> = combos
        .iter()
        .map(|c| first + c.period.unwrap() - 1)
        .collect();

    let _total = rows.checked_mul(cols).ok_or(TrimaError::InvalidRange {
        start: sweep.period.0,
        end: sweep.period.1,
        step: sweep.period.2,
    })?;
    let mut raw = make_uninit_matrix(rows, cols);
    unsafe { init_matrix_prefixes(&mut raw, cols, &warm) };

    let do_row = |row: usize, dst_mu: &mut [MaybeUninit<f64>]| unsafe {
        let period = combos[row].period.unwrap();

        let out_row =
            core::slice::from_raw_parts_mut(dst_mu.as_mut_ptr() as *mut f64, dst_mu.len());

        match kern {
            Kernel::Scalar => {
                trima_row_scalar(data, first, period, 0, core::ptr::null(), 1.0, out_row)
            }
            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
            Kernel::Avx2 => trima_row_avx2(data, first, period, 0, core::ptr::null(), 1.0, out_row),
            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
            Kernel::Avx512 => {
                trima_row_avx512(data, first, period, 0, core::ptr::null(), 1.0, out_row)
            }
            _ => trima_row_scalar(data, first, period, 0, core::ptr::null(), 1.0, out_row),
        }
    };

    if parallel {
        #[cfg(not(target_arch = "wasm32"))]
        {
            raw.par_chunks_mut(cols)
                .enumerate()
                .for_each(|(row, slice)| do_row(row, slice));
        }

        #[cfg(target_arch = "wasm32")]
        {
            for (row, slice) in raw.chunks_mut(cols).enumerate() {
                do_row(row, slice);
            }
        }
    } else {
        for (row, slice) in raw.chunks_mut(cols).enumerate() {
            do_row(row, slice);
        }
    }

    let mut buf_guard = core::mem::ManuallyDrop::new(raw);
    let values = unsafe {
        Vec::from_raw_parts(
            buf_guard.as_mut_ptr() as *mut f64,
            buf_guard.len(),
            buf_guard.capacity(),
        )
    };

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

#[inline(always)]
pub fn trima_batch_inner_into(
    data: &[f64],
    sweep: &TrimaBatchRange,
    kern: Kernel,
    parallel: bool,
    out: &mut [f64],
) -> Result<Vec<TrimaParams>, TrimaError> {
    let combos = expand_grid(sweep);
    if combos.is_empty() {
        return Err(TrimaError::InvalidRange {
            start: sweep.period.0,
            end: sweep.period.1,
            step: sweep.period.2,
        });
    }

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

    let rows = combos.len();
    let cols = data.len();
    let expected = rows.checked_mul(cols).ok_or(TrimaError::InvalidRange {
        start: sweep.period.0,
        end: sweep.period.1,
        step: sweep.period.2,
    })?;
    if out.len() != expected {
        return Err(TrimaError::OutputLengthMismatch {
            expected,
            got: out.len(),
        });
    }

    let warm: Vec<usize> = combos
        .iter()
        .map(|c| first + c.period.unwrap() - 1)
        .collect();
    let out_mu = unsafe {
        core::slice::from_raw_parts_mut(out.as_mut_ptr() as *mut MaybeUninit<f64>, out.len())
    };
    init_matrix_prefixes(out_mu, cols, &warm);

    let do_row = |row: usize, dst_mu: &mut [MaybeUninit<f64>]| unsafe {
        let period = combos[row].period.unwrap();
        let out_row =
            core::slice::from_raw_parts_mut(dst_mu.as_mut_ptr() as *mut f64, dst_mu.len());
        match kern {
            Kernel::Scalar => {
                trima_row_scalar(data, first, period, 0, core::ptr::null(), 1.0, out_row)
            }
            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
            Kernel::Avx2 => trima_row_avx2(data, first, period, 0, core::ptr::null(), 1.0, out_row),
            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
            Kernel::Avx512 => {
                trima_row_avx512(data, first, period, 0, core::ptr::null(), 1.0, out_row)
            }
            _ => trima_row_scalar(data, first, period, 0, core::ptr::null(), 1.0, out_row),
        }
    };

    if parallel {
        #[cfg(not(target_arch = "wasm32"))]
        {
            out_mu
                .par_chunks_mut(cols)
                .enumerate()
                .for_each(|(row, slice)| do_row(row, slice));
        }
        #[cfg(target_arch = "wasm32")]
        {
            for (row, slice) in out_mu.chunks_mut(cols).enumerate() {
                do_row(row, slice);
            }
        }
    } else {
        for (row, slice) in out_mu.chunks_mut(cols).enumerate() {
            do_row(row, slice);
        }
    }

    Ok(combos)
}

#[cfg(feature = "python")]
use pyo3::exceptions::PyValueError;
#[cfg(feature = "python")]
use pyo3::prelude::*;

#[cfg(feature = "python")]
#[pyfunction(name = "trima")]
#[pyo3(signature = (data, period, kernel=None))]

pub fn trima_py<'py>(
    py: Python<'py>,
    data: numpy::PyReadonlyArray1<'py, f64>,
    period: usize,
    kernel: Option<&str>,
) -> PyResult<Bound<'py, numpy::PyArray1<f64>>> {
    use numpy::{IntoPyArray, PyArrayMethods};

    let slice_in = data.as_slice()?;
    let kern = validate_kernel(kernel, false)?;

    let params = TrimaParams {
        period: Some(period),
    };
    let trima_in = TrimaInput::from_slice(slice_in, params);

    let result_vec: Vec<f64> = py
        .allow_threads(|| trima_with_kernel(&trima_in, kern).map(|o| o.values))
        .map_err(|e| PyValueError::new_err(e.to_string()))?;

    Ok(result_vec.into_pyarray(py))
}

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

#[cfg(feature = "python")]
#[pymethods]
impl TrimaStreamPy {
    #[new]
    fn new(period: Option<usize>) -> PyResult<Self> {
        let params = TrimaParams { period };
        let stream =
            TrimaStream::try_new(params).map_err(|e| PyValueError::new_err(e.to_string()))?;
        Ok(TrimaStreamPy { stream })
    }

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

#[cfg(feature = "python")]
#[pyfunction(name = "trima_batch")]
#[pyo3(signature = (data, period_range, kernel=None))]

pub fn trima_batch_py<'py>(
    py: Python<'py>,
    data: numpy::PyReadonlyArray1<'py, f64>,
    period_range: (usize, usize, usize),
    kernel: Option<&str>,
) -> PyResult<Bound<'py, pyo3::types::PyDict>> {
    use numpy::{IntoPyArray, PyArray1, PyArrayMethods};
    use pyo3::types::PyDict;

    let slice_in = data.as_slice()?;
    let sweep = TrimaBatchRange {
        period: period_range,
    };

    let combos = expand_grid(&sweep);
    let rows = combos.len();
    let cols = slice_in.len();

    let total = rows
        .checked_mul(cols)
        .ok_or_else(|| PyValueError::new_err("size overflow: rows*cols exceeds usize"))?;

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

    let kern = validate_kernel(kernel, true)?;
    let kern = match kern {
        Kernel::Auto => detect_best_batch_kernel(),
        k => k,
    };
    let simd = match kern {
        Kernel::Avx512Batch => Kernel::Avx512,
        Kernel::Avx2Batch => Kernel::Avx2,
        Kernel::ScalarBatch => Kernel::Scalar,
        Kernel::Avx512 | Kernel::Avx2 | Kernel::Scalar => kern,
        _ => Kernel::Scalar,
    };

    let combos = py
        .allow_threads(|| trima_batch_inner_into(slice_in, &sweep, simd, 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(
        "periods",
        combos
            .iter()
            .map(|p| p.period.unwrap_or(30) as u64)
            .collect::<Vec<_>>()
            .into_pyarray(py),
    )?;
    Ok(dict)
}

#[cfg(all(feature = "python", feature = "cuda"))]
#[pyfunction(name = "trima_cuda_batch_dev")]
#[pyo3(signature = (data, period_range, device_id=0))]
pub fn trima_cuda_batch_dev_py(
    py: Python<'_>,
    data: numpy::PyReadonlyArray1<'_, f64>,
    period_range: (usize, usize, usize),
    device_id: usize,
) -> PyResult<DeviceArrayF32TrimaPy> {
    use numpy::PyArrayMethods;

    if !cuda_available() {
        return Err(PyValueError::new_err("CUDA not available"));
    }

    let slice_in = data.as_slice()?;
    let sweep = TrimaBatchRange {
        period: period_range,
    };

    let data_f32: Vec<f32> = slice_in.iter().map(|&v| v as f32).collect();

    let inner = py.allow_threads(|| {
        let cuda = CudaTrima::new(device_id).map_err(|e| PyValueError::new_err(e.to_string()))?;
        cuda.trima_batch_dev(&data_f32, &sweep)
            .map_err(|e| PyValueError::new_err(e.to_string()))
    })?;

    Ok(DeviceArrayF32TrimaPy { inner: Some(inner) })
}

#[cfg(all(feature = "python", feature = "cuda"))]
#[pyfunction(name = "trima_cuda_many_series_one_param_dev")]
#[pyo3(signature = (data_tm_f32, period, device_id=0))]
pub fn trima_cuda_many_series_one_param_dev_py(
    py: Python<'_>,
    data_tm_f32: numpy::PyReadonlyArray2<'_, f32>,
    period: usize,
    device_id: usize,
) -> PyResult<DeviceArrayF32TrimaPy> {
    use numpy::PyUntypedArrayMethods;

    if !cuda_available() {
        return Err(PyValueError::new_err("CUDA not available"));
    }

    let flat_in = data_tm_f32.as_slice()?;
    let rows = data_tm_f32.shape()[0];
    let cols = data_tm_f32.shape()[1];
    let params = TrimaParams {
        period: Some(period),
    };

    let inner = py.allow_threads(|| {
        let cuda = CudaTrima::new(device_id).map_err(|e| PyValueError::new_err(e.to_string()))?;
        cuda.trima_multi_series_one_param_time_major_dev(flat_in, cols, rows, &params)
            .map_err(|e| PyValueError::new_err(e.to_string()))
    })?;

    Ok(DeviceArrayF32TrimaPy { inner: Some(inner) })
}

#[cfg(all(feature = "python", feature = "cuda"))]
#[pyclass(
    module = "ta_indicators.cuda",
    name = "DeviceArrayF32Trima",
    unsendable
)]
pub struct DeviceArrayF32TrimaPy {
    pub(crate) inner: Option<DeviceArrayF32Trima>,
}

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

        let ptr_val: usize = if inner.rows == 0 || inner.cols == 0 {
            0
        } else {
            inner.device_ptr() as usize
        };
        d.set_item("data", (ptr_val, false))?;

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

    fn __dlpack_device__(&self) -> PyResult<(i32, i32)> {
        let inner = self
            .inner
            .as_ref()
            .ok_or_else(|| PyValueError::new_err("buffer already exported via __dlpack__"))?;
        Ok((2, inner.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<PyObject>,
        max_version: Option<PyObject>,
        dl_device: Option<PyObject>,
        copy: Option<PyObject>,
    ) -> PyResult<PyObject> {
        let (kdl, alloc_dev) = self.__dlpack_device__()?;

        if let Some(dev_obj) = dl_device.as_ref() {
            if let Ok((dev_ty, dev_id)) = dev_obj.extract::<(i32, i32)>(py) {
                if dev_ty != kdl || dev_id != alloc_dev {
                    let wants_copy = copy
                        .as_ref()
                        .and_then(|c| c.extract::<bool>(py).ok())
                        .unwrap_or(false);
                    if wants_copy {
                        return Err(PyValueError::new_err(
                            "device copy not implemented for __dlpack__",
                        ));
                    } else {
                        return Err(PyValueError::new_err("dl_device mismatch for __dlpack__"));
                    }
                }
            }
        }

        let _ = stream;

        let inner = self
            .inner
            .take()
            .ok_or_else(|| PyValueError::new_err("buffer already exported via __dlpack__"))?;

        let DeviceArrayF32Trima {
            buf,
            rows,
            cols,
            ctx: _ctx,
            device_id,
        } = inner;

        if device_id as i32 != alloc_dev {
            return Err(PyValueError::new_err("device id mismatch for __dlpack__"));
        }

        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(target_arch = "wasm32", feature = "wasm"))]
use serde::{Deserialize, Serialize};
#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
use serde_wasm_bindgen;
#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
use wasm_bindgen::prelude::*;

#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
#[wasm_bindgen]

pub fn trima_js(data: &[f64], period: usize) -> Result<Vec<f64>, JsValue> {
    let params = TrimaParams {
        period: Some(period),
    };
    let input = TrimaInput::from_slice(data, params);

    let mut output = vec![0.0; data.len()];

    trima_into_slice(&mut output, &input, Kernel::Auto)
        .map_err(|e| JsValue::from_str(&e.to_string()))?;

    Ok(output)
}

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

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

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

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

    let js_output = TrimaBatchJsOutput {
        values: output.values,
        combos: output.combos,
        rows: output.rows,
        cols: output.cols,
    };
    serde_wasm_bindgen::to_value(&js_output)
        .map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))
}

#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
#[wasm_bindgen]

pub fn trima_batch_js(
    data: &[f64],
    period_start: usize,
    period_end: usize,
    period_step: usize,
) -> Result<Vec<f64>, JsValue> {
    let sweep = TrimaBatchRange {
        period: (period_start, period_end, period_step),
    };

    trima_batch_inner(data, &sweep, Kernel::Auto, false)
        .map(|output| output.values)
        .map_err(|e| JsValue::from_str(&e.to_string()))
}

#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
#[wasm_bindgen]

pub fn trima_batch_metadata_js(
    period_start: usize,
    period_end: usize,
    period_step: usize,
) -> Result<Vec<f64>, JsValue> {
    let sweep = TrimaBatchRange {
        period: (period_start, period_end, period_step),
    };

    let combos = expand_grid(&sweep);
    let metadata: Vec<f64> = combos
        .iter()
        .map(|combo| combo.period.unwrap_or(30) as f64)
        .collect();

    Ok(metadata)
}

#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
#[wasm_bindgen]
pub fn trima_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 trima_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 trima_into(
    in_ptr: *const f64,
    out_ptr: *mut f64,
    len: usize,
    period: usize,
) -> Result<(), JsValue> {
    if in_ptr.is_null() || out_ptr.is_null() {
        return Err(JsValue::from_str("null pointer passed to trima_into"));
    }
    unsafe {
        let data = std::slice::from_raw_parts(in_ptr, len);
        if period == 0 || period > len {
            return Err(JsValue::from_str("Invalid period"));
        }
        let params = TrimaParams {
            period: Some(period),
        };
        if in_ptr == out_ptr {
            let mut temp = vec![0.0; len];
            let input = TrimaInput::from_slice(data, params);
            trima_into_slice(&mut temp, &input, Kernel::Auto)
                .map_err(|e| JsValue::from_str(&e.to_string()))?;
            let out = std::slice::from_raw_parts_mut(out_ptr, len);
            out.copy_from_slice(&temp);
        } else {
            let input = TrimaInput::from_slice(data, params);
            let out = std::slice::from_raw_parts_mut(out_ptr, len);
            trima_into_slice(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 trima_batch_into(
    in_ptr: *const f64,
    out_ptr: *mut f64,
    len: usize,
    period_start: usize,
    period_end: usize,
    period_step: usize,
) -> Result<usize, JsValue> {
    if in_ptr.is_null() || out_ptr.is_null() {
        return Err(JsValue::from_str("null pointer passed to trima_batch_into"));
    }

    unsafe {
        let data = std::slice::from_raw_parts(in_ptr, len);

        let sweep = TrimaBatchRange {
            period: (period_start, period_end, period_step),
        };

        let combos = expand_grid(&sweep);
        let rows = combos.len();
        let cols = len;

        let out = std::slice::from_raw_parts_mut(out_ptr, rows * cols);

        trima_batch_inner_into(data, &sweep, Kernel::Auto, false, out)
            .map_err(|e| JsValue::from_str(&e.to_string()))?;

        Ok(rows)
    }
}

#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
#[wasm_bindgen]
#[deprecated(
    since = "1.0.0",
    note = "For streaming patterns, use the fast/unsafe API with persistent buffers"
)]
pub struct TrimaContext {
    period: usize,
    m1: usize,
    m2: usize,
    first: usize,
    kernel: Kernel,
}

#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
#[wasm_bindgen]
#[allow(deprecated)]
impl TrimaContext {
    #[wasm_bindgen(constructor)]
    #[deprecated(
        since = "1.0.0",
        note = "For streaming patterns, use the fast/unsafe API with persistent buffers"
    )]
    pub fn new(period: usize) -> Result<TrimaContext, JsValue> {
        if period == 0 {
            return Err(JsValue::from_str("Invalid period: 0"));
        }
        if period <= 3 {
            return Err(JsValue::from_str(&format!("Period too small: {}", period)));
        }

        let m1 = (period + 1) / 2;
        let m2 = period - m1 + 1;

        Ok(TrimaContext {
            period,
            m1,
            m2,
            first: 0,
            kernel: Kernel::Auto,
        })
    }

    pub fn update_into(
        &self,
        in_ptr: *const f64,
        out_ptr: *mut f64,
        len: usize,
    ) -> Result<(), JsValue> {
        if len < self.period {
            return Err(JsValue::from_str("Data length less than period"));
        }

        unsafe {
            let data = std::slice::from_raw_parts(in_ptr, len);
            let out = std::slice::from_raw_parts_mut(out_ptr, len);

            let first = data.iter().position(|x| !x.is_nan()).unwrap_or(0);

            if in_ptr == out_ptr {
                let mut temp = vec![0.0; len];
                trima_compute_into(
                    data,
                    self.period,
                    self.m1,
                    self.m2,
                    first,
                    self.kernel,
                    &mut temp,
                );

                out.copy_from_slice(&temp);
            } else {
                trima_compute_into(data, self.period, self.m1, self.m2, first, self.kernel, out);
            }

            let warmup = first + self.period - 1;
            for i in 0..warmup {
                out[i] = f64::NAN;
            }
        }

        Ok(())
    }

    pub fn get_warmup_period(&self) -> usize {
        self.period - 1
    }
}

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

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

        let default_params = TrimaParams { period: None };
        let input = TrimaInput::from_candles(&candles, "close", default_params);
        let output = trima_with_kernel(&input, kernel)?;
        assert_eq!(output.values.len(), candles.close.len());

        let params_period_10 = TrimaParams { period: Some(10) };
        let input2 = TrimaInput::from_candles(&candles, "hl2", params_period_10);
        let output2 = trima_with_kernel(&input2, kernel)?;
        assert_eq!(output2.values.len(), candles.close.len());

        let params_custom = TrimaParams { period: Some(14) };
        let input3 = TrimaInput::from_candles(&candles, "hlc3", params_custom);
        let output3 = trima_with_kernel(&input3, kernel)?;
        assert_eq!(output3.values.len(), candles.close.len());

        Ok(())
    }

    fn check_trima_accuracy(
        test_name: &str,
        kernel: Kernel,
    ) -> Result<(), Box<dyn std::error::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 close_prices = &candles.close;
        let params = TrimaParams { period: Some(30) };
        let input = TrimaInput::from_candles(&candles, "close", params);
        let trima_result = trima_with_kernel(&input, kernel)?;

        assert_eq!(
            trima_result.values.len(),
            close_prices.len(),
            "TRIMA output length should match input data length"
        );
        let expected_last_five_trima = [
            59957.916666666664,
            59846.770833333336,
            59750.620833333334,
            59665.2125,
            59581.612499999996,
        ];
        assert!(
            trima_result.values.len() >= 5,
            "Not enough TRIMA values for the test"
        );
        let start_index = trima_result.values.len() - 5;
        let result_last_five_trima = &trima_result.values[start_index..];
        for (i, &value) in result_last_five_trima.iter().enumerate() {
            let expected_value = expected_last_five_trima[i];
            assert!(
                (value - expected_value).abs() < 1e-6,
                "[{}] TRIMA value mismatch at index {}: expected {}, got {}",
                test_name,
                i,
                expected_value,
                value
            );
        }
        let period = input.params.period.unwrap_or(14);
        for i in 0..(period - 1) {
            assert!(
                trima_result.values[i].is_nan(),
                "[{}] Expected NaN at early index {} for TRIMA, got {}",
                test_name,
                i,
                trima_result.values[i]
            );
        }
        Ok(())
    }

    fn check_trima_default_candles(
        test_name: &str,
        kernel: Kernel,
    ) -> Result<(), Box<dyn std::error::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 = TrimaInput::with_default_candles(&candles);
        match input.data {
            TrimaData::Candles { source, .. } => assert_eq!(source, "close"),
            _ => panic!("Expected TrimaData::Candles"),
        }
        let output = trima_with_kernel(&input, kernel)?;
        assert_eq!(output.values.len(), candles.close.len());
        Ok(())
    }

    fn check_trima_zero_period(
        test_name: &str,
        kernel: Kernel,
    ) -> Result<(), Box<dyn std::error::Error>> {
        skip_if_unsupported!(kernel, test_name);
        let input_data = [10.0, 20.0, 30.0];
        let params = TrimaParams { period: Some(0) };
        let input = TrimaInput::from_slice(&input_data, params);
        let res = trima_with_kernel(&input, kernel);
        assert!(
            res.is_err(),
            "[{}] TRIMA should fail with zero period",
            test_name
        );
        Ok(())
    }

    fn check_trima_period_too_small(
        test_name: &str,
        kernel: Kernel,
    ) -> Result<(), Box<dyn std::error::Error>> {
        skip_if_unsupported!(kernel, test_name);
        let input_data = [10.0, 20.0, 30.0, 40.0];
        let params = TrimaParams { period: Some(3) };
        let input = TrimaInput::from_slice(&input_data, params);
        let res = trima_with_kernel(&input, kernel);
        assert!(
            res.is_err(),
            "[{}] TRIMA should fail with period <= 3",
            test_name
        );
        Ok(())
    }

    fn check_trima_period_exceeds_length(
        test_name: &str,
        kernel: Kernel,
    ) -> Result<(), Box<dyn std::error::Error>> {
        skip_if_unsupported!(kernel, test_name);
        let data_small = [10.0, 20.0, 30.0];
        let params = TrimaParams { period: Some(10) };
        let input = TrimaInput::from_slice(&data_small, params);
        let res = trima_with_kernel(&input, kernel);
        assert!(
            res.is_err(),
            "[{}] TRIMA should fail with period exceeding length",
            test_name
        );
        Ok(())
    }

    fn check_trima_very_small_dataset(
        test_name: &str,
        kernel: Kernel,
    ) -> Result<(), Box<dyn std::error::Error>> {
        skip_if_unsupported!(kernel, test_name);
        let single_point = [42.0];
        let params = TrimaParams { period: Some(14) };
        let input = TrimaInput::from_slice(&single_point, params);
        let res = trima_with_kernel(&input, kernel);
        assert!(
            res.is_err(),
            "[{}] TRIMA should fail with insufficient data",
            test_name
        );
        Ok(())
    }

    fn check_trima_reinput(
        test_name: &str,
        kernel: Kernel,
    ) -> Result<(), Box<dyn std::error::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 = TrimaParams { period: Some(14) };
        let first_input = TrimaInput::from_candles(&candles, "close", first_params);
        let first_result = trima_with_kernel(&first_input, kernel)?;

        let second_params = TrimaParams { period: Some(10) };
        let second_input = TrimaInput::from_slice(&first_result.values, second_params);
        let second_result = trima_with_kernel(&second_input, kernel)?;

        assert_eq!(second_result.values.len(), first_result.values.len());
        for val in &second_result.values[240..] {
            assert!(val.is_finite());
        }
        Ok(())
    }

    fn check_trima_nan_handling(
        test_name: &str,
        kernel: Kernel,
    ) -> Result<(), Box<dyn std::error::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 = TrimaInput::from_candles(&candles, "close", TrimaParams { period: Some(14) });
        let res = trima_with_kernel(&input, kernel)?;
        assert_eq!(res.values.len(), candles.close.len());
        if res.values.len() > 240 {
            for (i, &val) in res.values[240..].iter().enumerate() {
                assert!(
                    !val.is_nan(),
                    "[{}] Found unexpected NaN at out-index {}",
                    test_name,
                    240 + i
                );
            }
        }
        Ok(())
    }

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

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

        let period = 14;

        let input = TrimaInput::from_candles(
            &candles,
            "close",
            TrimaParams {
                period: Some(period),
            },
        );
        let batch_output = trima_with_kernel(&input, kernel)?.values;

        let mut stream = TrimaStream::try_new(TrimaParams {
            period: Some(period),
        })?;

        let mut stream_values = Vec::with_capacity(candles.close.len());
        for &price in &candles.close {
            match stream.update(price) {
                Some(trima_val) => stream_values.push(trima_val),
                None => stream_values.push(f64::NAN),
            }
        }

        assert_eq!(batch_output.len(), stream_values.len());
        for (i, (&b, &s)) in batch_output.iter().zip(stream_values.iter()).enumerate() {
            if b.is_nan() && s.is_nan() {
                continue;
            }
            let diff = (b - s).abs();
            assert!(
                diff < 1e-8,
                "[{}] TRIMA streaming f64 mismatch at idx {}: batch={}, stream={}, diff={}",
                test_name,
                i,
                b,
                s,
                diff
            );
        }
        Ok(())
    }

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

    #[cfg(debug_assertions)]
    fn check_trima_no_poison(
        test_name: &str,
        kernel: Kernel,
    ) -> Result<(), Box<dyn std::error::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_periods = vec![4, 10, 14, 30, 50, 100];
        let test_sources = vec!["close", "open", "high", "low", "hl2", "hlc3", "ohlc4"];

        for period in test_periods {
            for source in &test_sources {
                let params = TrimaParams {
                    period: Some(period),
                };
                let input = TrimaInput::from_candles(&candles, source, params);
                let output = trima_with_kernel(&input, kernel)?;

                for (i, &val) in output.values.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 {} (period={}, source={})",
                            test_name, val, bits, i, period, source
                        );
                    }

                    if bits == 0x22222222_22222222 {
                        panic!(
                            "[{}] Found init_matrix_prefixes poison value {} (0x{:016X}) at index {} (period={}, source={})",
                            test_name, val, bits, i, period, source
                        );
                    }

                    if bits == 0x33333333_33333333 {
                        panic!(
                            "[{}] Found make_uninit_matrix poison value {} (0x{:016X}) at index {} (period={}, source={})",
                            test_name, val, bits, i, period, source
                        );
                    }
                }
            }
        }

        Ok(())
    }

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

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

        let strat = (4usize..=100).prop_flat_map(|period| {
            (
                prop::collection::vec(
                    (-1e6f64..1e6f64).prop_filter("finite", |x| x.is_finite()),
                    period..400,
                ),
                Just(period),
            )
        });

        proptest::test_runner::TestRunner::default().run(&strat, |(data, period)| {
            let params = TrimaParams {
                period: Some(period),
            };
            let input = TrimaInput::from_slice(&data, params);

            let result = trima_with_kernel(&input, kernel)?;
            let scalar_result = trima_with_kernel(&input, Kernel::Scalar)?;

            let first = data.iter().position(|x| !x.is_nan()).unwrap_or(0);
            let warmup_end = first + period - 1;

            for i in 0..warmup_end.min(data.len()) {
                prop_assert!(
                    result.values[i].is_nan(),
                    "Expected NaN during warmup at index {}, got {}",
                    i,
                    result.values[i]
                );
            }

            for i in warmup_end..data.len() {
                prop_assert!(
                    result.values[i].is_finite() || data[i].is_nan(),
                    "Expected finite value after warmup at index {}, got {}",
                    i,
                    result.values[i]
                );
            }

            if data[first..]
                .windows(2)
                .all(|w| (w[0] - w[1]).abs() < 1e-10)
                && data.len() > first
            {
                let constant_val = data[first];
                for i in warmup_end..data.len() {
                    prop_assert!(
							(result.values[i] - constant_val).abs() < 1e-9,
							"Constant input should produce constant output at index {}: expected {}, got {}",
							i,
							constant_val,
							result.values[i]
						);
                }
            }

            for i in 0..data.len() {
                let val = result.values[i];
                let ref_val = scalar_result.values[i];

                if val.is_nan() && ref_val.is_nan() {
                    continue;
                }

                if !val.is_finite() || !ref_val.is_finite() {
                    prop_assert_eq!(
                        val.to_bits(),
                        ref_val.to_bits(),
                        "NaN/Inf mismatch at index {}: {} vs {}",
                        i,
                        val,
                        ref_val
                    );
                } else {
                    let ulp_diff = val.to_bits().abs_diff(ref_val.to_bits());
                    prop_assert!(
                        (val - ref_val).abs() < 1e-9 || ulp_diff <= 4,
                        "Cross-kernel mismatch at index {}: {} vs {} (ULP diff: {})",
                        i,
                        val,
                        ref_val,
                        ulp_diff
                    );
                }
            }

            for (i, &val) in result.values.iter().enumerate() {
                prop_assert!(
                    val.is_nan() || val.is_finite(),
                    "Value should be finite or NaN at index {}, got {}",
                    i,
                    val
                );
            }

            for i in warmup_end..data.len() {
                if i >= period - 1 {
                    let start = if i >= period - 1 { i + 1 - period } else { 0 };
                    let window = &data[start..=i];
                    let min_val = window
                        .iter()
                        .filter(|x| x.is_finite())
                        .fold(f64::INFINITY, |a, &b| a.min(b));
                    let max_val = window
                        .iter()
                        .filter(|x| x.is_finite())
                        .fold(f64::NEG_INFINITY, |a, &b| a.max(b));

                    if min_val.is_finite() && max_val.is_finite() {
                        let val = result.values[i];

                        prop_assert!(
                            val >= min_val - 1e-6 && val <= max_val + 1e-6,
                            "TRIMA value {} at index {} outside window bounds [{}, {}]",
                            val,
                            i,
                            min_val,
                            max_val
                        );
                    }
                }
            }

            if period == 4 {
                let m1 = 2;
                let m2 = 3;

                let sma1_input = SmaInput {
                    data: SmaData::Slice(&data),
                    params: SmaParams { period: Some(m1) },
                };
                let pass1 = sma(&sma1_input)?;

                let sma2_input = SmaInput {
                    data: SmaData::Slice(&pass1.values),
                    params: SmaParams { period: Some(m2) },
                };
                let expected = sma(&sma2_input)?;

                for i in warmup_end..data.len().min(warmup_end + 5) {
                    prop_assert!(
                        (result.values[i] - expected.values[i]).abs() < 1e-9,
                        "Period=4: TRIMA mismatch at index {}: got {}, expected {}",
                        i,
                        result.values[i],
                        expected.values[i]
                    );
                }
            }

            {
                let m1 = (period + 1) / 2;
                let m2 = period - m1 + 1;

                let sma1_input = SmaInput {
                    data: SmaData::Slice(&data),
                    params: SmaParams { period: Some(m1) },
                };
                let pass1 = sma(&sma1_input)?;

                let sma2_input = SmaInput {
                    data: SmaData::Slice(&pass1.values),
                    params: SmaParams { period: Some(m2) },
                };
                let expected = sma(&sma2_input)?;

                let check_points = vec![
                    warmup_end,
                    warmup_end + period / 2,
                    warmup_end + period,
                    data.len() - 1,
                ];

                for &idx in &check_points {
                    if idx < data.len() {
                        let trima_val = result.values[idx];
                        let expected_val = expected.values[idx];

                        if trima_val.is_finite() && expected_val.is_finite() {
                            prop_assert!(
                                (trima_val - expected_val).abs() < 1e-9,
                                "Two-pass SMA formula mismatch at index {}: TRIMA={}, Expected={}",
                                idx,
                                trima_val,
                                expected_val
                            );
                        }
                    }
                }
            }

            if data.len() >= warmup_end + 20 {
                let sma_input = SmaInput {
                    data: SmaData::Slice(&data),
                    params: SmaParams {
                        period: Some(period),
                    },
                };
                let single_sma = sma(&sma_input)?;

                let trima_roughness: f64 = result.values[warmup_end..warmup_end + 20]
                    .windows(2)
                    .map(|w| (w[1] - w[0]).abs())
                    .sum();

                let sma_roughness: f64 = single_sma.values[warmup_end..warmup_end + 20]
                    .windows(2)
                    .map(|w| (w[1] - w[0]).abs())
                    .sum();

                if sma_roughness > 1e-10 {
                    prop_assert!(
							trima_roughness <= sma_roughness * 1.1,
							"TRIMA should be smoother than single SMA: TRIMA roughness={}, SMA roughness={}",
							trima_roughness,
							sma_roughness
						);
                }
            }

            if data.len() == period {
                prop_assert!(
                    result.values[period - 1].is_finite(),
                    "With data.len()==period, last value should be finite, got {}",
                    result.values[period - 1]
                );

                for i in 0..period - 1 {
                    prop_assert!(
                        result.values[i].is_nan(),
                        "With data.len()==period, value at {} should be NaN, got {}",
                        i,
                        result.values[i]
                    );
                }
            }

            let is_monotonic_increasing = data[first..].windows(2).all(|w| w[1] >= w[0] - 1e-10);
            let is_monotonic_decreasing = data[first..].windows(2).all(|w| w[1] <= w[0] + 1e-10);

            if is_monotonic_increasing || is_monotonic_decreasing {
                let valid_trima = &result.values[warmup_end..];
                if valid_trima.len() >= 2 {
                    if is_monotonic_increasing {
                        for w in valid_trima.windows(2) {
                            prop_assert!(
                                w[1] >= w[0] - 1e-9,
                                "TRIMA should preserve increasing trend: {} < {}",
                                w[1],
                                w[0]
                            );
                        }
                    } else {
                        for w in valid_trima.windows(2) {
                            prop_assert!(
                                w[1] <= w[0] + 1e-9,
                                "TRIMA should preserve decreasing trend: {} > {}",
                                w[1],
                                w[0]
                            );
                        }
                    }
                }
            }

            #[cfg(debug_assertions)]
            {
                for (i, &val) in result.values.iter().enumerate() {
                    if !val.is_nan() {
                        let bits = val.to_bits();
                        prop_assert!(
                            bits != 0x11111111_11111111
                                && bits != 0x22222222_22222222
                                && bits != 0x33333333_33333333,
                            "Found poison value at index {}: {} (0x{:016X})",
                            i,
                            val,
                            bits
                        );
                    }
                }
            }

            Ok(())
        })?;

        Ok(())
    }

    #[cfg(feature = "proptest")]
    generate_all_trima_tests!(check_trima_property);

    generate_all_trima_tests!(
        check_trima_partial_params,
        check_trima_accuracy,
        check_trima_default_candles,
        check_trima_zero_period,
        check_trima_period_exceeds_length,
        check_trima_period_too_small,
        check_trima_very_small_dataset,
        check_trima_reinput,
        check_trima_nan_handling,
        check_trima_streaming,
        check_trima_no_poison
    );

    fn check_batch_default_row(
        test: &str,
        kernel: Kernel,
    ) -> Result<(), Box<dyn std::error::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 output = TrimaBatchBuilder::new()
            .kernel(kernel)
            .apply_candles(&c, "close")?;

        let def = TrimaParams::default();
        let row = output.values_for(&def).expect("default row missing");

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

        let expected = [
            59957.916666666664,
            59846.770833333336,
            59750.620833333334,
            59665.2125,
            59581.612499999996,
        ];
        let start = row.len() - 5;
        for (i, &v) in row[start..].iter().enumerate() {
            assert!(
                (v - expected[i]).abs() < 1e-6,
                "[{test}] default-row mismatch at idx {i}: {v} vs {expected:?}"
            );
        }
        Ok(())
    }

    macro_rules! gen_batch_tests {
        ($fn_name:ident) => {
            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);
                }
            }
        };
    }

    #[cfg(debug_assertions)]
    fn check_batch_no_poison(test: &str, kernel: Kernel) -> Result<(), Box<dyn std::error::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 period_ranges = vec![(4, 20, 4), (20, 50, 10), (50, 100, 25), (5, 15, 1)];

        let test_sources = vec!["close", "open", "high", "low", "hl2", "hlc3", "ohlc4"];

        for (start, end, step) in period_ranges {
            for source in &test_sources {
                let output = TrimaBatchBuilder::new()
                    .kernel(kernel)
                    .period_range(start, end, step)
                    .apply_candles(&c, source)?;

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

                    let bits = val.to_bits();
                    let row = idx / output.cols;
                    let col = idx % output.cols;

                    if bits == 0x11111111_11111111 {
                        panic!(
                            "[{}] Found alloc_with_nan_prefix poison value {} (0x{:016X}) at row {} col {} (flat index {}) with period_range({},{},{}) source={}",
                            test, val, bits, row, col, idx, start, end, step, source
                        );
                    }

                    if bits == 0x22222222_22222222 {
                        panic!(
                            "[{}] Found init_matrix_prefixes poison value {} (0x{:016X}) at row {} col {} (flat index {}) with period_range({},{},{}) source={}",
                            test, val, bits, row, col, idx, start, end, step, source
                        );
                    }

                    if bits == 0x33333333_33333333 {
                        panic!(
                            "[{}] Found make_uninit_matrix poison value {} (0x{:016X}) at row {} col {} (flat index {}) with period_range({},{},{}) source={}",
                            test, val, bits, row, col, idx, start, end, step, source
                        );
                    }
                }
            }
        }

        Ok(())
    }

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

    gen_batch_tests!(check_batch_default_row);
    gen_batch_tests!(check_batch_no_poison);

    #[test]
    fn test_trima_into_matches_api() -> Result<(), Box<dyn std::error::Error>> {
        let mut data = Vec::with_capacity(256);
        data.extend_from_slice(&[f64::NAN, f64::NAN, f64::NAN, f64::NAN]);
        for i in 0..252 {
            let x = (i as f64 * 0.131).sin() * 7.0 + (i as f64) * 0.02;
            data.push(x);
        }

        let input = TrimaInput::from_slice(&data, TrimaParams::default());

        let baseline = trima(&input)?;

        let mut out = vec![0.0; data.len()];
        #[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
        {
            trima_into(&input, &mut out)?;
        }
        #[cfg(all(target_arch = "wasm32", feature = "wasm"))]
        {
            trima_into_slice(&mut out, &input, Kernel::Auto)?;
        }

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

        for (a, b) in baseline.values.iter().copied().zip(out.iter().copied()) {
            let both_nan = a.is_nan() && b.is_nan();
            assert!(both_nan || a == b, "mismatch: got {b:?}, expected {a:?}");
        }
        Ok(())
    }
}
