use std::collections::VecDeque;
use std::sync::Arc;

use arrow::array::{Array, ArrayRef, BooleanArray, Float64Array};
use bt_kernel::{BtError, Result};

use crate::evaluator::coerce_f64;

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

fn extract_f64(input: &ArrayRef) -> Result<Vec<f64>> {
    let arr = coerce_f64(input)?;
    let len = arr.len();
    if arr.null_count() == 0 {
        Ok(arr.values().as_ref().to_vec())
    } else {
        let mut v = Vec::with_capacity(len);
        for i in 0..len {
            v.push(if arr.is_null(i) {
                f64::NAN
            } else {
                arr.value(i)
            });
        }
        Ok(v)
    }
}

fn to_f64_array(vals: Vec<f64>) -> ArrayRef {
    Arc::new(Float64Array::from(vals))
}

fn expect_inputs(inputs: &[ArrayRef], n: usize, name: &str) -> Result<()> {
    if inputs.len() != n {
        return Err(BtError::Expression(format!(
            "{name} expects {n} inputs, got {}",
            inputs.len()
        )));
    }
    Ok(())
}

/// Compute EMA in-place, returning a Vec. First value seeds the EMA.
fn compute_ema(vals: &[f64], period: usize) -> Vec<f64> {
    let len = vals.len();
    let mut out = vec![f64::NAN; len];
    if len == 0 || period == 0 {
        return out;
    }
    let alpha = 2.0 / (period as f64 + 1.0);
    let mut ema = f64::NAN;
    for i in 0..len {
        let v = vals[i];
        if v.is_nan() {
            continue;
        }
        if ema.is_nan() {
            ema = v;
        } else {
            ema = alpha * v + (1.0 - alpha) * ema;
        }
        out[i] = ema;
    }
    out
}

/// Compute WMA (linearly weighted) over a window.
fn compute_wma(vals: &[f64], window: usize) -> Vec<f64> {
    let len = vals.len();
    let mut out = vec![f64::NAN; len];
    if window == 0 || len < window {
        return out;
    }
    let denom = (window * (window + 1)) as f64 / 2.0;
    for i in (window - 1)..len {
        let mut sum = 0.0;
        let mut has_nan = false;
        for j in 0..window {
            let v = vals[i + 1 - window + j];
            if v.is_nan() {
                has_nan = true;
                break;
            }
            sum += v * (j + 1) as f64;
        }
        if !has_nan {
            out[i] = sum / denom;
        }
    }
    out
}

/// Compute SMA over a window using an O(n) rolling sum.
fn compute_sma(vals: &[f64], window: usize) -> Vec<f64> {
    let len = vals.len();
    let w = window as f64;
    let mut out = vec![f64::NAN; len];
    if window == 0 || len < window {
        return out;
    }
    let mut sum = 0.0;
    let mut nan_count = 0usize;
    for i in 0..len {
        if vals[i].is_nan() {
            nan_count += 1;
        } else {
            sum += vals[i];
        }
        if i >= window {
            if vals[i - window].is_nan() {
                nan_count -= 1;
            } else {
                sum -= vals[i - window];
            }
        }
        if i + 1 >= window && nan_count == 0 {
            out[i] = sum / w;
        }
    }
    out
}

/// Compute rolling standard deviation.
fn compute_rolling_std(vals: &[f64], window: usize) -> Vec<f64> {
    let len = vals.len();
    let w = window as f64;
    let mut out = vec![f64::NAN; len];
    if window == 0 || len < window {
        return out;
    }
    let mut sum = 0.0;
    let mut sum_sq = 0.0;
    let mut nan_count = 0usize;
    for i in 0..len {
        if vals[i].is_nan() {
            nan_count += 1;
        } else {
            sum += vals[i];
            sum_sq += vals[i] * vals[i];
        }
        if i >= window {
            if vals[i - window].is_nan() {
                nan_count -= 1;
            } else {
                sum -= vals[i - window];
                sum_sq -= vals[i - window] * vals[i - window];
            }
        }
        if i + 1 >= window && nan_count == 0 {
            let mean = sum / w;
            let var = (sum_sq / w - mean * mean).max(0.0);
            out[i] = var.sqrt();
        }
    }
    out
}

/// Compute True Range vector from H, L, C slices.
fn compute_tr(h: &[f64], l: &[f64], c: &[f64]) -> Vec<f64> {
    let len = h.len();
    let mut tr = vec![f64::NAN; len];
    if len == 0 {
        return tr;
    }
    tr[0] = h[0] - l[0]; // no prev close for first bar
    for i in 1..len {
        if h[i].is_nan() || l[i].is_nan() || c[i - 1].is_nan() {
            continue;
        }
        tr[i] = (h[i] - l[i])
            .max((h[i] - c[i - 1]).abs())
            .max((l[i] - c[i - 1]).abs());
    }
    tr
}

