#[cfg(all(feature = "python", feature = "cuda"))]
use crate::cuda::{cuda_available, CudaHalftrend};
#[cfg(all(feature = "python", feature = "cuda"))]
use crate::utilities::dlpack_cuda::{make_device_array_py, DeviceArrayF32Py};
#[cfg(feature = "python")]
use numpy::{IntoPyArray, PyArray1, PyArrayMethods, PyReadonlyArray1};
#[cfg(feature = "python")]
use pyo3::exceptions::PyValueError;
#[cfg(feature = "python")]
use pyo3::prelude::*;
#[cfg(feature = "python")]
use pyo3::types::PyDict;

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

use crate::utilities::data_loader::{source_type, CandleFieldFlags, Candles};
use crate::utilities::enums::Kernel;
use crate::utilities::helpers::{
    alloc_with_nan_prefix, detect_best_batch_kernel, detect_best_kernel, init_matrix_prefixes,
    make_uninit_matrix,
};
#[cfg(feature = "python")]
use crate::utilities::kernel_validation::validate_kernel;

use crate::indicators::atr::{atr, AtrInput, AtrOutput, AtrParams};
use crate::indicators::moving_averages::sma::{sma, SmaInput, SmaOutput, SmaParams};

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

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

use std::collections::{BTreeSet, HashMap};
use std::convert::AsRef;
use std::error::Error;
use std::mem::MaybeUninit;
use thiserror::Error;

#[derive(Debug, Clone)]
pub struct HalfTrendOutput {
    pub halftrend: Vec<f64>,
    pub trend: Vec<f64>,
    pub atr_high: Vec<f64>,
    pub atr_low: Vec<f64>,
    pub buy_signal: Vec<f64>,
    pub sell_signal: Vec<f64>,
}

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

impl Default for HalfTrendParams {
    fn default() -> Self {
        Self {
            amplitude: Some(2),
            channel_deviation: Some(2.0),
            atr_period: Some(100),
        }
    }
}

#[derive(Debug, Clone)]
pub enum HalfTrendData<'a> {
    Candles(&'a Candles),
    Slices {
        high: &'a [f64],
        low: &'a [f64],
        close: &'a [f64],
    },
}

#[derive(Debug, Clone)]
pub struct HalfTrendInput<'a> {
    pub data: HalfTrendData<'a>,
    pub params: HalfTrendParams,
}

impl<'a> HalfTrendInput<'a> {
    #[inline]
    pub fn from_candles(c: &'a Candles, p: HalfTrendParams) -> Self {
        Self {
            data: HalfTrendData::Candles(c),
            params: p,
        }
    }

    #[inline]
    pub fn from_slices(h: &'a [f64], l: &'a [f64], c: &'a [f64], p: HalfTrendParams) -> Self {
        Self {
            data: HalfTrendData::Slices {
                high: h,
                low: l,
                close: c,
            },
            params: p,
        }
    }

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

    #[inline]
    pub fn as_slices(&self) -> (&[f64], &[f64], &[f64]) {
        match &self.data {
            HalfTrendData::Candles(c) => (&c.high, &c.low, &c.close),
            HalfTrendData::Slices { high, low, close } => (*high, *low, *close),
        }
    }

    #[inline]
    pub fn get_amplitude(&self) -> usize {
        self.params.amplitude.unwrap_or(2)
    }

    #[inline]
    pub fn get_channel_deviation(&self) -> f64 {
        self.params.channel_deviation.unwrap_or(2.0)
    }

    #[inline]
    pub fn get_atr_period(&self) -> usize {
        self.params.atr_period.unwrap_or(100)
    }
}

#[derive(Copy, Clone, Debug)]
pub struct HalfTrendBuilder {
    amplitude: Option<usize>,
    channel_deviation: Option<f64>,
    atr_period: Option<usize>,
    kernel: Kernel,
}

impl Default for HalfTrendBuilder {
    fn default() -> Self {
        Self {
            amplitude: None,
            channel_deviation: None,
            atr_period: None,
            kernel: Kernel::Auto,
        }
    }
}

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

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

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

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

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

    #[inline(always)]
    pub fn apply(self, c: &Candles) -> Result<HalfTrendOutput, HalfTrendError> {
        let p = HalfTrendParams {
            amplitude: self.amplitude,
            channel_deviation: self.channel_deviation,
            atr_period: self.atr_period,
        };
        let i = HalfTrendInput::from_candles(c, p);
        halftrend_with_kernel(&i, self.kernel)
    }

    #[inline(always)]
    pub fn apply_slices(
        self,
        h: &[f64],
        l: &[f64],
        c: &[f64],
    ) -> Result<HalfTrendOutput, HalfTrendError> {
        let p = HalfTrendParams {
            amplitude: self.amplitude,
            channel_deviation: self.channel_deviation,
            atr_period: self.atr_period,
        };
        let i = HalfTrendInput::from_slices(h, l, c, p);
        halftrend_with_kernel(&i, self.kernel)
    }

    #[inline(always)]
    pub fn into_stream(self) -> Result<HalfTrendStream, HalfTrendError> {
        let p = HalfTrendParams {
            amplitude: self.amplitude,
            channel_deviation: self.channel_deviation,
            atr_period: self.atr_period,
        };
        HalfTrendStream::try_new(p)
    }

    pub fn with_default_candles(c: &Candles) -> Result<HalfTrendOutput, HalfTrendError> {
        Self::new().apply(c)
    }

    pub fn with_default_slices(
        h: &[f64],
        l: &[f64],
        c: &[f64],
    ) -> Result<HalfTrendOutput, HalfTrendError> {
        Self::new().apply_slices(h, l, c)
    }

    #[inline(always)]
    pub fn apply_candles(self, c: &Candles) -> Result<HalfTrendOutput, HalfTrendError> {
        self.apply(c)
    }

    #[inline(always)]
    pub fn apply_slice_triplet(
        self,
        h: &[f64],
        l: &[f64],
        c: &[f64],
    ) -> Result<HalfTrendOutput, HalfTrendError> {
        self.apply_slices(h, l, c)
    }
}

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

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

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

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

    #[error("halftrend: ATR calculation failed: {0}")]
    AtrError(String),

    #[error("halftrend: SMA calculation failed: {0}")]
    SmaError(String),

    #[error("halftrend: Invalid channel_deviation: {channel_deviation}")]
    InvalidChannelDeviation { channel_deviation: f64 },

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

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

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

#[inline(always)]
fn first_valid_ohlc(high: &[f64], low: &[f64], close: &[f64]) -> usize {
    let fh = high.iter().position(|x| !x.is_nan()).unwrap_or(usize::MAX);
    let fl = low.iter().position(|x| !x.is_nan()).unwrap_or(usize::MAX);
    let fc = close.iter().position(|x| !x.is_nan()).unwrap_or(usize::MAX);
    fh.min(fl).min(fc)
}

pub fn halftrend(input: &HalfTrendInput) -> Result<HalfTrendOutput, HalfTrendError> {
    halftrend_with_kernel(input, Kernel::Auto)
}

pub fn halftrend_with_kernel(
    input: &HalfTrendInput,
    kernel: Kernel,
) -> Result<HalfTrendOutput, HalfTrendError> {
    let mut chosen = match kernel {
        Kernel::Auto => Kernel::Scalar,
        k => k,
    };

    let (high, low, close) = input.as_slices();

    if high.is_empty() || low.is_empty() || close.is_empty() {
        return Err(HalfTrendError::EmptyInputData);
    }

    let len = high.len();
    if len != low.len() || len != close.len() {
        return Err(HalfTrendError::InvalidPeriod {
            period: len,
            data_len: high.len().max(low.len()).max(close.len()),
        });
    }

    let amplitude = input.get_amplitude();
    let channel_deviation = input.get_channel_deviation();
    let atr_period = input.get_atr_period();

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

    if !(channel_deviation.is_finite()) || channel_deviation <= 0.0 {
        return Err(HalfTrendError::InvalidChannelDeviation { channel_deviation });
    }

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

    if matches!(kernel, Kernel::Auto)
        && amplitude == 2
        && channel_deviation == 2.0
        && atr_period == 100
    {
        chosen = Kernel::Scalar;
    }

    let first = first_valid_ohlc(high, low, close);
    if first == usize::MAX {
        return Err(HalfTrendError::AllValuesNaN);
    }

    let warmup_span = amplitude.max(atr_period);
    if len - first < warmup_span {
        return Err(HalfTrendError::NotEnoughValidData {
            needed: warmup_span,
            valid: len - first,
        });
    }
    let warm = first + warmup_span - 1;

    if chosen == Kernel::Scalar && amplitude == 2 && channel_deviation == 2.0 && atr_period == 100 {
        let mut halftrend = alloc_with_nan_prefix(len, warm);
        let mut trend = alloc_with_nan_prefix(len, warm);
        let mut atr_high = alloc_with_nan_prefix(len, warm);
        let mut atr_low = alloc_with_nan_prefix(len, warm);
        let mut buy_signal = alloc_with_nan_prefix(len, warm);
        let mut sell_signal = alloc_with_nan_prefix(len, warm);

        unsafe {
            halftrend_scalar_classic(
                high,
                low,
                close,
                amplitude,
                channel_deviation,
                atr_period,
                first,
                warm,
                &mut halftrend,
                &mut trend,
                &mut atr_high,
                &mut atr_low,
                &mut buy_signal,
                &mut sell_signal,
            )?;
        }

        return Ok(HalfTrendOutput {
            halftrend,
            trend,
            atr_high,
            atr_low,
            buy_signal,
            sell_signal,
        });
    }

    let mut halftrend = alloc_with_nan_prefix(len, warm);
    let mut trend = alloc_with_nan_prefix(len, warm);
    let mut atr_high = alloc_with_nan_prefix(len, warm);
    let mut atr_low = alloc_with_nan_prefix(len, warm);
    let mut buy_signal = alloc_with_nan_prefix(len, warm);
    let mut sell_signal = alloc_with_nan_prefix(len, warm);

    let atr_input = AtrInput::from_slices(
        high,
        low,
        close,
        AtrParams {
            length: Some(atr_period),
        },
    );
    let AtrOutput { values: atr_values } =
        atr(&atr_input).map_err(|e| HalfTrendError::AtrError(e.to_string()))?;

    let sma_high_input = SmaInput::from_slice(
        high,
        SmaParams {
            period: Some(amplitude),
        },
    );
    let SmaOutput { values: highma } =
        sma(&sma_high_input).map_err(|e| HalfTrendError::SmaError(e.to_string()))?;

    let sma_low_input = SmaInput::from_slice(
        low,
        SmaParams {
            period: Some(amplitude),
        },
    );
    let SmaOutput { values: lowma } =
        sma(&sma_low_input).map_err(|e| HalfTrendError::SmaError(e.to_string()))?;

    halftrend_compute_into(
        high,
        low,
        close,
        amplitude,
        channel_deviation,
        &atr_values,
        &highma,
        &lowma,
        warm,
        chosen,
        &mut halftrend,
        &mut trend,
        &mut atr_high,
        &mut atr_low,
        &mut buy_signal,
        &mut sell_signal,
    );

    Ok(HalfTrendOutput {
        halftrend,
        trend,
        atr_high,
        atr_low,
        buy_signal,
        sell_signal,
    })
}

#[inline(always)]
fn halftrend_compute_into(
    high: &[f64],
    low: &[f64],
    close: &[f64],
    amplitude: usize,
    channel_deviation: f64,
    atr_values: &[f64],
    highma: &[f64],
    lowma: &[f64],
    start_idx: usize,
    kernel: Kernel,
    halftrend: &mut [f64],
    trend: &mut [f64],
    atr_high: &mut [f64],
    atr_low: &mut [f64],
    buy_signal: &mut [f64],
    sell_signal: &mut [f64],
) {
    match kernel {
        #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
        Kernel::Avx512 => unsafe {
            halftrend_avx512(
                high,
                low,
                close,
                amplitude,
                channel_deviation,
                atr_values,
                highma,
                lowma,
                start_idx,
                halftrend,
                trend,
                atr_high,
                atr_low,
                buy_signal,
                sell_signal,
            )
        },
        #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
        Kernel::Avx2 => unsafe {
            halftrend_avx2(
                high,
                low,
                close,
                amplitude,
                channel_deviation,
                atr_values,
                highma,
                lowma,
                start_idx,
                halftrend,
                trend,
                atr_high,
                atr_low,
                buy_signal,
                sell_signal,
            )
        },
        _ => halftrend_scalar(
            high,
            low,
            close,
            amplitude,
            channel_deviation,
            atr_values,
            highma,
            lowma,
            start_idx,
            halftrend,
            trend,
            atr_high,
            atr_low,
            buy_signal,
            sell_signal,
        ),
    }
}

