"""
exit_giveback_room.py

[2026-09-21] Wired into monitor.py's live loop -- checked BEFORE exit.py's
three layers, since this rule is meant to react faster and tighter than
trend/imbalance/hard-stop can.

Motivated by the 2026-09-21 peak-vs-exit analysis: 17 of 24 trades that
day gave back a real, meaningful chunk of a genuine peak run-up before
any of exit.py's existing layers fired -- none of VWAP/trend/imbalance
reference the position's OWN high since entry, only absolute/session-wide
readings (AGEN +5.39% peak run-up, BNC +2.55%, CRML +2.39%, etc.).

Rule, confirmed 2026-09-22:
  1. Track peak_price = max close since entry (same pattern as
     exit_giveback_experimental.py / exit_step_lock_experimental.py).
  2. Inactive until price actually starts sloping down -- reuses the
     same normalized_slope_pct + classify_slope ATR-scaled dead-zone
     exit.py's own trend layer uses, so "sloping down" means the same
     thing everywhere in this codebase. No confirm-read wait on this
     gate -- the moment the slope classifies "negative," this layer
     starts evaluating every poll (speed is the whole point here, same
     reasoning as exit_fast_slope_experimental.py).
  3. Also inactive until peak_gain_cents (peak_price - entry_price, in
     cents) has cleared min_peak_gain_cents (15 cents by default,
     config-adjustable) -- below that floor the position hasn't run up
     enough for a giveback rule to mean anything yet (ordinary
     spread/noise).
  4. Once both gates are open, the allowed giveback room in cents is
     peak_gain_cents * giveback_ratio (1/4 default since 2026-09-22,
     was 2/3; config-adjustable) -- e.g. 20c peak gain -> 5c room,
     40c -> 10c, scaling linearly with every new peak. Exits the instant
     price drops through peak_price - room, no separate confirmation
     wait -- same no-confirm-needed shape as exit.py's Layer 1 hard
     stop, deliberately, since this is meant to fire BEFORE exit.py's
     slower layers ever get a chance to.

Sits ALONGSIDE exit.py, not replacing it -- monitor.py checks this first
each poll and only falls through to exit.py's three layers when this one
holds.
"""

from dataclasses import dataclass, field

from config_loader import get_config
from indicators import normalized_slope_pct, classify_slope, atr as calc_atr

STATE_HOLD = "HOLD"
STATE_EXIT = "EXIT"

DEFAULT_CONFIG = {
    "min_peak_gain_cents": 15.0,     # peak must have run up at least this much before this layer applies
    "giveback_ratio": 0.25,          # allowed giveback = peak_gain_cents * this ratio (was 2/3 until 2026-09-22)
    "slope_lookback_bars": 4,        # same structure/trend lookback shape as exit.py's other layers
    "min_flat_threshold_pct": 0.05,
    "flat_threshold_atr_fraction": 0.25,
    "atr_period": 14,
    # [2026-09-23] Exit-isolation options (defaults keep the live behavior):
    "breakeven_lock_cents": 0.0,     # >0: once peak gain reaches this, exit if price falls to entry + buffer
    "breakeven_buffer_cents": 1.0,
    "require_slope": True,           # False: the giveback floor fires without waiting for a negative slope
}


@dataclass
class GivebackRoomDecision:
    symbol: str
    state: str = STATE_HOLD
    should_exit: bool = False
    reason: str = ""
    metrics: dict = field(default_factory=dict)
    state_out: dict = field(default_factory=dict)


def _merge_cfg(cfg):
    return {**DEFAULT_CONFIG, **(cfg or get_config().get("giveback_room", {}))}


def evaluate(symbol: str, bars: list, entry_price: float, state_in: dict, cfg: dict = None) -> GivebackRoomDecision:
    """
    bars: same short rolling 1-min-bar window exit.py takes.
    state_in: caller-held state fed back every poll -- {"peak_price"}.
        Fresh {} at entry.
    """
    cfg = _merge_cfg(cfg)
    state_in = dict(state_in or {})

    if len(bars) < 3:
        return GivebackRoomDecision(symbol=symbol, reason="insufficient bar data",
                                     state_out=state_in or {"peak_price": entry_price})

    price = bars[-1]["c"]
    prior_peak = state_in.get("peak_price", entry_price)
    peak_price = max(prior_peak, price)

    a = calc_atr(bars, period=min(cfg["atr_period"], max(2, len(bars) - 1)))
    atr_pct = (a / price * 100.0) if price else 0.0
    closes = [b["c"] for b in bars[-cfg["slope_lookback_bars"]:]]
    slope_pct = normalized_slope_pct(closes)
    flat_thresh = max(cfg["min_flat_threshold_pct"], atr_pct * cfg["flat_threshold_atr_fraction"])
    trend = classify_slope(slope_pct, flat_thresh)

    peak_gain_cents = (peak_price - entry_price) * 100.0

    metrics = {"price": price, "peak_price": round(peak_price, 4),
               "peak_gain_cents": round(peak_gain_cents, 2), "slope_pct": round(slope_pct, 4),
               "trend": trend}

    # [2026-09-23] Breakeven lock: a trade that has been up
    # breakeven_lock_cents is never allowed to turn into a loss.
    if cfg["breakeven_lock_cents"] and peak_gain_cents >= cfg["breakeven_lock_cents"]:
        be_floor = entry_price + cfg["breakeven_buffer_cents"] / 100.0
        if price <= be_floor:
            return GivebackRoomDecision(
                symbol=symbol, state=STATE_EXIT, should_exit=True,
                reason=(f"breakeven lock: peak +{peak_gain_cents:.1f}c >= {cfg['breakeven_lock_cents']:.0f}c, "
                        f"price ${price:.4f} back to ${be_floor:.4f}"),
                metrics=metrics, state_out={"peak_price": peak_price})

    if cfg["require_slope"] and trend != "negative":
        return GivebackRoomDecision(symbol=symbol, state=STATE_HOLD,
                                     reason=f"not sloping down yet ({trend}, slope {slope_pct:.3f}%)",
                                     metrics=metrics, state_out={"peak_price": peak_price})

    if peak_gain_cents < cfg["min_peak_gain_cents"]:
        return GivebackRoomDecision(
            symbol=symbol, state=STATE_HOLD,
            reason=(f"sloping down but peak gain {peak_gain_cents:.1f}c below "
                    f"{cfg['min_peak_gain_cents']:.1f}c floor"),
            metrics=metrics, state_out={"peak_price": peak_price})

    room_cents = peak_gain_cents * cfg["giveback_ratio"]
    floor = peak_price - room_cents / 100.0
    metrics["room_cents"] = round(room_cents, 2)
    metrics["floor"] = round(floor, 4)

    if price <= floor:
        return GivebackRoomDecision(
            symbol=symbol, state=STATE_EXIT, should_exit=True,
            reason=(f"giveback room exhausted: price ${price:.4f} <= floor ${floor:.4f} "
                    f"({room_cents:.1f}c room off {peak_gain_cents:.1f}c peak gain, "
                    f"ratio {cfg['giveback_ratio']:.3f})"),
            metrics=metrics, state_out={"peak_price": peak_price})

    return GivebackRoomDecision(
        symbol=symbol, state=STATE_HOLD,
        reason=f"sloping down, within giveback room (floor ${floor:.4f})",
        metrics=metrics, state_out={"peak_price": peak_price})
