use crate::utilities::data_loader::{source_type, Candles};
use crate::utilities::enums::Kernel;
use crate::utilities::helpers::{
    alloc_with_nan_prefix, detect_best_batch_kernel, detect_best_kernel, init_matrix_prefixes,
    make_uninit_matrix,
};
#[cfg(not(target_arch = "wasm32"))]
use rayon::prelude::*;
use std::convert::AsRef;
use std::mem::MaybeUninit;
use thiserror::Error;

#[cfg(feature = "python")]
use crate::utilities::kernel_validation::validate_kernel;
#[cfg(feature = "python")]
use numpy;
#[cfg(feature = "python")]
use numpy::PyUntypedArrayMethods;
#[cfg(feature = "python")]
use pyo3::exceptions::PyValueError;
#[cfg(feature = "python")]
use pyo3::prelude::*;

#[cfg(all(feature = "python", feature = "cuda"))]
use crate::cuda::cuda_available;
#[cfg(all(feature = "python", feature = "cuda"))]
use crate::cuda::moving_averages::CudaEma;
#[cfg(all(feature = "python", feature = "cuda"))]
use cust::context::Context;
#[cfg(all(feature = "python", feature = "cuda"))]
use cust::memory::DeviceBuffer;
#[cfg(all(feature = "python", feature = "cuda"))]
use std::sync::Arc;

#[cfg(all(feature = "python", feature = "cuda"))]
#[pyclass(module = "ta_indicators.cuda", unsendable)]
pub struct EmaDeviceArrayF32Py {
    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 EmaDeviceArrayF32Py {
    #[getter]
    fn __cuda_array_interface__<'py>(
        &self,
        py: Python<'py>,
    ) -> PyResult<Bound<'py, pyo3::types::PyDict>> {
        let d = pyo3::types::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 (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 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));

        crate::utilities::dlpack_cuda::export_f32_cuda_dlpack_2d(
            py,
            buf,
            rows,
            cols,
            alloc_dev,
            max_version_bound,
        )
    }
}
impl<'a> AsRef<[f64]> for EmaInput<'a> {
    #[inline(always)]
    fn as_ref(&self) -> &[f64] {
        match &self.data {
            EmaData::Slice(slice) => slice,
            EmaData::Candles { candles, source } => source_type(candles, source),
        }
    }
}

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

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

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

impl Default for EmaParams {
    fn default() -> Self {
        Self { period: Some(9) }
    }
}

#[derive(Debug, Clone)]
pub struct EmaInput<'a> {
    pub data: EmaData<'a>,
    pub params: EmaParams,
}

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

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

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

impl EmaBuilder {
    #[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<EmaOutput, EmaError> {
        let p = EmaParams {
            period: self.period,
        };
        let i = EmaInput::from_candles(c, "close", p);
        ema_with_kernel(&i, self.kernel)
    }

    #[inline(always)]
    pub fn apply_slice(self, d: &[f64]) -> Result<EmaOutput, EmaError> {
        let p = EmaParams {
            period: self.period,
        };
        let i = EmaInput::from_slice(d, p);
        ema_with_kernel(&i, self.kernel)
    }

    #[inline(always)]
    pub fn into_stream(self) -> Result<EmaStream, EmaError> {
        let p = EmaParams {
            period: self.period,
        };
        EmaStream::try_new(p)
    }
}

#[derive(Debug, Error)]
pub enum EmaError {
    #[error("ema: Input data slice is empty.")]
    EmptyInputData,
    #[error("ema: All values are NaN.")]
    AllValuesNaN,
    #[error("ema: Invalid period: period = {period}, data length = {data_len}")]
    InvalidPeriod { period: usize, data_len: usize },
    #[error("ema: Not enough valid data: needed = {needed}, valid = {valid}")]
    NotEnoughValidData { needed: usize, valid: usize },
    #[error("ema: Output length mismatch: expected = {expected}, got = {got}")]
    OutputLengthMismatch { expected: usize, got: usize },
    #[error("ema: Invalid range: start = {start}, end = {end}, step = {step}")]
    InvalidRange {
        start: usize,
        end: usize,
        step: usize,
    },
    #[error("ema: Invalid kernel for batch API: {0:?}")]
    InvalidKernelForBatch(Kernel),
    #[error("ema: arithmetic overflow while computing {context}")]
    ArithmeticOverflow { context: &'static str },
}

#[inline]
pub fn ema(input: &EmaInput) -> Result<EmaOutput, EmaError> {
    ema_with_kernel(input, Kernel::Auto)
}

#[inline(always)]
fn ema_prepare<'a>(
    input: &'a EmaInput,
    kernel: Kernel,
) -> Result<(&'a [f64], usize, usize, f64, f64, Kernel), EmaError> {
    let data: &[f64] = input.as_ref();

    let len = data.len();
    if len == 0 {
        return Err(EmaError::EmptyInputData);
    }

    let first = data
        .iter()
        .position(|x| !x.is_nan())
        .ok_or(EmaError::AllValuesNaN)?;
    let period = input.get_period();
    if period == 0 || period > len {
        return Err(EmaError::InvalidPeriod {
            period,
            data_len: len,
        });
    }
    if len - first < period {
        return Err(EmaError::NotEnoughValidData {
            needed: period,
            valid: len - first,
        });
    }

    let alpha = 2.0 / (period as f64 + 1.0);
    let beta = 1.0 - alpha;
    let chosen = if matches!(kernel, Kernel::Auto) {
        Kernel::Scalar
    } else {
        kernel
    };
    Ok((data, period, first, alpha, beta, chosen))
}

