"""Retroactive exchange gap sweep: classify existing Stathera gaps as exchange gaps.

One-time script that examines all existing Stathera gaps on bigblack, checks if
checksum-verified Parquet confirms 0 trades in the TID range, and logs the gap
as result='exchange_gap' in kintsugi_log for permanent dismiss.

Usage:
    python scripts/retroactive_exchange_gap_sweep.py [--dry-run] [--symbol BTCUSDT] [--threshold 250]
"""

from __future__ import annotations

import argparse
import logging
import sys
from datetime import UTC, datetime, timedelta

import polars as pl

from opendeviationbar.kintsugi._helpers import _get_mode_for_symbol, _get_worker_cache
from opendeviationbar.kintsugi.discovery import Shard, _get_dismissed_set, discover_shards
from opendeviationbar.storage.parquet import TickStorage

logger = logging.getLogger(__name__)


def _classify_shard(
    shard: Shard,
    storage: TickStorage,
) -> str:
    """Classify a single shard using column-projected TID read (v7.3).

    FAST PATH: reads ONLY the agg_trade_id column (~3ms/file vs 43ms for all 7).
    Anti-pattern: do NOT use storage.read_ticks() here (loads all columns).
    Anti-pattern: do NOT download Vision ZIPs to compute SHA-256.

    Returns one of:
    - 'exchange_gap': Parquet TIDs sequential + 0 trades in shard TID range
    - 'pipeline_gap': trades exist in shard TID range (bar-level gap, kintsugi-healable)
    - 'no_parquet': Parquet files missing for some dates
    - 'non_sequential_tids': TIDs have gaps in Parquet (needs Vision checksum verify)
    """
    from pathlib import Path

    lo_us = min(shard.gap_start_ms, shard.gap_end_ms)
    hi_us = max(shard.gap_start_ms, shard.gap_end_ms)
    start_date = datetime.fromtimestamp(lo_us / 1_000_000, tz=UTC).date()
    end_date = datetime.fromtimestamp(hi_us / 1_000_000, tz=UTC).date()

    # Resolve Parquet paths and check existence (O(1) per day via Path.exists)
    parquet_paths: list[Path] = []
    base = Path(storage.ticks_dir) / shard.symbol
    # Also check BINANCE_SPOT_ prefixed directory (symlink convention)
    if not base.exists():
        base = Path(storage.ticks_dir) / f"BINANCE_SPOT_{shard.symbol}"
    if not base.exists():
        return "no_parquet"

    current = start_date
    while current <= end_date:
        p = base / f"{current.isoformat()}.parquet"
        if not p.exists():
            return "no_parquet"
        parquet_paths.append(p)
        current += timedelta(days=1)

    if not parquet_paths:
        return "no_parquet"

    # Column-projected read: ONLY agg_trade_id (~3ms/file vs 43ms for all 7)
    tid_frames: list[pl.Series] = []
    for p in parquet_paths:
        tids = pl.read_parquet(p, columns=["agg_trade_id"])["agg_trade_id"]
        if len(tids) > 0:
            tid_frames.append(tids)

    if not tid_frames:
        return "no_parquet"

    # Combine and check TID sequentiality
    all_tids = pl.concat(tid_frames, rechunk=False) if len(tid_frames) > 1 else tid_frames[0]
    del tid_frames
    tids_sequential = bool((all_tids.diff().drop_nulls() <= 1).all())

    if not tids_sequential:
        return "non_sequential_tids"

    # TIDs are sequential → source data complete. Check if shard TID range has trades.
    if shard.anchor_last_tid is not None and shard.resume_first_tid is not None:
        has_trades = bool(
            ((all_tids > shard.anchor_last_tid) & (all_tids < shard.resume_first_tid)).any()
        )
    elif shard.anchor_last_tid is not None:
        has_trades = bool((all_tids > shard.anchor_last_tid).any())
    else:
        has_trades = len(all_tids) > 0

    if not has_trades:
        return "exchange_gap"

    return "pipeline_gap"


def _log_exchange_gap(shard: Shard) -> None:
    """INSERT exchange gap into kintsugi_log with result='exchange_gap'."""
    cache = _get_worker_cache()
    cache.client.command(
        "INSERT INTO opendeviationbar_cache.kintsugi_log "
        "(symbol, threshold_decimal_bps, priority, "
        "gap_start_ms, gap_end_ms, gap_hours, "
        "action, result, ariadne_from_id, ouroboros_splits, "
        "bars_written, duration_seconds, error_message, ouroboros_mode) "
        "VALUES "
        "({symbol:String}, {threshold:UInt32}, {priority:String}, "
        "{gap_start:Int64}, {gap_end:Int64}, {gap_hours:Float64}, "
        "{action:String}, {result:String}, {ariadne:Int64}, "
        "{splits:UInt8}, {bars:UInt32}, {duration:Float64}, "
        "{error:String}, {ouroboros_mode:String})",
        parameters={
            "symbol": shard.symbol,
            "threshold": shard.threshold_decimal_bps,
            "priority": "",
            "gap_start": shard.gap_start_ms,
            "gap_end": shard.gap_end_ms,
            "gap_hours": shard.gap_hours,
            "action": "retroactive_sweep",
            "result": "exchange_gap",
            "ariadne": shard.anchor_last_tid or 0,
            "splits": 0,
            "bars": 0,
            "duration": 0.0,
            "error": "",
            "ouroboros_mode": _get_mode_for_symbol(shard.symbol),
        },
    )


