use crate::utilities::data_loader::{source_type, Candles};
#[cfg(all(feature = "python", feature = "cuda"))]
use crate::utilities::dlpack_cuda::export_f32_cuda_dlpack_2d;
use crate::utilities::enums::Kernel;
use crate::utilities::helpers::{
    alloc_with_nan_prefix, detect_best_batch_kernel, detect_best_kernel, init_matrix_prefixes,
    make_uninit_matrix,
};
#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
use core::arch::x86_64::*;
#[cfg(all(feature = "python", feature = "cuda"))]
use cust::memory::DeviceBuffer;
#[cfg(not(target_arch = "wasm32"))]
use rayon::prelude::*;
use std::error::Error;
use thiserror::Error;

#[inline(always)]
fn ttm_numeric_compute_into(
    source: &[f64],
    close: &[f64],
    period: usize,
    first: usize,
    out: &mut [f64],
) {
    let mut sum = 0.0;
    for &v in &source[first..first + period] {
        sum += v;
    }
    let inv_p = 1.0 / (period as f64);
    let mut i = first + period - 1;
    out[i] = if close[i] > sum * inv_p { 1.0 } else { 0.0 };
    i += 1;
    let n = source.len().min(close.len());
    while i < n {
        sum += source[i] - source[i - period];
        out[i] = if close[i] > sum * inv_p { 1.0 } else { 0.0 };
        i += 1;
    }
}

#[inline(always)]
fn ttm_prepare<'a>(
    input: &'a TtmTrendInput<'a>,
    kernel: Kernel,
) -> Result<(&'a [f64], &'a [f64], usize, usize, Kernel), TtmTrendError> {
    let (source, close) = input.as_slices();
    let len = source.len().min(close.len());
    if len == 0 {
        return Err(TtmTrendError::EmptyInputData);
    }
    let first = source
        .iter()
        .zip(close)
        .position(|(&s, &c)| !s.is_nan() && !c.is_nan())
        .ok_or(TtmTrendError::AllValuesNaN)?;
    let period = input.get_period();
    if period == 0 || period > len {
        return Err(TtmTrendError::InvalidPeriod {
            period,
            data_len: len,
        });
    }
    if len - first < period {
        return Err(TtmTrendError::NotEnoughValidData {
            needed: period,
            valid: len - first,
        });
    }
    let chosen = match kernel {
        Kernel::Auto => detect_best_kernel(),
        k => k,
    };
    Ok((source, close, period, first, chosen))
}

#[inline(always)]
fn ttm_numeric_compute_with_params(
    dst: &mut [f64],
    source: &[f64],
    close: &[f64],
    period: usize,
    first: usize,
    _kernel: Kernel,
) -> Result<(), TtmTrendError> {
    let len = source.len().min(close.len());
    if dst.len() != len {
        return Err(TtmTrendError::OutputLengthMismatch {
            expected: len,
            got: dst.len(),
        });
    }

    let warmup = first + period - 1;
    for v in &mut dst[..warmup] {
        *v = f64::NAN;
    }
    ttm_numeric_compute_into(source, close, period, first, dst);
    Ok(())
}

#[inline(always)]
fn ttm_numeric_into_slice(
    dst: &mut [f64],
    input: &TtmTrendInput,
    kern: Kernel,
) -> Result<(), TtmTrendError> {
    let (source, close, period, first, chosen) = ttm_prepare(input, kern)?;
    ttm_numeric_compute_with_params(dst, source, close, period, first, chosen)
}

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

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

#[derive(Debug, Clone)]
pub struct TtmTrendParams {
    pub period: Option<usize>,
}

impl Default for TtmTrendParams {
    fn default() -> Self {
        Self { period: Some(5) }
    }
}

#[derive(Debug, Clone)]
pub struct TtmTrendInput<'a> {
    pub data: TtmTrendData<'a>,
    pub params: TtmTrendParams,
}

impl<'a> TtmTrendInput<'a> {
    #[inline]
    pub fn from_candles(c: &'a Candles, s: &'a str, p: TtmTrendParams) -> Self {
        Self {
            data: TtmTrendData::Candles {
                candles: c,
                source: s,
            },
            params: p,
        }
    }
    #[inline]
    pub fn from_slices(source: &'a [f64], close: &'a [f64], p: TtmTrendParams) -> Self {
        Self {
            data: TtmTrendData::Slices { source, close },
            params: p,
        }
    }
    #[inline]
    pub fn with_default_candles(c: &'a Candles) -> Self {
        Self::from_candles(c, "hl2", TtmTrendParams::default())
    }
    #[inline]
    pub fn get_period(&self) -> usize {
        self.params.period.unwrap_or(5)
    }
    #[inline(always)]
    pub fn as_slices(&self) -> (&[f64], &[f64]) {
        match &self.data {
            TtmTrendData::Slices { source, close } => (source, close),
            TtmTrendData::Candles { candles, source } => {
                (source_type(candles, source), source_type(candles, "close"))
            }
        }
    }
    #[inline(always)]
    pub fn as_ref(&self) -> (&[f64], &[f64]) {
        match &self.data {
            TtmTrendData::Slices { source, close } => (*source, *close),
            TtmTrendData::Candles { candles, source } => {
                (source_type(candles, source), source_type(candles, "close"))
            }
        }
    }
}

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

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

impl TtmTrendBuilder {
    #[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<TtmTrendOutput, TtmTrendError> {
        let p = TtmTrendParams {
            period: self.period,
        };
        let i = TtmTrendInput::from_candles(c, "hl2", p);
        ttm_trend_with_kernel(&i, self.kernel)
    }
    #[inline(always)]
    pub fn apply_slices(self, src: &[f64], close: &[f64]) -> Result<TtmTrendOutput, TtmTrendError> {
        let p = TtmTrendParams {
            period: self.period,
        };
        let i = TtmTrendInput::from_slices(src, close, p);
        ttm_trend_with_kernel(&i, self.kernel)
    }
    #[inline(always)]
    pub fn into_stream(self) -> Result<TtmTrendStream, TtmTrendError> {
        let p = TtmTrendParams {
            period: self.period,
        };
        TtmTrendStream::try_new(p)
    }
}

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

#[inline]
pub fn ttm_trend(input: &TtmTrendInput) -> Result<TtmTrendOutput, TtmTrendError> {
    ttm_trend_with_kernel(input, Kernel::Auto)
}

pub fn ttm_trend_with_kernel(
    input: &TtmTrendInput,
    kernel: Kernel,
) -> Result<TtmTrendOutput, TtmTrendError> {
    let (source, close, period, first, chosen) = ttm_prepare(input, kernel)?;
    let len = source.len().min(close.len());

    let mut values = vec![false; len];

    #[allow(unused_variables)]
    {
        match chosen {
            Kernel::Scalar | Kernel::ScalarBatch => {
                ttm_trend_scalar(source, close, period, first, &mut values)
            }
            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
            Kernel::Avx2 | Kernel::Avx2Batch => {
                ttm_trend_avx2(source, close, period, first, &mut values)
            }
            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
            Kernel::Avx512 | Kernel::Avx512Batch => {
                ttm_trend_avx512(source, close, period, first, &mut values)
            }
            #[cfg(not(all(feature = "nightly-avx", target_arch = "x86_64")))]
            Kernel::Avx2 | Kernel::Avx2Batch | Kernel::Avx512 | Kernel::Avx512Batch => {
                ttm_trend_scalar(source, close, period, first, &mut values)
            }
            _ => unreachable!(),
        }
    }

    Ok(TtmTrendOutput { values })
}