#[inline(always)]
fn ema_compute_into(
    data: &[f64],
    period: usize,
    first: usize,
    alpha: f64,
    beta: f64,
    kernel: Kernel,
    out: &mut [f64],
) {
    unsafe {
        match kernel {
            Kernel::Scalar | Kernel::ScalarBatch => {
                ema_scalar_into(data, period, first, alpha, beta, out)
            }
            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
            Kernel::Avx2 | Kernel::Avx2Batch => {
                ema_avx2_into(data, period, first, alpha, beta, out)
            }
            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
            Kernel::Avx512 | Kernel::Avx512Batch => {
                ema_avx512_into(data, period, first, alpha, beta, out)
            }

            #[cfg(not(all(feature = "nightly-avx", target_arch = "x86_64")))]
            Kernel::Avx2 | Kernel::Avx2Batch | Kernel::Avx512 | Kernel::Avx512Batch => {
                ema_scalar_into(data, period, first, alpha, beta, out)
            }
            _ => unreachable!(),
        }
    }
}

pub fn ema_with_kernel(input: &EmaInput, kernel: Kernel) -> Result<EmaOutput, EmaError> {
    let (data, period, first, alpha, beta, chosen) = ema_prepare(input, kernel)?;

    let mut out = alloc_with_nan_prefix(data.len(), first);
    ema_compute_into(data, period, first, alpha, beta, chosen, &mut out);

    Ok(EmaOutput { values: out })
}

#[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
pub fn ema_into(input: &EmaInput, out: &mut [f64]) -> Result<(), EmaError> {
    let (data, period, first, alpha, beta, chosen) = ema_prepare(input, Kernel::Auto)?;

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

    let warm = first.min(out.len());
    for i in 0..warm {
        out[i] = f64::from_bits(0x7ff8_0000_0000_0000);
    }

    ema_compute_into(data, period, first, alpha, beta, chosen, out);

    Ok(())
}

#[inline(always)]
fn is_finite_fast(x: f64) -> bool {
    const EXP_MASK: u64 = 0x7ff0_0000_0000_0000;
    (x.to_bits() & EXP_MASK) != EXP_MASK
}

#[inline]
pub fn ema_into_slice(dst: &mut [f64], input: &EmaInput, kern: Kernel) -> Result<(), EmaError> {
    let (data, period, first, alpha, beta, chosen) = ema_prepare(input, kern)?;

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

    ema_compute_into(data, period, first, alpha, beta, chosen, dst);

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

    Ok(())
}

#[inline(always)]
pub unsafe fn ema_scalar(
    data: &[f64],
    period: usize,
    first_val: usize,
    out: &mut Vec<f64>,
) -> Result<EmaOutput, EmaError> {
    let alpha = 2.0 / (period as f64 + 1.0);
    let beta = 1.0 - alpha;
    ema_scalar_into(data, period, first_val, alpha, beta, out);
    let values = std::mem::take(out);
    Ok(EmaOutput { values })
}

#[inline(always)]
unsafe fn ema_scalar_into(
    data: &[f64],
    period: usize,
    first_val: usize,
    alpha: f64,
    beta: f64,
    out: &mut [f64],
) {
    let len = data.len();
    debug_assert_eq!(out.len(), len);

    let mut mean = *data.get_unchecked(first_val);
    *out.get_unchecked_mut(first_val) = mean;
    let mut valid_count = 1usize;

    let warmup_end = (first_val + period).min(len);
    for i in (first_val + 1)..warmup_end {
        let x = *data.get_unchecked(i);
        if is_finite_fast(x) {
            valid_count += 1;
            let vc = valid_count as f64;
            mean = ((vc - 1.0) * mean + x) / vc;
        }

        *out.get_unchecked_mut(i) = mean;
    }

    if warmup_end < len {
        let mut prev = mean;
        for i in warmup_end..len {
            let x = *data.get_unchecked(i);
            if is_finite_fast(x) {
                prev = beta.mul_add(prev, alpha * x);
            }

            *out.get_unchecked_mut(i) = prev;
        }
    }
}

#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
#[inline(always)]
pub unsafe fn ema_avx2(
    data: &[f64],
    period: usize,
    first_val: usize,
    out: &mut Vec<f64>,
) -> Result<EmaOutput, EmaError> {
    ema_scalar(data, period, first_val, out)
}

#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
#[inline(always)]
unsafe fn ema_avx2_into(
    data: &[f64],
    period: usize,
    first_val: usize,
    alpha: f64,
    beta: f64,
    out: &mut [f64],
) {
    ema_scalar_into(data, period, first_val, alpha, beta, out)
}

