#!/usr/bin/env python3
"""delor sport CLI — diagnostic wrapper for one-off checks that used to be
hand-written `python3 -c "..."` snippets over SSH (risk_tier/market
distributions, notify-pipeline health, a live model probe on a real match).
Reads advisor.db directly for everything except `probe` (needs Keychain +
OmniRoute). Always prints JSON to stdout.

Usage: python3 sport_cli.py <command> [args]
Commands: stats, notify, matches, probe, insights, db
"""
from __future__ import annotations

import argparse
import asyncio
import collections
import datetime
import json
import os
import sqlite3
import sys
import time
from pathlib import Path

SCRIPT_DIR = Path(__file__).resolve().parent
SRC_DIR = SCRIPT_DIR.parent / "src"
DB_PATH = SCRIPT_DIR.parent / "data" / "advisor" / "advisor.db"


def _conn() -> sqlite3.Connection:
    conn = sqlite3.connect(str(DB_PATH))
    conn.row_factory = sqlite3.Row
    return conn


def _load_omniroute_env() -> None:
    """probe needs OMNIROUTE_API_KEY; load it from the same env file the
    launchd services use so the caller doesn't have to `source` it first."""
    if os.environ.get("OMNIROUTE_API_KEY"):
        return
    env_path = Path.home() / ".codex" / "omniroute.env"
    if not env_path.exists():
        return
    for line in env_path.read_text().splitlines():
        line = line.strip()
        if line.startswith("export "):
            line = line[len("export "):]
        if line and not line.startswith("#") and "=" in line:
            k, _, v = line.partition("=")
            v = v.strip()
            if len(v) >= 2 and v[0] == v[-1] and v[0] in ("'", '"'):
                v = v[1:-1]
            os.environ.setdefault(k.strip(), v)


# --------------------------------------------------------------- stats


def cmd_stats(args: argparse.Namespace) -> dict:
    conn = _conn()
    cutoff = int(time.time()) - args.days * 86400

    def _dist(sql: str) -> dict:
        return {(r[0] if r[0] is not None else "?"): r[1]
                for r in conn.execute(sql, (cutoff,))}

    risk = _dist("SELECT risk_tier, COUNT(*) FROM sport_pick_results "
                  "WHERE created_ts > ? GROUP BY risk_tier")
    result = _dist("SELECT result, COUNT(*) FROM sport_pick_results "
                    "WHERE created_ts > ? GROUP BY result")
    market_family = collections.Counter()
    for (label,) in conn.execute(
        "SELECT market_label FROM sport_pick_results WHERE created_ts > ?",
        (cutoff,),
    ):
        market_family[(label or "?").split("/")[0].strip()] += 1
    conn.close()
    return {
        "days": args.days,
        "risk_tier": risk,
        "result": result,
        "market_family": dict(market_family.most_common()),
    }


# --------------------------------------------------------------- notify


def cmd_notify(args: argparse.Namespace) -> dict:
    conn = _conn()
    cutoff = int(time.time()) - args.days * 86400
    subs = [dict(r) for r in conn.execute(
        "SELECT user_id, risk_tier, enabled FROM notify_subscriptions "
        "ORDER BY user_id, risk_tier",
    )]
    pushed_total = conn.execute(
        "SELECT COUNT(*) FROM notify_sent WHERE sent_ts > ?", (cutoff,),
    ).fetchone()[0]
    pushed_distinct = conn.execute(
        "SELECT COUNT(DISTINCT cache_key || outcome_id) FROM notify_sent "
        "WHERE sent_ts > ?", (cutoff,),
    ).fetchone()[0]
    settled = conn.execute(
        "SELECT COUNT(*) FROM sport_pick_results WHERE created_ts > ? "
        "AND result IN ('win','loss')", (cutoff,),
    ).fetchone()[0]
    with_note = conn.execute(
        "SELECT COUNT(*) FROM sport_pick_results WHERE created_ts > ? "
        "AND result IN ('win','loss') AND outcome_note IS NOT NULL", (cutoff,),
    ).fetchone()[0]
    delivered = conn.execute(
        "SELECT COUNT(*) FROM sport_pick_results WHERE created_ts > ? "
        "AND result IN ('win','loss') AND notified_ts IS NOT NULL", (cutoff,),
    ).fetchone()[0]
    conn.close()
    return {
        "days": args.days,
        "subscriptions": subs,
        "push_notifications": {
            "distinct_picks": pushed_distinct, "total_sends": pushed_total,
        },
        "learning_conversion": {
            "settled_win_loss": settled,
            "lesson_generated": with_note,
            "outcome_notified": delivered,
            "conversion_pct": (
                round(100.0 * with_note / settled, 1) if settled else None
            ),
        },
    }