#[inline]
pub fn ttm_trend_scalar(
    source: &[f64],
    close: &[f64],
    period: usize,
    first: usize,
    out: &mut [bool],
) {
    let n = source.len().min(close.len()).min(out.len());
    if n == 0 {
        return;
    }

    let warmup_end = first + period - 1;
    if warmup_end >= n {
        for i in 0..n {
            out[i] = false;
        }
        return;
    }

    for i in 0..first {
        out[i] = false;
    }

    if period == 1 {
        let mut i = first;
        while i < n {
            out[i] = close[i] > source[i];
            i += 1;
        }
        return;
    }

    let mut sum = 0.0;
    let mut k = first;
    while k < warmup_end {
        sum += source[k];
        out[k] = false;
        k += 1;
    }
    sum += source[warmup_end];

    let inv_period = 1.0 / (period as f64);
    let mut idx = warmup_end;
    out[idx] = close[idx] > sum * inv_period;
    idx += 1;
    while idx < n {
        sum += source[idx] - source[idx - period];
        out[idx] = close[idx] > sum * inv_period;
        idx += 1;
    }
}

#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
#[inline]
pub fn ttm_trend_avx512(
    source: &[f64],
    close: &[f64],
    period: usize,
    first: usize,
    out: &mut [bool],
) {
    if period == 1 {
        unsafe {
            let n = source.len().min(close.len()).min(out.len());
            if first >= n {
                return;
            }

            for i in 0..first {
                *out.get_unchecked_mut(i) = false;
            }
            let mut i = first;
            const W: usize = 8;
            while i + W <= n {
                let s = _mm512_loadu_pd(source.as_ptr().add(i));
                let c = _mm512_loadu_pd(close.as_ptr().add(i));
                let m: u8 = _mm512_cmp_pd_mask(c, s, _CMP_GT_OQ);

                (*out.get_unchecked_mut(i + 0)) = (m & (1 << 0)) != 0;
                (*out.get_unchecked_mut(i + 1)) = (m & (1 << 1)) != 0;
                (*out.get_unchecked_mut(i + 2)) = (m & (1 << 2)) != 0;
                (*out.get_unchecked_mut(i + 3)) = (m & (1 << 3)) != 0;
                (*out.get_unchecked_mut(i + 4)) = (m & (1 << 4)) != 0;
                (*out.get_unchecked_mut(i + 5)) = (m & (1 << 5)) != 0;
                (*out.get_unchecked_mut(i + 6)) = (m & (1 << 6)) != 0;
                (*out.get_unchecked_mut(i + 7)) = (m & (1 << 7)) != 0;
                i += W;
            }
            while i < n {
                *out.get_unchecked_mut(i) = *close.get_unchecked(i) > *source.get_unchecked(i);
                i += 1;
            }
        }
        return;
    }

    ttm_trend_scalar(source, close, period, first, out)
}

#[inline]
pub fn ttm_trend_avx2(
    source: &[f64],
    close: &[f64],
    period: usize,
    first: usize,
    out: &mut [bool],
) {
    if period == 1 {
        #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
        unsafe {
            let n = source.len().min(close.len()).min(out.len());
            if first >= n {
                return;
            }

            for i in 0..first {
                *out.get_unchecked_mut(i) = false;
            }
            let mut i = first;
            const W: usize = 4;
            while i + W <= n {
                let s = _mm256_loadu_pd(source.as_ptr().add(i));
                let c = _mm256_loadu_pd(close.as_ptr().add(i));
                let m = _mm256_cmp_pd(c, s, _CMP_GT_OQ);
                let bits = _mm256_movemask_pd(m) as i32;
                (*out.get_unchecked_mut(i + 0)) = (bits & (1 << 0)) != 0;
                (*out.get_unchecked_mut(i + 1)) = (bits & (1 << 1)) != 0;
                (*out.get_unchecked_mut(i + 2)) = (bits & (1 << 2)) != 0;
                (*out.get_unchecked_mut(i + 3)) = (bits & (1 << 3)) != 0;
                i += W;
            }
            while i < n {
                *out.get_unchecked_mut(i) = *close.get_unchecked(i) > *source.get_unchecked(i);
                i += 1;
            }
        }
        #[cfg(not(all(feature = "nightly-avx", target_arch = "x86_64")))]
        {
            ttm_trend_scalar(source, close, period, first, out)
        }
        return;
    }
    ttm_trend_scalar(source, close, period, first, out)
}

#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
#[inline]
pub unsafe fn ttm_trend_avx512_short(
    source: &[f64],
    close: &[f64],
    period: usize,
    first: usize,
    out: &mut [bool],
) {
    ttm_trend_scalar(source, close, period, first, out)
}

#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
#[inline]
pub unsafe fn ttm_trend_avx512_long(
    source: &[f64],
    close: &[f64],
    period: usize,
    first: usize,
    out: &mut [bool],
) {
    ttm_trend_scalar(source, close, period, first, out)
}

#[derive(Debug, Clone)]
pub struct TtmTrendStream {
    period: usize,
    inv_period: f64,
    buffer: Vec<f64>,
    sum: f64,
    head: usize,
    len: usize,
    pow2_mask: usize,
}

impl TtmTrendStream {
    #[inline(always)]
    pub fn try_new(params: TtmTrendParams) -> Result<Self, TtmTrendError> {
        let period = params.period.unwrap_or(5);
        if period == 0 {
            return Err(TtmTrendError::InvalidPeriod {
                period,
                data_len: 0,
            });
        }
        let pow2_mask = if period.is_power_of_two() {
            period - 1
        } else {
            0
        };
        Ok(Self {
            period,
            inv_period: 1.0 / (period as f64),
            buffer: vec![0.0; period],
            sum: 0.0,
            head: 0,
            len: 0,
            pow2_mask,
        })
    }

    #[inline(always)]
    fn bump(&mut self) {
        if self.pow2_mask != 0 {
            self.head = (self.head + 1) & self.pow2_mask;
        } else {
            let h = self.head + 1;
            self.head = if h == self.period { 0 } else { h };
        }
    }

    #[inline(always)]
    pub fn update(&mut self, src_val: f64, close_val: f64) -> Option<bool> {
        if self.period == 1 {
            let old = self.buffer[self.head];
            self.buffer[self.head] = src_val;
            self.sum += src_val - old;
            self.len = 1;

            return Some(close_val > src_val);
        }

        let old = self.buffer[self.head];
        self.buffer[self.head] = src_val;

        self.sum += src_val - old;
        self.bump();

        if self.len < self.period {
            self.len += 1;
            if self.len < self.period {
                return None;
            }
        }

        let avg = self.sum * self.inv_period;
        Some(close_val > avg)
    }
}

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

