from __future__ import annotations

import argparse
import json
import time
from collections.abc import Callable
from pathlib import Path
from typing import Any

import numpy as np

import ferro_ta as ft

try:
    from benchmarks.metadata import benchmark_metadata
except ModuleNotFoundError:  # pragma: no cover - script execution fallback
    from metadata import benchmark_metadata


def _time_min(fn: Callable[[], object], rounds: int = 5) -> float:
    fn()
    samples: list[float] = []
    for _ in range(rounds):
        t0 = time.perf_counter()
        fn()
        samples.append(time.perf_counter() - t0)
    return min(samples)


def _stream_close(close: np.ndarray, factory: Callable[[], Any]) -> float:
    streamer = factory()
    last = np.nan
    for value in close:
        last = streamer.update(float(value))
    return float(last) if not np.isnan(last) else np.nan


def _stream_hlc(
    high: np.ndarray,
    low: np.ndarray,
    close: np.ndarray,
    factory: Callable[[], Any],
) -> float:
    streamer = factory()
    last = np.nan
    for high_value, low_value, close_value in zip(high, low, close):
        last = streamer.update(float(high_value), float(low_value), float(close_value))
    return float(last) if not np.isnan(last) else np.nan


def _stream_hlcv(
    high: np.ndarray,
    low: np.ndarray,
    close: np.ndarray,
    volume: np.ndarray,
    factory: Callable[[], Any],
) -> float:
    streamer = factory()
    last = np.nan
    for high_value, low_value, close_value, volume_value in zip(
        high, low, close, volume
    ):
        last = streamer.update(
            float(high_value),
            float(low_value),
            float(close_value),
            float(volume_value),
        )
    return float(last) if not np.isnan(last) else np.nan


def run_streaming_benchmark(
    *,
    n_bars: int = 100_000,
    seed: int = 2026,
) -> dict[str, Any]:
    rng = np.random.default_rng(seed)
    close = 100.0 + np.cumsum(rng.normal(0.0, 1.0, n_bars)).astype(np.float64)
    high = close + rng.uniform(0.1, 2.0, n_bars)
    low = close - rng.uniform(0.1, 2.0, n_bars)
    volume = rng.uniform(1_000.0, 100_000.0, n_bars)

    cases = [
        (
            "StreamingSMA",
            "close",
            lambda: _stream_close(close, lambda: ft.StreamingSMA(period=20)),
            lambda: ft.SMA(close, timeperiod=20),
        ),
        (
            "StreamingEMA",
            "close",
            lambda: _stream_close(close, lambda: ft.StreamingEMA(period=20)),
            lambda: ft.EMA(close, timeperiod=20),
        ),
        (
            "StreamingRSI",
            "close",
            lambda: _stream_close(close, lambda: ft.StreamingRSI(period=14)),
            lambda: ft.RSI(close, timeperiod=14),
        ),
        (
            "StreamingATR",
            "hlc",
            lambda: _stream_hlc(
                high,
                low,
                close,
                lambda: ft.StreamingATR(period=14),
            ),
            lambda: ft.ATR(high, low, close, timeperiod=14),
        ),
        (
            "StreamingVWAP",
            "hlcv",
            lambda: _stream_hlcv(
                high,
                low,
                close,
                volume,
                lambda: ft.StreamingVWAP(),
            ),
            lambda: ft.VWAP(high, low, close, volume),
        ),
    ]

    rows: list[dict[str, Any]] = []
    for name, input_kind, stream_fn, batch_fn in cases:
        stream_s = _time_min(stream_fn)
        batch_s = _time_min(batch_fn)
        rows.append(
            {
                "indicator": name,
                "inputs": input_kind,
                "stream_total_ms": round(stream_s * 1000.0, 4),
                "batch_total_ms": round(batch_s * 1000.0, 4),
                "stream_ns_per_update": round(stream_s * 1e9 / n_bars, 2),
                "batch_ns_per_bar": round(batch_s * 1e9 / n_bars, 2),
                "updates_per_second": round(n_bars / stream_s, 2),
                "stream_over_batch_ratio": round(stream_s / batch_s, 4),
            }
        )

    return {
        "metadata": benchmark_metadata(
            "streaming",
            extra={
                "dataset": {
                    "n_bars": n_bars,
                    "seed": seed,
                }
            },
        ),
        "results": rows,
    }


def main() -> int:
    parser = argparse.ArgumentParser(
        description="Benchmark streaming indicator execution."
    )
    parser.add_argument("--bars", type=int, default=100_000)
    parser.add_argument("--seed", type=int, default=2026)
    parser.add_argument("--json", dest="json_path")
    args = parser.parse_args()

    payload = run_streaming_benchmark(n_bars=args.bars, seed=args.seed)

    dataset = payload["metadata"]["dataset"]
    print(f"Streaming Benchmark: {dataset['n_bars']} bars")
    print("-" * 86)
    print(
        f"{'Indicator':<16} {'Stream (ms)':>12} {'Batch (ms)':>12} "
        f"{'ns/update':>12} {'upd/s':>12} {'ratio':>10}"
    )
    print("-" * 86)
    for row in payload["results"]:
        print(
            f"{row['indicator']:<16} {row['stream_total_ms']:12.2f} "
            f"{row['batch_total_ms']:12.2f} {row['stream_ns_per_update']:12.2f} "
            f"{row['updates_per_second']:12.1f} {row['stream_over_batch_ratio']:10.2f}"
        )

    if args.json_path:
        path = Path(args.json_path)
        path.write_text(json.dumps(payload, indent=2), encoding="utf-8")
        print(f"\nWrote JSON results to {path}")

    return 0


if __name__ == "__main__":
    raise SystemExit(main())
