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

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

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

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

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

use std::convert::AsRef;
use std::error::Error;
use std::f64::consts::PI;
use std::mem::MaybeUninit;
use thiserror::Error;

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

#[derive(Debug, Clone)]
pub struct LpcOutput {
    pub filter: Vec<f64>,
    pub high_band: Vec<f64>,
    pub low_band: Vec<f64>,
}

#[derive(Debug, Clone)]
#[cfg_attr(
    all(target_arch = "wasm32", feature = "wasm"),
    derive(Serialize, Deserialize)
)]
pub struct LpcParams {
    pub cutoff_type: Option<String>,
    pub fixed_period: Option<usize>,
    pub max_cycle_limit: Option<usize>,
    pub cycle_mult: Option<f64>,
    pub tr_mult: Option<f64>,
}

impl Default for LpcParams {
    fn default() -> Self {
        Self {
            cutoff_type: Some("adaptive".to_string()),
            fixed_period: Some(20),
            max_cycle_limit: Some(60),
            cycle_mult: Some(1.0),
            tr_mult: Some(1.0),
        }
    }
}

#[derive(Debug, Clone)]
pub struct LpcInput<'a> {
    pub data: LpcData<'a>,
    pub params: LpcParams,
}

impl<'a> AsRef<[f64]> for LpcInput<'a> {
    fn as_ref(&self) -> &[f64] {
        match &self.data {
            LpcData::Candles { candles, source } => source_type(candles, source),
            LpcData::Slices { src, .. } => src,
        }
    }
}

impl<'a> LpcInput<'a> {
    #[inline]
    pub fn from_candles(c: &'a Candles, s: &'a str, p: LpcParams) -> Self {
        Self {
            data: LpcData::Candles {
                candles: c,
                source: s,
            },
            params: p,
        }
    }

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

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

    #[inline]
    pub fn get_cutoff_type(&self) -> String {
        self.params
            .cutoff_type
            .clone()
            .unwrap_or_else(|| "adaptive".to_string())
    }

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

    #[inline]
    pub fn get_max_cycle_limit(&self) -> usize {
        self.params.max_cycle_limit.unwrap_or(60)
    }

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

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

#[derive(Clone, Debug)]
pub struct LpcBuilder {
    cutoff_type: Option<String>,
    fixed_period: Option<usize>,
    max_cycle_limit: Option<usize>,
    cycle_mult: Option<f64>,
    tr_mult: Option<f64>,
    kernel: Kernel,
}

impl Default for LpcBuilder {
    fn default() -> Self {
        Self {
            cutoff_type: None,
            fixed_period: None,
            max_cycle_limit: None,
            cycle_mult: None,
            tr_mult: None,
            kernel: Kernel::Auto,
        }
    }
}

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

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

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

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

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

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

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

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

    #[inline(always)]
    pub fn apply_candles(self, c: &Candles, s: &str) -> Result<LpcOutput, LpcError> {
        let p = LpcParams {
            cutoff_type: self.cutoff_type,
            fixed_period: self.fixed_period,
            max_cycle_limit: self.max_cycle_limit,
            cycle_mult: self.cycle_mult,
            tr_mult: self.tr_mult,
        };
        let i = LpcInput::from_candles(c, s, p);
        lpc_with_kernel(&i, self.kernel)
    }

    #[inline(always)]
    pub fn apply_slices(
        self,
        high: &[f64],
        low: &[f64],
        close: &[f64],
        src: &[f64],
    ) -> Result<LpcOutput, LpcError> {
        let p = LpcParams {
            cutoff_type: self.cutoff_type,
            fixed_period: self.fixed_period,
            max_cycle_limit: self.max_cycle_limit,
            cycle_mult: self.cycle_mult,
            tr_mult: self.tr_mult,
        };
        let i = LpcInput::from_slices(high, low, close, src, p);
        lpc_with_kernel(&i, self.kernel)
    }

    #[inline(always)]
    pub fn apply_slice(
        self,
        high: &[f64],
        low: &[f64],
        close: &[f64],
        src: &[f64],
    ) -> Result<LpcOutput, LpcError> {
        self.apply_slices(high, low, close, src)
    }

    #[inline(always)]
    pub fn into_stream(self) -> Result<LpcStream, LpcError> {
        let p = LpcParams {
            cutoff_type: self.cutoff_type,
            fixed_period: self.fixed_period,
            max_cycle_limit: self.max_cycle_limit,
            cycle_mult: self.cycle_mult,
            tr_mult: self.tr_mult,
        };
        LpcStream::try_new(p)
    }
}

#[derive(Clone, Debug)]
pub struct LpcBatchRange {
    pub fixed_period: (usize, usize, usize),
    pub cycle_mult: (f64, f64, f64),
    pub tr_mult: (f64, f64, f64),
    pub cutoff_type: String,
    pub max_cycle_limit: usize,
}

impl Default for LpcBatchRange {
    fn default() -> Self {
        Self {
            fixed_period: (20, 269, 1),
            cycle_mult: (1.0, 1.0, 0.0),
            tr_mult: (1.0, 1.0, 0.0),
            cutoff_type: "adaptive".to_string(),
            max_cycle_limit: 60,
        }
    }
}

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

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

    pub fn fixed_period_range(mut self, s: usize, e: usize, st: usize) -> Self {
        self.range.fixed_period = (s, e, st);
        self
    }
    pub fn fixed_period_static(mut self, p: usize) -> Self {
        self.range.fixed_period = (p, p, 0);
        self
    }

    pub fn cycle_mult_range(mut self, s: f64, e: f64, st: f64) -> Self {
        self.range.cycle_mult = (s, e, st);
        self
    }
    pub fn cycle_mult_static(mut self, x: f64) -> Self {
        self.range.cycle_mult = (x, x, 0.0);
        self
    }

    pub fn tr_mult_range(mut self, s: f64, e: f64, st: f64) -> Self {
        self.range.tr_mult = (s, e, st);
        self
    }
    pub fn tr_mult_static(mut self, x: f64) -> Self {
        self.range.tr_mult = (x, x, 0.0);
        self
    }

    pub fn cutoff_type(mut self, ct: &str) -> Self {
        self.range.cutoff_type = ct.to_string();
        self
    }
    pub fn max_cycle_limit(mut self, m: usize) -> Self {
        self.range.max_cycle_limit = m;
        self
    }

    pub fn apply_slices(
        self,
        h: &[f64],
        l: &[f64],
        c: &[f64],
        s: &[f64],
    ) -> Result<LpcBatchOutput, LpcError> {
        lpc_batch_with_kernel(h, l, c, s, &self.range, self.kernel)
    }
}

#[derive(Clone, Debug)]
pub struct LpcBatchOutput {
    pub values: Vec<f64>,
    pub combos: Vec<LpcParams>,
    pub rows: usize,
    pub cols: usize,
}

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

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

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

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

    #[error("lpc: Invalid cutoff type: {cutoff_type}, must be 'adaptive' or 'fixed'")]
    InvalidCutoffType { cutoff_type: String },

    #[error("lpc: Required OHLC data is missing or has mismatched lengths")]
    MissingData,

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

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

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

pub(crate) fn dom_cycle(src: &[f64], max_cycle_limit: usize) -> Vec<f64> {
    let len = src.len();
    let mut dom_cycles = vec![f64::NAN; len];

    if len < 8 {
        return dom_cycles;
    }

    let mut in_phase = vec![0.0; len];
    let mut quadrature = vec![0.0; len];
    let mut real_part = vec![0.0; len];
    let mut imag_part = vec![0.0; len];
    let mut delta_phase = vec![0.0; len];
    let mut inst_per = vec![0.0; len];

    for i in 7..len {
        let val1 = src[i] - src[i - 7];

        if i >= 4 {
            let val1_4 = if i >= 4 {
                src[i - 4] - src[i.saturating_sub(11)]
            } else {
                0.0
            };
            let val1_2 = if i >= 2 {
                src[i - 2] - src[i.saturating_sub(9)]
            } else {
                0.0
            };
            in_phase[i] = 1.25 * (val1_4 - 0.635 * val1_2)
                + if i >= 3 { 0.635 * in_phase[i - 3] } else { 0.0 };
        }

        if i >= 2 {
            let val1_2 = src[i - 2] - src[i.saturating_sub(9)];
            quadrature[i] = val1_2 - 0.338 * val1
                + if i >= 2 {
                    0.338 * quadrature[i - 2]
                } else {
                    0.0
                };
        }

        if i >= 1 {
            real_part[i] = 0.2
                * (in_phase[i] * in_phase[i - 1] + quadrature[i] * quadrature[i - 1])
                + 0.8 * real_part[i - 1];
            imag_part[i] = 0.2
                * (in_phase[i] * quadrature[i - 1] - in_phase[i - 1] * quadrature[i])
                + 0.8 * imag_part[i - 1];
        }

        if real_part[i] != 0.0 {
            delta_phase[i] = (imag_part[i] / real_part[i]).atan();
        }

        let mut val2 = 0.0;
        let mut found_period = false;
        for j in 0..=max_cycle_limit.min(i) {
            if i >= j {
                val2 += delta_phase[i - j];
                if val2 > 2.0 * PI && !found_period {
                    inst_per[i] = j as f64;
                    found_period = true;
                    break;
                }
            }
        }

        if !found_period {
            inst_per[i] = if i > 0 { inst_per[i - 1] } else { 20.0 };
        }

        if i > 0 && !dom_cycles[i - 1].is_nan() {
            dom_cycles[i] = 0.25 * inst_per[i] + 0.75 * dom_cycles[i - 1];
        } else {
            dom_cycles[i] = inst_per[i];
        }
    }

    dom_cycles
}

fn lp_filter(src: &[f64], period: usize) -> Vec<f64> {
    let len = src.len();
    let mut output = vec![f64::NAN; len];

    if period == 0 || len == 0 {
        return output;
    }

    let omega = 2.0 * PI / (period as f64);
    let alpha = (1.0 - omega.sin()) / omega.cos();

    if !src[0].is_nan() {
        output[0] = src[0];
    }

    for i in 1..len {
        if !src[i].is_nan() && !src[i - 1].is_nan() && !output[i - 1].is_nan() {
            output[i] = 0.5 * (1.0 - alpha) * (src[i] + src[i - 1]) + alpha * output[i - 1];
        } else if !src[i].is_nan() {
            output[i] = src[i];
        }
    }

    output
}

fn calculate_true_range(high: &[f64], low: &[f64], close: &[f64]) -> Vec<f64> {
    let len = high.len();
    let mut tr = vec![0.0; len];

    if len == 0 {
        return tr;
    }

    tr[0] = high[0] - low[0];

    for i in 1..len {
        let hl = high[i] - low[i];
        let c_low1 = (close[i] - low[i - 1]).abs();
        let c_high1 = (close[i] - high[i - 1]).abs();
        tr[i] = hl.max(c_low1).max(c_high1);
    }

    tr
}

#[inline(always)]

