"""CrossSectionalWorker — paper/live worker per XS01 (reversione cross-sectional, 8 asset).

Mirror ESATTO di scripts.strategies.XS01_cross_sectional.xsec_sim: ogni HOLD barre
classifica gli asset per rendimento su LB barre, pesi w = -(ret - media)/gross (market-
neutral gross 1), entra al close, esce dopo HOLD barre, riallinea (1 barra di stacco fra
uscita e nuovo ingresso, come l'engine). PnL su book log-return netto fee 0.10% RT.
Stato persistente (resume). Solo SIM (esecuzione reale a 8 gambe non implementata).

PHASE-TRANCHING (2026-06-11, gate xs01_tranche_gate.py): param `tranches`=K divide il
book in K sub-book sfasati di hold/K barre, capitale comune (PnL/K per tranche). La fase
del roll non-sovrapposto e' arbitraria e da sola muove Sharpe FULL daily 1.52-2.33 e DD
13.8-33.1% (timing-luck): l'ensemble di fase la elimina SENZA parametri fittati (plateau
K=2 e K=3 entrambi promossi; PORT06 OOS Sh 10.07->10.15, DD 1.48->1.38). Solo path live,
come disp_min: il backtest canonico resta single-phase. K=1 = comportamento storico.
"""
from __future__ import annotations

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

import numpy as np
import pandas as pd

from src.live.telegram_notifier import notify_event


