#[cfg(feature = "python")]
use numpy::{IntoPyArray, PyArray1, PyArrayMethods, PyUntypedArrayMethods};
#[cfg(feature = "python")]
use pyo3::exceptions::PyValueError;
#[cfg(feature = "python")]
use pyo3::prelude::*;

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

use crate::indicators::moving_averages::sma::{sma_with_kernel, SmaInput, SmaParams};
use crate::utilities::data_loader::Candles;
use crate::utilities::enums::Kernel;
use crate::utilities::helpers::{alloc_with_nan_prefix, detect_best_kernel};
use std::error::Error;
use thiserror::Error;

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

#[derive(Debug, Clone)]
pub struct TtmSqueezeOutput {
    pub momentum: Vec<f64>,
    pub squeeze: Vec<f64>,
}

#[derive(Debug, Clone)]
#[cfg_attr(
    all(target_arch = "wasm32", feature = "wasm"),
    derive(Serialize, Deserialize)
)]
pub struct TtmSqueezeParams {
    pub length: Option<usize>,
    pub bb_mult: Option<f64>,
    pub kc_mult_high: Option<f64>,
    pub kc_mult_mid: Option<f64>,
    pub kc_mult_low: Option<f64>,
}

impl Default for TtmSqueezeParams {
    fn default() -> Self {
        Self {
            length: Some(20),
            bb_mult: Some(2.0),
            kc_mult_high: Some(1.0),
            kc_mult_mid: Some(1.5),
            kc_mult_low: Some(2.0),
        }
    }
}

#[derive(Debug, Clone)]
pub struct TtmSqueezeInput<'a> {
    pub data: TtmSqueezeData<'a>,
    pub params: TtmSqueezeParams,
}

impl<'a> TtmSqueezeInput<'a> {
    #[inline]
    pub fn from_candles(candles: &'a Candles, params: TtmSqueezeParams) -> Self {
        Self {
            data: TtmSqueezeData::Candles { candles },
            params,
        }
    }

    #[inline]
    pub fn from_slices(
        high: &'a [f64],
        low: &'a [f64],
        close: &'a [f64],
        params: TtmSqueezeParams,
    ) -> Self {
        Self {
            data: TtmSqueezeData::Slices { high, low, close },
            params,
        }
    }

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

    #[inline]
    pub fn get_length(&self) -> usize {
        self.params.length.unwrap_or(20)
    }

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

    #[inline]
    pub fn get_kc_mult_high(&self) -> f64 {
        self.params.kc_mult_high.unwrap_or(1.0)
    }

    #[inline]
    pub fn get_kc_mult_mid(&self) -> f64 {
        self.params.kc_mult_mid.unwrap_or(1.5)
    }

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

#[derive(Debug, Clone)]
pub struct TtmSqueezeBuilder {
    length: Option<usize>,
    bb_mult: Option<f64>,
    kc_mult_high: Option<f64>,
    kc_mult_mid: Option<f64>,
    kc_mult_low: Option<f64>,
    kernel: Kernel,
}

impl Default for TtmSqueezeBuilder {
    fn default() -> Self {
        Self {
            length: None,
            bb_mult: None,
            kc_mult_high: None,
            kc_mult_mid: None,
            kc_mult_low: None,
            kernel: Kernel::Auto,
        }
    }
}

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

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

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

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

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

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

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

    #[inline]
    pub fn build_params(self) -> TtmSqueezeParams {
        TtmSqueezeParams {
            length: self.length,
            bb_mult: self.bb_mult,
            kc_mult_high: self.kc_mult_high,
            kc_mult_mid: self.kc_mult_mid,
            kc_mult_low: self.kc_mult_low,
        }
    }

    #[inline(always)]
    pub fn apply(self, candles: &Candles) -> Result<TtmSqueezeOutput, TtmSqueezeError> {
        let kernel = self.kernel;
        let params = self.build_params();
        let input = TtmSqueezeInput::from_candles(candles, params);
        ttm_squeeze_with_kernel(&input, kernel)
    }

    #[inline(always)]
    pub fn apply_slices(
        self,
        high: &[f64],
        low: &[f64],
        close: &[f64],
    ) -> Result<TtmSqueezeOutput, TtmSqueezeError> {
        let kernel = self.kernel;
        let params = self.build_params();
        let input = TtmSqueezeInput::from_slices(high, low, close, params);
        ttm_squeeze_with_kernel(&input, kernel)
    }

    #[inline(always)]
    pub fn into_stream(self) -> Result<TtmSqueezeStream, TtmSqueezeError> {
        TtmSqueezeStream::try_new(self.build_params())
    }
}

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

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

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

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

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

    #[error("ttm_squeeze: Inconsistent slice lengths - high={high}, low={low}, close={close}")]
    InconsistentSliceLengths {
        high: usize,
        low: usize,
        close: usize,
    },

    #[error("ttm_squeeze: Invalid bb_mult: must be positive")]
    InvalidBbMult { bb_mult: f64 },

    #[error("ttm_squeeze: Invalid kc_mult_high: must be positive")]
    InvalidKcMultHigh { kc_mult_high: f64 },

    #[error("ttm_squeeze: Invalid kc_mult_mid: must be positive")]
    InvalidKcMultMid { kc_mult_mid: f64 },

    #[error("ttm_squeeze: Invalid kc_mult_low: must be positive")]
    InvalidKcMultLow { kc_mult_low: f64 },

    #[error("ttm_squeeze: Invalid range in batch sweep: start={start}, end={end}, step={step}")]
    InvalidRange { start: f64, end: f64, step: f64 },

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

    #[error("ttm_squeeze: SMA error: {0}")]
    SmaError(String),

    #[error("ttm_squeeze: LinReg error: {0}")]
    LinRegError(String),
}

#[inline]
fn std_dev(data: &[f64], mean: f64, start: usize, end: usize) -> f64 {
    let mut sum_sq = 0.0;
    let mut count = 0;

    for i in start..=end {
        if !data[i].is_nan() {
            let diff = data[i] - mean;
            sum_sq += diff * diff;
            count += 1;
        }
    }

    if count > 1 {
        (sum_sq / count as f64).sqrt()
    } else {
        f64::NAN
    }
}

#[inline]
fn true_range(high: f64, low: f64, prev_close: Option<f64>) -> f64 {
    match prev_close {
        Some(pc) => {
            let hl = high - low;
            let hc = (high - pc).abs();
            let lc = (low - pc).abs();
            hl.max(hc).max(lc)
        }
        None => high - low,
    }
}

fn validate_params(params: &TtmSqueezeParams) -> Result<(), TtmSqueezeError> {
    let ok = |x: f64| x.is_finite() && x > 0.0;

    if let Some(bb) = params.bb_mult {
        if !ok(bb) {
            return Err(TtmSqueezeError::InvalidBbMult { bb_mult: bb });
        }
    }

    if let Some(x) = params.kc_mult_high {
        if !ok(x) {
            return Err(TtmSqueezeError::InvalidKcMultHigh { kc_mult_high: x });
        }
    }

    if let Some(x) = params.kc_mult_mid {
        if !ok(x) {
            return Err(TtmSqueezeError::InvalidKcMultMid { kc_mult_mid: x });
        }
    }

    if let Some(x) = params.kc_mult_low {
        if !ok(x) {
            return Err(TtmSqueezeError::InvalidKcMultLow { kc_mult_low: x });
        }
    }

    Ok(())
}

#[inline]
pub fn ttm_squeeze(input: &TtmSqueezeInput) -> Result<TtmSqueezeOutput, TtmSqueezeError> {
    ttm_squeeze_with_kernel(input, Kernel::Auto)
}

pub fn ttm_squeeze_with_kernel(
    input: &TtmSqueezeInput,
    kernel: Kernel,
) -> Result<TtmSqueezeOutput, TtmSqueezeError> {
    validate_params(&input.params)?;

    let (high, low, close) = match &input.data {
        TtmSqueezeData::Candles { candles } => {
            if candles.close.is_empty() {
                return Err(TtmSqueezeError::EmptyInputData);
            }
            (&candles.high[..], &candles.low[..], &candles.close[..])
        }
        TtmSqueezeData::Slices { high, low, close } => {
            if high.len() != low.len() || low.len() != close.len() {
                return Err(TtmSqueezeError::InconsistentSliceLengths {
                    high: high.len(),
                    low: low.len(),
                    close: close.len(),
                });
            }
            if close.is_empty() {
                return Err(TtmSqueezeError::EmptyInputData);
            }
            (*high, *low, *close)
        }
    };

    let len = close.len();
    let length = input.get_length();
    let bb_mult = input.params.bb_mult.unwrap_or(2.0);
    let kc_mult_high = input.params.kc_mult_high.unwrap_or(1.0);
    let kc_mult_mid = input.params.kc_mult_mid.unwrap_or(1.5);
    let kc_mult_low = input.params.kc_mult_low.unwrap_or(2.0);

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

    let first = close
        .iter()
        .position(|&x| !x.is_nan())
        .ok_or(TtmSqueezeError::AllValuesNaN)?;
    if len - first < length {
        return Err(TtmSqueezeError::NotEnoughValidData {
            needed: length,
            valid: len - first,
        });
    }

    let warmup = first + length - 1;

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

    if chosen == Kernel::Scalar
        && length == 20
        && bb_mult == 2.0
        && kc_mult_high == 1.0
        && kc_mult_mid == 1.5
        && kc_mult_low == 2.0
    {
        let mut momentum = alloc_with_nan_prefix(len, warmup);
        let mut squeeze = alloc_with_nan_prefix(len, warmup);

        unsafe {
            ttm_squeeze_scalar_classic(
                high,
                low,
                close,
                length,
                bb_mult,
                kc_mult_high,
                kc_mult_mid,
                kc_mult_low,
                first,
                warmup,
                &mut momentum,
                &mut squeeze,
            )?;
        }

        return Ok(TtmSqueezeOutput { momentum, squeeze });
    }

    let sma_params = SmaParams {
        period: Some(length),
    };
    let sma_input = SmaInput::from_slice(close, sma_params);
    let sma_result = sma_with_kernel(&sma_input, kernel)
        .map_err(|e| TtmSqueezeError::SmaError(e.to_string()))?;
    let sma_values = sma_result.values;

    let mut tr = alloc_with_nan_prefix(len, first);
    for i in first..len {
        tr[i] = if i == first {
            high[i] - low[i]
        } else {
            let pc = close[i - 1];
            let hl = high[i] - low[i];
            let hc = (high[i] - pc).abs();
            let lc = (low[i] - pc).abs();
            hl.max(hc).max(lc)
        };
    }

    let tr_sma_params = SmaParams {
        period: Some(length),
    };
    let tr_sma_input = SmaInput::from_slice(&tr, tr_sma_params);
    let tr_sma_result = sma_with_kernel(&tr_sma_input, kernel)
        .map_err(|e| TtmSqueezeError::SmaError(e.to_string()))?;
    let dev_kc = tr_sma_result.values;

    let mut squeeze = alloc_with_nan_prefix(len, warmup);
    let mut momentum = alloc_with_nan_prefix(len, warmup);

    for i in warmup..len {
        let m = sma_values[i];
        let dkc = dev_kc[i];
        if m.is_nan() || dkc.is_nan() {
            continue;
        }

        let start = i + 1 - length;
        let mut sum = 0.0;
        let mut cnt = 0usize;
        for j in start..=i {
            let v = close[j];
            if v.is_nan() {
                continue;
            }
            let d = v - m;
            sum += d * d;
            cnt += 1;
        }

        if cnt > 1 {
            let std = (sum / cnt as f64).sqrt();
            let bb_upper = m + bb_mult * std;
            let bb_lower = m - bb_mult * std;

            let kc_upper_low = m + dkc * kc_mult_low;
            let kc_lower_low = m - dkc * kc_mult_low;
            let kc_upper_mid = m + dkc * kc_mult_mid;
            let kc_lower_mid = m - dkc * kc_mult_mid;
            let kc_upper_high = m + dkc * kc_mult_high;
            let kc_lower_high = m - dkc * kc_mult_high;

            let no_sqz = bb_lower < kc_lower_low || bb_upper > kc_upper_low;
            squeeze[i] = if no_sqz {
                0.0
            } else if bb_lower >= kc_lower_high || bb_upper <= kc_upper_high {
                3.0
            } else if bb_lower >= kc_lower_mid || bb_upper <= kc_upper_mid {
                2.0
            } else {
                1.0
            };
        }

        let mut highest = f64::NEG_INFINITY;
        let mut lowest = f64::INFINITY;
        let mut has_valid = false;

        for j in start..=i {
            if high[j].is_finite() && low[j].is_finite() {
                highest = highest.max(high[j]);
                lowest = lowest.min(low[j]);
                has_valid = true;
            }
        }

        if has_valid {
            let midpoint = (highest + lowest) * 0.5;

            let avg = (midpoint + m) * 0.5;

            let mut sx = 0.0;
            let mut sy = 0.0;
            let mut sxy = 0.0;
            let mut sx2 = 0.0;
            let mut n = 0.0;

            for (k, j) in (start..=i).enumerate() {
                let y = close[j] - avg;
                if y.is_nan() {
                    continue;
                }
                let x = k as f64;
                sx += x;
                sy += y;
                sxy += x * y;
                sx2 += x * x;
                n += 1.0;
            }

            if n >= 2.0 {
                let slope = (n * sxy - sx * sy) / (n * sx2 - sx * sx);
                let intercept = (sy - slope * sx) / n;
                momentum[i] = intercept + slope * ((length - 1) as f64);
            }
        }
    }

    Ok(TtmSqueezeOutput { momentum, squeeze })
}

