"""The declarative, versioned strategy-level ALLOCATION policy (sub-project B2a).

Turns the deploy-tier strategies into ex-ante-sized target weights. This is the single source for the FORWARD
strategy-level allocation policy; it does NOT redirect the legacy `book_allocator` function defaults / config
settings, which remain separate and in use by the research/overlay plane — B2a adds a new forward-allocation
consumer alongside them, not a replacement for them. A change = a code commit + an ALLOCATION_POLICY_VERSION
bump; the content-derived allocation_policy_hash self-check forces the bump (same discipline as A's
definition_hash and B1's policy_hash). The capital_ceiling — the HARD human gate on total deployed gross —
lives in this versioned policy, so a ceiling change is audited. NO reactive de-sizing: the sizing derives from
full-history vol, and the only drawdown-reactive element is the rare aggregate killswitch."""
from __future__ import annotations

import hashlib
import json
from dataclasses import asdict, dataclass


@dataclass(frozen=True, slots=True)
class AllocationPolicy:
    target_vol: float = 0.12  # ex-ante annualised vol target for the book
    kelly_fraction: float = 0.5  # fractional-Kelly scalar
    max_leverage: float = 1.5  # hard cap on the ex-ante leverage K
    max_strategy_weight: float = 0.5  # per-strategy weight cap (before leverage)
    max_venue_weight: float = 1.0  # cap on the fraction of equity deployed as gross to any single venue (1.0 = no cap)
    marginal_gate: bool = True  # leave-one-out equal-weight marginal-Sharpe quality filter
    min_history_days: int = 60  # a strategy needs >= this many RETURN OBSERVATIONS (not calendar days) for a
    # stable ex-ante vol — e.g. 60 obs on a weekday-only ETF series is ~84 calendar days
    kill_dd: float = 0.25  # RARE aggregate killswitch: flatten beyond this drawdown (latching)
    reenter_dd: float = 0.10  # re-enter once the drawdown recovers inside this
    cvar_alpha: float = 0.10  # tail level for the CVaR leverage floor (mean of the worst alpha of book days)
    capital_ceiling: float = 35_000.0  # HARD fund-level dollar cap on total deployed gross (the human gate)
    capacity_min_sharpe: float = 1.0  # capacity ceiling = the largest AUM whose honest Sharpe stays >= this
    equity_default_ceiling: float = 10_000_000.0  # v1 default capacity for non-crypto (IBKR equity) edges; a
                                                   # proper equity-ADV model is a follow-on


# Gaussian expected-shortfall multiplier ES_alpha/sigma = phi(Phi^-1(alpha))/alpha. Calibrates target_cvar
# to target_vol so the CVaR floor is a NO-OP for a Gaussian book and only bites fatter-than-normal tails.
_GAUSSIAN_ES: dict[float, float] = {0.10: 1.7550}

ALLOCATION_POLICY = AllocationPolicy()
ALLOCATION_POLICY_VERSION = 4


def resolve_allocation_policy(settings: object) -> AllocationPolicy:
    """The live policy with the mode-selected ceiling injected (paper unless execution.allow_live). The
    resolved ceiling folds into allocation_policy_hash, so a ceiling change / paper->live flip stays audited."""
    ceiling = (settings.allocation.capital_ceiling_live if settings.execution.allow_live
               else settings.allocation.capital_ceiling_paper)
    return AllocationPolicy(capital_ceiling=ceiling)


def allocation_policy_hash(policy: AllocationPolicy, version: int) -> str:
    canonical = json.dumps(asdict(policy), sort_keys=True, separators=(",", ":"))
    return hashlib.sha256(f"{canonical}|v{version}".encode()).hexdigest()
