#!/usr/bin/env python3
"""Oracle-based bit-exact verification: WebSocket bars vs REST-recomputed bars.

Fetches recent bars from ClickHouse (produced by sidecar via WebSocket
process_single_trade path), then independently re-fetches the exact same
raw trades from Binance REST API and recomputes bars through BOTH processor
paths (batch and streaming). Asserts digit-exact identity at every field.

Hierarchy of verification:
  Level 1 (atomic):  Threshold computation, breach condition, OHLCV updates
  Level 2 (bar):     Full bar field equality (batch vs streaming vs ClickHouse)
  Level 3 (chain):   Close-to-next-open transition analysis across bar sequences

Usage:
    # Local (with SSH tunnel to bigblack ClickHouse on port 9000):
    .venv/bin/python3 scripts/oracle_verify_ws_vs_rest.py

    # On bigblack:
    ssh bigblack "cd ~/opendeviationbar-py && .venv/bin/python3 scripts/oracle_verify_ws_vs_rest.py"

Exit codes:
    0 - all verifications pass (bit-exact)
    1 - divergence detected (details in output)
    2 - infrastructure error (ClickHouse, Binance API unreachable)
"""

from __future__ import annotations

import json
import os
import sys
import time
from dataclasses import dataclass, field
from datetime import UTC, datetime
from decimal import Decimal, getcontext
from pathlib import Path
from typing import Any

# High precision for oracle computations
getcontext().prec = 50

# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------

SYMBOL = "BTCUSDT"
THRESHOLD_DBPS = 250  # 0.25%
N_BARS = 50  # Number of recent bars to verify
ARTIFACTS_DIR = Path(__file__).resolve().parent.parent / "artifacts"

# Fields compared at digit-exact level (Float64 from ClickHouse vs f64 from Rust)
OHLCV_FIELDS = ["open", "high", "low", "close", "volume"]
TRADE_ID_FIELDS = ["first_agg_trade_id", "last_agg_trade_id"]
COUNT_FIELDS = ["individual_trade_count", "agg_record_count"]
MICRO_FIELDS = [
    "duration_us", "ofi", "vwap_close_deviation", "price_impact",
    "kyle_lambda_proxy", "trade_intensity", "volume_per_trade",
    "aggression_ratio", "aggregation_density", "turnover_imbalance",
]


# ---------------------------------------------------------------------------
# Data classes
# ---------------------------------------------------------------------------


@dataclass
class BarComparison:
    """Comparison result for a single bar across all three sources."""
    bar_index: int
    ch_close_time_ms: int
    batch_match: bool = True
    streaming_match: bool = True
    divergences: list[str] = field(default_factory=list)


@dataclass
class TransitionCheck:
    """Close-to-next-open transition analysis."""
    bar_index: int
    close_price: float
    next_open_price: float
    gap_bps: float
    close_trade_id: int
    next_open_trade_id: int
    # Oracle: independently verify the trade that opened the next bar
    oracle_next_trade_price: float | None = None
    oracle_match: bool = True


# ---------------------------------------------------------------------------
# ClickHouse helpers
# ---------------------------------------------------------------------------


def _get_ch_client() -> Any:  # noqa: ANN401
    """Get ClickHouse client (local or remote)."""
    mode = os.environ.get("OPENDEVIATIONBAR_MODE", "local")
    try:
        import clickhouse_connect
    except ImportError:
        print("ERROR: clickhouse_connect not installed", file=sys.stderr)
        sys.exit(2)

    if mode == "remote":
        host = os.environ.get("OPENDEVIATIONBAR_CH_HOSTS", "bigblack").split(",")[0]
        port = int(os.environ.get("OPENDEVIATIONBAR_CH_PORT", "8123"))
    else:
        host = "localhost"
        port = 8123

    try:
        client = clickhouse_connect.get_client(host=host, port=port)
        client.query("SELECT 1")
        return client
    except (OSError, ConnectionError, RuntimeError) as e:
        print(f"ERROR: ClickHouse connection failed ({host}:{port}): {e}", file=sys.stderr)
        sys.exit(2)