#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
#[inline(always)]
pub unsafe fn ema_avx512(
    data: &[f64],
    period: usize,
    first_val: usize,
    out: &mut Vec<f64>,
) -> Result<EmaOutput, EmaError> {
    ema_scalar(data, period, first_val, out)
}

#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
#[inline(always)]
unsafe fn ema_avx512_into(
    data: &[f64],
    period: usize,
    first_val: usize,
    alpha: f64,
    beta: f64,
    out: &mut [f64],
) {
    ema_scalar_into(data, period, first_val, alpha, beta, out)
}

#[derive(Debug, Clone)]
pub struct EmaStream {
    period: usize,
    alpha: f64,
    beta: f64,
    count: usize,
    mean: f64,
    filled: bool,

    inv: Box<[f64]>,
}

impl EmaStream {
    #[inline]
    pub fn try_new(params: EmaParams) -> Result<Self, EmaError> {
        let period = params.period.unwrap_or(9);
        if period == 0 {
            return Err(EmaError::InvalidPeriod {
                period,
                data_len: 0,
            });
        }

        let alpha = 2.0 / (period as f64 + 1.0);
        let beta = 1.0 - alpha;

        let mut inv = Vec::with_capacity(period);
        for n in 1..=period {
            inv.push(1.0 / n as f64);
        }

        Ok(Self {
            period,
            alpha,
            beta,
            count: 0,
            mean: f64::NAN,
            filled: false,
            inv: inv.into_boxed_slice(),
        })
    }

    #[inline(always)]
    pub fn update(&mut self, x: f64) -> Option<f64> {
        if !is_finite_fast(x) {
            return if self.filled { Some(self.mean) } else { None };
        }

        self.count += 1;
        let c = self.count;

        if c == 1 {
            self.mean = x;
        } else if c <= self.period {
            let inv = self.inv[c - 1];
            self.mean = (x - self.mean).mul_add(inv, self.mean);
        } else {
            self.mean = self.beta.mul_add(self.mean, self.alpha * x);
        }

        if !self.filled && c >= self.period {
            self.filled = true;
        }
        if self.filled {
            Some(self.mean)
        } else {
            None
        }
    }
}

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

impl Default for EmaBatchRange {
    fn default() -> Self {
        Self {
            period: (9, 258, 1),
        }
    }
}

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

impl EmaBatchBuilder {
    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<EmaBatchOutput, EmaError> {
        ema_batch_with_kernel(data, &self.range, self.kernel)
    }

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

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

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

pub fn ema_batch_with_kernel(
    data: &[f64],
    sweep: &EmaBatchRange,
    k: Kernel,
) -> Result<EmaBatchOutput, EmaError> {
    let kernel = match k {
        Kernel::Auto => detect_best_batch_kernel(),
        other if other.is_batch() => other,
        _ => return Err(EmaError::InvalidKernelForBatch(k)),
    };

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

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

    pub fn values_for(&self, p: &EmaParams) -> 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: &EmaBatchRange) -> Result<Vec<EmaParams>, EmaError> {
    fn axis_usize((start, end, step): (usize, usize, usize)) -> Vec<usize> {
        if step == 0 || start == end {
            return vec![start];
        }
        let (lo, hi) = if start <= end {
            (start, end)
        } else {
            (end, start)
        };
        (lo..=hi).step_by(step).collect()
    }

    let periods = axis_usize(r.period);
    if periods.is_empty() {
        return Err(EmaError::InvalidRange {
            start: r.period.0,
            end: r.period.1,
            step: r.period.2,
        });
    }
    let mut out = Vec::with_capacity(periods.len());
    for &p in &periods {
        out.push(EmaParams { period: Some(p) });
    }
    Ok(out)
}

#[inline(always)]
pub fn ema_batch_slice(
    data: &[f64],
    sweep: &EmaBatchRange,
    kern: Kernel,
) -> Result<EmaBatchOutput, EmaError> {
    ema_batch_inner(data, sweep, kern, false)
}

#[inline(always)]
pub fn ema_batch_par_slice(
    data: &[f64],
    sweep: &EmaBatchRange,
    kern: Kernel,
) -> Result<EmaBatchOutput, EmaError> {
    ema_batch_inner(data, sweep, kern, true)
}

#[inline(always)]
fn ema_batch_inner(
    data: &[f64],
    sweep: &EmaBatchRange,
    kern: Kernel,
    parallel: bool,
) -> Result<EmaBatchOutput, EmaError> {
    let combos = expand_grid(sweep)?;
    let rows = combos.len();
    let cols = data.len();

    if cols == 0 {
        return Err(EmaError::EmptyInputData);
    }

    let first = data
        .iter()
        .position(|x| !x.is_nan())
        .ok_or(EmaError::AllValuesNaN)?;

    let _total = rows.checked_mul(cols).ok_or(EmaError::ArithmeticOverflow {
        context: "rows*cols",
    })?;
    let mut buf_mu = make_uninit_matrix(rows, cols);

    let warm: Vec<usize> = std::iter::repeat(first).take(rows).collect();
    init_matrix_prefixes(&mut buf_mu, cols, &warm);

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

    let returned_combos = ema_batch_inner_into(data, sweep, kern, parallel, out)?;

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

    Ok(EmaBatchOutput {
        values,
        combos: returned_combos,
        rows,
        cols,
    })
}

#[inline(always)]
fn ema_batch_inner_into(
    data: &[f64],
    sweep: &EmaBatchRange,
    kern: Kernel,
    parallel: bool,
    out: &mut [f64],
) -> Result<Vec<EmaParams>, EmaError> {
    let combos = expand_grid(sweep)?;

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

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

    let rows = combos.len();
    let cols = data.len();

    let raw = unsafe {
        core::slice::from_raw_parts_mut(out.as_mut_ptr() as *mut MaybeUninit<f64>, out.len())
    };

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

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

        match kern {
            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
            Kernel::Avx512 => ema_row_avx512(data, first, period, dst),
            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
            Kernel::Avx2 => ema_row_avx2(data, first, period, dst),
            _ => ema_row_scalar(data, first, period, dst),
        }
    };

    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);
        }
    }

    Ok(combos)
}