pub fn lpc_scalar(
    high: &[f64],
    low: &[f64],
    close: &[f64],
    src: &[f64],
    cutoff_type: &str,
    fixed_period: usize,
    max_cycle_limit: usize,
    cycle_mult: f64,
    tr_mult: f64,
    first: usize,
    out_filter: &mut [f64],
    out_high: &mut [f64],
    out_low: &mut [f64],
) {
    let len = src.len();

    if first > 0 {
        out_filter[..first].fill(f64::NAN);
        out_high[..first].fill(f64::NAN);
        out_low[..first].fill(f64::NAN);
    }

    let dc = if cutoff_type.eq_ignore_ascii_case("adaptive") {
        Some(dom_cycle(src, max_cycle_limit))
    } else {
        None
    };

    if first >= len {
        return;
    }

    out_filter[first] = src[first];
    let mut tr_prev = high[first] - low[first];
    let mut ftr_prev = tr_prev;
    let tm = tr_mult;

    out_high[first] = out_filter[first] + tr_prev * tm;
    out_low[first] = out_filter[first] - tr_prev * tm;

    #[inline(always)]
    fn alpha_from_period(p: usize) -> f64 {
        let omega = 2.0 * std::f64::consts::PI / (p as f64);
        let (s, c) = omega.sin_cos();
        (1.0 - s) / c
    }
    #[inline(always)]
    fn per_bar_period(dc_opt: Option<&[f64]>, idx: usize, fixed_p: usize, cm: f64) -> usize {
        if let Some(dc) = dc_opt {
            let base = dc[idx];
            if base.is_nan() {
                fixed_p
            } else {
                (base * cm).round().max(3.0) as usize
            }
        } else {
            fixed_p
        }
    }

    let mut last_p: usize = if dc.is_none() { fixed_period } else { 0 };
    let mut alpha: f64 = if dc.is_none() {
        alpha_from_period(fixed_period)
    } else {
        0.0
    };

    let mut i = first + 1;
    while i + 1 < len {
        let p_i = per_bar_period(dc.as_deref(), i, fixed_period, cycle_mult);
        if p_i != last_p {
            last_p = p_i;
            alpha = alpha_from_period(last_p);
        }
        let one_m_a = 1.0 - alpha;
        let s_im1 = src[i - 1];
        let s_i = src[i];
        let prev_f = out_filter[i - 1];
        let f_i = alpha.mul_add(prev_f, 0.5 * one_m_a * (s_i + s_im1));
        out_filter[i] = f_i;

        let hl = high[i] - low[i];
        let c_low1 = (close[i] - low[i - 1]).abs();
        let c_hi1 = (close[i] - high[i - 1]).abs();
        let tr_i = hl.max(c_low1).max(c_hi1);
        let ftr_i = alpha.mul_add(ftr_prev, 0.5 * one_m_a * (tr_i + tr_prev));
        tr_prev = tr_i;
        ftr_prev = ftr_i;
        out_high[i] = f_i + ftr_i * tm;
        out_low[i] = f_i - ftr_i * tm;

        let i1 = i + 1;
        let p_i1 = per_bar_period(dc.as_deref(), i1, fixed_period, cycle_mult);
        if p_i1 != last_p {
            last_p = p_i1;
            alpha = alpha_from_period(last_p);
        }
        let one_m_a1 = 1.0 - alpha;
        let s_i1 = src[i1];
        let f_i1 = alpha.mul_add(f_i, 0.5 * one_m_a1 * (s_i1 + s_i));
        out_filter[i1] = f_i1;

        let hl1 = high[i1] - low[i1];
        let c_low1b = (close[i1] - low[i1 - 1]).abs();
        let c_hi1b = (close[i1] - high[i1 - 1]).abs();
        let tr_i1 = hl1.max(c_low1b).max(c_hi1b);
        let ftr_i1 = alpha.mul_add(ftr_prev, 0.5 * one_m_a1 * (tr_i1 + tr_prev));
        tr_prev = tr_i1;
        ftr_prev = ftr_i1;
        out_high[i1] = f_i1 + ftr_i1 * tm;
        out_low[i1] = f_i1 - ftr_i1 * tm;

        i += 2;
    }

    if i < len {
        let p_i = per_bar_period(dc.as_deref(), i, fixed_period, cycle_mult);
        if p_i != last_p {
            last_p = p_i;
            alpha = alpha_from_period(last_p);
        }
        let one_m_a = 1.0 - alpha;
        let s_im1 = src[i - 1];
        let s_i = src[i];
        let prev_f = out_filter[i - 1];
        let f_i = alpha.mul_add(prev_f, 0.5 * one_m_a * (s_i + s_im1));
        out_filter[i] = f_i;

        let hl = high[i] - low[i];
        let c_low1 = (close[i] - low[i - 1]).abs();
        let c_hi1 = (close[i] - high[i - 1]).abs();
        let tr_i = hl.max(c_low1).max(c_hi1);
        let ftr_i = alpha.mul_add(ftr_prev, 0.5 * one_m_a * (tr_i + tr_prev));
        out_high[i] = f_i + ftr_i * tm;
        out_low[i] = f_i - ftr_i * tm;
    }
}

#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
pub fn lpc_avx2(
    high: &[f64],
    low: &[f64],
    close: &[f64],
    src: &[f64],
    cutoff_type: &str,
    fixed_period: usize,
    max_cycle_limit: usize,
    cycle_mult: f64,
    tr_mult: f64,
    first: usize,
    out_filter: &mut [f64],
    out_high: &mut [f64],
    out_low: &mut [f64],
) {
    unsafe {
        if src.len() > first + 32 {
            _mm_prefetch(src.as_ptr().add(first + 16) as *const i8, _MM_HINT_T0);
            _mm_prefetch(high.as_ptr().add(first + 16) as *const i8, _MM_HINT_T0);
            _mm_prefetch(low.as_ptr().add(first + 16) as *const i8, _MM_HINT_T0);
            _mm_prefetch(close.as_ptr().add(first + 16) as *const i8, _MM_HINT_T0);
        }
    }
    lpc_scalar(
        high,
        low,
        close,
        src,
        cutoff_type,
        fixed_period,
        max_cycle_limit,
        cycle_mult,
        tr_mult,
        first,
        out_filter,
        out_high,
        out_low,
    )
}

#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
pub fn lpc_avx512(
    high: &[f64],
    low: &[f64],
    close: &[f64],
    src: &[f64],
    cutoff_type: &str,
    fixed_period: usize,
    max_cycle_limit: usize,
    cycle_mult: f64,
    tr_mult: f64,
    first: usize,
    out_filter: &mut [f64],
    out_high: &mut [f64],
    out_low: &mut [f64],
) {
    unsafe {
        if src.len() > first + 64 {
            _mm_prefetch(src.as_ptr().add(first + 32) as *const i8, _MM_HINT_T0);
            _mm_prefetch(high.as_ptr().add(first + 32) as *const i8, _MM_HINT_T0);
            _mm_prefetch(low.as_ptr().add(first + 32) as *const i8, _MM_HINT_T0);
            _mm_prefetch(close.as_ptr().add(first + 32) as *const i8, _MM_HINT_T0);
        }
    }
    lpc_scalar(
        high,
        low,
        close,
        src,
        cutoff_type,
        fixed_period,
        max_cycle_limit,
        cycle_mult,
        tr_mult,
        first,
        out_filter,
        out_high,
        out_low,
    )
}

#[inline(always)]
fn lpc_compute_into(
    high: &[f64],
    low: &[f64],
    close: &[f64],
    src: &[f64],
    cutoff_type: &str,
    fixed_period: usize,
    max_cycle_limit: usize,
    cycle_mult: f64,
    tr_mult: f64,
    first: usize,
    kernel: Kernel,
    out_filter: &mut [f64],
    out_high: &mut [f64],
    out_low: &mut [f64],
) {
    let actual_kernel = match kernel {
        Kernel::Auto => Kernel::Scalar,
        k => k,
    };

    match actual_kernel {
        Kernel::Scalar | Kernel::ScalarBatch | Kernel::Auto => lpc_scalar(
            high,
            low,
            close,
            src,
            cutoff_type,
            fixed_period,
            max_cycle_limit,
            cycle_mult,
            tr_mult,
            first,
            out_filter,
            out_high,
            out_low,
        ),
        #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
        Kernel::Avx2 | Kernel::Avx2Batch => lpc_avx2(
            high,
            low,
            close,
            src,
            cutoff_type,
            fixed_period,
            max_cycle_limit,
            cycle_mult,
            tr_mult,
            first,
            out_filter,
            out_high,
            out_low,
        ),
        #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
        Kernel::Avx512 | Kernel::Avx512Batch => lpc_avx512(
            high,
            low,
            close,
            src,
            cutoff_type,
            fixed_period,
            max_cycle_limit,
            cycle_mult,
            tr_mult,
            first,
            out_filter,
            out_high,
            out_low,
        ),
        #[cfg(not(all(feature = "nightly-avx", target_arch = "x86_64")))]
        _ => lpc_scalar(
            high,
            low,
            close,
            src,
            cutoff_type,
            fixed_period,
            max_cycle_limit,
            cycle_mult,
            tr_mult,
            first,
            out_filter,
            out_high,
            out_low,
        ),
    }
}

#[inline(always)]
fn lpc_compute_into_prefilled(
    high: &[f64],
    low: &[f64],
    close: &[f64],
    src: &[f64],
    cutoff_type: &str,
    fixed_period: usize,
    max_cycle_limit: usize,
    cycle_mult: f64,
    tr_mult: f64,
    first: usize,
    out_filter: &mut [f64],
    out_high: &mut [f64],
    out_low: &mut [f64],
) {
    let len = src.len();
    if first >= len {
        return;
    }

    out_filter[first] = src[first];
    let mut tr_prev = high[first] - low[first];
    let mut ftr_prev = tr_prev;
    let tm = tr_mult;
    out_high[first] = out_filter[first] + tr_prev * tm;
    out_low[first] = out_filter[first] - tr_prev * tm;

    let dc = if cutoff_type.eq_ignore_ascii_case("adaptive") {
        Some(dom_cycle(src, max_cycle_limit))
    } else {
        None
    };

    #[inline(always)]
    fn alpha_from_period(p: usize) -> f64 {
        let omega = 2.0 * std::f64::consts::PI / (p as f64);
        let (s, c) = omega.sin_cos();
        (1.0 - s) / c
    }
    #[inline(always)]
    fn per_bar_period(dc_opt: Option<&[f64]>, idx: usize, fixed_p: usize, cm: f64) -> usize {
        if let Some(dc) = dc_opt {
            let base = dc[idx];
            if base.is_nan() {
                fixed_p
            } else {
                (base * cm).round().max(3.0) as usize
            }
        } else {
            fixed_p
        }
    }

    let mut last_p: usize = if dc.is_none() { fixed_period } else { 0 };
    let mut alpha: f64 = if dc.is_none() {
        alpha_from_period(fixed_period)
    } else {
        0.0
    };

    let mut i = first + 1;
    while i + 1 < len {
        let p_i = per_bar_period(dc.as_deref(), i, fixed_period, cycle_mult);
        if p_i != last_p {
            last_p = p_i;
            alpha = alpha_from_period(last_p);
        }
        let one_m_a = 1.0 - alpha;
        let f_i = alpha.mul_add(out_filter[i - 1], 0.5 * one_m_a * (src[i] + src[i - 1]));
        out_filter[i] = f_i;

        let hl = high[i] - low[i];
        let c_low1 = (close[i] - low[i - 1]).abs();
        let c_hi1 = (close[i] - high[i - 1]).abs();
        let tr_i = hl.max(c_low1).max(c_hi1);
        let ftr_i = alpha.mul_add(ftr_prev, 0.5 * one_m_a * (tr_i + tr_prev));
        tr_prev = tr_i;
        ftr_prev = ftr_i;
        out_high[i] = f_i + ftr_i * tm;
        out_low[i] = f_i - ftr_i * tm;

        let i1 = i + 1;
        let p_i1 = per_bar_period(dc.as_deref(), i1, fixed_period, cycle_mult);
        if p_i1 != last_p {
            last_p = p_i1;
            alpha = alpha_from_period(last_p);
        }
        let one_m_a1 = 1.0 - alpha;
        let f_i1 = alpha.mul_add(f_i, 0.5 * one_m_a1 * (src[i1] + src[i]));
        out_filter[i1] = f_i1;

        let hl1 = high[i1] - low[i1];
        let c_low1b = (close[i1] - low[i1 - 1]).abs();
        let c_hi1b = (close[i1] - high[i1 - 1]).abs();
        let tr_i1 = hl1.max(c_low1b).max(c_hi1b);
        let ftr_i1 = alpha.mul_add(ftr_prev, 0.5 * one_m_a1 * (tr_i1 + tr_prev));
        tr_prev = tr_i1;
        ftr_prev = ftr_i1;
        out_high[i1] = f_i1 + ftr_i1 * tm;
        out_low[i1] = f_i1 - ftr_i1 * tm;

        i += 2;
    }

    if i < len {
        let p_i = per_bar_period(dc.as_deref(), i, fixed_period, cycle_mult);
        if p_i != last_p {
            last_p = p_i;
            alpha = alpha_from_period(last_p);
        }
        let one_m_a = 1.0 - alpha;
        let f_i = alpha.mul_add(out_filter[i - 1], 0.5 * one_m_a * (src[i] + src[i - 1]));
        out_filter[i] = f_i;

        let hl = high[i] - low[i];
        let c_low1 = (close[i] - low[i - 1]).abs();
        let c_hi1 = (close[i] - high[i - 1]).abs();
        let tr_i = hl.max(c_low1).max(c_hi1);
        let ftr_i = alpha.mul_add(ftr_prev, 0.5 * one_m_a * (tr_i + tr_prev));
        out_high[i] = f_i + ftr_i * tm;
        out_low[i] = f_i - ftr_i * tm;
    }
}