#[inline]
#[inline(always)]
pub unsafe fn halftrend_scalar_classic(
    high: &[f64],
    low: &[f64],
    close: &[f64],
    amplitude: usize,
    channel_deviation: f64,
    atr_period: usize,
    first: usize,
    warm: usize,
    halftrend: &mut [f64],
    trend: &mut [f64],
    atr_high: &mut [f64],
    atr_low: &mut [f64],
    buy_signal: &mut [f64],
    sell_signal: &mut [f64],
) -> Result<(), HalfTrendError> {
    let len = high.len();
    let qnan = f64::from_bits(0x7ff8_0000_0000_0000);

    let alpha = 1.0 / atr_period as f64;
    let atr_warm = first + atr_period - 1;
    let sma_warm = first + amplitude - 1;

    let mut sum_tr = 0.0;
    for i in first..=atr_warm.min(len - 1) {
        let tr = if i == first {
            high[i] - low[i]
        } else {
            let hl = high[i] - low[i];
            let hc = (high[i] - close[i - 1]).abs();
            let lc = (low[i] - close[i - 1]).abs();
            hl.max(hc).max(lc)
        };
        sum_tr += tr;
    }
    let mut rma = sum_tr / atr_period as f64;

    let mut sum_high = 0.0;
    let mut sum_low = 0.0;
    for i in first..=sma_warm.min(len - 1) {
        sum_high += high[i];
        sum_low += low[i];
    }

    for i in (sma_warm + 1)..=warm.min(len - 1) {
        sum_high = sum_high - high[i - amplitude] + high[i];
        sum_low = sum_low - low[i - amplitude] + low[i];
    }
    let inv_amp = 1.0 / amplitude as f64;

    let mut current_trend = 0i32;
    let mut next_trend = 0i32;
    let mut up = 0.0f64;
    let mut down = 0.0f64;
    let mut max_low_price = if warm > 0 { low[warm - 1] } else { low[0] };
    let mut min_high_price = if warm > 0 { high[warm - 1] } else { high[0] };

    let ch_half = channel_deviation * 0.5;
    for i in warm..len {
        buy_signal[i] = qnan;
        sell_signal[i] = qnan;

        let highma_i = sum_high * inv_amp;
        let lowma_i = sum_low * inv_amp;

        let high_price = if high[i] > high[i - 1] {
            high[i]
        } else {
            high[i - 1]
        };
        let low_price = if low[i] < low[i - 1] {
            low[i]
        } else {
            low[i - 1]
        };

        let prev_low = low[i - 1];
        let prev_high = high[i - 1];

        if next_trend == 1 {
            if low_price > max_low_price {
                max_low_price = low_price;
            }
            if highma_i < max_low_price && close[i] < prev_low {
                current_trend = 1;
                next_trend = 0;
                min_high_price = high_price;
            }
        } else {
            if high_price < min_high_price {
                min_high_price = high_price;
            }
            if lowma_i > min_high_price && close[i] > prev_high {
                current_trend = 0;
                next_trend = 1;
                max_low_price = low_price;
            }
        }

        let a = rma;
        let atr2 = 0.5 * a;
        let dev = a.mul_add(ch_half, 0.0);

        if current_trend == 0 {
            if i > warm && trend[i - 1] != 0.0 {
                up = down;
                buy_signal[i] = up - atr2;
            } else {
                up = if i == warm || up == 0.0 {
                    max_low_price
                } else if max_low_price > up {
                    max_low_price
                } else {
                    up
                };
            }
            halftrend[i] = up;
            atr_high[i] = up + dev;
            atr_low[i] = up - dev;
            trend[i] = 0.0;
        } else {
            if i > warm && trend[i - 1] != 1.0 {
                down = up;
                sell_signal[i] = down + atr2;
            } else {
                down = if i == warm || down == 0.0 {
                    min_high_price
                } else if min_high_price < down {
                    min_high_price
                } else {
                    down
                };
            }
            halftrend[i] = down;
            atr_high[i] = down + dev;
            atr_low[i] = down - dev;
            trend[i] = 1.0;
        }

        let ni = i + 1;
        if ni < len {
            sum_high = sum_high - high[ni - amplitude] + high[ni];
            sum_low = sum_low - low[ni - amplitude] + low[ni];

            let hl = high[ni] - low[ni];
            let hc = (high[ni] - close[ni - 1]).abs();
            let lc = (low[ni] - close[ni - 1]).abs();
            let tr = hl.max(hc).max(lc);
            rma += alpha * (tr - rma);
        }
    }

    Ok(())
}

pub fn halftrend_scalar(
    high: &[f64],
    low: &[f64],
    close: &[f64],
    amplitude: usize,
    channel_deviation: f64,
    atr_values: &[f64],
    highma: &[f64],
    lowma: &[f64],
    start_idx: usize,
    halftrend: &mut [f64],
    trend: &mut [f64],
    atr_high: &mut [f64],
    atr_low: &mut [f64],
    buy_signal: &mut [f64],
    sell_signal: &mut [f64],
) {
    let len = high.len();
    let qnan = f64::from_bits(0x7ff8_0000_0000_0000);

    let cap = amplitude.max(1);
    let mut max_idx = vec![0usize; cap];
    let mut max_val = vec![0.0f64; cap];
    let mut min_idx = vec![0usize; cap];
    let mut min_val = vec![0.0f64; cap];
    let (mut max_head, mut max_tail, mut max_cnt) = (0usize, 0usize, 0usize);
    let (mut min_head, mut min_tail, mut min_cnt) = (0usize, 0usize, 0usize);

    #[inline(always)]
    fn inc(i: usize, cap: usize) -> usize {
        let j = i + 1;
        if j == cap {
            0
        } else {
            j
        }
    }
    #[inline(always)]
    fn dec(i: usize, cap: usize) -> usize {
        if i == 0 {
            cap - 1
        } else {
            i - 1
        }
    }

    if start_idx < len {
        debug_assert!(start_idx + 1 >= cap);
        let wstart0 = start_idx + 1 - cap;
        for k in wstart0..=start_idx {
            let hv = high[k];
            while max_cnt > 0 {
                let back = dec(max_tail, cap);
                if max_val[back] <= hv {
                    max_tail = back;
                    max_cnt -= 1;
                } else {
                    break;
                }
            }
            max_val[max_tail] = hv;
            max_idx[max_tail] = k;
            max_tail = inc(max_tail, cap);
            max_cnt += 1;

            let lv = low[k];
            while min_cnt > 0 {
                let back = dec(min_tail, cap);
                if min_val[back] >= lv {
                    min_tail = back;
                    min_cnt -= 1;
                } else {
                    break;
                }
            }
            min_val[min_tail] = lv;
            min_idx[min_tail] = k;
            min_tail = inc(min_tail, cap);
            min_cnt += 1;
        }
    }

    let mut current_trend = 0i32;
    let mut next_trend = 0i32;
    let mut up = 0.0f64;
    let mut down = 0.0f64;
    let mut max_low_price = if start_idx > 0 {
        low[start_idx - 1]
    } else {
        low[0]
    };
    let mut min_high_price = if start_idx > 0 {
        high[start_idx - 1]
    } else {
        high[0]
    };

    let ch_half = channel_deviation * 0.5;

    for i in start_idx..len {
        buy_signal[i] = qnan;
        sell_signal[i] = qnan;

        if i > start_idx {
            let wstart = i + 1 - cap;
            while max_cnt > 0 && max_idx[max_head] < wstart {
                max_head = inc(max_head, cap);
                max_cnt -= 1;
            }
            while min_cnt > 0 && min_idx[min_head] < wstart {
                min_head = inc(min_head, cap);
                min_cnt -= 1;
            }

            let hv = high[i];
            while max_cnt > 0 {
                let back = dec(max_tail, cap);
                if max_val[back] <= hv {
                    max_tail = back;
                    max_cnt -= 1;
                } else {
                    break;
                }
            }
            max_val[max_tail] = hv;
            max_idx[max_tail] = i;
            max_tail = inc(max_tail, cap);
            max_cnt += 1;

            let lv = low[i];
            while min_cnt > 0 {
                let back = dec(min_tail, cap);
                if min_val[back] >= lv {
                    min_tail = back;
                    min_cnt -= 1;
                } else {
                    break;
                }
            }
            min_val[min_tail] = lv;
            min_idx[min_tail] = i;
            min_tail = inc(min_tail, cap);
            min_cnt += 1;
        }

        debug_assert!(max_cnt > 0 && min_cnt > 0);
        let high_price = max_val[max_head];
        let low_price = min_val[min_head];

        let prev_low = if i > 0 { low[i - 1] } else { low[0] };
        let prev_high = if i > 0 { high[i - 1] } else { high[0] };

        if next_trend == 1 {
            if low_price > max_low_price {
                max_low_price = low_price;
            }
            if highma[i] < max_low_price && close[i] < prev_low {
                current_trend = 1;
                next_trend = 0;
                min_high_price = high_price;
            }
        } else {
            if high_price < min_high_price {
                min_high_price = high_price;
            }
            if lowma[i] > min_high_price && close[i] > prev_high {
                current_trend = 0;
                next_trend = 1;
                max_low_price = low_price;
            }
        }

        let a = atr_values[i];
        let atr2 = 0.5 * a;
        let dev = a * ch_half;

        if current_trend == 0 {
            if i > start_idx && trend[i - 1] != 0.0 {
                up = down;
                buy_signal[i] = up - atr2;
            } else {
                up = if i == start_idx || up == 0.0 {
                    max_low_price
                } else if max_low_price > up {
                    max_low_price
                } else {
                    up
                };
            }
            halftrend[i] = up;
            atr_high[i] = up + dev;
            atr_low[i] = up - dev;
            trend[i] = 0.0;
        } else {
            if i > start_idx && trend[i - 1] != 1.0 {
                down = up;
                sell_signal[i] = down + atr2;
            } else {
                down = if i == start_idx || down == 0.0 {
                    min_high_price
                } else if min_high_price < down {
                    min_high_price
                } else {
                    down
                };
            }
            halftrend[i] = down;
            atr_high[i] = down + dev;
            atr_low[i] = down - dev;
            trend[i] = 1.0;
        }
    }
}

#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
#[target_feature(enable = "avx2,fma")]
unsafe fn halftrend_avx2(
    high: &[f64],
    low: &[f64],
    close: &[f64],
    amplitude: usize,
    channel_deviation: f64,
    atr_values: &[f64],
    highma: &[f64],
    lowma: &[f64],
    start_idx: usize,
    halftrend: &mut [f64],
    trend: &mut [f64],
    atr_high: &mut [f64],
    atr_low: &mut [f64],
    buy_signal: &mut [f64],
    sell_signal: &mut [f64],
) {
    halftrend_scalar(
        high,
        low,
        close,
        amplitude,
        channel_deviation,
        atr_values,
        highma,
        lowma,
        start_idx,
        halftrend,
        trend,
        atr_high,
        atr_low,
        buy_signal,
        sell_signal,
    )
}

#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
#[target_feature(enable = "avx512f,fma")]
unsafe fn halftrend_avx512(
    high: &[f64],
    low: &[f64],
    close: &[f64],
    amplitude: usize,
    channel_deviation: f64,
    atr_values: &[f64],
    highma: &[f64],
    lowma: &[f64],
    start_idx: usize,
    halftrend: &mut [f64],
    trend: &mut [f64],
    atr_high: &mut [f64],
    atr_low: &mut [f64],
    buy_signal: &mut [f64],
    sell_signal: &mut [f64],
) {
    halftrend_scalar(
        high,
        low,
        close,
        amplitude,
        channel_deviation,
        atr_values,
        highma,
        lowma,
        start_idx,
        halftrend,
        trend,
        atr_high,
        atr_low,
        buy_signal,
        sell_signal,
    )
}

#[inline]
pub fn halftrend_into_slices(
    out_halftrend: &mut [f64],
    out_trend: &mut [f64],
    out_atr_high: &mut [f64],
    out_atr_low: &mut [f64],
    out_buy_signal: &mut [f64],
    out_sell_signal: &mut [f64],
    input: &HalfTrendInput,
) -> Result<(), HalfTrendError> {
    let (high, low, close) = input.as_slices();

    if high.is_empty() || low.is_empty() || close.is_empty() {
        return Err(HalfTrendError::EmptyInputData);
    }

    let len = high.len();
    if out_halftrend.len() != len
        || out_trend.len() != len
        || out_atr_high.len() != len
        || out_atr_low.len() != len
        || out_buy_signal.len() != len
        || out_sell_signal.len() != len
    {
        return Err(HalfTrendError::OutputLengthMismatch {
            expected: len,
            got: out_halftrend.len(),
        });
    }

    let amplitude = input.get_amplitude();
    let atr_period = input.get_atr_period();
    let channel_deviation = input.get_channel_deviation();

    let first = first_valid_ohlc(high, low, close);
    if first == usize::MAX {
        return Err(HalfTrendError::AllValuesNaN);
    }

    let warmup_span = amplitude.max(atr_period);
    if len - first < warmup_span {
        return Err(HalfTrendError::NotEnoughValidData {
            needed: warmup_span,
            valid: len - first,
        });
    }
    let warm = first + warmup_span - 1;

    for v in [
        &mut *out_halftrend,
        out_trend,
        out_atr_high,
        out_atr_low,
        out_buy_signal,
        out_sell_signal,
    ] {
        for x in &mut v[..warm] {
            *x = f64::NAN;
        }
    }

    let atr_out = atr(&AtrInput::from_slices(
        high,
        low,
        close,
        AtrParams {
            length: Some(atr_period),
        },
    ))
    .map_err(|e| HalfTrendError::AtrError(e.to_string()))?
    .values;
    let highma = sma(&SmaInput::from_slice(
        high,
        SmaParams {
            period: Some(amplitude),
        },
    ))
    .map_err(|e| HalfTrendError::SmaError(e.to_string()))?
    .values;
    let lowma = sma(&SmaInput::from_slice(
        low,
        SmaParams {
            period: Some(amplitude),
        },
    ))
    .map_err(|e| HalfTrendError::SmaError(e.to_string()))?
    .values;

    halftrend_scalar(
        high,
        low,
        close,
        amplitude,
        channel_deviation,
        &atr_out,
        &highma,
        &lowma,
        warm,
        out_halftrend,
        out_trend,
        out_atr_high,
        out_atr_low,
        out_buy_signal,
        out_sell_signal,
    );

    Ok(())
}