#[inline(always)]
unsafe fn ema_row_scalar(data: &[f64], first: usize, period: usize, out: &mut [f64]) {
    let alpha = 2.0 / (period as f64 + 1.0);
    let beta = 1.0 - alpha;

    let len = data.len();

    let mut mean = unsafe { *data.get_unchecked(first) };
    unsafe { *out.get_unchecked_mut(first) = mean };
    let mut valid_count = 1usize;

    let warmup_end = (first + period).min(len);
    for i in (first + 1)..warmup_end {
        let x = unsafe { *data.get_unchecked(i) };
        if is_finite_fast(x) {
            valid_count += 1;
            let vc = valid_count as f64;
            mean = ((vc - 1.0) * mean + x) / vc;
        }

        unsafe { *out.get_unchecked_mut(i) = mean };
    }

    if warmup_end < len {
        let mut prev = mean;
        for i in warmup_end..len {
            let x = unsafe { *data.get_unchecked(i) };
            if is_finite_fast(x) {
                prev = beta.mul_add(prev, alpha * x);
            }

            unsafe { *out.get_unchecked_mut(i) = prev };
        }
    }
}

#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
#[inline(always)]
unsafe fn ema_row_avx2(data: &[f64], first: usize, period: usize, out: &mut [f64]) {
    ema_row_scalar(data, first, period, out);
}