impl Default for TtmTrendBatchRange {
    fn default() -> Self {
        Self {
            period: (5, 254, 1),
        }
    }
}

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

impl TtmTrendBatchBuilder {
    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_slices(
        self,
        source: &[f64],
        close: &[f64],
    ) -> Result<TtmTrendBatchOutput, TtmTrendError> {
        ttm_trend_batch_with_kernel(source, close, &self.range, self.kernel)
    }
    pub fn with_default_slices(
        source: &[f64],
        close: &[f64],
        k: Kernel,
    ) -> Result<TtmTrendBatchOutput, TtmTrendError> {
        TtmTrendBatchBuilder::new()
            .kernel(k)
            .apply_slices(source, close)
    }
    pub fn apply_candles(
        self,
        c: &Candles,
        src: &str,
    ) -> Result<TtmTrendBatchOutput, TtmTrendError> {
        let source = source_type(c, src);
        let close = source_type(c, "close");
        self.apply_slices(source, close)
    }
    pub fn with_default_candles(c: &Candles) -> Result<TtmTrendBatchOutput, TtmTrendError> {
        TtmTrendBatchBuilder::new()
            .kernel(Kernel::Auto)
            .apply_candles(c, "hl2")
    }
}

pub fn ttm_trend_batch_with_kernel(
    source: &[f64],
    close: &[f64],
    sweep: &TtmTrendBatchRange,
    k: Kernel,
) -> Result<TtmTrendBatchOutput, TtmTrendError> {
    let kernel = match k {
        Kernel::Auto => detect_best_batch_kernel(),
        other if other.is_batch() => other,
        other => return Err(TtmTrendError::InvalidKernelForBatch(other)),
    };
    let simd = match kernel {
        Kernel::Avx512Batch => Kernel::Avx512,
        Kernel::Avx2Batch => Kernel::Avx2,
        Kernel::ScalarBatch => Kernel::Scalar,
        _ => Kernel::Scalar,
    };
    ttm_trend_batch_par_slice(source, close, sweep, simd)
}

#[derive(Clone, Debug)]
pub struct TtmTrendBatchOutput {
    pub values: Vec<bool>,
    pub combos: Vec<TtmTrendParams>,
    pub rows: usize,
    pub cols: usize,
}

impl TtmTrendBatchOutput {
    pub fn row_for_params(&self, p: &TtmTrendParams) -> Option<usize> {
        self.combos
            .iter()
            .position(|c| c.period.unwrap_or(5) == p.period.unwrap_or(5))
    }
    pub fn values_for(&self, p: &TtmTrendParams) -> Option<&[bool]> {
        self.row_for_params(p).and_then(|row| {
            let start = row.checked_mul(self.cols)?;
            self.values.get(start..start + self.cols)
        })
    }
}

