"""Report orario PORT06 -> Telegram.

Legge lo stato persistito del paper trader a portafoglio (data/portfolio_paper/*/ +
data/portfolios/PORT06/status.json) e invia su Telegram:
  1) trade CHIUSI: positivi/negativi (netto fee) con breakdown per motivo e PnL;
  2) trade IN CORSO (posizioni aperte);
  3) PnL realizzato totale + equity mark-to-market.

Eseguibile standalone (es. da cron orario):
    cd /opt/docker/PythagorasGoal && uv run python scripts/portfolios/hourly_report.py

Carica .env da solo (cron non eredita l'env del container). Legge file world-readable
scritti dal container; non tocca lo stato del trader.
"""
from __future__ import annotations

import json
import glob
from collections import defaultdict
from datetime import datetime, timezone
from pathlib import Path

ROOT = Path(__file__).resolve().parents[2]
PAPER = ROOT / "data" / "portfolio_paper"
PAPER_STATS = ROOT / "data" / "portfolio_paper_stats"   # sleeve PAPER fuori dal conto reale
PORT_STATUS = ROOT / "data" / "portfolios" / "PORT06" / "status.json"


def _load_env():
    """Carica TELEGRAM_* da .env nell'os.environ (cron non li ha)."""
    import os
    envf = ROOT / ".env"
    if not envf.exists():
        return
    for line in envf.read_text().splitlines():
        line = line.strip()
        if not line or line.startswith("#") or "=" not in line:
            continue
        k, v = line.split("=", 1)
        os.environ.setdefault(k.strip(), v.strip())


def _short(wid: str) -> str:
    """SH01_shape_ml__BTC__1h -> SH01/BTC ; PR01_..._ETH_SOL__1h -> PR01/ETH_SOL."""
    parts = wid.split("__")
    code = parts[0].split("_")[0]
    tag = parts[1] if len(parts) > 1 else ""
    return f"{code}/{tag}" if tag else code


# --- monitor filtri fade: stop-rate per epoca di config ---
# epoche: PRE (nessun filtro) -> HURST (loss-guard 0.55, v1.0.0) -> TREND (swap
# hurst->trend_max=3.0, 2026-06-07: gate trendmax_port06_impact, hurst ridondante
# post-EXIT-16). Confronta lo stop-rate live di ogni epoca col backtest.
LOSSGUARD_SINCE = "2026-06-02T14:34:30"
TRENDSWAP_SINCE = "2026-06-07T10:10:00"
FADE_PREFIXES = ("MR01", "MR02", "MR07")
LOSSGUARD_MIN_SAMPLE = 30


def lossguard_section() -> str:
    pre = [0, 0]      # [closes, stops] nessun filtro
    hurst = [0, 0]    # epoca loss-guard Hurst
    trend = [0, 0]    # epoca filtro trend (config attuale)
    for sp in glob.glob(str(PAPER / "*" / "status.json")):
        wid = Path(sp).parent.name
        if not wid.startswith(FADE_PREFIXES):
            continue
        tp = Path(sp).parent / "trades.jsonl"
        if not tp.exists():
            continue
        for line in tp.read_text().splitlines():
            if not line.strip():
                continue
            ev = json.loads(line)
            if ev.get("event") != "CLOSE":
                continue
            ts = ev.get("ts", "")
            b = trend if ts >= TRENDSWAP_SINCE else hurst if ts >= LOSSGUARD_SINCE else pre
            b[0] += 1
            if ev.get("reason") == "stop_loss":
                b[1] += 1

    def rate(b):
        return b[1] / b[0] * 100 if b[0] else 0.0

    L = ["🛡️ <b>Filtri fade — stop-rate per epoca</b>"]
    L.append(f"  PRE {rate(pre):.0f}% (n={pre[0]}) → HURST {rate(hurst):.0f}% (n={hurst[0]})"
             f" → TREND {rate(trend):.0f}% (n={trend[0]})")
    if trend[0] >= LOSSGUARD_MIN_SAMPLE:
        delta = rate(pre) - rate(trend)
        L.append(f"  VERDETTO epoca TREND (n≥{LOSSGUARD_MIN_SAMPLE}): {delta:+.0f}pp vs PRE → "
                 f"{'✅ riduce gli stop' if delta > 0 else '⚠️ nessuna riduzione'}")
    else:
        L.append(f"  campione TREND {trend[0]}/{LOSSGUARD_MIN_SAMPLE} → verdetto rimandato")
    return "\n".join(L)


