"""Exit-plan validation for LLM trade proposals.

The LLM chooses direction and levels. This module checks whether those levels
survive current market noise and whether the claimed playbook contradicts the
latest price response.
"""
from __future__ import annotations

from dataclasses import dataclass
from typing import Any

from .models import Candle, OrderflowSnapshot


@dataclass(frozen=True)
class ExitPlanGateResult:
    allowed: bool
    reasons: list[str]
    metrics: dict[str, Any]

    @property
    def reason(self) -> str:
        return "; ".join(self.reasons)


def build_noise_state(
    *,
    of: OrderflowSnapshot,
    candles_1m: list[Candle],
    candles_5m: list[Candle],
    stop_atr_multiplier: float = 1.2,
) -> dict[str, Any]:
    """Summarize live noise so the LLM and deterministic gate use the same facts."""
    ranges_1m = [c.range for c in candles_1m[-40:] if c.range > 0]
    ranges_5m = [c.range for c in candles_5m[-40:] if c.range > 0]
    last_1m = candles_1m[-1] if candles_1m else None
    last_5m = candles_5m[-1] if candles_5m else None
    spread_px = of.mid * of.spread_bps / 10000 if of.mid > 0 else 0.0

    range_p80_1m = _percentile(ranges_1m, 0.80)
    range_p80_5m = _percentile(ranges_5m, 0.80)
    dynamic_stop_floor = max(
        of.atr_14 * stop_atr_multiplier if of.atr_14 > 0 else 0.0,
        range_p80_1m,
        range_p80_5m * 0.75,
        spread_px * 3.0,
    )

    return {
        "atr14": _round(of.atr_14),
        "spread_px": _round(spread_px),
        "range_p80_1m": _round(range_p80_1m),
        "range_p80_5m": _round(range_p80_5m),
        "last_range_1m": _round(last_1m.range if last_1m else 0.0),
        "last_range_5m": _round(last_5m.range if last_5m else 0.0),
        "dynamic_stop_floor": _round(dynamic_stop_floor),
        "stop_atr_multiplier": _round(stop_atr_multiplier),
        "last_1m": _candle_summary(last_1m),
        "last_5m": _candle_summary(last_5m),
    }