/// Compute ATR using Wilder's smoothing.
fn compute_atr(h: &[f64], l: &[f64], c: &[f64], period: usize) -> Vec<f64> {
    let tr = compute_tr(h, l, c);
    let len = tr.len();
    let mut out = vec![f64::NAN; len];
    if len <= period || period == 0 {
        return out;
    }
    let p = period as f64;
    let mut tr_sum = 0.0;
    for i in 1..=period {
        if tr[i].is_nan() {
            return out;
        }
        tr_sum += tr[i];
    }
    let mut atr_val = tr_sum / p;
    out[period] = atr_val;
    for i in (period + 1)..len {
        if tr[i].is_nan() {
            continue;
        }
        atr_val = (atr_val * (p - 1.0) + tr[i]) / p;
        out[i] = atr_val;
    }
    out
}

// ---------------------------------------------------------------------------
// Moving Averages
// ---------------------------------------------------------------------------

/// Double Exponential Moving Average: 2*EMA - EMA(EMA)
pub fn dema(inputs: &[ArrayRef], period: usize) -> Result<ArrayRef> {
    expect_inputs(inputs, 1, "dema")?;
    let vals = extract_f64(&inputs[0])?;
    let ema1 = compute_ema(&vals, period);
    let ema2 = compute_ema(&ema1, period);
    let out: Vec<f64> = ema1
        .iter()
        .zip(ema2.iter())
        .map(|(&e1, &e2)| {
            if e1.is_nan() || e2.is_nan() {
                f64::NAN
            } else {
                2.0 * e1 - e2
            }
        })
        .collect();
    Ok(to_f64_array(out))
}

/// Triple Exponential Moving Average: 3*EMA - 3*EMA(EMA) + EMA(EMA(EMA))
pub fn tema(inputs: &[ArrayRef], period: usize) -> Result<ArrayRef> {
    expect_inputs(inputs, 1, "tema")?;
    let vals = extract_f64(&inputs[0])?;
    let ema1 = compute_ema(&vals, period);
    let ema2 = compute_ema(&ema1, period);
    let ema3 = compute_ema(&ema2, period);
    let out: Vec<f64> = (0..vals.len())
        .map(|i| {
            let (e1, e2, e3) = (ema1[i], ema2[i], ema3[i]);
            if e1.is_nan() || e2.is_nan() || e3.is_nan() {
                f64::NAN
            } else {
                3.0 * e1 - 3.0 * e2 + e3
            }
        })
        .collect();
    Ok(to_f64_array(out))
}

/// Weighted Moving Average
pub fn wma(inputs: &[ArrayRef], window: usize) -> Result<ArrayRef> {
    expect_inputs(inputs, 1, "wma")?;
    let vals = extract_f64(&inputs[0])?;
    Ok(to_f64_array(compute_wma(&vals, window)))
}

/// Hull Moving Average: WMA(2*WMA(n/2) - WMA(n), sqrt(n))
pub fn hma(inputs: &[ArrayRef], window: usize) -> Result<ArrayRef> {
    expect_inputs(inputs, 1, "hma")?;
    if window < 2 {
        return Err(BtError::Expression("hma window must be >= 2".into()));
    }
    let vals = extract_f64(&inputs[0])?;
    let half = window / 2;
    let sqrt_w = (window as f64).sqrt().round() as usize;
    let wma_half = compute_wma(&vals, half.max(1));
    let wma_full = compute_wma(&vals, window);
    let diff: Vec<f64> = wma_half
        .iter()
        .zip(wma_full.iter())
        .map(|(&h, &f)| {
            if h.is_nan() || f.is_nan() {
                f64::NAN
            } else {
                2.0 * h - f
            }
        })
        .collect();
    Ok(to_f64_array(compute_wma(&diff, sqrt_w.max(1))))
}

/// Kaufman Adaptive Moving Average
pub fn kama(inputs: &[ArrayRef], period: usize) -> Result<ArrayRef> {
    expect_inputs(inputs, 1, "kama")?;
    if period == 0 {
        return Err(BtError::Expression("kama period must be > 0".into()));
    }
    let vals = extract_f64(&inputs[0])?;
    let len = vals.len();
    let mut out = vec![f64::NAN; len];
    if len <= period {
        return Ok(to_f64_array(out));
    }
    let fast_sc = 2.0 / (2.0 + 1.0); // fast period = 2
    let slow_sc = 2.0 / (30.0 + 1.0); // slow period = 30

    out[period - 1] = vals[period - 1];
    let mut kama_val = vals[period - 1];

    for i in period..len {
        let direction = (vals[i] - vals[i - period]).abs();
        let mut volatility = 0.0;
        for j in (i - period + 1)..=i {
            volatility += (vals[j] - vals[j - 1]).abs();
        }
        let er = if volatility == 0.0 {
            0.0
        } else {
            direction / volatility
        };
        let sc = (er * (fast_sc - slow_sc) + slow_sc).powi(2);
        kama_val += sc * (vals[i] - kama_val);
        out[i] = kama_val;
    }
    Ok(to_f64_array(out))
}

