//! Cross-library Criterion benchmark: Wickra vs `kand` vs `ta` (ta-rs) vs `yata`.
//!
//! All four are pure-Rust technical-analysis crates, so this is a like-for-like
//! Rust-vs-Rust comparison with no language-binding overhead. It feeds the exact
//! same BTCUSDT 1-minute candle series used by `crates/wickra/benches/indicators.rs`.
//!
//! Two arenas, kept honest:
//!
//! * **Streaming** (`*/stream`): one value fed at a time. Wickra (`Indicator::update`),
//!   ta-rs (`Next::next`) and yata (`Method::next`) carry their own state; `kand`
//!   exposes stateless `*_inc` helpers, so the per-tick state is threaded manually
//!   here, seeded from `kand`'s own batch output (the seed is computed outside the
//!   timed closure). yata only appears for SMA/EMA — its RSI/MACD/Bollinger/ATR are
//!   exposed through a heavier signal-oriented indicator API, not a raw-value method,
//!   so they are intentionally left out rather than compared unfairly.
//! * **Batch** (`*/batch`): the whole series at once. Only Wickra (`BatchExt::batch`)
//!   and `kand` (TA-Lib-style fill-the-output-slice functions) have a real batch API;
//!   ta-rs and yata are streaming-only and are deliberately absent from this arena.
//!
//! Run: `cargo bench -p wickra-bench`

// Each indicator's benchmark group spells out every library arm explicitly, which
// runs a few groups over the 100-line lint threshold; that verbosity is the point.
#![allow(clippy::too_many_lines)]

use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
use std::hint::black_box;
use wickra::{Atr, BollingerBands, Candle, Ema, Indicator, MacdIndicator, Rsi, Sma};
use wickra_data::csv::CandleReader;
use yata::prelude::Method;

const SIZES: &[usize] = &[1_000, 10_000, 50_000];

const SMA_PERIOD: usize = 20;
const EMA_PERIOD: usize = 20;
const RSI_PERIOD: usize = 14;
const ATR_PERIOD: usize = 14;
const BB_PERIOD: usize = 20;
const BB_DEV: f64 = 2.0;
const MACD_FAST: usize = 12;
const MACD_SLOW: usize = 26;
const MACD_SIGNAL: usize = 9;

fn load_candles() -> Vec<Candle> {
    let path = concat!(
        env!("CARGO_MANIFEST_DIR"),
        "/../../examples/data/btcusdt-1m.csv"
    );
    CandleReader::open(path)
        .expect("dataset present")
        .read_all()
        .expect("valid OHLCV rows")
}

/// Mean of the first `period` samples — the warmup seed for `kand`'s SMA/EMA `*_inc`.
fn window_mean(series: &[f64], period: usize) -> f64 {
    series[..period].iter().sum::<f64>() / period as f64
}