#[inline]
pub fn halftrend_into_slices_kernel(
    out_halftrend: &mut [f64],
    out_trend: &mut [f64],
    out_atr_high: &mut [f64],
    out_atr_low: &mut [f64],
    out_buy_signal: &mut [f64],
    out_sell_signal: &mut [f64],
    input: &HalfTrendInput,
    kern: Kernel,
) -> Result<(), HalfTrendError> {
    let (high, low, close) = input.as_slices();
    if high.is_empty() || low.is_empty() || close.is_empty() {
        return Err(HalfTrendError::EmptyInputData);
    }
    let len = high.len();
    if out_halftrend.len() != len
        || out_trend.len() != len
        || out_atr_high.len() != len
        || out_atr_low.len() != len
        || out_buy_signal.len() != len
        || out_sell_signal.len() != len
    {
        return Err(HalfTrendError::OutputLengthMismatch {
            expected: len,
            got: out_halftrend.len(),
        });
    }

    let amplitude = input.get_amplitude();
    let atr_period = input.get_atr_period();
    let ch = input.get_channel_deviation();

    if amplitude == 0 || amplitude > len {
        return Err(HalfTrendError::InvalidPeriod {
            period: amplitude,
            data_len: len,
        });
    }
    if atr_period == 0 || atr_period > len {
        return Err(HalfTrendError::InvalidPeriod {
            period: atr_period,
            data_len: len,
        });
    }
    if !(ch.is_finite()) || ch <= 0.0 {
        return Err(HalfTrendError::InvalidChannelDeviation {
            channel_deviation: ch,
        });
    }

    let first = first_valid_ohlc(high, low, close);
    if first == usize::MAX {
        return Err(HalfTrendError::AllValuesNaN);
    }
    let warmup_span = amplitude.max(atr_period);
    if len - first < warmup_span {
        return Err(HalfTrendError::NotEnoughValidData {
            needed: warmup_span,
            valid: len - first,
        });
    }
    let warm = first + warmup_span - 1;

    let qnan = f64::from_bits(0x7ff8_0000_0000_0000);
    for x in &mut out_halftrend[..warm] {
        *x = qnan;
    }
    for x in &mut out_trend[..warm] {
        *x = qnan;
    }
    for x in &mut out_atr_high[..warm] {
        *x = qnan;
    }
    for x in &mut out_atr_low[..warm] {
        *x = qnan;
    }
    for x in &mut out_buy_signal[..warm] {
        *x = qnan;
    }
    for x in &mut out_sell_signal[..warm] {
        *x = qnan;
    }

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

    if matches!(kern, Kernel::Auto) && amplitude == 2 && ch == 2.0 && atr_period == 100 {
        chosen = Kernel::Scalar;
    }

    if chosen == Kernel::Scalar && amplitude == 2 && ch == 2.0 && atr_period == 100 {
        unsafe {
            halftrend_scalar_classic(
                high,
                low,
                close,
                amplitude,
                ch,
                atr_period,
                first,
                warm,
                out_halftrend,
                out_trend,
                out_atr_high,
                out_atr_low,
                out_buy_signal,
                out_sell_signal,
            )?;
        }
        return Ok(());
    }

    let AtrOutput { values: av } = atr(&AtrInput::from_slices(
        high,
        low,
        close,
        AtrParams {
            length: Some(atr_period),
        },
    ))
    .map_err(|e| HalfTrendError::AtrError(e.to_string()))?;
    let SmaOutput { values: hma } = sma(&SmaInput::from_slice(
        high,
        SmaParams {
            period: Some(amplitude),
        },
    ))
    .map_err(|e| HalfTrendError::SmaError(e.to_string()))?;
    let SmaOutput { values: lma } = sma(&SmaInput::from_slice(
        low,
        SmaParams {
            period: Some(amplitude),
        },
    ))
    .map_err(|e| HalfTrendError::SmaError(e.to_string()))?;

    halftrend_compute_into(
        high,
        low,
        close,
        amplitude,
        ch,
        &av,
        &hma,
        &lma,
        warm,
        chosen,
        out_halftrend,
        out_trend,
        out_atr_high,
        out_atr_low,
        out_buy_signal,
        out_sell_signal,
    );

    Ok(())
}

#[inline]
pub fn halftrend_into_slice(
    out_halftrend: &mut [f64],
    out_trend: &mut [f64],
    out_atr_high: &mut [f64],
    out_atr_low: &mut [f64],
    out_buy_signal: &mut [f64],
    out_sell_signal: &mut [f64],
    input: &HalfTrendInput,
) -> Result<(), HalfTrendError> {
    halftrend_into_slices_kernel(
        out_halftrend,
        out_trend,
        out_atr_high,
        out_atr_low,
        out_buy_signal,
        out_sell_signal,
        input,
        Kernel::Auto,
    )
}

#[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
#[inline]
pub fn halftrend_into(
    input: &HalfTrendInput,
    out_halftrend: &mut [f64],
    out_trend: &mut [f64],
    out_atr_high: &mut [f64],
    out_atr_low: &mut [f64],
    out_buy_signal: &mut [f64],
    out_sell_signal: &mut [f64],
) -> Result<(), HalfTrendError> {
    halftrend_into_slices_kernel(
        out_halftrend,
        out_trend,
        out_atr_high,
        out_atr_low,
        out_buy_signal,
        out_sell_signal,
        input,
        Kernel::Auto,
    )
}

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

    #[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
    #[test]
    fn test_halftrend_into_matches_api() -> Result<(), Box<dyn Error>> {
        let len = 256usize;
        let mut high = Vec::with_capacity(len);
        let mut low = Vec::with_capacity(len);
        let mut close = Vec::with_capacity(len);
        for i in 0..len {
            let t = i as f64;
            let c = 100.0 + 0.1 * t + (t * 0.03).sin();
            close.push(c);
            high.push(c + 1.0 + (t * 0.01).cos() * 0.1);
            low.push(c - 1.0 - (t * 0.02).sin() * 0.1);
        }

        let input = HalfTrendInput::from_slices(&high, &low, &close, HalfTrendParams::default());

        let base = halftrend(&input)?;

        let mut ht = vec![0.0; len];
        let mut tr = vec![0.0; len];
        let mut ah = vec![0.0; len];
        let mut al = vec![0.0; len];
        let mut bs = vec![0.0; len];
        let mut ss = vec![0.0; len];

        halftrend_into(&input, &mut ht, &mut tr, &mut ah, &mut al, &mut bs, &mut ss)?;

        fn eq_or_both_nan(a: f64, b: f64) -> bool {
            (a.is_nan() && b.is_nan()) || (a == b) || ((a - b).abs() <= 1e-12)
        }

        assert_eq!(base.halftrend.len(), ht.len());
        assert_eq!(base.trend.len(), tr.len());
        assert_eq!(base.atr_high.len(), ah.len());
        assert_eq!(base.atr_low.len(), al.len());
        assert_eq!(base.buy_signal.len(), bs.len());
        assert_eq!(base.sell_signal.len(), ss.len());

        for i in 0..len {
            assert!(
                eq_or_both_nan(base.halftrend[i], ht[i]),
                "halftrend mismatch at {}",
                i
            );
            assert!(
                eq_or_both_nan(base.trend[i], tr[i]),
                "trend mismatch at {}",
                i
            );
            assert!(
                eq_or_both_nan(base.atr_high[i], ah[i]),
                "atr_high mismatch at {}",
                i
            );
            assert!(
                eq_or_both_nan(base.atr_low[i], al[i]),
                "atr_low mismatch at {}",
                i
            );
            assert!(
                eq_or_both_nan(base.buy_signal[i], bs[i]),
                "buy_signal mismatch at {}",
                i
            );
            assert!(
                eq_or_both_nan(base.sell_signal[i], ss[i]),
                "sell_signal mismatch at {}",
                i
            );
        }

        Ok(())
    }

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

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

        let input = HalfTrendInput::from_candles(&candles, HalfTrendParams::default());
        let output = halftrend_with_kernel(&input, kernel)?;

        let test_indices = vec![15570, 15571, 15574, 15575, 15576];
        let expected_halftrend = vec![59763.0, 59763.0, 59763.0, 59310.0, 59310.0];
        let expected_trend = vec![0.0, 0.0, 1.0, 1.0, 1.0];

        for (i, &idx) in test_indices.iter().enumerate() {
            assert!(
                (output.halftrend[idx] - expected_halftrend[i]).abs() < 1.0,
                "[{}] HalfTrend mismatch at index {}: expected {}, got {}",
                test_name,
                idx,
                expected_halftrend[i],
                output.halftrend[idx]
            );
            assert!(
                (output.trend[idx] - expected_trend[i]).abs() < 0.01,
                "[{}] Trend mismatch at index {}: expected {}, got {}",
                test_name,
                idx,
                expected_trend[i],
                output.trend[idx]
            );
        }
        Ok(())
    }

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

        let candles = Candles {
            timestamp: vec![],
            high: vec![],
            low: vec![],
            close: vec![],
            open: vec![],
            volume: vec![],
            fields: CandleFieldFlags {
                open: true,
                high: true,
                low: true,
                close: true,
                volume: true,
            },
            hl2: vec![],
            hlc3: vec![],
            ohlc4: vec![],
            hlcc4: vec![],
        };

        let input = HalfTrendInput::from_candles(&candles, HalfTrendParams::default());
        let result = halftrend_with_kernel(&input, kernel);

        assert!(
            matches!(result, Err(HalfTrendError::EmptyInputData)),
            "[{}] Expected EmptyInputData error",
            test_name
        );
        Ok(())
    }

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

        let candles = Candles {
            timestamp: vec![0; 100],
            high: vec![f64::NAN; 100],
            low: vec![f64::NAN; 100],
            close: vec![f64::NAN; 100],
            open: vec![f64::NAN; 100],
            volume: vec![f64::NAN; 100],
            fields: CandleFieldFlags {
                open: true,
                high: true,
                low: true,
                close: true,
                volume: true,
            },
            hl2: vec![f64::NAN; 100],
            hlc3: vec![f64::NAN; 100],
            ohlc4: vec![f64::NAN; 100],
            hlcc4: vec![f64::NAN; 100],
        };

        let input = HalfTrendInput::from_candles(&candles, HalfTrendParams::default());
        let result = halftrend_with_kernel(&input, kernel);

        assert!(
            matches!(result, Err(HalfTrendError::AllValuesNaN)),
            "[{}] Expected AllValuesNaN error",
            test_name
        );
        Ok(())
    }

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

        let high = vec![1.0; 10];
        let low = vec![1.0; 10];
        let close = vec![1.0; 10];
        let candles = Candles {
            timestamp: vec![0; 10],
            high: high.clone(),
            low: low.clone(),
            close: close.clone(),
            open: vec![1.0; 10],
            volume: vec![1.0; 10],
            fields: CandleFieldFlags {
                open: true,
                high: true,
                low: true,
                close: true,
                volume: true,
            },
            hl2: high
                .iter()
                .zip(low.iter())
                .map(|(h, l)| (h + l) / 2.0)
                .collect(),
            hlc3: high
                .iter()
                .zip(low.iter())
                .zip(close.iter())
                .map(|((h, l), c)| (h + l + c) / 3.0)
                .collect(),
            ohlc4: vec![1.0; 10],
            hlcc4: high
                .iter()
                .zip(low.iter())
                .zip(close.iter())
                .map(|((h, l), c)| (h + l + c + c) / 4.0)
                .collect(),
        };

        let params = HalfTrendParams {
            amplitude: Some(20),
            channel_deviation: Some(2.0),
            atr_period: Some(100),
        };

        let input = HalfTrendInput::from_candles(&candles, params);
        let result = halftrend_with_kernel(&input, kernel);

        assert!(
            matches!(result, Err(HalfTrendError::InvalidPeriod { .. })),
            "[{}] Expected InvalidPeriod error",
            test_name
        );
        Ok(())
    }

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

    fn check_halftrend_default_candles(
        test_name: &str,
        kernel: Kernel,
    ) -> Result<(), Box<dyn Error>> {
        skip_if_unsupported!(kernel, test_name);
        let file = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
        let c = read_candles_from_csv(file)?;
        let input = HalfTrendInput::with_default_candles(&c);
        let out = halftrend_with_kernel(&input, kernel)?;
        assert_eq!(out.halftrend.len(), c.close.len());
        Ok(())
    }

    fn check_halftrend_nan_handling(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
        skip_if_unsupported!(kernel, test_name);
        let file = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
        let c = read_candles_from_csv(file)?;
        let input = HalfTrendInput::from_candles(&c, HalfTrendParams::default());
        let out = halftrend_with_kernel(&input, kernel)?;
        let a = HalfTrendParams::default().amplitude.unwrap_or(2);
        let p = HalfTrendParams::default().atr_period.unwrap_or(100);
        let warm = a.max(p) - 1;
        for &v in &out.halftrend[warm.min(out.halftrend.len())..] {
            assert!(!v.is_nan(), "[{}] Found NaN after warmup", test_name);
        }
        Ok(())
    }

    fn check_halftrend_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 c = read_candles_from_csv(file)?;
        let p = HalfTrendParams::default();

        let batch = halftrend_with_kernel(&HalfTrendInput::from_candles(&c, p.clone()), kernel)?;
        let mut s = HalfTrendStream::try_new(p)?;
        let mut ht = Vec::with_capacity(c.close.len());
        let mut tr = Vec::with_capacity(c.close.len());
        let mut ah = Vec::with_capacity(c.close.len());
        let mut al = Vec::with_capacity(c.close.len());
        let mut bs = Vec::with_capacity(c.close.len());
        let mut ss = Vec::with_capacity(c.close.len());
        for i in 0..c.close.len() {
            match s.update(c.high[i], c.low[i], c.close[i]) {
                Some(o) => {
                    ht.push(o.halftrend);
                    tr.push(o.trend);
                    ah.push(o.atr_high);
                    al.push(o.atr_low);
                    bs.push(o.buy_signal.unwrap_or(f64::NAN));
                    ss.push(o.sell_signal.unwrap_or(f64::NAN));
                }
                None => {
                    ht.push(f64::NAN);
                    tr.push(f64::NAN);
                    ah.push(f64::NAN);
                    al.push(f64::NAN);
                    bs.push(f64::NAN);
                    ss.push(f64::NAN);
                }
            }
        }
        assert_eq!(batch.halftrend.len(), ht.len());

        Ok(())
    }

    #[cfg(debug_assertions)]
    fn check_halftrend_no_poison(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
        skip_if_unsupported!(kernel, test_name);
        let file = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
        let c = read_candles_from_csv(file)?;
        let out = halftrend_with_kernel(&HalfTrendInput::with_default_candles(&c), kernel)?;
        let poison = [
            0x1111_1111_1111_1111u64,
            0x2222_2222_2222_2222u64,
            0x3333_3333_3333_3333u64,
        ];
        for (name, vec) in [
            ("halftrend", &out.halftrend),
            ("trend", &out.trend),
            ("atr_high", &out.atr_high),
            ("atr_low", &out.atr_low),
            ("buy", &out.buy_signal),
            ("sell", &out.sell_signal),
        ] {
            for (i, &v) in vec.iter().enumerate() {
                if v.is_nan() {
                    continue;
                }
                let b = v.to_bits();
                for p in poison {
                    assert_ne!(b, p, "[{}] poison in {} at {}", test_name, name, i);
                }
            }
        }
        Ok(())
    }

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

    fn check_halftrend_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 c = read_candles_from_csv(file)?;
        let input = HalfTrendInput::from_candles(
            &c,
            HalfTrendParams {
                amplitude: None,
                channel_deviation: None,
                atr_period: None,
            },
        );
        let out = halftrend_with_kernel(&input, kernel)?;
        assert_eq!(out.halftrend.len(), c.close.len());
        Ok(())
    }

    fn check_halftrend_not_enough_valid(
        test_name: &str,
        kernel: Kernel,
    ) -> Result<(), Box<dyn Error>> {
        skip_if_unsupported!(kernel, test_name);
        let n = 10;
        let mut c = Candles {
            timestamp: vec![0; n],
            high: vec![f64::NAN; n],
            low: vec![f64::NAN; n],
            close: vec![f64::NAN; n],
            open: vec![f64::NAN; n],
            volume: vec![f64::NAN; n],
            fields: CandleFieldFlags {
                open: true,
                high: true,
                low: true,
                close: true,
                volume: true,
            },
            hl2: vec![f64::NAN; n],
            hlc3: vec![f64::NAN; n],
            ohlc4: vec![f64::NAN; n],
            hlcc4: vec![f64::NAN; n],
        };
        c.high[5] = 1.0;
        c.low[5] = 1.0;
        c.close[5] = 1.0;
        let p = HalfTrendParams {
            amplitude: Some(9),
            channel_deviation: Some(2.0),
            atr_period: Some(9),
        };
        let r = halftrend_with_kernel(&HalfTrendInput::from_candles(&c, p), kernel);
        assert!(matches!(r, Err(HalfTrendError::NotEnoughValidData { .. })));
        Ok(())
    }

    fn check_halftrend_invalid_chdev(
        test_name: &str,
        kernel: Kernel,
    ) -> Result<(), Box<dyn Error>> {
        skip_if_unsupported!(kernel, test_name);
        let h = [1.0, 1.0, 1.0];
        let l = [1.0, 1.0, 1.0];
        let c = [1.0, 1.0, 1.0];
        let inp = HalfTrendInput::from_slices(
            &h,
            &l,
            &c,
            HalfTrendParams {
                amplitude: Some(2),
                channel_deviation: Some(0.0),
                atr_period: Some(2),
            },
        );
        let r = halftrend_with_kernel(&inp, kernel);
        assert!(matches!(
            r,
            Err(HalfTrendError::InvalidChannelDeviation { .. })
        ));
        Ok(())
    }

    generate_all_halftrend_tests!(
        check_halftrend_accuracy,
        check_halftrend_empty_data,
        check_halftrend_all_nan,
        check_halftrend_invalid_period,
        check_halftrend_default_candles,
        check_halftrend_nan_handling,
        check_halftrend_streaming,
        check_halftrend_no_poison,
        check_halftrend_partial_params,
        check_halftrend_not_enough_valid,
        check_halftrend_invalid_chdev
    );

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

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

        let output = HalfTrendBatchBuilder::new()
            .kernel(kernel)
            .apply_candles(&c)?;

        let def = HalfTrendParams::default();
        let row = output.halftrend_for(&def).expect("default row missing");

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

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

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

        let output = HalfTrendBatchBuilder::new()
            .kernel(kernel)
            .amplitude_range(2, 4, 1)
            .channel_deviation_range(1.5, 2.5, 0.5)
            .atr_period_range(50, 150, 50)
            .apply_candles(&c)?;

        let expected_combos = 3 * 3 * 3;
        assert_eq!(output.combos.len(), expected_combos);
        assert_eq!(output.rows, expected_combos);
        assert_eq!(output.cols, c.close.len());

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