#[inline(always)]
fn expand_grid(r: &TtmTrendBatchRange) -> Result<Vec<TtmTrendParams>, TtmTrendError> {
    fn axis_usize((start, end, step): (usize, usize, usize)) -> Result<Vec<usize>, TtmTrendError> {
        if step == 0 || start == end {
            return Ok(vec![start]);
        }
        if start < end {
            let st = step.max(1);
            let v: Vec<usize> = (start..=end).step_by(st).collect();
            if v.is_empty() {
                return Err(TtmTrendError::InvalidRange { start, end, step });
            }
            return Ok(v);
        }

        let mut v = Vec::new();
        let mut x = start as isize;
        let end_i = end as isize;
        let st = (step as isize).max(1);
        while x >= end_i {
            v.push(x as usize);
            x -= st;
        }
        if v.is_empty() {
            return Err(TtmTrendError::InvalidRange { start, end, step });
        }
        Ok(v)
    }
    let periods = axis_usize(r.period)?;
    if periods.is_empty() {
        return Err(TtmTrendError::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(TtmTrendParams { period: Some(p) });
    }
    Ok(out)
}

#[inline(always)]
pub fn ttm_trend_batch_slice(
    source: &[f64],
    close: &[f64],
    sweep: &TtmTrendBatchRange,
    kern: Kernel,
) -> Result<TtmTrendBatchOutput, TtmTrendError> {
    ttm_trend_batch_inner(source, close, sweep, kern, false)
}

#[inline(always)]
pub fn ttm_trend_batch_par_slice(
    source: &[f64],
    close: &[f64],
    sweep: &TtmTrendBatchRange,
    kern: Kernel,
) -> Result<TtmTrendBatchOutput, TtmTrendError> {
    ttm_trend_batch_inner(source, close, sweep, kern, true)
}

#[inline(always)]
fn ttm_batch_inner_f64(
    source: &[f64],
    close: &[f64],
    sweep: &TtmTrendBatchRange,
    kern: Kernel,
    parallel: bool,
) -> Result<(Vec<f64>, Vec<TtmTrendParams>, usize, usize), TtmTrendError> {
    let combos = expand_grid(sweep)?;

    let len = source.len().min(close.len());
    if len == 0 {
        return Err(TtmTrendError::EmptyInputData);
    }
    let first = source
        .iter()
        .zip(close)
        .position(|(&s, &c)| !s.is_nan() && !c.is_nan())
        .ok_or(TtmTrendError::AllValuesNaN)?;
    let max_p = combos.iter().map(|c| c.period.unwrap()).max().unwrap();
    if len - first < max_p {
        return Err(TtmTrendError::NotEnoughValidData {
            needed: max_p,
            valid: len - first,
        });
    }

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

    let mut buf_mu = make_uninit_matrix(rows, cols);
    let warm: Vec<usize> = combos
        .iter()
        .map(|c| first + c.period.unwrap() - 1)
        .collect();
    init_matrix_prefixes(&mut buf_mu, cols, &warm);

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

    let mut psum = vec![0.0f64; len];
    if first < len {
        psum[first] = source[first];
        for i in (first + 1)..len {
            psum[i] = psum[i - 1] + source[i];
        }
    }

    let do_row = |row: usize, row_dst: &mut [f64]| {
        let p = combos[row].period.unwrap();
        let warm_i = warm[row];
        if warm_i >= len {
            return;
        }

        let inv_p = 1.0 / (p as f64);
        let mut i = warm_i;
        row_dst[i] = if close[i] > psum[i] * inv_p { 1.0 } else { 0.0 };
        i += 1;

        while i < len {
            let sum = psum[i] - psum[i - p];
            row_dst[i] = if close[i] > sum * inv_p { 1.0 } else { 0.0 };
            i += 1;
        }
    };

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

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

#[inline(always)]
fn ttm_trend_batch_inner(
    source: &[f64],
    close: &[f64],
    sweep: &TtmTrendBatchRange,
    kern: Kernel,
    parallel: bool,
) -> Result<TtmTrendBatchOutput, TtmTrendError> {
    let (vals_f64, combos, rows, cols) = ttm_batch_inner_f64(source, close, sweep, kern, parallel)?;

    let values: Vec<bool> = vals_f64.into_iter().map(|v| v == 1.0).collect();
    Ok(TtmTrendBatchOutput {
        values,
        combos,
        rows,
        cols,
    })
}

#[inline(always)]
fn ttm_trend_batch_inner_into_f64(
    source: &[f64],
    close: &[f64],
    sweep: &TtmTrendBatchRange,
    kern: Kernel,
    parallel: bool,
    out: &mut [f64],
) -> Result<Vec<TtmTrendParams>, TtmTrendError> {
    let combos = expand_grid(sweep)?;
    let len = source.len().min(close.len());
    if len == 0 {
        return Err(TtmTrendError::EmptyInputData);
    }
    let first = source
        .iter()
        .zip(close.iter())
        .position(|(&s, &c)| !s.is_nan() && !c.is_nan())
        .ok_or(TtmTrendError::AllValuesNaN)?;
    let max_p = combos.iter().map(|c| c.period.unwrap()).max().unwrap();
    if len - first < max_p {
        return Err(TtmTrendError::NotEnoughValidData {
            needed: max_p,
            valid: len - first,
        });
    }
    let rows = combos.len();
    let cols = len;
    let expected = rows.checked_mul(cols).ok_or(TtmTrendError::InvalidRange {
        start: sweep.period.0,
        end: sweep.period.1,
        step: sweep.period.2,
    })?;
    if out.len() != expected {
        return Err(TtmTrendError::OutputLengthMismatch {
            expected,
            got: out.len(),
        });
    }

    for (row, combo) in combos.iter().enumerate() {
        let warmup = first + combo.period.unwrap() - 1;
        let row_start = row * cols;
        for i in 0..warmup.min(cols) {
            out[row_start + i] = f64::NAN;
        }
    }

    let do_row = |row: usize, out_row: &mut [f64]| {
        let period = combos[row].period.unwrap();
        ttm_numeric_compute_into(source, close, period, first, out_row);
    };

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

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

    Ok(combos)
}

#[inline(always)]
fn ttm_trend_batch_inner_into(
    source: &[f64],
    close: &[f64],
    sweep: &TtmTrendBatchRange,
    kern: Kernel,
    parallel: bool,
    out: &mut [bool],
) -> Result<Vec<TtmTrendParams>, TtmTrendError> {
    let len = source.len().min(close.len());
    let combos = expand_grid(sweep)?;
    let rows = combos.len();
    let cols = len;
    let expected = rows.checked_mul(cols).ok_or(TtmTrendError::InvalidRange {
        start: sweep.period.0,
        end: sweep.period.1,
        step: sweep.period.2,
    })?;
    if out.len() != expected {
        return Err(TtmTrendError::OutputLengthMismatch {
            expected,
            got: out.len(),
        });
    }

    let mut tmp = vec![f64::NAN; expected];
    let result = ttm_trend_batch_inner_into_f64(source, close, sweep, kern, parallel, &mut tmp)?;

    for (i, &v) in tmp.iter().enumerate() {
        out[i] = v == 1.0;
    }

    Ok(result)
}

#[inline(always)]
unsafe fn ttm_trend_row_scalar(
    source: &[f64],
    close: &[f64],
    first: usize,
    period: usize,
    out: &mut [bool],
) {
    out.fill(false);
    let mut sum = 0.0;
    for &v in &source[first..first + period] {
        sum += v;
    }
    let inv_p = 1.0 / (period as f64);
    let mut idx = first + period - 1;
    out[idx] = close[idx] > sum * inv_p;
    idx += 1;
    while idx < source.len().min(close.len()) {
        sum += source[idx] - source[idx - period];
        out[idx] = close[idx] > sum * inv_p;
        idx += 1;
    }
}

#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
#[inline(always)]
unsafe fn ttm_trend_row_avx2(
    source: &[f64],
    close: &[f64],
    first: usize,
    period: usize,
    out: &mut [bool],
) {
    ttm_trend_row_scalar(source, close, first, period, out)
}

#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
#[inline(always)]
pub unsafe fn ttm_trend_row_avx512(
    source: &[f64],
    close: &[f64],
    first: usize,
    period: usize,
    out: &mut [bool],
) {
    if period <= 32 {
        ttm_trend_row_avx512_short(source, close, first, period, out);
    } else {
        ttm_trend_row_avx512_long(source, close, first, period, out);
    }
}

#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
#[inline(always)]
unsafe fn ttm_trend_row_avx512_short(
    source: &[f64],
    close: &[f64],
    first: usize,
    period: usize,
    out: &mut [bool],
) {
    ttm_trend_row_scalar(source, close, first, period, out)
}

#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
#[inline(always)]
unsafe fn ttm_trend_row_avx512_long(
    source: &[f64],
    close: &[f64],
    first: usize,
    period: usize,
    out: &mut [bool],
) {
    ttm_trend_row_scalar(source, close, first, period, out)
}

#[cfg(feature = "python")]
pub fn register_ttm_trend_module(m: &Bound<'_, pyo3::types::PyModule>) -> PyResult<()> {
    m.add_function(wrap_pyfunction!(ttm_trend_py, m)?)?;
    m.add_function(wrap_pyfunction!(ttm_trend_batch_py, m)?)?;
    m.add_class::<TtmTrendStreamPy>()?;
    #[cfg(all(feature = "python", feature = "cuda"))]
    {
        m.add_class::<TtmTrendDeviceArrayF32Py>()?;
        m.add_function(wrap_pyfunction!(ttm_trend_cuda_batch_dev_py, m)?)?;
        m.add_function(wrap_pyfunction!(
            ttm_trend_cuda_many_series_one_param_dev_py,
            m
        )?)?;
    }
    Ok(())
}

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

#[cfg(all(feature = "python", feature = "cuda"))]
#[pyclass(
    module = "ta_indicators.cuda",
    name = "TtmTrendDeviceArrayF32",
    unsendable
)]
pub struct TtmTrendDeviceArrayF32Py {
    pub(crate) inner: DeviceArrayF32,
    pub(crate) _ctx: Arc<Context>,
    pub(crate) device_id: u32,
}

