"""Position sizing engine for the autonomous trading system.

Computes dollar allocation and share quantity for a trade by applying
a sequential adjustment pipeline: confidence gate, correlation reduction,
sector exposure, diversification bonus, earnings proximity, portfolio
heat check, active-pool minimum, absolute cap, and share rounding.

Also provides v3 fractional Kelly position sizing (Requirements 14.1–14.7).
"""

from __future__ import annotations

import math
from dataclasses import dataclass
from datetime import datetime, timezone

from services.trading.models import (
    OpenPosition,
    PortfolioState,
    PositionSizeResult,
    RiskTierConfig,
)


class PositionSizer:
    """Compute position size through a multi-step adjustment pipeline."""

    # ------------------------------------------------------------------
    # Public API
    # ------------------------------------------------------------------

    def compute(
        self,
        confidence: float,
        ticker: str,
        sector: str,
        current_price: float,
        active_pool: float,
        risk_tier: RiskTierConfig,
        portfolio_state: PortfolioState,
        correlation_matrix: dict[tuple[str, str], float],
        earnings_calendar: dict[str, datetime],
        absolute_position_cap: float = 50.0,
        active_pool_minimum: float = 100.0,
    ) -> PositionSizeResult:
        """Run the full adjustment pipeline and return a sizing result."""

        adjustments: list[str] = []

        # ---- 1. Active pool minimum check (early reject) -------------
        if active_pool < active_pool_minimum:
            return self._rejected(
                f"Active pool ${active_pool:.2f} below minimum ${active_pool_minimum:.2f}",
                adjustments,
            )

        # ---- 2. Confidence gate --------------------------------------
        if confidence < risk_tier.min_confidence:
            return self._rejected(
                f"Confidence {confidence:.4f} below tier minimum {risk_tier.min_confidence}",
                adjustments,
            )

        # ---- 3. Base sizing formula ----------------------------------
        base_allocation_pct = risk_tier.max_position_pct * 0.5
        multiplier = 1.0  # default multiplier

        raw_pct = (
            base_allocation_pct
            * (confidence / risk_tier.min_confidence)
            * multiplier
        )
        clamped_pct = min(raw_pct, risk_tier.max_position_pct)
        dollar_amount = active_pool * clamped_pct
        dollar_amount = min(dollar_amount, absolute_position_cap)

        adjustments.append(
            f"Base sizing: raw_pct={raw_pct:.6f}, clamped_pct={clamped_pct:.6f}, "
            f"dollar=${dollar_amount:.2f}"
        )

        # ---- 4. Correlation reduction --------------------------------
        dollar_amount, clamped_pct = self._apply_correlation_reduction(
            ticker,
            dollar_amount,
            clamped_pct,
            portfolio_state,
            correlation_matrix,
            adjustments,
        )
        if dollar_amount == 0.0:
            return self._rejected(adjustments[-1], adjustments)

        # ---- 5. Sector exposure reduction ----------------------------
        dollar_amount, clamped_pct = self._apply_sector_exposure_reduction(
            sector,
            dollar_amount,
            clamped_pct,
            active_pool,
            risk_tier,
            portfolio_state,
            adjustments,
        )

        # ---- 6. Diversification bonus --------------------------------
        dollar_amount, clamped_pct = self._apply_diversification_bonus(
            sector,
            dollar_amount,
            clamped_pct,
            risk_tier,
            portfolio_state,
            adjustments,
        )

        # ---- 7. Earnings proximity -----------------------------------
        result = self._apply_earnings_proximity(
            ticker,
            dollar_amount,
            clamped_pct,
            earnings_calendar,
            adjustments,
        )
        if isinstance(result, PositionSizeResult):
            return result
        dollar_amount, clamped_pct = result

        # ---- 8. Absolute cap enforcement (re-apply after adjustments) -
        if dollar_amount > absolute_position_cap:
            dollar_amount = absolute_position_cap
            clamped_pct = dollar_amount / active_pool if active_pool > 0 else 0.0
            adjustments.append(
                f"Absolute cap enforced: capped to ${absolute_position_cap:.2f}"
            )

        # ---- 9. Portfolio heat check ---------------------------------
        stop_loss_distance_pct = risk_tier.stop_loss_atr_multiplier * 0.02
        new_position_heat = dollar_amount * stop_loss_distance_pct
        max_heat_dollars = risk_tier.max_portfolio_heat * active_pool
        current_heat = portfolio_state.portfolio_heat

        if current_heat + new_position_heat > max_heat_dollars:
            return self._rejected(
                f"Portfolio heat would exceed limit: current={current_heat:.2f} + "
                f"new={new_position_heat:.2f} > max={max_heat_dollars:.2f}",
                adjustments,
            )

        # ---- 10. Share rounding --------------------------------------
        if current_price <= 0:
            return self._rejected("Invalid current price", adjustments)

        share_quantity = math.floor(dollar_amount / current_price)
        if share_quantity == 0:
            return self._rejected(
                f"Zero shares after rounding: ${dollar_amount:.2f} / ${current_price:.2f}",
                adjustments,
            )

        # Final dollar amount based on whole shares
        final_dollar = share_quantity * current_price
        final_pct = final_dollar / active_pool if active_pool > 0 else 0.0
        adjustments.append(
            f"Final: {share_quantity} shares @ ${current_price:.2f} = ${final_dollar:.2f} "
            f"({final_pct:.4%} of active pool)"
        )

        return PositionSizeResult(
            dollar_amount=final_dollar,
            share_quantity=share_quantity,
            allocation_pct=final_pct,
            adjustments=adjustments,
            rejected=False,
            rejection_reason="",
        )

    # ------------------------------------------------------------------
    # Private helpers
    # ------------------------------------------------------------------

    @staticmethod
    def _rejected(reason: str, adjustments: list[str]) -> PositionSizeResult:
        return PositionSizeResult(
            dollar_amount=0.0,
            share_quantity=0,
            allocation_pct=0.0,
            adjustments=adjustments,
            rejected=True,
            rejection_reason=reason,
        )

    @staticmethod
    def _apply_correlation_reduction(
        ticker: str,
        dollar_amount: float,
        allocation_pct: float,
        portfolio_state: PortfolioState,
        correlation_matrix: dict[tuple[str, str], float],
        adjustments: list[str],
    ) -> tuple[float, float]:
        """Reduce or reject based on weighted average correlation."""
        positions: list[OpenPosition] = portfolio_state.positions
        if not positions:
            return dollar_amount, allocation_pct

        total_weight = 0.0
        weighted_corr = 0.0
        for pos in positions:
            corr = correlation_matrix.get(
                (ticker, pos.ticker),
                correlation_matrix.get((pos.ticker, ticker), 0.0),
            )
            weight = pos.market_value
            weighted_corr += corr * weight
            total_weight += weight

        if total_weight == 0.0:
            return dollar_amount, allocation_pct

        avg_corr = weighted_corr / total_weight

        if avg_corr > 0.8:
            adjustments.append(
                f"Correlation rejection: avg={avg_corr:.4f} > 0.8"
            )
            return 0.0, 0.0

        if avg_corr > 0.5:
            # Reduce proportionally: scale factor goes from 1.0 at 0.5 to 0.0 at 0.8
            reduction = (avg_corr - 0.5) / (0.8 - 0.5)
            factor = 1.0 - reduction
            new_dollar = dollar_amount * factor
            new_pct = allocation_pct * factor
            adjustments.append(
                f"Correlation reduction: avg={avg_corr:.4f}, factor={factor:.4f}, "
                f"${dollar_amount:.2f} -> ${new_dollar:.2f}"
            )
            return new_dollar, new_pct

        return dollar_amount, allocation_pct

    @staticmethod
    def _apply_sector_exposure_reduction(
        sector: str,
        dollar_amount: float,
        allocation_pct: float,
        active_pool: float,
        risk_tier: RiskTierConfig,
        portfolio_state: PortfolioState,
        adjustments: list[str],
    ) -> tuple[float, float]:
        """Reduce allocation if sector would exceed max_sector_pct."""
        max_sector_dollars = risk_tier.max_sector_pct * active_pool
        current_sector_exposure = portfolio_state.sector_exposure.get(sector, 0.0)

        if current_sector_exposure + dollar_amount > max_sector_dollars:
            available = max(max_sector_dollars - current_sector_exposure, 0.0)
            if available <= 0:
                adjustments.append(
                    f"Sector exposure at limit: {sector} "
                    f"${current_sector_exposure:.2f} >= max ${max_sector_dollars:.2f}"
                )
                return 0.0, 0.0
            new_pct = available / active_pool if active_pool > 0 else 0.0
            adjustments.append(
                f"Sector exposure reduction: {sector} "
                f"${current_sector_exposure:.2f} + ${dollar_amount:.2f} > "
                f"max ${max_sector_dollars:.2f}, reduced to ${available:.2f}"
            )
            return available, new_pct

        return dollar_amount, allocation_pct

    @staticmethod
    def _apply_diversification_bonus(
        sector: str,
        dollar_amount: float,
        allocation_pct: float,
        risk_tier: RiskTierConfig,
        portfolio_state: PortfolioState,
        adjustments: list[str],
    ) -> tuple[float, float]:
        """Apply 1.2x bonus for under-represented sectors when < 3 sectors held."""
        existing_sectors = set(portfolio_state.sector_exposure.keys())
        if len(existing_sectors) < 3 and sector not in existing_sectors:
            bonus = 1.2
            new_dollar = dollar_amount * bonus
            new_pct = allocation_pct * bonus
            # Re-clamp to max_position_pct after bonus
            max_dollar = risk_tier.max_position_pct * (
                portfolio_state.active_pool
                if portfolio_state.active_pool > 0
                else 1.0
            )
            if new_dollar > max_dollar:
                new_dollar = max_dollar
                new_pct = risk_tier.max_position_pct
            adjustments.append(
                f"Diversification bonus: 1.2x applied for new sector '{sector}' "
                f"(portfolio has {len(existing_sectors)} sectors), "
                f"${dollar_amount:.2f} -> ${new_dollar:.2f}"
            )
            return new_dollar, new_pct

        return dollar_amount, allocation_pct

    @staticmethod
    def _apply_earnings_proximity(
        ticker: str,
        dollar_amount: float,
        allocation_pct: float,
        earnings_calendar: dict[str, datetime],
        adjustments: list[str],
    ) -> tuple[float, float] | PositionSizeResult:
        """Reduce by 50% within 3 trading days; reject within 1 trading day."""
        if ticker not in earnings_calendar:
            return dollar_amount, allocation_pct

        earnings_dt = earnings_calendar[ticker]
        now = datetime.now(tz=timezone.utc)
        delta = earnings_dt - now
        # Use total_seconds for precise fractional-day comparison
        trading_days_until = delta.total_seconds() / 86400.0

        if trading_days_until < 0:
            # Earnings already passed
            return dollar_amount, allocation_pct

        if trading_days_until <= 1:
            adjustments.append(
                f"Earnings rejection: {ticker} earnings in {trading_days_until:.1f} day(s)"
            )
            return PositionSizeResult(
                dollar_amount=0.0,
                share_quantity=0,
                allocation_pct=0.0,
                adjustments=adjustments,
                rejected=True,
                rejection_reason=f"Earnings within 1 trading day for {ticker}",
            )

        if trading_days_until <= 3:
            new_dollar = dollar_amount * 0.5
            new_pct = allocation_pct * 0.5
            adjustments.append(
                f"Earnings proximity: {ticker} earnings in {trading_days_until:.1f} days, "
                f"50% reduction: ${dollar_amount:.2f} -> ${new_dollar:.2f}"
            )
            return new_dollar, new_pct

        return dollar_amount, allocation_pct



