"""Pydantic schema for ``strategy.yaml``.

The configuration is the single source of truth for every threshold used
by the :mod:`cerbero_bite.core` algorithms. The schema is intentionally
verbose: every numeric threshold from
``docs/10-config-spec.md`` is exposed as a typed field so that callers
can refer to the same name they would write in YAML.

All monetary and price-related thresholds use :class:`decimal.Decimal`
to preserve the precision conventions documented in
``docs/03-algorithms.md §0.2`` (no ``float`` for money or option prices).

Only the fields required by Phase 1 (core algorithms) are populated with
defaults that match the golden config; fields belonging to runtime,
storage, MCP, etc. are typed as ``dict[str, Any]`` or made optional so
that the schema can absorb the full ``strategy.yaml`` without forcing
non-core sections to be implemented before their owning module is
written.
"""

from __future__ import annotations

from decimal import Decimal
from typing import Any, Literal

from pydantic import BaseModel, ConfigDict, Field, model_validator

# ---------------------------------------------------------------------------
# Asset
# ---------------------------------------------------------------------------


class AssetConfig(BaseModel):
    model_config = ConfigDict(frozen=True, extra="forbid")

    symbol: str = "ETH"
    exchange: str = "deribit"


# ---------------------------------------------------------------------------
# Entry
# ---------------------------------------------------------------------------


class EntryConfig(BaseModel):
    """Filters and bias thresholds documented in ``01-strategy-rules.md``."""

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

    cron: str = "0 14 * * *"
    skip_holidays_country: str = "IT"

    # access filters (§2)
    capital_min_usd: Decimal = Field(default=Decimal("720"))
    dvol_min: Decimal = Field(default=Decimal("35"))
    dvol_max: Decimal = Field(default=Decimal("90"))
    funding_perp_abs_max_annualized: Decimal = Field(default=Decimal("0.80"))
    eth_holdings_pct_max: Decimal = Field(default=Decimal("0.30"))
    no_position_concurrent: bool = True
    exclude_macro_severity: list[str] = Field(default_factory=lambda: ["high"])
    exclude_macro_countries: list[str] = Field(default_factory=lambda: ["US", "EU"])
    # Finestra (giorni) entro cui un evento macro high-severity blocca
    # l'entry. Disaccoppiata da `structure.dte_target`: il filtro macro
    # è una protezione di rischio evento, indipendente dalla scadenza
    # scelta per le opzioni. Default 18 = comportamento storico.
    exclude_macro_within_days: int = 18

    # directional bias (§3.1)
    trend_window_days: int = 30
    trend_bull_threshold_pct: Decimal = Field(default=Decimal("0.05"))
    trend_bear_threshold_pct: Decimal = Field(default=Decimal("-0.05"))
    funding_bull_threshold_annualized: Decimal = Field(default=Decimal("0.20"))
    funding_bear_threshold_annualized: Decimal = Field(default=Decimal("-0.20"))
    iron_condor_dvol_min: Decimal = Field(default=Decimal("55"))
    iron_condor_adx_max: Decimal = Field(default=Decimal("20"))
    iron_condor_trend_neutral_band_pct: Decimal = Field(default=Decimal("0.05"))

    # quant filters (§2.8 — added in Phase 4 hardening)
    dealer_gamma_min: Decimal = Field(default=Decimal("0"))
    dealer_gamma_filter_enabled: bool = True
    liquidation_filter_enabled: bool = True

    # IV richness filter (§2.9). `iv_minus_rv_min` è la soglia in
    # punti vol che la IV implicita 30g deve eccedere la RV30g per
    # ammettere l'entry. Letteratura short-vol systematic: l'edge
    # sostenibile esiste solo con un margine misurabile fra IV e RV.
    # Default disabilitato + soglia 0 per non bloccare l'avvio finché
    # non si è calibrato sui dati raccolti (vedi `📐 Calibrazione`).
    iv_minus_rv_min: Decimal = Field(default=Decimal("0"))
    iv_minus_rv_filter_enabled: bool = False

    # IV richness gate adattivo (Phase 5+). Quando
    # `iv_minus_rv_adaptive_enabled=True`, la soglia statica
    # `iv_minus_rv_min` diventa il floor assoluto e la soglia
    # effettiva è `max(P_q rolling, floor)` calcolata su
    # `market_snapshots`. Vedi
    # `docs/superpowers/specs/2026-05-08-iv-rv-adaptive-gate-design.md`.
    iv_minus_rv_adaptive_enabled: bool = False
    iv_minus_rv_percentile: Decimal = Field(default=Decimal("0.25"))
    iv_minus_rv_window_target_days: int = 60
    iv_minus_rv_window_min_days: int = 30

    # Vol-of-Vol guard (§4-quater roadmap punto 2): blocca entry se
    # |DVOL_now - DVOL_24h_ago| supera la soglia. Cattura regime
    # shift bruschi non riflessi nel percentile rolling.
    vol_of_vol_guard_enabled: bool = False
    vol_of_vol_threshold_pt: Decimal = Field(default=Decimal("5"))
    vol_of_vol_lookback_hours: int = 24