#[derive(Debug, Clone)]
pub struct HalfTrendStream {
    amplitude: usize,
    atr_period: usize,
    ch_half: f64,
    inv_amp: f64,

    atr_stream: crate::indicators::atr::AtrStream,

    max_idx: Vec<usize>,
    max_val: Vec<f64>,
    min_idx: Vec<usize>,
    min_val: Vec<f64>,
    max_head: usize,
    max_tail: usize,
    max_cnt: usize,
    min_head: usize,
    min_tail: usize,
    min_cnt: usize,

    ring_high: Vec<f64>,
    ring_low: Vec<f64>,
    ring_pos: usize,
    filled: usize,
    high_sum: f64,
    low_sum: f64,

    i: usize,
    warmup_need: usize,

    current_trend: i32,
    next_trend: i32,
    last_trend: i8,
    max_low_price: f64,
    min_high_price: f64,
    up: f64,
    down: f64,

    prev_high: f64,
    prev_low: f64,
    have_prev: bool,
}

impl HalfTrendStream {
    #[inline]
    pub fn try_new(params: HalfTrendParams) -> Result<Self, HalfTrendError> {
        let amplitude = params.amplitude.unwrap_or(2);
        let channel_deviation = params.channel_deviation.unwrap_or(2.0);
        let atr_period = params.atr_period.unwrap_or(100);

        if amplitude == 0 {
            return Err(HalfTrendError::InvalidPeriod {
                period: amplitude,
                data_len: 0,
            });
        }
        if atr_period == 0 {
            return Err(HalfTrendError::InvalidPeriod {
                period: atr_period,
                data_len: 0,
            });
        }
        if !(channel_deviation.is_finite()) || channel_deviation <= 0.0 {
            return Err(HalfTrendError::InvalidChannelDeviation { channel_deviation });
        }

        let atr_stream =
            crate::indicators::atr::AtrStream::try_new(crate::indicators::atr::AtrParams {
                length: Some(atr_period),
            })
            .map_err(|e| HalfTrendError::AtrError(e.to_string()))?;

        let cap = amplitude.max(1);

        Ok(Self {
            amplitude,
            atr_period,
            ch_half: channel_deviation * 0.5,
            inv_amp: 1.0 / (amplitude as f64),

            atr_stream,

            max_idx: vec![0; cap],
            max_val: vec![0.0; cap],
            min_idx: vec![0; cap],
            min_val: vec![0.0; cap],
            max_head: 0,
            max_tail: 0,
            max_cnt: 0,
            min_head: 0,
            min_tail: 0,
            min_cnt: 0,

            ring_high: vec![0.0; cap],
            ring_low: vec![0.0; cap],
            ring_pos: 0,
            filled: 0,
            high_sum: 0.0,
            low_sum: 0.0,

            i: 0,
            warmup_need: amplitude.max(atr_period),

            current_trend: 0,
            next_trend: 0,
            last_trend: -1,
            max_low_price: f64::NAN,
            min_high_price: f64::NAN,
            up: 0.0,
            down: 0.0,

            prev_high: f64::NAN,
            prev_low: f64::NAN,
            have_prev: false,
        })
    }

    #[inline(always)]
    fn inc(i: usize, cap: usize) -> usize {
        let j = i + 1;
        if j == cap {
            0
        } else {
            j
        }
    }
    #[inline(always)]
    fn dec(i: usize, cap: usize) -> usize {
        if i == 0 {
            cap - 1
        } else {
            i - 1
        }
    }

    #[inline(always)]
    fn q_push_max(&mut self, idx: usize, v: f64) {
        let cap = self.amplitude;
        while self.max_cnt > 0 {
            let back = Self::dec(self.max_tail, cap);
            if self.max_val[back] <= v {
                self.max_tail = back;
                self.max_cnt -= 1;
            } else {
                break;
            }
        }
        self.max_val[self.max_tail] = v;
        self.max_idx[self.max_tail] = idx;
        self.max_tail = Self::inc(self.max_tail, cap);
        self.max_cnt += 1;
    }

    #[inline(always)]
    fn q_push_min(&mut self, idx: usize, v: f64) {
        let cap = self.amplitude;
        while self.min_cnt > 0 {
            let back = Self::dec(self.min_tail, cap);
            if self.min_val[back] >= v {
                self.min_tail = back;
                self.min_cnt -= 1;
            } else {
                break;
            }
        }
        self.min_val[self.min_tail] = v;
        self.min_idx[self.min_tail] = idx;
        self.min_tail = Self::inc(self.min_tail, cap);
        self.min_cnt += 1;
    }

    #[inline(always)]
    fn q_evict(&mut self, idx: usize) {
        let cap = self.amplitude;
        let limit = idx.saturating_sub(self.amplitude - 1);
        while self.max_cnt > 0 && self.max_idx[self.max_head] < limit {
            self.max_head = Self::inc(self.max_head, cap);
            self.max_cnt -= 1;
        }
        while self.min_cnt > 0 && self.min_idx[self.min_head] < limit {
            self.min_head = Self::inc(self.min_head, cap);
            self.min_cnt -= 1;
        }
    }