#[inline]
pub fn ttm_squeeze_into_slices(
    dst_momentum: &mut [f64],
    dst_squeeze: &mut [f64],
    input: &TtmSqueezeInput,
    kernel: Kernel,
) -> Result<(), TtmSqueezeError> {
    validate_params(&input.params)?;

    let (high, low, close) = match &input.data {
        TtmSqueezeData::Candles { candles } => {
            (&candles.high[..], &candles.low[..], &candles.close[..])
        }
        TtmSqueezeData::Slices { high, low, close } => (*high, *low, *close),
    };

    if close.is_empty() {
        return Err(TtmSqueezeError::EmptyInputData);
    }

    if dst_momentum.len() != close.len() || dst_squeeze.len() != close.len() {
        return Err(TtmSqueezeError::OutputLengthMismatch {
            expected: close.len(),
            got: dst_momentum.len().min(dst_squeeze.len()),
        });
    }

    let len = close.len();
    let length = input.get_length();
    let bb_mult = input.get_bb_mult();
    let kc_mult_high = input.get_kc_mult_high();
    let kc_mult_mid = input.get_kc_mult_mid();
    let kc_mult_low = input.get_kc_mult_low();

    let first = close
        .iter()
        .position(|&x| !x.is_nan())
        .ok_or(TtmSqueezeError::AllValuesNaN)?;

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

    if len - first < length {
        return Err(TtmSqueezeError::NotEnoughValidData {
            needed: length,
            valid: len - first,
        });
    }

    let warmup = first + length - 1;

    for i in 0..warmup {
        dst_momentum[i] = f64::NAN;
        dst_squeeze[i] = f64::NAN;
    }

    let sma_params = SmaParams {
        period: Some(length),
    };
    let sma_input = SmaInput::from_slice(close, sma_params);
    let sma_result = sma_with_kernel(&sma_input, kernel)
        .map_err(|e| TtmSqueezeError::SmaError(e.to_string()))?;
    let sma_values = sma_result.values;

    let mut tr_values = alloc_with_nan_prefix(len, first);
    for i in first..len {
        tr_values[i] = if i == first {
            high[i] - low[i]
        } else {
            true_range(high[i], low[i], Some(close[i - 1]))
        };
    }

    let tr_sma_params = SmaParams {
        period: Some(length),
    };
    let tr_sma_input = SmaInput::from_slice(&tr_values, tr_sma_params);
    let tr_sma_result = sma_with_kernel(&tr_sma_input, kernel)
        .map_err(|e| TtmSqueezeError::SmaError(e.to_string()))?;
    let dev_kc = tr_sma_result.values;

    for i in warmup..len {
        let m = sma_values[i];
        let dev_kc_val = dev_kc[i];

        if m.is_nan() || dev_kc_val.is_nan() {
            dst_squeeze[i] = f64::NAN;
            continue;
        }

        let start = i + 1 - length;
        let mut sum = 0.0;
        let mut count = 0;

        for j in start..=i {
            if !close[j].is_nan() {
                let d = close[j] - m;
                sum += d * d;
                count += 1;
            }
        }

        let std = if count > 1 {
            (sum / count as f64).sqrt()
        } else {
            f64::NAN
        };

        if std.is_nan() {
            dst_squeeze[i] = f64::NAN;
            continue;
        }

        let bb_upper = m + bb_mult * std;
        let bb_lower = m - bb_mult * std;
        let kc_upper_low = m + dev_kc_val * kc_mult_low;
        let kc_lower_low = m - dev_kc_val * kc_mult_low;
        let kc_upper_mid = m + dev_kc_val * kc_mult_mid;
        let kc_lower_mid = m - dev_kc_val * kc_mult_mid;
        let kc_upper_high = m + dev_kc_val * kc_mult_high;
        let kc_lower_high = m - dev_kc_val * kc_mult_high;

        let no_sqz = bb_lower < kc_lower_low || bb_upper > kc_upper_low;

        dst_squeeze[i] = if no_sqz {
            0.0
        } else if bb_lower >= kc_lower_high || bb_upper <= kc_upper_high {
            3.0
        } else if bb_lower >= kc_lower_mid || bb_upper <= kc_upper_mid {
            2.0
        } else {
            1.0
        };
    }

    for end_idx in warmup..len {
        let start_idx = end_idx + 1 - length;

        let mut highest = f64::NEG_INFINITY;
        let mut lowest = f64::INFINITY;
        let mut has_valid = false;

        for j in start_idx..=end_idx {
            if high[j].is_finite() && low[j].is_finite() {
                highest = highest.max(high[j]);
                lowest = lowest.min(low[j]);
                has_valid = true;
            }
        }

        if !has_valid || sma_values[end_idx].is_nan() {
            dst_momentum[end_idx] = f64::NAN;
            continue;
        }

        let midpoint = (highest + lowest) * 0.5;
        let avg = (midpoint + sma_values[end_idx]) / 2.0;

        let mut sum_x = 0.0;
        let mut sum_y = 0.0;
        let mut sum_xy = 0.0;
        let mut sum_x2 = 0.0;
        let mut n = 0.0;

        for (k, j) in (start_idx..=end_idx).enumerate() {
            if close[j].is_nan() {
                continue;
            }
            let x = k as f64;
            let y = close[j] - avg;
            sum_x += x;
            sum_y += y;
            sum_xy += x * y;
            sum_x2 += x * x;
            n += 1.0;
        }

        if n >= 2.0 {
            let slope = (n * sum_xy - sum_x * sum_y) / (n * sum_x2 - sum_x * sum_x);
            let intercept = (sum_y - slope * sum_x) / n;
            dst_momentum[end_idx] = intercept + slope * ((length - 1) as f64);
        } else {
            dst_momentum[end_idx] = f64::NAN;
        }
    }

    Ok(())
}

#[inline]
pub fn ttm_squeeze_into(
    dst_momentum: &mut [f64],
    dst_squeeze: &mut [f64],
    input: &TtmSqueezeInput,
    kernel: Kernel,
) -> Result<(), TtmSqueezeError> {
    ttm_squeeze_into_slices(dst_momentum, dst_squeeze, input, kernel)
}

#[derive(Debug, Clone)]
struct MonoDeque {
    idx: Vec<usize>,
    val: Vec<f64>,
    head: usize,
    tail: usize,
    len: usize,
    cap: usize,
    is_max: bool,
}

impl MonoDeque {
    #[inline(always)]
    fn new(cap: usize, is_max: bool) -> Self {
        Self {
            idx: vec![0; cap],
            val: vec![f64::NAN; cap],
            head: 0,
            tail: 0,
            len: 0,
            cap,
            is_max,
        }
    }

    #[inline(always)]
    fn clear(&mut self) {
        self.head = 0;
        self.tail = 0;
        self.len = 0;
    }

    #[inline(always)]
    fn is_empty(&self) -> bool {
        self.len == 0
    }

    #[inline(always)]
    fn front_val(&self) -> f64 {
        debug_assert!(self.len > 0);
        self.val[self.head]
    }

    #[inline(always)]
    fn expire(&mut self, min_idx: usize) {
        while self.len > 0 {
            let i = self.idx[self.head];
            if i >= min_idx {
                break;
            }
            self.head += 1;
            if self.head == self.cap {
                self.head = 0;
            }
            self.len -= 1;
        }
    }

    #[inline(always)]
    fn push(&mut self, idx: usize, value: f64) {
        while self.len > 0 {
            let back_pos = if self.tail == 0 {
                self.cap - 1
            } else {
                self.tail - 1
            };
            let back_val = self.val[back_pos];

            let ok = if self.is_max {
                back_val >= value
            } else {
                back_val <= value
            };
            if ok {
                break;
            }
            self.tail = back_pos;
            self.len -= 1;
        }
        self.idx[self.tail] = idx;
        self.val[self.tail] = value;
        self.tail += 1;
        if self.tail == self.cap {
            self.tail = 0;
        }
        self.len += 1;
    }
}

#[derive(Debug, Clone)]
pub struct TtmSqueezeStream {
    params: TtmSqueezeParams,

    hi: Vec<f64>,
    lo: Vec<f64>,
    cl: Vec<f64>,
    tr: Vec<f64>,

    head: usize,
    filled: bool,
    t: usize,

    sum0: f64,
    sum1: f64,
    sumsq: f64,
    tr_sum: f64,

    prev_close: Option<f64>,

    n: usize,
    n_f64: f64,
    inv_n: f64,
    sx: f64,
    sx2: f64,
    inv_den: f64,
    half_nm1: f64,