# Epoca v1.1.26 (deploy 2026-06-11 ~21:40 UTC): gate TP_PHANTOM attivo. I close
# precedenti includono il churn da TP fantasma dell'11-06 17:32-17:58 (~24 giri,
# win-rate inquinato) -> le accuratezze "pulite" si leggono da qui in poi.
EPOCH_V1126 = "2026-06-11T21:40:00"


def collect():
    closed = []          # (sleeve, reason, net_return, pnl, win, ts)
    open_pos = []         # dict per posizione aperta
    realized = 0.0
    for sp in sorted(glob.glob(str(PAPER / "*" / "status.json"))):
        d = Path(sp).parent
        wid = d.name
        st = json.loads(Path(sp).read_text())
        tp = d / "trades.jsonl"
        if tp.exists():
            for line in tp.read_text().splitlines():
                if not line.strip():
                    continue
                ev = json.loads(line)
                if ev.get("event") != "CLOSE":
                    continue
                nr = ev.get("net_return", 0.0)
                pnl = ev.get("pnl", 0.0)
                realized += pnl
                # win = flag del worker (col real-truth segue il PnL REALE; net_return
                # resta il sim diagnostico: sui TP fantasma da spike testnet diceva
                # 26/0 mentre il reale era 11/15). Fallback nr>0 per eventi storici.
                closed.append((_short(wid), ev.get("reason", "?"), nr, pnl,
                               bool(ev.get("win", nr > 0)), ev.get("ts", "")))
        if "positions" in st or "weights" in st:
            continue        # multi-asset (TR01/ROT02/TSM01): sezione dedicata
        if st.get("in_position"):
            open_pos.append({
                "sleeve": _short(wid),
                "dir": st.get("direction", 0),
                "entry": st.get("entry_price") or st.get("entry_a"),
                "entry_b": st.get("entry_b"),
                "bars": st.get("bars_held", 0),
                "cap": st.get("capital", 0.0),
            })
    return closed, open_pos, realized


def multi_asset_section() -> str:
    """Sleeve PAPER (TR01/ROT02/TSM01): SOLO statistica, FUORI dal conto reale dei
    €2000 (2026-06-08). Vivono in data/portfolio_paper_stats/ con capitale nozionale
    fisso, raccolti per eventuali future implementazioni reali. Book corrente + eta'
    ultimo flip + freschezza."""
    now = datetime.now(timezone.utc)
    rows = []
    for sp in sorted(glob.glob(str(PAPER_STATS / "*" / "status.json"))):
        d = Path(sp).parent
        st = json.loads(Path(sp).read_text())
        book = st.get("positions") if "positions" in st else st.get("weights")
        if book is None and "books" in st:
            # XS01 tranched (2026-06-11): aggrega i sub-book in un book medio per-asset
            k = max(1, len(st["books"]))
            book = {}
            for b in st["books"]:
                for a, v in (b.get("weights") or {}).items():
                    book[a] = book.get(a, 0.0) + v / k
        if book is None:
            continue        # single-leg/pairs: gia' coperti da collect()
        # abs(): il book XS01 e' long/short market-neutral — col filtro v>0 le
        # gambe SHORT sparivano e un book net-short appariva "flat" nel report
        held = {a: v for a, v in book.items() if abs(v) > 1e-4}
        flip = "mai"
        tp = d / "trades.jsonl"
        if tp.exists():
            lines = [ln for ln in tp.read_text().splitlines() if ln.strip()]
            if lines:
                ts = json.loads(lines[-1]).get("ts", "")
                if ts:
                    days = (now - datetime.fromisoformat(ts)).days
                    flip = f"{days}g fa"
        fresh = "?"
        lu = st.get("ts") or st.get("last_update")
        if lu:
            h = (now - datetime.fromisoformat(lu)).total_seconds() / 3600
            fresh = "OK" if h < 2 else f"STALE {h:.0f}h"
        code = d.name.split("__")[0].split("_")[0]   # TR01_basket__... -> TR01
        hb = ",".join(f"{a}:{v:.2f}" for a, v in sorted(held.items())) if held else "flat"
        rows.append(f"{code:<7}{hb:<26}{flip:>8}  {fresh}")
    if not rows:
        return ""
    return ("📈 <b>PAPER — solo statistica, FUORI dal conto reale</b> (book | ultimo flip | status)\n<pre>"
            + "\n".join(rows) + "</pre>")