    pub fn update(&mut self, high: f64, low: f64, close: f64) -> Option<HalfTrendStreamOutput> {
        if !(high.is_finite() && low.is_finite() && close.is_finite()) {
            return None;
        }

        let idx = self.i;

        self.q_evict(idx);
        self.q_push_max(idx, high);
        self.q_push_min(idx, low);

        if self.filled == self.amplitude {
            let old_h = self.ring_high[self.ring_pos];
            let old_l = self.ring_low[self.ring_pos];
            self.high_sum -= old_h;
            self.low_sum -= old_l;
        } else {
            self.filled += 1;
        }
        self.ring_high[self.ring_pos] = high;
        self.ring_low[self.ring_pos] = low;
        self.high_sum += high;
        self.low_sum += low;
        self.ring_pos = Self::inc(self.ring_pos, self.amplitude);

        let atr_opt = self.atr_stream.update(high, low, close);

        let warmed =
            self.filled == self.amplitude && (idx + 1) >= self.warmup_need && atr_opt.is_some();

        if !warmed {
            self.prev_high = high;
            self.prev_low = low;
            self.have_prev = true;
            self.i = idx + 1;
            return None;
        }

        debug_assert!(self.max_cnt > 0 && self.min_cnt > 0);
        let high_price = self.max_val[self.max_head];
        let low_price = self.min_val[self.min_head];
        let atr = atr_opt.unwrap();
        let atr2 = 0.5 * atr;
        let dev = atr * self.ch_half;

        let prev_low = if self.have_prev { self.prev_low } else { low };
        let prev_high = if self.have_prev { self.prev_high } else { high };
        if self.max_low_price.is_nan() {
            self.max_low_price = prev_low;
        }
        if self.min_high_price.is_nan() {
            self.min_high_price = prev_high;
        }

        let highma = self.high_sum * self.inv_amp;
        let lowma = self.low_sum * self.inv_amp;

        if self.next_trend == 1 {
            if low_price > self.max_low_price {
                self.max_low_price = low_price;
            }
            if highma < self.max_low_price && close < prev_low {
                self.current_trend = 1;
                self.next_trend = 0;
                self.min_high_price = high_price;
            }
        } else {
            if high_price < self.min_high_price {
                self.min_high_price = high_price;
            }
            if lowma > self.min_high_price && close > prev_high {
                self.current_trend = 0;
                self.next_trend = 1;
                self.max_low_price = low_price;
            }
        }

        let prev_trend = self.last_trend;
        let mut buy_sig: Option<f64> = None;
        let mut sell_sig: Option<f64> = None;

        let (ht, atr_hi, atr_lo, tr_val) = if self.current_trend == 0 {
            if prev_trend == 1 {
                self.up = self.down;
                buy_sig = Some(self.up - atr2);
            } else {
                self.up = if self.up == 0.0 {
                    self.max_low_price
                } else if self.max_low_price > self.up {
                    self.max_low_price
                } else {
                    self.up
                };
            }
            let h = self.up;
            (h, h + dev, h - dev, 0.0)
        } else {
            if prev_trend == 0 {
                self.down = self.up;
                sell_sig = Some(self.down + atr2);
            } else {
                self.down = if self.down == 0.0 {
                    self.min_high_price
                } else if self.min_high_price < self.down {
                    self.min_high_price
                } else {
                    self.down
                };
            }
            let d = self.down;
            (d, d + dev, d - dev, 1.0)
        };

        self.last_trend = self.current_trend as i8;
        self.prev_high = high;
        self.prev_low = low;
        self.have_prev = true;
        self.i = idx + 1;

        Some(HalfTrendStreamOutput {
            halftrend: ht,
            trend: tr_val,
            atr_high: atr_hi,
            atr_low: atr_lo,
            buy_signal: buy_sig,
            sell_signal: sell_sig,
        })
    }
}

#[derive(Debug, Clone)]
pub struct HalfTrendStreamOutput {
    pub halftrend: f64,
    pub trend: f64,
    pub atr_high: f64,
    pub atr_low: f64,
    pub buy_signal: Option<f64>,
    pub sell_signal: Option<f64>,
}

#[derive(Clone, Debug)]
pub struct HalfTrendBatchRange {
    pub amplitude: (usize, usize, usize),
    pub channel_deviation: (f64, f64, f64),
    pub atr_period: (usize, usize, usize),
}

impl Default for HalfTrendBatchRange {
    fn default() -> Self {
        Self {
            amplitude: (2, 2, 0),
            channel_deviation: (2.0, 2.0, 0.0),
            atr_period: (100, 349, 1),
        }
    }
}

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

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

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

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

    pub fn amplitude_static(mut self, a: usize) -> Self {
        self.range.amplitude = (a, a, 0);
        self
    }

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

    pub fn channel_deviation_static(mut self, c: f64) -> Self {
        self.range.channel_deviation = (c, c, 0.0);
        self
    }

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

    pub fn atr_period_static(mut self, p: usize) -> Self {
        self.range.atr_period = (p, p, 0);
        self
    }

    pub fn apply_candles(self, c: &Candles) -> Result<HalfTrendBatchOutput, HalfTrendError> {
        halftrend_batch_with_kernel(c, &self.range, self.kernel)
    }

    pub fn apply_slices(
        self,
        h: &[f64],
        l: &[f64],
        c: &[f64],
    ) -> Result<HalfTrendBatchOutput, HalfTrendError> {
        halftrend_batch_with_kernel_slices(h, l, c, &self.range, self.kernel)
    }

    pub fn with_default_candles(c: &Candles) -> Result<HalfTrendBatchOutput, HalfTrendError> {
        HalfTrendBatchBuilder::new()
            .kernel(Kernel::Auto)
            .apply_candles(c)
    }

    pub fn with_default_slices(
        h: &[f64],
        l: &[f64],
        c: &[f64],
    ) -> Result<HalfTrendBatchOutput, HalfTrendError> {
        HalfTrendBatchBuilder::new()
            .kernel(Kernel::Auto)
            .apply_slices(h, l, c)
    }
}

pub struct HalfTrendBatchOutput {
    pub halftrend: Vec<f64>,
    pub trend: Vec<f64>,
    pub atr_high: Vec<f64>,
    pub atr_low: Vec<f64>,
    pub buy_signal: Vec<f64>,
    pub sell_signal: Vec<f64>,
    pub combos: Vec<HalfTrendParams>,
    pub rows: usize,
    pub cols: usize,
}

