"""Pure risk gates. `pretrade_block` returns a blocking reason (or None) for the hard gates; `gross_scaler`
returns the [0,1+] multiplier applied to target gross = clamp(edge_theta) × min(kelly_fraction, kelly_cap),
bounded by gross_cap. Edge decays -> gross shrinks; edge dies (theta->0) -> flat."""
from __future__ import annotations

import math
from dataclasses import dataclass


@dataclass(frozen=True, slots=True)
class GateConfig:
    allow_live: bool
    kill_switch: bool
    testnet: bool
    gross_cap: float
    session_dd: float        # negative threshold, e.g. -0.10
    kelly_cap: float         # maximum Kelly fraction, e.g. 0.25


def pretrade_block(cfg: GateConfig, *, session_pnl_pct: float) -> str | None:
    if cfg.kill_switch:
        return "kill_switch"
    if not cfg.testnet and not cfg.allow_live:
        return "allow_live"
    # M3: misconfigured session_dd (non-finite or non-negative) must block, not pass.
    # session_dd must be a negative threshold; anything else is a configuration error.
    if not math.isfinite(cfg.session_dd) or cfg.session_dd >= 0:
        return "bad_session_dd_config"
    # C2: NaN session_pnl_pct is unknown state; fail safe -> block.
    if not math.isfinite(session_pnl_pct):
        return "session_pnl_unknown"
    if session_pnl_pct <= cfg.session_dd:
        return "session_drawdown"
    return None


def gross_scaler(*, edge_theta: float, kelly_fraction: float, kelly_cap: float, gross_cap: float) -> float:
    # Real money: any non-finite input -> 0 gross (flat). NaN is the natural sentinel from EMAs before
    # warmup / Kelly with avg_win=0 / divide-by-zero; min()/max() silently pass the non-NaN arg, so we
    # must guard explicitly or the "edge dies -> flat" contract inverts to max exposure.
    if not all(math.isfinite(x) for x in (edge_theta, kelly_fraction, kelly_cap, gross_cap)):
        return 0.0
    theta = max(0.0, min(1.0, edge_theta))
    kelly = max(0.0, min(kelly_cap, kelly_fraction))
    return max(0.0, min(gross_cap, theta * kelly))