#[inline(always)]
fn lpc_compute_into_prefilled_pretr(
    _high: &[f64],
    _low: &[f64],
    _close: &[f64],
    src: &[f64],
    tr: &[f64],
    cutoff_type: &str,
    fixed_period: usize,
    max_cycle_limit: usize,
    cycle_mult: f64,
    tr_mult: f64,
    first: usize,
    out_filter: &mut [f64],
    out_high: &mut [f64],
    out_low: &mut [f64],
) {
    let len = src.len();
    if first >= len {
        return;
    }

    out_filter[first] = src[first];
    let mut tr_prev = tr[first];
    let mut ftr_prev = tr_prev;
    let tm = tr_mult;
    out_high[first] = out_filter[first] + tr_prev * tm;
    out_low[first] = out_filter[first] - tr_prev * tm;

    let dc = if cutoff_type.eq_ignore_ascii_case("adaptive") {
        Some(dom_cycle(src, max_cycle_limit))
    } else {
        None
    };

    #[inline(always)]
    fn alpha_from_period(p: usize) -> f64 {
        let omega = 2.0 * std::f64::consts::PI / (p as f64);
        let (s, c) = omega.sin_cos();
        (1.0 - s) / c
    }
    #[inline(always)]
    fn per_bar_period(dc_opt: Option<&[f64]>, idx: usize, fixed_p: usize, cm: f64) -> usize {
        if let Some(dc) = dc_opt {
            let base = dc[idx];
            if base.is_nan() {
                fixed_p
            } else {
                (base * cm).round().max(3.0) as usize
            }
        } else {
            fixed_p
        }
    }

    let mut last_p: usize = if dc.is_none() { fixed_period } else { 0 };
    let mut alpha: f64 = if dc.is_none() {
        alpha_from_period(fixed_period)
    } else {
        0.0
    };

    let mut i = first + 1;
    while i + 1 < len {
        let p_i = per_bar_period(dc.as_deref(), i, fixed_period, cycle_mult);
        if p_i != last_p {
            last_p = p_i;
            alpha = alpha_from_period(last_p);
        }
        let one_m_a = 1.0 - alpha;
        let f_i = alpha.mul_add(out_filter[i - 1], 0.5 * one_m_a * (src[i] + src[i - 1]));
        out_filter[i] = f_i;

        let tr_i = tr[i];
        let ftr_i = alpha.mul_add(ftr_prev, 0.5 * one_m_a * (tr_i + tr_prev));
        tr_prev = tr_i;
        ftr_prev = ftr_i;
        out_high[i] = f_i + ftr_i * tm;
        out_low[i] = f_i - ftr_i * tm;

        let i1 = i + 1;
        let p_i1 = per_bar_period(dc.as_deref(), i1, fixed_period, cycle_mult);
        if p_i1 != last_p {
            last_p = p_i1;
            alpha = alpha_from_period(last_p);
        }
        let one_m_a1 = 1.0 - alpha;
        let f_i1 = alpha.mul_add(f_i, 0.5 * one_m_a1 * (src[i1] + src[i]));
        out_filter[i1] = f_i1;

        let tr_i1 = tr[i1];
        let ftr_i1 = alpha.mul_add(ftr_prev, 0.5 * one_m_a1 * (tr_i1 + tr_prev));
        tr_prev = tr_i1;
        ftr_prev = ftr_i1;
        out_high[i1] = f_i1 + ftr_i1 * tm;
        out_low[i1] = f_i1 - ftr_i1 * tm;

        i += 2;
    }

    if i < len {
        let p_i = per_bar_period(dc.as_deref(), i, fixed_period, cycle_mult);
        if p_i != last_p {
            last_p = p_i;
            alpha = alpha_from_period(last_p);
        }
        let one_m_a = 1.0 - alpha;
        let f_i = alpha.mul_add(out_filter[i - 1], 0.5 * one_m_a * (src[i] + src[i - 1]));
        out_filter[i] = f_i;

        let tr_i = tr[i];
        let ftr_i = alpha.mul_add(ftr_prev, 0.5 * one_m_a * (tr_i + tr_prev));
        out_high[i] = f_i + ftr_i * tr_mult;
        out_low[i] = f_i - ftr_i * tr_mult;
    }
}

#[inline]
pub fn lpc(input: &LpcInput) -> Result<LpcOutput, LpcError> {
    lpc_with_kernel(input, Kernel::Auto)
}

pub fn lpc_with_kernel(input: &LpcInput, kernel: Kernel) -> Result<LpcOutput, LpcError> {
    let (h, l, c, s, cutoff, fp, mcl, cm, tm, first, _chosen) = lpc_prepare(input, kernel)?;
    let len = s.len();

    let mut filter = alloc_with_nan_prefix(len, first);
    let mut high_band = alloc_with_nan_prefix(len, first);
    let mut low_band = alloc_with_nan_prefix(len, first);

    lpc_compute_into(
        h,
        l,
        c,
        s,
        &cutoff,
        fp,
        mcl,
        cm,
        tm,
        first,
        kernel,
        &mut filter,
        &mut high_band,
        &mut low_band,
    );

    Ok(LpcOutput {
        filter,
        high_band,
        low_band,
    })
}

#[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
pub fn lpc_into(
    input: &LpcInput,
    filter_out: &mut [f64],
    high_out: &mut [f64],
    low_out: &mut [f64],
) -> Result<(), LpcError> {
    lpc_into_slices(filter_out, high_out, low_out, input, Kernel::Auto)
}

fn lpc_prepare<'a>(
    input: &'a LpcInput<'a>,
    kernel: Kernel,
) -> Result<
    (
        &'a [f64],
        &'a [f64],
        &'a [f64],
        &'a [f64],
        String,
        usize,
        usize,
        f64,
        f64,
        usize,
        Kernel,
    ),
    LpcError,
> {
    let (high, low, close, src) = match &input.data {
        LpcData::Candles { candles, source } => {
            let src_data = source_type(candles, source);
            (
                &candles.high[..],
                &candles.low[..],
                &candles.close[..],
                src_data,
            )
        }
        LpcData::Slices {
            high,
            low,
            close,
            src,
        } => (*high, *low, *close, *src),
    };

    if src.is_empty() {
        return Err(LpcError::EmptyInputData);
    }

    if high.len() != src.len() || low.len() != src.len() || close.len() != src.len() {
        return Err(LpcError::MissingData);
    }

    if src.iter().all(|v| v.is_nan())
        || high.iter().all(|v| v.is_nan())
        || low.iter().all(|v| v.is_nan())
        || close.iter().all(|v| v.is_nan())
    {
        return Err(LpcError::AllValuesNaN);
    }

    let cutoff_type = input.get_cutoff_type();
    if !cutoff_type.eq_ignore_ascii_case("adaptive") && !cutoff_type.eq_ignore_ascii_case("fixed") {
        return Err(LpcError::InvalidCutoffType { cutoff_type });
    }

    let fixed_period = input.get_fixed_period();
    let max_cycle_limit = input.get_max_cycle_limit();
    let cycle_mult = input.get_cycle_mult();
    let tr_mult = input.get_tr_mult();

    if fixed_period == 0 || fixed_period > src.len() {
        return Err(LpcError::InvalidPeriod {
            period: fixed_period,
            data_len: src.len(),
        });
    }

    let mut first = 0;
    for i in 0..src.len() {
        if !src[i].is_nan() && !high[i].is_nan() && !low[i].is_nan() && !close[i].is_nan() {
            first = i;
            break;
        }
    }

    let valid = src.len().saturating_sub(first);
    if valid < 2 {
        return Err(LpcError::NotEnoughValidData { needed: 2, valid });
    }

    let chosen = if kernel == Kernel::Auto {
        Kernel::Scalar
    } else {
        kernel
    };

    Ok((
        high,
        low,
        close,
        src,
        cutoff_type,
        fixed_period,
        max_cycle_limit,
        cycle_mult,
        tr_mult,
        first,
        chosen,
    ))
}

pub struct LpcStream {
    cutoff_type: String,
    fixed_period: usize,
    max_cycle_limit: usize,
    cycle_mult: f64,
    tr_mult: f64,
    adaptive_enabled: bool,

    prev_src: f64,
    prev_high: f64,
    prev_low: f64,
    prev_close: f64,

    prev_filter: f64,
    prev_tr: f64,
    prev_ftr: f64,

    last_p: usize,
    alpha: f64,
    one_minus_alpha: f64,

    dc: DomCycleState,
}

#[derive(Clone)]
struct DomCycleState {
    buf: [f64; 12],
    idx: usize,
    count: usize,

    ip_l1: f64,
    ip_l2: f64,
    ip_l3: f64,
    q_l1: f64,
    q_l2: f64,

    real_prev: f64,
    imag_prev: f64,

    phase_accum: f64,
    bars_since_cross: usize,
    last_inst_per: f64,

    dom_cycle_prev: f64,
}

impl Default for DomCycleState {
    fn default() -> Self {
        Self {
            buf: [0.0; 12],
            idx: 0,
            count: 0,
            ip_l1: 0.0,
            ip_l2: 0.0,
            ip_l3: 0.0,
            q_l1: 0.0,
            q_l2: 0.0,
            real_prev: 0.0,
            imag_prev: 0.0,
            phase_accum: 0.0,
            bars_since_cross: 0,
            last_inst_per: 20.0,
            dom_cycle_prev: 20.0,
        }
    }
}

impl DomCycleState {
    #[inline(always)]
    fn push_src(&mut self, x: f64) {
        self.buf[self.idx] = x;
        self.idx = (self.idx + 1) % 12;
        self.count = self.count.saturating_add(1);
    }

    #[inline(always)]
    fn at(&self, lag: usize) -> f64 {
        debug_assert!(lag < 12);
        let pos = (self.idx + 12 - 1 - lag) % 12;
        self.buf[pos]
    }