fn sma_group(crit: &mut Criterion, closes: &[f64]) {
    let mut group = crit.benchmark_group("sma_20");
    for &len in SIZES {
        let len = len.min(closes.len());
        let series: &[f64] = &closes[..len];
        group.throughput(Throughput::Elements(len as u64));

        group.bench_with_input(
            BenchmarkId::new("wickra/stream", len),
            &series,
            |bencher, &series| {
                bencher.iter(|| {
                    let mut ind = Sma::new(SMA_PERIOD).unwrap();
                    for &price in series {
                        black_box(ind.update(price));
                    }
                });
            },
        );
        group.bench_with_input(
            BenchmarkId::new("wickra/batch", len),
            &series,
            |bencher, &series| {
                bencher.iter(|| {
                    let mut ind = Sma::new(SMA_PERIOD).unwrap();
                    black_box(ind.batch_nan(series));
                });
            },
        );
        group.bench_with_input(
            BenchmarkId::new("kand/stream", len),
            &series,
            |bencher, &series| {
                let seed = window_mean(series, SMA_PERIOD);
                bencher.iter(|| {
                    let mut prev = seed;
                    for idx in SMA_PERIOD..series.len() {
                        prev = kand::ohlcv::sma::sma_inc(
                            prev,
                            series[idx],
                            series[idx - SMA_PERIOD],
                            SMA_PERIOD,
                        )
                        .unwrap();
                        black_box(prev);
                    }
                });
            },
        );
        group.bench_with_input(
            BenchmarkId::new("kand/batch", len),
            &series,
            |bencher, &series| {
                bencher.iter(|| {
                    let mut out = vec![0.0; series.len()];
                    kand::ohlcv::sma::sma(series, SMA_PERIOD, &mut out).unwrap();
                    black_box(&out);
                });
            },
        );
        group.bench_with_input(
            BenchmarkId::new("ta-rs/stream", len),
            &series,
            |bencher, &series| {
                bencher.iter(|| {
                    let mut ind = ta::indicators::SimpleMovingAverage::new(SMA_PERIOD).unwrap();
                    for &price in series {
                        black_box(ta::Next::next(&mut ind, price));
                    }
                });
            },
        );
        group.bench_with_input(
            BenchmarkId::new("yata/stream", len),
            &series,
            |bencher, &series| {
                bencher.iter(|| {
                    let mut ind = yata::methods::SMA::new(SMA_PERIOD as u8, &series[0]).unwrap();
                    for price in series {
                        black_box(ind.next(price));
                    }
                });
            },
        );
    }
    group.finish();
}

fn ema_group(crit: &mut Criterion, closes: &[f64]) {
    let mut group = crit.benchmark_group("ema_20");
    for &len in SIZES {
        let len = len.min(closes.len());
        let series: &[f64] = &closes[..len];
        group.throughput(Throughput::Elements(len as u64));

        group.bench_with_input(
            BenchmarkId::new("wickra/stream", len),
            &series,
            |bencher, &series| {
                bencher.iter(|| {
                    let mut ind = Ema::new(EMA_PERIOD).unwrap();
                    for &price in series {
                        black_box(ind.update(price));
                    }
                });
            },
        );
        group.bench_with_input(
            BenchmarkId::new("wickra/batch", len),
            &series,
            |bencher, &series| {
                bencher.iter(|| {
                    let mut ind = Ema::new(EMA_PERIOD).unwrap();
                    black_box(ind.batch_nan(series));
                });
            },
        );
        group.bench_with_input(
            BenchmarkId::new("kand/stream", len),
            &series,
            |bencher, &series| {
                let seed = window_mean(series, EMA_PERIOD);
                bencher.iter(|| {
                    let mut prev = seed;
                    for &price in &series[EMA_PERIOD..] {
                        prev = kand::ohlcv::ema::ema_inc(price, prev, EMA_PERIOD, None).unwrap();
                        black_box(prev);
                    }
                });
            },
        );
        group.bench_with_input(
            BenchmarkId::new("kand/batch", len),
            &series,
            |bencher, &series| {
                bencher.iter(|| {
                    let mut out = vec![0.0; series.len()];
                    kand::ohlcv::ema::ema(series, EMA_PERIOD, None, &mut out).unwrap();
                    black_box(&out);
                });
            },
        );
        group.bench_with_input(
            BenchmarkId::new("ta-rs/stream", len),
            &series,
            |bencher, &series| {
                bencher.iter(|| {
                    let mut ind =
                        ta::indicators::ExponentialMovingAverage::new(EMA_PERIOD).unwrap();
                    for &price in series {
                        black_box(ta::Next::next(&mut ind, price));
                    }
                });
            },
        );
        group.bench_with_input(
            BenchmarkId::new("yata/stream", len),
            &series,
            |bencher, &series| {
                bencher.iter(|| {
                    let mut ind = yata::methods::EMA::new(EMA_PERIOD as u8, &series[0]).unwrap();
                    for price in series {
                        black_box(ind.next(price));
                    }
                });
            },
        );
    }
    group.finish();
}

