"""Reconciler READ-ONLY conto Deribit vs libri dei worker (primo passo verso il
position-manager, audit 2026-06-11: il conto era short 0.027 ETH oltre i libri e
nessuno se n'era accorto per ore).

Confronta, per ogni strumento USDC:
    atteso  = Σ quote reali dei worker (status.json persistiti: fade/DIP/SH single-leg
              + pairs a 2 gambe) + Σ gambe ORFANE registrate (orphan_legs: posizioni
              che il conto ha ancora ma i libri hanno chiuso — drift SPIEGATO)
    reale   = get_positions(currency=USDC) (size USD / mark = amount in coin)

Drift oltre tolleranza (1.5×step del contratto) -> tabella + alert Telegram
`ACCOUNT_DRIFT` (con --telegram). Anti-race: se al primo passaggio c'e' drift,
rilegge libri+conto dopo qualche secondo e segnala solo se persiste (un worker
poteva essere a meta' di un open/close).

Nessun ordine, nessuna modifica di stato: SOLO lettura.

    uv run python scripts/analysis/reconcile_account.py             # stampa
    uv run python scripts/analysis/reconcile_account.py --telegram  # + alert se drift
"""
from __future__ import annotations

import json
import sys
import time
from datetime import datetime, timezone
from pathlib import Path

PROJECT_ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(PROJECT_ROOT))

from src.live.cerbero_client import CerberoClient
from src.live.execution import contract_spec
from src.live.books import real_books, account_net, expected_resting, PAPER
                                                     # fonte UNICA dei libri (usata
                                                     # anche dal guard del netting)

RECHECK_SLEEP = 10          # anti-race: secondi fra i due passaggi
TOL_STEPS = 1.5             # tolleranza = 1.5 × step contratto
STALE_REAL_MIN = 15         # un worker vivo aggiorna status.json ~ogni poll (60s);
                            # real_in_position con mtime oltre questo = NON gestito

# --- Watermark FONDI (2026-06-17): rileva aumenti di capitale (depositi/top-up) e
# cali anomali (prelievi) confrontando il balance USDC col precedente osservato.
# Serve a sapere quando un versamento e' arrivato (per scalare total_capital). ---
FUNDS_STATE = PROJECT_ROOT / "data" / "funds_watch.json"   # data/ e' scrivibile dal
                            # cron host (adriano); data/portfolios e' root (container)
FUNDS_DELTA_MIN = 25.0      # USDC: sotto questo Δbalance e' rumore di trading (PnL/fee orarie)
FUNDS_DELTA_PCT = 0.05      # ...o 5% del balance precedente (robusto al crescere del conto)


def compute_drift(client: CerberoClient | None = None) -> list[dict]:
    client = client or CerberoClient()
    books, orphans = real_books()
    acct = account_net(client)
    rows = []
    for inst in sorted(set(books) | set(orphans) | set(acct)):
        exp = books.get(inst, 0.0) + orphans.get(inst, 0.0)
        real = acct.get(inst, 0.0)
        step = contract_spec(inst).get("step", 0.001)
        tol = TOL_STEPS * step
        rows.append(dict(inst=inst, books=books.get(inst, 0.0),
                         orphans=orphans.get(inst, 0.0), exp=exp, real=real,
                         drift=real - exp, tol=tol,
                         ok=abs(real - exp) <= tol))
    return rows


def _book_orders(client: CerberoClient) -> dict[str, dict]:
    """Ordini aperti sul conto, merge type='all' + 'trigger_all' per order_id
    (Deribit puo' omettere i trigger untriggered da 'all')."""
    orders: dict[str, dict] = {}
    for typ in ("all", "trigger_all"):
        for o in client.get_open_orders(currency="USDC", type=typ) or []:
            oid = str(o.get("order_id") or "")
            if oid:
                orders[oid] = o
    return orders


def _resting_filled(client: CerberoClient, instrument: str, order_id: str) -> float:
    """Amount fillato di un ordine resting dal trade history (fonte autorevole)."""
    try:
        return sum(float(t.get("amount", 0) or 0)
                   for t in client.get_trade_history(limit=100,
                                                     instrument_name=instrument)
                   if str(t.get("order_id")) == str(order_id))
    except Exception:
        return 0.0