// ---------------------------------------------------------------------------
// Momentum
// ---------------------------------------------------------------------------

/// Rate of Change: (close - close[n]) / close[n] * 100
pub fn roc(inputs: &[ArrayRef], period: usize) -> Result<ArrayRef> {
    expect_inputs(inputs, 1, "roc")?;
    let vals = extract_f64(&inputs[0])?;
    let len = vals.len();
    let mut out = vec![f64::NAN; len];
    for i in period..len {
        let prev = vals[i - period];
        if !prev.is_nan() && prev != 0.0 && !vals[i].is_nan() {
            out[i] = (vals[i] - prev) / prev * 100.0;
        }
    }
    Ok(to_f64_array(out))
}

/// MACD line: EMA(fast) - EMA(slow)
pub fn macd(inputs: &[ArrayRef], fast: usize, slow: usize) -> Result<ArrayRef> {
    expect_inputs(inputs, 1, "macd")?;
    let vals = extract_f64(&inputs[0])?;
    let ema_fast = compute_ema(&vals, fast);
    let ema_slow = compute_ema(&vals, slow);
    let out: Vec<f64> = ema_fast
        .iter()
        .zip(ema_slow.iter())
        .map(|(&f, &s)| {
            if f.is_nan() || s.is_nan() {
                f64::NAN
            } else {
                f - s
            }
        })
        .collect();
    Ok(to_f64_array(out))
}

/// MACD Signal line: EMA(MACD, signal_period)
pub fn macd_signal(
    inputs: &[ArrayRef],
    fast: usize,
    slow: usize,
    signal: usize,
) -> Result<ArrayRef> {
    expect_inputs(inputs, 1, "macd_signal")?;
    let vals = extract_f64(&inputs[0])?;
    let ema_fast = compute_ema(&vals, fast);
    let ema_slow = compute_ema(&vals, slow);
    let macd_line: Vec<f64> = ema_fast
        .iter()
        .zip(ema_slow.iter())
        .map(|(&f, &s)| {
            if f.is_nan() || s.is_nan() {
                f64::NAN
            } else {
                f - s
            }
        })
        .collect();
    Ok(to_f64_array(compute_ema(&macd_line, signal)))
}

/// MACD Histogram: MACD - Signal
pub fn macd_hist(inputs: &[ArrayRef], fast: usize, slow: usize, signal: usize) -> Result<ArrayRef> {
    expect_inputs(inputs, 1, "macd_hist")?;
    let vals = extract_f64(&inputs[0])?;
    let ema_fast = compute_ema(&vals, fast);
    let ema_slow = compute_ema(&vals, slow);
    let macd_line: Vec<f64> = ema_fast
        .iter()
        .zip(ema_slow.iter())
        .map(|(&f, &s)| {
            if f.is_nan() || s.is_nan() {
                f64::NAN
            } else {
                f - s
            }
        })
        .collect();
    let signal_line = compute_ema(&macd_line, signal);
    let out: Vec<f64> = macd_line
        .iter()
        .zip(signal_line.iter())
        .map(|(&m, &s)| {
            if m.is_nan() || s.is_nan() {
                f64::NAN
            } else {
                m - s
            }
        })
        .collect();
    Ok(to_f64_array(out))
}

/// Stochastic %K: (C - LL) / (HH - LL) * 100
pub fn stoch_k(inputs: &[ArrayRef], period: usize) -> Result<ArrayRef> {
    expect_inputs(inputs, 3, "stoch_k")?;
    let high = extract_f64(&inputs[0])?;
    let low = extract_f64(&inputs[1])?;
    let close = extract_f64(&inputs[2])?;
    let len = high.len();
    let mut out = vec![f64::NAN; len];
    if period == 0 || len < period {
        return Ok(to_f64_array(out));
    }
    // Use deques for O(n) rolling min/max
    let mut max_deque: VecDeque<usize> = VecDeque::new();
    let mut min_deque: VecDeque<usize> = VecDeque::new();
    for i in 0..len {
        while let Some(&front) = max_deque.front() {
            if front + period <= i {
                max_deque.pop_front();
            } else {
                break;
            }
        }
        while let Some(&front) = min_deque.front() {
            if front + period <= i {
                min_deque.pop_front();
            } else {
                break;
            }
        }
        if !high[i].is_nan() {
            while let Some(&back) = max_deque.back() {
                if high[back] <= high[i] {
                    max_deque.pop_back();
                } else {
                    break;
                }
            }
            max_deque.push_back(i);
        }
        if !low[i].is_nan() {
            while let Some(&back) = min_deque.back() {
                if low[back] >= low[i] {
                    min_deque.pop_back();
                } else {
                    break;
                }
            }
            min_deque.push_back(i);
        }
        if i + 1 >= period && !close[i].is_nan() {
            if let (Some(&max_idx), Some(&min_idx)) = (max_deque.front(), min_deque.front()) {
                let hh = high[max_idx];
                let ll = low[min_idx];
                let range = hh - ll;
                if range != 0.0 {
                    out[i] = (close[i] - ll) / range * 100.0;
                } else {
                    out[i] = 50.0;
                }
            }
        }
    }
    Ok(to_f64_array(out))
}

