"""
CLV Report — the single most important edge metric.
===================================================
Closing Line Value = did we bet at better odds than the market's closing line?
Consistently positive CLV is the only reliable proof of a real betting edge;
negative CLV means no edge, regardless of short-term wins/losses.

This codebase stores the BET-TIME odds for ~92% of runs (prediction_runs.
odds_snapshot.source = 'live_match' with the live odds blob, and the pick's
odds in payload main_pick.odds). For the closing line we use, in order:
  1. odds_snapshot.closing_odds  (captured by capture_closing_odds.py, forward)
  2. odd_selections current value (the static near-final capture — a proxy)

CLV per bet = bet_odds / closing_odds - 1   (positive = beat the close = good).

Read-only. SELECT only.
Usage:
  python scripts/clv_report.py
  python scripts/clv_report.py --staked-only
"""
from __future__ import annotations

import argparse
import json
import os
import sys
from collections import defaultdict
from typing import Any, Dict, Optional, Tuple

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

# market code -> (Turkish odds-category name, pick-normalizer -> selection key)
OU_CATS = {"OU05": "0,5 Alt/Üst", "OU15": "1,5 Alt/Üst", "OU25": "2,5 Alt/Üst",
           "OU35": "3,5 Alt/Üst", "OU45": "4,5 Alt/Üst"}


def _f(x: Any, d: Optional[float] = None) -> Optional[float]:
    try:
        return float(x) if x is not None else d
    except (TypeError, ValueError):
        return d


def _parse(j: Any) -> Dict[str, Any]:
    if isinstance(j, str):
        try:
            return json.loads(j)
        except Exception:
            return {}
    return j or {}


def map_pick(market: str, pick: str) -> Optional[Tuple[str, str]]:
    """Return (category_name, selection_key) for the live-odds JSON / odd_selections."""
    m = (market or "").upper()
    p = (pick or "").strip()
    pl = p.casefold()
    if m in ("MS", "ML", "1X2"):
        return ("Maç Sonucu", p if p in ("1", "X", "2") else None) if p in ("1", "X", "2") else None
    if m == "HT":
        return ("1. Yarı Sonucu", p) if p in ("1", "X", "2") else None
    if m in OU_CATS:
        if "üst" in pl or "ust" in pl or "over" in pl:
            return (OU_CATS[m], "Üst")
        if "alt" in pl or "under" in pl:
            return (OU_CATS[m], "Alt")
        return None
    if m == "DC":
        key = p.upper().replace(" ", "").replace("/", "-")
        norm = {"1X": "1-X", "X1": "1-X", "X2": "X-2", "2X": "X-2",
                "12": "1-2", "21": "1-2", "1-X": "1-X", "X-2": "X-2", "1-2": "1-2"}.get(key)
        return ("Çifte Şans", norm) if norm else None
    if m == "BTTS":
        if "var" in pl or "yes" in pl:
            return ("Karşılıklı Gol", "Var")
        if "yok" in pl or "no" in pl:
            return ("Karşılıklı Gol", "Yok")
        return None
    if m == "OE":
        if "tek" in pl or "odd" in pl:
            return ("Tek/Çift", "Tek")
        if "çift" in pl or "cift" in pl or "even" in pl:
            return ("Tek/Çift", "Çift")
        return None
    return None


def closing_from_blob(blob: Any, cat: str, sel: str) -> Optional[float]:
    blob = _parse(blob)
    cat_map = blob.get(cat) if isinstance(blob, dict) else None
    if isinstance(cat_map, dict):
        return _f(cat_map.get(sel))
    return None