def compute_resting_drift(client: CerberoClient | None = None) -> list[dict]:
    """Reconcile degli ordini RESTING (estensione 2026-06-12, dopo il caso MR02_BTC:
    TP resting fillato sul book di notte + disaster-SL sparito, scoperti solo al
    close sim ore dopo). Tre classi di anomalia:

      FILLED_UNBOOKED  l'ordine atteso non e' in book e ha fill nel trade history
                       mentre il worker si crede ancora in posizione (il caso MR02)
      MISSING          l'ordine atteso non e' in book e non ha fill (cancellato da
                       altri/exchange; per il DSL triggered il fill ha un order_id
                       NUOVO -> qui appare MISSING e il drift posizioni completa)
      STALE            ordine in book con label di un nostro worker ma NON atteso
                       dai libri (worker flat/morto: fillerebbe a sorpresa)
    """
    client = client or CerberoClient()
    expected = expected_resting()
    book = _book_orders(client)
    rows: list[dict] = []
    for e in expected:
        if e["order_id"] in book:
            rows.append({**e, "status": "OK"})
            continue
        filled = _resting_filled(client, e["instrument"], e["order_id"])
        rows.append({**e, "status": "FILLED_UNBOOKED" if filled > 0 else "MISSING",
                     "filled": filled})
    exp_ids = {e["order_id"] for e in expected}
    workers = {p.name for p in PAPER.glob("*") if p.is_dir()}
    for oid, o in book.items():
        if oid not in exp_ids and str(o.get("label") or "") in workers:
            rows.append(dict(worker=o.get("label"), instrument=o.get("instrument"),
                             order_id=oid, kind=o.get("order_type"),
                             status="STALE"))
    return rows


def compute_stale_real_positions(max_age_min: int = STALE_REAL_MIN) -> list[dict]:
    """Worker che dichiarano una posizione REALE ma il cui status.json e' fermo da
    oltre max_age_min minuti = posizione NON gestita da nessun runner (caso
    MR02_BTC 1h del 2026-06-12: ritirato dallo swap a 15m mentre short reale, con
    TP perso nel netting -> short nudo su disaster-SL). Il drift di compute_drift
    NON lo vede: lo status fermo conta ancora come 'libro' -> conto == libri.
    Discriminante robusto e venue-agnostico: la STALENESS (un worker vivo scrive
    a ogni poll), cattura ritirati-da-swap, crashati, worker rimossi dal config."""
    now = time.time()
    out: list[dict] = []
    for sp in sorted(PAPER.glob("*/status.json")):
        try:
            st = json.loads(sp.read_text())
        except Exception:
            continue
        if not st.get("real_in_position"):
            continue
        age_min = (now - sp.stat().st_mtime) / 60.0
        if age_min > max_age_min:
            out.append(dict(worker=sp.parent.name, age_min=round(age_min, 1),
                            real_amount=st.get("real_amount") or st.get("real_amount_a"),
                            real_side=st.get("real_side") or st.get("real_side_a", "")))
    return out