/// Williams %R: (HH - C) / (HH - LL) * -100
pub fn williams_r(inputs: &[ArrayRef], period: usize) -> Result<ArrayRef> {
    expect_inputs(inputs, 3, "williams_r")?;
    let high = extract_f64(&inputs[0])?;
    let low = extract_f64(&inputs[1])?;
    let close = extract_f64(&inputs[2])?;
    let len = high.len();
    let mut out = vec![f64::NAN; len];
    if period == 0 || len < period {
        return Ok(to_f64_array(out));
    }
    let mut max_deque: VecDeque<usize> = VecDeque::new();
    let mut min_deque: VecDeque<usize> = VecDeque::new();
    for i in 0..len {
        while let Some(&front) = max_deque.front() {
            if front + period <= i {
                max_deque.pop_front();
            } else {
                break;
            }
        }
        while let Some(&front) = min_deque.front() {
            if front + period <= i {
                min_deque.pop_front();
            } else {
                break;
            }
        }
        if !high[i].is_nan() {
            while let Some(&back) = max_deque.back() {
                if high[back] <= high[i] {
                    max_deque.pop_back();
                } else {
                    break;
                }
            }
            max_deque.push_back(i);
        }
        if !low[i].is_nan() {
            while let Some(&back) = min_deque.back() {
                if low[back] >= low[i] {
                    min_deque.pop_back();
                } else {
                    break;
                }
            }
            min_deque.push_back(i);
        }
        if i + 1 >= period && !close[i].is_nan() {
            if let (Some(&max_idx), Some(&min_idx)) = (max_deque.front(), min_deque.front()) {
                let hh = high[max_idx];
                let ll = low[min_idx];
                let range = hh - ll;
                if range != 0.0 {
                    out[i] = (hh - close[i]) / range * -100.0;
                } else {
                    out[i] = -50.0;
                }
            }
        }
    }
    Ok(to_f64_array(out))
}

/// Commodity Channel Index: (TP - SMA(TP)) / (0.015 * mean_deviation)
pub fn cci(inputs: &[ArrayRef], period: usize) -> Result<ArrayRef> {
    expect_inputs(inputs, 3, "cci")?;
    let high = extract_f64(&inputs[0])?;
    let low = extract_f64(&inputs[1])?;
    let close = extract_f64(&inputs[2])?;
    let len = high.len();
    let mut out = vec![f64::NAN; len];
    if period == 0 || len < period {
        return Ok(to_f64_array(out));
    }
    // Typical price
    let tp: Vec<f64> = (0..len)
        .map(|i| {
            if high[i].is_nan() || low[i].is_nan() || close[i].is_nan() {
                f64::NAN
            } else {
                (high[i] + low[i] + close[i]) / 3.0
            }
        })
        .collect();
    let sma_tp = compute_sma(&tp, period);
    for i in (period - 1)..len {
        if sma_tp[i].is_nan() {
            continue;
        }
        // Mean deviation over the window
        let mut md = 0.0;
        for j in (i + 1 - period)..=i {
            md += (tp[j] - sma_tp[i]).abs();
        }
        md /= period as f64;
        if md == 0.0 {
            out[i] = 0.0;
        } else {
            out[i] = (tp[i] - sma_tp[i]) / (0.015 * md);
        }
    }
    Ok(to_f64_array(out))
}