def fetch_ch_bars(client, n: int) -> list[dict]:
    """Fetch N most recent bars from ClickHouse."""
    query = f"""
        SELECT
            open_time_ms,
            close_time_ms,
            open, high, low, close, volume,
            vwap,
            buy_volume, sell_volume,
            individual_trade_count,
            agg_record_count,
            first_agg_trade_id,
            last_agg_trade_id,
            duration_us,
            ofi, vwap_close_deviation, price_impact,
            kyle_lambda_proxy, trade_intensity, volume_per_trade,
            aggression_ratio, aggregation_density, turnover_imbalance
        FROM opendeviationbar_cache.open_deviation_bars
        WHERE symbol = '{SYMBOL}'
          AND threshold_decimal_bps = {THRESHOLD_DBPS}
          AND first_agg_trade_id > 0
          AND last_agg_trade_id > 0
          AND open_time_ms > 0
        ORDER BY close_time_ms DESC
        LIMIT {n}
    """
    result = client.query(query)
    columns = result.column_names
    bars = []
    for row in result.result_rows:
        bars.append(dict(zip(columns, row, strict=False)))
    # Reverse to chronological order
    bars.reverse()
    return bars


# ---------------------------------------------------------------------------
# Trade fetching (oracle source: Binance REST API)
# ---------------------------------------------------------------------------


def fetch_trades_by_id_range(first_id: int, last_id: int) -> list[dict]:
    """Fetch all trades in [first_id, last_id] from Binance REST API.

    Uses zero-gap fromId pagination for completeness guarantee.
    """
    from opendeviationbar._core import fetch_aggtrades_by_id

    all_trades = []
    current_id = first_id

    while current_id <= last_id:
        limit = min(1000, last_id - current_id + 1)
        batch = fetch_aggtrades_by_id(SYMBOL, current_id, limit)
        if not batch:
            break

        for t in batch:
            if t["agg_trade_id"] <= last_id:
                all_trades.append(t)

        # Advance cursor past the last fetched trade
        max_fetched_id = max(t["agg_trade_id"] for t in batch)
        if max_fetched_id >= last_id:
            break
        current_id = max_fetched_id + 1

        # Rate limit courtesy
        time.sleep(0.1)

    return all_trades


# ---------------------------------------------------------------------------
# Oracle: independent threshold + breach computation (pure Python, Decimal)
# ---------------------------------------------------------------------------


def oracle_compute_thresholds(open_price: float, threshold_dbps: int) -> tuple[Decimal, Decimal]:
    """Independently compute breach thresholds using Decimal arithmetic.

    Mirrors Rust FixedPoint: delta = (price * threshold_dbps) / 100_000
    """
    price = Decimal(str(open_price))
    dbps = Decimal(str(threshold_dbps))
    basis = Decimal("100000")  # BASIS_POINTS_SCALE

    delta = (price * dbps) / basis

    upper = price + delta
    lower = price - delta
    return upper, lower


def oracle_check_breach(price: float, upper: Decimal, lower: Decimal) -> bool:
    """Check breach condition: price >= upper OR price <= lower."""
    p = Decimal(str(price))
    return p >= upper or p <= lower


# ---------------------------------------------------------------------------
# Level 1: Atomic verification (per-trade, per-field)
# ---------------------------------------------------------------------------


