"""Multi-Strategy Paper Trader — orchestratore per N strategie in parallelo."""
from __future__ import annotations

import time
import yaml
from datetime import datetime, timedelta, timezone
from pathlib import Path

import pandas as pd

from src.live.cerbero_client import CerberoClient
from src.live.strategy_loader import load_strategy
from src.live.strategy_worker import StrategyWorker
from src.live.pairs_worker import PairsWorker
from src.live.signal_engine import SignalEngine
from src.live.telegram_notifier import send_telegram

PROJECT_ROOT = Path(__file__).resolve().parents[2]
DATA_DIR = PROJECT_ROOT / "data" / "paper_trades"

RESOLUTION_MAP = {"15m": "15", "1h": "60", "5m": "5"}
# Convenzione Deribit (verificata via Cerbero, 2026-05-29):
#  - BTC/ETH = perpetui INVERSE (margine coin): "<COIN>-PERPETUAL"
#  - altcoin = perpetui LINEARI USDC (margine USDC): "<COIN>_USDC-PERPETUAL", storia dal 2022
# Trappola: "LTC-PERPETUAL"/"ADA-PERPETUAL" = 0 candele; "SOL-PERPETUAL" = contratto vecchio
# con dati sbagliati. Per gli alt usare SEMPRE la forma _USDC-PERPETUAL.
INSTRUMENT_MAP = {
    "BTC": "BTC-PERPETUAL",
    "ETH": "ETH-PERPETUAL",
    "SOL": "SOL_USDC-PERPETUAL",
    "LTC": "LTC_USDC-PERPETUAL",
    "ADA": "ADA_USDC-PERPETUAL",
    "XRP": "XRP_USDC-PERPETUAL",
    "BNB": "BNB_USDC-PERPETUAL",
    "DOGE": "DOGE_USDC-PERPETUAL",
}


class MLWorkerWrapper:
    """Wrapper speciale per ML01 che usa SignalEngine con training."""

    def __init__(self, worker: StrategyWorker, config: dict):
        self.worker = worker
        self.engine = SignalEngine(
            bb_w=config.get("params", {}).get("bb_window", 14),
            sq_thr=config.get("params", {}).get("sq_threshold", 0.8),
            ml_thr=config.get("params", {}).get("ml_threshold", 0.70),
        )
        self.trained = False
        self.last_train: datetime | None = None
        self.retrain_hours = config.get("retrain_hours", 24)

    def needs_training(self) -> bool:
        if not self.trained:
            return True
        if self.last_train is None:
            return True
        elapsed = (datetime.now(timezone.utc) - self.last_train).total_seconds()
        return elapsed > self.retrain_hours * 3600

    def train(self, df: pd.DataFrame, hold: int = 3):
        result = self.engine.train(df, lookahead=hold)
        if "error" not in result:
            self.trained = True
            self.last_train = datetime.now(timezone.utc)
            print(f"  [{self.worker.worker_id}] TRAIN OK: {result}")
        else:
            print(f"  [{self.worker.worker_id}] TRAIN FAIL: {result}")

    def tick(self, df: pd.DataFrame):
        if not self.trained:
            return

        worker = self.worker
        c = df["close"].values
        current_price = float(c[-1])
        current_ts = int(df["timestamp"].iloc[-1])

        if worker.in_position:
            if current_ts > worker.last_bar_ts:
                worker.bars_held += 1
                worker.last_bar_ts = current_ts
            if worker.bars_held >= worker.hold_bars:
                worker._close_position(current_price, "hold_limit")
            else:
                pnl_pct = (current_price - worker.entry_price) / worker.entry_price * worker.direction
                if pnl_pct <= -0.02:
                    worker._close_position(current_price, "stop_loss")
            worker._save_state()
            return

        signal = self.engine.check_signal(df)
        if signal:
            from src.strategies.base import Signal
            direction = 1 if signal["direction"] == "buy" else -1
            sig = Signal(idx=len(df)-1, direction=direction, entry_price=current_price)
            worker._open_position(sig, current_price)
            worker.last_bar_ts = current_ts

        worker._save_state()


def load_config(path: Path) -> dict:
    with open(path) as f:
        return yaml.safe_load(f)