/// Average Directional Index
pub fn adx(inputs: &[ArrayRef], period: usize) -> Result<ArrayRef> {
    expect_inputs(inputs, 3, "adx")?;
    let high = extract_f64(&inputs[0])?;
    let low = extract_f64(&inputs[1])?;
    let close = extract_f64(&inputs[2])?;
    let len = high.len();
    let mut out = vec![f64::NAN; len];
    if period == 0 || len <= 2 * period {
        return Ok(to_f64_array(out));
    }
    let p = period as f64;

    // 1. Compute +DM, -DM, TR for each bar
    let mut plus_dm = vec![0.0f64; len];
    let mut minus_dm = vec![0.0f64; len];
    let tr = compute_tr(&high, &low, &close);

    for i in 1..len {
        let up = high[i] - high[i - 1];
        let down = low[i - 1] - low[i];
        if up > down && up > 0.0 {
            plus_dm[i] = up;
        }
        if down > up && down > 0.0 {
            minus_dm[i] = down;
        }
    }

    // 2. Wilder's smoothing of +DM, -DM, TR over period
    let mut atr_val = 0.0;
    let mut plus_dm_smooth = 0.0;
    let mut minus_dm_smooth = 0.0;
    for i in 1..=period {
        if tr[i].is_nan() {
            return Ok(to_f64_array(out));
        }
        atr_val += tr[i];
        plus_dm_smooth += plus_dm[i];
        minus_dm_smooth += minus_dm[i];
    }

    // 3. Compute DX starting at index=period, then smooth to get ADX
    let mut dx_vals = vec![f64::NAN; len];
    let plus_di = if atr_val == 0.0 {
        0.0
    } else {
        100.0 * plus_dm_smooth / atr_val
    };
    let minus_di = if atr_val == 0.0 {
        0.0
    } else {
        100.0 * minus_dm_smooth / atr_val
    };
    let di_sum = plus_di + minus_di;
    dx_vals[period] = if di_sum == 0.0 {
        0.0
    } else {
        100.0 * (plus_di - minus_di).abs() / di_sum
    };

    for i in (period + 1)..len {
        if tr[i].is_nan() {
            continue;
        }
        atr_val = (atr_val * (p - 1.0) + tr[i]) / p;
        plus_dm_smooth = (plus_dm_smooth * (p - 1.0) + plus_dm[i]) / p;
        minus_dm_smooth = (minus_dm_smooth * (p - 1.0) + minus_dm[i]) / p;
        let pdi = if atr_val == 0.0 {
            0.0
        } else {
            100.0 * plus_dm_smooth / atr_val
        };
        let mdi = if atr_val == 0.0 {
            0.0
        } else {
            100.0 * minus_dm_smooth / atr_val
        };
        let s = pdi + mdi;
        dx_vals[i] = if s == 0.0 {
            0.0
        } else {
            100.0 * (pdi - mdi).abs() / s
        };
    }

    // 4. ADX = Wilder's smoothing of DX over period, starting at index 2*period
    let mut adx_sum = 0.0;
    for i in period..=(2 * period - 1) {
        if dx_vals[i].is_nan() {
            return Ok(to_f64_array(out));
        }
        adx_sum += dx_vals[i];
    }
    let mut adx_val = adx_sum / p;
    out[2 * period - 1] = adx_val;

    for i in (2 * period)..len {
        if dx_vals[i].is_nan() {
            continue;
        }
        adx_val = (adx_val * (p - 1.0) + dx_vals[i]) / p;
        out[i] = adx_val;
    }

    Ok(to_f64_array(out))
}

// ---------------------------------------------------------------------------
// Volatility
// ---------------------------------------------------------------------------

/// True Range: max(H-L, |H-prevC|, |L-prevC|) per bar.
pub fn true_range(inputs: &[ArrayRef]) -> Result<ArrayRef> {
    expect_inputs(inputs, 3, "true_range")?;
    let high = extract_f64(&inputs[0])?;
    let low = extract_f64(&inputs[1])?;
    let close = extract_f64(&inputs[2])?;
    Ok(to_f64_array(compute_tr(&high, &low, &close)))
}

/// Normalized ATR: ATR / close * 100
pub fn natr(inputs: &[ArrayRef], period: usize) -> Result<ArrayRef> {
    expect_inputs(inputs, 3, "natr")?;
    let high = extract_f64(&inputs[0])?;
    let low = extract_f64(&inputs[1])?;
    let close = extract_f64(&inputs[2])?;
    let atr_vals = compute_atr(&high, &low, &close, period);
    let out: Vec<f64> = atr_vals
        .iter()
        .zip(close.iter())
        .map(|(&a, &c)| {
            if a.is_nan() || c.is_nan() || c == 0.0 {
                f64::NAN
            } else {
                a / c * 100.0
            }
        })
        .collect();
    Ok(to_f64_array(out))
}

/// Bollinger Upper Band: SMA + num_std * rolling_std
pub fn bollinger_upper(inputs: &[ArrayRef], window: usize, num_std: f64) -> Result<ArrayRef> {
    expect_inputs(inputs, 1, "bollinger_upper")?;
    let vals = extract_f64(&inputs[0])?;
    let sma = compute_sma(&vals, window);
    let std = compute_rolling_std(&vals, window);
    let out: Vec<f64> = sma
        .iter()
        .zip(std.iter())
        .map(|(&m, &s)| {
            if m.is_nan() || s.is_nan() {
                f64::NAN
            } else {
                m + num_std * s
            }
        })
        .collect();
    Ok(to_f64_array(out))
}

/// Bollinger Lower Band: SMA - num_std * rolling_std
pub fn bollinger_lower(inputs: &[ArrayRef], window: usize, num_std: f64) -> Result<ArrayRef> {
    expect_inputs(inputs, 1, "bollinger_lower")?;
    let vals = extract_f64(&inputs[0])?;
    let sma = compute_sma(&vals, window);
    let std = compute_rolling_std(&vals, window);
    let out: Vec<f64> = sma
        .iter()
        .zip(std.iter())
        .map(|(&m, &s)| {
            if m.is_nan() || s.is_nan() {
                f64::NAN
            } else {
                m - num_std * s
            }
        })
        .collect();
    Ok(to_f64_array(out))
}