    #[inline(always)]
    fn update_ifm(&mut self) -> Option<f64> {
        if self.count < 12 {
            return None;
        }

        let v0 = self.at(0);
        let v2 = self.at(2);
        let v4 = self.at(4);
        let v7 = self.at(7);
        let v9 = self.at(9);
        let v11 = self.at(11);

        let ip_prev = self.ip_l1;
        let q_prev = self.q_l1;

        let ip_cur = 1.25 * ((v4 - v11) - 0.635 * (v2 - v9)) + 0.635 * self.ip_l3;

        let q_cur = (v2 - v9) - 0.338 * (v0 - v7) + 0.338 * self.q_l2;

        let real_cur = 0.2 * (ip_cur * ip_prev + q_cur * q_prev) + 0.8 * self.real_prev;
        let imag_cur = 0.2 * (ip_cur * q_prev - ip_prev * q_cur) + 0.8 * self.imag_prev;

        let delta = if real_cur != 0.0 {
            (imag_cur / real_cur).atan()
        } else {
            0.0
        };

        const TAU: f64 = std::f64::consts::PI * 2.0;
        self.phase_accum += delta;
        self.bars_since_cross = self.bars_since_cross.saturating_add(1);

        let mut inst = self.last_inst_per;
        if self.phase_accum > TAU {
            inst = self.bars_since_cross as f64;
            self.phase_accum = 0.0;
            self.bars_since_cross = 0;
            self.last_inst_per = inst;
        }

        let dom = 0.25 * inst + 0.75 * self.dom_cycle_prev;

        self.ip_l3 = self.ip_l2;
        self.ip_l2 = self.ip_l1;
        self.ip_l1 = ip_cur;

        self.q_l2 = self.q_l1;
        self.q_l1 = q_cur;

        self.real_prev = real_cur;
        self.imag_prev = imag_cur;
        self.dom_cycle_prev = dom;

        Some(dom)
    }
}

impl LpcStream {
    pub fn try_new(params: LpcParams) -> Result<Self, LpcError> {
        let cutoff_type = params.cutoff_type.unwrap_or_else(|| "adaptive".to_string());
        let ct_lower = cutoff_type.to_ascii_lowercase();
        if ct_lower != "adaptive" && ct_lower != "fixed" {
            return Err(LpcError::InvalidCutoffType { cutoff_type });
        }

        let fixed_period = params.fixed_period.unwrap_or(20);
        if fixed_period == 0 {
            return Err(LpcError::InvalidPeriod {
                period: 0,
                data_len: 0,
            });
        }

        let mut s = Self {
            cutoff_type,
            fixed_period,
            max_cycle_limit: params.max_cycle_limit.unwrap_or(60),
            cycle_mult: params.cycle_mult.unwrap_or(1.0),
            tr_mult: params.tr_mult.unwrap_or(1.0),
            adaptive_enabled: ct_lower == "adaptive",

            prev_src: f64::NAN,
            prev_high: f64::NAN,
            prev_low: f64::NAN,
            prev_close: f64::NAN,

            prev_filter: f64::NAN,
            prev_tr: f64::NAN,
            prev_ftr: f64::NAN,

            last_p: 0,
            alpha: 0.0,
            one_minus_alpha: 0.0,

            dc: DomCycleState::default(),
        };

        s.set_alpha(fixed_period);
        Ok(s)
    }

    #[inline(always)]
    fn set_alpha(&mut self, p: usize) {
        if p == self.last_p {
            return;
        }

        let omega = 2.0 * std::f64::consts::PI / (p as f64);
        let (s, c) = omega.sin_cos();
        let a = if c.abs() < 1e-12 {
            if self.last_p == 0 {
                2.0 / (p as f64 + 1.0)
            } else {
                self.alpha
            }
        } else {
            (1.0 - s) / c
        };
        self.alpha = a;
        self.one_minus_alpha = 1.0 - a;
        self.last_p = p;
    }

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

        self.dc.push_src(src);

        let mut period = self.fixed_period;
        if self.adaptive_enabled {
            if let Some(dom) = self.dc.update_ifm() {
                let p = (dom * self.cycle_mult).round().max(3.0) as usize;
                period = if self.max_cycle_limit > 0 {
                    p.min(self.max_cycle_limit)
                } else {
                    p
                };
            }
        }
        self.set_alpha(period);

        let filt = if self.prev_filter.is_nan() || self.prev_src.is_nan() {
            src
        } else {
            self.alpha.mul_add(
                self.prev_filter,
                0.5 * self.one_minus_alpha * (src + self.prev_src),
            )
        };

        let tr = if self.prev_high.is_nan() || self.prev_low.is_nan() || self.prev_close.is_nan() {
            (high - low).abs()
        } else {
            let hl = high - low;
            let c_low1 = (close - self.prev_low).abs();
            let c_high1 = (close - self.prev_high).abs();
            hl.max(c_low1).max(c_high1)
        };

        let ftr = if self.prev_ftr.is_nan() || self.prev_tr.is_nan() {
            tr
        } else {
            self.alpha.mul_add(
                self.prev_ftr,
                0.5 * self.one_minus_alpha * (tr + self.prev_tr),
            )
        };

        let band_high = filt + ftr * self.tr_mult;
        let band_low = filt - ftr * self.tr_mult;

        self.prev_src = src;
        self.prev_high = high;
        self.prev_low = low;
        self.prev_close = close;

        self.prev_tr = tr;
        self.prev_filter = filt;
        self.prev_ftr = ftr;

        Some((filt, band_high, band_low))
    }
}

#[cfg(feature = "python")]
#[pyfunction(name = "lpc")]
#[pyo3(signature = (high, low, close, src, cutoff_type=None, fixed_period=None, max_cycle_limit=None, cycle_mult=None, tr_mult=None, kernel=None))]
pub fn lpc_py<'py>(
    py: Python<'py>,
    high: PyReadonlyArray1<'py, f64>,
    low: PyReadonlyArray1<'py, f64>,
    close: PyReadonlyArray1<'py, f64>,
    src: PyReadonlyArray1<'py, f64>,
    cutoff_type: Option<String>,
    fixed_period: Option<usize>,
    max_cycle_limit: Option<usize>,
    cycle_mult: Option<f64>,
    tr_mult: Option<f64>,
    kernel: Option<&str>,
) -> PyResult<(
    Bound<'py, PyArray1<f64>>,
    Bound<'py, PyArray1<f64>>,
    Bound<'py, PyArray1<f64>>,
)> {
    let h = high.as_slice()?;
    let l = low.as_slice()?;
    let c = close.as_slice()?;
    let s = src.as_slice()?;

    if h.len() != s.len() || l.len() != s.len() || c.len() != s.len() {
        return Err(PyValueError::new_err(
            "All arrays must have the same length",
        ));
    }

    let params = LpcParams {
        cutoff_type,
        fixed_period,
        max_cycle_limit,
        cycle_mult,
        tr_mult,
    };

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

    match lpc_with_kernel(&input, kern) {
        Ok(output) => Ok((
            output.filter.into_pyarray(py),
            output.high_band.into_pyarray(py),
            output.low_band.into_pyarray(py),
        )),
        Err(e) => Err(PyValueError::new_err(e.to_string())),
    }
}

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

#[cfg(feature = "python")]
#[pymethods]
impl LpcStreamPy {
    #[new]
    #[pyo3(signature = (cutoff_type=None, fixed_period=None, max_cycle_limit=None, cycle_mult=None, tr_mult=None))]
    pub fn new(
        cutoff_type: Option<String>,
        fixed_period: Option<usize>,
        max_cycle_limit: Option<usize>,
        cycle_mult: Option<f64>,
        tr_mult: Option<f64>,
    ) -> PyResult<Self> {
        let params = LpcParams {
            cutoff_type,
            fixed_period,
            max_cycle_limit,
            cycle_mult,
            tr_mult,
        };

        match LpcStream::try_new(params) {
            Ok(stream) => Ok(Self { inner: stream }),
            Err(e) => Err(PyValueError::new_err(e.to_string())),
        }
    }

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

#[cfg(feature = "python")]
#[pyfunction(name = "lpc_batch")]
#[pyo3(signature = (
    high, low, close, src,
    fixed_period_range, cycle_mult_range, tr_mult_range,
    cutoff_type="fixed", max_cycle_limit=60, kernel=None
))]
pub fn lpc_batch_py<'py>(
    py: Python<'py>,
    high: numpy::PyReadonlyArray1<'py, f64>,
    low: numpy::PyReadonlyArray1<'py, f64>,
    close: numpy::PyReadonlyArray1<'py, f64>,
    src: numpy::PyReadonlyArray1<'py, f64>,
    fixed_period_range: (usize, usize, usize),
    cycle_mult_range: (f64, f64, f64),
    tr_mult_range: (f64, f64, f64),
    cutoff_type: &str,
    max_cycle_limit: usize,
    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 s = src.as_slice()?;
    if h.len() != s.len() || l.len() != s.len() || c.len() != s.len() {
        return Err(PyValueError::new_err(
            "All arrays must have the same length",
        ));
    }

    let sweep = LpcBatchRange {
        fixed_period: fixed_period_range,
        cycle_mult: cycle_mult_range,
        tr_mult: tr_mult_range,
        cutoff_type: cutoff_type.to_string(),
        max_cycle_limit,
    };
    let combos = expand_grid_lpc(&sweep).map_err(|e| PyValueError::new_err(e.to_string()))?;
    let rows = combos.len() * 3;
    let cols = s.len();

    let kern = validate_kernel(kernel, true)?;
    let first = (0..s.len())
        .find(|&i| !s[i].is_nan() && !h[i].is_nan() && !l[i].is_nan() && !c[i].is_nan())
        .unwrap_or(0);

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

    for row in 0..rows {
        for col in 0..first {
            slice_out[row * cols + col] = f64::NAN;
        }
    }

    py.allow_threads(|| {
        lpc_batch_inner_into(h, l, c, s, &sweep, kern, first, slice_out)
            .map_err(|e| PyValueError::new_err(e.to_string()))
    })?;

    let dict = pyo3::types::PyDict::new(py);
    dict.set_item("values", out_arr.reshape((rows, cols))?)?;
    dict.set_item(
        "fixed_periods",
        combos
            .iter()
            .map(|p| p.fixed_period.unwrap() as u64)
            .collect::<Vec<_>>()
            .into_pyarray(py),
    )?;
    dict.set_item(
        "cycle_mults",
        combos
            .iter()
            .map(|p| p.cycle_mult.unwrap())
            .collect::<Vec<_>>()
            .into_pyarray(py),
    )?;
    dict.set_item(
        "tr_mults",
        combos
            .iter()
            .map(|p| p.tr_mult.unwrap())
            .collect::<Vec<_>>()
            .into_pyarray(py),
    )?;

    let order_list = PyList::new(py, vec!["filter", "high", "low"])?;
    dict.set_item("order", order_list)?;
    dict.set_item("rows", rows)?;
    dict.set_item("cols", cols)?;
    Ok(dict)
}

#[cfg(feature = "python")]
pub fn register_lpc_module(m: &Bound<'_, pyo3::types::PyModule>) -> PyResult<()> {
    m.add_function(wrap_pyfunction!(lpc_py, m)?)?;
    m.add_function(wrap_pyfunction!(lpc_batch_py, m)?)?;
    #[cfg(feature = "cuda")]
    {
        m.add_function(wrap_pyfunction!(lpc_cuda_batch_dev_py, m)?)?;
        m.add_function(wrap_pyfunction!(lpc_cuda_many_series_one_param_dev_py, m)?)?;
    }
    Ok(())
}

#[cfg(all(feature = "python", feature = "cuda"))]
use crate::cuda::cuda_available as cuda_is_available;
#[cfg(all(feature = "python", feature = "cuda"))]
use crate::cuda::lpc_wrapper::CudaLpc;
#[cfg(all(feature = "python", feature = "cuda"))]
use crate::indicators::moving_averages::alma::DeviceArrayF32Py;