# ---------------------------------------------------------------------------
# Structure
# ---------------------------------------------------------------------------


class DeltaByDvolBand(BaseModel):
    """Banda della step function delta-target per regime DVOL (§3.2 A)."""

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

    dvol_under: Decimal
    delta_target: Decimal
    delta_min: Decimal
    delta_max: Decimal


class ShortStrikeSpec(BaseModel):
    model_config = ConfigDict(frozen=True, extra="forbid")

    delta_target: Decimal = Field(default=Decimal("0.12"))
    delta_min: Decimal = Field(default=Decimal("0.10"))
    delta_max: Decimal = Field(default=Decimal("0.15"))
    distance_otm_pct_min: Decimal = Field(default=Decimal("0.15"))
    distance_otm_pct_max: Decimal = Field(default=Decimal("0.25"))

    # §3.2 enhancement (A): step function delta-target by DVOL regime.
    # Empty list = behaviour invariato (delta_target sopra è il singolo
    # valore). Quando popolato, il combo_builder sceglie la prima
    # banda ordinata ascending su `dvol_under` con
    # `dvol_now ≤ dvol_under`. Esempio:
    #   - dvol_under=50 → delta 0.15 (bassa vol → più premio)
    #   - dvol_under=70 → delta 0.12
    #   - dvol_under=90 → delta 0.10 (alta vol → più safety)
    delta_by_dvol: list[DeltaByDvolBand] = Field(default_factory=list)


class SpreadWidthSpec(BaseModel):
    model_config = ConfigDict(frozen=True, extra="forbid")

    target_pct_of_spot: Decimal = Field(default=Decimal("0.04"))
    min_pct_of_spot: Decimal = Field(default=Decimal("0.03"))
    max_pct_of_spot: Decimal = Field(default=Decimal("0.05"))


class StructureConfig(BaseModel):
    model_config = ConfigDict(frozen=True, extra="forbid")

    dte_target: int = 18
    dte_min: int = 14
    dte_max: int = 21

    short_strike: ShortStrikeSpec = Field(default_factory=ShortStrikeSpec)
    spread_width: SpreadWidthSpec = Field(default_factory=SpreadWidthSpec)
    credit_to_width_ratio_min: Decimal = Field(default=Decimal("0.30"))


# ---------------------------------------------------------------------------
# Liquidity
# ---------------------------------------------------------------------------


class LiquidityConfig(BaseModel):
    model_config = ConfigDict(frozen=True, extra="forbid")

    open_interest_min: int = 100
    volume_24h_min: int = 20
    bid_ask_spread_pct_max: Decimal = Field(default=Decimal("0.15"))
    book_depth_top3_min: int = 5
    slippage_pct_of_credit_max: Decimal = Field(default=Decimal("0.08"))


# ---------------------------------------------------------------------------
# Sizing
# ---------------------------------------------------------------------------


class DvolAdjustmentBand(BaseModel):
    model_config = ConfigDict(frozen=True, extra="forbid")

    dvol_under: Decimal
    multiplier: Decimal


def _default_dvol_bands() -> list[DvolAdjustmentBand]:
    return [
        DvolAdjustmentBand(dvol_under=Decimal("45"), multiplier=Decimal("1.00")),
        DvolAdjustmentBand(dvol_under=Decimal("60"), multiplier=Decimal("0.85")),
        DvolAdjustmentBand(dvol_under=Decimal("80"), multiplier=Decimal("0.65")),
    ]


class SizingConfig(BaseModel):
    model_config = ConfigDict(frozen=True, extra="forbid")

    kelly_fraction: Decimal = Field(default=Decimal("0.13"))

    cap_per_trade_eur: Decimal = Field(default=Decimal("200"))
    cap_aggregate_open_eur: Decimal = Field(default=Decimal("1000"))
    max_concurrent_positions: int = 5
    max_contracts_per_trade: int = 4

    dvol_adjustment: list[DvolAdjustmentBand] = Field(default_factory=_default_dvol_bands)
    dvol_no_entry_threshold: Decimal = Field(default=Decimal("80"))