class CrossSectionalWorker:
    def __init__(self, universe, tf="1h", params=None, capital=1000.0,
                 position_size=0.15, leverage=3.0, fee_rt=0.0005,
                 name="XS01", data_dir=Path("data/portfolio_paper")):
        self.universe = list(universe)
        p = params or {}
        self.lb = int(p.get("lb", 48))
        self.hold = int(p.get("hold", 12))
        # dispersion-gate (2026-06-10): entra solo se la std cross-section del
        # momentum lb supera disp_min — senza dispersione da far rientrare i
        # trade sono fee. None = off (parita' col backtest canonico non filtrato).
        self.disp_min = p.get("disp_min")
        self.tf = tf
        self.initial_capital = capital
        self.position_size = position_size
        self.leverage = leverage
        self.fee_rt = fee_rt
        self.worker_id = f"{name}__{tf}"
        self.work_dir = Path(data_dir) / self.worker_id
        self.work_dir.mkdir(parents=True, exist_ok=True)
        self.status_path = self.work_dir / "status.json"
        self.trades_path = self.work_dir / "trades.jsonl"

        self.k = max(1, int(p.get("tranches", 1)))
        self._step = max(1, round(self.hold / self.k))   # sfasamento iniziale fra tranche
        self.capital = capital
        self.books = [self._flat_book(j * self._step) for j in range(self.k)]
        self.total_trades = 0
        self.total_wins = 0
        self.last_bar_ts = 0
        self._load()

    def _flat_book(self, wait: int = 0):
        return {"weights": {a: 0.0 for a in self.universe},
                "entry_px": {a: 0.0 for a in self.universe},
                "bars_held": 0, "in_position": False, "wait": int(wait)}

    @property
    def in_position(self) -> bool:
        return any(b["in_position"] for b in self.books)

    # ---------- persistenza ----------
    def _load(self):
        if not self.status_path.exists():
            self._log("INIT", {"capital": self.capital, "universe": self.universe,
                               "lb": self.lb, "hold": self.hold, "tranches": self.k})
            return
        s = json.loads(self.status_path.read_text())
        self.capital = s.get("capital", self.initial_capital)
        self.total_trades = s.get("total_trades", 0)
        self.total_wins = s.get("total_wins", 0)
        self.last_bar_ts = s.get("last_bar_ts", 0)
        if "books" in s:
            for j, bs in enumerate(s["books"][: self.k]):
                b = self.books[j]
                b["weights"] = {**{a: 0.0 for a in self.universe}, **bs.get("weights", {})}
                b["entry_px"] = {**{a: 0.0 for a in self.universe}, **bs.get("entry_px", {})}
                b["bars_held"] = int(bs.get("bars_held", 0))
                b["in_position"] = bool(bs.get("in_position", False))
                b["wait"] = int(bs.get("wait", 0))
        elif s.get("in_position") or s.get("weights"):
            # migrazione dallo schema legacy single-book: il vecchio book diventa la
            # tranche 0; le altre partono flat col loro sfasamento (gia' in __init__)
            b = self.books[0]
            b["weights"] = {**{a: 0.0 for a in self.universe}, **s.get("weights", {})}
            b["entry_px"] = {**{a: 0.0 for a in self.universe}, **s.get("entry_px", {})}
            b["bars_held"] = int(s.get("bars_held", 0))
            b["in_position"] = bool(s.get("in_position", False))
            b["wait"] = 0

    def _save(self):
        self.status_path.write_text(json.dumps({
            "capital": round(float(self.capital), 2), "in_position": bool(self.in_position),
            "tranches": int(self.k),
            "books": [{"weights": {a: round(float(v), 5) for a, v in b["weights"].items()},
                       "entry_px": {a: float(v) for a, v in b["entry_px"].items()},
                       "bars_held": int(b["bars_held"]), "in_position": bool(b["in_position"]),
                       "wait": int(b["wait"])} for b in self.books],
            "total_trades": int(self.total_trades), "total_wins": int(self.total_wins),
            "last_bar_ts": int(self.last_bar_ts),
            "last_update": datetime.now(timezone.utc).isoformat(),
        }, indent=2))

    def _log(self, event, data=None):
        entry = {"ts": datetime.now(timezone.utc).isoformat(), "worker": self.worker_id,
                 "event": event, **(data or {})}
        with open(self.trades_path, "a") as f:
            f.write(json.dumps(entry, default=str) + "\n")
        print(f"  [{self.worker_id}] {event}: {json.dumps(data or {}, default=str)[:160]}")

    def _notify(self, event, data=None):
        notify_event(event, {"worker": self.worker_id, **(data or {})})

    # ---------- pannello allineato ----------
    def _panel(self, data: dict):
        frames = []
        for a in self.universe:
            df = data.get(a)
            if df is None or df.empty:
                return None
            frames.append(df[["timestamp", "close"]].rename(columns={"close": a}).set_index("timestamp"))
        M = pd.concat(frames, axis=1, join="inner").sort_index()
        # scarta la barra IN FORMAZIONE (close non settled) — come gli altri worker
        from src.live.bars import last_bar_is_forming
        ts = M.index.to_numpy()
        if len(ts) and last_bar_is_forming(ts):
            M = M.iloc[:-1]
        return M

    # ---------- weights (identici all'engine) ----------
    def _weights(self, logC_row, logC_lb_row):
        dm = logC_row - logC_lb_row
        dm = dm - dm.mean()
        w = -dm
        gw = np.sum(np.abs(w))
        return w / gw if gw > 1e-9 else None

    def _close_book(self, b, closes_now, tranche: int):
        """Realizza il PnL del book della tranche al prezzo attuale (log-return netto fee).
        Capitale comune: il notional della tranche e' 1/K del book virtuale."""
        book = 0.0
        for k, a in enumerate(self.universe):
            book += b["weights"][a] * np.log(closes_now[k] / b["entry_px"][a])
        # cast a tipi Python: i numpy (float64/int64/bool_) rompono json.dumps in _save
        net = float(book - 2 * self.fee_rt)
        pnl = float(self.capital * self.position_size * self.leverage * net / self.k)
        self.capital = max(self.capital + pnl, 10.0)
        self.total_trades += 1
        self.total_wins += 1 if net > 0 else 0
        acc = self.total_wins / self.total_trades * 100 if self.total_trades else 0
        self._log("CLOSE", {"tranche": tranche, "book_ret": round(book * 100, 3),
                            "net": round(net * 100, 3),
                            "pnl": round(pnl, 2), "capital": round(self.capital, 2),
                            "trades": self.total_trades, "acc": round(acc, 1)})
        b["in_position"] = False
        b["weights"] = {a: 0.0 for a in self.universe}

    def _open_book(self, M, i, b, tranche: int):
        cols = list(M.columns)
        logC = np.log(M.values)
        if self.disp_min is not None:
            disp = float(np.nanstd(logC[i] - logC[i - self.lb]))
            if disp < float(self.disp_min):
                return                      # regime senza dispersione: skip entry
        w = self._weights(logC[i], logC[i - self.lb])
        if w is None:
            return
        closes = M.iloc[i].values
        b["weights"] = {a: float(w[cols.index(a)]) for a in self.universe}
        b["entry_px"] = {a: float(closes[cols.index(a)]) for a in self.universe}
        b["bars_held"] = 0
        b["in_position"] = True
        self._log("OPEN", {"tranche": tranche,
                           "long": [a for a in self.universe if b["weights"][a] > 0.05],
                           "short": [a for a in self.universe if b["weights"][a] < -0.05],
                           "capital": round(self.capital, 2)})

    # ---------- tick ----------
    def tick(self, data: dict):
        M = self._panel(data)
        if M is None or len(M) < self.lb + 1:     # serve close[i] e close[i-lb] -> lb+1 barre
            return
        i = len(M) - 1
        cur_ts = int(M.index[i])
        new_bar = cur_ts > self.last_bar_ts

        for j, b in enumerate(self.books):
            if b["in_position"]:
                if new_bar:
                    b["bars_held"] += 1
                # esce dopo HOLD barre; NON rientra nello stesso tick -> entry-to-entry = hold+1
                if b["bars_held"] >= self.hold:
                    self._close_book(b, M.iloc[i].values, j)
            elif b["wait"] > 0:
                if new_bar:
                    b["wait"] -= 1            # sfasamento iniziale della tranche
            else:
                self._open_book(M, i, b, j)   # entra al bar corrente (i = lb alla prima volta)
        # solo avanti: se il panel si accorcia per un feed in ritardo (inner join),
        # non si regredisce — una barra gia' contata non va ricontata
        self.last_bar_ts = max(self.last_bar_ts, cur_ts)
        self._save()

    @property
    def status_summary(self) -> str:
        acc = self.total_wins / self.total_trades * 100 if self.total_trades else 0
        nb = sum(1 for b in self.books if b["in_position"])
        st = f"BOOK {nb}/{self.k}" if nb else "FLAT"
        return f"{self.worker_id}: €{self.capital:.0f} | {self.total_trades}t {acc:.0f}% | {st}"