    bb_sq: f64,
    kc_low_sq: f64,
    kc_mid_sq: f64,
    kc_high_sq: f64,

    max_q: MonoDeque,
    min_q: MonoDeque,
}

impl TtmSqueezeStream {
    pub fn try_new(params: TtmSqueezeParams) -> Result<Self, TtmSqueezeError> {
        let n = params.length.unwrap_or(20);
        if n == 0 {
            return Err(TtmSqueezeError::InvalidPeriod {
                period: 0,
                data_len: 0,
            });
        }

        let n_f64 = n as f64;
        let inv_n = 1.0 / n_f64;
        let sx = 0.5 * n_f64 * (n_f64 - 1.0);
        let sx2 = (n_f64 - 1.0) * n_f64 * (2.0 * n_f64 - 1.0) / 6.0;
        let den = n_f64 * sx2 - sx * sx;
        let inv_den = if den > 0.0 { 1.0 / den } else { 0.0 };
        let half_nm1 = 0.5 * (n_f64 - 1.0);

        let bb = params.bb_mult.unwrap_or(2.0);
        let kc_hi = params.kc_mult_high.unwrap_or(1.0);
        let kc_md = params.kc_mult_mid.unwrap_or(1.5);
        let kc_lo = params.kc_mult_low.unwrap_or(2.0);

        Ok(Self {
            params,
            hi: vec![f64::NAN; n],
            lo: vec![f64::NAN; n],
            cl: vec![f64::NAN; n],
            tr: vec![0.0; n],

            head: 0,
            filled: false,
            t: 0,

            sum0: 0.0,
            sum1: 0.0,
            sumsq: 0.0,
            tr_sum: 0.0,

            prev_close: None,

            n,
            n_f64,
            inv_n,
            sx,
            sx2,
            inv_den,
            half_nm1,

            bb_sq: bb * bb,
            kc_low_sq: kc_lo * kc_lo,
            kc_mid_sq: kc_md * kc_md,
            kc_high_sq: kc_hi * kc_hi,

            max_q: MonoDeque::new(n, true),
            min_q: MonoDeque::new(n, false),
        })
    }

    #[inline]
    pub fn update(&mut self, high: f64, low: f64, close: f64) -> Option<(f64, f64)> {
        let n = self.n;
        let pos = self.head;

        let tr_new = match self.prev_close {
            Some(pc) => {
                let hl = high - low;
                let hc = (high - pc).abs();
                let lc = (low - pc).abs();
                if hl >= hc {
                    if hl >= lc {
                        hl
                    } else {
                        lc
                    }
                } else if hc >= lc {
                    hc
                } else {
                    lc
                }
            }
            None => high - low,
        };

        if self.filled {
            let min_idx = self.t + 1 - n;
            self.max_q.expire(min_idx);
            self.min_q.expire(min_idx);
        }

        self.max_q.push(self.t, high);
        self.min_q.push(self.t, low);

        let old_c = self.cl[pos];
        let old_tr = self.tr[pos];

        self.hi[pos] = high;
        self.lo[pos] = low;
        self.cl[pos] = close;
        self.tr[pos] = tr_new;

        if !self.filled {
            self.sum0 += close;
            self.sumsq = close.mul_add(close, self.sumsq);
            self.sum1 += (self.t as f64) * close;
            self.tr_sum += tr_new;

            self.prev_close = Some(close);
            self.head = (pos + 1) % n;
            self.t += 1;

            if self.t < n {
                return None;
            }

            self.filled = true;
            return Some(self.emit());
        }

        let sum0_old = self.sum0;

        self.sum0 += close - old_c;
        self.sumsq = close.mul_add(close, self.sumsq - old_c * old_c);

        self.sum1 = self.sum1 - sum0_old + old_c + (self.n_f64 - 1.0) * close;

        self.tr_sum += tr_new - old_tr;

        self.prev_close = Some(close);
        self.head = (pos + 1) % n;
        self.t += 1;

        Some(self.emit())
    }

    #[inline]
    fn emit(&self) -> (f64, f64) {
        let m = self.sum0 * self.inv_n;
        let var = (-m).mul_add(m, self.sumsq * self.inv_n);
        let var_pos = if var > 0.0 { var } else { 0.0 };

        let dkc = self.tr_sum * self.inv_n;
        let dkc2 = dkc * dkc;

        let bbv = self.bb_sq * var_pos;
        let t_low = self.kc_low_sq * dkc2;
        let t_mid = self.kc_mid_sq * dkc2;
        let t_hi = self.kc_high_sq * dkc2;

        let sqz = if bbv > t_low {
            0.0
        } else if bbv <= t_hi {
            3.0
        } else if bbv <= t_mid {
            2.0
        } else {
            1.0
        };

        let highest = if self.max_q.is_empty() {
            f64::NAN
        } else {
            self.max_q.front_val()
        };
        let lowest = if self.min_q.is_empty() {
            f64::NAN
        } else {
            self.min_q.front_val()
        };

        let midpoint = 0.5 * (highest + lowest);
        let avg = 0.5 * (midpoint + m);

        let sy = self.sum0 - avg * self.n_f64;
        let sxy = self.sum1 - avg * self.sx;

        let mom = if self.n >= 2 && self.inv_den.is_finite() {
            let slope = self.n_f64.mul_add(sxy, -(self.sx * sy)) * self.inv_den;
            sy * self.inv_n + slope * self.half_nm1
        } else {
            f64::NAN
        };

        (mom, sqz)
    }

    pub fn reset(&mut self) {
        self.hi.fill(f64::NAN);
        self.lo.fill(f64::NAN);
        self.cl.fill(f64::NAN);
        self.tr.fill(0.0);

        self.head = 0;
        self.filled = false;
        self.t = 0;

        self.sum0 = 0.0;
        self.sum1 = 0.0;
        self.sumsq = 0.0;
        self.tr_sum = 0.0;

        self.prev_close = None;

        self.max_q.clear();
        self.min_q.clear();
    }
}

#[inline(always)]
pub unsafe fn ttm_squeeze_scalar_classic(
    high: &[f64],
    low: &[f64],
    close: &[f64],
    length: usize,
    bb_mult: f64,
    kc_mult_high: f64,
    kc_mult_mid: f64,
    kc_mult_low: f64,
    first: usize,
    warmup: usize,
    momentum: &mut [f64],
    squeeze: &mut [f64],
) -> Result<(), TtmSqueezeError> {
    let len = close.len();
    if len == 0 || length < 2 || warmup >= len {
        return Ok(());
    }

    let n = length as f64;
    let sx = 0.5 * n * (n - 1.0);
    let sx2 = (n - 1.0) * n * (2.0 * n - 1.0) / 6.0;
    let den = n * sx2 - sx * sx;
    let inv_den = 1.0 / den;
    let inv_n = 1.0 / n;
    let half_nm1 = 0.5 * (n - 1.0);

    let mut cbuf = vec![0.0f64; length];
    let mut trbuf = vec![0.0f64; length];
    let mut cpos = 0usize;
    let mut trpos = 0usize;

    let mut sum0 = 0.0f64;
    let mut sum1 = 0.0f64;
    let mut sumsq = 0.0f64;
    let mut tr_sum = 0.0f64;

    let cap = length;
    let mut max_q = vec![0usize; cap];
    let mut min_q = vec![0usize; cap];
    let (mut max_head, mut max_tail, mut max_len) = (0usize, 0usize, 0usize);
    let (mut min_head, mut min_tail, mut min_len) = (0usize, 0usize, 0usize);

    let bb_sq = bb_mult * bb_mult;
    let kc_low_sq = kc_mult_low * kc_mult_low;
    let kc_mid_sq = kc_mult_mid * kc_mult_mid;
    let kc_high_sq = kc_mult_high * kc_mult_high;

    {
        let mut r = 0usize;
        let mut i = first;
        while i <= warmup {
            let c = *close.get_unchecked(i);
            *cbuf.get_unchecked_mut(cpos) = c;
            sum0 += c;
            sumsq = c.mul_add(c, sumsq);
            sum1 += (r as f64) * c;

            let tr_val = if i == first {
                *high.get_unchecked(i) - *low.get_unchecked(i)
            } else {
                let pc = *close.get_unchecked(i - 1);
                let hl = *high.get_unchecked(i) - *low.get_unchecked(i);
                let hc = (*high.get_unchecked(i) - pc).abs();
                let lc = (*low.get_unchecked(i) - pc).abs();
                if hl >= hc {
                    if hl >= lc {
                        hl
                    } else {
                        lc
                    }
                } else {
                    if hc >= lc {
                        hc
                    } else {
                        lc
                    }
                }
            };
            *trbuf.get_unchecked_mut(trpos) = tr_val;
            tr_sum += tr_val;

            while max_len > 0 {
                let back_pos = if max_tail == 0 { cap - 1 } else { max_tail - 1 };
                let back_idx = *max_q.get_unchecked(back_pos);
                if *high.get_unchecked(i) <= *high.get_unchecked(back_idx) {
                    break;
                }
                max_tail = back_pos;
                max_len -= 1;
            }
            *max_q.get_unchecked_mut(max_tail) = i;
            max_tail += 1;
            if max_tail == cap {
                max_tail = 0;
            }
            max_len += 1;

            while min_len > 0 {
                let back_pos = if min_tail == 0 { cap - 1 } else { min_tail - 1 };
                let back_idx = *min_q.get_unchecked(back_pos);
                if *low.get_unchecked(i) >= *low.get_unchecked(back_idx) {
                    break;
                }
                min_tail = back_pos;
                min_len -= 1;
            }
            *min_q.get_unchecked_mut(min_tail) = i;
            min_tail += 1;
            if min_tail == cap {
                min_tail = 0;
            }
            min_len += 1;

            cpos += 1;
            if cpos == length {
                cpos = 0;
            }
            trpos += 1;
            if trpos == length {
                trpos = 0;
            }
            r += 1;
            i += 1;
        }
    }

    {
        let m = sum0 * inv_n;
        let var = (-m).mul_add(m, sumsq * inv_n);
        let var_pos = if var > 0.0 { var } else { 0.0 };
        let dkc = tr_sum * inv_n;
        let dkc2 = dkc * dkc;

        let bbv = bb_sq * var_pos;
        let t_low = kc_low_sq * dkc2;
        let t_mid = kc_mid_sq * dkc2;
        let t_high = kc_high_sq * dkc2;

        *squeeze.get_unchecked_mut(warmup) = if bbv > t_low {
            0.0
        } else if bbv <= t_high {
            3.0
        } else if bbv <= t_mid {
            2.0
        } else {
            1.0
        };

        let hi_idx = *max_q.get_unchecked(max_head);
        let lo_idx = *min_q.get_unchecked(min_head);
        let highest = *high.get_unchecked(hi_idx);
        let lowest = *low.get_unchecked(lo_idx);

        let midpoint = 0.5 * (highest + lowest);
        let avg = 0.5 * (midpoint + m);
        let sy = sum0 - avg * n;
        let sxy = sum1 - avg * sx;
        let slope = n.mul_add(sxy, -(sx * sy)) * inv_den;
        *momentum.get_unchecked_mut(warmup) = sy * inv_n + slope * half_nm1;
    }

    let mut i = warmup + 1;
    while i < len {
        let start_idx = i + 1 - length;

        while max_len > 0 {
            let front_idx = *max_q.get_unchecked(max_head);
            if front_idx >= start_idx {
                break;
            }
            max_head += 1;
            if max_head == cap {
                max_head = 0;
            }
            max_len -= 1;
        }
        while min_len > 0 {
            let front_idx = *min_q.get_unchecked(min_head);
            if front_idx >= start_idx {
                break;
            }
            min_head += 1;
            if min_head == cap {
                min_head = 0;
            }
            min_len -= 1;
        }

        while max_len > 0 {
            let back_pos = if max_tail == 0 { cap - 1 } else { max_tail - 1 };
            let back_idx = *max_q.get_unchecked(back_pos);
            if *high.get_unchecked(i) <= *high.get_unchecked(back_idx) {
                break;
            }
            max_tail = back_pos;
            max_len -= 1;
        }
        *max_q.get_unchecked_mut(max_tail) = i;
        max_tail += 1;
        if max_tail == cap {
            max_tail = 0;
        }
        max_len += 1;

        while min_len > 0 {
            let back_pos = if min_tail == 0 { cap - 1 } else { min_tail - 1 };
            let back_idx = *min_q.get_unchecked(back_pos);
            if *low.get_unchecked(i) >= *low.get_unchecked(back_idx) {
                break;
            }
            min_tail = back_pos;
            min_len -= 1;
        }
        *min_q.get_unchecked_mut(min_tail) = i;
        min_tail += 1;
        if min_tail == cap {
            min_tail = 0;
        }
        min_len += 1;

        let old = *cbuf.get_unchecked(cpos);
        let new = *close.get_unchecked(i);
        let sum0_old = sum0;
        sum0 += new - old;
        sumsq = new.mul_add(new, sumsq - old * old);
        sum1 = sum1 - sum0_old + old + (n - 1.0) * new;
        *cbuf.get_unchecked_mut(cpos) = new;
        cpos += 1;
        if cpos == length {
            cpos = 0;
        }

        let old_tr = *trbuf.get_unchecked(trpos);
        let pc = *close.get_unchecked(i - 1);
        let hi_i = *high.get_unchecked(i);
        let lo_i = *low.get_unchecked(i);
        let hl = hi_i - lo_i;
        let hc = (hi_i - pc).abs();
        let lc = (lo_i - pc).abs();
        let tr_new = hl.max(hc).max(lc);
        tr_sum += tr_new - old_tr;
        *trbuf.get_unchecked_mut(trpos) = tr_new;
        trpos += 1;
        if trpos == length {
            trpos = 0;
        }

        let m = sum0 * inv_n;
        let var = (-m).mul_add(m, sumsq * inv_n);
        let var_pos = if var > 0.0 { var } else { 0.0 };
        let dkc = tr_sum * inv_n;
        let dkc2 = dkc * dkc;
        let bbv = bb_sq * var_pos;
        let t_low = kc_low_sq * dkc2;
        let t_mid = kc_mid_sq * dkc2;
        let t_high = kc_high_sq * dkc2;
        *squeeze.get_unchecked_mut(i) = if bbv > t_low {
            0.0
        } else if bbv <= t_high {
            3.0
        } else if bbv <= t_mid {
            2.0
        } else {
            1.0
        };

        let hi_idx = *max_q.get_unchecked(max_head);
        let lo_idx = *min_q.get_unchecked(min_head);
        let highest = *high.get_unchecked(hi_idx);
        let lowest = *low.get_unchecked(lo_idx);

        let midpoint = 0.5 * (highest + lowest);
        let avg = 0.5 * (midpoint + m);
        let sy = sum0 - avg * n;
        let sxy = sum1 - avg * sx;
        let slope = n.mul_add(sxy, -(sx * sy)) * inv_den;
        *momentum.get_unchecked_mut(i) = sy * inv_n + slope * half_nm1;

        i += 1;
    }

    Ok(())
}