def build_workers(config: dict) -> tuple[list[StrategyWorker], list[MLWorkerWrapper]]:
    """Crea worker da config YAML."""
    defaults = config.get("defaults", {})
    regular_workers: list[StrategyWorker] = []
    ml_workers: list[MLWorkerWrapper] = []

    for entry in config.get("strategies", []):
        if not entry.get("enabled", True):
            continue

        name = entry["name"]
        asset = entry["asset"]
        tf = entry["tf"]
        capital = entry.get("capital", defaults.get("capital", 1000))
        pos_size = entry.get("position_size", defaults.get("position_size", 0.15))
        leverage = entry.get("leverage", defaults.get("leverage", 3))
        hold = entry.get("hold_bars", defaults.get("hold_bars", 3))
        params = entry.get("params", {})

        strategy = load_strategy(name)

        worker = StrategyWorker(
            strategy=strategy, asset=asset, tf=tf,
            capital=capital, position_size=pos_size,
            leverage=leverage, hold_bars=hold,
            params=params, data_dir=DATA_DIR,
        )

        if name == "ML01_squeeze_gbm":
            ml_wrapper = MLWorkerWrapper(worker, {**defaults, **entry})
            ml_workers.append(ml_wrapper)
        else:
            regular_workers.append(worker)

    return regular_workers, ml_workers


def build_pairs_workers(config: dict) -> list[PairsWorker]:
    """Crea i PairsWorker (2 gambe) dalla sezione `pairs:` dello YAML."""
    defaults = config.get("defaults", {})
    workers: list[PairsWorker] = []
    for entry in config.get("pairs", []):
        if not entry.get("enabled", True):
            continue
        workers.append(PairsWorker(
            asset_a=entry["a"], asset_b=entry["b"], tf=entry.get("tf", "1h"),
            params=entry.get("params", {}),
            capital=entry.get("capital", defaults.get("capital", 1000)),
            position_size=entry.get("position_size", defaults.get("position_size", 0.15)),
            leverage=entry.get("leverage", defaults.get("leverage", 3)),
            fee_rt=entry.get("fee_rt", 0.001),
            name=entry.get("name", "PR01_pairs_reversion"),
            data_dir=DATA_DIR,
        ))
    return workers