def main() -> int:
    ap = argparse.ArgumentParser(description=__doc__)
    ap.add_argument("--staked-only", action="store_true",
                    help="Only playable/staked bets (default: all picks with a mappable market)")
    args = ap.parse_args()

    rows_out = []
    with psycopg2.connect(get_clean_dsn()) as conn:
        with conn.cursor(cursor_factory=RealDictCursor) as cur:
            cur.execute("""
                SELECT match_id, engine_version, odds_snapshot, payload_summary,
                       eventual_outcome, unit_profit
                FROM prediction_runs
                WHERE odds_snapshot->>'source' = 'live_match'
                ORDER BY generated_at ASC
            """)
            runs = cur.fetchall()

            for r in runs:
                snap = _parse(r["odds_snapshot"])
                ps = _parse(r["payload_summary"])
                mp = ps.get("main_pick") or {}
                market = mp.get("market")
                pick = mp.get("pick")
                bet_odds = _f(mp.get("odds"))
                playable = bool(mp.get("playable"))
                if args.staked_only and not playable:
                    continue
                if not market or not pick or not bet_odds or bet_odds <= 1.0:
                    continue
                mapped = map_pick(market, pick)
                if not mapped or not mapped[1]:
                    continue
                cat, sel = mapped

                # closing line: prefer captured closing_odds, else static odd_selections
                closing = closing_from_blob(snap.get("closing_odds"), cat, sel)
                src = "captured"
                if closing is None:
                    cur.execute("""
                        SELECT os.odd_value FROM odd_categories oc
                        JOIN odd_selections os ON os.odd_category_db_id = oc.db_id
                        WHERE oc.match_id = %s AND oc.name = %s AND os.name = %s
                        LIMIT 1
                    """, (r["match_id"], cat, sel))
                    row = cur.fetchone()
                    closing = _f(row["odd_value"]) if row else None
                    src = "static_proxy"
                if closing is None or closing <= 1.0:
                    continue

                clv = bet_odds / closing - 1.0
                rows_out.append({
                    "market": market, "playable": playable,
                    "bet_odds": bet_odds, "closing": closing, "clv": clv,
                    "src": src, "profit": _f(r["unit_profit"], 0.0) or 0.0,
                    "settled": r["eventual_outcome"] is not None
                    and not str(r["eventual_outcome"]).startswith("NO_BET"),
                })

    if not rows_out:
        print("No mappable runs with both bet-time and closing odds found.")
        return 0

    def agg(rs):
        n = len(rs)
        clvs = [x["clv"] for x in rs]
        pos = sum(1 for c in clvs if c > 0)
        return {
            "n": n,
            "mean_clv_pct": round(100.0 * sum(clvs) / n, 2),
            "pct_positive": round(100.0 * pos / n, 1),
            "captured": sum(1 for x in rs if x["src"] == "captured"),
        }

    print("=" * 70)
    print("CLV REPORT  —  did we beat the closing line? (the edge compass)")
    print("=" * 70)
    o = agg(rows_out)
    print(f"runs analyzed: {o['n']}   (closing source: {o['captured']} captured, "
          f"{o['n'] - o['captured']} static-proxy)")
    print(f"\nOVERALL mean CLV: {o['mean_clv_pct']}%   "
          f"bets beating close: {o['pct_positive']}%")
    print("  (positive mean CLV = real edge; ~0 or negative = no edge)\n")

    staked = [x for x in rows_out if x["playable"]]
    if staked:
        s = agg(staked)
        print(f"STAKED only: n={s['n']}  mean CLV={s['mean_clv_pct']}%  "
              f"beating close={s['pct_positive']}%\n")

    print("BY MARKET")
    by_m = defaultdict(list)
    for x in rows_out:
        by_m[x["market"]].append(x)
    for m, rs in sorted(by_m.items(), key=lambda kv: -len(kv[1])):
        a = agg(rs)
        print(f"  {m:<8} n={a['n']:>4}  mean CLV={a['mean_clv_pct']:>7}%  "
              f"beating close={a['pct_positive']:>5}%")

    # CLV vs outcome sanity: do positive-CLV bets actually win more / lose less?
    print("\nCLV vs realized P/L (settled staked)")
    ss = [x for x in rows_out if x["playable"] and x["settled"]]
    if ss:
        posc = [x for x in ss if x["clv"] > 0]
        negc = [x for x in ss if x["clv"] <= 0]
        for label, grp in (("CLV>0", posc), ("CLV<=0", negc)):
            if grp:
                pr = sum(x["profit"] for x in grp)
                print(f"  {label:<7} n={len(grp):>3}  profit={pr:>7.2f}u  "
                      f"ROI(flat1u)={round(100*pr/len(grp),1)}%")
    print("=" * 70)
    return 0


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