#[cfg(all(feature = "python", feature = "cuda"))]
#[pymethods]
impl TtmTrendDeviceArrayF32Py {
    #[getter]
    fn __cuda_array_interface__<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyDict>> {
        let d = PyDict::new(py);
        let itemsize = std::mem::size_of::<f32>();
        d.set_item("shape", (self.inner.rows, self.inner.cols))?;
        d.set_item("typestr", "<f4")?;
        d.set_item("strides", (self.inner.cols * itemsize, itemsize))?;
        d.set_item("data", (self.inner.device_ptr() as usize, 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<PyObject>,
        max_version: Option<PyObject>,
        dl_device: Option<PyObject>,
        copy: Option<PyObject>,
    ) -> PyResult<PyObject> {
        if let Some(ref s_obj) = stream {
            if let Ok(s) = s_obj.extract::<usize>(py) {
                if s == 0 {
                    return Err(PyValueError::new_err(
                        "__dlpack__ stream=0 is invalid for CUDA",
                    ));
                }
            }
        }

        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; cross-device copy not supported for TtmTrendDeviceArrayF32",
                        ));
                    }
                }
            }
        }

        if let Some(ref c_obj) = copy {
            if let Ok(true) = c_obj.extract::<bool>(py) {
                return Err(PyValueError::new_err(
                    "copy=True not supported for TtmTrendDeviceArrayF32",
                ));
            }
        }

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

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

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

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

#[cfg(all(feature = "python", feature = "cuda"))]
impl TtmTrendDeviceArrayF32Py {
    pub fn new_from_rust(inner: DeviceArrayF32, ctx_guard: Arc<Context>, device_id: u32) -> Self {
        Self {
            inner,
            _ctx: ctx_guard,
            device_id,
        }
    }
}

#[cfg(all(feature = "python", feature = "cuda"))]
#[pyfunction(name = "ttm_trend_cuda_batch_dev")]
#[pyo3(signature = (source_f32, close_f32, period_range, device_id=0))]
pub fn ttm_trend_cuda_batch_dev_py(
    py: Python<'_>,
    source_f32: PyReadonlyArray1<'_, f32>,
    close_f32: PyReadonlyArray1<'_, f32>,
    period_range: (usize, usize, usize),
    device_id: usize,
) -> PyResult<TtmTrendDeviceArrayF32Py> {
    if !cuda_available() {
        return Err(PyValueError::new_err("CUDA not available"));
    }
    let src = source_f32.as_slice()?;
    let cls = close_f32.as_slice()?;
    let sweep = TtmTrendBatchRange {
        period: period_range,
    };
    let (inner, ctx, dev_id) = py.allow_threads(|| {
        let cuda =
            CudaTtmTrend::new(device_id).map_err(|e| PyValueError::new_err(e.to_string()))?;
        let ctx = cuda.context_arc();
        let dev_id = cuda.device_id();
        let arr = cuda
            .ttm_trend_batch_dev(src, cls, &sweep)
            .map_err(|e| PyValueError::new_err(e.to_string()))?;
        Ok::<_, PyErr>((arr, ctx, dev_id))
    })?;
    Ok(TtmTrendDeviceArrayF32Py::new_from_rust(inner, ctx, dev_id))
}