#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
#[inline(always)]
unsafe fn ema_row_avx512(data: &[f64], first: usize, period: usize, out: &mut [f64]) {
    ema_row_scalar(data, first, period, out);
}

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

    #[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
    #[test]
    fn test_ema_into_matches_api() -> Result<(), Box<dyn std::error::Error>> {
        let mut data = Vec::with_capacity(256);
        for _ in 0..5 {
            data.push(f64::NAN);
        }
        for i in 0..251 {
            let x = (i as f64).sin() * 3.14159 + 100.0 + ((i % 7) as f64) * 0.01;
            data.push(x);
        }

        let input = EmaInput::from_slice(&data, EmaParams::default());
        let baseline = ema(&input)?.values;

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

        assert_eq!(baseline.len(), out.len());
        fn eq_or_both_nan(a: f64, b: f64) -> bool {
            (a.is_nan() && b.is_nan()) || (a == b) || (a - b).abs() <= 1e-12
        }
        for (i, (&a, &b)) in baseline.iter().zip(out.iter()).enumerate() {
            assert!(
                eq_or_both_nan(a, b),
                "mismatch at index {}: api={} into={}",
                i,
                a,
                b
            );
        }
        Ok(())
    }

    fn check_ema_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 = EmaParams { period: None };
        let input = EmaInput::from_candles(&candles, "close", default_params);
        let output = ema_with_kernel(&input, kernel)?;
        assert_eq!(output.values.len(), candles.close.len());
        Ok(())
    }

    fn check_ema_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 input = EmaInput::from_candles(&candles, "close", EmaParams::default());
        let result = ema_with_kernel(&input, kernel)?;
        let expected_last_five = [59302.2, 59277.9, 59230.2, 59215.1, 59103.1];
        let start = result.values.len().saturating_sub(5);
        for (i, &val) in result.values[start..].iter().enumerate() {
            let diff = (val - expected_last_five[i]).abs();
            assert!(
                diff < 1e-1,
                "[{}] EMA {:?} mismatch at idx {}: got {}, expected {}",
                test_name,
                kernel,
                i,
                val,
                expected_last_five[i]
            );
        }
        Ok(())
    }

    fn check_ema_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 = EmaInput::with_default_candles(&candles);
        match input.data {
            EmaData::Candles { source, .. } => assert_eq!(source, "close"),
            _ => panic!("Expected EmaData::Candles"),
        }
        let output = ema_with_kernel(&input, kernel)?;
        assert_eq!(output.values.len(), candles.close.len());
        Ok(())
    }

    fn check_ema_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 = EmaParams { period: Some(0) };
        let input = EmaInput::from_slice(&input_data, params);
        let res = ema_with_kernel(&input, kernel);
        assert!(
            res.is_err(),
            "[{}] EMA should fail with zero period",
            test_name
        );
        Ok(())
    }

    fn check_ema_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 = EmaParams { period: Some(10) };
        let input = EmaInput::from_slice(&data_small, params);
        let res = ema_with_kernel(&input, kernel);
        assert!(
            res.is_err(),
            "[{}] EMA should fail with period exceeding length",
            test_name
        );
        Ok(())
    }

    fn check_ema_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 = EmaParams { period: Some(9) };
        let input = EmaInput::from_slice(&single_point, params);
        let res = ema_with_kernel(&input, kernel);
        assert!(
            res.is_err(),
            "[{}] EMA should fail with insufficient data",
            test_name
        );
        Ok(())
    }

    fn check_ema_empty_input(
        test_name: &str,
        kernel: Kernel,
    ) -> Result<(), Box<dyn std::error::Error>> {
        skip_if_unsupported!(kernel, test_name);
        let empty: [f64; 0] = [];
        let input = EmaInput::from_slice(&empty, EmaParams::default());
        let res = ema_with_kernel(&input, kernel);
        assert!(
            matches!(res, Err(EmaError::EmptyInputData)),
            "[{}] EMA should fail with empty input",
            test_name
        );
        Ok(())
    }

    fn check_ema_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 = EmaParams { period: Some(9) };
        let first_input = EmaInput::from_candles(&candles, "close", first_params);
        let first_result = ema_with_kernel(&first_input, kernel)?;

        let second_params = EmaParams { period: Some(5) };
        let second_input = EmaInput::from_slice(&first_result.values, second_params);
        let second_result = ema_with_kernel(&second_input, kernel)?;

        assert_eq!(second_result.values.len(), first_result.values.len());
        if second_result.values.len() > 240 {
            for (i, &val) in second_result.values[240..].iter().enumerate() {
                assert!(
                    !val.is_nan(),
                    "[{}] Found unexpected NaN at out-index {}",
                    test_name,
                    240 + i
                );
            }
        }
        Ok(())
    }

    fn check_ema_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 = EmaInput::from_candles(&candles, "close", EmaParams { period: Some(9) });
        let res = ema_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_ema_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 = 9;
        let warm_up = 240;

        let input = EmaInput::from_candles(
            &candles,
            "close",
            EmaParams {
                period: Some(period),
            },
        );
        let batch_output = ema_with_kernel(&input, kernel)?.values;

        let mut stream = EmaStream::try_new(EmaParams {
            period: Some(period),
        })?;
        let mut stream_values = Vec::with_capacity(candles.close.len());

        for (i, &price) in candles.close.iter().enumerate() {
            let stream_val = stream.update(price);

            if i < period - 1 {
                assert!(
                    stream_val.is_none(),
                    "[{}] Stream should return None during warmup at idx {}",
                    test_name,
                    i
                );
                stream_values.push(f64::NAN);
            } else {
                stream_values.push(stream_val.unwrap_or(f64::NAN));
            }
        }

        assert_eq!(batch_output.len(), stream_values.len());

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

    #[cfg(debug_assertions)]
    fn check_ema_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![2, 5, 9, 14, 20, 50, 100, 200];
        let test_sources = vec!["open", "high", "low", "close", "hl2", "hlc3", "ohlc4"];

        for period in &test_periods {
            for source in &test_sources {
                let input = EmaInput::from_candles(
                    &candles,
                    source,
                    EmaParams {
                        period: Some(*period),
                    },
                );
                let output = ema_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 {} with period={}, source={}",
                            test_name, val, bits, i, period, source
                        );
                    }

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

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

        Ok(())
    }

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

    fn check_ema_property(
        test_name: &str,
        kernel: Kernel,
    ) -> Result<(), Box<dyn std::error::Error>> {
        use proptest::prelude::*;
        skip_if_unsupported!(kernel, test_name);

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

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

                let EmaOutput { values: out } = ema_with_kernel(&input, kernel).unwrap();

                let EmaOutput { values: ref_out } =
                    ema_with_kernel(&input, Kernel::Scalar).unwrap();

                let alpha = 2.0 / (period as f64 + 1.0);
                let beta = 1.0 - alpha;

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

                for i in 0..data.len() {
                    let y = out[i];
                    let r = ref_out[i];

                    if i < first_valid {
                        prop_assert!(
                            y.is_nan(),
                            "[{}] Expected NaN during warmup at idx {}, got {}",
                            test_name,
                            i,
                            y
                        );
                        continue;
                    }

                    if i >= first_valid {
                        let window = &data[first_valid..=i];
                        let lo = window
                            .iter()
                            .cloned()
                            .filter(|x| x.is_finite())
                            .fold(f64::INFINITY, f64::min);
                        let hi = window
                            .iter()
                            .cloned()
                            .filter(|x| x.is_finite())
                            .fold(f64::NEG_INFINITY, f64::max);

                        if !y.is_nan() && lo.is_finite() && hi.is_finite() {
                            prop_assert!(
                                y >= lo - 1e-9 && y <= hi + 1e-9,
                                "[{}] idx {}: {} not in [{}, {}]",
                                test_name,
                                i,
                                y,
                                lo,
                                hi
                            );
                        }
                    }

                    if period == 1 && i >= first_valid && data[i].is_finite() {
                        prop_assert!(
                            (y - data[i]).abs() <= 1e-10,
                            "[{}] Period=1 mismatch at idx {}: {} vs {}",
                            test_name,
                            i,
                            y,
                            data[i]
                        );
                    }

                    if i >= first_valid + period {
                        let window_start = i.saturating_sub(period);
                        let window = &data[window_start..=i];
                        if window
                            .iter()
                            .all(|&x| (x - data[window_start]).abs() < 1e-10)
                        {
                            let expected = data[window_start];
                            prop_assert!(
                                (y - expected).abs() <= 1e-6,
                                "[{}] Constant data convergence failed at idx {}: {} vs {}",
                                test_name,
                                i,
                                y,
                                expected
                            );
                        }
                    }

                    if !y.is_finite() || !r.is_finite() {
                        prop_assert!(
                            y.to_bits() == r.to_bits(),
                            "[{}] NaN/infinite mismatch at idx {}: {} vs {}",
                            test_name,
                            i,
                            y,
                            r
                        );
                    } else {
                        let abs_diff = (y - r).abs();
                        let rel_diff = if r.abs() > 1e-10 {
                            abs_diff / r.abs()
                        } else {
                            abs_diff
                        };

                        prop_assert!(
                            abs_diff <= 1e-9 || rel_diff <= 1e-9,
                            "[{}] Kernel mismatch at idx {}: {} vs {} (abs_diff={}, rel_diff={})",
                            test_name,
                            i,
                            y,
                            r,
                            abs_diff,
                            rel_diff
                        );
                    }

                    if i >= first_valid + period
                        && y.is_finite()
                        && out[i - 1].is_finite()
                        && data[i].is_finite()
                    {
                        let expected_ema = alpha * data[i] + beta * out[i - 1];
                        let diff = (y - expected_ema).abs();

                        prop_assert!(
                            diff <= 1e-9 * ((i - first_valid) as f64).max(1.0),
                            "[{}] EMA recursive property failed at idx {}: {} vs {} (diff={})",
                            test_name,
                            i,
                            y,
                            expected_ema,
                            diff
                        );
                    }

                    if i >= first_valid + period * 2 {
                        let historical = &data[first_valid..=i];
                        let hist_min = historical
                            .iter()
                            .filter(|x| x.is_finite())
                            .fold(f64::INFINITY, |a, &b| a.min(b));
                        let hist_max = historical
                            .iter()
                            .filter(|x| x.is_finite())
                            .fold(f64::NEG_INFINITY, |a, &b| a.max(b));

                        if hist_min.is_finite() && hist_max.is_finite() && y.is_finite() {
                            prop_assert!(
                                y >= hist_min - 1e-6 && y <= hist_max + 1e-6,
                                "[{}] EMA outside historical bounds at idx {}: {} not in [{}, {}]",
                                test_name,
                                i,
                                y,
                                hist_min,
                                hist_max
                            );
                        }
                    }
                }

                if first_valid < data.len() && out[first_valid].is_finite() {
                    prop_assert!(
                        (out[first_valid] - data[first_valid]).abs() <= 1e-10,
                        "[{}] First valid output should equal first valid input: {} vs {}",
                        test_name,
                        out[first_valid],
                        data[first_valid]
                    );
                }

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

        Ok(())
    }

    macro_rules! generate_all_ema_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);
                    }

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

    generate_all_ema_tests!(
        check_ema_partial_params,
        check_ema_accuracy,
        check_ema_default_candles,
        check_ema_zero_period,
        check_ema_period_exceeds_length,
        check_ema_very_small_dataset,
        check_ema_empty_input,
        check_ema_reinput,
        check_ema_nan_handling,
        check_ema_streaming,
        check_ema_property,
        check_ema_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 = EmaBatchBuilder::new()
            .kernel(kernel)
            .apply_candles(&c, "close")?;

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

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

        let expected = [59302.2, 59277.9, 59230.2, 59215.1, 59103.1];
        let start = row.len() - 5;
        for (i, &v) in row[start..].iter().enumerate() {
            assert!(
                (v - expected[i]).abs() < 1e-1,
                "[{test}] default-row mismatch at idx {i}: {v} vs {expected:?}"
            );
        }
        Ok(())
    }

    #[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 test_sources = vec!["open", "high", "low", "close", "hl2", "hlc3", "ohlc4"];

        for source in &test_sources {
            let output = EmaBatchBuilder::new()
                .kernel(kernel)
                .period_range(2, 200, 3)
                .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 source={}",
                        test, val, bits, row, col, idx, source
                    );
                }

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

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

        let edge_case_ranges = vec![(2, 5, 1), (190, 200, 2), (50, 100, 10)];
        for (start, end, step) in edge_case_ranges {
            let output = EmaBatchBuilder::new()
                .kernel(kernel)
                .period_range(start, end, step)
                .apply_candles(&c, "close")?;

            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
                    || bits == 0x22222222_22222222
                    || bits == 0x33333333_33333333
                {
                    panic!(
						"[{}] Found poison value {} (0x{:016X}) at row {} col {} with range ({},{},{})",
						test, val, bits, row, col, start, end, step
					);
                }
            }
        }

        Ok(())
    }

    #[cfg(not(debug_assertions))]
    fn check_batch_no_poison(
        _test: &str,
        _kernel: Kernel,
    ) -> Result<(), Box<dyn std::error::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_no_poison);

    #[test]
    fn test_batch_stream_consistency() -> Result<(), Box<dyn std::error::Error>> {
        let test_data = vec![
            1.0,
            2.0,
            3.0,
            4.0,
            5.0,
            f64::NAN,
            6.0,
            7.0,
            8.0,
            9.0,
            10.0,
            f64::NAN,
            11.0,
            12.0,
            13.0,
            14.0,
            15.0,
        ];

        let period = 5;

        let params = EmaParams {
            period: Some(period),
        };
        let input = EmaInput::from_slice(&test_data, params.clone());
        let batch_output = ema(&input)?;

        let mut stream = EmaStream::try_new(params)?;
        let mut stream_output = Vec::new();
        for &val in &test_data {
            let result = stream.update(val);

            stream_output.push(result.unwrap_or(f64::NAN));
        }

        for i in period..test_data.len() {
            let batch_val = batch_output.values[i];
            let stream_val = stream_output[i];

            if batch_val.is_finite() && stream_val.is_finite() {
                let diff = (batch_val - stream_val).abs();
                assert!(
                    diff < 1e-10,
                    "Batch/Stream mismatch at index {}: batch={}, stream={}, diff={}",
                    i,
                    batch_val,
                    stream_val,
                    diff
                );
            } else {
                assert_eq!(
                    batch_val.is_nan(),
                    stream_val.is_nan(),
                    "Batch/Stream NaN mismatch at index {}: batch={}, stream={}",
                    i,
                    batch_val,
                    stream_val
                );
            }
        }

        for i in 0..period.min(test_data.len()) {
            if test_data[i].is_finite() {
                if i > 0 && batch_output.values[i].is_finite() {
                    assert!(
                        (batch_output.values[i] - test_data[0]).abs() > 1e-10 || i == 0,
                        "Batch should use running mean during warmup, not just first value"
                    );
                }
            }
        }

        Ok(())
    }
}