#[cfg(feature = "python")]
use crate::utilities::kernel_validation::validate_kernel;

#[cfg(feature = "python")]
#[pyfunction(name = "ttm_squeeze")]
#[pyo3(signature = (high, low, close, length=20, bb_mult=2.0, kc_mult_high=1.0, kc_mult_mid=1.5, kc_mult_low=2.0, kernel=None))]
pub fn ttm_squeeze_py<'py>(
    py: Python<'py>,
    high: numpy::PyReadonlyArray1<'py, f64>,
    low: numpy::PyReadonlyArray1<'py, f64>,
    close: numpy::PyReadonlyArray1<'py, f64>,
    length: usize,
    bb_mult: f64,
    kc_mult_high: f64,
    kc_mult_mid: f64,
    kc_mult_low: f64,
    kernel: Option<&str>,
) -> PyResult<(Bound<'py, PyArray1<f64>>, Bound<'py, PyArray1<f64>>)> {
    let h = high.as_slice()?;
    let l = low.as_slice()?;
    let c = close.as_slice()?;

    if h.len() != l.len() || l.len() != c.len() {
        return Err(PyValueError::new_err(format!(
            "ttm_squeeze: Inconsistent slice lengths - high={}, low={}, close={}",
            h.len(),
            l.len(),
            c.len()
        )));
    }

    let params = TtmSqueezeParams {
        length: Some(length),
        bb_mult: Some(bb_mult),
        kc_mult_high: Some(kc_mult_high),
        kc_mult_mid: Some(kc_mult_mid),
        kc_mult_low: Some(kc_mult_low),
    };

    let input = TtmSqueezeInput::from_slices(h, l, c, params);
    let kern = validate_kernel(kernel, false)?;

    let mut momentum = vec![f64::NAN; c.len()];
    let mut squeeze = vec![f64::NAN; c.len()];

    py.allow_threads(|| ttm_squeeze_into_slices(&mut momentum, &mut squeeze, &input, kern))
        .map_err(|e| PyValueError::new_err(e.to_string()))?;

    Ok((momentum.into_pyarray(py), squeeze.into_pyarray(py)))
}

#[cfg(all(feature = "python", feature = "cuda"))]
use crate::cuda::{cuda_available, CudaTtmSqueeze};
#[cfg(all(feature = "python", feature = "cuda"))]
use crate::indicators::moving_averages::alma::DeviceArrayF32Py;
#[cfg(all(feature = "python", feature = "cuda"))]
use numpy::PyReadonlyArray1;
#[cfg(all(feature = "python", feature = "cuda"))]
#[cfg(all(feature = "python", feature = "cuda"))]
#[pyfunction(name = "ttm_squeeze_cuda_batch_dev")]
#[pyo3(signature = (high_f32, low_f32, close_f32, length_range, bb_mult_range, kc_high_range, kc_mid_range, kc_low_range, device_id=0))]
pub fn ttm_squeeze_cuda_batch_dev_py(
    py: Python<'_>,
    high_f32: PyReadonlyArray1<'_, f32>,
    low_f32: PyReadonlyArray1<'_, f32>,
    close_f32: PyReadonlyArray1<'_, f32>,
    length_range: (usize, usize, usize),
    bb_mult_range: (f64, f64, f64),
    kc_high_range: (f64, f64, f64),
    kc_mid_range: (f64, f64, f64),
    kc_low_range: (f64, f64, f64),
    device_id: usize,
) -> PyResult<(DeviceArrayF32Py, DeviceArrayF32Py)> {
    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 = TtmSqueezeBatchRange {
        length: length_range,
        bb_mult: bb_mult_range,
        kc_high: kc_high_range,
        kc_mid: kc_mid_range,
        kc_low: kc_low_range,
    };
    let (mo, sq, ctx, dev_id_u32) = py.allow_threads(|| {
        let cuda =
            CudaTtmSqueeze::new(device_id).map_err(|e| PyValueError::new_err(e.to_string()))?;
        let ctx = cuda.context_arc();
        let dev_id_u32 = cuda.device_id();
        match cuda.ttm_squeeze_batch_dev(h, l, c, &sweep) {
            Ok((mo, sq)) => Ok((mo, sq, ctx, dev_id_u32)),
            Err(e) => Err(PyValueError::new_err(e.to_string())),
        }
    })?;
    Ok((
        DeviceArrayF32Py {
            inner: mo,
            _ctx: Some(ctx.clone()),
            device_id: Some(dev_id_u32),
        },
        DeviceArrayF32Py {
            inner: sq,
            _ctx: Some(ctx),
            device_id: Some(dev_id_u32),
        },
    ))
}

#[cfg(all(feature = "python", feature = "cuda"))]
#[pyfunction(name = "ttm_squeeze_cuda_many_series_one_param_dev")]
#[pyo3(signature = (high_tm_f32, low_tm_f32, close_tm_f32, cols, rows, length, bb_mult, kc_high, kc_mid, kc_low, device_id=0))]
pub fn ttm_squeeze_cuda_many_series_one_param_dev_py(
    py: Python<'_>,
    high_tm_f32: PyReadonlyArray1<'_, f32>,
    low_tm_f32: PyReadonlyArray1<'_, f32>,
    close_tm_f32: PyReadonlyArray1<'_, f32>,
    cols: usize,
    rows: usize,
    length: usize,
    bb_mult: f32,
    kc_high: f32,
    kc_mid: f32,
    kc_low: f32,
    device_id: usize,
) -> PyResult<(DeviceArrayF32Py, DeviceArrayF32Py)> {
    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 (mo, sq, ctx, dev_id_u32) = py.allow_threads(|| {
        let cuda =
            CudaTtmSqueeze::new(device_id).map_err(|e| PyValueError::new_err(e.to_string()))?;
        let ctx = cuda.context_arc();
        let dev_id_u32 = cuda.device_id();
        match cuda.ttm_squeeze_many_series_one_param_time_major_dev(
            h, l, c, cols, rows, length, bb_mult, kc_high, kc_mid, kc_low,
        ) {
            Ok((mo, sq)) => Ok((mo, sq, ctx, dev_id_u32)),
            Err(e) => Err(PyValueError::new_err(e.to_string())),
        }
    })?;
    Ok((
        DeviceArrayF32Py {
            inner: mo,
            _ctx: Some(ctx.clone()),
            device_id: Some(dev_id_u32),
        },
        DeviceArrayF32Py {
            inner: sq,
            _ctx: Some(ctx),
            device_id: Some(dev_id_u32),
        },
    ))
}

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