impl HalfTrendBatchOutput {
    pub fn row_for_params(&self, p: &HalfTrendParams) -> Option<usize> {
        self.combos.iter().position(|c| {
            c.amplitude.unwrap_or(2) == p.amplitude.unwrap_or(2)
                && (c.channel_deviation.unwrap_or(2.0) - p.channel_deviation.unwrap_or(2.0)).abs()
                    < 1e-12
                && c.atr_period.unwrap_or(100) == p.atr_period.unwrap_or(100)
        })
    }

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

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

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

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

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

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

fn halftrend_batch_with_kernel(
    candles: &Candles,
    sweep: &HalfTrendBatchRange,
    k: Kernel,
) -> Result<HalfTrendBatchOutput, HalfTrendError> {
    halftrend_batch_with_kernel_slices(&candles.high, &candles.low, &candles.close, sweep, k)
}

fn halftrend_batch_with_kernel_slices(
    high: &[f64],
    low: &[f64],
    close: &[f64],
    sweep: &HalfTrendBatchRange,
    k: Kernel,
) -> Result<HalfTrendBatchOutput, HalfTrendError> {
    let combos = expand_grid_halftrend(sweep)?;
    let rows = combos.len();
    let cols = close.len();

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

    let batch = match k {
        Kernel::Auto => detect_best_batch_kernel(),
        other if other.is_batch() => other,
        _ => return Err(HalfTrendError::InvalidKernelForBatch(k)),
    };
    let simd = match batch {
        Kernel::Avx512Batch => Kernel::Avx512,
        Kernel::Avx2Batch => Kernel::Avx2,
        Kernel::ScalarBatch => Kernel::Scalar,
        _ => Kernel::Scalar,
    };

    let _cap = rows
        .checked_mul(cols)
        .ok_or_else(|| HalfTrendError::InvalidRange {
            start: "rows".into(),
            end: "cols".into(),
            step: "mul".into(),
        })?;

    let mut mu_ht = make_uninit_matrix(rows, cols);
    let mut mu_tr = make_uninit_matrix(rows, cols);
    let mut mu_ah = make_uninit_matrix(rows, cols);
    let mut mu_al = make_uninit_matrix(rows, cols);
    let mut mu_bs = make_uninit_matrix(rows, cols);
    let mut mu_ss = make_uninit_matrix(rows, cols);

    let first = first_valid_ohlc(high, low, close);
    if first == usize::MAX {
        return Err(HalfTrendError::AllValuesNaN);
    }
    let warms: Vec<usize> = combos
        .iter()
        .map(|p| warmup_from(first, p.amplitude.unwrap(), p.atr_period.unwrap()))
        .collect();

    init_matrix_prefixes(&mut mu_ht, cols, &warms);
    init_matrix_prefixes(&mut mu_tr, cols, &warms);
    init_matrix_prefixes(&mut mu_ah, cols, &warms);
    init_matrix_prefixes(&mut mu_al, cols, &warms);
    init_matrix_prefixes(&mut mu_bs, cols, &warms);
    init_matrix_prefixes(&mut mu_ss, cols, &warms);

    let dst_ht =
        unsafe { core::slice::from_raw_parts_mut(mu_ht.as_mut_ptr() as *mut f64, mu_ht.len()) };
    let dst_tr =
        unsafe { core::slice::from_raw_parts_mut(mu_tr.as_mut_ptr() as *mut f64, mu_tr.len()) };
    let dst_ah =
        unsafe { core::slice::from_raw_parts_mut(mu_ah.as_mut_ptr() as *mut f64, mu_ah.len()) };
    let dst_al =
        unsafe { core::slice::from_raw_parts_mut(mu_al.as_mut_ptr() as *mut f64, mu_al.len()) };
    let dst_bs =
        unsafe { core::slice::from_raw_parts_mut(mu_bs.as_mut_ptr() as *mut f64, mu_bs.len()) };
    let dst_ss =
        unsafe { core::slice::from_raw_parts_mut(mu_ss.as_mut_ptr() as *mut f64, mu_ss.len()) };

    halftrend_batch_rows_into(
        high, low, close, sweep, simd, dst_ht, dst_tr, dst_ah, dst_al, dst_bs, dst_ss,
    )?;

    let take = |v: Vec<MaybeUninit<f64>>| unsafe {
        let ptr = v.as_ptr() as *mut f64;
        let len = v.len();
        let cap = v.capacity();
        core::mem::forget(v);
        Vec::from_raw_parts(ptr, len, cap)
    };

    Ok(HalfTrendBatchOutput {
        halftrend: take(mu_ht),
        trend: take(mu_tr),
        atr_high: take(mu_ah),
        atr_low: take(mu_al),
        buy_signal: take(mu_bs),
        sell_signal: take(mu_ss),
        combos,
        rows,
        cols,
    })
}

fn expand_grid_halftrend(r: &HalfTrendBatchRange) -> Result<Vec<HalfTrendParams>, HalfTrendError> {
    fn axis_usize((start, end, step): (usize, usize, usize)) -> Result<Vec<usize>, HalfTrendError> {
        if step == 0 || start == end {
            return Ok(vec![start]);
        }
        if start < end {
            return Ok((start..=end).step_by(step.max(1)).collect());
        }

        let mut v = Vec::new();
        let mut x = start as isize;
        let end_i = end as isize;
        let st = (step as isize).max(1);
        while x >= end_i {
            v.push(x as usize);
            x -= st;
        }
        if v.is_empty() {
            return Err(HalfTrendError::InvalidRange {
                start: start.to_string(),
                end: end.to_string(),
                step: step.to_string(),
            });
        }
        Ok(v)
    }
    fn axis_f64((start, end, step): (f64, f64, f64)) -> Result<Vec<f64>, HalfTrendError> {
        if step.abs() < 1e-12 || (start - end).abs() < 1e-12 {
            return Ok(vec![start]);
        }
        if start < end {
            let mut v = Vec::new();
            let mut x = start;
            let st = step.abs();
            while x <= end + 1e-12 {
                v.push(x);
                x += st;
            }
            if v.is_empty() {
                return Err(HalfTrendError::InvalidRange {
                    start: start.to_string(),
                    end: end.to_string(),
                    step: step.to_string(),
                });
            }
            return Ok(v);
        }
        let mut v = Vec::new();
        let mut x = start;
        let st = step.abs();
        while x + 1e-12 >= end {
            v.push(x);
            x -= st;
        }
        if v.is_empty() {
            return Err(HalfTrendError::InvalidRange {
                start: start.to_string(),
                end: end.to_string(),
                step: step.to_string(),
            });
        }
        Ok(v)
    }

    let amplitudes = axis_usize(r.amplitude)?;
    let channel_deviations = axis_f64(r.channel_deviation)?;
    let atr_periods = axis_usize(r.atr_period)?;

    let cap = amplitudes
        .len()
        .checked_mul(channel_deviations.len())
        .and_then(|x| x.checked_mul(atr_periods.len()))
        .ok_or_else(|| HalfTrendError::InvalidRange {
            start: "cap".into(),
            end: "overflow".into(),
            step: "mul".into(),
        })?;

    let mut out = Vec::with_capacity(cap);
    for &a in &amplitudes {
        for &c in &channel_deviations {
            for &p in &atr_periods {
                out.push(HalfTrendParams {
                    amplitude: Some(a),
                    channel_deviation: Some(c),
                    atr_period: Some(p),
                });
            }
        }
    }
    Ok(out)
}

#[inline(always)]
fn warmup_from(first: usize, amplitude: usize, atr_period: usize) -> usize {
    first + amplitude.max(atr_period) - 1
}

#[inline(always)]
fn halftrend_row_into(
    high: &[f64],
    low: &[f64],
    close: &[f64],
    amplitude: usize,
    ch_dev: f64,
    atr: &[f64],
    highma: &[f64],
    lowma: &[f64],
    warm: usize,
    out_halftrend: &mut [f64],
    out_trend: &mut [f64],
    out_atr_high: &mut [f64],
    out_atr_low: &mut [f64],
    out_buy: &mut [f64],
    out_sell: &mut [f64],
) {
    halftrend_scalar(
        high,
        low,
        close,
        amplitude,
        ch_dev,
        atr,
        highma,
        lowma,
        warm,
        out_halftrend,
        out_trend,
        out_atr_high,
        out_atr_low,
        out_buy,
        out_sell,
    );
}

#[inline(always)]
fn rolling_max_series(src: &[f64], win: usize) -> Vec<f64> {
    let n = src.len();
    if n == 0 {
        return Vec::new();
    }
    let cap = win.max(1);
    let mut idx = vec![0usize; cap];
    let mut val = vec![0.0f64; cap];
    let (mut head, mut tail, mut cnt) = (0usize, 0usize, 0usize);
    #[inline(always)]
    fn inc(i: usize, cap: usize) -> usize {
        let j = i + 1;
        if j == cap {
            0
        } else {
            j
        }
    }
    #[inline(always)]
    fn dec(i: usize, cap: usize) -> usize {
        if i == 0 {
            cap - 1
        } else {
            i - 1
        }
    }
    let mut out = vec![f64::NAN; n];
    for i in 0..n {
        let wstart = i.saturating_add(1).saturating_sub(cap);
        while cnt > 0 && idx[head] < wstart {
            head = inc(head, cap);
            cnt -= 1;
        }
        let x = src[i];
        while cnt > 0 {
            let back = dec(tail, cap);
            if val[back] <= x {
                tail = back;
                cnt -= 1;
            } else {
                break;
            }
        }
        val[tail] = x;
        idx[tail] = i;
        tail = inc(tail, cap);
        cnt += 1;
        out[i] = val[head];
    }
    out
}

#[inline(always)]
fn rolling_min_series(src: &[f64], win: usize) -> Vec<f64> {
    let n = src.len();
    if n == 0 {
        return Vec::new();
    }
    let cap = win.max(1);
    let mut idx = vec![0usize; cap];
    let mut val = vec![0.0f64; cap];
    let (mut head, mut tail, mut cnt) = (0usize, 0usize, 0usize);
    #[inline(always)]
    fn inc(i: usize, cap: usize) -> usize {
        let j = i + 1;
        if j == cap {
            0
        } else {
            j
        }
    }
    #[inline(always)]
    fn dec(i: usize, cap: usize) -> usize {
        if i == 0 {
            cap - 1
        } else {
            i - 1
        }
    }
    let mut out = vec![f64::NAN; n];
    for i in 0..n {
        let wstart = i.saturating_add(1).saturating_sub(cap);
        while cnt > 0 && idx[head] < wstart {
            head = inc(head, cap);
            cnt -= 1;
        }
        let x = src[i];
        while cnt > 0 {
            let back = dec(tail, cap);
            if val[back] >= x {
                tail = back;
                cnt -= 1;
            } else {
                break;
            }
        }
        val[tail] = x;
        idx[tail] = i;
        tail = inc(tail, cap);
        cnt += 1;
        out[i] = val[head];
    }
    out
}

#[inline(always)]
fn halftrend_row_into_precomputed(
    high: &[f64],
    low: &[f64],
    close: &[f64],
    ch_dev: f64,
    atr: &[f64],
    highma: &[f64],
    lowma: &[f64],
    warm: usize,
    roll_high: &[f64],
    roll_low: &[f64],
    out_halftrend: &mut [f64],
    out_trend: &mut [f64],
    out_atr_high: &mut [f64],
    out_atr_low: &mut [f64],
    out_buy: &mut [f64],
    out_sell: &mut [f64],
) {
    let len = close.len();
    let qnan = f64::from_bits(0x7ff8_0000_0000_0000);
    let ch_half = ch_dev * 0.5;

    let mut current_trend = 0i32;
    let mut next_trend = 0i32;
    let mut up = 0.0f64;
    let mut down = 0.0f64;
    let mut max_low_price = if warm > 0 { low[warm - 1] } else { low[0] };
    let mut min_high_price = if warm > 0 { high[warm - 1] } else { high[0] };

    for i in warm..len {
        out_buy[i] = qnan;
        out_sell[i] = qnan;

        let high_price = roll_high[i];
        let low_price = roll_low[i];
        let prev_low = if i > 0 { low[i - 1] } else { low[0] };
        let prev_high = if i > 0 { high[i - 1] } else { high[0] };

        if next_trend == 1 {
            if low_price > max_low_price {
                max_low_price = low_price;
            }
            if highma[i] < max_low_price && close[i] < prev_low {
                current_trend = 1;
                next_trend = 0;
                min_high_price = high_price;
            }
        } else {
            if high_price < min_high_price {
                min_high_price = high_price;
            }
            if lowma[i] > min_high_price && close[i] > prev_high {
                current_trend = 0;
                next_trend = 1;
                max_low_price = low_price;
            }
        }

        let a = atr[i];
        let atr2 = 0.5 * a;
        let dev = a.mul_add(ch_half, 0.0);

        if current_trend == 0 {
            if i > warm && out_trend[i - 1] != 0.0 {
                up = down;
                out_buy[i] = up - atr2;
            } else {
                up = if i == warm || up == 0.0 {
                    max_low_price
                } else if max_low_price > up {
                    max_low_price
                } else {
                    up
                };
            }
            out_halftrend[i] = up;
            out_atr_high[i] = up + dev;
            out_atr_low[i] = up - dev;
            out_trend[i] = 0.0;
        } else {
            if i > warm && out_trend[i - 1] != 1.0 {
                down = up;
                out_sell[i] = down + atr2;
            } else {
                down = if i == warm || down == 0.0 {
                    min_high_price
                } else if min_high_price < down {
                    min_high_price
                } else {
                    down
                };
            }
            out_halftrend[i] = down;
            out_atr_high[i] = down + dev;
            out_atr_low[i] = down - dev;
            out_trend[i] = 1.0;
        }
    }
}

#[inline(always)]
pub fn halftrend_batch_rows_into(
    high: &[f64],
    low: &[f64],
    close: &[f64],
    sweep: &HalfTrendBatchRange,
    kern: Kernel,
    dst_halftrend: &mut [f64],
    dst_trend: &mut [f64],
    dst_atr_high: &mut [f64],
    dst_atr_low: &mut [f64],
    dst_buy: &mut [f64],
    dst_sell: &mut [f64],
) -> Result<Vec<HalfTrendParams>, HalfTrendError> {
    let combos = expand_grid_halftrend(sweep)?;

    let len = high.len();
    let first = first_valid_ohlc(high, low, close);
    if first == usize::MAX {
        return Err(HalfTrendError::AllValuesNaN);
    }

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

    use std::collections::BTreeSet;
    let uniq_amp: BTreeSet<usize> = combos.iter().map(|p| p.amplitude.unwrap()).collect();
    let uniq_atr: BTreeSet<usize> = combos.iter().map(|p| p.atr_period.unwrap()).collect();

    use std::collections::HashMap;
    let mut hi_map: HashMap<usize, Vec<f64>> = HashMap::new();
    let mut lo_map: HashMap<usize, Vec<f64>> = HashMap::new();
    for &a in &uniq_amp {
        hi_map.insert(
            a,
            sma(&SmaInput::from_slice(high, SmaParams { period: Some(a) }))
                .map_err(|e| HalfTrendError::SmaError(e.to_string()))?
                .values,
        );
        lo_map.insert(
            a,
            sma(&SmaInput::from_slice(low, SmaParams { period: Some(a) }))
                .map_err(|e| HalfTrendError::SmaError(e.to_string()))?
                .values,
        );
    }

    let mut roll_high_map: HashMap<usize, Vec<f64>> = HashMap::new();
    let mut roll_low_map: HashMap<usize, Vec<f64>> = HashMap::new();
    for &a in &uniq_amp {
        roll_high_map.insert(a, rolling_max_series(high, a));
        roll_low_map.insert(a, rolling_min_series(low, a));
    }
    let mut atr_map: HashMap<usize, Vec<f64>> = HashMap::new();
    for &p in &uniq_atr {
        atr_map.insert(
            p,
            atr(&AtrInput::from_slices(
                high,
                low,
                close,
                AtrParams { length: Some(p) },
            ))
            .map_err(|e| HalfTrendError::AtrError(e.to_string()))?
            .values,
        );
    }

    for row in 0..rows {
        let prm = &combos[row];
        let amp = prm.amplitude.unwrap();
        let ap = prm.atr_period.unwrap();
        let ch = prm.channel_deviation.unwrap_or(2.0);
        let warm = warmup_from(first, amp, ap);

        let base = row * cols;
        let (ht, tr, ah, al, bs, ss) = (
            &mut dst_halftrend[base..base + cols],
            &mut dst_trend[base..base + cols],
            &mut dst_atr_high[base..base + cols],
            &mut dst_atr_low[base..base + cols],
            &mut dst_buy[base..base + cols],
            &mut dst_sell[base..base + cols],
        );

        let hma = hi_map.get(&amp).unwrap().as_slice();
        let lma = lo_map.get(&amp).unwrap().as_slice();
        let av = atr_map.get(&ap).unwrap().as_slice();
        let rhi = roll_high_map.get(&amp).unwrap().as_slice();
        let rlo = roll_low_map.get(&amp).unwrap().as_slice();

        halftrend_row_into_precomputed(
            high, low, close, ch, av, hma, lma, warm, rhi, rlo, ht, tr, ah, al, bs, ss,
        );
    }

    Ok(combos)
}

#[cfg(feature = "python")]
#[pyfunction]
#[pyo3(name = "halftrend", signature = (
    high,
    low,
    close,
    amplitude,
    channel_deviation,
    atr_period,
    kernel = None
))]
pub fn halftrend_py<'py>(
    py: Python<'py>,
    high: PyReadonlyArray1<'py, f64>,
    low: PyReadonlyArray1<'py, f64>,
    close: PyReadonlyArray1<'py, f64>,
    amplitude: usize,
    channel_deviation: f64,
    atr_period: usize,
    kernel: Option<&str>,
) -> PyResult<Bound<'py, PyDict>> {
    let h = high.as_slice()?;
    let l = low.as_slice()?;
    let c = close.as_slice()?;
    let kern = validate_kernel(kernel, false)?;
    let params = HalfTrendParams {
        amplitude: Some(amplitude),
        channel_deviation: Some(channel_deviation),
        atr_period: Some(atr_period),
    };
    let input = HalfTrendInput::from_slices(h, l, c, params);

    let out = py
        .allow_threads(|| halftrend_with_kernel(&input, kern))
        .map_err(|e| PyValueError::new_err(e.to_string()))?;

    let dict = PyDict::new(py);
    dict.set_item("halftrend", out.halftrend.into_pyarray(py))?;
    dict.set_item("trend", out.trend.into_pyarray(py))?;
    dict.set_item("atr_high", out.atr_high.into_pyarray(py))?;
    dict.set_item("atr_low", out.atr_low.into_pyarray(py))?;
    dict.set_item("buy_signal", out.buy_signal.into_pyarray(py))?;
    dict.set_item("sell_signal", out.sell_signal.into_pyarray(py))?;

    Ok(dict)
}

#[cfg(feature = "python")]
#[pyfunction]
#[pyo3(name = "halftrend_tuple", signature = (
    high,
    low,
    close,
    amplitude,
    channel_deviation,
    atr_period,
    kernel = None
))]
pub fn halftrend_tuple_py<'py>(
    py: Python<'py>,
    high: PyReadonlyArray1<'py, f64>,
    low: PyReadonlyArray1<'py, f64>,
    close: PyReadonlyArray1<'py, f64>,
    amplitude: usize,
    channel_deviation: f64,
    atr_period: usize,
    kernel: Option<&str>,
) -> PyResult<(
    Bound<'py, PyArray1<f64>>,
    Bound<'py, PyArray1<f64>>,
    Bound<'py, PyArray1<f64>>,
    Bound<'py, PyArray1<f64>>,
    Bound<'py, PyArray1<f64>>,
    Bound<'py, PyArray1<f64>>,
)> {
    let h = high.as_slice()?;
    let l = low.as_slice()?;
    let c = close.as_slice()?;
    let kern = validate_kernel(kernel, false)?;
    let params = HalfTrendParams {
        amplitude: Some(amplitude),
        channel_deviation: Some(channel_deviation),
        atr_period: Some(atr_period),
    };
    let input = HalfTrendInput::from_slices(h, l, c, params);

    let out = py
        .allow_threads(|| halftrend_with_kernel(&input, kern))
        .map_err(|e| PyValueError::new_err(e.to_string()))?;
    Ok((
        out.halftrend.into_pyarray(py),
        out.trend.into_pyarray(py),
        out.atr_high.into_pyarray(py),
        out.atr_low.into_pyarray(py),
        out.buy_signal.into_pyarray(py),
        out.sell_signal.into_pyarray(py),
    ))
}