#[cfg(feature = "python")]
#[pyfunction(name = "ema")]
#[pyo3(signature = (data, period, kernel=None))]
pub fn ema_py<'py>(
    py: Python<'py>,
    data: numpy::PyReadonlyArray1<'py, f64>,
    period: usize,
    kernel: Option<&str>,
) -> PyResult<Bound<'py, numpy::PyArray1<f64>>> {
    use numpy::{IntoPyArray, PyArray1, PyArrayMethods};

    let kern = validate_kernel(kernel, false)?;

    let params = EmaParams {
        period: Some(period),
    };

    let result_vec: Vec<f64> = if let Ok(slice_in) = data.as_slice() {
        let ema_in = EmaInput::from_slice(slice_in, params);
        py.allow_threads(|| ema_with_kernel(&ema_in, kern).map(|o| o.values))
            .map_err(|e| PyValueError::new_err(e.to_string()))?
    } else {
        let owned = data.as_array().to_owned();
        let slice_in = owned
            .as_slice()
            .expect("owned numpy array should be contiguous");
        let ema_in = EmaInput::from_slice(slice_in, params);
        py.allow_threads(|| ema_with_kernel(&ema_in, kern).map(|o| o.values))
            .map_err(|e| PyValueError::new_err(e.to_string()))?
    };

    Ok(result_vec.into_pyarray(py))
}