/// Bollinger Bandwidth: (upper - lower) / middle
pub fn bollinger_width(inputs: &[ArrayRef], window: usize, num_std: f64) -> Result<ArrayRef> {
    expect_inputs(inputs, 1, "bollinger_width")?;
    let vals = extract_f64(&inputs[0])?;
    let sma = compute_sma(&vals, window);
    let std = compute_rolling_std(&vals, window);
    let out: Vec<f64> = sma
        .iter()
        .zip(std.iter())
        .map(|(&m, &s)| {
            if m.is_nan() || s.is_nan() || m == 0.0 {
                f64::NAN
            } else {
                2.0 * num_std * s / m
            }
        })
        .collect();
    Ok(to_f64_array(out))
}

/// Keltner Upper Channel: EMA(close) + multiplier * ATR
pub fn keltner_upper(inputs: &[ArrayRef], period: usize, multiplier: f64) -> Result<ArrayRef> {
    expect_inputs(inputs, 3, "keltner_upper")?;
    let high = extract_f64(&inputs[0])?;
    let low = extract_f64(&inputs[1])?;
    let close = extract_f64(&inputs[2])?;
    let ema_c = compute_ema(&close, period);
    let atr_vals = compute_atr(&high, &low, &close, period);
    let out: Vec<f64> = ema_c
        .iter()
        .zip(atr_vals.iter())
        .map(|(&e, &a)| {
            if e.is_nan() || a.is_nan() {
                f64::NAN
            } else {
                e + multiplier * a
            }
        })
        .collect();
    Ok(to_f64_array(out))
}

/// Keltner Lower Channel: EMA(close) - multiplier * ATR
pub fn keltner_lower(inputs: &[ArrayRef], period: usize, multiplier: f64) -> Result<ArrayRef> {
    expect_inputs(inputs, 3, "keltner_lower")?;
    let high = extract_f64(&inputs[0])?;
    let low = extract_f64(&inputs[1])?;
    let close = extract_f64(&inputs[2])?;
    let ema_c = compute_ema(&close, period);
    let atr_vals = compute_atr(&high, &low, &close, period);
    let out: Vec<f64> = ema_c
        .iter()
        .zip(atr_vals.iter())
        .map(|(&e, &a)| {
            if e.is_nan() || a.is_nan() {
                f64::NAN
            } else {
                e - multiplier * a
            }
        })
        .collect();
    Ok(to_f64_array(out))
}

/// SuperTrend indicator.
/// Returns the supertrend line value (lower band in uptrend, upper band in downtrend).
pub fn supertrend(inputs: &[ArrayRef], period: usize, multiplier: f64) -> Result<ArrayRef> {
    expect_inputs(inputs, 3, "supertrend")?;
    let high = extract_f64(&inputs[0])?;
    let low = extract_f64(&inputs[1])?;
    let close = extract_f64(&inputs[2])?;
    let atr_vals = compute_atr(&high, &low, &close, period);
    let len = high.len();
    let mut out = vec![f64::NAN; len];
    if len <= period {
        return Ok(to_f64_array(out));
    }

    let mut upper_band;
    let mut lower_band;
    let mut in_uptrend;

    // Start from the first valid ATR
    let start = period;
    let hl2 = (high[start] + low[start]) / 2.0;
    let mut prev_upper = hl2 + multiplier * atr_vals[start];
    let mut prev_lower = hl2 - multiplier * atr_vals[start];
    in_uptrend = close[start] > prev_upper;
    out[start] = if in_uptrend { prev_lower } else { prev_upper };

    for i in (start + 1)..len {
        if atr_vals[i].is_nan() {
            continue;
        }
        let hl2 = (high[i] + low[i]) / 2.0;
        let basic_upper = hl2 + multiplier * atr_vals[i];
        let basic_lower = hl2 - multiplier * atr_vals[i];

        upper_band = if basic_upper < prev_upper || close[i - 1] > prev_upper {
            basic_upper
        } else {
            prev_upper
        };
        lower_band = if basic_lower > prev_lower || close[i - 1] < prev_lower {
            basic_lower
        } else {
            prev_lower
        };

        if in_uptrend {
            if close[i] < lower_band {
                in_uptrend = false;
                out[i] = upper_band;
            } else {
                out[i] = lower_band;
            }
        } else if close[i] > upper_band {
            in_uptrend = true;
            out[i] = lower_band;
        } else {
            out[i] = upper_band;
        }

        prev_upper = upper_band;
        prev_lower = lower_band;
    }

    Ok(to_f64_array(out))
}

// ---------------------------------------------------------------------------
// Volume
// ---------------------------------------------------------------------------