#[cfg(feature = "python")]
#[pymethods]
impl TtmSqueezeStreamPy {
    #[new]
    fn new(
        length: usize,
        bb_mult: f64,
        kc_mult_high: f64,
        kc_mult_mid: f64,
        kc_mult_low: f64,
    ) -> PyResult<Self> {
        let params = TtmSqueezeParams {
            length: Some(length),
            bb_mult: Some(bb_mult),
            kc_mult_high: Some(kc_mult_high),
            kc_mult_mid: Some(kc_mult_mid),
            kc_mult_low: Some(kc_mult_low),
        };
        Ok(Self {
            stream: TtmSqueezeStream::try_new(params)
                .map_err(|e| PyValueError::new_err(e.to_string()))?,
        })
    }

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

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

#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
#[wasm_bindgen(js_name = ttm_squeeze)]
pub fn ttm_squeeze_js(
    high: &[f64],
    low: &[f64],
    close: &[f64],
    length: usize,
    bb_mult: f64,
    kc_mult_high: f64,
    kc_mult_mid: f64,
    kc_mult_low: f64,
) -> Result<JsValue, JsValue> {
    let params = TtmSqueezeParams {
        length: Some(length),
        bb_mult: Some(bb_mult),
        kc_mult_high: Some(kc_mult_high),
        kc_mult_mid: Some(kc_mult_mid),
        kc_mult_low: Some(kc_mult_low),
    };

    let input = TtmSqueezeInput::from_slices(high, low, close, params);

    let result = ttm_squeeze(&input).map_err(|e| JsValue::from_str(&e.to_string()))?;

    let cols = result.momentum.len();
    let mut values = Vec::with_capacity(2 * cols);
    values.extend_from_slice(&result.momentum);
    values.extend_from_slice(&result.squeeze);

    let js_result = TtmSqueezeJsResult {
        values,
        rows: 2,
        cols,
    };

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

#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
#[wasm_bindgen(js_name = ttm_squeeze_into)]
pub fn ttm_squeeze_into_js(
    high: &[f64],
    low: &[f64],
    close: &[f64],
    length: usize,
    bb_mult: f64,
    kc_mult_high: f64,
    kc_mult_mid: f64,
    kc_mult_low: f64,
    out_momentum: &mut [f64],
    out_squeeze: &mut [f64],
) -> Result<(), JsValue> {
    if high.len() != low.len() || low.len() != close.len() {
        return Err(JsValue::from_str("slice length mismatch"));
    }
    if out_momentum.len() != close.len() || out_squeeze.len() != close.len() {
        return Err(JsValue::from_str("output length mismatch"));
    }

    let params = TtmSqueezeParams {
        length: Some(length),
        bb_mult: Some(bb_mult),
        kc_mult_high: Some(kc_mult_high),
        kc_mult_mid: Some(kc_mult_mid),
        kc_mult_low: Some(kc_mult_low),
    };

    let input = TtmSqueezeInput::from_slices(high, low, close, params);

    ttm_squeeze_into_slices(out_momentum, out_squeeze, &input, detect_best_kernel())
        .map_err(|e| JsValue::from_str(&e.to_string()))
}

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

#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
#[wasm_bindgen]
pub fn ttm_squeeze_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 = ttm_squeeze_into_ptrs)]
pub fn ttm_squeeze_into_js_ptrs(
    high: *const f64,
    low: *const f64,
    close: *const f64,
    out_momentum: *mut f64,
    out_squeeze: *mut f64,
    len: usize,
    length: usize,
    bb_mult: f64,
    kc_high: f64,
    kc_mid: f64,
    kc_low: f64,
) -> Result<(), JsValue> {
    if high.is_null()
        || low.is_null()
        || close.is_null()
        || out_momentum.is_null()
        || out_squeeze.is_null()
    {
        return Err(JsValue::from_str("null pointer"));
    }

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

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

    unsafe {
        let h = core::slice::from_raw_parts(high, len);
        let l = core::slice::from_raw_parts(low, len);
        let c = core::slice::from_raw_parts(close, len);

        let params = TtmSqueezeParams {
            length: Some(length),
            bb_mult: Some(bb_mult),
            kc_mult_high: Some(kc_high),
            kc_mult_mid: Some(kc_mid),
            kc_mult_low: Some(kc_low),
        };

        let input = TtmSqueezeInput::from_slices(h, l, c, params);
        let out = ttm_squeeze(&input).map_err(|e| JsValue::from_str(&e.to_string()))?;

        let dst_momentum = core::slice::from_raw_parts_mut(out_momentum, len);
        let dst_squeeze = core::slice::from_raw_parts_mut(out_squeeze, len);
        dst_momentum.copy_from_slice(&out.momentum);
        dst_squeeze.copy_from_slice(&out.squeeze);

        Ok(())
    }
}

use crate::utilities::helpers::{
    detect_best_batch_kernel, init_matrix_prefixes, make_uninit_matrix,
};

#[derive(Clone, Debug)]
pub struct TtmSqueezeBatchRange {
    pub length: (usize, usize, usize),
    pub bb_mult: (f64, f64, f64),
    pub kc_high: (f64, f64, f64),
    pub kc_mid: (f64, f64, f64),
    pub kc_low: (f64, f64, f64),
}

impl Default for TtmSqueezeBatchRange {
    fn default() -> Self {
        Self {
            length: (20, 269, 1),
            bb_mult: (2.0, 2.0, 0.0),
            kc_high: (1.0, 1.0, 0.0),
            kc_mid: (1.5, 1.5, 0.0),
            kc_low: (2.0, 2.0, 0.0),
        }
    }
}

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

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

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

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

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

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

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

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

    pub fn apply_candles(
        self,
        candles: &Candles,
    ) -> Result<TtmSqueezeBatchOutput, TtmSqueezeError> {
        ttm_squeeze_batch_with_kernel(
            &candles.high,
            &candles.low,
            &candles.close,
            &self.range,
            self.kernel,
        )
    }

    pub fn apply_slices(
        self,
        high: &[f64],
        low: &[f64],
        close: &[f64],
    ) -> Result<TtmSqueezeBatchOutput, TtmSqueezeError> {
        ttm_squeeze_batch_with_kernel(high, low, close, &self.range, self.kernel)
    }

    pub fn with_default_candles(
        candles: &Candles,
    ) -> Result<TtmSqueezeBatchOutput, TtmSqueezeError> {
        TtmSqueezeBatchBuilder::new()
            .kernel(Kernel::Auto)
            .apply_candles(candles)
    }
}

#[derive(Clone, Debug)]
pub struct TtmSqueezeBatchOutput {
    pub momentum: Vec<f64>,
    pub squeeze: Vec<f64>,
    pub combos: Vec<TtmSqueezeParams>,
    pub rows: usize,
    pub cols: usize,
}

impl TtmSqueezeBatchOutput {
    pub fn row_for_params(&self, p: &TtmSqueezeParams) -> Option<usize> {
        self.combos.iter().position(|q| {
            q.length.unwrap_or(20) == p.length.unwrap_or(20)
                && (q.bb_mult.unwrap_or(2.0) - p.bb_mult.unwrap_or(2.0)).abs() < 1e-12
                && (q.kc_mult_high.unwrap_or(1.0) - p.kc_mult_high.unwrap_or(1.0)).abs() < 1e-12
                && (q.kc_mult_mid.unwrap_or(1.5) - p.kc_mult_mid.unwrap_or(1.5)).abs() < 1e-12
                && (q.kc_mult_low.unwrap_or(2.0) - p.kc_mult_low.unwrap_or(2.0)).abs() < 1e-12
        })
    }

    pub fn momentum_for(&self, p: &TtmSqueezeParams) -> Option<&[f64]> {
        self.row_for_params(p).map(|r| {
            let s = r * self.cols;
            &self.momentum[s..s + self.cols]
        })
    }

    pub fn squeeze_for(&self, p: &TtmSqueezeParams) -> Option<&[f64]> {
        self.row_for_params(p).map(|r| {
            let s = r * self.cols;
            &self.squeeze[s..s + self.cols]
        })
    }
}

fn axis_usize(a: (usize, usize, usize)) -> Result<Vec<usize>, TtmSqueezeError> {
    let (start, end, step) = a;
    if step == 0 || start == end {
        return Ok(vec![start]);
    }

    let mut v = Vec::new();
    if start < end {
        let mut x = start;
        while x <= end {
            v.push(x);
            match x.checked_add(step) {
                Some(next) => {
                    if next == x {
                        break;
                    }
                    x = next;
                }
                None => break,
            }
        }
    } else {
        let mut x = start;
        loop {
            if x < end {
                break;
            }
            v.push(x);
            match x.checked_sub(step) {
                Some(next) => {
                    if next == x {
                        break;
                    }
                    x = next;
                }
                None => break,
            }
        }
    }

    if v.is_empty() {
        return Err(TtmSqueezeError::InvalidRange {
            start: start as f64,
            end: end as f64,
            step: step as f64,
        });
    }

    Ok(v)
}

fn axis_f64(a: (f64, f64, f64)) -> Result<Vec<f64>, TtmSqueezeError> {
    let (start, end, step) = a;
    let step_mag = step.abs();
    if step_mag < 1e-12 || (start - end).abs() < 1e-12 {
        return Ok(vec![start]);
    }

    let mut v = Vec::new();
    let mut x = start;
    if start <= end {
        while x <= end + 1e-12 {
            v.push(x);
            x += step_mag;
        }
    } else {
        while x >= end - 1e-12 {
            v.push(x);
            x -= step_mag;
        }
    }

    if v.is_empty() {
        return Err(TtmSqueezeError::InvalidRange { start, end, step });
    }

    Ok(v)
}

fn expand_grid_squeeze(r: &TtmSqueezeBatchRange) -> Result<Vec<TtmSqueezeParams>, TtmSqueezeError> {
    let lengths = axis_usize(r.length)?;
    let bb_mults = axis_f64(r.bb_mult)?;
    let kc_highs = axis_f64(r.kc_high)?;
    let kc_mids = axis_f64(r.kc_mid)?;
    let kc_lows = axis_f64(r.kc_low)?;

    let cap = lengths
        .len()
        .checked_mul(bb_mults.len())
        .and_then(|v| v.checked_mul(kc_highs.len()))
        .and_then(|v| v.checked_mul(kc_mids.len()))
        .and_then(|v| v.checked_mul(kc_lows.len()))
        .ok_or(TtmSqueezeError::InvalidRange {
            start: r.length.0 as f64,
            end: r.length.1 as f64,
            step: r.length.2 as f64,
        })?;

    if cap == 0 {
        return Err(TtmSqueezeError::InvalidRange {
            start: r.length.0 as f64,
            end: r.length.1 as f64,
            step: r.length.2 as f64,
        });
    }

    let mut out = Vec::with_capacity(cap);

    for &l in &lengths {
        for &bb in &bb_mults {
            for &h in &kc_highs {
                for &m in &kc_mids {
                    for &lo in &kc_lows {
                        out.push(TtmSqueezeParams {
                            length: Some(l),
                            bb_mult: Some(bb),
                            kc_mult_high: Some(h),
                            kc_mult_mid: Some(m),
                            kc_mult_low: Some(lo),
                        });
                    }
                }
            }
        }
    }

    Ok(out)
}