def verify_atomic_operations(trades: list[dict], bars: list[dict]) -> list[str]:
    """Verify threshold computation and breach detection at the primitive level.

    For each bar:
      1. First trade price == bar open (digit-exact)
      2. Oracle thresholds match what would produce the observed breach
      3. No intermediate trade should have breached (except the close trade)
      4. The close trade must breach
    """
    issues = []

    # Build trade lookup by agg_trade_id for fast access
    trade_by_id = {t["agg_trade_id"]: t for t in trades}

    for i, bar in enumerate(bars):
        first_id = bar["first_agg_trade_id"]
        last_id = bar["last_agg_trade_id"]

        # Get trades for this bar
        bar_trades = [
            trade_by_id[tid]
            for tid in range(first_id, last_id + 1)
            if tid in trade_by_id
        ]

        if not bar_trades:
            issues.append(f"bar[{i}]: no trades found for ID range [{first_id}, {last_id}]")
            continue

        # 1. First trade price == bar open
        first_price = bar_trades[0]["price"]
        if first_price != bar["open"]:
            issues.append(
                f"bar[{i}]: open mismatch: first_trade.price={first_price} != bar.open={bar['open']}"
            )

        # 2. Compute oracle thresholds from open price
        upper, lower = oracle_compute_thresholds(bar["open"], THRESHOLD_DBPS)

        # 3. Check that no intermediate trade breaches (except possibly the last)
        for j, t in enumerate(bar_trades[:-1]):
            if oracle_check_breach(t["price"], upper, lower):
                # This is allowed ONLY if it's the first trade (open == breach)
                # or if same-timestamp-close prevention applies
                if j == 0 and t["timestamp"] == bar_trades[0]["timestamp"]:
                    continue  # Same-timestamp gate: can't close on open timestamp
                # Check if same timestamp as open
                if t["timestamp"] == bar_trades[0]["timestamp"]:
                    continue  # Timestamp gate prevents close
                issues.append(
                    f"bar[{i}]: intermediate trade[{j}] (id={t['agg_trade_id']}) "
                    f"breaches at price={t['price']} but is not the closing trade"
                )

        # 4. Last trade must be the closing trade
        last_trade = bar_trades[-1]
        if last_trade["price"] != bar["close"]:
            issues.append(
                f"bar[{i}]: close mismatch: last_trade.price={last_trade['price']} "
                f"!= bar.close={bar['close']}"
            )

        # 5. Verify OHLC invariants
        prices = [t["price"] for t in bar_trades]
        expected_high = max(prices)
        expected_low = min(prices)
        if bar["high"] != expected_high:
            issues.append(
                f"bar[{i}]: high mismatch: computed={expected_high} vs stored={bar['high']}"
            )
        if bar["low"] != expected_low:
            issues.append(
                f"bar[{i}]: low mismatch: computed={expected_low} vs stored={bar['low']}"
            )

    return issues


# ---------------------------------------------------------------------------
# Level 2: Bar-level verification (batch vs streaming vs ClickHouse)
# ---------------------------------------------------------------------------


def verify_bar_equality(
    ch_bars: list[dict],
    batch_bars: list[dict],
    streaming_bars: list[dict],
) -> list[BarComparison]:
    """Compare every field across all three bar sources."""
    results = []

    # We may get different bar counts if boundaries don't align perfectly
    min_count = min(len(ch_bars), len(batch_bars), len(streaming_bars))

    for i in range(min_count):
        ch = ch_bars[i]
        batch = batch_bars[i]
        stream = streaming_bars[i]
        comp = BarComparison(bar_index=i, ch_close_time_ms=ch["close_time_ms"])

        # Compare batch vs streaming (should be IDENTICAL — same code path in Rust)
        for f in OHLCV_FIELDS + TRADE_ID_FIELDS + COUNT_FIELDS:
            bv = batch.get(f)
            sv = stream.get(f)
            if bv != sv:
                comp.streaming_match = False
                comp.divergences.append(
                    f"batch_vs_stream[{f}]: {bv} != {sv}"
                )

        # Compare batch vs ClickHouse
        for f in OHLCV_FIELDS:
            bv = batch.get(f)
            cv = ch.get(f)
            # ClickHouse stores as Float64, Rust outputs f64 — both IEEE 754
            # Use relative tolerance for float comparison
            if bv is not None and cv is not None:
                if bv == 0.0 and cv == 0.0:
                    continue
                if bv != 0.0:
                    rel_diff = abs(bv - cv) / abs(bv)
                    if rel_diff > 1e-12:  # tighter than epsilon
                        comp.batch_match = False
                        comp.divergences.append(
                            f"batch_vs_ch[{f}]: batch={bv} ch={cv} rel_diff={rel_diff:.2e}"
                        )

        # Compare trade ID ranges
        for f in TRADE_ID_FIELDS:
            bv = batch.get(f)
            cv = ch.get(f)
            if bv != cv:
                comp.batch_match = False
                comp.divergences.append(
                    f"batch_vs_ch[{f}]: batch={bv} ch={cv}"
                )

        # Compare trade counts
        for f in COUNT_FIELDS:
            bv = batch.get(f)
            cv = ch.get(f)
            if bv is not None and cv is not None and bv != cv:
                comp.batch_match = False
                comp.divergences.append(
                    f"batch_vs_ch[{f}]: batch={bv} ch={cv}"
                )

        # Compare microstructure features (relaxed tolerance for computed features)
        for f in MICRO_FIELDS:
            bv = batch.get(f)
            cv = ch.get(f)
            if (
                bv is not None
                and cv is not None
                and isinstance(bv, int | float)
                and isinstance(cv, int | float)
            ):
                if bv == 0.0 and cv == 0.0:
                    continue
                denom = max(abs(bv), abs(cv), 1e-15)
                rel_diff = abs(bv - cv) / denom
                if rel_diff > 1e-9:
                    comp.batch_match = False
                    comp.divergences.append(
                        f"batch_vs_ch[{f}]: batch={bv} ch={cv} rel_diff={rel_diff:.2e}"
                    )

        results.append(comp)

    return results