#[cfg(all(feature = "python", feature = "cuda"))]
#[pyfunction(name = "ttm_trend_cuda_many_series_one_param_dev")]
#[pyo3(signature = (source_tm_f32, close_tm_f32, cols, rows, period, device_id=0))]
pub fn ttm_trend_cuda_many_series_one_param_dev_py(
    py: Python<'_>,
    source_tm_f32: PyReadonlyArray1<'_, f32>,
    close_tm_f32: PyReadonlyArray1<'_, f32>,
    cols: usize,
    rows: usize,
    period: usize,
    device_id: usize,
) -> PyResult<TtmTrendDeviceArrayF32Py> {
    if !cuda_available() {
        return Err(PyValueError::new_err("CUDA not available"));
    }
    let src_tm = source_tm_f32.as_slice()?;
    let cls_tm = close_tm_f32.as_slice()?;
    let (inner, ctx, dev_id) = py.allow_threads(|| {
        let cuda =
            CudaTtmTrend::new(device_id).map_err(|e| PyValueError::new_err(e.to_string()))?;
        let ctx = cuda.context_arc();
        let dev_id = cuda.device_id();
        let arr = cuda
            .ttm_trend_many_series_one_param_time_major_dev(src_tm, cls_tm, cols, rows, period)
            .map_err(|e| PyValueError::new_err(e.to_string()))?;
        Ok::<_, PyErr>((arr, ctx, dev_id))
    })?;
    Ok(TtmTrendDeviceArrayF32Py::new_from_rust(inner, ctx, dev_id))
}

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

    #[test]
    fn test_ttm_trend_into_matches_api() -> Result<(), Box<dyn Error>> {
        let file = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
        let candles = read_candles_from_csv(file)?;
        let source = source_type(&candles, "hl2");
        let close = source_type(&candles, "close");
        let input = TtmTrendInput::from_slices(source, close, TtmTrendParams::default());

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

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

        let (_s, _c, p, first, _k) = ttm_prepare(&input, Kernel::Auto)?;
        let warmup_end = first + p - 1;

        let mut expected = vec![0.0f64; out.len()];
        for i in 0..out.len() {
            if i < warmup_end {
                expected[i] = f64::NAN;
            } else {
                expected[i] = if baseline[i] { 1.0 } else { 0.0 };
            }
        }

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

        assert_eq!(out.len(), expected.len());
        for i in 0..out.len() {
            assert!(
                eq_or_both_nan(out[i], expected[i]),
                "Mismatch at index {}: out={:?}, expected={:?}",
                i,
                out[i],
                expected[i]
            );
        }

        Ok(())
    }

    fn check_ttm_partial_params(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
        skip_if_unsupported!(kernel, test_name);
        let file = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
        let candles = read_candles_from_csv(file)?;
        let default_params = TtmTrendParams { period: None };
        let input = TtmTrendInput::from_candles(&candles, "hl2", default_params);
        let output = ttm_trend_with_kernel(&input, kernel)?;
        assert_eq!(output.values.len(), candles.close.len());
        Ok(())
    }

    fn check_ttm_accuracy(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
        skip_if_unsupported!(kernel, test_name);
        let file = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
        let candles = read_candles_from_csv(file)?;
        let close = source_type(&candles, "close");
        let params = TtmTrendParams { period: Some(5) };
        let input = TtmTrendInput::from_candles(&candles, "hl2", params);
        let result = ttm_trend_with_kernel(&input, kernel)?;
        assert_eq!(result.values.len(), close.len());
        let expected_last_five = [true, false, false, false, false];
        let start = result.values.len().saturating_sub(5);
        for (i, &val) in result.values[start..].iter().enumerate() {
            assert_eq!(val, expected_last_five[i]);
        }
        Ok(())
    }

    fn check_ttm_zero_period(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
        skip_if_unsupported!(kernel, test_name);
        let src = [10.0, 20.0, 30.0];
        let close = [12.0, 22.0, 32.0];
        let params = TtmTrendParams { period: Some(0) };
        let input = TtmTrendInput::from_slices(&src, &close, params);
        let res = ttm_trend_with_kernel(&input, kernel);
        assert!(res.is_err());
        Ok(())
    }

    fn check_ttm_period_exceeds_length(
        test_name: &str,
        kernel: Kernel,
    ) -> Result<(), Box<dyn Error>> {
        skip_if_unsupported!(kernel, test_name);
        let src = [1.0, 2.0, 3.0];
        let close = [1.0, 2.0, 3.0];
        let params = TtmTrendParams { period: Some(10) };
        let input = TtmTrendInput::from_slices(&src, &close, params);
        let res = ttm_trend_with_kernel(&input, kernel);
        assert!(res.is_err());
        Ok(())
    }

    fn check_ttm_very_small_dataset(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
        skip_if_unsupported!(kernel, test_name);
        let src = [42.0];
        let close = [43.0];
        let params = TtmTrendParams { period: Some(5) };
        let input = TtmTrendInput::from_slices(&src, &close, params);
        let res = ttm_trend_with_kernel(&input, kernel);
        assert!(res.is_err());
        Ok(())
    }

    fn check_ttm_all_nan(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
        skip_if_unsupported!(kernel, test_name);
        let src = [f64::NAN, f64::NAN, f64::NAN];
        let close = [f64::NAN, f64::NAN, f64::NAN];
        let params = TtmTrendParams { period: Some(5) };
        let input = TtmTrendInput::from_slices(&src, &close, params);
        let res = ttm_trend_with_kernel(&input, kernel);
        assert!(res.is_err());
        Ok(())
    }

    fn check_ttm_streaming(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
        skip_if_unsupported!(kernel, test_name);
        let file = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
        let candles = read_candles_from_csv(file)?;
        let src = source_type(&candles, "hl2");
        let close = source_type(&candles, "close");
        let period = 5;
        let input = TtmTrendInput::from_slices(
            src,
            close,
            TtmTrendParams {
                period: Some(period),
            },
        );
        let batch_output = ttm_trend_with_kernel(&input, kernel)?.values;
        let mut stream = TtmTrendStream::try_new(TtmTrendParams {
            period: Some(period),
        })?;
        let mut stream_values = Vec::with_capacity(close.len());
        for (&s, &c) in src.iter().zip(close.iter()) {
            match stream.update(s, c) {
                Some(v) => stream_values.push(v),
                None => stream_values.push(false),
            }
        }
        assert_eq!(batch_output, stream_values);
        Ok(())
    }

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

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

        let test_params = vec![
            TtmTrendParams::default(),
            TtmTrendParams { period: Some(1) },
            TtmTrendParams { period: Some(2) },
            TtmTrendParams { period: Some(3) },
            TtmTrendParams { period: Some(7) },
            TtmTrendParams { period: Some(10) },
            TtmTrendParams { period: Some(14) },
            TtmTrendParams { period: Some(20) },
            TtmTrendParams { period: Some(50) },
            TtmTrendParams { period: Some(100) },
            TtmTrendParams { period: Some(200) },
        ];

        for (param_idx, params) in test_params.iter().enumerate() {
            let input = TtmTrendInput::from_slices(src, close, params.clone());
            let output = ttm_trend_with_kernel(&input, kernel)?;

            let first_valid = src
                .iter()
                .zip(close.iter())
                .position(|(&s, &c)| !s.is_nan() && !c.is_nan())
                .unwrap_or(0);
            let period = params.period.unwrap_or(5);
            let warmup_end = first_valid + period - 1;

            for i in 0..warmup_end.min(output.values.len()) {
                if output.values[i] {
                    panic!(
                        "[{}] Found unexpected true value (poison) at index {} in warmup period \
						 with params: period={} (param set {}). \
						 Expected false during warmup (indices 0-{})",
                        test_name,
                        i,
                        period,
                        param_idx,
                        warmup_end - 1
                    );
                }
            }

            if warmup_end < output.values.len() {
                let after_warmup = &output.values[warmup_end..];
                let all_true = after_warmup.iter().all(|&v| v);
                let all_false = after_warmup.iter().all(|&v| !v);

                if all_true && after_warmup.len() > 10 {
                    panic!(
                        "[{}] All values after warmup are true, possible poison pattern \
						 with params: period={} (param set {}). This is highly unlikely \
						 for real TTM trend calculations.",
                        test_name, period, param_idx
                    );
                }
            }
        }

        Ok(())
    }

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

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

    generate_all_ttm_tests!(
        check_ttm_partial_params,
        check_ttm_accuracy,
        check_ttm_zero_period,
        check_ttm_period_exceeds_length,
        check_ttm_very_small_dataset,
        check_ttm_all_nan,
        check_ttm_streaming,
        check_ttm_trend_no_poison
    );

    fn check_batch_default_row(test: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
        skip_if_unsupported!(kernel, test);
        let file = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
        let c = read_candles_from_csv(file)?;
        let src = source_type(&c, "hl2");
        let close = source_type(&c, "close");
        let output = TtmTrendBatchBuilder::new()
            .kernel(kernel)
            .apply_slices(src, close)?;
        let def = TtmTrendParams::default();
        let row = output.values_for(&def).expect("default row missing");
        assert_eq!(row.len(), close.len());
        let expected = [true, false, false, false, false];
        let start = row.len() - 5;
        for (i, &v) in row[start..].iter().enumerate() {
            assert_eq!(v, expected[i]);
        }
        Ok(())
    }

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

        let file = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
        let c = read_candles_from_csv(file)?;
        let src = source_type(&c, "hl2");
        let close = source_type(&c, "close");

        let test_configs = vec![
            (1, 10, 1),
            (2, 10, 2),
            (5, 25, 5),
            (10, 50, 10),
            (1, 5, 1),
            (20, 100, 20),
            (7, 21, 7),
            (3, 30, 3),
            (15, 15, 0),
        ];

        for (cfg_idx, &(p_start, p_end, p_step)) in test_configs.iter().enumerate() {
            let output = TtmTrendBatchBuilder::new()
                .kernel(kernel)
                .period_range(p_start, p_end, p_step)
                .apply_slices(src, close)?;

            let first_valid = src
                .iter()
                .zip(close.iter())
                .position(|(&s, &c)| !s.is_nan() && !c.is_nan())
                .unwrap_or(0);

            for (idx, &val) in output.values.iter().enumerate() {
                let row = idx / output.cols;
                let col = idx % output.cols;
                let combo = &output.combos[row];
                let period = combo.period.unwrap_or(5);
                let warmup_end = first_valid + period - 1;

                if col < warmup_end {
                    if val {
                        panic!(
                            "[{}] Config {}: Found unexpected true value (poison) \
							 at row {} col {} (flat index {}) in warmup period \
							 with params: period={} (warmup ends at col {})",
                            test,
                            cfg_idx,
                            row,
                            col,
                            idx,
                            period,
                            warmup_end - 1
                        );
                    }
                }
            }

            for row in 0..output.rows {
                let start_idx = row * output.cols;
                let row_values = &output.values[start_idx..start_idx + output.cols];
                let period = output.combos[row].period.unwrap_or(5);
                let warmup_end = first_valid + period - 1;

                if warmup_end < row_values.len() {
                    let after_warmup = &row_values[warmup_end..];
                    if after_warmup.len() > 10 && after_warmup.iter().all(|&v| v) {
                        panic!(
                            "[{}] Config {}: Row {} has all true values after warmup, \
							 possible poison pattern with period={}. This is highly unlikely \
							 for real TTM trend calculations.",
                            test, cfg_idx, row, period
                        );
                    }
                }
            }
        }

        Ok(())
    }

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

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

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

        {
            let source = vec![100.0, 200.0, 300.0, 400.0, 500.0];
            let close = vec![150.0, 250.0, 350.0, 450.0, 550.0];
            let period = 2;
            let params = TtmTrendParams {
                period: Some(period),
            };
            let input = TtmTrendInput::from_slices(&source, &close, params);
            let result = ttm_trend_with_kernel(&input, kernel)?;

            assert!(
                result.values[1],
                "Manual test failed at index 1 for {}",
                test_name
            );

            assert!(
                result.values[2],
                "Manual test failed at index 2 for {}",
                test_name
            );
        }

        let strat = (2usize..=50).prop_flat_map(|period| {
            let data_len = period * 2 + 50;
            (
                (100f64..10000f64),
                prop::collection::vec((-0.02f64..0.02f64), data_len - 1),
                Just(period),
                (0.005f64..0.02f64),
            )
        });

        proptest::test_runner::TestRunner::default()
            .run(
                &strat,
                |(start_price, price_changes, period, spread_factor)| {
                    let mut base_prices = Vec::with_capacity(price_changes.len() + 1);
                    base_prices.push(start_price);

                    let mut current_price = start_price;
                    for &change_pct in &price_changes {
                        current_price *= 1.0 + change_pct;
                        current_price = current_price.max(10.0);
                        base_prices.push(current_price);
                    }

                    let mut source = Vec::with_capacity(base_prices.len());
                    let mut close = Vec::with_capacity(base_prices.len());

                    for (i, &base) in base_prices.iter().enumerate() {
                        let spread = base * spread_factor;
                        let high = base + spread;
                        let low = base - spread;

                        source.push((high + low) / 2.0);

                        let close_ratio = ((i as f64 * 0.3).sin() + 1.0) / 2.0;
                        close.push(low + (high - low) * close_ratio);
                    }
                    let params = TtmTrendParams {
                        period: Some(period),
                    };
                    let input = TtmTrendInput::from_slices(&source, &close, params);

                    let result = ttm_trend_with_kernel(&input, kernel)?;
                    let values = result.values;

                    let ref_result = ttm_trend_with_kernel(&input, Kernel::Scalar)?;
                    let ref_values = ref_result.values;

                    let first_valid = source
                        .iter()
                        .zip(close.iter())
                        .position(|(&s, &c)| !s.is_nan() && !c.is_nan())
                        .unwrap_or(0);
                    let warmup_end = first_valid + period - 1;

                    prop_assert_eq!(values.len(), source.len());
                    prop_assert_eq!(values.len(), close.len());

                    for i in 0..warmup_end.min(values.len()) {
                        prop_assert!(
                            !values[i],
                            "Expected false during warmup at index {} (warmup ends at {})",
                            i,
                            warmup_end - 1
                        );
                    }

                    if warmup_end + 1 < values.len() {
                        let mut sum = 0.0;
                        for j in (first_valid + 1)..(first_valid + period + 1) {
                            sum += source[j];
                        }

                        for i in (warmup_end + 1)..values.len() {
                            let avg = sum / (period as f64);
                            let expected = close[i] > avg;

                            prop_assert_eq!(
							values[i], expected,
							"Calculation mismatch at index {}: close={:.4}, avg={:.4}, expected={}, got={}",
							i, close[i], avg, expected, values[i]
						);

                            if i + 1 < source.len() {
                                sum += source[i + 1] - source[i + 1 - period];
                            }
                        }
                    }

                    for i in 0..values.len() {
                        prop_assert_eq!(
                            values[i],
                            ref_values[i],
                            "Kernel mismatch at index {}: {} kernel={}, scalar={}",
                            i,
                            test_name,
                            values[i],
                            ref_values[i]
                        );
                    }

                    if period == 1 {
                        for i in first_valid..values.len() {
                            let expected = close[i] > source[i];
                            prop_assert_eq!(
							values[i], expected,
							"Period=1 mismatch at index {}: close={}, source={}, expected={}, got={}",
							i, close[i], source[i], expected, values[i]
						);
                        }
                    }

                    let all_source_same = source.windows(2).all(|w| (w[0] - w[1]).abs() < 1e-10);
                    let all_close_same = close.windows(2).all(|w| (w[0] - w[1]).abs() < 1e-10);
                    if all_source_same && all_close_same && !source.is_empty() && !close.is_empty()
                    {
                        let expected_const = close[0] > source[0];
                        for i in warmup_end..values.len() {
                            prop_assert_eq!(
                                values[i],
                                expected_const,
                                "Constant input mismatch at index {}: expected={}, got={}",
                                i,
                                expected_const,
                                values[i]
                            );
                        }
                    }

                    let mut transitions = 0;
                    for i in (warmup_end + 1)..values.len() {
                        if values[i] != values[i - 1] {
                            transitions += 1;

                            let mut sum = 0.0;
                            for j in (i + 1 - period)..=i {
                                sum += source[j];
                            }
                            let avg = sum / (period as f64);

                            prop_assert!(
                                (close[i] - avg).abs() < source[i] * 0.1
                                    || (values[i] && close[i] > avg)
                                    || (!values[i] && close[i] <= avg),
                                "Invalid transition at index {}: close={:.4}, avg={:.4}, value={}",
                                i,
                                close[i],
                                avg,
                                values[i]
                            );
                        }
                    }

                    if period == source.len() - 1 && source.len() > 2 {
                        for i in 0..(source.len() - 1) {
                            prop_assert!(
                                !values[i],
                                "Expected false for extreme period at index {} (period={}, len={})",
                                i,
                                period,
                                source.len()
                            );
                        }
                    }

                    let result2 = ttm_trend_with_kernel(&input, kernel)?;
                    for i in 0..values.len() {
                        prop_assert_eq!(
                            values[i],
                            result2.values[i],
                            "Non-deterministic result at index {}",
                            i
                        );
                    }

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

        Ok(())
    }

    #[cfg(feature = "proptest")]
    generate_all_ttm_tests!(check_ttm_trend_property);
}

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

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

