#!/usr/bin/env python3
"""Paper Trading Gate — simulated trading with promotion criteria.

Before a strategy goes live, it must prove itself in paper trading.  This
module runs a paper trading simulation, tracks performance, and enforces
the promotion gates.

Trust level progression: sandbox → paper → live_reduced → live_full → proven

Usage::

    from paper_trading_gate import PaperTradingGate

    gate = PaperTradingGate()
    gate.start_paper("my_strategy")
    gate.feed_bar(bar)           # feed bars one by one
    report = gate.status()
    if report["can_promote"]:
        gate.promote_to_live_reduced()
"""

from __future__ import annotations

import json
import time as _time
from dataclasses import dataclass, field
from datetime import datetime, timezone
from pathlib import Path
from typing import Any

import config_manager


@dataclass
class PaperGateConfig:
    """Promotion criteria for advancing from paper to live_reduced."""

    min_trades: int = 15
    min_win_rate: float = 40.0
    min_profit_factor: float = 1.2
    max_drawdown_pct: float = 30.0
    min_days_in_paper: int = 2
    contract_size: float = 100.0
    lots: float = 0.05
    min_pnl: float = 0.0


class PaperTradingGate:
    """Paper trading simulator with promotion gating.

    Tracks every simulated trade, builds a performance record, and
    determines when a strategy can advance to live_reduced.
    """

    def __init__(self, config: PaperGateConfig | None = None):
        self._config = config or PaperGateConfig()
        self._active: dict[str, dict[str, Any]] = {}
        self._load_state()

    # -- paths -----------------------------------------------------------

    def _state_path(self) -> Path:
        return config_manager.get_logs_dir() / "paper_trading_gate.json"

    def _ledger_path(self) -> Path:
        return config_manager.get_logs_dir() / "paper_trading_ledger.ndjson"

    # -- state management -------------------------------------------------

    def _load_state(self) -> None:
        path = self._state_path()
        if not path.exists():
            return
        try:
            self._active = json.loads(path.read_text(encoding="utf-8"))
        except Exception:
            pass

    def _save_state(self) -> None:
        path = self._state_path()
        path.parent.mkdir(parents=True, exist_ok=True)
        path.write_text(
            json.dumps(self._active, indent=2, default=str), encoding="utf-8"
        )

    def _append_ledger(self, entry: dict[str, Any]) -> None:
        path = self._ledger_path()
        path.parent.mkdir(parents=True, exist_ok=True)
        with open(path, "a", encoding="utf-8") as f:
            f.write(json.dumps(entry, ensure_ascii=False) + "\n")

    # -- lifecycle --------------------------------------------------------

    def start_paper(self, strategy_id: str) -> dict[str, Any]:
        """Begin paper trading for a strategy."""
        sid = str(strategy_id)
        self._active[sid] = {
            "strategy_id": sid,
            "trust_level": "paper",
            "started_at": datetime.now(timezone.utc).isoformat(),
            "status": "running",
            "trades": [],
            "total_trades": 0,
            "wins": 0,
            "losses": 0,
            "total_pnl": 0.0,
            "equity": [self._config.contract_size * self._config.lots * 100],
            "max_drawdown_pct": 0.0,
            "can_promote": False,
            "promotion_blockers": [],
        }
        self._save_state()
        return self._active[sid]

    def stop_paper(self, strategy_id: str) -> dict[str, Any]:
        sid = str(strategy_id)
        if sid not in self._active:
            raise KeyError(f"No paper session: {sid}")
        self._active[sid]["status"] = "stopped"
        self._save_state()
        return self._active[sid]

    # -- bar feeding ------------------------------------------------------

    def feed_signal_result(
        self,
        strategy_id: str,
        action: str,
        side: str,
        entry: float,
        exit_price: float,
        pnl: float,
        *,
        entry_bar: int = 0,
        exit_bar: int = 0,
        confidence: float = 0.0,
    ) -> dict[str, Any]:
        """Record a completed paper trade and re-evaluate promotion."""
        sid = str(strategy_id)
        if sid not in self._active:
            self._active[sid] = self.start_paper(sid)

        session = self._active[sid]
        outcome = "WIN" if pnl > 0 else "LOSS"

        trade = {
            "strategy_id": sid,
            "mode": "paper",
            "action": action,
            "side": side,
            "entry": entry,
            "exit": exit_price,
            "pnl": round(pnl, 2),
            "outcome": outcome,
            "entry_bar": entry_bar,
            "exit_bar": exit_bar,
            "confidence": confidence,
            "timestamp": datetime.now(timezone.utc).isoformat(),
        }

        session["trades"].append(trade)
        session["total_trades"] += 1
        if outcome == "WIN":
            session["wins"] += 1
        else:
            session["losses"] += 1
        session["total_pnl"] = round(session["total_pnl"] + pnl, 2)
        session["equity"].append(session["equity"][-1] + pnl)

        # Recalculate drawdown
        eq = session["equity"]
        peak = max(eq)
        if peak > 0:
            trough = min(eq[eq.index(peak):])
            session["max_drawdown_pct"] = round((peak - trough) / peak * 100, 2)

        self._append_ledger(trade)
        self._check_promotion(sid)
        self._save_state()
        return trade

    # -- promotion logic --------------------------------------------------

    def _check_promotion(self, strategy_id: str) -> None:
        session = self._active[strategy_id]
        blockers: list[str] = []

        trades = session["total_trades"]
        if trades < self._config.min_trades:
            blockers.append(
                f"insufficient trades ({trades}/{self._config.min_trades})"
            )

        wr = (session["wins"] / trades * 100) if trades > 0 else 0.0
        if wr < self._config.min_win_rate:
            blockers.append(
                f"win_rate too low ({wr:.1f}%/{self._config.min_win_rate}%)"
            )

        gross_profit = sum(
            t["pnl"] for t in session["trades"] if t["outcome"] == "WIN"
        )
        gross_loss = abs(
            sum(t["pnl"] for t in session["trades"] if t["outcome"] == "LOSS")
        )
        pf = gross_profit / gross_loss if gross_loss > 0 else 0.0
        if pf < self._config.min_profit_factor:
            blockers.append(
                f"profit_factor too low ({pf:.2f}/{self._config.min_profit_factor})"
            )

        if session["max_drawdown_pct"] > self._config.max_drawdown_pct:
            blockers.append(
                f"max_drawdown too high ({session['max_drawdown_pct']:.1f}%/{self._config.max_drawdown_pct}%)"
            )

        if self._config.min_pnl > 0 and session["total_pnl"] < self._config.min_pnl:
            blockers.append(f"min_pnl not met ({session['total_pnl']:.0f}/{self._config.min_pnl:.0f})")

        session["promotion_blockers"] = blockers
        session["can_promote"] = len(blockers) == 0

    def promote_to_live_reduced(self, strategy_id: str) -> dict[str, Any]:
        """Promote a paper strategy to live_reduced trust level."""
        sid = str(strategy_id)
        self._check_promotion(sid)
        if not self._active[sid]["can_promote"]:
            blockers = self._active[sid]["promotion_blockers"]
            raise ValueError(
                f"Cannot promote {sid}: {', '.join(blockers)}"
            )

        self._active[sid]["trust_level"] = "live_reduced"
        self._active[sid]["promoted_at"] = datetime.now(timezone.utc).isoformat()

        # Update manifest
        self._update_manifest_trust(sid, "live_reduced")
        self._save_state()
        return self._active[sid]

    # -- query ------------------------------------------------------------

    def status(self, strategy_id: str) -> dict[str, Any] | None:
        return self._active.get(str(strategy_id))

    def list_active(self) -> list[str]:
        return [
            sid
            for sid, s in self._active.items()
            if s.get("status") == "running"
        ]

    def list_promotable(self) -> list[str]:
        return [
            sid
            for sid, s in self._active.items()
            if s.get("can_promote", False)
        ]

    def _update_manifest_trust(self, strategy_id: str, level: str) -> None:
        manifest_path = (
            config_manager.get_project_dir()
            / "strategies"
            / f"{strategy_id}.manifest.json"
        )
        if not manifest_path.exists():
            return
        try:
            manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
            manifest["trust_level"] = level
            manifest_path.write_text(
                json.dumps(manifest, indent=2, ensure_ascii=False),
                encoding="utf-8",
            )
        except Exception:
            pass

    def reset(self, strategy_id: str) -> None:
        sid = str(strategy_id)
        self._active.pop(sid, None)
        self._save_state()