# ===========================================================================
# v3 Fractional Kelly Position Sizing (Requirements 14.1–14.7)
# ===========================================================================


@dataclass(frozen=True)
class KellySizingResult:
    """Result of v3 fractional Kelly position sizing computation."""

    portfolio_pct: float
    f_kelly: float
    reward_ratio: float
    downgrade: bool
    downgrade_reason: str


def _clamp(value: float, lo: float, hi: float) -> float:
    """Clamp value to [lo, hi]."""
    return max(lo, min(hi, value))


def compute_reward_ratio(
    confidence: float, strength: float, contradiction: float
) -> float:
    """Compute reward ratio b = clamp(1.2 + 2.0*confidence + 1.0*strength - contradiction, 1.2, 3.0).

    Requirements: 14.2
    """
    raw = 1.2 + 2.0 * confidence + 1.0 * strength - contradiction
    return _clamp(raw, 1.2, 3.0)


def compute_kelly_sizing(
    p_win: float,
    b: float,
    confidence: float,
    data_quality: float,
    contradiction: float,
    max_position_pct: float,
    available_caps: dict[str, float],
) -> KellySizingResult:
    """Compute fractional Kelly position sizing.

    f_kelly = (p_win * b - (1 - p_win)) / b
    portfolio_pct = clamp(max(0, f_kelly) * 0.25 * confidence * data_quality * (1 - contradiction), 0, max_position_pct)

    Apply min of all capacity constraints from available_caps:
      - sector_capacity
      - correlation_capacity (0 if avg corr > 0.80)
      - heat_capacity

    Downgrade rules:
      - If f_kelly <= 0 → portfolio_pct = 0, downgrade with reason "negative_edge"
      - If portfolio_pct < 0.005 → downgrade with reason "position_below_minimum"

    Requirements: 14.1, 14.2, 14.3, 14.4, 14.5, 14.6, 14.7
    """
    # Compute Kelly fraction (Req 14.3)
    f_kelly = (p_win * b - (1.0 - p_win)) / b

    # Negative edge → immediate downgrade (Req 14.7)
    if f_kelly <= 0.0:
        return KellySizingResult(
            portfolio_pct=0.0,
            f_kelly=f_kelly,
            reward_ratio=b,
            downgrade=True,
            downgrade_reason="negative_edge",
        )

    # Apply fractional Kelly with dampening factors (Req 14.4)
    raw_pct = f_kelly * 0.25 * confidence * data_quality * (1.0 - contradiction)
    portfolio_pct = _clamp(max(0.0, raw_pct), 0.0, max_position_pct)

    # Apply capacity constraints (Req 14.5)
    sector_capacity = available_caps.get("sector_capacity", max_position_pct)
    correlation_capacity = available_caps.get("correlation_capacity", max_position_pct)
    heat_capacity = available_caps.get("heat_capacity", max_position_pct)

    # Correlation capacity of 0 means avg corr > 0.80 → force zero
    portfolio_pct = min(
        portfolio_pct,
        max_position_pct,
        sector_capacity,
        correlation_capacity,
        heat_capacity,
    )

    # Position below minimum threshold → downgrade (Req 14.6)
    if portfolio_pct < 0.005:
        return KellySizingResult(
            portfolio_pct=0.0,
            f_kelly=f_kelly,
            reward_ratio=b,
            downgrade=True,
            downgrade_reason="position_below_minimum",
        )

    return KellySizingResult(
        portfolio_pct=portfolio_pct,
        f_kelly=f_kelly,
        reward_ratio=b,
        downgrade=False,
        downgrade_reason="",
    )