#[cfg(feature = "python")]
#[pyfunction(name = "ttm_trend")]
#[pyo3(signature = (source, close, period, kernel=None))]
pub fn ttm_trend_py<'py>(
    py: Python<'py>,
    source: PyReadonlyArray1<'py, f64>,
    close: PyReadonlyArray1<'py, f64>,
    period: usize,
    kernel: Option<&str>,
) -> PyResult<Bound<'py, PyArray1<f64>>> {
    let s = source.as_slice()?;
    let c = close.as_slice()?;
    let kern = validate_kernel(kernel, false)?;
    let params = TtmTrendParams {
        period: Some(period),
    };
    let input = TtmTrendInput::from_slices(s, c, params);

    let len = s.len().min(c.len());
    let out = unsafe { PyArray1::<f64>::new(py, [len], false) };
    let dst = unsafe { out.as_slice_mut()? };

    py.allow_threads(|| {
        let (ss, cc, p, first, chosen) = ttm_prepare(&input, kern).map_err(|e| e.to_string())?;
        for v in &mut dst[..first + p - 1] {
            *v = f64::NAN;
        }
        ttm_numeric_compute_into(ss, cc, p, first, dst);
        Ok::<(), String>(())
    })
    .map_err(|e: String| PyValueError::new_err(e))?;

    Ok(out)
}

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

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

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