#[cfg(feature = "python")]
#[pyfunction(name = "ema_batch")]
#[pyo3(signature = (data, period_range, kernel=None))]
pub fn ema_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 = EmaBatchRange {
        period: period_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 out_arr = unsafe { PyArray1::<f64>::new(py, [rows * cols], false) };
    let slice_out = unsafe { out_arr.as_slice_mut()? };

    let kern = validate_kernel(kernel, true)?;

    let first = slice_in.iter().position(|x| !x.is_nan()).unwrap_or(0);
    for r in 0..rows {
        let row_start = r * cols;
        for i in 0..first {
            slice_out[row_start + i] = f64::NAN;
        }
    }

    let combos = py
        .allow_threads(|| {
            let kernel = match kern {
                Kernel::Auto => detect_best_batch_kernel(),
                k => k,
            };
            let simd = match kernel {
                Kernel::Avx512Batch => Kernel::Avx512,
                Kernel::Avx2Batch => Kernel::Avx2,
                Kernel::ScalarBatch => Kernel::Scalar,
                _ => unreachable!(),
            };
            ema_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() as u64)
            .collect::<Vec<_>>()
            .into_pyarray(py),
    )?;

    Ok(dict.into())
}

#[cfg(all(feature = "python", feature = "cuda"))]
#[pyfunction(name = "ema_cuda_batch_dev")]
#[pyo3(signature = (data_f32, period_range=(9, 9, 0), device_id=0))]
pub fn ema_cuda_batch_dev_py(
    py: Python<'_>,
    data_f32: numpy::PyReadonlyArray1<'_, f32>,
    period_range: (usize, usize, usize),
    device_id: usize,
) -> PyResult<EmaDeviceArrayF32Py> {
    if !cuda_available() {
        return Err(PyValueError::new_err("CUDA not available"));
    }

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

    let (buf, rows, cols, ctx, dev) = py.allow_threads(|| {
        let cuda = CudaEma::new(device_id).map_err(|e| PyValueError::new_err(e.to_string()))?;
        let handle = cuda
            .ema_batch_dev(slice_in, &sweep)
            .map_err(|e| PyValueError::new_err(e.to_string()))?;
        let ctx = cuda.context_arc();
        let dev = cuda.device_id();
        Ok::<_, PyErr>((handle.buf, handle.rows, handle.cols, ctx, dev))
    })?;

    Ok(EmaDeviceArrayF32Py {
        buf: Some(buf),
        rows,
        cols,
        _ctx: ctx,
        device_id: dev,
    })
}