# ---------------------------------------------------------------------------
# Exit
# ---------------------------------------------------------------------------


class PartialProfitLevel(BaseModel):
    """Livello della scala di profit-take graduale (§7.1bis C).

    `mark_at_pct_credit`: il livello è triggerato quando
    `mark_combo ≤ mark_at_pct_credit × credito_iniziale` (es. 0.25 =
    25% del credito = 75% di profitto sulla porzione chiusa).

    `close_pct_of_initial_contracts`: frazione dei contratti aperti
    INIZIALMENTE da chiudere a questo livello (es. 0.50 = chiudi metà).
    Le frazioni sono cumulative; chiudere oltre i contratti residui
    è no-op.
    """

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

    mark_at_pct_credit: Decimal
    close_pct_of_initial_contracts: Decimal


class ExitConfig(BaseModel):
    model_config = ConfigDict(frozen=True, extra="forbid")

    profit_take_pct_of_credit: Decimal = Field(default=Decimal("0.50"))
    stop_loss_mark_x_credit: Decimal = Field(default=Decimal("2.50"))
    vol_stop_dvol_increase: Decimal = Field(default=Decimal("10"))
    time_stop_dte_remaining: int = 7
    time_stop_skip_if_close_to_profit_pct: Decimal = Field(default=Decimal("0.70"))
    delta_breach_threshold: Decimal = Field(default=Decimal("0.30"))
    adverse_move_4h_pct: Decimal = Field(default=Decimal("0.05"))

    # §7.1ter (D): vol-collapse harvest. Esce in profit anche se il
    # profit-take non è ancora colpito quando DVOL è scesa di tot
    # punti rispetto all'entry (edge IV-RV catturato, vol attesa già
    # rientrata). 0 = filtro disabilitato.
    vol_harvest_dvol_decrease: Decimal = Field(default=Decimal("0"))

    # §7.1bis (C): scala graduata di profit-take. Lista vuota =
    # comportamento invariato (chiusura atomica al
    # `profit_take_pct_of_credit`). Quando popolata, l'engine
    # interpreta come "chiudi N% dei contratti iniziali al livello
    # di mark M%×credito". Le entry sono ordinate dal mark più alto
    # (più profit, livello triggerato prima) al più basso. Vedi
    # `core/exit_decision.py` per la semantica esatta.
    #
    # ATTENZIONE: questa funzione richiede il supporto di chiusure
    # parziali nel runtime (entry_cycle / repository / clients).
    # Fino al merge della partial-close pipeline, l'engine la mappa
    # a CLOSE_PROFIT atomico al primo livello triggerato (vedi
    # commento in `evaluate`). Default vuoto = no-op.
    profit_take_partial_levels: list[PartialProfitLevel] = Field(
        default_factory=list
    )

    monitor_cron: str = "0 2,14 * * *"
    user_confirmation_timeout_min: int = 30
    escalate_on_timeout: list[str] = Field(
        default_factory=lambda: ["CLOSE_STOP", "CLOSE_VOL", "CLOSE_DELTA"]
    )


# ---------------------------------------------------------------------------
# Auto-pause (F): circuit breaker su drawdown rolling
# ---------------------------------------------------------------------------


class AutoPauseConfig(BaseModel):
    """Configurazione del circuit breaker su drawdown.

    Quando abilitato, il rule engine valuta — prima di ogni entry —
    il P/L cumulato delle ultime `lookback_trades` posizioni chiuse
    in proporzione al capitale attuale. Se la perdita supera la
    soglia, l'engine si auto-mette in pausa per `pause_days`
    giorni (skip-day). La pausa si annulla automaticamente alla
    scadenza, oppure manualmente via comando dalla GUI.

    Difende da regime change non rilevati dai filtri quant: se i
    filtri stanno fallendo sistematicamente, vale la pena fermarsi
    e attendere che le condizioni cambino, invece di continuare a
    sanguinare. È un'estensione conservativa del kill switch
    (che oggi reagisce solo a errori tecnici).
    """

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

    enabled: bool = False
    lookback_trades: int = 5
    max_drawdown_pct: Decimal = Field(default=Decimal("0.10"))
    pause_days: int = 14


# ---------------------------------------------------------------------------
# Kelly recalibration
# ---------------------------------------------------------------------------


class KellyConfig(BaseModel):
    model_config = ConfigDict(frozen=True, extra="forbid")

    lookback_days: int = 365
    min_sample_low_confidence: int = 30
    min_sample_high_confidence: int = 100
    weight_when_medium_confidence: Decimal = Field(default=Decimal("0.50"))


