"""
HYPERION — regime detector + strategy primitives.

Common interface:  primitive(candles, ctx) -> signal dict | None
  signal = {side: 'long'|'short', strength: 0..1, reason: str}
Regime gates which primitives may fire:
  trending -> momentum_breakout, trend_pullback
  ranging  -> zscore_fade, meanrev_rsi
  volatile -> (nothing by default; volatility is where accounts die)
"""
from __future__ import annotations
from indicators import adx, atr, zscore, rsi, ema_series, realized_vol, donchian

REGIME_TRENDING = "trending"
REGIME_RANGING = "ranging"
REGIME_VOLATILE = "volatile"

VOL_SPIKE_MULT = 1.8       # current vol vs its own median => volatile
ADX_TREND = 25             # above => trending
ADX_RANGE = 20             # below => ranging


def classify_regime(candles: list[dict]) -> str:
    closes = [c["c"] for c in candles]
    a = adx(candles, 14)
    rv_now = realized_vol(closes, 14)
    # baseline from a window that ends BEFORE the recent period, so a fresh
    # spike can't inflate its own reference
    rv_base = realized_vol(closes[:-30], 60) if len(closes) > 100 else None
    if rv_now is not None and rv_base and rv_now > rv_base * VOL_SPIKE_MULT:
        return REGIME_VOLATILE
    if a is None:
        return REGIME_RANGING
    if a >= ADX_TREND:
        return REGIME_TRENDING
    if a <= ADX_RANGE:
        return REGIME_RANGING
    return REGIME_RANGING  # dead zone treated as ranging (conservative)


# ---------------- primitives ----------------

def zscore_fade(candles: list[dict], ctx: dict) -> dict | None:
    closes = [c["c"] for c in candles]
    z = zscore(closes, 20)
    if z is None:
        return None
    if z <= -2.0:
        return {"side": "long", "strength": min(abs(z) / 3.0, 1.0),
                "reason": f"zscore {z:.2f} <= -2"}
    if z >= 2.0:
        return {"side": "short", "strength": min(abs(z) / 3.0, 1.0),
                "reason": f"zscore {z:.2f} >= 2"}
    return None


def meanrev_rsi(candles: list[dict], ctx: dict) -> dict | None:
    closes = [c["c"] for c in candles]
    r = rsi(closes, 14)
    if r is None:
        return None
    if r <= 28:
        return {"side": "long", "strength": (30 - r) / 30 + 0.3,
                "reason": f"rsi {r:.1f} oversold"}
    if r >= 72:
        return {"side": "short", "strength": (r - 70) / 30 + 0.3,
                "reason": f"rsi {r:.1f} overbought"}
    return None


def momentum_breakout(candles: list[dict], ctx: dict) -> dict | None:
    d = donchian(candles, 20)
    if d is None:
        return None
    hi, lo = d
    px = candles[-1]["c"]
    a = atr(candles, 14) or 0
    if a and px > hi:
        return {"side": "long", "strength": min((px - hi) / a, 1.0),
                "reason": f"breakout above 20-bar high {hi:.4g}"}
    if a and px < lo:
        return {"side": "short", "strength": min((lo - px) / a, 1.0),
                "reason": f"breakdown below 20-bar low {lo:.4g}"}
    return None


def trend_pullback(candles: list[dict], ctx: dict) -> dict | None:
    closes = [c["c"] for c in candles]
    if len(closes) < 60:
        return None
    e20 = ema_series(closes, 20)
    e50 = ema_series(closes, 50)
    px = closes[-1]
    r = rsi(closes, 14)
    if r is None:
        return None
    uptrend = e20[-1] > e50[-1] and e20[-1] > e20[-6]
    downtrend = e20[-1] < e50[-1] and e20[-1] < e20[-6]
    near = abs(px - e20[-1]) / px < 0.004
    if uptrend and near and 38 <= r <= 55:
        return {"side": "long", "strength": 0.6 + (55 - r) / 60,
                "reason": f"pullback to ema20 in uptrend, rsi {r:.1f}"}
    if downtrend and near and 45 <= r <= 62:
        return {"side": "short", "strength": 0.6 + (r - 45) / 60,
                "reason": f"pullback to ema20 in downtrend, rsi {r:.1f}"}
    return None


PRIMITIVES = {
    "zscore_fade": zscore_fade,
    "meanrev_rsi": meanrev_rsi,
    "momentum_breakout": momentum_breakout,
    "trend_pullback": trend_pullback,
}

REGIME_GATES = {
    REGIME_TRENDING: ["momentum_breakout", "trend_pullback"],
    REGIME_RANGING: ["zscore_fade", "meanrev_rsi"],
    REGIME_VOLATILE: [],
}


def best_signal(candles: list[dict], ctx: dict) -> tuple[str, dict, str] | None:
    """Returns (strategy_name, signal, regime) for the strongest allowed signal."""
    regime = classify_regime(candles)
    best = None
    for name in REGIME_GATES.get(regime, []):
        sig = PRIMITIVES[name](candles, ctx)
        if sig and (best is None or sig["strength"] > best[1]["strength"]):
            best = (name, sig, regime)
    return best