fn rsi_group(crit: &mut Criterion, closes: &[f64]) {
    let mut group = crit.benchmark_group("rsi_14");
    for &len in SIZES {
        let len = len.min(closes.len());
        let series: &[f64] = &closes[..len];
        group.throughput(Throughput::Elements(len as u64));

        group.bench_with_input(
            BenchmarkId::new("wickra/stream", len),
            &series,
            |bencher, &series| {
                bencher.iter(|| {
                    let mut ind = Rsi::new(RSI_PERIOD).unwrap();
                    for &price in series {
                        black_box(ind.update(price));
                    }
                });
            },
        );
        group.bench_with_input(
            BenchmarkId::new("wickra/batch", len),
            &series,
            |bencher, &series| {
                bencher.iter(|| {
                    let mut ind = Rsi::new(RSI_PERIOD).unwrap();
                    black_box(ind.batch_nan(series));
                });
            },
        );
        group.bench_with_input(
            BenchmarkId::new("kand/stream", len),
            &series,
            |bencher, &series| {
                // Wilder seed: simple average of the first `period` gains and losses.
                let mut gain = 0.0;
                let mut loss = 0.0;
                for idx in 1..=RSI_PERIOD {
                    let delta = series[idx] - series[idx - 1];
                    if delta > 0.0 {
                        gain += delta;
                    } else {
                        loss -= delta;
                    }
                }
                let seed_gain = gain / RSI_PERIOD as f64;
                let seed_loss = loss / RSI_PERIOD as f64;
                bencher.iter(|| {
                    let mut avg_gain = seed_gain;
                    let mut avg_loss = seed_loss;
                    let mut prev_price = series[RSI_PERIOD];
                    for &price in &series[RSI_PERIOD + 1..] {
                        let (rsi, next_gain, next_loss) = kand::ohlcv::rsi::rsi_inc(
                            price, prev_price, avg_gain, avg_loss, RSI_PERIOD,
                        )
                        .unwrap();
                        avg_gain = next_gain;
                        avg_loss = next_loss;
                        prev_price = price;
                        black_box(rsi);
                    }
                });
            },
        );
        group.bench_with_input(
            BenchmarkId::new("kand/batch", len),
            &series,
            |bencher, &series| {
                bencher.iter(|| {
                    let mut rsi = vec![0.0; series.len()];
                    let mut avg_gain = vec![0.0; series.len()];
                    let mut avg_loss = vec![0.0; series.len()];
                    kand::ohlcv::rsi::rsi(
                        series,
                        RSI_PERIOD,
                        &mut rsi,
                        &mut avg_gain,
                        &mut avg_loss,
                    )
                    .unwrap();
                    black_box(&rsi);
                });
            },
        );
        group.bench_with_input(
            BenchmarkId::new("ta-rs/stream", len),
            &series,
            |bencher, &series| {
                bencher.iter(|| {
                    let mut ind = ta::indicators::RelativeStrengthIndex::new(RSI_PERIOD).unwrap();
                    for &price in series {
                        black_box(ta::Next::next(&mut ind, price));
                    }
                });
            },
        );
    }
    group.finish();
}