#[cfg(feature = "python")]
#[pyfunction]
#[pyo3(name = "halftrend_batch", signature = (
    high,
    low,
    close,
    amplitude_start = None,
    amplitude_end = None,
    amplitude_step = None,
    channel_deviation_start = None,
    channel_deviation_end = None,
    channel_deviation_step = None,
    atr_period_start = None,
    atr_period_end = None,
    atr_period_step = None,
    kernel = None
))]
pub fn halftrend_batch_py<'py>(
    py: Python<'py>,
    high: PyReadonlyArray1<'py, f64>,
    low: PyReadonlyArray1<'py, f64>,
    close: PyReadonlyArray1<'py, f64>,
    amplitude_start: Option<usize>,
    amplitude_end: Option<usize>,
    amplitude_step: Option<usize>,
    channel_deviation_start: Option<f64>,
    channel_deviation_end: Option<f64>,
    channel_deviation_step: Option<f64>,
    atr_period_start: Option<usize>,
    atr_period_end: Option<usize>,
    atr_period_step: Option<usize>,
    kernel: Option<&str>,
) -> PyResult<Bound<'py, PyDict>> {
    use numpy::{IntoPyArray, PyArray1, PyArrayMethods};

    let h = high.as_slice()?;
    let l = low.as_slice()?;
    let c = close.as_slice()?;
    let kern = validate_kernel(kernel, true)?;

    let mut range = HalfTrendBatchRange::default();
    if let (Some(s), Some(e), Some(st)) = (amplitude_start, amplitude_end, amplitude_step) {
        range.amplitude = (s, e, st);
    } else if let Some(v) = amplitude_start {
        range.amplitude = (v, v, 0);
    }
    if let (Some(s), Some(e), Some(st)) = (
        channel_deviation_start,
        channel_deviation_end,
        channel_deviation_step,
    ) {
        range.channel_deviation = (s, e, st);
    } else if let Some(v) = channel_deviation_start {
        range.channel_deviation = (v, v, 0.0);
    }
    if let (Some(s), Some(e), Some(st)) = (atr_period_start, atr_period_end, atr_period_step) {
        range.atr_period = (s, e, st);
    } else if let Some(v) = atr_period_start {
        range.atr_period = (v, v, 0);
    }

    let combos = expand_grid_halftrend(&range).map_err(|e| PyValueError::new_err(e.to_string()))?;
    if combos.is_empty() {
        return Err(PyValueError::new_err("empty sweep"));
    }
    let rows = combos.len();
    let cols = h.len();

    let total = rows
        .checked_mul(cols)
        .ok_or_else(|| PyValueError::new_err("halftrend_batch: rows*cols overflow"))?;
    let arr_ht = unsafe { PyArray1::<f64>::new(py, [total], false) };
    let arr_tr = unsafe { PyArray1::<f64>::new(py, [total], false) };
    let arr_ah = unsafe { PyArray1::<f64>::new(py, [total], false) };
    let arr_al = unsafe { PyArray1::<f64>::new(py, [total], false) };
    let arr_bs = unsafe { PyArray1::<f64>::new(py, [total], false) };
    let arr_ss = unsafe { PyArray1::<f64>::new(py, [total], false) };

    let dst_ht = unsafe { arr_ht.as_slice_mut()? };
    let dst_tr = unsafe { arr_tr.as_slice_mut()? };
    let dst_ah = unsafe { arr_ah.as_slice_mut()? };
    let dst_al = unsafe { arr_al.as_slice_mut()? };
    let dst_bs = unsafe { arr_bs.as_slice_mut()? };
    let dst_ss = unsafe { arr_ss.as_slice_mut()? };

    let first = first_valid_ohlc(h, l, c);
    let warms: Vec<usize> = combos
        .iter()
        .map(|p| warmup_from(first, p.amplitude.unwrap(), p.atr_period.unwrap()))
        .collect();
    let qnan = f64::from_bits(0x7ff8_0000_0000_0000);
    for (row, &w) in warms.iter().enumerate() {
        let base = row * cols;
        for x in &mut dst_ht[base..base + w] {
            *x = qnan;
        }
        for x in &mut dst_tr[base..base + w] {
            *x = qnan;
        }
        for x in &mut dst_ah[base..base + w] {
            *x = qnan;
        }
        for x in &mut dst_al[base..base + w] {
            *x = qnan;
        }
        for x in &mut dst_bs[base..base + w] {
            *x = qnan;
        }
        for x in &mut dst_ss[base..base + w] {
            *x = qnan;
        }
    }

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

    py.allow_threads(|| {
        halftrend_batch_rows_into(
            h, l, c, &range, simd, dst_ht, dst_tr, dst_ah, dst_al, dst_bs, dst_ss,
        )
    })
    .map_err(|e| PyValueError::new_err(e.to_string()))?;

    let total_rows = rows
        .checked_mul(6)
        .ok_or_else(|| PyValueError::new_err("halftrend_batch: rows*6 overflow"))?;
    let total_stacked = total_rows
        .checked_mul(cols)
        .ok_or_else(|| PyValueError::new_err("halftrend_batch: stacked size overflow"))?;
    let stacked = unsafe { PyArray1::<f64>::new(py, [total_stacked], false) };
    let dst_stacked = unsafe { stacked.as_slice_mut()? };

    let block = rows
        .checked_mul(cols)
        .ok_or_else(|| PyValueError::new_err("halftrend_batch: block size overflow"))?;
    dst_stacked[0..block].copy_from_slice(dst_ht);
    dst_stacked[block..2 * block].copy_from_slice(dst_tr);
    dst_stacked[2 * block..3 * block].copy_from_slice(dst_ah);
    dst_stacked[3 * block..4 * block].copy_from_slice(dst_al);
    dst_stacked[4 * block..5 * block].copy_from_slice(dst_bs);
    dst_stacked[5 * block..6 * block].copy_from_slice(dst_ss);

    let dict = PyDict::new(py);
    dict.set_item("values", stacked.reshape((total_rows, cols))?)?;

    use pyo3::types::PyList;
    let series = PyList::new(
        py,
        vec!["halftrend", "trend", "atr_high", "atr_low", "buy", "sell"],
    )?;
    dict.set_item("series", series)?;
    dict.set_item("rows", rows)?;
    dict.set_item("cols", cols)?;
    dict.set_item(
        "amplitudes",
        combos
            .iter()
            .map(|p| p.amplitude.unwrap() as u64)
            .collect::<Vec<_>>()
            .into_pyarray(py),
    )?;
    dict.set_item(
        "channel_deviations",
        combos
            .iter()
            .map(|p| p.channel_deviation.unwrap())
            .collect::<Vec<_>>()
            .into_pyarray(py),
    )?;
    dict.set_item(
        "atr_periods",
        combos
            .iter()
            .map(|p| p.atr_period.unwrap() as u64)
            .collect::<Vec<_>>()
            .into_pyarray(py),
    )?;
    Ok(dict.into())
}

#[cfg(all(feature = "python", feature = "cuda"))]
#[pyfunction(name = "halftrend_cuda_batch_dev")]
#[pyo3(signature = (high_f32, low_f32, close_f32, amplitude_range, channel_deviation_range=(2.0,2.0,0.0), atr_period_range=(14,14,0), device_id=0))]
pub fn halftrend_cuda_batch_dev_py<'py>(
    py: Python<'py>,
    high_f32: numpy::PyReadonlyArray1<'py, f32>,
    low_f32: numpy::PyReadonlyArray1<'py, f32>,
    close_f32: numpy::PyReadonlyArray1<'py, f32>,
    amplitude_range: (usize, usize, usize),
    channel_deviation_range: (f64, f64, f64),
    atr_period_range: (usize, usize, usize),
    device_id: usize,
) -> PyResult<Bound<'py, PyDict>> {
    if !cuda_available() {
        return Err(PyValueError::new_err("CUDA not available"));
    }
    let h = high_f32.as_slice()?;
    let l = low_f32.as_slice()?;
    let c = close_f32.as_slice()?;
    let sweep = HalfTrendBatchRange {
        amplitude: amplitude_range,
        channel_deviation: channel_deviation_range,
        atr_period: atr_period_range,
    };
    let (batch, ctx_arc, dev_id) = py.allow_threads(|| {
        let cuda =
            CudaHalftrend::new(device_id).map_err(|e| PyValueError::new_err(e.to_string()))?;
        let out = cuda
            .halftrend_batch_dev(h, l, c, &sweep)
            .map_err(|e| PyValueError::new_err(e.to_string()))?;
        Ok::<_, PyErr>((out, cuda.context_arc(), cuda.device_id()))
    })?;
    let mut halftrend_dev = make_device_array_py(dev_id as usize, batch.halftrend)?;
    halftrend_dev._ctx = Some(ctx_arc.clone());
    let mut trend_dev = make_device_array_py(dev_id as usize, batch.trend)?;
    trend_dev._ctx = Some(ctx_arc.clone());
    let mut atr_high_dev = make_device_array_py(dev_id as usize, batch.atr_high)?;
    atr_high_dev._ctx = Some(ctx_arc.clone());
    let mut atr_low_dev = make_device_array_py(dev_id as usize, batch.atr_low)?;
    atr_low_dev._ctx = Some(ctx_arc.clone());
    let mut buy_dev = make_device_array_py(dev_id as usize, batch.buy)?;
    buy_dev._ctx = Some(ctx_arc.clone());
    let mut sell_dev = make_device_array_py(dev_id as usize, batch.sell)?;
    sell_dev._ctx = Some(ctx_arc.clone());
    let dict = PyDict::new(py);
    dict.set_item("halftrend", Py::new(py, halftrend_dev)?)?;
    dict.set_item("trend", Py::new(py, trend_dev)?)?;
    dict.set_item("atr_high", Py::new(py, atr_high_dev)?)?;
    dict.set_item("atr_low", Py::new(py, atr_low_dev)?)?;
    dict.set_item("buy_signal", Py::new(py, buy_dev)?)?;
    dict.set_item("sell_signal", Py::new(py, sell_dev)?)?;
    use numpy::IntoPyArray;
    dict.set_item(
        "amplitudes",
        batch
            .combos
            .iter()
            .map(|p| p.amplitude.unwrap() as u64)
            .collect::<Vec<_>>()
            .into_pyarray(py),
    )?;
    dict.set_item(
        "channel_deviations",
        batch
            .combos
            .iter()
            .map(|p| p.channel_deviation.unwrap())
            .collect::<Vec<_>>()
            .into_pyarray(py),
    )?;
    dict.set_item(
        "atr_periods",
        batch
            .combos
            .iter()
            .map(|p| p.atr_period.unwrap() as u64)
            .collect::<Vec<_>>()
            .into_pyarray(py),
    )?;
    Ok(dict)
}

#[cfg(all(feature = "python", feature = "cuda"))]
#[pyfunction(name = "halftrend_cuda_many_series_one_param_dev")]
#[pyo3(signature = (high_tm_f32, low_tm_f32, close_tm_f32, cols, rows, amplitude, channel_deviation, atr_period, device_id=0))]
pub fn halftrend_cuda_many_series_one_param_dev_py<'py>(
    py: Python<'py>,
    high_tm_f32: numpy::PyReadonlyArray1<'_, f32>,
    low_tm_f32: numpy::PyReadonlyArray1<'_, f32>,
    close_tm_f32: numpy::PyReadonlyArray1<'_, f32>,
    cols: usize,
    rows: usize,
    amplitude: usize,
    channel_deviation: f64,
    atr_period: usize,
    device_id: usize,
) -> PyResult<Bound<'py, PyDict>> {
    if !cuda_available() {
        return Err(PyValueError::new_err("CUDA not available"));
    }
    let h = high_tm_f32.as_slice()?;
    let l = low_tm_f32.as_slice()?;
    let c = close_tm_f32.as_slice()?;
    let (out, ctx_arc, dev_id) = py.allow_threads(|| {
        let cuda =
            CudaHalftrend::new(device_id).map_err(|e| PyValueError::new_err(e.to_string()))?;
        let out = cuda
            .halftrend_many_series_one_param_time_major_dev(
                h,
                l,
                c,
                cols,
                rows,
                amplitude,
                channel_deviation,
                atr_period,
            )
            .map_err(|e| PyValueError::new_err(e.to_string()))?;
        Ok::<_, PyErr>((out, cuda.context_arc(), cuda.device_id()))
    })?;
    let mut halftrend_dev = make_device_array_py(dev_id as usize, out.halftrend)?;
    halftrend_dev._ctx = Some(ctx_arc.clone());
    let mut trend_dev = make_device_array_py(dev_id as usize, out.trend)?;
    trend_dev._ctx = Some(ctx_arc.clone());
    let mut atr_high_dev = make_device_array_py(dev_id as usize, out.atr_high)?;
    atr_high_dev._ctx = Some(ctx_arc.clone());
    let mut atr_low_dev = make_device_array_py(dev_id as usize, out.atr_low)?;
    atr_low_dev._ctx = Some(ctx_arc.clone());
    let mut buy_dev = make_device_array_py(dev_id as usize, out.buy)?;
    buy_dev._ctx = Some(ctx_arc.clone());
    let mut sell_dev = make_device_array_py(dev_id as usize, out.sell)?;
    sell_dev._ctx = Some(ctx_arc.clone());
    let dict = PyDict::new(py);
    dict.set_item("halftrend", Py::new(py, halftrend_dev)?)?;
    dict.set_item("trend", Py::new(py, trend_dev)?)?;
    dict.set_item("atr_high", Py::new(py, atr_high_dev)?)?;
    dict.set_item("atr_low", Py::new(py, atr_low_dev)?)?;
    dict.set_item("buy_signal", Py::new(py, buy_dev)?)?;
    dict.set_item("sell_signal", Py::new(py, sell_dev)?)?;
    Ok(dict)
}

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