pub fn ttm_squeeze_batch_with_kernel(
    high: &[f64],
    low: &[f64],
    close: &[f64],
    sweep: &TtmSqueezeBatchRange,
    k: Kernel,
) -> Result<TtmSqueezeBatchOutput, TtmSqueezeError> {
    if high.len() != low.len() || low.len() != close.len() {
        return Err(TtmSqueezeError::InconsistentSliceLengths {
            high: high.len(),
            low: low.len(),
            close: close.len(),
        });
    }

    let combos = expand_grid_squeeze(sweep)?;
    let rows = combos.len();
    let cols = close.len();
    let _total = rows
        .checked_mul(cols)
        .ok_or(TtmSqueezeError::InvalidRange {
            start: sweep.length.0 as f64,
            end: sweep.length.1 as f64,
            step: sweep.length.2 as f64,
        })?;

    let first = close
        .iter()
        .position(|x| !x.is_nan())
        .ok_or(TtmSqueezeError::AllValuesNaN)?;
    let warmup_periods: Vec<usize> = combos
        .iter()
        .map(|c| first + c.length.unwrap() - 1)
        .collect();

    let mut mom_mu = make_uninit_matrix(rows, cols);
    let mut sqz_mu = make_uninit_matrix(rows, cols);
    init_matrix_prefixes(&mut mom_mu, cols, &warmup_periods);
    init_matrix_prefixes(&mut sqz_mu, cols, &warmup_periods);

    let mut mom_guard = core::mem::ManuallyDrop::new(mom_mu);
    let mut sqz_guard = core::mem::ManuallyDrop::new(sqz_mu);

    let mom_slice: &mut [f64] = unsafe {
        core::slice::from_raw_parts_mut(mom_guard.as_mut_ptr() as *mut f64, mom_guard.len())
    };
    let sqz_slice: &mut [f64] = unsafe {
        core::slice::from_raw_parts_mut(sqz_guard.as_mut_ptr() as *mut f64, sqz_guard.len())
    };

    let chosen_batch = match k {
        Kernel::Auto => detect_best_batch_kernel(),
        kb if kb.is_batch() => kb,
        other => {
            return Err(TtmSqueezeError::InvalidKernelForBatch(other));
        }
    };

    let row_kernel = match chosen_batch {
        Kernel::Avx512Batch => Kernel::Avx512,
        Kernel::Avx2Batch => Kernel::Avx2,
        Kernel::ScalarBatch => Kernel::Scalar,
        _ => unreachable!(),
    };

    if chosen_batch == Kernel::ScalarBatch {
        let mut lengths: Vec<usize> = Vec::new();
        for p in &combos {
            let l = p.length.unwrap_or(20);
            if !lengths.contains(&l) {
                lengths.push(l);
            }
        }

        for l in lengths {
            if l < 2 || first + l > cols {
                continue;
            }

            struct RowCfg {
                row: usize,
                bb_sq: f64,
                kc_low_sq: f64,
                kc_mid_sq: f64,
                kc_high_sq: f64,
            }
            let mut group: Vec<RowCfg> = Vec::new();
            for (idx, p) in combos.iter().enumerate() {
                if p.length.unwrap_or(20) == l {
                    let bb = p.bb_mult.unwrap_or(2.0);
                    let kh = p.kc_mult_high.unwrap_or(1.0);
                    let km = p.kc_mult_mid.unwrap_or(1.5);
                    let kl = p.kc_mult_low.unwrap_or(2.0);
                    group.push(RowCfg {
                        row: idx,
                        bb_sq: bb * bb,
                        kc_low_sq: kl * kl,
                        kc_mid_sq: km * km,
                        kc_high_sq: kh * kh,
                    });
                }
            }
            if group.is_empty() {
                continue;
            }

            let n = l as f64;
            let sx = 0.5 * n * (n - 1.0);
            let sx2 = (n - 1.0) * n * (2.0 * n - 1.0) / 6.0;
            let den = n * sx2 - sx * sx;
            let inv_den = 1.0 / den;
            let inv_n = 1.0 / n;
            let half_nm1 = 0.5 * (n - 1.0);

            let mut cbuf = vec![0.0f64; l];
            let mut trbuf = vec![0.0f64; l];
            let (mut cpos, mut trpos) = (0usize, 0usize);
            let mut sum0 = 0.0f64;
            let mut sum1 = 0.0f64;
            let mut sumsq = 0.0f64;
            let mut tr_sum = 0.0f64;

            let cap = l;
            let mut max_q = vec![0usize; cap];
            let mut min_q = vec![0usize; cap];
            let (mut max_head, mut max_tail, mut max_len) = (0usize, 0usize, 0usize);
            let (mut min_head, mut min_tail, mut min_len) = (0usize, 0usize, 0usize);

            let warm = first + l - 1;

            let mut r = 0usize;
            let mut i = first;
            while i <= warm {
                let c = close[i];
                cbuf[cpos] = c;
                sum0 += c;
                sumsq = c.mul_add(c, sumsq);
                sum1 += (r as f64) * c;

                let tr_val = if i == first {
                    high[i] - low[i]
                } else {
                    let pc = close[i - 1];
                    let hl = high[i] - low[i];
                    let hc = (high[i] - pc).abs();
                    let lc = (low[i] - pc).abs();
                    hl.max(hc).max(lc)
                };
                trbuf[trpos] = tr_val;
                tr_sum += tr_val;

                while max_len > 0 {
                    let back_pos = if max_tail == 0 { cap - 1 } else { max_tail - 1 };
                    let back_idx = max_q[back_pos];
                    if high[i] <= high[back_idx] {
                        break;
                    }
                    max_tail = back_pos;
                    max_len -= 1;
                }
                max_q[max_tail] = i;
                max_tail += 1;
                if max_tail == cap {
                    max_tail = 0;
                }
                max_len += 1;

                while min_len > 0 {
                    let back_pos = if min_tail == 0 { cap - 1 } else { min_tail - 1 };
                    let back_idx = min_q[back_pos];
                    if low[i] >= low[back_idx] {
                        break;
                    }
                    min_tail = back_pos;
                    min_len -= 1;
                }
                min_q[min_tail] = i;
                min_tail += 1;
                if min_tail == cap {
                    min_tail = 0;
                }
                min_len += 1;

                cpos += 1;
                if cpos == l {
                    cpos = 0;
                }
                trpos += 1;
                if trpos == l {
                    trpos = 0;
                }
                r += 1;
                i += 1;
            }

            let m = sum0 * inv_n;
            let var = (-m).mul_add(m, sumsq * inv_n);
            let var_pos = if var > 0.0 { var } else { 0.0 };
            let dkc = tr_sum * inv_n;
            let dkc2 = dkc * dkc;

            let hi_idx = max_q[max_head];
            let lo_idx = min_q[min_head];
            let highest = high[hi_idx];
            let lowest = low[lo_idx];
            let midpoint = 0.5 * (highest + lowest);
            let avg = 0.5 * (midpoint + m);
            let sy = sum0 - avg * n;
            let sxy = sum1 - avg * sx;
            let slope = n.mul_add(sxy, -(sx * sy)) * inv_den;
            let mom_val = sy * inv_n + slope * half_nm1;

            for rc in &group {
                let bbv = rc.bb_sq * var_pos;
                let t_low = rc.kc_low_sq * dkc2;
                let t_mid = rc.kc_mid_sq * dkc2;
                let t_high = rc.kc_high_sq * dkc2;
                let sqz = if bbv > t_low {
                    0.0
                } else if bbv <= t_high {
                    3.0
                } else if bbv <= t_mid {
                    2.0
                } else {
                    1.0
                };
                let s_off = rc.row * cols + warm;
                let m_off = rc.row * cols + warm;
                sqz_slice[s_off] = sqz;
                mom_slice[m_off] = mom_val;
            }

            let mut i = warm + 1;
            while i < cols {
                let start_idx = i + 1 - l;

                while max_len > 0 {
                    let front_idx = max_q[max_head];
                    if front_idx >= start_idx {
                        break;
                    }
                    max_head += 1;
                    if max_head == cap {
                        max_head = 0;
                    }
                    max_len -= 1;
                }
                while min_len > 0 {
                    let front_idx = min_q[min_head];
                    if front_idx >= start_idx {
                        break;
                    }
                    min_head += 1;
                    if min_head == cap {
                        min_head = 0;
                    }
                    min_len -= 1;
                }

                while max_len > 0 {
                    let back_pos = if max_tail == 0 { cap - 1 } else { max_tail - 1 };
                    let back_idx = max_q[back_pos];
                    if high[i] <= high[back_idx] {
                        break;
                    }
                    max_tail = back_pos;
                    max_len -= 1;
                }
                max_q[max_tail] = i;
                max_tail += 1;
                if max_tail == cap {
                    max_tail = 0;
                }
                max_len += 1;

                while min_len > 0 {
                    let back_pos = if min_tail == 0 { cap - 1 } else { min_tail - 1 };
                    let back_idx = min_q[back_pos];
                    if low[i] >= low[back_idx] {
                        break;
                    }
                    min_tail = back_pos;
                    min_len -= 1;
                }
                min_q[min_tail] = i;
                min_tail += 1;
                if min_tail == cap {
                    min_tail = 0;
                }
                min_len += 1;

                let old = cbuf[cpos];
                let new = close[i];
                let sum0_old = sum0;
                sum0 += new - old;
                sumsq = new.mul_add(new, sumsq - old * old);
                sum1 = sum1 - sum0_old + old + (n - 1.0) * new;
                cbuf[cpos] = new;
                cpos += 1;
                if cpos == l {
                    cpos = 0;
                }

                let old_tr = trbuf[trpos];
                let pc = close[i - 1];
                let hi_i = high[i];
                let lo_i = low[i];
                let hl = hi_i - lo_i;
                let hc = (hi_i - pc).abs();
                let lc = (lo_i - pc).abs();
                let tr_new = hl.max(hc).max(lc);
                tr_sum += tr_new - old_tr;
                trbuf[trpos] = tr_new;
                trpos += 1;
                if trpos == l {
                    trpos = 0;
                }

                let m = sum0 * inv_n;
                let var = (-m).mul_add(m, sumsq * inv_n);
                let var_pos = if var > 0.0 { var } else { 0.0 };
                let dkc = tr_sum * inv_n;
                let dkc2 = dkc * dkc;

                let hi_idx = max_q[max_head];
                let lo_idx = min_q[min_head];
                let highest = high[hi_idx];
                let lowest = low[lo_idx];
                let midpoint = 0.5 * (highest + lowest);
                let avg = 0.5 * (midpoint + m);
                let sy = sum0 - avg * n;
                let sxy = sum1 - avg * sx;
                let slope = n.mul_add(sxy, -(sx * sy)) * inv_den;
                let mom_val = sy * inv_n + slope * half_nm1;

                for rc in &group {
                    let bbv = rc.bb_sq * var_pos;
                    let t_low = rc.kc_low_sq * dkc2;
                    let t_mid = rc.kc_mid_sq * dkc2;
                    let t_high = rc.kc_high_sq * dkc2;
                    let sqz = if bbv > t_low {
                        0.0
                    } else if bbv <= t_high {
                        3.0
                    } else if bbv <= t_mid {
                        2.0
                    } else {
                        1.0
                    };
                    let s_off = rc.row * cols + i;
                    let m_off = rc.row * cols + i;
                    sqz_slice[s_off] = sqz;
                    mom_slice[m_off] = mom_val;
                }

                i += 1;
            }
        }
    } else {
        for (row, p) in combos.iter().enumerate() {
            let input = TtmSqueezeInput::from_slices(high, low, close, p.clone());
            let dst_m = &mut mom_slice[row * cols..(row + 1) * cols];
            let dst_s = &mut sqz_slice[row * cols..(row + 1) * cols];

            ttm_squeeze_into_slices(dst_m, dst_s, &input, row_kernel)?;
        }
    }

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

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

    Ok(TtmSqueezeBatchOutput {
        momentum,
        squeeze,
        combos,
        rows,
        cols,
    })
}