def compute_funds_change(client: CerberoClient | None = None,
                         currency: str = "USDC") -> dict:
    """Watermark dei FONDI: confronta il `balance` USDC corrente con l'ultimo
    osservato (persistito in FUNDS_STATE) e segnala un AUMENTO (probabile deposito/
    top-up di capitale) o un CALO anomalo (probabile prelievo). Read-only; aggiorna
    SEMPRE il watermark (-> l'alert scatta UNA volta sul gradino, non si ripete).

    Si traccia `balance` (cassa realizzata: cambia su deposito/prelievo/PnL chiuso/fee),
    NON `equity` (che ondeggia con l'unrealized -> falsi positivi). Soglia robusta =
    max(FUNDS_DELTA_MIN, FUNDS_DELTA_PCT × balance_prec): il PnL realizzato di trading
    in un'ora resta sotto, un versamento la supera nettamente. Primo run = solo seed."""
    client = client or CerberoClient()
    s = client.get_account_summary(currency)
    cur_bal = float(s.get("balance", s.get("equity", 0.0)) or 0.0)
    cur_eq = float(s.get("equity", cur_bal) or 0.0)
    iso = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
    out = dict(currency=currency, balance=cur_bal, equity=cur_eq, ts=iso,
               prev_balance=None, delta=0.0, thr=0.0, kind="seed")
    prev = None
    if FUNDS_STATE.exists():
        try:
            prev = json.loads(FUNDS_STATE.read_text())
        except Exception:
            prev = None
    if prev is not None and "balance" in prev:
        pb = float(prev["balance"])
        delta = cur_bal - pb
        thr = max(FUNDS_DELTA_MIN, FUNDS_DELTA_PCT * abs(pb))
        out.update(prev_balance=pb, prev_ts=prev.get("ts"), delta=delta, thr=thr,
                   kind="increase" if delta >= thr else
                        "decrease" if delta <= -thr else "flat")
    try:                                            # persisti il nuovo watermark
        FUNDS_STATE.parent.mkdir(parents=True, exist_ok=True)
        FUNDS_STATE.write_text(json.dumps(
            {"ts": iso, "balance": cur_bal, "equity": cur_eq, "currency": currency}))
    except Exception as e:
        out["persist_error"] = str(e)
    return out