fn macd_group(crit: &mut Criterion, closes: &[f64]) {
    let mut group = crit.benchmark_group("macd_12_26_9");
    for &len in SIZES {
        let len = len.min(closes.len());
        let series: &[f64] = &closes[..len];
        group.throughput(Throughput::Elements(len as u64));

        group.bench_with_input(
            BenchmarkId::new("wickra/stream", len),
            &series,
            |bencher, &series| {
                bencher.iter(|| {
                    let mut ind = MacdIndicator::classic();
                    for &price in series {
                        black_box(ind.update(price));
                    }
                });
            },
        );
        group.bench_with_input(
            BenchmarkId::new("wickra/batch", len),
            &series,
            |bencher, &series| {
                bencher.iter(|| {
                    let mut ind = MacdIndicator::classic();
                    black_box(ind.batch_macd(series));
                });
            },
        );
        group.bench_with_input(
            BenchmarkId::new("kand/stream", len),
            &series,
            |bencher, &series| {
                // Seed the fast/slow/signal EMAs from kand's own warmed-up batch state.
                let lookback =
                    kand::ohlcv::macd::lookback(MACD_FAST, MACD_SLOW, MACD_SIGNAL).unwrap();
                let mut macd_line = vec![0.0; series.len()];
                let mut signal_line = vec![0.0; series.len()];
                let mut histogram = vec![0.0; series.len()];
                let mut fast_ema = vec![0.0; series.len()];
                let mut slow_ema = vec![0.0; series.len()];
                kand::ohlcv::macd::macd(
                    series,
                    MACD_FAST,
                    MACD_SLOW,
                    MACD_SIGNAL,
                    &mut macd_line,
                    &mut signal_line,
                    &mut histogram,
                    &mut fast_ema,
                    &mut slow_ema,
                )
                .unwrap();
                let seed_fast = fast_ema[lookback];
                let seed_slow = slow_ema[lookback];
                let seed_signal = signal_line[lookback];
                bencher.iter(|| {
                    // macd_inc returns (macd, signal, hist) but not the new EMAs, so the
                    // fast/slow/signal state is threaded with kand's own ema_inc primitive.
                    let mut prev_fast = seed_fast;
                    let mut prev_slow = seed_slow;
                    let mut prev_signal = seed_signal;
                    for &price in &series[lookback + 1..] {
                        let fast =
                            kand::ohlcv::ema::ema_inc(price, prev_fast, MACD_FAST, None).unwrap();
                        let slow =
                            kand::ohlcv::ema::ema_inc(price, prev_slow, MACD_SLOW, None).unwrap();
                        let macd = fast - slow;
                        let signal =
                            kand::ohlcv::ema::ema_inc(macd, prev_signal, MACD_SIGNAL, None)
                                .unwrap();
                        prev_fast = fast;
                        prev_slow = slow;
                        prev_signal = signal;
                        black_box((macd, signal, macd - signal));
                    }
                });
            },
        );
        group.bench_with_input(
            BenchmarkId::new("kand/batch", len),
            &series,
            |bencher, &series| {
                bencher.iter(|| {
                    let mut macd_line = vec![0.0; series.len()];
                    let mut signal_line = vec![0.0; series.len()];
                    let mut histogram = vec![0.0; series.len()];
                    let mut fast_ema = vec![0.0; series.len()];
                    let mut slow_ema = vec![0.0; series.len()];
                    kand::ohlcv::macd::macd(
                        series,
                        MACD_FAST,
                        MACD_SLOW,
                        MACD_SIGNAL,
                        &mut macd_line,
                        &mut signal_line,
                        &mut histogram,
                        &mut fast_ema,
                        &mut slow_ema,
                    )
                    .unwrap();
                    black_box(&macd_line);
                });
            },
        );
        group.bench_with_input(
            BenchmarkId::new("ta-rs/stream", len),
            &series,
            |bencher, &series| {
                bencher.iter(|| {
                    let mut ind = ta::indicators::MovingAverageConvergenceDivergence::new(
                        MACD_FAST,
                        MACD_SLOW,
                        MACD_SIGNAL,
                    )
                    .unwrap();
                    for &price in series {
                        black_box(ta::Next::next(&mut ind, price));
                    }
                });
            },
        );
    }
    group.finish();
}