#[cfg(all(feature = "python", feature = "cuda"))]
#[pyfunction(name = "lpc_cuda_batch_dev")]
#[pyo3(signature = (high_f32, low_f32, close_f32, src_f32, fixed_period_range, cycle_mult_range, tr_mult_range, cutoff_type="fixed", max_cycle_limit=60, device_id=0))]
pub fn lpc_cuda_batch_dev_py<'py>(
    py: Python<'py>,
    high_f32: numpy::PyReadonlyArray1<'py, f32>,
    low_f32: numpy::PyReadonlyArray1<'py, f32>,
    close_f32: numpy::PyReadonlyArray1<'py, f32>,
    src_f32: numpy::PyReadonlyArray1<'py, f32>,
    fixed_period_range: (usize, usize, usize),
    cycle_mult_range: (f64, f64, f64),
    tr_mult_range: (f64, f64, f64),
    cutoff_type: &str,
    max_cycle_limit: usize,
    device_id: usize,
) -> PyResult<Bound<'py, pyo3::types::PyDict>> {
    use numpy::IntoPyArray;
    if !cuda_is_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 s = src_f32.as_slice()?;
    if h.len() != s.len() || l.len() != s.len() || c.len() != s.len() {
        return Err(PyValueError::new_err(
            "All arrays must have the same length",
        ));
    }
    let sweep = LpcBatchRange {
        fixed_period: fixed_period_range,
        cycle_mult: cycle_mult_range,
        tr_mult: tr_mult_range,
        cutoff_type: cutoff_type.to_string(),
        max_cycle_limit,
    };
    let (triplet, combos, ctx, dev_id) = py.allow_threads(|| {
        let cuda = CudaLpc::new(device_id).map_err(|e| PyValueError::new_err(e.to_string()))?;
        let ctx = cuda.context_arc();
        let dev_id = cuda.device_id();
        let (triplet, combos) = cuda
            .lpc_batch_dev(h, l, c, s, &sweep)
            .map_err(|e| PyValueError::new_err(e.to_string()))?;
        cuda.synchronize()
            .map_err(|e| PyValueError::new_err(e.to_string()))?;
        Ok::<_, PyErr>((triplet, combos, ctx, dev_id))
    })?;
    let d = pyo3::types::PyDict::new(py);
    d.set_item(
        "filter",
        DeviceArrayF32Py {
            inner: triplet.wt1,
            _ctx: Some(ctx.clone()),
            device_id: Some(dev_id),
        },
    )?;
    d.set_item(
        "high",
        DeviceArrayF32Py {
            inner: triplet.wt2,
            _ctx: Some(ctx.clone()),
            device_id: Some(dev_id),
        },
    )?;
    d.set_item(
        "low",
        DeviceArrayF32Py {
            inner: triplet.hist,
            _ctx: Some(ctx),
            device_id: Some(dev_id),
        },
    )?;
    d.set_item(
        "fixed_periods",
        combos
            .iter()
            .map(|p| p.fixed_period.unwrap() as u64)
            .collect::<Vec<_>>()
            .into_pyarray(py),
    )?;
    d.set_item(
        "cycle_mults",
        combos
            .iter()
            .map(|p| p.cycle_mult.unwrap())
            .collect::<Vec<_>>()
            .into_pyarray(py),
    )?;
    d.set_item(
        "tr_mults",
        combos
            .iter()
            .map(|p| p.tr_mult.unwrap())
            .collect::<Vec<_>>()
            .into_pyarray(py),
    )?;
    d.set_item("rows", combos.len())?;
    d.set_item("cols", s.len())?;
    Ok(d)
}

#[cfg(all(feature = "python", feature = "cuda"))]
#[pyfunction(name = "lpc_cuda_many_series_one_param_dev")]
#[pyo3(signature = (high_tm_f32, low_tm_f32, close_tm_f32, src_tm_f32, cutoff_type="fixed", fixed_period=20, tr_mult=1.0, device_id=0))]
pub fn lpc_cuda_many_series_one_param_dev_py<'py>(
    py: Python<'py>,
    high_tm_f32: numpy::PyReadonlyArray2<'py, f32>,
    low_tm_f32: numpy::PyReadonlyArray2<'py, f32>,
    close_tm_f32: numpy::PyReadonlyArray2<'py, f32>,
    src_tm_f32: numpy::PyReadonlyArray2<'py, f32>,
    cutoff_type: &str,
    fixed_period: usize,
    tr_mult: f64,
    device_id: usize,
) -> PyResult<Bound<'py, pyo3::types::PyDict>> {
    if !cuda_is_available() {
        return Err(PyValueError::new_err("CUDA not available"));
    }
    if !cutoff_type.eq_ignore_ascii_case("fixed") {
        return Err(PyValueError::new_err(
            "many-series CUDA supports fixed cutoff only",
        ));
    }
    let sh = high_tm_f32.shape();
    let sl = low_tm_f32.shape();
    let sc = close_tm_f32.shape();
    let ss = src_tm_f32.shape();
    if sh != sl || sh != sc || sh != ss || sh.len() != 2 {
        return Err(PyValueError::new_err(
            "expected matching 2D arrays [rows, cols]",
        ));
    }
    let rows = sh[0];
    let cols = sh[1];
    let h = high_tm_f32.as_slice()?;
    let l = low_tm_f32.as_slice()?;
    let c = close_tm_f32.as_slice()?;
    let s = src_tm_f32.as_slice()?;
    let params = LpcParams {
        cutoff_type: Some(cutoff_type.to_string()),
        fixed_period: Some(fixed_period),
        max_cycle_limit: Some(60),
        cycle_mult: Some(1.0),
        tr_mult: Some(tr_mult),
    };
    let (triplet, ctx, dev_id) = py.allow_threads(|| {
        let cuda = CudaLpc::new(device_id).map_err(|e| PyValueError::new_err(e.to_string()))?;
        let ctx = cuda.context_arc();
        let dev_id = cuda.device_id();
        let triplet = cuda
            .lpc_many_series_one_param_time_major_dev(h, l, c, s, cols, rows, &params)
            .map_err(|e| PyValueError::new_err(e.to_string()))?;
        cuda.synchronize()
            .map_err(|e| PyValueError::new_err(e.to_string()))?;
        Ok::<_, PyErr>((triplet, ctx, dev_id))
    })?;
    let d = pyo3::types::PyDict::new(py);
    d.set_item(
        "filter",
        DeviceArrayF32Py {
            inner: triplet.wt1,
            _ctx: Some(ctx.clone()),
            device_id: Some(dev_id),
        },
    )?;
    d.set_item(
        "high",
        DeviceArrayF32Py {
            inner: triplet.wt2,
            _ctx: Some(ctx.clone()),
            device_id: Some(dev_id),
        },
    )?;
    d.set_item(
        "low",
        DeviceArrayF32Py {
            inner: triplet.hist,
            _ctx: Some(ctx),
            device_id: Some(dev_id),
        },
    )?;
    d.set_item("rows", rows)?;
    d.set_item("cols", cols)?;
    d.set_item("fixed_period", fixed_period)?;
    d.set_item("tr_mult", tr_mult)?;
    Ok(d)
}

#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
#[derive(Serialize, Deserialize)]
struct LpcResult {
    filter: Vec<f64>,
    high_band: Vec<f64>,
    low_band: Vec<f64>,
}

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

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

#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
#[wasm_bindgen]
pub fn lpc_into(
    high_ptr: *const f64,
    low_ptr: *const f64,
    close_ptr: *const f64,
    src_ptr: *const f64,
    filter_out_ptr: *mut f64,
    high_out_ptr: *mut f64,
    low_out_ptr: *mut f64,
    len: usize,
    cutoff_type: &str,
    fixed_period: usize,
    max_cycle_limit: usize,
    cycle_mult: f64,
    tr_mult: f64,
) -> Result<(), JsValue> {
    if high_ptr.is_null()
        || low_ptr.is_null()
        || close_ptr.is_null()
        || src_ptr.is_null()
        || filter_out_ptr.is_null()
        || high_out_ptr.is_null()
        || low_out_ptr.is_null()
    {
        return Err(JsValue::from_str("null pointer passed to lpc_into"));
    }

    unsafe {
        let h = std::slice::from_raw_parts(high_ptr, len);
        let l = std::slice::from_raw_parts(low_ptr, len);
        let c = std::slice::from_raw_parts(close_ptr, len);
        let s = std::slice::from_raw_parts(src_ptr, len);

        let params = LpcParams {
            cutoff_type: Some(cutoff_type.to_string()),
            fixed_period: Some(fixed_period),
            max_cycle_limit: Some(max_cycle_limit),
            cycle_mult: Some(cycle_mult),
            tr_mult: Some(tr_mult),
        };
        let input = LpcInput::from_slices(h, l, c, s, params);

        let alias = filter_out_ptr as *const f64 == high_ptr
            || filter_out_ptr as *const f64 == low_ptr
            || filter_out_ptr as *const f64 == close_ptr
            || filter_out_ptr as *const f64 == src_ptr
            || high_out_ptr as *const f64 == high_ptr
            || high_out_ptr as *const f64 == low_ptr
            || high_out_ptr as *const f64 == close_ptr
            || high_out_ptr as *const f64 == src_ptr
            || low_out_ptr as *const f64 == high_ptr
            || low_out_ptr as *const f64 == low_ptr
            || low_out_ptr as *const f64 == close_ptr
            || low_out_ptr as *const f64 == src_ptr;

        if alias {
            let mut f = vec![0.0; len];
            let mut hb = vec![0.0; len];
            let mut lb = vec![0.0; len];
            lpc_into_slices(&mut f, &mut hb, &mut lb, &input, Kernel::Auto)
                .map_err(|e| JsValue::from_str(&e.to_string()))?;
            std::slice::from_raw_parts_mut(filter_out_ptr, len).copy_from_slice(&f);
            std::slice::from_raw_parts_mut(high_out_ptr, len).copy_from_slice(&hb);
            std::slice::from_raw_parts_mut(low_out_ptr, len).copy_from_slice(&lb);
        } else {
            let f = std::slice::from_raw_parts_mut(filter_out_ptr, len);
            let hb = std::slice::from_raw_parts_mut(high_out_ptr, len);
            let lb = std::slice::from_raw_parts_mut(low_out_ptr, len);
            lpc_into_slices(f, hb, lb, &input, Kernel::Auto)
                .map_err(|e| JsValue::from_str(&e.to_string()))?;
        }
        Ok(())
    }
}

#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
#[wasm_bindgen]
pub fn lpc_wasm(
    high: &[f64],
    low: &[f64],
    close: &[f64],
    src: &[f64],
    cutoff_type: &str,
    fixed_period: usize,
    max_cycle_limit: usize,
    cycle_mult: f64,
    tr_mult: f64,
) -> Result<JsValue, JsValue> {
    let params = LpcParams {
        cutoff_type: Some(cutoff_type.to_string()),
        fixed_period: Some(fixed_period),
        max_cycle_limit: Some(max_cycle_limit),
        cycle_mult: Some(cycle_mult),
        tr_mult: Some(tr_mult),
    };

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

    match lpc(&input) {
        Ok(output) => {
            let result = LpcResult {
                filter: output.filter,
                high_band: output.high_band,
                low_band: output.low_band,
            };
            serde_wasm_bindgen::to_value(&result).map_err(|e| JsValue::from_str(&e.to_string()))
        }
        Err(e) => Err(JsValue::from_str(&e.to_string())),
    }
}

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

