"""GridWorker — Price Ladder (griglia) live SIM/PAPER, shadow-stage 1.

Worker live per la strategia Price Ladder (griglia geometrica con regime-gate + SL/TP,
config vincente del branch price_ladder_research). STAGE 1 = SIM/PAPER: gira sul feed LIVE
Deribit (stessi dati di decisione degli altri worker) e contabilizza l'equity mark-to-market
col MOTORE CANONICO `grid_mtm` (parita' col backtest per costruzione), MA non piazza ordini
reali. Accumula un track record paper per validare live-vs-backtest prima dello shadow reale.

NON esegue ordini: l'esecuzione reale (griglia di LIMIT resting su Deribit, gestione fill
parziali/episodi) e' lo STAGE 2, dietro testnet + autorizzazione esplicita (soldi veri,
siamo su mainnet). Per costruzione il runner avvia ordini reali solo per kind in
('single','ml'); kind='grid' resta sim.

Stato persistente (status.json): capital, peak, max_dd, n_trades, last_ts -> resume al restart.
"""
from __future__ import annotations

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

import numpy as np
import pandas as pd

from scripts.analysis.grid_game_gate import grid_mtm


def _regime_mask(df: pd.DataFrame, ema_n: int, trend_max: float) -> np.ndarray:
    """Mask CAUSALE 'range-bound' allineata a df (== ladder_search.regime_mask, ma su df live)."""
    c = df["close"].to_numpy(float)
    h = df["high"].to_numpy(float); l = df["low"].to_numpy(float)
    ema = pd.Series(c).ewm(span=ema_n, adjust=False).mean().to_numpy()
    pc = np.roll(c, 1); pc[0] = c[0]
    tr = np.maximum(h - l, np.maximum(np.abs(h - pc), np.abs(l - pc)))
    atr = pd.Series(tr).rolling(14).mean().to_numpy()
    with np.errstate(invalid="ignore", divide="ignore"):
        dist = np.abs(c - ema) / np.where(atr == 0, np.nan, atr)
    m = dist < trend_max
    m[~np.isfinite(dist)] = False
    return m


class GridWorker:
    KIND = "grid"

    def __init__(self, sid: str, asset: str, params: dict, capital: float,
                 work_dir: Path, leverage: float = 3.0, position_size: float = 0.15,
                 fee_side: float = 0.0005, notifier=None, hist: pd.DataFrame | None = None):
        self.sid = sid
        self.asset = asset
        self.p = dict(params)                       # tf,range_down,range_up,levels,sl_buf,tp_buf,max_bars,regime,trend_max
        self.leverage = leverage
        self.position_size = position_size
        self.fee_side = fee_side
        self.notifier = notifier
        self.initial_capital = capital
        self.capital = capital
        self.peak = capital
        self.max_dd = 0.0
        self.n_trades = 0
        self.last_ts = ""
        # base_norm = valore dell'equity-norm (cumulata da inizio storia) al DEPLOY: la
        # capital forward = initial * eq[-1]/base_norm -> parte da `initial` e segue il
        # ritorno della griglia DA QUEL MOMENTO (start FISSO: niente salti da finestra mobile).
        self.base_norm = None
        # bootstrap STORIA FULL (start fisso, come SH01): il feed live e' una finestra mobile,
        # ma normalizzando su una serie a start fisso l'equity forward e' stabile.
        if hist is None:
            try:
                from src.data.downloader import load_data
                hist = load_data(asset, self.p.get("tf", "1h"))
            except Exception:
                hist = None
        self.hist = hist
        self.work_dir = Path(work_dir)
        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.in_position = False                    # compat dashboard (la griglia non ha una posizione singola)
        self._load_state()

    def _merge(self, live_df: pd.DataFrame) -> pd.DataFrame:
        """Storia bootstrap + feed live, dedup su timestamp (il live prevale), start FISSO."""
        if self.hist is None or len(self.hist) == 0:
            return live_df
        cols = ["timestamp", "open", "high", "low", "close", "volume"]
        h = self.hist[[c for c in cols if c in self.hist.columns]]
        l = live_df[[c for c in cols if c in live_df.columns]]
        m = pd.concat([h, l], ignore_index=True)
        m = m.drop_duplicates(subset="timestamp", keep="last").sort_values("timestamp")
        return m.reset_index(drop=True)

    def _load_state(self):
        if not self.status_path.exists():
            self._log("INIT", {"capital": round(self.capital, 2), "sid": self.sid})
            return
        s = json.loads(self.status_path.read_text())
        self.capital = s.get("capital", self.initial_capital)
        self.peak = s.get("peak", self.capital)
        self.max_dd = s.get("max_dd", 0.0)
        self.n_trades = s.get("n_trades", 0)
        self.last_ts = s.get("last_ts", "")
        self.base_norm = s.get("base_norm")
        self._log("RESUME", {"capital": round(self.capital, 2), "n_trades": self.n_trades,
                             "base_norm": self.base_norm})

    def _save_state(self):
        self.status_path.write_text(json.dumps({
            "sid": self.sid, "kind": self.KIND, "asset": self.asset,
            "capital": round(self.capital, 4), "peak": round(self.peak, 4),
            "max_dd": round(self.max_dd, 4), "n_trades": self.n_trades,
            "base_norm": self.base_norm, "in_position": self.in_position, "params": self.p,
            "last_ts": self.last_ts, "ts": datetime.now(timezone.utc).isoformat(),
        }, indent=2))

    def _log(self, event: str, extra: dict):
        row = {"ts": datetime.now(timezone.utc).isoformat(), "sid": getattr(self, "sid", "?"),
               "event": event, **extra}
        try:
            with open(self.work_dir / "trades.jsonl", "a") as f:
                f.write(json.dumps(row) + "\n")
        except Exception:
            pass

    def tick(self, df: pd.DataFrame):
        """df = OHLCV live (finestra mobile) fino ad ora. Merge con la storia bootstrap
        (start FISSO), ricomputa la griglia col motore canonico, e mappa il capitale forward:
        capital = initial * eq[-1]/base_norm (parte da `initial` al deploy, segue la griglia
        da li' in poi). SIM only (nessun ordine reale)."""
        if df is None or len(df) < 40:
            return
        full = self._merge(df)
        p = self.p
        regime = p.get("regime", "none")
        mask = (_regime_mask(full, p.get("ema_n", 200), p.get("trend_max", 2.0))
                if regime == "range" else None)
        eqd, st = grid_mtm(
            self.asset, tf=p["tf"], range_down=p["range_down"], range_up=p["range_up"],
            levels=p["levels"], sl_buf=p["sl_buf"], tp_buf=p["tp_buf"], max_bars=p["max_bars"],
            pos=self.position_size, lev=self.leverage, fee_side=self.fee_side,
            flat_skip=True, deploy_mask=mask, df=full)
        if eqd is None or len(eqd) == 0:
            return
        cur = float(eqd.iloc[-1])
        if self.base_norm is None or self.base_norm <= 0:
            self.base_norm = cur                    # baseline al primo tick (deploy)
        self.capital = max(self.initial_capital * cur / self.base_norm, 0.0)
        self.peak = max(self.peak, self.capital)
        if self.peak > 0:
            self.max_dd = max(self.max_dd, (self.peak - self.capital) / self.peak)
        self.n_trades = int(st.get("trades", self.n_trades))
        self.last_ts = str(full.iloc[-1].get("timestamp", ""))
        self._save_state()
        self._log("GRID_MTM", {"capital": round(self.capital, 2), "n_trades": self.n_trades,
                               "win": st.get("win"), "stops": st.get("stops"),
                               "pnl_source": "sim"})
        return self.capital
