"""
Odds Movement Monitor — opening→closing line movement + steam radar.
===================================================================
Reads live_odds_history (filled by data-fetcher.task.ts every 15 min for
upcoming matches, all markets) and reports, PER MATCH:
  * opening odd (first capture) vs closing odd (latest capture)
  * total move % = (closing - opening) / opening   ← the headline signal
  * the steam side (the selection that shortened the most = money/info/şike)

Why opening→closing matters: it is the market's TOTAL revision. A side that
shortened a lot from open to close = the market learned something. If you can
bet EARLY (before the shortening), that gap is real value (positive CLV) — the
one realistic edge vs İddaa. As a closing bettor it's a RISK FILTER: heavy
late steam against your pick = skip.

Capture is done by the NestJS cron now (DB); this is a pure READER.

Usage:
  python scripts/monitor_odds_movement.py                 # MS movers
  python scripts/monitor_odds_movement.py --min-move 0.08 --market "Maç Sonucu"
"""
from __future__ import annotations
import argparse, os, sys, time
from collections import defaultdict

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

AI_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, AI_DIR)
from data.db import get_clean_dsn  # noqa: E402
import psycopg2  # noqa: E402
from psycopg2.extras import RealDictCursor  # noqa: E402


def connect():
    last = None
    for _ in range(8):
        try:
            return psycopg2.connect(get_clean_dsn())
        except Exception as e:
            last = e; time.sleep(3)
    raise last


def main():
    ap = argparse.ArgumentParser(description=__doc__)
    ap.add_argument("--min-move", type=float, default=0.05,
                    help="flag matches whose focus-market move >= this fraction (default 0.05)")
    ap.add_argument("--market", default="Maç Sonucu", help="focus market for the watchlist")
    ap.add_argument("--limit", type=int, default=25)
    args = ap.parse_args()

    with connect() as c, c.cursor(cursor_factory=RealDictCursor) as cur:
        cur.execute("SELECT to_regclass('public.live_odds_history') AS ex")
        if not cur.fetchall()[0]["ex"]:
            print("live_odds_history yok — NestJS cron'u henüz yazmamış (deploy/build kontrol)."); return

        # opening (earliest) + closing (latest) per match/market/selection
        cur.execute("""
            SELECT match_id, market, selection,
                   (array_agg(new_value ORDER BY change_time ASC))[1]  AS opening,
                   (array_agg(new_value ORDER BY change_time DESC))[1] AS closing,
                   count(*) AS ticks
            FROM live_odds_history
            GROUP BY match_id, market, selection
        """)
        rows = cur.fetchall()
        if not rows:
            print("live_odds_history boş (henüz yakalama yok)."); return

        # per match aggregation
        by_match = defaultdict(lambda: {"focus": {}, "any_ticks": 0, "max_abs": 0.0})
        for r in rows:
            mid = r["match_id"]; o = r["opening"]; cl = r["closing"]
            d = by_match[mid]
            d["any_ticks"] = max(d["any_ticks"], r["ticks"])
            if o and cl and o > 0:
                mv = (cl - o) / o
                d["max_abs"] = max(d["max_abs"], abs(mv))
                if r["market"] == args.market:
                    d["focus"][r["selection"]] = (o, cl, mv)

        # team names + kickoff
        ids = list(by_match.keys())
        names = {}
        if ids:
            cur.execute("""SELECT lm.id, ht.name h, at.name a, lm.mst_utc
                           FROM live_matches lm
                           JOIN teams ht ON ht.id=lm.home_team_id
                           JOIN teams at ON at.id=lm.away_team_id
                           WHERE lm.id = ANY(%s)""", (ids,))
            for r in cur.fetchall():
                names[r["id"]] = (f"{r['h']} v {r['a']}", r["mst_utc"])

    moved = [(m, d) for m, d in by_match.items() if d["any_ticks"] > 1]
    print("="*78)
    print("ODDS MOVEMENT — açılış→kapanış (live_odds_history)")
    print("="*78)
    print(f"izlenen maç: {len(by_match)} | hareket başlamış (>1 yakalama): {len(moved)}")
    if not moved:
        print("\nHenüz hareket yok — hepsi tek yakalama (açılış). Oranlar oynadıkça dolacak.")
        print("(NestJS 15-dk cron'u her tazelemede değişen oranı ekliyor.)")
        return

    flagged = sorted(
        [(m, d) for m, d in moved if d["focus"] and d["max_abs"] >= args.min_move],
        key=lambda x: -x[1]["max_abs"],
    )
    now = int(time.time()*1000)
    print(f"\n{args.market} hareketi >= %{args.min_move*100:.0f} olan maçlar:")
    print(f"  {'maç':<32}{'sel':>5}{'açılış':>8}{'kapanış':>9}{'hareket':>9}")
    print("  "+"-"*64)
    for mid, d in flagged[:args.limit]:
        nm, mst = names.get(mid, (mid[:30], None))
        ko = ""
        if mst:
            mins = (mst-now)/60000
            ko = f"  KO~{mins/60:.1f}h" if mins > 0 else "  (başladı)"
        # steam side = most shortened (most negative move)
        steam = min(d["focus"].items(), key=lambda kv: kv[1][2])
        print(f"  {nm[:30]:<32}{'':>5}{'':>8}{'':>9}{'':>9}{ko}")
        for sel, (o, cl, mv) in d["focus"].items():
            tag = " ↓STEAM" if sel == steam[0] and mv < 0 else ""
            print(f"  {'':<32}{sel:>5}{o:>8.2f}{cl:>9.2f}{100*mv:>+8.1f}%{tag}")
    if not flagged:
        print("  (eşiği geçen yok — hareketler küçük)")
    print("\nOKUMA: kapanışta oynuyorsan, pick'ine KARŞI ↓STEAM olan maçı PAS geç.")
    print("Erken oynayabiliyorsan, kısalan tarafı açılışta yakalamak = gerçek değer (CLV).")


if __name__ == "__main__":
    main()