def evaluate_exit_plan(
    decision: Any,
    coin_data: dict[str, Any],
    *,
    min_rr: float = 1.5,
    stop_atr_multiplier: float = 1.2,
    min_stop_noise_multiple: float = 1.10,
    min_target_pct: float = 0.0,
    min_target_atr_multiple: float = 0.0,
) -> ExitPlanGateResult:
    """Reject LLM entries with invalid SL/TP structure or failed-push logic."""
    if getattr(decision, "action", "") != "enter":
        return ExitPlanGateResult(True, [], {})

    of: OrderflowSnapshot = coin_data["of"]
    side = str(getattr(decision, "side", "") or "").lower()
    entry = float(getattr(decision, "entry_price", 0.0) or 0.0)
    stop = float(getattr(decision, "stop_price", 0.0) or 0.0)
    target = float(getattr(decision, "target_price", 0.0) or 0.0)
    candles_1m = coin_data.get("candles_1m") or []
    candles_5m = coin_data.get("candles_5m") or []
    trigger_state = coin_data.get("trigger_state") or {}
    noise = coin_data.get("noise_state") or build_noise_state(
        of=of,
        candles_1m=candles_1m,
        candles_5m=candles_5m,
        stop_atr_multiplier=stop_atr_multiplier,
    )

    reasons: list[str] = []
    if side not in ("long", "short"):
        return ExitPlanGateResult(False, ["invalid side"], {"noise_state": noise})
    if entry <= 0 or stop <= 0 or target <= 0:
        return ExitPlanGateResult(False, ["invalid entry/stop/target price"], {"noise_state": noise})

    if side == "long":
        if not stop < entry < target:
            reasons.append("long levels invalid: expected stop < entry < target")
    else:
        if not target < entry < stop:
            reasons.append("short levels invalid: expected target < entry < stop")

    stop_distance = abs(entry - stop)
    reward_distance = abs(target - entry)
    rr = reward_distance / stop_distance if stop_distance > 0 else 0.0
    dynamic_floor = float(noise.get("dynamic_stop_floor") or 0.0)
    atr14 = float(noise.get("atr14") or getattr(of, "atr_14", 0.0) or 0.0)
    rr_after_noise_stop = reward_distance / dynamic_floor if dynamic_floor > 0 else rr

    required_stop_distance = dynamic_floor * min_stop_noise_multiple if dynamic_floor > 0 else 0.0
    required_target_distance = max(
        entry * float(min_target_pct or 0.0),
        atr14 * float(min_target_atr_multiple or 0.0),
    )
    if required_stop_distance > 0 and stop_distance < required_stop_distance:
        reasons.append(
            "stop too close to current noise: "
            f"stop={stop_distance:.8g}, required_stop={required_stop_distance:.8g}, "
            f"noise_floor={dynamic_floor:.8g}, multiple={min_stop_noise_multiple:.2f}"
        )
    if rr_after_noise_stop < min_rr:
        reasons.append(
            f"target too small after noise-valid stop: rr_after_noise={rr_after_noise_stop:.2f} < {min_rr:.2f}"
        )
    if required_target_distance > 0 and reward_distance < required_target_distance:
        reasons.append(
            "target too small for larger-trade mode: "
            f"reward={reward_distance:.8g}, required_target={required_target_distance:.8g}, "
            f"min_pct={float(min_target_pct or 0.0):.4%}, atr_multiple={float(min_target_atr_multiple or 0.0):.2f}"
        )

    last_1m = candles_1m[-1] if candles_1m else None
    last_5m = candles_5m[-1] if candles_5m else None
    if side == "short":
        if trigger_state.get("breakout_acceptance_long") and not _has_short_reversal(trigger_state):
            reasons.append("short blocked: latest trigger_state shows long breakout acceptance, not failed-push rejection")
        if _bullish_expansion(last_1m, float(noise.get("range_p80_1m") or 0.0)) and _bullish_expansion(
            last_5m, float(noise.get("range_p80_5m") or 0.0)
        ):
            reasons.append("short blocked: latest 1m and 5m candles are bullish expansion, no completed rejection")
    else:
        if trigger_state.get("breakdown_acceptance_short") and not _has_long_reversal(trigger_state):
            reasons.append("long blocked: latest trigger_state shows short breakdown acceptance, not sweep-reclaim")
        if _bearish_expansion(last_1m, float(noise.get("range_p80_1m") or 0.0)) and _bearish_expansion(
            last_5m, float(noise.get("range_p80_5m") or 0.0)
        ):
            reasons.append("long blocked: latest 1m and 5m candles are bearish expansion, no completed reclaim")

    metrics = {
        "entry": _round(entry),
        "stop": _round(stop),
        "target": _round(target),
        "stop_distance": _round(stop_distance),
        "reward_distance": _round(reward_distance),
        "rr": round(rr, 3),
        "rr_after_noise_stop": round(rr_after_noise_stop, 3),
        "required_stop_distance": _round(required_stop_distance),
        "required_target_distance": _round(required_target_distance),
        "min_stop_noise_multiple": _round(min_stop_noise_multiple),
        "min_target_pct": _round(min_target_pct),
        "min_target_atr_multiple": _round(min_target_atr_multiple),
        "noise_state": noise,
        "trigger_state": trigger_state,
    }
    return ExitPlanGateResult(not reasons, reasons, metrics)


def _has_short_reversal(trigger_state: dict[str, Any]) -> bool:
    return any(
        bool(trigger_state.get(key))
        for key in (
            "sweep_reclaim_short",
            "absorption_reversal_short",
            "cvd_divergence_short",
            "crowding_fade_short",
            "pullback_retest_short",
        )
    )


def _has_long_reversal(trigger_state: dict[str, Any]) -> bool:
    return any(
        bool(trigger_state.get(key))
        for key in (
            "sweep_reclaim_long",
            "absorption_reversal_long",
            "cvd_divergence_long",
            "crowding_fade_long",
            "pullback_retest_long",
        )
    )


def _bullish_expansion(candle: Candle | None, range_floor: float) -> bool:
    if candle is None or candle.open <= 0:
        return False
    body = candle.close - candle.open
    return body > 0 and candle.range >= max(range_floor * 0.80, candle.open * 0.0005)


def _bearish_expansion(candle: Candle | None, range_floor: float) -> bool:
    if candle is None or candle.open <= 0:
        return False
    body = candle.open - candle.close
    return body > 0 and candle.range >= max(range_floor * 0.80, candle.open * 0.0005)


def _percentile(values: list[float], q: float) -> float:
    vals = sorted(v for v in values if v > 0)
    if not vals:
        return 0.0
    idx = int(round((len(vals) - 1) * q))
    return vals[max(0, min(idx, len(vals) - 1))]


def _candle_summary(candle: Candle | None) -> dict[str, Any]:
    if candle is None:
        return {}
    return {
        "open": _round(candle.open),
        "high": _round(candle.high),
        "low": _round(candle.low),
        "close": _round(candle.close),
        "range": _round(candle.range),
        "direction": "up" if candle.close > candle.open else "down" if candle.close < candle.open else "flat",
    }


def _round(value: float) -> float:
    return float(f"{float(value):.10g}")