def main():
    client = CerberoClient()
    rows = compute_drift(client)
    resting = compute_resting_drift(client)
    stale = compute_stale_real_positions()
    try:
        funds = compute_funds_change(client)
    except Exception as e:
        funds = {"kind": "error", "error": str(e)}
    bad = [r for r in rows if not r["ok"]]
    bad_rest = [r for r in resting if r["status"] != "OK"]
    if bad or bad_rest:
        # anti-race: un worker poteva essere a meta' open/close -> ricontrolla
        print(f"anomalie (pos={len(bad)} resting={len(bad_rest)}): "
              f"ricontrollo fra {RECHECK_SLEEP}s (anti-race)...")
        time.sleep(RECHECK_SLEEP)
        rows = compute_drift(client)
        resting = compute_resting_drift(client)
        bad = [r for r in rows if not r["ok"]]
        bad_rest = [r for r in resting if r["status"] != "OK"]

    print(f"{'strumento':<22}{'libri':>10}{'orfani':>9}{'atteso':>10}{'conto':>10}{'drift':>10}  esito")
    for r in rows:
        print(f"{r['inst']:<22}{r['books']:>10.4f}{r['orphans']:>9.4f}{r['exp']:>10.4f}"
              f"{r['real']:>10.4f}{r['drift']:>+10.4f}  {'OK' if r['ok'] else '⚠️ DRIFT'}")
    if not rows:
        print("(nessuna posizione attesa ne' reale)")

    print(f"\n{'resting':<45}{'kind':>6}  {'order_id':<22}  stato")
    for r in resting:
        print(f"{r['worker']:<45}{r['kind']:>6}  {r['order_id']:<22}  "
              f"{'OK' if r['status'] == 'OK' else '⚠️ ' + r['status']}")
    if not resting:
        print("(nessun ordine resting atteso ne' in book)")

    print(f"\n{'worker stantio (pos. reale non gestita)':<45}{'amount':>10}{'eta_min':>9}")
    for r in stale:
        print(f"{r['worker']:<45}{(r['real_amount'] or 0):>10}{r['age_min']:>9.0f}  ⚠️ STALE_REAL")
    if not stale:
        print("(nessuna posizione reale stantia)")

    print(f"\n{'FONDI conto (' + funds.get('currency', 'USDC') + ')':<30}{'prec':>12}{'attuale':>12}{'Δ':>12}")
    if funds["kind"] == "error":
        print(f"   ⚠️ lettura fondi fallita: {funds.get('error', '')[:120]}")
    elif funds["kind"] == "seed":
        print(f"   baseline registrata: balance={funds['balance']:.2f} (primo run, nessun confronto)")
    else:
        tag = {"increase": "⬆️ AUMENTO (deposito?)", "decrease": "⬇️ CALO (prelievo?)",
               "flat": "= invariato"}[funds["kind"]]
        print(f"   {'balance':<27}{funds['prev_balance']:>12.2f}{funds['balance']:>12.2f}"
              f"{funds['delta']:>+12.2f}  {tag}  (soglia ±{funds['thr']:.2f})")

    print("\nESITO:", "OK — conto allineato ai libri" if not (bad or bad_rest or stale)
          else f"⚠️ ANOMALIE (drift_pos={len(bad)} resting={len(bad_rest)} stale={len(stale)})")

    if stale and "--telegram" in sys.argv:
        from src.live.telegram_notifier import notify_event
        notify_event("STALE_REAL_POSITION", {
            "worker": [{k: r.get(k) for k in ("worker", "real_amount", "real_side",
                                              "age_min")} for r in stale],
            "note": ("posizione REALE con status.json fermo da >%d min = worker non "
                     "gestito (ritirato da uno swap di timeframe / crashato): la "
                     "posizione e' nuda sul conto, flattare a mano. Caso MR02_BTC 1h "
                     "del 2026-06-12." % STALE_REAL_MIN)})
        print("[telegram] alert STALE_REAL_POSITION inviato")

    if (bad or bad_rest) and "--telegram" in sys.argv:
        from src.live.telegram_notifier import notify_event
        if bad:
            notify_event("ACCOUNT_DRIFT", {
                "strumenti": {r["inst"]: {"atteso": round(r["exp"], 5),
                                          "conto": round(r["real"], 5),
                                          "drift": round(r["drift"], 5)} for r in bad},
                "note": ("conto != libri worker oltre tolleranza (drift NON spiegato dagli "
                         "orfani registrati): verificare close cappati/gambe respinte — "
                         "vedi docs/diary/2026-06-11-system-audit.md")})
            print("[telegram] alert ACCOUNT_DRIFT inviato")
        if bad_rest:
            notify_event("RESTING_DRIFT", {
                "ordini": [{k: r.get(k) for k in ("worker", "kind", "order_id", "status",
                                                  "filled")} for r in bad_rest],
                "note": ("FILLED_UNBOOKED = resting fillato col worker ancora in posizione "
                         "(caso MR02_BTC 2026-06-12); MISSING = atteso ma non in book; "
                         "STALE = in book senza libro corrispondente")})
            print("[telegram] alert RESTING_DRIFT inviato")

    if funds.get("kind") in ("increase", "decrease") and "--telegram" in sys.argv:
        from src.live.telegram_notifier import notify_event
        ev = "FUNDS_INCREASE" if funds["kind"] == "increase" else "FUNDS_DECREASE"
        notify_event(ev, {
            "valuta": funds["currency"],
            "balance_prec": round(funds["prev_balance"], 2),
            "balance_attuale": round(funds["balance"], 2),
            "delta": round(funds["delta"], 2),
            "equity": round(funds["equity"], 2),
            "note": ("AUMENTO fondi (probabile DEPOSITO/top-up): per usarli, scala "
                     "total_capital in portfolios.yml e riavvia il runner "
                     "(docker compose up -d)" if funds["kind"] == "increase"
                     else "CALO fondi non spiegato dal trading (prelievo? verifica il conto)")})
        print(f"[telegram] alert {ev} inviato")
    return bool(bad or bad_rest or stale)


if __name__ == "__main__":
    # Il run delle 11:40 del 2026-06-12 e' morto in silenzio su un 502 (Deribit/gateway
    # giu') -> il guardiano che non suona e' indistinguibile dal "tutto ok". Su errore:
    # alert RECONCILE_FAIL (con --telegram) + exit 2.
    try:
        sys.exit(1 if main() else 0)
    except Exception as exc:
        print(f"RECONCILE_FAIL: {exc}")
        if "--telegram" in sys.argv:
            try:
                from src.live.telegram_notifier import notify_event
                notify_event("RECONCILE_FAIL", {
                    "errore": str(exc)[:200],
                    "note": "reconciler NON eseguito: conto non verificato in quest'ora"})
            except Exception:
                pass
        sys.exit(2)