# ---------------------------------------------------------------------------
# Level 3: Close-to-next-open transition verification
# ---------------------------------------------------------------------------


def verify_transitions(
    bars: list[dict],
    all_trades: list[dict],
) -> list[TransitionCheck]:
    """Verify close[N] → open[N+1] transitions with oracle trade lookup.

    For each transition:
      1. Compute gap in bps
      2. Verify that bar[N+1].first_agg_trade_id == bar[N].last_agg_trade_id + 1
         (no dropped trades) OR identify the gap
      3. Look up the actual trade that opened bar[N+1] from raw trades
      4. Confirm oracle_next_trade.price == bar[N+1].open (digit-exact)
    """
    trade_by_id = {t["agg_trade_id"]: t for t in all_trades}
    results = []

    for i in range(len(bars) - 1):
        curr = bars[i]
        nxt = bars[i + 1]

        close_price = curr["close"]
        next_open = nxt["open"]

        # Gap in basis points
        if close_price != 0:
            gap_bps = abs(next_open - close_price) / close_price * 100_000
        else:
            gap_bps = 0.0

        check = TransitionCheck(
            bar_index=i,
            close_price=close_price,
            next_open_price=next_open,
            gap_bps=round(gap_bps, 4),
            close_trade_id=curr["last_agg_trade_id"],
            next_open_trade_id=nxt["first_agg_trade_id"],
        )

        # Oracle: look up the trade that opened the next bar
        next_first_id = nxt["first_agg_trade_id"]
        oracle_trade = trade_by_id.get(next_first_id)
        if oracle_trade:
            check.oracle_next_trade_price = oracle_trade["price"]
            # The open of the next bar must equal the price of its first trade
            if oracle_trade["price"] != next_open:
                check.oracle_match = False

        results.append(check)

    return results


# ---------------------------------------------------------------------------
# Boundary condition enumeration
# ---------------------------------------------------------------------------


def check_boundary_conditions(
    ch_bars: list[dict],
    trades: list[dict],
) -> list[str]:
    """Enumerate boundary conditions that could cause divergences.

    - Bootstrap: first bar in sequence (no prior state)
    - Steady-state: middle bars
    - Off-by-one: trade ID continuity (last_id[N] + 1 == first_id[N+1])
    - Fence-post: are there trades between bars that belong to neither?
    """
    issues = []
    trade_ids = {t["agg_trade_id"] for t in trades}

    for i in range(len(ch_bars) - 1):
        curr = ch_bars[i]
        nxt = ch_bars[i + 1]

        curr_last = curr["last_agg_trade_id"]
        nxt_first = nxt["first_agg_trade_id"]

        # Off-by-one check: next bar should open on exactly last_id + 1
        # (due to defer_open: closing trade stays in current bar,
        #  next trade opens new bar)
        expected_next = curr_last + 1
        if nxt_first != expected_next:
            gap_size = nxt_first - curr_last - 1
            issues.append(
                f"transition[{i}→{i+1}]: trade ID gap of {gap_size} "
                f"(expected next={expected_next}, got={nxt_first})"
            )

            # Check if any of the gap trades exist in our fetched data
            missing = []
            for tid in range(expected_next, nxt_first):
                if tid in trade_ids:
                    missing.append(tid)
            if missing:
                issues.append(
                    f"  -> {len(missing)} gap trades found in REST data but missing from bars"
                )

        # Fence-post: check for timestamp ordering
        if nxt["open_time_ms"] < curr["close_time_ms"]:
            issues.append(
                f"transition[{i}→{i+1}]: temporal overlap! "
                f"bar[{i}].close_time={curr['close_time_ms']} > "
                f"bar[{i+1}].open_time={nxt['open_time_ms']}"
            )

    return issues