def build_report() -> str:
    closed, open_pos, realized = collect()
    pos = sum(1 for c in closed if c[4])
    neg = len(closed) - pos

    # breakdown per motivo
    by_reason = defaultdict(lambda: [0, 0, 0.0])   # reason -> [win, loss, pnl]
    for _, reason, _, pnl, win, _ in closed:
        r = by_reason[reason]
        r[0 if win else 1] += 1
        r[2] += pnl

    now = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
    try:
        ver = (ROOT / "VERSION").read_text().strip()
    except Exception:
        ver = "?"
    eq = dd = cap = None
    if PORT_STATUS.exists():
        ps = json.loads(PORT_STATUS.read_text())
        eq, cap, dd = ps.get("equity"), ps.get("total_capital"), ps.get("max_dd")

    L = [f"📊 <b>PORT06 — Report orario</b> <code>v{ver}</code>", now]
    if eq is not None:
        L.append(f"Equity €{eq:.2f} | Cap €{cap:.2f} | maxDD {dd:.3f}%")

    # 1) CHIUSI — totale storico + epoca corrente (post gate TP_PHANTOM): i
    # numeri pre-v1.1.26 includono il churn fantasma e non misurano la strategia
    cur = [c for c in closed if c[5] >= EPOCH_V1126]
    cpos = sum(1 for c in cur if c[4])
    L.append(f"\n✅ <b>CHIUSI</b>: {pos} positivi / {neg} negativi (netto fee)")
    L.append(f"   epoca v1.1.26+ (TP_PHANTOM attivo): {cpos}/{len(cur) - cpos}")
    rows = [f"{'motivo':<12}{'✅':>3}{'❌':>4}{'PnL€':>9}"]
    for reason, (w, l, pnl) in sorted(by_reason.items(), key=lambda x: x[1][2]):
        rows.append(f"{reason:<12}{w:>3}{l:>4}{pnl:>+9.2f}")
    L.append("<pre>" + "\n".join(rows) + "</pre>")

    # 2) IN CORSO
    L.append(f"🟢 <b>IN CORSO</b>: {len(open_pos)} posizioni")
    if open_pos:
        rows = [f"{'sleeve':<14}{'d':<2}{'barre':>6}  {'entry'}"]
        for p in sorted(open_pos, key=lambda x: x["sleeve"]):
            d = "L" if p["dir"] == 1 else "S" if p["dir"] == -1 else "-"
            entry = p["entry"]
            es = f"{entry:.6g}" if isinstance(entry, (int, float)) else str(entry)
            if p["entry_b"]:
                es = f"{entry:.6g}/{p['entry_b']:.6g}"     # coppia: 2 gambe
            rows.append(f"{p['sleeve']:<14}{d:<2}{p['bars']:>6}  {es}")
        L.append("<pre>" + "\n".join(rows) + "</pre>")

    # 2a) worker multi-asset (TR01/ROT02/TSM01)
    mas = multi_asset_section()
    if mas:
        L.append(mas)

    # 2b) monitor loss-guard
    L.append(lossguard_section())

    # 3) TOTALE
    L.append(f"💰 <b>PnL realizzato totale: €{realized:+.2f}</b>")
    if eq is not None:
        unreal = eq - cap
        L.append(f"   equity mark-to-market: €{eq:.2f} (non realizz. €{unreal:+.2f})")
    return "\n".join(L)


def main():
    _load_env()
    import sys
    sys.path.insert(0, str(ROOT))
    from src.live.telegram_notifier import send_telegram
    report = build_report()
    print(report)
    ok = send_telegram(report)
    print("\n[telegram]", "inviato" if ok else "NON inviato (token/chat mancanti o errore rete)")


if __name__ == "__main__":
    main()