#[cfg(feature = "python")]
#[pyfunction(name = "ttm_squeeze_batch")]
#[pyo3(signature = (high, low, close, length_range, bb_mult_range, kc_high_range, kc_mid_range, kc_low_range, kernel=None))]
pub fn ttm_squeeze_batch_py<'py>(
    py: Python<'py>,
    high: numpy::PyReadonlyArray1<'py, f64>,
    low: numpy::PyReadonlyArray1<'py, f64>,
    close: numpy::PyReadonlyArray1<'py, f64>,
    length_range: (usize, usize, usize),
    bb_mult_range: (f64, f64, f64),
    kc_high_range: (f64, f64, f64),
    kc_mid_range: (f64, f64, f64),
    kc_low_range: (f64, f64, f64),
    kernel: Option<&str>,
) -> PyResult<Bound<'py, pyo3::types::PyDict>> {
    use numpy::{IntoPyArray, PyArray1, PyArrayMethods};

    let h = high.as_slice()?;
    let l = low.as_slice()?;
    let c = close.as_slice()?;

    let sweep = TtmSqueezeBatchRange {
        length: length_range,
        bb_mult: bb_mult_range,
        kc_high: kc_high_range,
        kc_mid: kc_mid_range,
        kc_low: kc_low_range,
    };

    let kern = validate_kernel(kernel, true)?;

    let out = py
        .allow_threads(|| ttm_squeeze_batch_with_kernel(h, l, c, &sweep, kern))
        .map_err(|e| PyValueError::new_err(e.to_string()))?;

    let rows = out.rows;
    let cols = out.cols;
    let dict = pyo3::types::PyDict::new(py);

    let mom = unsafe { PyArray1::<f64>::from_vec(py, out.momentum).reshape((rows, cols))? };
    let sqz = unsafe { PyArray1::<f64>::from_vec(py, out.squeeze).reshape((rows, cols))? };

    dict.set_item("momentum", mom)?;
    dict.set_item("squeeze", sqz)?;
    dict.set_item(
        "lengths",
        out.combos
            .iter()
            .map(|p| p.length.unwrap() as u64)
            .collect::<Vec<_>>()
            .into_pyarray(py),
    )?;
    dict.set_item(
        "bb_mults",
        out.combos
            .iter()
            .map(|p| p.bb_mult.unwrap())
            .collect::<Vec<_>>()
            .into_pyarray(py),
    )?;
    dict.set_item(
        "kc_highs",
        out.combos
            .iter()
            .map(|p| p.kc_mult_high.unwrap())
            .collect::<Vec<_>>()
            .into_pyarray(py),
    )?;
    dict.set_item(
        "kc_mids",
        out.combos
            .iter()
            .map(|p| p.kc_mult_mid.unwrap())
            .collect::<Vec<_>>()
            .into_pyarray(py),
    )?;
    dict.set_item(
        "kc_lows",
        out.combos
            .iter()
            .map(|p| p.kc_mult_low.unwrap())
            .collect::<Vec<_>>()
            .into_pyarray(py),
    )?;

    Ok(dict)
}

