"""Beginner guardrails, trade reviews, and in-dashboard performance summaries.

Everything here is pure calculation over data already stored in ``signals.db``
and the dashboard payload. No network access and no trading capability.
"""

from __future__ import annotations

from datetime import datetime, timedelta, timezone
from decimal import Decimal, InvalidOperation
from typing import Any


def _decimal(value: object) -> Decimal:
    try:
        number = Decimal(str(value))
    except (InvalidOperation, TypeError, ValueError):
        return Decimal("0")
    return number if number.is_finite() else Decimal("0")


def _text(value: Decimal, places: int = 4) -> str:
    return format(value, f".{places}f").rstrip("0").rstrip(".") or "0"


# --- Daily loss guardrail -------------------------------------------------


def daily_loss_state(
    closed_trades: list[dict[str, Any]],
    open_trades: list[dict[str, Any]],
    *,
    limit_percent: Decimal = Decimal("2"),
    notional_usdt: Decimal = Decimal("100"),
    today: datetime | None = None,
) -> dict[str, Any]:
    """Measure today's paper-trading result against the daily loss limit.

    The base is one notional per trade touched today (closed today or still
    open), so the limit scales with actual engagement instead of the balance.
    """
    current = (today or datetime.now(timezone.utc)).astimezone(timezone.utc)
    day_prefix = current.date().isoformat()
    closed_today = [
        trade
        for trade in closed_trades
        if str(trade.get("closed_at", "")).startswith(day_prefix)
    ]
    realized = sum((_decimal(trade.get("net_pnl_usdt")) for trade in closed_today), Decimal("0"))
    floating = Decimal("0")
    for trade in open_trades:
        entry = _decimal(trade.get("effective_entry_price"))
        last = _decimal(trade.get("last_price"))
        quantity = _decimal(trade.get("quantity"))
        if entry > 0 and last > 0 and quantity > 0:
            floating += (last - entry) * quantity
    touched = len(closed_today) + len(open_trades)
    base = max(Decimal("1"), notional_usdt) * max(1, touched)
    result = realized + floating
    loss_percent = (-result / base * Decimal("100")) if base > 0 and result < 0 else Decimal("0")
    halted = touched > 0 and loss_percent >= limit_percent
    return {
        "date": day_prefix,
        "limit_percent": _text(limit_percent, 2),
        "closed_today": len(closed_today),
        "open_count": len(open_trades),
        "realized_pnl_usdt": _text(realized, 4),
        "floating_pnl_usdt": _text(floating, 4),
        "loss_percent": _text(loss_percent, 2),
        "halted": halted,
    }


# --- Paper-trading progression --------------------------------------------


def progression_state(
    closed_count: int,
    *,
    required_trades: int = 20,
) -> dict[str, Any]:
    required = max(1, int(required_trades))
    done = max(0, int(closed_count))
    percent = min(100, int((Decimal(done) / Decimal(required) * Decimal("100"))))
    return {
        "required_trades": required,
        "completed_trades": done,
        "remaining_trades": max(0, required - done),
        "percent": percent,
        "ready_for_live": done >= required,
    }


# --- Post-trade review -----------------------------------------------------

_LESSON_KEYS = {
    "stop_protected": "stop_protected",
    "stop_too_tight": "stop_too_tight",
    "invalidated_early_good": "invalidated_early_good",
    "invalidated_failed_fast": "invalidated_failed_fast",
    "target_discipline": "target_discipline",
    "review_emotions": "review_emotions",
}


def review_closed_trade(trade: dict[str, Any]) -> dict[str, Any]:
    """Build an honest learning card from data recorded while the trade was open.

    MFE/MAE describe movement *during* the trade only; lessons never claim to
    know what happened after the exit.
    """
    reason = str(trade.get("exit_reason") or "")
    pnl = _decimal(trade.get("net_pnl_usdt"))
    mfe = _decimal(trade.get("max_favorable_excursion_percent"))
    mae = _decimal(trade.get("max_adverse_excursion_percent"))
    lessons: list[str] = []
    if reason == "STOP_LOSS":
        if mfe >= Decimal("0.5"):
            lessons.append(_LESSON_KEYS["stop_too_tight"])
        else:
            lessons.append(_LESSON_KEYS["stop_protected"])
    elif reason == "STOP_AFTER_TARGET_1":
        lessons.append(_LESSON_KEYS["target_discipline"])
    elif reason in {"SIGNAL_INVALIDATED", "SIGNAL_EXPIRED"}:
        if pnl >= 0:
            lessons.append(_LESSON_KEYS["invalidated_early_good"])
        elif mfe >= Decimal("1"):
            lessons.append(_LESSON_KEYS["invalidated_early_good"])
        else:
            lessons.append(_LESSON_KEYS["invalidated_failed_fast"])
    elif reason == "TARGET_2":
        lessons.append(_LESSON_KEYS["target_discipline"])
    if pnl < 0:
        lessons.append(_LESSON_KEYS["review_emotions"])
    return {
        "exit_reason": reason,
        "net_pnl_usdt": str(trade.get("net_pnl_usdt") or "0"),
        "return_percent": str(trade.get("return_percent") or "0"),
        "mfe_percent": str(trade.get("max_favorable_excursion_percent") or "0"),
        "mae_percent": str(trade.get("max_adverse_excursion_percent") or "0"),
        "lessons": lessons,
    }