#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
#[wasm_bindgen(js_name = lpc)]
pub fn lpc_js(
    high: &[f64],
    low: &[f64],
    close: &[f64],
    src: &[f64],
    cutoff_type: &str,
    fixed_period: usize,
    max_cycle_limit: usize,
    cycle_mult: f64,
    tr_mult: f64,
) -> Result<Vec<f64>, JsValue> {
    let params = LpcParams {
        cutoff_type: Some(cutoff_type.to_string()),
        fixed_period: Some(fixed_period),
        max_cycle_limit: Some(max_cycle_limit),
        cycle_mult: Some(cycle_mult),
        tr_mult: Some(tr_mult),
    };
    let input = LpcInput::from_slices(high, low, close, src, params);
    let out = lpc(&input).map_err(|e| JsValue::from_str(&e.to_string()))?;
    let len = src.len();
    let mut values = Vec::with_capacity(3 * len);
    values.extend_from_slice(&out.filter);
    values.extend_from_slice(&out.high_band);
    values.extend_from_slice(&out.low_band);
    Ok(values)
}

#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
#[derive(Serialize, Deserialize)]
pub struct LpcBatchConfig {
    pub fixed_period_range: (usize, usize, usize),
    pub cycle_mult_range: (f64, f64, f64),
    pub tr_mult_range: (f64, f64, f64),
    pub cutoff_type: String,
    pub max_cycle_limit: usize,
}

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

#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
#[wasm_bindgen(js_name = lpc_batch)]
pub fn lpc_batch_unified_js(
    high: &[f64],
    low: &[f64],
    close: &[f64],
    src: &[f64],
    config: JsValue,
) -> Result<JsValue, JsValue> {
    let cfg: LpcBatchConfig = serde_wasm_bindgen::from_value(config)
        .map_err(|e| JsValue::from_str(&format!("Invalid config: {e}")))?;
    let sweep = LpcBatchRange {
        fixed_period: cfg.fixed_period_range,
        cycle_mult: cfg.cycle_mult_range,
        tr_mult: cfg.tr_mult_range,
        cutoff_type: cfg.cutoff_type,
        max_cycle_limit: cfg.max_cycle_limit,
    };
    let out = lpc_batch_with_kernel(high, low, close, src, &sweep, Kernel::Auto)
        .map_err(|e| JsValue::from_str(&e.to_string()))?;

    let mut values_2d = Vec::with_capacity(out.rows);
    for i in 0..out.rows {
        let start = i * out.cols;
        let end = start + out.cols;
        values_2d.push(out.values[start..end].to_vec());
    }

    let num_combos = out.combos.len();
    let mut fixed_periods = Vec::with_capacity(num_combos);
    let mut cycle_mults = Vec::with_capacity(num_combos);
    let mut tr_mults = Vec::with_capacity(num_combos);

    for combo in &out.combos {
        fixed_periods.push(combo.fixed_period.unwrap());
        cycle_mults.push(combo.cycle_mult.unwrap());
        tr_mults.push(combo.tr_mult.unwrap());
    }

    let js = LpcBatchJsOutput {
        values: values_2d,
        fixed_periods,
        cycle_mults,
        tr_mults,
        rows: out.rows,
        cols: out.cols,
        order: vec!["filter".to_string(), "high".to_string(), "low".to_string()],
    };
    serde_wasm_bindgen::to_value(&js)
        .map_err(|e| JsValue::from_str(&format!("Serialization error: {e}")))
}

#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
#[wasm_bindgen]
pub fn lpc_batch_into(
    high_ptr: *const f64,
    low_ptr: *const f64,
    close_ptr: *const f64,
    src_ptr: *const f64,
    out_ptr: *mut f64,
    len: usize,
    fixed_start: usize,
    fixed_end: usize,
    fixed_step: usize,
    cm_start: f64,
    cm_end: f64,
    cm_step: f64,
    tm_start: f64,
    tm_end: f64,
    tm_step: f64,
    cutoff_type: &str,
    max_cycle_limit: usize,
) -> Result<usize, JsValue> {
    if [high_ptr, low_ptr, close_ptr, src_ptr, out_ptr]
        .iter()
        .any(|&p| p.is_null())
    {
        return Err(JsValue::from_str("null pointer passed to lpc_batch_into"));
    }
    unsafe {
        let h = std::slice::from_raw_parts(high_ptr, len);
        let l = std::slice::from_raw_parts(low_ptr, len);
        let c = std::slice::from_raw_parts(close_ptr, len);
        let s = std::slice::from_raw_parts(src_ptr, len);

        let sweep = LpcBatchRange {
            fixed_period: (fixed_start, fixed_end, fixed_step),
            cycle_mult: (cm_start, cm_end, cm_step),
            tr_mult: (tm_start, tm_end, tm_step),
            cutoff_type: cutoff_type.to_string(),
            max_cycle_limit,
        };
        let combos = expand_grid_lpc(&sweep).map_err(|e| JsValue::from_str(&e.to_string()))?;
        let rows = combos.len().checked_mul(3).ok_or_else(|| {
            JsValue::from_str(
                &LpcError::InvalidRange {
                    start: fixed_start,
                    end: fixed_end,
                    step: fixed_step,
                }
                .to_string(),
            )
        })?;
        let cols = len;

        let total = rows.checked_mul(cols).ok_or_else(|| {
            JsValue::from_str(
                &LpcError::InvalidRange {
                    start: fixed_start,
                    end: fixed_end,
                    step: fixed_step,
                }
                .to_string(),
            )
        })?;

        let out = std::slice::from_raw_parts_mut(out_ptr, total);
        let first = (0..len)
            .find(|&i| !s[i].is_nan() && !h[i].is_nan() && !l[i].is_nan() && !c[i].is_nan())
            .unwrap_or(0);

        for row in 0..rows {
            for col in 0..first {
                out[row * cols + col] = f64::NAN;
            }
        }

        lpc_batch_inner_into(
            h,
            l,
            c,
            s,
            &sweep,
            crate::utilities::enums::Kernel::Auto,
            first,
            out,
        )
        .map_err(|e| JsValue::from_str(&e.to_string()))?;
        Ok(rows)
    }
}

#[inline]
pub fn lpc_into_slices(
    filter_dst: &mut [f64],
    high_band_dst: &mut [f64],
    low_band_dst: &mut [f64],
    input: &LpcInput,
    kern: Kernel,
) -> Result<(), LpcError> {
    let (h, l, c, s, cutoff, fp, mcl, cm, tm, first, _chosen) = lpc_prepare(input, kern)?;
    let n = s.len();
    if filter_dst.len() != n || high_band_dst.len() != n || low_band_dst.len() != n {
        return Err(LpcError::OutputLengthMismatch {
            expected: n,
            got: filter_dst.len(),
        });
    }

    if first > 0 {
        let qnan = f64::from_bits(0x7ff8_0000_0000_0000);
        let w = first.min(n);
        for v in &mut filter_dst[..w] {
            *v = qnan;
        }
        for v in &mut high_band_dst[..w] {
            *v = qnan;
        }
        for v in &mut low_band_dst[..w] {
            *v = qnan;
        }
    }
    lpc_compute_into(
        h,
        l,
        c,
        s,
        &cutoff,
        fp,
        mcl,
        cm,
        tm,
        first,
        kern,
        filter_dst,
        high_band_dst,
        low_band_dst,
    );
    Ok(())
}

#[inline]
fn axis_usize((start, end, step): (usize, usize, usize)) -> Result<Vec<usize>, LpcError> {
    if step == 0 || start == end {
        return Ok(vec![start]);
    }
    let mut vals = Vec::new();
    if start < end {
        let mut v = start;
        while v <= end {
            vals.push(v);
            match v.checked_add(step) {
                Some(next) => {
                    if next == v {
                        break;
                    }
                    v = next;
                }
                None => break,
            }
        }
    } else {
        let mut v = start;
        while v >= end {
            vals.push(v);
            if v == 0 {
                break;
            }
            let next = v.saturating_sub(step);
            if next == v {
                break;
            }
            v = next;
            if v < end {
                break;
            }
        }
    }
    if vals.is_empty() {
        return Err(LpcError::InvalidRange { start, end, step });
    }
    Ok(vals)
}

#[inline]
fn axis_f64((start, end, step): (f64, f64, f64)) -> Result<Vec<f64>, LpcError> {
    if step.abs() < 1e-12 || (start - end).abs() < 1e-12 {
        return Ok(vec![start]);
    }
    let mut out = Vec::new();
    if start < end {
        let st = if step > 0.0 { step } else { -step };
        let mut x = start;
        while x <= end + 1e-12 {
            out.push(x);
            x += st;
        }
    } else {
        let st = if step > 0.0 { -step } else { step };
        if st.abs() < 1e-12 {
            return Ok(vec![start]);
        }
        let mut x = start;
        while x >= end - 1e-12 {
            out.push(x);
            x += st;
        }
    }
    if out.is_empty() {
        return Err(LpcError::InvalidRange {
            start: start as usize,
            end: end as usize,
            step: step as usize,
        });
    }
    Ok(out)
}

#[inline]
fn expand_grid_lpc(r: &LpcBatchRange) -> Result<Vec<LpcParams>, LpcError> {
    let ps = axis_usize(r.fixed_period)?;
    let cms = axis_f64(r.cycle_mult)?;
    let tms = axis_f64(r.tr_mult)?;
    let cap = ps
        .len()
        .checked_mul(cms.len())
        .and_then(|v| v.checked_mul(tms.len()))
        .ok_or(LpcError::InvalidRange {
            start: r.fixed_period.0,
            end: r.fixed_period.1,
            step: r.fixed_period.2,
        })?;
    let mut out = Vec::with_capacity(cap);
    for &p in &ps {
        for &cm in &cms {
            for &tm in &tms {
                out.push(LpcParams {
                    cutoff_type: Some(r.cutoff_type.clone()),
                    fixed_period: Some(p),
                    max_cycle_limit: Some(r.max_cycle_limit),
                    cycle_mult: Some(cm),
                    tr_mult: Some(tm),
                });
            }
        }
    }
    Ok(out)
}