#[cfg(all(feature = "python", feature = "cuda"))]
#[pyfunction(name = "ema_cuda_many_series_one_param_dev")]
#[pyo3(signature = (data_tm_f32, period, device_id=0))]
pub fn ema_cuda_many_series_one_param_dev_py(
    py: Python<'_>,
    data_tm_f32: numpy::PyReadonlyArray2<'_, f32>,
    period: usize,
    device_id: usize,
) -> PyResult<EmaDeviceArrayF32Py> {
    if !cuda_available() {
        return Err(PyValueError::new_err("CUDA not available"));
    }
    if period == 0 {
        return Err(PyValueError::new_err("period must be positive"));
    }

    let flat = data_tm_f32.as_slice()?;
    let shape = data_tm_f32.shape();
    let series_len = shape[0];
    let num_series = shape[1];
    let params = EmaParams {
        period: Some(period),
    };

    let (buf, rows, cols, ctx, dev) = py.allow_threads(|| {
        let cuda = CudaEma::new(device_id).map_err(|e| PyValueError::new_err(e.to_string()))?;
        let handle = cuda
            .ema_many_series_one_param_time_major_dev(flat, num_series, series_len, &params)
            .map_err(|e| PyValueError::new_err(e.to_string()))?;
        let ctx = cuda.context_arc();
        let dev = cuda.device_id();
        Ok::<_, PyErr>((handle.buf, handle.rows, handle.cols, ctx, dev))
    })?;

    Ok(EmaDeviceArrayF32Py {
        buf: Some(buf),
        rows,
        cols,
        _ctx: ctx,
        device_id: dev,
    })
}

#[cfg(feature = "python")]
#[pyclass(name = "EmaStream")]
pub struct EmaStreamPy {
    inner: EmaStream,
}

#[cfg(feature = "python")]
#[pymethods]
impl EmaStreamPy {
    #[new]
    pub fn new(period: usize) -> PyResult<Self> {
        let params = EmaParams {
            period: Some(period),
        };
        let inner = EmaStream::try_new(params).map_err(|e| PyValueError::new_err(e.to_string()))?;
        Ok(Self { inner })
    }

    pub fn update(&mut self, value: f64) -> f64 {
        self.inner.update(value).unwrap_or(f64::NAN)
    }
}

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

#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
#[wasm_bindgen]
pub fn ema_js(data: &[f64], period: usize) -> Result<Vec<f64>, JsValue> {
    let params = EmaParams {
        period: Some(period),
    };
    let input = EmaInput::from_slice(data, params);

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

    ema_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 EmaBatchConfig {
    pub period_range: (usize, usize, usize),
}

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

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

    let sweep = EmaBatchRange {
        period: config.period_range,
    };

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

    let js_output = EmaBatchJsOutput {
        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 ema_batch_metadata_js(
    period_start: usize,
    period_end: usize,
    period_step: usize,
) -> Result<Vec<f64>, JsValue> {
    let sweep = EmaBatchRange {
        period: (period_start, period_end, period_step),
    };

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

    for combo in combos {
        metadata.push(combo.period.unwrap() as f64);
    }

    Ok(metadata)
}

#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
#[wasm_bindgen]
pub fn ema_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 ema_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"))]
#[wasm_bindgen]
pub fn ema_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 ema_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 = EmaParams {
            period: Some(period),
        };
        let input = EmaInput::from_slice(data, params);

        if in_ptr == out_ptr {
            let mut temp = vec![0.0; len];
            ema_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 out = std::slice::from_raw_parts_mut(out_ptr, len);
            ema_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 ema_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 ema_batch_into"));
    }

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

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

        let combos = expand_grid(&sweep).map_err(|e| JsValue::from_str(&e.to_string()))?;
        let rows = combos.len();
        let cols = len;
        let elems = rows
            .checked_mul(cols)
            .ok_or(JsValue::from_str("overflow rows*cols"))?;
        let out = std::slice::from_raw_parts_mut(out_ptr, elems);

        let first = data
            .iter()
            .position(|x| !x.is_nan())
            .ok_or(JsValue::from_str("All NaN"))?;
        for r in 0..rows {
            let s = r * cols;
            out[s..s + first].fill(f64::NAN);
        }

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

        Ok(rows)
    }
}
