"""
Risk Manager — Kelly Criterion position sizing with safety constraints.

Uses 1/4 Kelly fraction to be conservative during paper trading phase.
Hard limits: max 5% per position, max 30% total exposure.
"""
import logging
from dataclasses import dataclass
from typing import Optional

from bot.strategy.bayesian import TradingSignal

log = logging.getLogger(__name__)

KELLY_FRACTION = 0.25  # Quarter Kelly — conservative


@dataclass
class Portfolio:
    cash: float
    positions: dict[str, float]  # market_id -> USDC amount allocated

    @property
    def total_value(self) -> float:
        return self.cash + sum(self.positions.values())

    @property
    def total_exposure(self) -> float:
        return sum(self.positions.values())

    @property
    def exposure_pct(self) -> float:
        if self.total_value == 0:
            return 0
        return self.total_exposure / self.total_value


@dataclass
class Order:
    market_id: str
    question: str
    direction: str        # "BUY_YES" | "BUY_NO"
    size_usdc: float      # Amount to risk in USDC
    market_price: float   # Polymarket YES price (0-1) — used for entry_price calculation
    signal_edge: float
    signal_confidence: float
    reasoning: str
    # Phase 1 — edge neto audit fields (passed through from TradingSignal)
    edge_gross: float = 0.0
    edge_net: float = 0.0
    prior_prob: float = 0.0
    final_prob: float = 0.0
    mid_price: float = 0.0
    spread_estimate: float = 0.02
    # Phase 2 — market family
    family_key: str = ""
    # Phase 4 — regime threshold applied
    regime_min_edge: float = 0.10
    # Phase 6 — per-feature log-odds contributions (see TradingSignal for semantics)
    feat_fg_lo:      float = 0.0
    feat_mom_lo:     float = 0.0
    feat_news_lo:    float = 0.0
    feat_mfld_lo:    float = 0.0
    feat_btc_dom_lo: float = 0.0
    # Manifold audit fields (propagated from TradingSignal → Trade → DB)
    mfld_audit_id:    Optional[str]   = None
    mfld_market_id:   Optional[str]   = None
    mfld_market_title: Optional[str]  = None
    mfld_market_url:  Optional[str]   = None
    mfld_prob_raw:    Optional[float] = None
    mfld_prob_final:  Optional[float] = None
    mfld_inverted:    bool            = False
    mfld_match_score: Optional[float] = None
    mfld_match_reason: Optional[str]  = None
    mfld_match_status: Optional[str]  = None


class RiskManager:
    def __init__(
        self,
        max_position_pct: float = 0.05,
        max_exposure_pct: float = 0.30,
    ) -> None:
        self.max_position_pct = max_position_pct
        self.max_exposure_pct = max_exposure_pct

    def size_order(
        self,
        signal: TradingSignal,
        portfolio: Portfolio,
    ) -> Optional[Order]:
        """
        Apply Kelly criterion to size the order.
        Returns None if constraints are not met.
        """
        # Check total exposure limit
        if portfolio.exposure_pct >= self.max_exposure_pct:
            log.info(
                "Exposure limit reached: %.1f%% >= %.1f%%",
                portfolio.exposure_pct * 100,
                self.max_exposure_pct * 100,
            )
            return None

        # Check if already in this market
        if signal.market_id in portfolio.positions:
            log.debug("Already have position in market %s", signal.market_id)
            return None

        # Kelly formula: f = (bp - q) / b
        # b = odds (1/price - 1), p = estimated_prob, q = 1 - p
        price = signal.polymarket_price if signal.direction == "BUY_YES" else (1 - signal.polymarket_price)
        if price <= 0 or price >= 1:
            return None

        b = (1 / price) - 1  # decimal odds
        p = signal.estimated_prob if signal.direction == "BUY_YES" else (1 - signal.estimated_prob)
        q = 1 - p

        kelly_full = (b * p - q) / b
        if kelly_full <= 0:
            log.debug("Kelly fraction negative — no edge after fees")
            return None

        kelly_fraction = kelly_full * KELLY_FRACTION

        # Apply position size limits
        max_by_kelly = portfolio.total_value * kelly_fraction
        max_by_rule = portfolio.total_value * self.max_position_pct
        remaining_exposure = portfolio.total_value * self.max_exposure_pct - portfolio.total_exposure

        size = min(max_by_kelly, max_by_rule, remaining_exposure, portfolio.cash)

        if size < 5:  # Minimum trade size $5
            log.debug("Order too small: $%.2f", size)
            return None

        log.info(
            "Order sized: %s %s $%.2f (kelly=%.1f%% capped at %.1f%%)",
            signal.direction,
            signal.question[:40],
            size,
            kelly_fraction * 100,
            self.max_position_pct * 100,
        )

        return Order(
            market_id=signal.market_id,
            question=signal.question,
            direction=signal.direction,
            size_usdc=size,
            market_price=signal.polymarket_price,
            signal_edge=signal.edge,
            signal_confidence=signal.confidence,
            reasoning=signal.reasoning,
            # Phase 1 — pass audit fields through to executor
            edge_gross=signal.edge_gross,
            edge_net=signal.edge_net,
            prior_prob=signal.prior_prob,
            final_prob=signal.final_prob,
            mid_price=signal.mid_price,
            spread_estimate=signal.spread_estimate,
            # Phase 2 — family
            family_key=signal.family_key,
            # Phase 4 — regime
            regime_min_edge=signal.regime_min_edge,
            # Phase 6 — feature log-odds
            feat_fg_lo=signal.feat_fg_lo,
            feat_mom_lo=signal.feat_mom_lo,
            feat_news_lo=signal.feat_news_lo,
            feat_mfld_lo=signal.feat_mfld_lo,
            feat_btc_dom_lo=signal.feat_btc_dom_lo,
            # Manifold audit
            mfld_audit_id=signal.mfld_audit_id,
            mfld_market_id=signal.mfld_market_id,
            mfld_market_title=signal.mfld_market_title,
            mfld_market_url=signal.mfld_market_url,
            mfld_prob_raw=signal.mfld_prob_raw,
            mfld_prob_final=signal.mfld_prob_final,
            mfld_inverted=signal.mfld_inverted,
            mfld_match_score=signal.mfld_match_score,
            mfld_match_reason=signal.mfld_match_reason,
            mfld_match_status=signal.mfld_match_status,
        )