#[cfg(feature = "python")]
#[pymethods]
impl HalfTrendStreamPy {
    #[new]
    fn new(amplitude: usize, channel_deviation: f64, atr_period: usize) -> PyResult<Self> {
        let params = HalfTrendParams {
            amplitude: Some(amplitude),
            channel_deviation: Some(channel_deviation),
            atr_period: Some(atr_period),
        };

        let stream =
            HalfTrendStream::try_new(params).map_err(|e| PyValueError::new_err(e.to_string()))?;

        Ok(Self { stream })
    }

    pub fn update(
        &mut self,
        high: f64,
        low: f64,
        close: f64,
    ) -> Option<(f64, f64, f64, f64, Option<f64>, Option<f64>)> {
        self.stream.update(high, low, close).map(|output| {
            (
                output.halftrend,
                output.trend,
                output.atr_high,
                output.atr_low,
                output.buy_signal,
                output.sell_signal,
            )
        })
    }
}

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

#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
#[wasm_bindgen(js_name = "halftrend")]
pub fn halftrend_js(
    high: &[f64],
    low: &[f64],
    close: &[f64],
    amplitude: usize,
    channel_deviation: f64,
    atr_period: usize,
) -> Result<JsValue, JsValue> {
    if high.is_empty() || low.is_empty() || close.is_empty() {
        return Err(JsValue::from_str("halftrend: Input data slice is empty."));
    }

    let len = high.len();
    if len != low.len() || len != close.len() {
        return Err(JsValue::from_str(&format!(
            "halftrend: Mismatched array lengths: high={}, low={}, close={}",
            high.len(),
            low.len(),
            close.len()
        )));
    }

    if channel_deviation <= 0.0 {
        return Err(JsValue::from_str(&format!(
            "halftrend: Invalid channel_deviation: {}",
            channel_deviation
        )));
    }

    if amplitude == 0 {
        return Err(JsValue::from_str(&format!(
            "halftrend: Invalid period: period = {}, data length = {}",
            amplitude, len
        )));
    }

    if atr_period == 0 {
        return Err(JsValue::from_str(&format!(
            "halftrend: Invalid period: period = {}, data length = {}",
            atr_period, len
        )));
    }

    if atr_period > len {
        return Err(JsValue::from_str(&format!(
            "halftrend: Invalid period: period = {}, data length = {}",
            atr_period, len
        )));
    }

    let mut valid_count = 0;
    for i in 0..len {
        if !high[i].is_nan() && !low[i].is_nan() && !close[i].is_nan() {
            valid_count += 1;
        }
    }

    if valid_count == 0 {
        return Err(JsValue::from_str("halftrend: All values are NaN."));
    }

    let required = amplitude.max(atr_period);
    if valid_count < required {
        return Err(JsValue::from_str(&format!(
            "halftrend: Not enough valid data: needed = {}, valid = {}",
            required, valid_count
        )));
    }

    let params = HalfTrendParams {
        amplitude: Some(amplitude),
        channel_deviation: Some(channel_deviation),
        atr_period: Some(atr_period),
    };
    let input = HalfTrendInput::from_slices(high, low, close, params);

    let cols = high.len();
    let rows: usize = 6;
    let total = rows
        .checked_mul(cols)
        .ok_or_else(|| JsValue::from_str("halftrend: rows*cols overflow"))?;
    let mut values = vec![0.0; total];

    let (ht, rest) = values.split_at_mut(cols);
    let (tr, rest) = rest.split_at_mut(cols);
    let (ah, rest) = rest.split_at_mut(cols);
    let (al, rest) = rest.split_at_mut(cols);
    let (bs, ss) = rest.split_at_mut(cols);

    halftrend_into_slices_kernel(ht, tr, ah, al, bs, ss, &input, Kernel::Auto)
        .map_err(|e| JsValue::from_str(&e.to_string()))?;

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

#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
#[wasm_bindgen]
pub fn halftrend_wasm(
    high: &[f64],
    low: &[f64],
    close: &[f64],
    amplitude: Option<usize>,
    channel_deviation: Option<f64>,
    atr_period: Option<usize>,
) -> Result<JsValue, JsValue> {
    let len = high.len();
    let candles = Candles {
        timestamp: vec![0; len],
        high: high.to_vec(),
        low: low.to_vec(),
        close: close.to_vec(),
        open: vec![f64::NAN; len],
        volume: vec![f64::NAN; len],
        fields: CandleFieldFlags {
            open: false,
            high: true,
            low: true,
            close: true,
            volume: false,
        },
        hl2: high
            .iter()
            .zip(low.iter())
            .map(|(h, l)| (h + l) / 2.0)
            .collect(),
        hlc3: high
            .iter()
            .zip(low.iter())
            .zip(close.iter())
            .map(|((h, l), c)| (h + l + c) / 3.0)
            .collect(),
        ohlc4: vec![f64::NAN; len],
        hlcc4: high
            .iter()
            .zip(low.iter())
            .zip(close.iter())
            .map(|((h, l), c)| (h + l + c + c) / 4.0)
            .collect(),
    };

    let params = HalfTrendParams {
        amplitude,
        channel_deviation,
        atr_period,
    };

    let input = HalfTrendInput::from_candles(&candles, params);
    let output = halftrend(&input).map_err(|e| JsValue::from_str(&e.to_string()))?;

    let result = js_sys::Object::new();

    let halftrend_array = js_sys::Float64Array::from(&output.halftrend[..]);
    let trend_array = js_sys::Float64Array::from(&output.trend[..]);
    let atr_high_array = js_sys::Float64Array::from(&output.atr_high[..]);
    let atr_low_array = js_sys::Float64Array::from(&output.atr_low[..]);
    let buy_signal_array = js_sys::Float64Array::from(&output.buy_signal[..]);
    let sell_signal_array = js_sys::Float64Array::from(&output.sell_signal[..]);

    js_sys::Reflect::set(&result, &"halftrend".into(), &halftrend_array)?;
    js_sys::Reflect::set(&result, &"trend".into(), &trend_array)?;
    js_sys::Reflect::set(&result, &"atr_high".into(), &atr_high_array)?;
    js_sys::Reflect::set(&result, &"atr_low".into(), &atr_low_array)?;
    js_sys::Reflect::set(&result, &"buy_signal".into(), &buy_signal_array)?;
    js_sys::Reflect::set(&result, &"sell_signal".into(), &sell_signal_array)?;

    Ok(result.into())
}

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

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

#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
#[wasm_bindgen(js_name = "halftrend_into")]
pub fn halftrend_into(
    high_ptr: *const f64,
    low_ptr: *const f64,
    close_ptr: *const f64,
    out_ptr: *mut f64,
    len: usize,
    amplitude: usize,
    channel_deviation: f64,
    atr_period: usize,
) -> Result<(), JsValue> {
    if high_ptr.is_null() || low_ptr.is_null() || close_ptr.is_null() || out_ptr.is_null() {
        return Err(JsValue::from_str("null pointer"));
    }

    if len == 0 {
        return Err(JsValue::from_str("halftrend: Input data slice is empty."));
    }

    if channel_deviation <= 0.0 {
        return Err(JsValue::from_str(&format!(
            "halftrend: Invalid channel_deviation: {}",
            channel_deviation
        )));
    }

    if amplitude == 0 {
        return Err(JsValue::from_str(&format!(
            "halftrend: Invalid period: period = {}, data length = {}",
            amplitude, len
        )));
    }

    if atr_period == 0 {
        return Err(JsValue::from_str(&format!(
            "halftrend: Invalid period: period = {}, data length = {}",
            atr_period, len
        )));
    }

    if atr_period > len {
        return Err(JsValue::from_str(&format!(
            "halftrend: Invalid period: period = {}, data length = {}",
            atr_period, len
        )));
    }

    unsafe {
        let h = core::slice::from_raw_parts(high_ptr, len);
        let l = core::slice::from_raw_parts(low_ptr, len);
        let c = core::slice::from_raw_parts(close_ptr, len);
        let out = core::slice::from_raw_parts_mut(out_ptr, 6 * len);

        let (ht, rest) = out.split_at_mut(len);
        let (tr, rest) = rest.split_at_mut(len);
        let (ah, rest) = rest.split_at_mut(len);
        let (al, rest) = rest.split_at_mut(len);
        let (bs, ss) = rest.split_at_mut(len);

        let input = HalfTrendInput::from_slices(
            h,
            l,
            c,
            HalfTrendParams {
                amplitude: Some(amplitude),
                channel_deviation: Some(channel_deviation),
                atr_period: Some(atr_period),
            },
        );

        halftrend_into_slices_kernel(ht, tr, ah, al, bs, ss, &input, Kernel::Auto)
            .map_err(|e| JsValue::from_str(&e.to_string()))?;
    }
    Ok(())
}

#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
#[derive(Serialize, Deserialize)]
pub struct HalfTrendBatchConfig {
    pub amplitude_range: (usize, usize, usize),
    pub channel_deviation_range: (f64, f64, f64),
    pub atr_period_range: (usize, usize, usize),
}

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

#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
#[wasm_bindgen(js_name = "halftrend_batch")]
pub fn halftrend_batch_unified_js(
    high: &[f64],
    low: &[f64],
    close: &[f64],
    config: JsValue,
) -> Result<JsValue, JsValue> {
    let cfg: HalfTrendBatchConfig =
        serde_wasm_bindgen::from_value(config).map_err(|e| JsValue::from_str(&e.to_string()))?;

    let sweep = HalfTrendBatchRange {
        amplitude: cfg.amplitude_range,
        channel_deviation: cfg.channel_deviation_range,
        atr_period: cfg.atr_period_range,
    };

    let combos = expand_grid_halftrend(&sweep).map_err(|e| JsValue::from_str(&e.to_string()))?;
    let rows_ind = combos.len();
    let cols = high.len();

    let mut ht = vec![0.0; rows_ind * cols];
    let mut tr = vec![0.0; rows_ind * cols];
    let mut ah = vec![0.0; rows_ind * cols];
    let mut al = vec![0.0; rows_ind * cols];
    let mut bs = vec![0.0; rows_ind * cols];
    let mut ss = vec![0.0; rows_ind * cols];

    halftrend_batch_rows_into(
        high,
        low,
        close,
        &sweep,
        Kernel::Auto,
        &mut ht,
        &mut tr,
        &mut ah,
        &mut al,
        &mut bs,
        &mut ss,
    )
    .map_err(|e| JsValue::from_str(&e.to_string()))?;

    let mut values = Vec::with_capacity(6 * rows_ind * cols);
    values.extend_from_slice(&ht);
    values.extend_from_slice(&tr);
    values.extend_from_slice(&ah);
    values.extend_from_slice(&al);
    values.extend_from_slice(&bs);
    values.extend_from_slice(&ss);

    let out = HalfTrendBatchJsOutput {
        values,
        combos,
        rows: 6 * rows_ind,
        cols,
    };
    serde_wasm_bindgen::to_value(&out).map_err(|e| JsValue::from_str(&e.to_string()))
}

#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
#[wasm_bindgen]
pub fn halftrend_batch_into(
    high_ptr: *const f64,
    low_ptr: *const f64,
    close_ptr: *const f64,
    out_ptr: *mut f64,
    len: usize,
    amp_start: usize,
    amp_end: usize,
    amp_step: usize,
    ch_start: f64,
    ch_end: f64,
    ch_step: f64,
    atr_start: usize,
    atr_end: usize,
    atr_step: usize,
) -> Result<usize, JsValue> {
    if high_ptr.is_null() || low_ptr.is_null() || close_ptr.is_null() || out_ptr.is_null() {
        return Err(JsValue::from_str(
            "null pointer passed to halftrend_batch_into",
        ));
    }
    unsafe {
        let h = std::slice::from_raw_parts(high_ptr, len);
        let l = std::slice::from_raw_parts(low_ptr, len);
        let c = std::slice::from_raw_parts(close_ptr, len);

        let sweep = HalfTrendBatchRange {
            amplitude: (amp_start, amp_end, amp_step),
            channel_deviation: (ch_start, ch_end, ch_step),
            atr_period: (atr_start, atr_end, atr_step),
        };
        let combos =
            expand_grid_halftrend(&sweep).map_err(|e| JsValue::from_str(&e.to_string()))?;
        let rows_ind = combos.len();
        let cols = len;

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

        let block = rows_ind * cols;
        let (ht, rest) = out.split_at_mut(block);
        let (tr, rest) = rest.split_at_mut(block);
        let (ah, rest) = rest.split_at_mut(block);
        let (al, rest) = rest.split_at_mut(block);
        let (bs, ss) = rest.split_at_mut(block);

        halftrend_batch_rows_into(h, l, c, &sweep, Kernel::Auto, ht, tr, ah, al, bs, ss)
            .map_err(|e| JsValue::from_str(&e.to_string()))?;

        Ok(rows_ind)
    }
}