def run():
    config_path = PROJECT_ROOT / "strategies.yml"
    if not config_path.exists():
        print(f"ERRORE: {config_path} non trovato")
        return

    config = load_config(config_path)
    defaults = config.get("defaults", {})
    poll_seconds = defaults.get("poll_seconds", 60)
    lookback_days = 60
    train_lookback_days = 365

    regular_workers, ml_workers = build_workers(config)
    pairs_workers = build_pairs_workers(config)
    all_worker_count = len(regular_workers) + len(ml_workers) + len(pairs_workers)

    if all_worker_count == 0:
        print("Nessuna strategia abilitata in strategies.yml")
        return

    client = CerberoClient()

    print("=" * 70)
    print(f"  MULTI-STRATEGY PAPER TRADER")
    print(f"  Strategie attive: {all_worker_count}")
    print(f"  Poll: ogni {poll_seconds}s")
    print(f"  Data dir: {DATA_DIR}")
    print("=" * 70)

    for w in regular_workers:
        print(f"  • {w.status_summary}")
    for mw in ml_workers:
        print(f"  • {mw.worker.status_summary} [ML]")
    for pw in pairs_workers:
        print(f"  • {pw.status_summary} [PAIRS]")

    send_telegram(f"🚀 Multi-Strategy avviato: {all_worker_count} strategie")

    # Raccogli asset/tf unici per fetch raggruppato
    def _get_data_keys() -> set[tuple[str, str]]:
        keys = set()
        for w in regular_workers:
            keys.add((w.asset, w.tf))
        for mw in ml_workers:
            keys.add((mw.worker.asset, mw.worker.tf))
        for pw in pairs_workers:                       # entrambe le gambe del pair
            keys.add((pw.asset_a, pw.tf))
            keys.add((pw.asset_b, pw.tf))
        return keys

    # Training iniziale ML
    for mw in ml_workers:
        asset = mw.worker.asset
        instrument = INSTRUMENT_MAP.get(asset, f"{asset}-PERPETUAL")
        resolution = RESOLUTION_MAP.get(mw.worker.tf, "15")
        end = datetime.now(timezone.utc)
        start = end - timedelta(days=train_lookback_days)
        candles = client.get_historical(instrument, start.strftime("%Y-%m-%d"),
                                         end.strftime("%Y-%m-%d"), resolution)
        if candles:
            df_train = pd.DataFrame(candles)
            df_train["timestamp"] = df_train["timestamp"].astype("int64")
            df_train = df_train.sort_values("timestamp").reset_index(drop=True)
            mw.train(df_train, hold=mw.worker.hold_bars)

    while True:
        try:
            data_keys = _get_data_keys()
            candle_cache: dict[tuple[str, str], pd.DataFrame] = {}

            for asset, tf in data_keys:
                instrument = INSTRUMENT_MAP.get(asset, f"{asset}-PERPETUAL")
                resolution = RESOLUTION_MAP.get(tf, "15")
                end = datetime.now(timezone.utc)
                start = end - timedelta(days=lookback_days)

                candles = client.get_historical(
                    instrument, start.strftime("%Y-%m-%d"),
                    end.strftime("%Y-%m-%d"), resolution,
                )
                if candles:
                    df = pd.DataFrame(candles)
                    df["timestamp"] = df["timestamp"].astype("int64")
                    df = df.sort_values("timestamp").reset_index(drop=True)
                    candle_cache[(asset, tf)] = df

            # Fetch 1h live per strategie multi-timeframe (es. MT01):
            # il trend va preso da Cerbero, non dal parquet statico (che resta indietro).
            htf_cache: dict[str, pd.DataFrame] = {}
            mtf_assets = {w.asset for w in regular_workers if w.strategy.name.startswith("MT01")}
            for asset in mtf_assets:
                instrument = INSTRUMENT_MAP.get(asset, f"{asset}-PERPETUAL")
                end = datetime.now(timezone.utc)
                start = end - timedelta(days=lookback_days)
                try:
                    candles_1h = client.get_historical(
                        instrument, start.strftime("%Y-%m-%d"),
                        end.strftime("%Y-%m-%d"), "60",
                    )
                    if candles_1h:
                        df1h = pd.DataFrame(candles_1h)
                        df1h["timestamp"] = df1h["timestamp"].astype("int64")
                        htf_cache[asset] = df1h.sort_values("timestamp").reset_index(drop=True)
                except Exception as e:
                    print(f"  [1h fetch {asset}] ERRORE: {e}")

            # Tick regular workers
            for w in regular_workers:
                key = (w.asset, w.tf)
                if key in candle_cache:
                    try:
                        w.tick(candle_cache[key], df_1h=htf_cache.get(w.asset))
                    except Exception as e:
                        print(f"  [{w.worker_id}] ERRORE: {e}")

            # Tick ML workers
            for mw in ml_workers:
                key = (mw.worker.asset, mw.worker.tf)
                if key not in candle_cache:
                    continue

                if mw.needs_training():
                    mw.train(candle_cache[key], hold=mw.worker.hold_bars)

                try:
                    mw.tick(candle_cache[key])
                except Exception as e:
                    print(f"  [{mw.worker.worker_id}] ERRORE: {e}")

            # Tick pairs workers (2 gambe)
            for pw in pairs_workers:
                ka, kb = (pw.asset_a, pw.tf), (pw.asset_b, pw.tf)
                if ka in candle_cache and kb in candle_cache:
                    try:
                        pw.tick(candle_cache[ka], candle_cache[kb])
                    except Exception as e:
                        print(f"  [{pw.worker_id}] ERRORE: {e}")

            # Status periodico
            now = datetime.now(timezone.utc)
            if now.minute == 0 and now.second < poll_seconds:
                lines = [f"📊 Status {now.strftime('%H:%M')} UTC"]
                for w in regular_workers:
                    lines.append(f"  {w.status_summary}")
                for mw in ml_workers:
                    lines.append(f"  {mw.worker.status_summary} [ML]")
                for pw in pairs_workers:
                    lines.append(f"  {pw.status_summary} [PAIRS]")
                send_telegram("\n".join(lines))

        except KeyboardInterrupt:
            print("\nShutdown...")
            for w in regular_workers:
                if w.in_position:
                    df = candle_cache.get((w.asset, w.tf))
                    if df is not None and not df.empty:
                        w._close_position(float(df["close"].iloc[-1]), "shutdown")
                w._save_state()
            for mw in ml_workers:
                if mw.worker.in_position:
                    df = candle_cache.get((mw.worker.asset, mw.worker.tf))
                    if df is not None and not df.empty:
                        mw.worker._close_position(float(df["close"].iloc[-1]), "shutdown")
                mw.worker._save_state()
            for pw in pairs_workers:                    # salva stato; non forzo la chiusura a 2 gambe
                pw._save_state()
            send_telegram("🛑 Multi-Strategy arrestato")
            break
        except Exception as e:
            print(f"  ERRORE GLOBALE: {e}")
            import traceback
            traceback.print_exc()

        time.sleep(poll_seconds)


if __name__ == "__main__":
    run()
