"""Quarter-Kelly sizing with caps (``docs/03-algorithms.md §3``).

Pure function: given capital, max-loss-per-contract, DVOL and the
aggregate state, returns the number of contracts to send. Returns
``n_contracts == 0`` together with a human-readable ``reason_if_zero``
when no entry is allowed; the orchestrator never opens a position when
``n_contracts == 0``.
"""

from __future__ import annotations

from decimal import Decimal

from pydantic import BaseModel, ConfigDict

from cerbero_bite.config import StrategyConfig

__all__ = ["SizingContext", "SizingResult", "compute_contracts"]


class SizingContext(BaseModel):
    """Snapshot of the inputs needed to size a single trade."""

    model_config = ConfigDict(frozen=True, extra="forbid")

    capital_usd: Decimal
    max_loss_per_contract_usd: Decimal
    dvol_now: Decimal
    open_engagement_usd: Decimal
    eur_to_usd: Decimal
    other_open_positions: int


class SizingResult(BaseModel):
    """Result of :func:`compute_contracts`. ``reason_if_zero`` is set iff n=0."""

    model_config = ConfigDict(frozen=True, extra="forbid")

    n_contracts: int
    risk_dollars: Decimal
    reason_if_zero: str | None


def _zero(reason: str) -> SizingResult:
    return SizingResult(n_contracts=0, risk_dollars=Decimal("0"), reason_if_zero=reason)


def _dvol_multiplier(dvol_now: Decimal, cfg: StrategyConfig) -> Decimal | None:
    """Return the size multiplier for the current DVOL, or ``None`` if no entry."""
    for band in cfg.sizing.dvol_adjustment:
        if dvol_now < band.dvol_under:
            return band.multiplier
    return None


def compute_contracts(ctx: SizingContext, cfg: StrategyConfig) -> SizingResult:
    """Return the contract count after Kelly, caps, DVOL and engagement checks."""
    if ctx.max_loss_per_contract_usd <= 0:
        return _zero(f"non-positive max_loss_per_contract ({ctx.max_loss_per_contract_usd})")

    risk_target = ctx.capital_usd * cfg.sizing.kelly_fraction
    cap_per_trade_usd = cfg.sizing.cap_per_trade_eur * ctx.eur_to_usd
    risk_target = min(risk_target, cap_per_trade_usd)

    multiplier = _dvol_multiplier(ctx.dvol_now, cfg)
    if multiplier is None:
        return _zero(f"dvol {ctx.dvol_now} above no-entry threshold")
    risk_target *= multiplier

    n = int(risk_target // ctx.max_loss_per_contract_usd)
    n = min(n, cfg.sizing.max_contracts_per_trade)

    cap_aggregate_usd = cfg.sizing.cap_aggregate_open_eur * ctx.eur_to_usd
    while n > 0 and (
        Decimal(n) * ctx.max_loss_per_contract_usd + ctx.open_engagement_usd
    ) > cap_aggregate_usd:
        n -= 1

    if ctx.other_open_positions >= cfg.sizing.max_concurrent_positions:
        return _zero(
            f"already {ctx.other_open_positions} concurrent position(s); "
            f"cap is {cfg.sizing.max_concurrent_positions}"
        )

    if n < 1:
        return _zero("undersize after caps")

    return SizingResult(
        n_contracts=n,
        risk_dollars=Decimal(n) * ctx.max_loss_per_contract_usd,
        reason_if_zero=None,
    )