# --------------------------------------------------------------- matches


def cmd_matches(args: argparse.Namespace) -> dict:
    conn = _conn()
    clauses: list[str] = []
    params: list = []
    if args.status:
        clauses.append("status = ?")
        params.append(args.status)
    if args.sport:
        clauses.append("sport = ?")
        params.append(args.sport)
    where = ("WHERE " + " AND ".join(clauses)) if clauses else ""
    params.append(args.limit)
    rows = conn.execute(
        "SELECT match_id, sport, league, team_home, team_away, start_ts, "
        "status, score, "
        "(SELECT COUNT(*) FROM sport_match_markets k "
        " WHERE k.match_id=m.match_id AND k.is_active=1) AS markets "
        f"FROM sport_matches m {where} ORDER BY start_ts ASC LIMIT ?",
        params,
    ).fetchall()
    conn.close()
    out = []
    for r in rows:
        d = dict(r)
        d["start_at"] = (
            datetime.datetime.fromtimestamp(d["start_ts"]).isoformat()
            if d["start_ts"] else None
        )
        out.append(d)
    return {"matches": out}


# --------------------------------------------------------------- insights


def cmd_insights(_args: argparse.Namespace) -> dict:
    sys.path.insert(0, str(SRC_DIR))
    from delor.advisor.domain.sport.pick_insights import (
        bad_slices,
        compute_insights,
    )
    ins = compute_insights(str(DB_PATH))
    # slice keys are (sport, market_family) tuples -- not JSON-serializable
    ins["slices"] = {
        (f"{k[0]}/{k[1]}" if isinstance(k, tuple) else k): v
        for k, v in ins.get("slices", {}).items()
    }
    bad = sorted(f"{sport}/{fam}" for sport, fam in bad_slices(str(DB_PATH)))
    return {"insights": ins, "bad_slices": bad}


# --------------------------------------------------------------- db (raw SQL)


def cmd_db(args: argparse.Namespace) -> dict:
    sql = args.sql.strip()
    if not sql.lower().startswith("select"):
        return {"error": "only SELECT queries are allowed"}
    conn = _conn()
    try:
        rows = conn.execute(sql).fetchall()
    finally:
        conn.close()
    return {"rows": [dict(r) for r in rows], "count": len(rows)}


# --------------------------------------------------------------- probe