# ---------------------------------------------------------------------------
# Sections that core/ algorithms do not consume — kept loose for now
# ---------------------------------------------------------------------------


class _LooseSection(BaseModel):
    """Catch-all section used for parts of strategy.yaml not yet typed."""

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


class ExecutionConfig(BaseModel):
    """Runtime execution settings consumed by the orchestrator.

    The remaining knobs (initial_limit, reprice_step_ticks, …) live as
    extra fields validated lazily — they will graduate to typed fields
    when the order-management layer needs them.
    """

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

    environment: Literal["testnet", "mainnet"] = "testnet"
    eur_to_usd: Decimal = Field(default=Decimal("1.075"))


class MonitoringConfig(_LooseSection): ...


class StorageConfig(_LooseSection): ...


class McpConfig(_LooseSection): ...


class TelegramConfig(_LooseSection): ...


# ---------------------------------------------------------------------------
# Root
# ---------------------------------------------------------------------------


class StrategyConfig(BaseModel):
    """Validated representation of ``strategy.yaml``."""

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

    config_version: str
    config_hash: str
    last_review: str
    last_reviewer: str

    asset: AssetConfig = Field(default_factory=AssetConfig)
    entry: EntryConfig = Field(default_factory=EntryConfig)
    structure: StructureConfig = Field(default_factory=StructureConfig)
    liquidity: LiquidityConfig = Field(default_factory=LiquidityConfig)
    sizing: SizingConfig = Field(default_factory=SizingConfig)
    exit: ExitConfig = Field(default_factory=ExitConfig)
    kelly_recalibration: KellyConfig = Field(default_factory=KellyConfig)
    auto_pause: AutoPauseConfig = Field(default_factory=AutoPauseConfig)

    execution: ExecutionConfig = Field(default_factory=ExecutionConfig)
    monitoring: MonitoringConfig = Field(default_factory=MonitoringConfig)
    storage: StorageConfig = Field(default_factory=StorageConfig)
    mcp: McpConfig = Field(default_factory=McpConfig)
    telegram: TelegramConfig = Field(default_factory=TelegramConfig)

    @model_validator(mode="after")
    def _validate_consistency(self) -> StrategyConfig:
        s = self.structure.short_strike
        if not (s.delta_min <= s.delta_target <= s.delta_max):
            raise ValueError("delta_target outside [delta_min, delta_max]")
        if s.distance_otm_pct_min >= s.distance_otm_pct_max:
            raise ValueError("OTM range invalid")

        sw = self.structure.spread_width
        if not (sw.min_pct_of_spot <= sw.target_pct_of_spot <= sw.max_pct_of_spot):
            raise ValueError("spread_width target outside min/max")

        if not (Decimal("0") < self.sizing.kelly_fraction < Decimal("0.5")):
            raise ValueError("kelly_fraction out of safe range (0, 0.5)")

        if self.exit.profit_take_pct_of_credit >= Decimal("1"):
            raise ValueError("profit_take >= 100% impossible")
        if self.exit.stop_loss_mark_x_credit <= Decimal("1"):
            raise ValueError("stop_loss multiple <= 1× makes no sense")

        if self.entry.dvol_min >= self.entry.dvol_max:
            raise ValueError("dvol_min must be < dvol_max")

        e = self.entry
        if e.iv_minus_rv_window_min_days >= e.iv_minus_rv_window_target_days:
            raise ValueError(
                "iv_minus_rv_window_min_days must be < iv_minus_rv_window_target_days"
            )
        if not (Decimal("0") < e.iv_minus_rv_percentile < Decimal("1")):
            raise ValueError(
                "iv_minus_rv_percentile must be in (0, 1)"
            )

        return self


# ---------------------------------------------------------------------------
# Convenience
# ---------------------------------------------------------------------------


SpreadType = Literal["bull_put", "bear_call", "iron_condor"]


def golden_config(**overrides: Any) -> StrategyConfig:
    """Return a :class:`StrategyConfig` populated with the golden defaults.

    The caller may pass per-section overrides (``entry={...}``,
    ``sizing={...}``, …) to mutate specific values without rebuilding the
    whole tree. Useful in tests.
    """
    base: dict[str, Any] = {
        "config_version": "1.0.0-test",
        "config_hash": "0" * 64,
        "last_review": "2026-04-26",
        "last_reviewer": "test",
    }
    base.update(overrides)
    return StrategyConfig(**base)