fn bbands_group(crit: &mut Criterion, closes: &[f64]) {
    let mut group = crit.benchmark_group("bollinger_20_2");
    for &len in SIZES {
        let len = len.min(closes.len());
        let series: &[f64] = &closes[..len];
        group.throughput(Throughput::Elements(len as u64));

        group.bench_with_input(
            BenchmarkId::new("wickra/stream", len),
            &series,
            |bencher, &series| {
                bencher.iter(|| {
                    let mut ind = BollingerBands::new(BB_PERIOD, BB_DEV).unwrap();
                    for &price in series {
                        black_box(ind.update(price));
                    }
                });
            },
        );
        group.bench_with_input(
            BenchmarkId::new("wickra/batch", len),
            &series,
            |bencher, &series| {
                bencher.iter(|| {
                    let mut ind = BollingerBands::new(BB_PERIOD, BB_DEV).unwrap();
                    black_box(ind.batch_bands(series));
                });
            },
        );
        group.bench_with_input(
            BenchmarkId::new("kand/stream", len),
            &series,
            |bencher, &series| {
                // Seed running sma/sum/sum_sq from kand's batch state at the warmup edge.
                let mut upper = vec![0.0; series.len()];
                let mut middle = vec![0.0; series.len()];
                let mut lower = vec![0.0; series.len()];
                let mut sma = vec![0.0; series.len()];
                let mut variance = vec![0.0; series.len()];
                let mut sum = vec![0.0; series.len()];
                let mut sum_sq = vec![0.0; series.len()];
                kand::ohlcv::bbands::bbands(
                    series,
                    BB_PERIOD,
                    BB_DEV,
                    BB_DEV,
                    &mut upper,
                    &mut middle,
                    &mut lower,
                    &mut sma,
                    &mut variance,
                    &mut sum,
                    &mut sum_sq,
                )
                .unwrap();
                let seed_sma = sma[BB_PERIOD - 1];
                let seed_sum = sum[BB_PERIOD - 1];
                let seed_sum_sq = sum_sq[BB_PERIOD - 1];
                bencher.iter(|| {
                    let mut prev_sma = seed_sma;
                    let mut prev_sum = seed_sum;
                    let mut prev_sum_sq = seed_sum_sq;
                    for idx in BB_PERIOD..series.len() {
                        let result = kand::ohlcv::bbands::bbands_inc(
                            series[idx],
                            prev_sma,
                            prev_sum,
                            prev_sum_sq,
                            series[idx - BB_PERIOD],
                            BB_PERIOD,
                            BB_DEV,
                            BB_DEV,
                        )
                        .unwrap();
                        prev_sma = result.1;
                        prev_sum = result.4;
                        prev_sum_sq = result.5;
                        black_box((result.0, result.1, result.2));
                    }
                });
            },
        );
        group.bench_with_input(
            BenchmarkId::new("kand/batch", len),
            &series,
            |bencher, &series| {
                bencher.iter(|| {
                    let mut upper = vec![0.0; series.len()];
                    let mut middle = vec![0.0; series.len()];
                    let mut lower = vec![0.0; series.len()];
                    let mut sma = vec![0.0; series.len()];
                    let mut variance = vec![0.0; series.len()];
                    let mut sum = vec![0.0; series.len()];
                    let mut sum_sq = vec![0.0; series.len()];
                    kand::ohlcv::bbands::bbands(
                        series,
                        BB_PERIOD,
                        BB_DEV,
                        BB_DEV,
                        &mut upper,
                        &mut middle,
                        &mut lower,
                        &mut sma,
                        &mut variance,
                        &mut sum,
                        &mut sum_sq,
                    )
                    .unwrap();
                    black_box(&upper);
                });
            },
        );
        group.bench_with_input(
            BenchmarkId::new("ta-rs/stream", len),
            &series,
            |bencher, &series| {
                bencher.iter(|| {
                    let mut ind = ta::indicators::BollingerBands::new(BB_PERIOD, BB_DEV).unwrap();
                    for &price in series {
                        black_box(ta::Next::next(&mut ind, price));
                    }
                });
            },
        );
    }
    group.finish();
}