# --- Performance summary ---------------------------------------------------


def performance_summary(
    closed_trades: list[dict[str, Any]],
    signals: list[dict[str, Any]],
    *,
    now: datetime | None = None,
) -> dict[str, Any]:
    current = (now or datetime.now(timezone.utc)).astimezone(timezone.utc)
    week_ago = current - timedelta(days=7)

    def _dt(value: object) -> datetime | None:
        try:
            return datetime.fromisoformat(str(value)).astimezone(timezone.utc)
        except (ValueError, TypeError):
            return None

    wins = [trade for trade in closed_trades if _decimal(trade.get("net_pnl_usdt")) > 0]
    losses = [trade for trade in closed_trades if _decimal(trade.get("net_pnl_usdt")) <= 0]
    gross_win = sum((_decimal(t.get("net_pnl_usdt")) for t in wins), Decimal("0"))
    gross_loss = sum((_decimal(t.get("net_pnl_usdt")) for t in losses), Decimal("0"))
    total = gross_win + gross_loss
    closed_count = len(closed_trades)
    week_trades = [t for t in closed_trades if (_dt(t.get("closed_at")) or current) >= week_ago]
    week_pnl = sum((_decimal(t.get("net_pnl_usdt")) for t in week_trades), Decimal("0"))

    by_symbol: dict[str, dict[str, Any]] = {}
    for trade in closed_trades:
        symbol = str(trade.get("symbol", ""))
        bucket = by_symbol.setdefault(symbol, {"trades": 0, "net_pnl_usdt": Decimal("0")})
        bucket["trades"] += 1
        bucket["net_pnl_usdt"] += _decimal(trade.get("net_pnl_usdt"))
    symbol_rows = [
        {
            "symbol": symbol,
            "trades": bucket["trades"],
            "net_pnl_usdt": _text(bucket["net_pnl_usdt"], 4),
        }
        for symbol, bucket in sorted(
            by_symbol.items(), key=lambda item: item[1]["net_pnl_usdt"], reverse=True
        )
    ]

    invalidations: dict[str, int] = {}
    for signal in signals:
        reason = str(signal.get("invalidation_reason") or "")
        if reason.startswith("FAILED_GATES:"):
            for gate in reason.removeprefix("FAILED_GATES:").split(","):
                invalidations[gate] = invalidations.get(gate, 0) + 1
    top_invalidation_reasons = [
        {"gate": gate, "count": count}
        for gate, count in sorted(invalidations.items(), key=lambda item: item[1], reverse=True)[:5]
    ]

    equity = Decimal("0")
    max_drawdown = Decimal("0")
    peak = Decimal("0")
    running_max = Decimal("0")
    equity_curve: list[dict[str, str]] = []
    ordered = sorted(
        closed_trades,
        key=lambda t: str(t.get("closed_at") or ""),
    )
    for trade in ordered:
        equity += _decimal(trade.get("net_pnl_usdt"))
        peak = max(peak, equity)
        max_drawdown = min(max_drawdown, equity - peak)
        if equity > running_max or len(equity_curve) == 0:
            running_max = equity
            equity_curve.append(
                {"closed_at": str(trade.get("closed_at") or "")[:10], "equity_usdt": _text(equity, 4)}
            )

    average_win = gross_win / Decimal(len(wins)) if wins else Decimal("0")
    average_loss = gross_loss / Decimal(len(losses)) if losses else Decimal("0")
    profit_factor = (
        (gross_win / abs(gross_loss)) if gross_loss < 0 else Decimal("0")
    )
    return {
        "generated_at": current.isoformat(),
        "closed_count": closed_count,
        "wins": len(wins),
        "losses": len(losses),
        "win_rate_percent": _text(
            (Decimal(len(wins)) / Decimal(closed_count) * Decimal("100")) if closed_count else Decimal("0"), 2
        ),
        "total_net_pnl_usdt": _text(total, 4),
        "average_win_usdt": _text(average_win, 4),
        "average_loss_usdt": _text(average_loss, 4),
        "profit_factor": _text(profit_factor, 2),
        "max_drawdown_usdt": _text(max_drawdown, 4),
        "week_closed_count": len(week_trades),
        "week_net_pnl_usdt": _text(week_pnl, 4),
        "by_symbol": symbol_rows,
        "signal_count": len(signals),
        "invalidated_count": sum(
            1 for s in signals if str(s.get("status") or "") == "INVALIDATED"
        ),
        "top_invalidation_reasons": top_invalidation_reasons,
        "equity_curve": equity_curve[-30:],
    }