#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
#[derive(Serialize, Deserialize)]
pub struct TtmSqueezeBatchConfig {
    pub length_range: (usize, usize, usize),
    pub bb_mult_range: (f64, f64, f64),
    pub kc_high_range: (f64, f64, f64),
    pub kc_mid_range: (f64, f64, f64),
    pub kc_low_range: (f64, f64, f64),
}

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

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

    let sweep = TtmSqueezeBatchRange {
        length: cfg.length_range,
        bb_mult: cfg.bb_mult_range,
        kc_high: cfg.kc_high_range,
        kc_mid: cfg.kc_mid_range,
        kc_low: cfg.kc_low_range,
    };

    let out = ttm_squeeze_batch_with_kernel(high, low, close, &sweep, detect_best_batch_kernel())
        .map_err(|e| JsValue::from_str(&e.to_string()))?;

    let mut values = Vec::with_capacity(2 * out.rows * out.cols);
    for r in 0..out.rows {
        let s = r * out.cols;
        values.extend_from_slice(&out.momentum[s..s + out.cols]);
        values.extend_from_slice(&out.squeeze[s..s + out.cols]);
    }

    let js = TtmSqueezeBatchJsOutput {
        values,
        rows: out.rows * 2,
        cols: out.cols,
        combos: out.combos,
    };

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

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

    macro_rules! skip_if_unsupported {
        ($kernel:expr, $test_name:expr) => {
            #[cfg(not(all(feature = "nightly-avx", target_arch = "x86_64")))]
            {
                if matches!(
                    $kernel,
                    Kernel::Avx2 | Kernel::Avx512 | Kernel::Avx2Batch | Kernel::Avx512Batch
                ) {
                    eprintln!("Skipping {} - AVX not supported", $test_name);
                    return Ok(());
                }
            }
        };
    }

    fn check_ttm_squeeze_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 = TtmSqueezeInput::with_default_candles(&candles);
        let result = ttm_squeeze_with_kernel(&input, kernel)?;

        assert_eq!(result.momentum.len(), candles.close.len());
        assert_eq!(result.squeeze.len(), candles.close.len());

        let expected_momentum = [
            -167.98676428571423,
            -154.99159285714336,
            -148.98427857142892,
            -131.80910714285744,
            -89.35822142857162,
        ];

        let expected_squeeze = [0.0, 0.0, 0.0, 0.0, 1.0];

        let warmup_period = 19;

        for (i, &expected) in expected_momentum.iter().enumerate() {
            let actual = result.momentum[warmup_period + i];
            let diff = (actual - expected).abs();
            assert!(
                diff < 0.0001,
                "[{}] Momentum at index {}: expected {}, got {}, diff: {}",
                test_name,
                i,
                expected,
                actual,
                diff
            );
        }

        for (i, &expected) in expected_squeeze.iter().enumerate() {
            let actual = result.squeeze[warmup_period + i];
            assert_eq!(
                actual, expected,
                "[{}] Squeeze mismatch at index {}: expected {}, got {}",
                test_name, i, expected, actual
            );
        }

        let first_valid_momentum = result.momentum.iter().position(|&x| !x.is_nan());
        let first_valid_squeeze = result.squeeze.iter().position(|&x| !x.is_nan());

        assert!(
            first_valid_momentum.is_some(),
            "[{}] No valid momentum values found",
            test_name
        );
        assert!(
            first_valid_squeeze.is_some(),
            "[{}] No valid squeeze values found",
            test_name
        );

        if let Some(first_mom) = first_valid_momentum {
            for i in 0..first_mom.min(10) {
                assert!(
                    result.momentum[i].is_nan(),
                    "[{}] Expected NaN at index {}",
                    test_name,
                    i
                );
            }
        }

        if let Some(first_sqz) = first_valid_squeeze {
            for i in 0..first_sqz.min(10) {
                assert!(
                    result.squeeze[i].is_nan(),
                    "[{}] Expected NaN at index {}",
                    test_name,
                    i
                );
            }
        }

        Ok(())
    }

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

        let params = TtmSqueezeParams {
            length: None,
            bb_mult: None,
            kc_mult_high: None,
            kc_mult_mid: None,
            kc_mult_low: None,
        };

        let input = TtmSqueezeInput::from_candles(&candles, params);
        let result = ttm_squeeze_with_kernel(&input, kernel)?;

        assert_eq!(result.momentum.len(), candles.close.len());
        assert_eq!(result.squeeze.len(), candles.close.len());

        Ok(())
    }

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

        let input = TtmSqueezeInput::with_default_candles(&candles);
        let result = ttm_squeeze_with_kernel(&input, kernel)?;

        assert_eq!(result.momentum.len(), candles.close.len());
        assert_eq!(result.squeeze.len(), candles.close.len());

        Ok(())
    }

    fn check_ttm_squeeze_zero_period(
        test_name: &str,
        kernel: Kernel,
    ) -> Result<(), Box<dyn Error>> {
        skip_if_unsupported!(kernel, test_name);
        let data = vec![1.0, 2.0, 3.0, 4.0, 5.0];
        let params = TtmSqueezeParams {
            length: Some(0),
            bb_mult: None,
            kc_mult_high: None,
            kc_mult_mid: None,
            kc_mult_low: None,
        };

        let input = TtmSqueezeInput::from_slices(&data, &data, &data, params);
        let result = ttm_squeeze_with_kernel(&input, kernel);

        assert!(
            result.is_err(),
            "[{}] Should fail with zero period",
            test_name
        );
        Ok(())
    }

    fn check_ttm_squeeze_period_exceeds_length(
        test_name: &str,
        kernel: Kernel,
    ) -> Result<(), Box<dyn Error>> {
        skip_if_unsupported!(kernel, test_name);
        let data = vec![1.0, 2.0, 3.0];
        let params = TtmSqueezeParams {
            length: Some(10),
            bb_mult: None,
            kc_mult_high: None,
            kc_mult_mid: None,
            kc_mult_low: None,
        };

        let input = TtmSqueezeInput::from_slices(&data, &data, &data, params);
        let result = ttm_squeeze_with_kernel(&input, kernel);

        assert!(
            result.is_err(),
            "[{}] Should fail when period exceeds length",
            test_name
        );
        Ok(())
    }

    fn check_ttm_squeeze_very_small_dataset(
        test_name: &str,
        kernel: Kernel,
    ) -> Result<(), Box<dyn Error>> {
        skip_if_unsupported!(kernel, test_name);
        let data = vec![42.0];
        let params = TtmSqueezeParams::default();

        let input = TtmSqueezeInput::from_slices(&data, &data, &data, params);
        let result = ttm_squeeze_with_kernel(&input, kernel);

        assert!(
            result.is_err(),
            "[{}] Should fail with very small dataset",
            test_name
        );
        Ok(())
    }

    fn check_ttm_squeeze_empty_input(
        test_name: &str,
        kernel: Kernel,
    ) -> Result<(), Box<dyn Error>> {
        skip_if_unsupported!(kernel, test_name);
        let empty_data: Vec<f64> = vec![];
        let params = TtmSqueezeParams::default();

        let input = TtmSqueezeInput::from_slices(&empty_data, &empty_data, &empty_data, params);
        let result = ttm_squeeze_with_kernel(&input, kernel);

        assert!(
            result.is_err(),
            "[{}] Should fail with empty input",
            test_name
        );
        Ok(())
    }

    fn check_ttm_squeeze_all_nan(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
        skip_if_unsupported!(kernel, test_name);
        let nan_data = vec![f64::NAN; 50];
        let params = TtmSqueezeParams::default();

        let input = TtmSqueezeInput::from_slices(&nan_data, &nan_data, &nan_data, params);
        let result = ttm_squeeze_with_kernel(&input, kernel);

        assert!(
            result.is_err(),
            "[{}] Should fail with all NaN values",
            test_name
        );
        Ok(())
    }

    fn check_ttm_squeeze_inconsistent_slices(
        test_name: &str,
        kernel: Kernel,
    ) -> Result<(), Box<dyn Error>> {
        skip_if_unsupported!(kernel, test_name);
        let high = vec![1.0; 10];
        let low = vec![0.9; 10];
        let close = vec![0.95; 5];
        let params = TtmSqueezeParams::default();

        let input = TtmSqueezeInput::from_slices(&high, &low, &close, params);
        let result = ttm_squeeze_with_kernel(&input, kernel);

        assert!(
            result.is_err(),
            "[{}] Should fail with inconsistent slice lengths",
            test_name
        );
        Ok(())
    }

    fn check_ttm_squeeze_nan_handling(
        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 = TtmSqueezeInput::with_default_candles(&candles);
        let result = ttm_squeeze_with_kernel(&input, kernel)?;

        assert_eq!(result.momentum.len(), candles.close.len());
        assert_eq!(result.squeeze.len(), candles.close.len());

        if result.momentum.len() > 40 {
            for i in 40..result.momentum.len() {
                assert!(
                    !result.momentum[i].is_nan(),
                    "[{}] Unexpected NaN in momentum at {}",
                    test_name,
                    i
                );
                assert!(
                    !result.squeeze[i].is_nan(),
                    "[{}] Unexpected NaN in squeeze at {}",
                    test_name,
                    i
                );
            }
        }

        Ok(())
    }

    fn check_ttm_squeeze_builder(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 result = TtmSqueezeBuilder::new()
            .length(30)
            .bb_mult(2.5)
            .kc_mult_high(1.2)
            .kc_mult_mid(1.8)
            .kc_mult_low(2.5)
            .kernel(kernel)
            .apply(&candles)?;

        assert_eq!(result.momentum.len(), candles.close.len());
        assert_eq!(result.squeeze.len(), candles.close.len());

        Ok(())
    }

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

        let params = TtmSqueezeParams::default();
        let mut stream = TtmSqueezeStream::try_new(params.clone())?;

        let input = TtmSqueezeInput::from_candles(&candles, params);
        let batch_result = ttm_squeeze_with_kernel(&input, kernel)?;

        let mut stream_momentum = Vec::new();
        let mut stream_squeeze = Vec::new();

        for i in 0..candles.close.len().min(100) {
            if let Some((mom, sqz)) =
                stream.update(candles.high[i], candles.low[i], candles.close[i])
            {
                stream_momentum.push(mom);
                stream_squeeze.push(sqz);
            }
        }

        assert!(
            !stream_momentum.is_empty(),
            "[{}] Stream should produce values",
            test_name
        );

        Ok(())
    }

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

        let test_params = vec![
            TtmSqueezeParams::default(),
            TtmSqueezeParams {
                length: Some(10),
                bb_mult: Some(1.5),
                kc_mult_high: Some(0.8),
                kc_mult_mid: Some(1.2),
                kc_mult_low: Some(1.8),
            },
            TtmSqueezeParams {
                length: Some(30),
                bb_mult: Some(3.0),
                kc_mult_high: Some(1.5),
                kc_mult_mid: Some(2.0),
                kc_mult_low: Some(2.5),
            },
        ];

        for params in test_params {
            let input = TtmSqueezeInput::from_candles(&candles, params);
            let output = ttm_squeeze_with_kernel(&input, kernel)?;

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

                let bits = val.to_bits();
                assert!(
                    bits != 0x11111111_11111111
                        && bits != 0x22222222_22222222
                        && bits != 0x33333333_33333333,
                    "[{}] Found poison value in momentum at {}: 0x{:016X}",
                    test_name,
                    i,
                    bits
                );
            }

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

                let bits = val.to_bits();
                assert!(
                    bits != 0x11111111_11111111
                        && bits != 0x22222222_22222222
                        && bits != 0x33333333_33333333,
                    "[{}] Found poison value in squeeze at {}: 0x{:016X}",
                    test_name,
                    i,
                    bits
                );
            }
        }

        Ok(())
    }

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

    fn check_batch_default_row(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
        skip_if_unsupported!(kernel, test_name);
        let candles = read_candles_from_csv("src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv")?;
        let out = TtmSqueezeBatchBuilder::new()
            .kernel(kernel)
            .apply_candles(&candles)?;
        let def = TtmSqueezeParams::default();
        let row_m = out.momentum_for(&def).expect("default row missing");
        let row_s = out.squeeze_for(&def).expect("default row missing");
        assert_eq!(row_m.len(), candles.close.len());
        assert_eq!(row_s.len(), candles.close.len());
        Ok(())
    }

    fn check_batch_sweep_count(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
        skip_if_unsupported!(kernel, test_name);
        let candles = read_candles_from_csv("src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv")?;
        let out = TtmSqueezeBatchBuilder::new()
            .kernel(kernel)
            .length_range(20, 24, 1)
            .bb_mult_range(2.0, 2.0, 0.0)
            .kc_high_range(1.0, 1.2, 0.1)
            .kc_mid_range(1.5, 1.7, 0.1)
            .kc_low_range(2.0, 2.2, 0.1)
            .apply_candles(&candles)?;
        assert_eq!(out.rows, 5 * 1 * 3 * 3 * 3);
        assert_eq!(out.cols, candles.close.len());
        Ok(())
    }

    macro_rules! generate_ttm_squeeze_tests {
        ($($test_fn:ident),*) => {
            paste::paste! {
                $(
                    #[test]
                    fn [<$test_fn _scalar>]() {
                        let _ = $test_fn(stringify!([<$test_fn _scalar>]), Kernel::Scalar);
                    }
                )*

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

                    #[test]
                    fn [<$test_fn _avx512>]() {
                        let _ = $test_fn(stringify!([<$test_fn _avx512>]), Kernel::Avx512);
                    }
                )*
            }
        };
    }

    generate_ttm_squeeze_tests!(
        check_ttm_squeeze_accuracy,
        check_ttm_squeeze_partial_params,
        check_ttm_squeeze_default_candles,
        check_ttm_squeeze_zero_period,
        check_ttm_squeeze_period_exceeds_length,
        check_ttm_squeeze_very_small_dataset,
        check_ttm_squeeze_empty_input,
        check_ttm_squeeze_all_nan,
        check_ttm_squeeze_inconsistent_slices,
        check_ttm_squeeze_nan_handling,
        check_ttm_squeeze_builder,
        check_ttm_squeeze_streaming,
        check_ttm_squeeze_no_poison
    );

    macro_rules! gen_batch_tests {
        ($f:ident) => {
            paste::paste! {
                #[test]
                fn [<$f _scalar>]() {
                    let _ = $f(stringify!([<$f _scalar>]), Kernel::ScalarBatch);
                }

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

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

                #[test]
                fn [<$f _auto>]() {
                    let _ = $f(stringify!([<$f _auto>]), Kernel::Auto);
                }
            }
        };
    }

    gen_batch_tests!(check_batch_default_row);
    gen_batch_tests!(check_batch_sweep_count);

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

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

        let input = TtmSqueezeInput::with_default_candles(&candles);

        let baseline = ttm_squeeze(&input)?;

        let len = candles.close.len();
        let mut mom_out = vec![0.0f64; len];
        let mut sqz_out = vec![0.0f64; len];
        ttm_squeeze_into(&mut mom_out, &mut sqz_out, &input, Kernel::Auto)?;

        assert_eq!(baseline.momentum.len(), len);
        assert_eq!(baseline.squeeze.len(), len);

        for i in 0..len {
            assert!(
                eq_or_both_nan_eps(baseline.momentum[i], mom_out[i], 1e-7),
                "Momentum mismatch at {}: baseline={} into={}",
                i,
                baseline.momentum[i],
                mom_out[i]
            );
            assert!(
                eq_or_both_nan_eps(baseline.squeeze[i], sqz_out[i], 1e-7),
                "Squeeze mismatch at {}: baseline={} into={}",
                i,
                baseline.squeeze[i],
                sqz_out[i]
            );
        }

        Ok(())
    }

    #[test]
    fn ttm_squeeze_scalar_batch_matches_single_scalar_on_dispatch_fixture(
    ) -> Result<(), Box<dyn Error>> {
        let len = 192usize;
        let open: Vec<f64> = (0..len)
            .map(|i| 100.0f64 + (i as f64 * 0.1) + ((i as f64) * 0.03).sin())
            .collect();
        let high: Vec<f64> = open
            .iter()
            .enumerate()
            .map(|(i, v)| v + 0.8 + ((i as f64) * 0.02).cos().abs() * 0.3)
            .collect();
        let low: Vec<f64> = open
            .iter()
            .enumerate()
            .map(|(i, v)| v - 0.8 - ((i as f64) * 0.02).sin().abs() * 0.3)
            .collect();
        let close: Vec<f64> = open
            .iter()
            .enumerate()
            .map(|(i, v)| v + ((i as f64) * 0.05).sin() * 0.4)
            .collect();

        let params = TtmSqueezeParams {
            length: Some(20),
            bb_mult: Some(2.0),
            kc_mult_high: Some(1.0),
            kc_mult_mid: Some(1.5),
            kc_mult_low: Some(2.0),
        };
        let single = ttm_squeeze_with_kernel(
            &TtmSqueezeInput::from_slices(&high, &low, &close, params.clone()),
            Kernel::Scalar,
        )?;
        let batch = ttm_squeeze_batch_with_kernel(
            &high,
            &low,
            &close,
            &TtmSqueezeBatchRange {
                length: (20, 20, 0),
                bb_mult: (2.0, 2.0, 0.0),
                kc_high: (1.0, 1.0, 0.0),
                kc_mid: (1.5, 1.5, 0.0),
                kc_low: (2.0, 2.0, 0.0),
            },
            Kernel::ScalarBatch,
        )?;

        let row = batch
            .momentum_for(&params)
            .expect("default batch row should exist");
        let mut max_diff = 0.0f64;
        let mut worst = None;
        for (idx, (&lhs, &rhs)) in single.momentum.iter().zip(row.iter()).enumerate() {
            if lhs.is_nan() && rhs.is_nan() {
                continue;
            }
            let diff = (lhs - rhs).abs();
            if diff > max_diff {
                max_diff = diff;
                worst = Some((idx, lhs, rhs));
            }
        }
        if let Some((idx, lhs, rhs)) = worst {
            assert!(
                max_diff < 1e-6,
                "worst momentum mismatch at {idx}: lhs={lhs} rhs={rhs} diff={max_diff}"
            );
        }

        Ok(())
    }
}