fn atr_group(crit: &mut Criterion, candles: &[Candle]) {
    let mut group = crit.benchmark_group("atr_14");
    for &len in SIZES {
        let len = len.min(candles.len());
        let series: &[Candle] = &candles[..len];
        group.throughput(Throughput::Elements(len as u64));

        group.bench_with_input(
            BenchmarkId::new("wickra/stream", len),
            &series,
            |bencher, &series| {
                bencher.iter(|| {
                    let mut ind = Atr::new(ATR_PERIOD).unwrap();
                    for &candle in series {
                        black_box(ind.update(candle));
                    }
                });
            },
        );
        group.bench_with_input(
            BenchmarkId::new("wickra/batch", len),
            &series,
            |bencher, &series| {
                // Column extraction is outside the timed loop, mirroring kand's arm.
                let high: Vec<f64> = series.iter().map(|candle| candle.high).collect();
                let low: Vec<f64> = series.iter().map(|candle| candle.low).collect();
                let close: Vec<f64> = series.iter().map(|candle| candle.close).collect();
                bencher.iter(|| {
                    let mut ind = Atr::new(ATR_PERIOD).unwrap();
                    black_box(ind.batch_atr(&high, &low, &close));
                });
            },
        );
        group.bench_with_input(
            BenchmarkId::new("kand/stream", len),
            &series,
            |bencher, &series| {
                let high: Vec<f64> = series.iter().map(|candle| candle.high).collect();
                let low: Vec<f64> = series.iter().map(|candle| candle.low).collect();
                let close: Vec<f64> = series.iter().map(|candle| candle.close).collect();
                // Seed prev_atr from kand's batch ATR at the first valid index (= period).
                let mut atr_out = vec![0.0; series.len()];
                kand::ohlcv::atr::atr(&high, &low, &close, ATR_PERIOD, &mut atr_out).unwrap();
                let seed_atr = atr_out[ATR_PERIOD];
                bencher.iter(|| {
                    let mut prev_atr = seed_atr;
                    for idx in ATR_PERIOD + 1..series.len() {
                        prev_atr = kand::ohlcv::atr::atr_inc(
                            high[idx],
                            low[idx],
                            close[idx - 1],
                            prev_atr,
                            ATR_PERIOD,
                        )
                        .unwrap();
                        black_box(prev_atr);
                    }
                });
            },
        );
        group.bench_with_input(
            BenchmarkId::new("kand/batch", len),
            &series,
            |bencher, &series| {
                let high: Vec<f64> = series.iter().map(|candle| candle.high).collect();
                let low: Vec<f64> = series.iter().map(|candle| candle.low).collect();
                let close: Vec<f64> = series.iter().map(|candle| candle.close).collect();
                bencher.iter(|| {
                    let mut atr_out = vec![0.0; series.len()];
                    kand::ohlcv::atr::atr(&high, &low, &close, ATR_PERIOD, &mut atr_out).unwrap();
                    black_box(&atr_out);
                });
            },
        );
        group.bench_with_input(
            BenchmarkId::new("ta-rs/stream", len),
            &series,
            |bencher, &series| {
                let items: Vec<ta::DataItem> = series
                    .iter()
                    .map(|candle| {
                        ta::DataItem::builder()
                            .open(candle.open)
                            .high(candle.high)
                            .low(candle.low)
                            .close(candle.close)
                            .volume(candle.volume)
                            .build()
                            .unwrap()
                    })
                    .collect();
                bencher.iter(|| {
                    let mut ind = ta::indicators::AverageTrueRange::new(ATR_PERIOD).unwrap();
                    for item in &items {
                        black_box(ta::Next::next(&mut ind, item));
                    }
                });
            },
        );
    }
    group.finish();
}

fn benches(crit: &mut Criterion) {
    let candles = load_candles();
    let closes: Vec<f64> = candles.iter().map(|candle| candle.close).collect();
    sma_group(crit, &closes);
    ema_group(crit, &closes);
    rsi_group(crit, &closes);
    macd_group(crit, &closes);
    bbands_group(crit, &closes);
    atr_group(crit, &candles);
}

criterion_group!(name = cross_lib; config = Criterion::default(); targets = benches);
criterion_main!(cross_lib);