/// On Balance Volume: cumulative volume, + when close up, - when close down.
pub fn obv(inputs: &[ArrayRef]) -> Result<ArrayRef> {
    expect_inputs(inputs, 2, "obv")?;
    let close = extract_f64(&inputs[0])?;
    let volume = extract_f64(&inputs[1])?;
    let len = close.len();
    let mut out = vec![f64::NAN; len];
    if len == 0 {
        return Ok(to_f64_array(out));
    }
    let mut cumulative = 0.0;
    out[0] = 0.0;
    for i in 1..len {
        if close[i].is_nan() || close[i - 1].is_nan() || volume[i].is_nan() {
            out[i] = f64::NAN;
            continue;
        }
        if close[i] > close[i - 1] {
            cumulative += volume[i];
        } else if close[i] < close[i - 1] {
            cumulative -= volume[i];
        }
        out[i] = cumulative;
    }
    Ok(to_f64_array(out))
}

/// Cumulative VWAP: Σ(TP * V) / Σ(V) where TP = (H+L+C)/3
pub fn vwap(inputs: &[ArrayRef]) -> Result<ArrayRef> {
    expect_inputs(inputs, 4, "vwap")?;
    let high = extract_f64(&inputs[0])?;
    let low = extract_f64(&inputs[1])?;
    let close = extract_f64(&inputs[2])?;
    let volume = extract_f64(&inputs[3])?;
    let len = high.len();
    let mut out = vec![f64::NAN; len];
    let mut cum_tpv = 0.0;
    let mut cum_vol = 0.0;
    for i in 0..len {
        if high[i].is_nan() || low[i].is_nan() || close[i].is_nan() || volume[i].is_nan() {
            continue;
        }
        let tp = (high[i] + low[i] + close[i]) / 3.0;
        cum_tpv += tp * volume[i];
        cum_vol += volume[i];
        if cum_vol != 0.0 {
            out[i] = cum_tpv / cum_vol;
        }
    }
    Ok(to_f64_array(out))
}

/// Accumulation/Distribution Line: cumulative CLV * Volume
/// CLV = ((C-L) - (H-C)) / (H-L)
pub fn ad_line(inputs: &[ArrayRef]) -> Result<ArrayRef> {
    expect_inputs(inputs, 4, "ad_line")?;
    let high = extract_f64(&inputs[0])?;
    let low = extract_f64(&inputs[1])?;
    let close = extract_f64(&inputs[2])?;
    let volume = extract_f64(&inputs[3])?;
    let len = high.len();
    let mut out = vec![f64::NAN; len];
    let mut cumulative = 0.0;
    for i in 0..len {
        if high[i].is_nan() || low[i].is_nan() || close[i].is_nan() || volume[i].is_nan() {
            continue;
        }
        let range = high[i] - low[i];
        let clv = if range == 0.0 {
            0.0
        } else {
            ((close[i] - low[i]) - (high[i] - close[i])) / range
        };
        cumulative += clv * volume[i];
        out[i] = cumulative;
    }
    Ok(to_f64_array(out))
}

/// Money Flow Index: RSI applied to money flow.
pub fn mfi(inputs: &[ArrayRef], period: usize) -> Result<ArrayRef> {
    expect_inputs(inputs, 4, "mfi")?;
    let high = extract_f64(&inputs[0])?;
    let low = extract_f64(&inputs[1])?;
    let close = extract_f64(&inputs[2])?;
    let volume = extract_f64(&inputs[3])?;
    let len = high.len();
    let mut out = vec![f64::NAN; len];
    if period == 0 || len <= period {
        return Ok(to_f64_array(out));
    }

    // Typical price and raw money flow
    let tp: Vec<f64> = (0..len)
        .map(|i| {
            if high[i].is_nan() || low[i].is_nan() || close[i].is_nan() {
                f64::NAN
            } else {
                (high[i] + low[i] + close[i]) / 3.0
            }
        })
        .collect();

    // Positive/negative money flow over rolling window
    for i in period..len {
        let mut pos_flow = 0.0;
        let mut neg_flow = 0.0;
        for j in (i + 1 - period)..=i {
            if tp[j].is_nan() || tp[j - 1].is_nan() || volume[j].is_nan() {
                continue;
            }
            let mf = tp[j] * volume[j];
            if tp[j] > tp[j - 1] {
                pos_flow += mf;
            } else if tp[j] < tp[j - 1] {
                neg_flow += mf;
            }
        }
        if neg_flow == 0.0 {
            out[i] = 100.0;
        } else {
            let ratio = pos_flow / neg_flow;
            out[i] = 100.0 - 100.0 / (1.0 + ratio);
        }
    }
    Ok(to_f64_array(out))
}

// ---------------------------------------------------------------------------
// Signals
// ---------------------------------------------------------------------------