def main() -> None:
    """Run retroactive exchange gap sweep."""
    parser = argparse.ArgumentParser(
        description="Retroactive sweep: classify existing Stathera gaps as exchange gaps"
    )
    parser.add_argument(
        "--dry-run",
        action="store_true",
        help="Print what would be classified without INSERTing to kintsugi_log",
    )
    parser.add_argument(
        "--symbol",
        type=str,
        default=None,
        help="Filter to specific symbol (e.g. BTCUSDT)",
    )
    parser.add_argument(
        "--threshold",
        type=int,
        default=None,
        help="Filter to specific threshold in dbps (e.g. 250)",
    )
    args = parser.parse_args()

    logging.basicConfig(
        level=logging.INFO,
        format="%(asctime)s %(levelname)s %(name)s: %(message)s",
    )

    print("=== Retroactive Exchange Gap Sweep ===")
    print(f"Mode: {'DRY RUN' if args.dry_run else 'LIVE (will INSERT to kintsugi_log)'}")
    if args.symbol:
        print(f"Symbol filter: {args.symbol}")
    if args.threshold:
        print(f"Threshold filter: {args.threshold}")
    print()

    # Step 1: Discover all shards
    print("Discovering shards...")
    shards = discover_shards(symbol=args.symbol, threshold=args.threshold)
    total_shards = len(shards)
    print(f"Found {total_shards} total shards")

    # Step 2: Load dismissed set to skip already-dismissed shards
    dismissed = _get_dismissed_set()

    # Step 3: Initialize resources
    storage = TickStorage()

    # Counters
    n_dismissed = 0
    n_overlap = 0
    n_genesis = 0
    n_no_anchor = 0
    n_exchange_gap = 0
    n_pipeline_gap = 0
    n_no_parquet = 0
    n_unverified = 0
    exchange_gap_shards: list[Shard] = []

    # Step 3: Classify each shard
    for i, shard in enumerate(shards):
        # Progress every 100 shards
        if (i + 1) % 100 == 0:
            print(
                f"Processed {i + 1}/{total_shards} shards: "
                f"{n_exchange_gap} exchange_gap, {n_pipeline_gap} pipeline, "
                f"{n_no_parquet} no_parquet"
            )

        # Skip already-dismissed
        key = (shard.symbol, shard.threshold_decimal_bps, shard.gap_start_ms)
        if key in dismissed:
            n_dismissed += 1
            continue

        # Skip overlaps
        if shard.is_overlap:
            n_overlap += 1
            continue

        # Skip genesis shards
        if shard.is_genesis:
            n_genesis += 1
            continue

        # Skip shards without anchor TID (can't define TID range)
        if shard.anchor_last_tid is None:
            n_no_anchor += 1
            continue

        # Classify
        classification = _classify_shard(shard, storage)
        if classification == "exchange_gap":
            n_exchange_gap += 1
            exchange_gap_shards.append(shard)
        elif classification == "pipeline_gap":
            n_pipeline_gap += 1
        elif classification == "no_parquet":
            n_no_parquet += 1
        elif classification == "non_sequential_tids":
            n_unverified += 1

    # Step 4: Log exchange gaps to kintsugi_log
    if exchange_gap_shards and not args.dry_run:
        print(f"\nINSERTing {len(exchange_gap_shards)} exchange gap records to kintsugi_log...")
        for shard in exchange_gap_shards:
            _log_exchange_gap(shard)
        print("Done.")

    # Step 5: Summary report
    print()
    print("=" * 60)
    print("RETROACTIVE EXCHANGE GAP SWEEP SUMMARY")
    print("=" * 60)
    print(f"Total shards discovered:       {total_shards:>8}")
    print(f"Already dismissed (skipped):   {n_dismissed:>8}")
    print(f"Overlaps (skipped):            {n_overlap:>8}")
    print(f"Genesis shards (skipped):      {n_genesis:>8}")
    print(f"No anchor TID (skipped):       {n_no_anchor:>8}")
    print(f"---")
    print(f"Exchange gaps classified:      {n_exchange_gap:>8}  {'(DRY RUN)' if args.dry_run else '(logged to kintsugi_log)'}")
    print(f"Pipeline gaps remaining:       {n_pipeline_gap:>8}  (kintsugi will repair)")
    print(f"No Parquet available:          {n_no_parquet:>8}  (unclassifiable)")
    print(f"Unverified Parquet:            {n_unverified:>8}  (unclassifiable, no checksum)")
    print("=" * 60)

    if args.dry_run and exchange_gap_shards:
        print(f"\nDRY RUN: Would have classified {len(exchange_gap_shards)} shards as exchange_gap.")
        print("Run without --dry-run to INSERT to kintsugi_log.")


if __name__ == "__main__":
    main()
