"""Pre-trade liquidity gate (``docs/03-algorithms.md §2``).

Validates each leg against open-interest, volume, bid-ask, depth
thresholds and computes the expected slippage as a percentage of the
credit. Returns the full list of failing reasons together with the
slippage estimate so the report can render a precise rejection.
"""

from __future__ import annotations

from decimal import Decimal

from pydantic import BaseModel, ConfigDict

from cerbero_bite.config import StrategyConfig

__all__ = ["InstrumentSnapshot", "LiquidityCheck", "check"]


class InstrumentSnapshot(BaseModel):
    """Per-instrument market snapshot used by the liquidity check."""

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

    instrument: str
    bid: Decimal
    ask: Decimal
    mid: Decimal
    open_interest: int
    volume_24h: int
    book_depth_top3: int


class LiquidityCheck(BaseModel):
    """Outcome of :func:`check`: accepted flag, full reasons, slippage estimate."""

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

    accepted: bool
    reasons: list[str]
    estimated_slippage_pct_of_credit: Decimal


def _check_leg(
    snap: InstrumentSnapshot,
    *,
    label: str,
    cfg: StrategyConfig,
) -> list[str]:
    """Return the list of failing reasons for a single leg."""
    reasons: list[str] = []
    liq = cfg.liquidity

    if snap.open_interest < liq.open_interest_min:
        reasons.append(
            f"{label} open interest below threshold "
            f"({snap.open_interest} < {liq.open_interest_min})"
        )
    if snap.volume_24h < liq.volume_24h_min:
        reasons.append(
            f"{label} 24h volume below threshold "
            f"({snap.volume_24h} < {liq.volume_24h_min})"
        )
    if snap.book_depth_top3 < liq.book_depth_top3_min:
        reasons.append(
            f"{label} book depth top3 below threshold "
            f"({snap.book_depth_top3} < {liq.book_depth_top3_min})"
        )

    if snap.mid > 0:
        spread_pct = (snap.ask - snap.bid) / snap.mid
        if spread_pct > liq.bid_ask_spread_pct_max:
            reasons.append(
                f"{label} bid-ask spread {spread_pct:.4f} above cap "
                f"{liq.bid_ask_spread_pct_max}"
            )
    else:
        reasons.append(f"{label} mid price not positive ({snap.mid})")

    return reasons


def check(
    *,
    short_leg: InstrumentSnapshot,
    long_leg: InstrumentSnapshot,
    credit: Decimal,
    n_contracts: int,
    cfg: StrategyConfig,
) -> LiquidityCheck:
    """Validate the liquidity of a 2-leg vertical spread.

    Args:
        short_leg: the leg the engine intends to *sell*.
        long_leg: the leg the engine intends to *buy* as protection.
        credit: net mid-price credit of the combo, in ETH.
        n_contracts: number of combo contracts that will be sent.
        cfg: validated strategy configuration.

    Returns:
        :class:`LiquidityCheck` with ``accepted`` set when every leg
        passes its thresholds *and* the estimated slippage is within
        the configured cap.
    """
    reasons: list[str] = _check_leg(short_leg, label="short leg", cfg=cfg)
    reasons.extend(_check_leg(long_leg, label="long leg", cfg=cfg))

    if n_contracts <= 0:
        reasons.append(f"non-positive number of contracts ({n_contracts})")

    if credit <= 0:
        reasons.append(f"non-positive credit ({credit})")
        # Cannot compute slippage_pct without a positive credit.
        return LiquidityCheck(
            accepted=False,
            reasons=reasons,
            estimated_slippage_pct_of_credit=Decimal("0"),
        )

    slippage_eth_per_contract = (short_leg.ask - short_leg.mid) + (long_leg.mid - long_leg.bid)
    n_for_slippage = max(n_contracts, 0)
    slippage_total_eth = slippage_eth_per_contract * Decimal(n_for_slippage)
    pct = slippage_total_eth / credit

    if pct > cfg.liquidity.slippage_pct_of_credit_max:
        reasons.append(
            f"estimated slippage {pct:.4f} above cap "
            f"{cfg.liquidity.slippage_pct_of_credit_max}"
        )

    return LiquidityCheck(
        accepted=not reasons,
        reasons=reasons,
        estimated_slippage_pct_of_credit=pct,
    )
