"""
Capture Closing Odds — snapshot #2 of the minimal 2-snapshot CLV system.
=======================================================================
WHY: CLV (closing line value) is the only reliable proof of betting edge.
This codebase never captured it: odds are stored as a single static snapshot
and `odds_history` is empty. But the live sync (DataFetcherTask CRON 1) DOES
refresh `live_matches.odds` every 15 min before kickoff, and prediction_runs
already store the bet-time odds blob (odds_snapshot.odds, source=live_match).

This script supplies the missing half: just before kickoff it copies the
*current* live odds blob onto the match's latest prediction_run as
`odds_snapshot.closing_odds`. Later, CLV per bet = bet-time pick odds vs
closing pick odds (computed in live_scoreboard.py once enough data exists).

Run it every ~15 min (e.g. alongside the existing sync, or its own cron):
  python scripts/capture_closing_odds.py            # default 25-min window
  python scripts/capture_closing_odds.py --window-min 20 --dry-run

Structure-agnostic: stores the whole live odds blob; no pick parsing here.
Idempotent: skips runs that already have closing_odds. Only ADDS a JSON key,
never deletes. Safe to run repeatedly.

⚠️ Needs one supervised test run against a live DB with upcoming matches
   before scheduling (DB was down at authoring time).
"""
from __future__ import annotations

import argparse
import json
import os
import sys
import time
from datetime import datetime, timezone

if sys.stdout and hasattr(sys.stdout, "reconfigure"):
    try:
        sys.stdout.reconfigure(encoding="utf-8")
    except Exception:
        pass

SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
AI_ENGINE_DIR = os.path.dirname(SCRIPT_DIR)
sys.path.insert(0, AI_ENGINE_DIR)

from data.db import get_clean_dsn  # noqa: E402
import psycopg2  # noqa: E402
from psycopg2.extras import RealDictCursor  # noqa: E402


def main() -> int:
    ap = argparse.ArgumentParser(description=__doc__)
    ap.add_argument("--window-min", type=int, default=25,
                    help="Capture matches kicking off within the next N minutes (default 25)")
    ap.add_argument("--grace-min", type=int, default=10,
                    help="Also include matches that kicked off up to N min ago (default 10)")
    ap.add_argument("--dry-run", action="store_true",
                    help="Report what would be captured without writing")
    args = ap.parse_args()

    now_ms = int(time.time() * 1000)
    lo_ms = now_ms - args.grace_min * 60 * 1000
    hi_ms = now_ms + args.window_min * 60 * 1000

    captured = skipped = no_run = 0
    with psycopg2.connect(get_clean_dsn()) as conn:
        with conn.cursor(cursor_factory=RealDictCursor) as cur:
            # Upcoming/just-started live matches that still hold pre-kickoff odds.
            cur.execute(
                """
                SELECT id, mst_utc, odds
                FROM live_matches
                WHERE odds IS NOT NULL
                  AND mst_utc BETWEEN %s AND %s
                ORDER BY mst_utc ASC
                """,
                (lo_ms, hi_ms),
            )
            matches = cur.fetchall()
            print(f"[capture_closing_odds] window={args.window_min}m grace={args.grace_min}m "
                  f"upcoming_with_odds={len(matches)} dry_run={args.dry_run}")

            for m in matches:
                mid = m["id"]
                cur.execute(
                    """
                    SELECT id, odds_snapshot
                    FROM prediction_runs
                    WHERE match_id = %s
                    ORDER BY generated_at DESC
                    LIMIT 1
                    """,
                    (mid,),
                )
                run = cur.fetchone()
                if not run:
                    no_run += 1
                    continue
                snap = run["odds_snapshot"] or {}
                if isinstance(snap, str):
                    try:
                        snap = json.loads(snap)
                    except Exception:
                        snap = {}
                if snap.get("closing_odds") is not None:
                    skipped += 1
                    continue

                patch = {
                    "closing_odds": m["odds"],
                    "closing_captured_at": datetime.now(timezone.utc).isoformat(),
                    "closing_mst_utc": m["mst_utc"],
                    "closing_source": "live_match",
                }
                if args.dry_run:
                    captured += 1
                    print(f"  would capture match={mid} run_id={run['id']} mst_utc={m['mst_utc']}")
                    continue
                cur.execute(
                    """
                    UPDATE prediction_runs
                    SET odds_snapshot = COALESCE(odds_snapshot, '{}'::jsonb) || %s::jsonb
                    WHERE id = %s
                    """,
                    (json.dumps(patch, default=str), run["id"]),
                )
                captured += 1
        if not args.dry_run:
            conn.commit()

    print(f"[capture_closing_odds] captured={captured} already_had={skipped} "
          f"no_prediction_run={no_run}")
    return 0


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