/// CrossAbove: a[i] > b[i] && a[i-1] <= b[i-1]
pub fn cross_above(inputs: &[ArrayRef]) -> Result<ArrayRef> {
    expect_inputs(inputs, 2, "cross_above")?;
    let a = extract_f64(&inputs[0])?;
    let b = extract_f64(&inputs[1])?;
    let len = a.len();
    let mut out = Vec::with_capacity(len);
    if len > 0 {
        out.push(Some(false));
    }
    for i in 1..len {
        if a[i].is_nan() || b[i].is_nan() || a[i - 1].is_nan() || b[i - 1].is_nan() {
            out.push(None);
        } else {
            out.push(Some(a[i] > b[i] && a[i - 1] <= b[i - 1]));
        }
    }
    Ok(Arc::new(BooleanArray::from(out)))
}

/// CrossBelow: a[i] < b[i] && a[i-1] >= b[i-1]
pub fn cross_below(inputs: &[ArrayRef]) -> Result<ArrayRef> {
    expect_inputs(inputs, 2, "cross_below")?;
    let a = extract_f64(&inputs[0])?;
    let b = extract_f64(&inputs[1])?;
    let len = a.len();
    let mut out = Vec::with_capacity(len);
    if len > 0 {
        out.push(Some(false));
    }
    for i in 1..len {
        if a[i].is_nan() || b[i].is_nan() || a[i - 1].is_nan() || b[i - 1].is_nan() {
            out.push(None);
        } else {
            out.push(Some(a[i] < b[i] && a[i - 1] >= b[i - 1]));
        }
    }
    Ok(Arc::new(BooleanArray::from(out)))
}

// ---------------------------------------------------------------------------
// Statistics
// ---------------------------------------------------------------------------

/// Rolling Median using a sorted insertion approach.
pub fn rolling_median(inputs: &[ArrayRef], window: usize) -> Result<ArrayRef> {
    expect_inputs(inputs, 1, "rolling_median")?;
    if window == 0 {
        return Err(BtError::Expression(
            "rolling_median window must be > 0".into(),
        ));
    }
    let vals = extract_f64(&inputs[0])?;
    let len = vals.len();
    let mut out = vec![f64::NAN; len];
    if len < window {
        return Ok(to_f64_array(out));
    }

    // Simple approach: maintain sorted buffer
    let mut buf: Vec<f64> = Vec::with_capacity(window);
    for i in 0..len {
        if vals[i].is_nan() {
            // Reset buffer on NaN
            buf.clear();
            continue;
        }
        buf.push(vals[i]);
        if buf.len() > window {
            // Remove the element that's leaving the window
            let old = vals[i - window];
            if let Ok(pos) = buf.binary_search_by(|x| x.total_cmp(&old)) {
                buf.remove(pos);
            }
        }
        // Re-sort (insertion sort since mostly sorted)
        // For correctness, sort the buffer each time
        if buf.len() == window {
            let mut sorted = buf.clone();
            sorted.sort_by(|a, b| a.total_cmp(b));
            let mid = window / 2;
            if window.is_multiple_of(2) {
                out[i] = (sorted[mid - 1] + sorted[mid]) / 2.0;
            } else {
                out[i] = sorted[mid];
            }
        }
    }
    Ok(to_f64_array(out))
}

// ---------------------------------------------------------------------------
// Trend
// ---------------------------------------------------------------------------

/// Parabolic SAR indicator.
pub fn parabolic_sar(inputs: &[ArrayRef], af_step: f64, af_max: f64) -> Result<ArrayRef> {
    expect_inputs(inputs, 2, "parabolic_sar")?;
    let high = extract_f64(&inputs[0])?;
    let low = extract_f64(&inputs[1])?;
    let len = high.len();
    let mut out = vec![f64::NAN; len];
    if len < 2 {
        return Ok(to_f64_array(out));
    }

    // Initialize: assume uptrend starting from bar 0
    let mut is_long = high[1] >= high[0];
    let mut af = af_step;
    let mut ep;
    let mut sar;

    if is_long {
        sar = low[0];
        ep = high[1];
    } else {
        sar = high[0];
        ep = low[1];
    }
    out[0] = sar;
    out[1] = sar;

    for i in 2..len {
        if high[i].is_nan() || low[i].is_nan() {
            continue;
        }

        // Calculate new SAR
        sar = sar + af * (ep - sar);

        if is_long {
            // Ensure SAR is not above prior two lows
            sar = sar.min(low[i - 1]).min(low[i - 2]);

            if low[i] < sar {
                // Reversal to short
                is_long = false;
                sar = ep;
                ep = low[i];
                af = af_step;
            } else if high[i] > ep {
                ep = high[i];
                af = (af + af_step).min(af_max);
            }
        } else {
            // Ensure SAR is not below prior two highs
            sar = sar.max(high[i - 1]).max(high[i - 2]);

            if high[i] > sar {
                // Reversal to long
                is_long = true;
                sar = ep;
                ep = high[i];
                af = af_step;
            } else if low[i] < ep {
                ep = low[i];
                af = (af + af_step).min(af_max);
            }
        }

        out[i] = sar;
    }
    Ok(to_f64_array(out))
}