async def _probe_async(match_id: str, model_override: str | None = None) -> dict:
    sys.path.insert(0, str(SRC_DIR))
    _load_omniroute_env()

    from delor.advisor.domain.sport import sport_analyzer as sa
    from delor.advisor.domain.sport.llm_voters import build_sport_voters
    from delor.advisor.llm_client import AdvisorLLM
    from delor.advisor.domain.sport.pick_insights import market_family
    from delor.advisor.domain.sport.web_search import search_match_context
    from delor.advisor.storage.advisor_db import (
        AdvisorDB,
        derive_advisor_fernet_key,
    )
    from delor.config import settings
    from delor.models.omniroute import OmniRouteClient
    from delor.security.keychain import get_master_key

    db = AdvisorDB(
        settings.advisor_data_dir / "advisor.db",
        derive_advisor_fernet_key(get_master_key()),
    )

    match = sa._fetch_match(db, match_id)
    if not match:
        return {"error": f"match {match_id} not found"}
    markets = db.get_match_markets(match_id)
    candidates = sa._pick_candidate_markets(markets)
    fam_counts = collections.Counter(
        market_family(m.get("market_label")) for m in candidates
    )

    snippets = await search_match_context(
        match["team_home"], match["team_away"], match.get("league") or "",
        match.get("sport") or "", timeout=4.0, max_results=9, allow_merge=False,
    )
    context_snippets = [
        {"title": s.title, "url": s.url, "txt": s.snippet} for s in snippets
    ]
    context_snippets = sa._augment_with_sport_notes(db, match, context_snippets)

    payload = sa._build_user_payload(
        match, candidates, 10000, [], context_snippets, all_markets=markets,
    )
    user_msg = json.dumps(payload, ensure_ascii=False)
    system = sa._load_prompt()

    omni = OmniRouteClient(timeout_seconds=120.0)
    # --model оверрайд (для A/B-бенча: notion/fable-5 vs боевая). Без него —
    # ровно продовая цепочка build_sport_voters (DashScope sport_llm_model).
    if model_override:
        model = AdvisorLLM(omni, model=model_override)
        used_model = model_override
    else:
        model = build_sport_voters(omni)
        used_model = getattr(model, "model", "sport_voters")
    raw = await model.analyze(system, user_msg, temperature=0.0, max_tokens=2048)
    await omni.close()

    picks = sa._extract_json_array(raw) or []
    validated = sa._validate_model_picks(db, match_id, picks, match, context_snippets)
    validated_oids = {v.get("outcome_id") for v in validated}

    lbl_by_oid = {m.get("outcome_id"): m.get("market_label") for m in markets}
    odds_by_oid = {m.get("outcome_id"): m.get("odds") for m in markets}

    def _describe(p: dict) -> dict:
        oid = p.get("outcome_id")
        return {
            "outcome_id": oid,
            "market_label": lbl_by_oid.get(oid),
            "odds": odds_by_oid.get(oid),
            "risk_tier": sa._risk_tier_from_odds(odds_by_oid.get(oid)),
            "confidence_1_5": p.get("confidence_1_5"),
            "reasoning_ru": p.get("reasoning_ru"),
        }

    def _diagnose_rejection(p: dict) -> str:
        """Mirrors _validate_model_picks's exact check order so the reported
        reason matches what actually happened, not a guess."""
        oid = str(p.get("outcome_id") or "")
        presence = db.markets_exist(match_id, [oid]) if oid else {}
        if not (oid and presence.get(oid)):
            return "market_not_found"
        ok, why = sa._is_quality_sport_pick(p, match)
        if not ok:
            return why
        ok_a, why_a = sa._amateur_league_cap(p, match)
        if not ok_a:
            return why_a
        ok_cit, why_cit = sa._verify_citations_exist(
            str(p.get("reasoning_ru") or ""), len(context_snippets or []),
        )
        if not ok_cit:
            return why_cit
        try:
            if int(p.get("confidence_1_5") or 0) < 4:
                return "confidence_below_4"
        except (TypeError, ValueError):
            return "confidence_invalid"
        return (
            "passed_anti_hallucination (only later gates could still drop "
            "it: faithfulness/coherence/bad_slices)"
        )

    rejected = [
        {**_describe(p), "reject_reason": _diagnose_rejection(p)}
        for p in picks if p.get("outcome_id") not in validated_oids
    ]

    return {
        "match_id": match_id,
        "model_used": used_model,
        "teams": f"{match['team_home']} vs {match['team_away']}",
        "sport": match.get("sport"),
        "total_active_markets": len(markets),
        "candidate_pool_size": len(candidates),
        "candidate_pool_by_family": dict(fam_counts),
        "grounding_snippets": len(context_snippets),
        "raw_picks_from_model": len(picks),
        "survived_anti_hallucination": len(validated),
        "validated_picks": [_describe(p) for p in validated],
        "rejected_picks": rejected,
    }


def cmd_probe(args: argparse.Namespace) -> dict:
    return asyncio.run(_probe_async(args.match_id, getattr(args, "model", None)))


# --------------------------------------------------------------- main


def main() -> None:
    parser = argparse.ArgumentParser(description="Делор спорт — диагностический CLI")
    sub = parser.add_subparsers(dest="command", required=True)

    p_stats = sub.add_parser("stats", help="Распределение risk_tier/market/result за период")
    p_stats.add_argument("--days", type=int, default=7)

    p_notify = sub.add_parser("notify", help="Подписки, push-уведомления, конверсия в урок")
    p_notify.add_argument("--days", type=int, default=7)

    p_matches = sub.add_parser("matches", help="Список текущих матчей")
    p_matches.add_argument("--status", choices=["live", "prematch", "finished"])
    p_matches.add_argument("--sport")
    p_matches.add_argument("--limit", type=int, default=20)

    p_probe = sub.add_parser("probe", help="Живой прогон реальной модели на конкретном матче")
    p_probe.add_argument("match_id")
    p_probe.add_argument("--model", default=None,
                         help="Оверрайд модели для A/B-бенча, напр. notion/fable-5. "
                              "Без него — продовая цепочка sport_voters (DashScope).")

    sub.add_parser("insights", help="pick_insights: срезы hit-rate/ROI + bad_slices")

    p_db = sub.add_parser("db", help="Сырой read-only SQL (только SELECT)")
    p_db.add_argument("sql")

    args = parser.parse_args()
    handlers = {
        "stats": cmd_stats, "notify": cmd_notify, "matches": cmd_matches,
        "probe": cmd_probe, "insights": cmd_insights, "db": cmd_db,
    }
    result = handlers[args.command](args)
    print(json.dumps(result, ensure_ascii=False, indent=2, default=str))


if __name__ == "__main__":
    main()