#[cfg(feature = "python")]
#[pyfunction(name = "ttm_trend_batch")]
#[pyo3(signature = (source, close, period_range, kernel=None))]
pub fn ttm_trend_batch_py<'py>(
    py: Python<'py>,
    source: PyReadonlyArray1<'py, f64>,
    close: PyReadonlyArray1<'py, f64>,
    period_range: (usize, usize, usize),
    kernel: Option<&str>,
) -> PyResult<Bound<'py, PyDict>> {
    let s = source.as_slice()?;
    let c = close.as_slice()?;
    let kern = validate_kernel(kernel, true)?;
    let sweep = TtmTrendBatchRange {
        period: period_range,
    };

    let (vals_f64, combos, rows, cols) = py
        .allow_threads(|| {
            let k = match kern {
                Kernel::Auto => detect_best_batch_kernel(),
                k => k,
            };
            let simd = match k {
                Kernel::Avx512Batch => Kernel::Avx512,
                Kernel::Avx2Batch => Kernel::Avx2,
                Kernel::ScalarBatch => Kernel::Scalar,
                _ => k,
            };
            ttm_batch_inner_f64(s, c, &sweep, simd, true).map_err(|e| e.to_string())
        })
        .map_err(|e: String| PyValueError::new_err(e))?;

    let dict = PyDict::new(py);

    let arr = unsafe { PyArray1::<f64>::from_vec(py, vals_f64) }.reshape((rows, cols))?;
    dict.set_item("values", arr)?;
    dict.set_item(
        "periods",
        combos
            .iter()
            .map(|p| p.period.unwrap() as u64)
            .collect::<Vec<_>>()
            .into_pyarray(py),
    )?;
    Ok(dict)
}

#[inline]
pub fn ttm_trend_into_slice_f64(
    dst: &mut [f64],
    input: &TtmTrendInput,
    kern: Kernel,
) -> Result<(), TtmTrendError> {
    ttm_numeric_into_slice(dst, input, kern)
}

#[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
#[inline]
pub fn ttm_trend_into(input: &TtmTrendInput, out: &mut [f64]) -> Result<(), TtmTrendError> {
    ttm_trend_into_slice_f64(out, input, Kernel::Auto)
}

#[inline]
pub fn ttm_trend_into_slice(
    dst: &mut [bool],
    input: &TtmTrendInput,
    kern: Kernel,
) -> Result<(), TtmTrendError> {
    let (source, close, period, first, chosen) = ttm_prepare(input, kern)?;
    let len = source.len().min(close.len());
    if dst.len() != len {
        return Err(TtmTrendError::OutputLengthMismatch {
            expected: len,
            got: dst.len(),
        });
    }
    match chosen {
        Kernel::Scalar | Kernel::ScalarBatch => ttm_trend_scalar(source, close, period, first, dst),
        #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
        Kernel::Avx2 | Kernel::Avx2Batch => ttm_trend_avx2(source, close, period, first, dst),
        #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
        Kernel::Avx512 | Kernel::Avx512Batch => ttm_trend_avx512(source, close, period, first, dst),
        #[cfg(not(all(feature = "nightly-avx", target_arch = "x86_64")))]
        Kernel::Avx2 | Kernel::Avx2Batch | Kernel::Avx512 | Kernel::Avx512Batch => {
            ttm_trend_scalar(source, close, period, first, dst)
        }
        _ => unreachable!(),
    }
    Ok(())
}

#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
#[wasm_bindgen]
pub fn ttm_trend_js(source: &[f64], close: &[f64], period: usize) -> Result<Vec<f64>, JsValue> {
    let input = TtmTrendInput::from_slices(
        source,
        close,
        TtmTrendParams {
            period: Some(period),
        },
    );
    let (s, c, p, first, _) =
        ttm_prepare(&input, Kernel::Auto).map_err(|e| JsValue::from_str(&e.to_string()))?;
    let mut out = alloc_with_nan_prefix(s.len().min(c.len()), first + p - 1);
    ttm_numeric_compute_into(s, c, p, first, &mut out);
    Ok(out)
}

#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
#[wasm_bindgen]
pub fn ttm_trend_into(
    source_ptr: *const f64,
    close_ptr: *const f64,
    out_ptr: *mut f64,
    len: usize,
    period: usize,
) -> Result<(), JsValue> {
    if source_ptr.is_null() || close_ptr.is_null() || out_ptr.is_null() {
        return Err(JsValue::from_str("null pointer"));
    }
    unsafe {
        let s = std::slice::from_raw_parts(source_ptr, len);
        let c = std::slice::from_raw_parts(close_ptr, len);
        let input = TtmTrendInput::from_slices(
            s,
            c,
            TtmTrendParams {
                period: Some(period),
            },
        );
        let (ss, cc, p, first, _) =
            ttm_prepare(&input, Kernel::Auto).map_err(|e| JsValue::from_str(&e.to_string()))?;
        let out = std::slice::from_raw_parts_mut(out_ptr, ss.len().min(cc.len()));
        for v in &mut out[..first + p - 1] {
            *v = f64::NAN;
        }
        ttm_numeric_compute_into(ss, cc, p, first, out);
        Ok(())
    }
}

#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
#[wasm_bindgen]
pub fn ttm_trend_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 ttm_trend_free(ptr: *mut f64, len: usize) {
    if !ptr.is_null() {
        unsafe {
            let _ = Vec::from_raw_parts(ptr, len, len);
        }
    }
}

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

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

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

    if config.period_range.0 == 0 {
        return Err(JsValue::from_str("Invalid period: period must be > 0"));
    }
    if config.period_range.1 < config.period_range.0 {
        return Err(JsValue::from_str(
            "Invalid period range: end must be >= start",
        ));
    }

    let sweep = TtmTrendBatchRange {
        period: config.period_range,
    };
    let kernel = detect_best_batch_kernel();
    let simd = match kernel {
        Kernel::Avx512Batch => Kernel::Avx512,
        Kernel::Avx2Batch => Kernel::Avx2,
        Kernel::ScalarBatch => Kernel::Scalar,
        _ => kernel,
    };

    let (values, combos, rows, cols) = ttm_batch_inner_f64(source, close, &sweep, simd, false)
        .map_err(|e| JsValue::from_str(&e.to_string()))?;

    let output = TtmTrendBatchJsOutput {
        values,
        periods: combos.iter().map(|p| p.period.unwrap()).collect(),
        rows,
        cols,
    };

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