pub fn lpc_batch_with_kernel(
    high: &[f64],
    low: &[f64],
    close: &[f64],
    src: &[f64],
    sweep: &LpcBatchRange,
    k: Kernel,
) -> Result<LpcBatchOutput, LpcError> {
    if src.is_empty() {
        return Err(LpcError::EmptyInputData);
    }
    if high.len() != src.len() || low.len() != src.len() || close.len() != src.len() {
        return Err(LpcError::MissingData);
    }

    let first = (0..src.len())
        .find(|&i| !src[i].is_nan() && !high[i].is_nan() && !low[i].is_nan() && !close[i].is_nan())
        .ok_or(LpcError::AllValuesNaN)?;
    if src.len().saturating_sub(first) < 2 {
        return Err(LpcError::NotEnoughValidData {
            needed: 2,
            valid: src.len().saturating_sub(first),
        });
    }

    let combos = expand_grid_lpc(sweep)?;
    let cols = src.len();
    let rows = combos.len().checked_mul(3).ok_or(LpcError::InvalidRange {
        start: sweep.fixed_period.0,
        end: sweep.fixed_period.1,
        step: sweep.fixed_period.2,
    })?;
    rows.checked_mul(cols).ok_or(LpcError::InvalidRange {
        start: sweep.fixed_period.0,
        end: sweep.fixed_period.1,
        step: sweep.fixed_period.2,
    })?;

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

    let mut buf_mu = make_uninit_matrix(rows, cols);
    let warm = vec![first; rows];
    init_matrix_prefixes(&mut buf_mu, cols, &warm);

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

    lpc_batch_inner_into(high, low, close, src, sweep, kernel, first, out)?;

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

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

fn lpc_batch_inner_into(
    high: &[f64],
    low: &[f64],
    close: &[f64],
    src: &[f64],
    sweep: &LpcBatchRange,
    k: Kernel,
    first: usize,
    out: &mut [f64],
) -> Result<(), LpcError> {
    let _ = k;
    let combos = expand_grid_lpc(sweep)?;
    let cols = src.len();

    let tr_series = calculate_true_range(high, low, close);

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

    let do_row = |combo_idx: usize, dst3: &mut [MaybeUninit<f64>]| {
        let params = &combos[combo_idx];
        let mut rowslice = |k: usize| -> &mut [f64] {
            let start = k * cols;
            unsafe {
                core::slice::from_raw_parts_mut(dst3.as_mut_ptr().add(start) as *mut f64, cols)
            }
        };
        let (f_dst, h_dst, l_dst) = (rowslice(0), rowslice(1), rowslice(2));

        lpc_compute_into_prefilled_pretr(
            high,
            low,
            close,
            src,
            &tr_series,
            params.cutoff_type.as_ref().unwrap(),
            params.fixed_period.unwrap(),
            params.max_cycle_limit.unwrap(),
            params.cycle_mult.unwrap(),
            params.tr_mult.unwrap(),
            first,
            f_dst,
            h_dst,
            l_dst,
        );
    };

    #[cfg(not(target_arch = "wasm32"))]
    {
        out_mu
            .par_chunks_mut(3 * cols)
            .enumerate()
            .for_each(|(combo_idx, chunk)| do_row(combo_idx, chunk));
    }
    #[cfg(target_arch = "wasm32")]
    {
        for (combo_idx, chunk) in out_mu.chunks_mut(3 * cols).enumerate() {
            do_row(combo_idx, chunk);
        }
    }

    Ok(())
}

pub fn lpc_batch(
    high: &[f64],
    low: &[f64],
    close: &[f64],
    src: &[f64],
    sweep: &LpcBatchRange,
) -> Result<LpcBatchOutput, LpcError> {
    lpc_batch_with_kernel(high, low, close, src, sweep, Kernel::Auto)
}

pub fn lpc_batch_slice(
    high: &[f64],
    low: &[f64],
    close: &[f64],
    src: &[f64],
    sweep: &LpcBatchRange,
) -> Result<LpcBatchOutput, LpcError> {
    lpc_batch_with_kernel(high, low, close, src, sweep, detect_best_batch_kernel())
}

#[cfg(not(target_arch = "wasm32"))]
pub fn lpc_batch_par_slice(
    high: &[f64],
    low: &[f64],
    close: &[f64],
    src: &[f64],
    sweep: &LpcBatchRange,
) -> Result<LpcBatchOutput, LpcError> {
    lpc_batch_with_kernel(high, low, close, src, sweep, detect_best_batch_kernel())
}

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

    #[test]
    fn test_lpc_into_matches_api() -> Result<(), Box<dyn Error>> {
        let n = 256usize;
        let warm = 8usize;
        let mut ts = Vec::with_capacity(n);
        let mut open = Vec::with_capacity(n);
        let mut high = Vec::with_capacity(n);
        let mut low = Vec::with_capacity(n);
        let mut close = Vec::with_capacity(n);
        let mut vol = Vec::with_capacity(n);

        for i in 0..n {
            ts.push(i as i64);
            let base = 100.0 + 0.1 * (i as f64) + (i as f64 * 0.05).sin();
            if i < warm {
                open.push(f64::NAN);
                high.push(f64::NAN);
                low.push(f64::NAN);
                close.push(f64::NAN);
            } else {
                open.push(base - 0.2);
                high.push(base + 1.0);
                low.push(base - 1.0);
                close.push(base);
            }
            vol.push(1.0);
        }

        let candles = crate::utilities::data_loader::Candles::new(
            ts,
            open,
            high.clone(),
            low.clone(),
            close.clone(),
            vol,
        );
        let input = LpcInput::from_candles(&candles, "close", LpcParams::default());

        let baseline = lpc(&input)?;

        let mut f = vec![0.0; n];
        let mut hb = vec![0.0; n];
        let mut lb = vec![0.0; n];

        #[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
        {
            lpc_into(&input, &mut f, &mut hb, &mut lb)?;
        }
        #[cfg(all(target_arch = "wasm32", feature = "wasm"))]
        {
            lpc_into_slices(&mut f, &mut hb, &mut lb, &input, Kernel::Auto)?;
        }

        assert_eq!(f.len(), baseline.filter.len());
        assert_eq!(hb.len(), baseline.high_band.len());
        assert_eq!(lb.len(), baseline.low_band.len());

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

        for i in 0..n {
            assert!(
                eq_or_both_nan(f[i], baseline.filter[i]),
                "filter mismatch at {}: {} vs {}",
                i,
                f[i],
                baseline.filter[i]
            );
            assert!(
                eq_or_both_nan(hb[i], baseline.high_band[i]),
                "high band mismatch at {}: {} vs {}",
                i,
                hb[i],
                baseline.high_band[i]
            );
            assert!(
                eq_or_both_nan(lb[i], baseline.low_band[i]),
                "low band mismatch at {}: {} vs {}",
                i,
                lb[i],
                baseline.low_band[i]
            );
        }
        Ok(())
    }

    fn check_lpc_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 params = LpcParams::default();
        let input = LpcInput::from_candles(&candles, "close", params);
        let result = lpc_with_kernel(&input, kernel)?;

        let expected_filter = vec![
            59346.30519969,
            59327.59393858,
            59290.68770889,
            59257.83622820,
            59196.32617649,
        ];

        let expected_high_band = vec![
            60351.08358296,
            60220.19604722,
            60090.66513329,
            59981.40792457,
            59903.93414995,
        ];

        let expected_low_band = vec![
            58341.52681643,
            58434.99182994,
            58490.71028450,
            58534.26453184,
            58488.71820303,
        ];

        let start_idx = result.filter.len() - 5;
        for i in 0..5 {
            let filter_diff = (result.filter[start_idx + i] - expected_filter[i]).abs();
            let high_diff = (result.high_band[start_idx + i] - expected_high_band[i]).abs();
            let low_diff = (result.low_band[start_idx + i] - expected_low_band[i]).abs();

            assert!(
                filter_diff < 0.01,
                "[{}] LPC Filter {:?} mismatch at idx {}: got {}, expected {}",
                test_name,
                kernel,
                i,
                result.filter[start_idx + i],
                expected_filter[i]
            );

            assert!(
                high_diff < 0.01,
                "[{}] LPC High Band {:?} mismatch at idx {}: got {}, expected {}",
                test_name,
                kernel,
                i,
                result.high_band[start_idx + i],
                expected_high_band[i]
            );

            assert!(
                low_diff < 0.01,
                "[{}] LPC Low Band {:?} mismatch at idx {}: got {}, expected {}",
                test_name,
                kernel,
                i,
                result.low_band[start_idx + i],
                expected_low_band[i]
            );
        }

        Ok(())
    }

    fn check_lpc_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 = LpcParams {
            cutoff_type: None,
            fixed_period: None,
            max_cycle_limit: None,
            cycle_mult: None,
            tr_mult: None,
        };
        let input = LpcInput::from_candles(&candles, "close", params);
        let output = lpc_with_kernel(&input, kernel)?;
        assert_eq!(output.filter.len(), candles.close.len());

        Ok(())
    }

    fn check_lpc_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 = LpcInput::with_default_candles(&candles);
        match input.data {
            LpcData::Candles { source, .. } => assert_eq!(source, "close"),
            _ => panic!("Expected LpcData::Candles"),
        }
        let output = lpc_with_kernel(&input, kernel)?;
        assert_eq!(output.filter.len(), candles.close.len());

        Ok(())
    }

    fn check_lpc_zero_period(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
        skip_if_unsupported!(kernel, test_name);
        let data = vec![10.0, 20.0, 30.0];
        let params = LpcParams {
            cutoff_type: Some("fixed".to_string()),
            fixed_period: Some(0),
            max_cycle_limit: None,
            cycle_mult: None,
            tr_mult: None,
        };
        let input = LpcInput::from_slices(&data, &data, &data, &data, params);
        let res = lpc_with_kernel(&input, kernel);
        assert!(
            res.is_err(),
            "[{}] LPC should fail with zero period",
            test_name
        );
        Ok(())
    }

    fn check_lpc_period_exceeds_length(
        test_name: &str,
        kernel: Kernel,
    ) -> Result<(), Box<dyn Error>> {
        skip_if_unsupported!(kernel, test_name);
        let data = vec![10.0, 20.0, 30.0];
        let params = LpcParams {
            cutoff_type: Some("fixed".to_string()),
            fixed_period: Some(10),
            max_cycle_limit: None,
            cycle_mult: None,
            tr_mult: None,
        };
        let input = LpcInput::from_slices(&data, &data, &data, &data, params);
        let res = lpc_with_kernel(&input, kernel);
        assert!(
            res.is_err(),
            "[{}] LPC should fail with period exceeding length",
            test_name
        );
        Ok(())
    }

    fn check_lpc_very_small_dataset(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
        skip_if_unsupported!(kernel, test_name);
        let single_point = vec![42.0];
        let params = LpcParams {
            cutoff_type: Some("fixed".to_string()),
            fixed_period: Some(20),
            max_cycle_limit: None,
            cycle_mult: None,
            tr_mult: None,
        };
        let input = LpcInput::from_slices(
            &single_point,
            &single_point,
            &single_point,
            &single_point,
            params,
        );
        let res = lpc_with_kernel(&input, kernel);
        assert!(
            res.is_err(),
            "[{}] LPC should fail with insufficient data",
            test_name
        );
        Ok(())
    }

    fn check_lpc_empty_input(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
        skip_if_unsupported!(kernel, test_name);
        let empty: Vec<f64> = vec![];
        let params = LpcParams::default();
        let input = LpcInput::from_slices(&empty, &empty, &empty, &empty, params);

        let res = lpc_with_kernel(&input, kernel);
        assert!(
            matches!(res, Err(LpcError::EmptyInputData)),
            "[{}] LPC should fail with empty input",
            test_name
        );
        Ok(())
    }

    fn check_lpc_invalid_cutoff_type(
        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 = LpcParams {
            cutoff_type: Some("invalid".to_string()),
            fixed_period: Some(3),
            max_cycle_limit: None,
            cycle_mult: None,
            tr_mult: None,
        };
        let input = LpcInput::from_slices(&data, &data, &data, &data, params);
        let res = lpc_with_kernel(&input, kernel);
        assert!(
            matches!(res, Err(LpcError::InvalidCutoffType { .. })),
            "[{}] LPC should fail with invalid cutoff type",
            test_name
        );
        Ok(())
    }

    fn check_lpc_all_nan(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
        skip_if_unsupported!(kernel, test_name);
        let nan_data = vec![f64::NAN, f64::NAN, f64::NAN];
        let params = LpcParams::default();
        let input = LpcInput::from_slices(&nan_data, &nan_data, &nan_data, &nan_data, params);

        let res = lpc_with_kernel(&input, kernel);

        assert!(
            matches!(res, Err(LpcError::AllValuesNaN)),
            "[{}] LPC should fail with AllValuesNaN error",
            test_name
        );
        Ok(())
    }

    fn check_lpc_reinput(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 first_params = LpcParams {
            cutoff_type: Some("fixed".to_string()),
            fixed_period: Some(20),
            max_cycle_limit: None,
            cycle_mult: None,
            tr_mult: None,
        };
        let first_input = LpcInput::from_candles(&candles, "close", first_params);
        let first_result = lpc_with_kernel(&first_input, kernel)?;

        let second_params = LpcParams {
            cutoff_type: Some("fixed".to_string()),
            fixed_period: Some(20),
            max_cycle_limit: None,
            cycle_mult: None,
            tr_mult: None,
        };
        let second_input = LpcInput::from_slices(
            &candles.high,
            &candles.low,
            &candles.close,
            &first_result.filter,
            second_params,
        );
        let second_result = lpc_with_kernel(&second_input, kernel)?;

        assert_eq!(second_result.filter.len(), first_result.filter.len());
        Ok(())
    }

    fn check_lpc_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 = LpcInput::from_candles(
            &candles,
            "close",
            LpcParams {
                cutoff_type: Some("fixed".to_string()),
                fixed_period: Some(20),
                max_cycle_limit: None,
                cycle_mult: None,
                tr_mult: None,
            },
        );
        let res = lpc_with_kernel(&input, kernel)?;
        assert_eq!(res.filter.len(), candles.close.len());
        if res.filter.len() > 240 {
            for (i, &val) in res.filter[240..].iter().enumerate() {
                assert!(
                    !val.is_nan(),
                    "[{}] Found unexpected NaN at out-index {}",
                    test_name,
                    240 + i
                );
            }
        }
        Ok(())
    }

    fn check_lpc_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 cutoff_type = "fixed".to_string();
        let fixed_period = 20;
        let max_cycle_limit = 60;
        let cycle_mult = 1.0;
        let tr_mult = 1.0;

        let input = LpcInput::from_candles(
            &candles,
            "close",
            LpcParams {
                cutoff_type: Some(cutoff_type.clone()),
                fixed_period: Some(fixed_period),
                max_cycle_limit: Some(max_cycle_limit),
                cycle_mult: Some(cycle_mult),
                tr_mult: Some(tr_mult),
            },
        );
        let batch_output = lpc_with_kernel(&input, kernel)?;

        let mut stream = LpcStream::try_new(LpcParams {
            cutoff_type: Some(cutoff_type),
            fixed_period: Some(fixed_period),
            max_cycle_limit: Some(max_cycle_limit),
            cycle_mult: Some(cycle_mult),
            tr_mult: Some(tr_mult),
        })?;

        let mut stream_filter = Vec::with_capacity(candles.close.len());
        let mut stream_high = Vec::with_capacity(candles.close.len());
        let mut stream_low = Vec::with_capacity(candles.close.len());

        for i in 0..candles.close.len() {
            match stream.update(
                candles.high[i],
                candles.low[i],
                candles.close[i],
                candles.close[i],
            ) {
                Some((f, h, l)) => {
                    stream_filter.push(f);
                    stream_high.push(h);
                    stream_low.push(l);
                }
                None => {
                    stream_filter.push(f64::NAN);
                    stream_high.push(f64::NAN);
                    stream_low.push(f64::NAN);
                }
            }
        }

        assert_eq!(batch_output.filter.len(), stream_filter.len());

        for i in 20..100.min(stream_filter.len()) {
            if !stream_filter[i].is_nan() {
                assert!(
                    stream_low[i] <= stream_filter[i] && stream_filter[i] <= stream_high[i],
                    "[{}] Stream filter not between bands at idx {}",
                    test_name,
                    i
                );
            }
        }
        Ok(())
    }

    #[cfg(debug_assertions)]
    fn check_lpc_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![
            LpcParams::default(),
            LpcParams {
                cutoff_type: Some("fixed".to_string()),
                fixed_period: Some(10),
                max_cycle_limit: Some(30),
                cycle_mult: Some(0.5),
                tr_mult: Some(0.5),
            },
            LpcParams {
                cutoff_type: Some("adaptive".to_string()),
                fixed_period: Some(20),
                max_cycle_limit: Some(60),
                cycle_mult: Some(1.0),
                tr_mult: Some(1.0),
            },
            LpcParams {
                cutoff_type: Some("fixed".to_string()),
                fixed_period: Some(50),
                max_cycle_limit: Some(100),
                cycle_mult: Some(2.0),
                tr_mult: Some(2.0),
            },
        ];

        for (param_idx, params) in test_params.iter().enumerate() {
            let input = LpcInput::from_candles(&candles, "close", params.clone());
            let output = lpc_with_kernel(&input, kernel)?;

            for i in 0..output.filter.len() {
                let f = output.filter[i];
                let hi = output.high_band[i];
                let lo = output.low_band[i];

                for &val in &[f, hi, lo] {
                    if val.is_nan() {
                        continue;
                    }
                    let bits = val.to_bits();
                    if bits == 0x11111111_11111111 {
                        panic!("[{}] alloc_with_nan_prefix poison at {}", test_name, i);
                    }
                    if bits == 0x22222222_22222222 {
                        panic!("[{}] init_matrix_prefixes poison at {}", test_name, i);
                    }
                    if bits == 0x33333333_33333333 {
                        panic!("[{}] make_uninit_matrix poison at {}", test_name, i);
                    }
                }
            }
        }

        Ok(())
    }

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

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

        let strat = (3usize..=50).prop_flat_map(|period| {
            (
                prop::collection::vec(
                    (100.0f64..200.0f64).prop_filter("finite", |x| x.is_finite()),
                    period..400,
                ),
                Just(period),
                0.5f64..2.0f64,
                0.5f64..2.0f64,
            )
        });

        proptest::test_runner::TestRunner::default()
            .run(&strat, |(data, period, cycle_mult, tr_mult)| {
                let params = LpcParams {
                    cutoff_type: Some("fixed".to_string()),
                    fixed_period: Some(period),
                    max_cycle_limit: Some(60),
                    cycle_mult: Some(cycle_mult),
                    tr_mult: Some(tr_mult),
                };
                let input = LpcInput::from_slices(&data, &data, &data, &data, params);

                let result = lpc_with_kernel(&input, kernel).unwrap();
                let ref_result = lpc_with_kernel(&input, Kernel::Scalar).unwrap();

                prop_assert_eq!(result.filter.len(), data.len());
                prop_assert_eq!(result.high_band.len(), data.len());
                prop_assert_eq!(result.low_band.len(), data.len());

                let check_start = (period * 2).min(data.len());
                for i in check_start..data.len() {
                    let f = result.filter[i];
                    let h = result.high_band[i];
                    let l = result.low_band[i];

                    if !f.is_nan() && !h.is_nan() && !l.is_nan() {
                        prop_assert!(f.is_finite(), "filter at {i} not finite");
                        prop_assert!(h.is_finite(), "high_band at {i} not finite");
                        prop_assert!(l.is_finite(), "low_band at {i} not finite");
                    }

                    if !f.is_nan() && !ref_result.filter[i].is_nan() {
                        let diff = (f - ref_result.filter[i]).abs();
                        prop_assert!(
                            diff <= 1e-9,
                            "mismatch idx {i}: {} vs {} (diff={})",
                            f,
                            ref_result.filter[i],
                            diff
                        );
                    }
                }
                Ok(())
            })
            .unwrap();

        Ok(())
    }

    fn check_lpc_fixed_mode(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 = LpcParams {
            cutoff_type: Some("fixed".to_string()),
            fixed_period: Some(20),
            max_cycle_limit: Some(60),
            cycle_mult: Some(1.0),
            tr_mult: Some(1.0),
        };

        let input = LpcInput::from_candles(&candles, "close", params);
        let result = lpc_with_kernel(&input, kernel)?;

        assert_eq!(result.filter.len(), candles.close.len());
        assert_eq!(result.high_band.len(), candles.close.len());
        assert_eq!(result.low_band.len(), candles.close.len());

        Ok(())
    }

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

    generate_all_lpc_tests!(
        check_lpc_accuracy,
        check_lpc_partial_params,
        check_lpc_default_candles,
        check_lpc_zero_period,
        check_lpc_period_exceeds_length,
        check_lpc_very_small_dataset,
        check_lpc_empty_input,
        check_lpc_invalid_cutoff_type,
        check_lpc_all_nan,
        check_lpc_reinput,
        check_lpc_nan_handling,
        check_lpc_streaming,
        check_lpc_fixed_mode,
        check_lpc_no_poison
    );

    #[cfg(feature = "proptest")]
    generate_all_lpc_tests!(check_lpc_property);

    #[test]
    fn test_lpc_streaming_basic() {
        let params = LpcParams {
            cutoff_type: Some("fixed".to_string()),
            fixed_period: Some(10),
            max_cycle_limit: Some(60),
            cycle_mult: Some(1.0),
            tr_mult: Some(1.0),
        };

        let mut stream = LpcStream::try_new(params).unwrap();

        let test_data = vec![
            (100.0, 95.0, 98.0, 98.0),
            (102.0, 97.0, 101.0, 101.0),
            (105.0, 100.0, 104.0, 104.0),
            (103.0, 99.0, 100.0, 100.0),
            (104.0, 98.0, 102.0, 102.0),
            (106.0, 101.0, 105.0, 105.0),
            (108.0, 103.0, 107.0, 107.0),
            (107.0, 104.0, 106.0, 106.0),
            (109.0, 105.0, 108.0, 108.0),
            (110.0, 106.0, 109.0, 109.0),
        ];

        for (high, low, close, src) in test_data {
            let result = stream.update(high, low, close, src);
            if let Some((filter, high_band, low_band)) = result {
                assert!(filter >= low_band);
                assert!(filter <= high_band);
                assert!(high_band > low_band);
            }
        }
    }

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

        let sweep = LpcBatchRange {
            fixed_period: (10, 12, 1),
            cycle_mult: (1.0, 1.0, 0.0),
            tr_mult: (1.0, 1.0, 0.0),
            cutoff_type: "fixed".to_string(),
            max_cycle_limit: 60,
        };
        let out = lpc_batch_with_kernel(&c.high, &c.low, &c.close, &c.close, &sweep, kernel)?;
        let combos = 3;
        assert_eq!(out.rows, combos * 3);
        assert_eq!(out.cols, c.close.len());
        assert_eq!(out.values.len(), out.rows * out.cols);
        Ok(())
    }

    #[cfg(debug_assertions)]
    fn check_batch_no_poison(test: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
        skip_if_unsupported!(kernel, test);
        let file = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
        let c = read_candles_from_csv(file)?;
        let sweep = LpcBatchRange::default();
        let out = lpc_batch_with_kernel(&c.high, &c.low, &c.close, &c.close, &sweep, kernel)?;
        for &v in &out.values {
            if v.is_nan() {
                continue;
            }
            let b = v.to_bits();
            assert!(
                b != 0x11111111_11111111 && b != 0x22222222_22222222 && b != 0x33333333_33333333,
                "[{}] found poison value",
                test
            );
        }
        Ok(())
    }

    macro_rules! gen_lpc_batch_tests {
        ($name:ident) => {
            paste::paste! {
                #[test] fn [<$name _scalar>]() { let _ = $name(stringify!([<$name _scalar>]), Kernel::ScalarBatch); }
                #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
                #[test] fn [<$name _avx2>]()   { let _ = $name(stringify!([<$name _avx2>]), Kernel::Avx2Batch); }
                #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
                #[test] fn [<$name _avx512>]() { let _ = $name(stringify!([<$name _avx512>]), Kernel::Avx512Batch); }
                #[test] fn [<$name _auto>]()   { let _ = $name(stringify!([<$name _auto>]), Kernel::Auto); }
            }
        }
    }
    gen_lpc_batch_tests!(check_batch_shapes);
    #[cfg(debug_assertions)]
    gen_lpc_batch_tests!(check_batch_no_poison);
}