# ---------------------------------------------------------------------------
# Main orchestration
# ---------------------------------------------------------------------------


def main() -> int:
    os.chdir(Path(__file__).resolve().parent.parent)

    print("=" * 72, file=sys.stderr)
    print("Oracle Verification: WebSocket vs REST bar construction", file=sys.stderr)
    print(f"Symbol: {SYMBOL} | Threshold: {THRESHOLD_DBPS} dbps | Bars: {N_BARS}", file=sys.stderr)
    print("=" * 72, file=sys.stderr)

    # --- Phase 1: Fetch ClickHouse bars (system under test) ---
    print("\n[1/6] Fetching ClickHouse bars...", file=sys.stderr, flush=True)
    client = _get_ch_client()
    ch_bars = fetch_ch_bars(client, N_BARS)
    if len(ch_bars) < 2:
        print(f"ERROR: Only {len(ch_bars)} bars in ClickHouse — need at least 2", file=sys.stderr)
        return 2

    print(f"  Got {len(ch_bars)} bars", file=sys.stderr)
    print(
        f"  Time range: {ch_bars[0]['open_time_ms']} → {ch_bars[-1]['close_time_ms']}",
        file=sys.stderr,
    )

    # --- Phase 2: Determine trade ID range and fetch from REST ---
    first_trade_id = ch_bars[0]["first_agg_trade_id"]
    last_trade_id = ch_bars[-1]["last_agg_trade_id"]
    print(
        "\n[2/6] Fetching trades from Binance REST API...",
        file=sys.stderr,
        flush=True,
    )
    print(
        f"  Trade ID range: [{first_trade_id}, {last_trade_id}] "
        f"({last_trade_id - first_trade_id + 1} expected)",
        file=sys.stderr,
    )

    t0 = time.monotonic()
    all_trades = fetch_trades_by_id_range(first_trade_id, last_trade_id)
    elapsed = time.monotonic() - t0
    print(f"  Fetched {len(all_trades)} trades in {elapsed:.1f}s", file=sys.stderr)

    if not all_trades:
        print("ERROR: No trades fetched from REST API", file=sys.stderr)
        return 2

    # Verify trade continuity
    trade_ids = [t["agg_trade_id"] for t in all_trades]
    expected_ids = list(range(first_trade_id, last_trade_id + 1))
    missing_ids = set(expected_ids) - set(trade_ids)
    if missing_ids:
        print(
            f"  WARNING: {len(missing_ids)} missing trade IDs in REST response",
            file=sys.stderr,
        )

    # --- Phase 3: Recompute bars through BATCH processor ---
    print("\n[3/6] Recomputing bars via BATCH processor...", file=sys.stderr, flush=True)
    from opendeviationbar._core import PyOpenDeviationBarProcessor

    batch_processor = PyOpenDeviationBarProcessor(THRESHOLD_DBPS, symbol=SYMBOL)
    t0 = time.monotonic()
    batch_bars = batch_processor.process_trades(all_trades, "dict")
    batch_elapsed = time.monotonic() - t0
    print(f"  Batch produced {len(batch_bars)} bars in {batch_elapsed:.3f}s", file=sys.stderr)

    # --- Phase 4: Recompute bars through STREAMING processor ---
    print("\n[4/6] Recomputing bars via STREAMING processor...", file=sys.stderr, flush=True)
    stream_processor = PyOpenDeviationBarProcessor(THRESHOLD_DBPS, symbol=SYMBOL)
    t0 = time.monotonic()
    stream_bars = stream_processor.process_trades_streaming(all_trades)
    stream_elapsed = time.monotonic() - t0
    print(f"  Streaming produced {len(stream_bars)} bars in {stream_elapsed:.3f}s", file=sys.stderr)

    # --- Phase 5: Run all verification levels ---
    print("\n[5/6] Running verification levels...", file=sys.stderr, flush=True)

    report: dict = {
        "timestamp": datetime.now(UTC).isoformat(),
        "symbol": SYMBOL,
        "threshold_dbps": THRESHOLD_DBPS,
        "bar_counts": {
            "clickhouse": len(ch_bars),
            "batch_recomputed": len(batch_bars),
            "streaming_recomputed": len(stream_bars),
        },
        "trade_count": len(all_trades),
        "trade_id_range": [first_trade_id, last_trade_id],
        "missing_trade_ids": len(missing_ids),
    }

    # Level 1: Atomic verification
    print("  Level 1: Atomic operations (threshold, breach, OHLC)...", file=sys.stderr)
    atomic_issues = verify_atomic_operations(all_trades, batch_bars)
    report["level1_atomic"] = {
        "issues": atomic_issues,
        "pass": len(atomic_issues) == 0,
    }
    print(
        f"    {'PASS' if not atomic_issues else f'FAIL ({len(atomic_issues)} issues)'}",
        file=sys.stderr,
    )

    # Level 2: Bar equality (batch vs streaming vs ClickHouse)
    print("  Level 2: Bar equality (3-way comparison)...", file=sys.stderr)
    comparisons = verify_bar_equality(ch_bars, batch_bars, stream_bars)
    batch_vs_stream_ok = all(c.streaming_match for c in comparisons)
    batch_vs_ch_ok = all(c.batch_match for c in comparisons)
    divergent_bars = [c for c in comparisons if not c.batch_match or not c.streaming_match]

    report["level2_bar_equality"] = {
        "bars_compared": len(comparisons),
        "batch_vs_streaming_identical": batch_vs_stream_ok,
        "batch_vs_clickhouse_identical": batch_vs_ch_ok,
        "divergent_count": len(divergent_bars),
        "divergences": [
            {
                "bar_index": c.bar_index,
                "close_time_ms": c.ch_close_time_ms,
                "batch_match": c.batch_match,
                "streaming_match": c.streaming_match,
                "details": c.divergences,
            }
            for c in divergent_bars
        ],
    }
    print(
        f"    Batch vs Streaming: {'IDENTICAL' if batch_vs_stream_ok else 'DIVERGENT'}",
        file=sys.stderr,
    )
    print(
        f"    Batch vs ClickHouse: {'IDENTICAL' if batch_vs_ch_ok else 'DIVERGENT'}",
        file=sys.stderr,
    )
    if divergent_bars:
        for c in divergent_bars[:5]:
            for d in c.divergences:
                print(f"      bar[{c.bar_index}]: {d}", file=sys.stderr)

    # Level 3: Close-to-next-open transitions
    print("  Level 3: Close-to-next-open transitions...", file=sys.stderr)
    transitions = verify_transitions(batch_bars, all_trades)
    oracle_mismatches = [t for t in transitions if not t.oracle_match]

    # Statistics
    gaps_bps = [t.gap_bps for t in transitions]
    if gaps_bps:
        import statistics
        mean_gap = statistics.mean(gaps_bps)
        median_gap = statistics.median(gaps_bps)
        max_gap = max(gaps_bps)
        exact_matches = sum(1 for g in gaps_bps if g < 0.01)
    else:
        mean_gap = median_gap = max_gap = 0.0
        exact_matches = 0

    report["level3_transitions"] = {
        "total_transitions": len(transitions),
        "oracle_mismatches": len(oracle_mismatches),
        "gap_statistics_bps": {
            "mean": round(mean_gap, 4),
            "median": round(median_gap, 4),
            "max": round(max_gap, 4),
            "exact_matches_pct": round(exact_matches / max(len(transitions), 1) * 100, 1),
        },
        "trade_id_continuity": [],
    }

    # Check trade ID continuity at transitions
    id_gaps = []
    for t in transitions:
        expected = t.close_trade_id + 1
        actual = t.next_open_trade_id
        if expected != actual:
            id_gaps.append({
                "bar_index": t.bar_index,
                "close_trade_id": t.close_trade_id,
                "next_open_trade_id": t.next_open_trade_id,
                "gap": actual - expected,
            })
    report["level3_transitions"]["trade_id_continuity"] = id_gaps

    print(
        f"    Oracle matches: {len(transitions) - len(oracle_mismatches)}/{len(transitions)}",
        file=sys.stderr,
    )
    print(
        f"    Gap stats: mean={mean_gap:.2f} bps, median={median_gap:.2f} bps, "
        f"max={max_gap:.2f} bps",
        file=sys.stderr,
    )
    print(
        f"    Exact matches (gap < 0.01 bps): {exact_matches}/{len(transitions)} "
        f"({exact_matches / max(len(transitions), 1) * 100:.1f}%)",
        file=sys.stderr,
    )
    print(
        f"    Trade ID continuity gaps: {len(id_gaps)}",
        file=sys.stderr,
    )

    # Boundary conditions
    print("  Boundary conditions...", file=sys.stderr)
    boundary_issues = check_boundary_conditions(ch_bars, all_trades)
    report["boundary_conditions"] = {
        "issues": boundary_issues,
        "pass": len(boundary_issues) == 0,
    }
    if boundary_issues:
        for issue in boundary_issues[:10]:
            print(f"    {issue}", file=sys.stderr)

    # --- Phase 6: Summary ---
    print("\n[6/6] Summary", file=sys.stderr, flush=True)
    print("=" * 72, file=sys.stderr)

    all_pass = (
        report["level1_atomic"]["pass"]
        and batch_vs_stream_ok
        and batch_vs_ch_ok
        and len(oracle_mismatches) == 0
    )

    report["verdict"] = "PASS" if all_pass else "FAIL"
    report["exit_code"] = 0 if all_pass else 1

    # Print top-10 largest gaps for inspection
    if transitions:
        sorted_by_gap = sorted(transitions, key=lambda t: t.gap_bps, reverse=True)
        report["top_10_gaps"] = [
            {
                "bar_index": t.bar_index,
                "close_price": t.close_price,
                "next_open_price": t.next_open_price,
                "gap_bps": t.gap_bps,
                "close_trade_id": t.close_trade_id,
                "next_open_trade_id": t.next_open_trade_id,
                "oracle_match": t.oracle_match,
            }
            for t in sorted_by_gap[:10]
        ]

    status = "PASS" if all_pass else "FAIL"
    print(f"\n  Verdict: {status}", file=sys.stderr)
    print(f"  Level 1 (atomic):     {'PASS' if report['level1_atomic']['pass'] else 'FAIL'}", file=sys.stderr)
    print(f"  Level 2 (batch=stream): {'PASS' if batch_vs_stream_ok else 'FAIL'}", file=sys.stderr)
    print(f"  Level 2 (batch=CH):   {'PASS' if batch_vs_ch_ok else 'FAIL'}", file=sys.stderr)
    print(f"  Level 3 (transitions): {'PASS' if not oracle_mismatches else 'FAIL'}", file=sys.stderr)
    print(f"  Boundary conditions:  {'PASS' if report['boundary_conditions']['pass'] else 'FAIL'}", file=sys.stderr)

    # Write artifacts
    ARTIFACTS_DIR.mkdir(exist_ok=True)
    ts = datetime.now(UTC).strftime("%Y%m%d_%H%M%S")
    report_path = ARTIFACTS_DIR / f"oracle_verification_{ts}.json"
    report_path.write_text(json.dumps(report, indent=2, default=str) + "\n")

    latest = ARTIFACTS_DIR / "oracle_verification_latest.json"
    latest.unlink(missing_ok=True)
    latest.symlink_to(report_path.name)

    # Print full report to stdout
    print(json.dumps(report, indent=2, default=str))

    print(f"\n  Report: {report_path}", file=sys.stderr)
    return report["exit_code"]


if __name__ == "__main__":
    sys.exit(main())
