"""Multi-factor alpha model: factor-based self-calibrating edge estimation."""

from __future__ import annotations

import logging
import math
import threading
import time
from collections import deque
from dataclasses import dataclass
from typing import Callable

from horizon._horizon import edge, information_coefficient, shannon_entropy

logger = logging.getLogger("horizon.fund.alpha_model")

# ---------------------------------------------------------------------------
# Factor names
# ---------------------------------------------------------------------------

_FACTOR_NAMES: list[str] = [
    "liquidity",
    "momentum",
    "mean_reversion",
    "event_proximity",
    "spread",
    "cross_market",
    "entropy",
]

# ---------------------------------------------------------------------------
# Data classes
# ---------------------------------------------------------------------------


@dataclass(frozen=True)
class FactorValue:
    """A single factor's computed value for a market snapshot."""

    name: str
    value: float  # normalized to [-1, +1]
    raw_value: float
    timestamp: float


@dataclass(frozen=True)
class FactorIC:
    """Information coefficient statistics for a factor."""

    name: str
    ic: float
    ic_ir: float  # mean IC / std IC
    n_observations: int
    is_significant: bool


@dataclass(frozen=True)
class AlphaEstimate:
    """Composite alpha estimate for a market."""

    market_id: str
    composite_alpha: float
    factor_values: list[FactorValue]
    confidence: float


# ---------------------------------------------------------------------------
# Alpha model
# ---------------------------------------------------------------------------


class AlphaModel:
    """Multi-factor alpha model for prediction markets.

    Replaces naive ``0.05 * fitness`` edge estimation with factor-based,
    self-calibrating estimation. Seven prediction-market factors are
    computed, weighted by their historical information coefficient (IC),
    and combined into a composite alpha signal.

    Factors:
        1. liquidity     -- volume ratio vs. median
        2. momentum      -- recent price change
        3. mean_reversion -- distance from 0.5 midpoint
        4. event_proximity -- inverse days to expiry
        5. spread        -- current vs. historical spread
        6. cross_market  -- cross-exchange price divergence
        7. entropy       -- Shannon entropy of the price
    """

    def __init__(self, lookback: int = 100, min_ic_observations: int = 20) -> None:
        self._lookback = lookback
        self._min_ic_observations = min_ic_observations

        # IC-proportional weights per factor (start uniform)
        self._factor_weights: dict[str, float] = {
            name: 1.0 / len(_FACTOR_NAMES) for name in _FACTOR_NAMES
        }

        # Rolling predictions per factor
        self._factor_predictions: dict[str, deque[float]] = {
            name: deque(maxlen=lookback) for name in _FACTOR_NAMES
        }

        # Rolling outcomes
        self._outcomes: deque[float] = deque(maxlen=lookback)

        # Latest IC per factor
        self._factor_ics: dict[str, FactorIC] = {}

        self._lock = threading.Lock()

    # ------------------------------------------------------------------
    # Factor computation
    # ------------------------------------------------------------------

    @staticmethod
    def _compute_liquidity(volume_24h: float, median_volume: float) -> FactorValue:
        ratio = volume_24h / max(median_volume, 1e-9)
        value = math.tanh(ratio - 1.0)
        return FactorValue(
            name="liquidity",
            value=value,
            raw_value=ratio,
            timestamp=time.time(),
        )

    @staticmethod
    def _compute_momentum(price_history: list[float] | None) -> FactorValue:
        if price_history and len(price_history) >= 2:
            price_change = price_history[-1] - price_history[0]
        else:
            price_change = 0.0
        value = math.tanh(price_change * 10.0)
        return FactorValue(
            name="momentum",
            value=value,
            raw_value=price_change,
            timestamp=time.time(),
        )

    @staticmethod
    def _compute_mean_reversion(price: float) -> FactorValue:
        raw = abs(price - 0.5)
        value = 1.0 - raw * 2.0
        return FactorValue(
            name="mean_reversion",
            value=value,
            raw_value=raw,
            timestamp=time.time(),
        )

    @staticmethod
    def _compute_event_proximity(days_to_expiry: float) -> FactorValue:
        value = 1.0 / (1.0 + days_to_expiry)
        return FactorValue(
            name="event_proximity",
            value=value,
            raw_value=days_to_expiry,
            timestamp=time.time(),
        )

    @staticmethod
    def _compute_spread(current_spread: float, avg_spread: float) -> FactorValue:
        denom = max(avg_spread, 0.01)
        value = math.tanh((avg_spread - current_spread) / denom)
        return FactorValue(
            name="spread",
            value=value,
            raw_value=current_spread,
            timestamp=time.time(),
        )

    @staticmethod
    def _compute_cross_market(
        price: float, cross_price: float | None,
    ) -> FactorValue:
        if cross_price is None:
            return FactorValue(
                name="cross_market", value=0.0, raw_value=0.0, timestamp=time.time(),
            )
        diff = (cross_price - price) * 5.0
        value = max(-1.0, min(1.0, diff))
        return FactorValue(
            name="cross_market",
            value=value,
            raw_value=cross_price - price,
            timestamp=time.time(),
        )

    @staticmethod
    def _compute_entropy(price: float) -> FactorValue:
        # Clamp price to (0, 1) to keep shannon_entropy valid
        p = max(1e-9, min(1.0 - 1e-9, price))
        raw = shannon_entropy(p)
        normalized = (raw - 0.5) / 0.5
        value = max(-1.0, min(1.0, normalized))
        return FactorValue(
            name="entropy",
            value=value,
            raw_value=raw,
            timestamp=time.time(),
        )

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

    def estimate_alpha(
        self,
        market_id: str,
        price: float,
        volume_24h: float = 0.0,
        median_volume: float = 1.0,
        days_to_expiry: float = 30.0,
        current_spread: float = 0.05,
        avg_spread: float = 0.05,
        cross_price: float | None = None,
        price_history: list[float] | None = None,
    ) -> AlphaEstimate:
        """Compute multi-factor alpha estimate for a market.

        Args:
            market_id: Market identifier.
            price: Current mid price (0-1 for prediction markets).
            volume_24h: 24-hour trading volume.
            median_volume: Median historical volume (baseline).
            days_to_expiry: Days until market resolution.
            current_spread: Current bid-ask spread.
            avg_spread: Historical average spread.
            cross_price: Same-question price on another exchange.
            price_history: Recent price history for momentum.

        Returns:
            AlphaEstimate with composite alpha and per-factor breakdown.
        """
        factor_values = [
            self._compute_liquidity(volume_24h, median_volume),
            self._compute_momentum(price_history),
            self._compute_mean_reversion(price),
            self._compute_event_proximity(days_to_expiry),
            self._compute_spread(current_spread, avg_spread),
            self._compute_cross_market(price, cross_price),
            self._compute_entropy(price),
        ]

        with self._lock:
            # Composite alpha: weighted sum of factor values
            composite = 0.0
            for fv in factor_values:
                composite += fv.value * self._factor_weights.get(fv.name, 0.0)
            composite = max(-1.0, min(1.0, composite))

            # Record factor predictions for IC tracking
            for fv in factor_values:
                self._factor_predictions[fv.name].append(fv.value)

            # Confidence: mean |IC| * coverage fraction, or 0.3 default
            confidence = self._compute_confidence()

        return AlphaEstimate(
            market_id=market_id,
            composite_alpha=composite,
            factor_values=factor_values,
            confidence=confidence,
        )

    def record_outcome(self, market_id: str, outcome: float) -> None:
        """Record an actual outcome for IC updating.

        Args:
            market_id: Market identifier (for logging).
            outcome: Realized outcome value.
        """
        with self._lock:
            self._outcomes.append(outcome)
        logger.debug("Recorded outcome for %s: %.4f", market_id, outcome)

    def update_ics(self) -> None:
        """Recompute information coefficients for all factors.

        Uses the Rust ``information_coefficient()`` function for the
        rank correlation calculation. Weights are updated proportional
        to ``abs(IC_IR)``, with a minimum weight floor of 0.02.
        """
        with self._lock:
            outcomes = list(self._outcomes)
            predictions_map: dict[str, list[float]] = {}
            for name in _FACTOR_NAMES:
                predictions_map[name] = list(self._factor_predictions[name])

        if len(outcomes) < self._min_ic_observations:
            logger.debug(
                "Not enough outcomes for IC update (%d < %d)",
                len(outcomes), self._min_ic_observations,
            )
            return

        new_ics: dict[str, FactorIC] = {}
        abs_ic_irs: dict[str, float] = {}

        for name in _FACTOR_NAMES:
            preds = predictions_map[name]
            # Align lengths: use the most recent overlapping observations
            n = min(len(preds), len(outcomes))
            if n < self._min_ic_observations:
                new_ics[name] = FactorIC(
                    name=name, ic=0.0, ic_ir=0.0,
                    n_observations=n, is_significant=False,
                )
                abs_ic_irs[name] = 0.0
                continue

            preds_aligned = preds[-n:]
            outcomes_aligned = outcomes[-n:]

            ic_val = information_coefficient(preds_aligned, outcomes_aligned)

            # Compute IC_IR: rolling IC mean / IC std via windowed approach
            # Use expanding windows of size min_ic_observations
            window_ics: list[float] = []
            win = self._min_ic_observations
            for i in range(win, n + 1):
                w_preds = preds_aligned[i - win : i]
                w_outcomes = outcomes_aligned[i - win : i]
                w_ic = information_coefficient(w_preds, w_outcomes)
                window_ics.append(w_ic)

            if len(window_ics) >= 2:
                ic_mean = sum(window_ics) / len(window_ics)
                ic_var = sum((x - ic_mean) ** 2 for x in window_ics) / len(window_ics)
                ic_std = math.sqrt(ic_var) if ic_var > 0 else 1e-9
                ic_ir = ic_mean / max(ic_std, 1e-9)
            else:
                ic_ir = ic_val  # single window, use raw IC

            is_significant = abs(ic_val) > 0.05 and n >= self._min_ic_observations
            new_ics[name] = FactorIC(
                name=name,
                ic=ic_val,
                ic_ir=ic_ir,
                n_observations=n,
                is_significant=is_significant,
            )
            abs_ic_irs[name] = abs(ic_ir)

        # Update weights proportional to abs(IC_IR)
        total_abs_ir = sum(abs_ic_irs.values())
        min_weight = 0.02

        with self._lock:
            self._factor_ics = new_ics

            if total_abs_ir > 1e-9:
                raw_weights: dict[str, float] = {}
                for name in _FACTOR_NAMES:
                    w = abs_ic_irs[name] / total_abs_ir
                    # Sign-flip: if IC is negative, the factor predicts
                    # inversely, so we keep the weight but note the sign
                    # is handled by the factor value itself.
                    # Negative IC => flip the weight sign so the composite
                    # uses the factor in the correct direction.
                    ic_val = new_ics[name].ic
                    if ic_val < 0:
                        w = -w
                    raw_weights[name] = w

                # Enforce minimum absolute weight
                for name in _FACTOR_NAMES:
                    if abs(raw_weights[name]) < min_weight:
                        raw_weights[name] = (
                            min_weight if raw_weights[name] >= 0 else -min_weight
                        )

                # Renormalize so abs(weights) sum to 1
                total_abs = sum(abs(w) for w in raw_weights.values())
                if total_abs > 1e-9:
                    for name in _FACTOR_NAMES:
                        self._factor_weights[name] = raw_weights[name] / total_abs

        logger.info(
            "IC update: %s",
            ", ".join(
                f"{name}={new_ics[name].ic:.3f}" for name in _FACTOR_NAMES
            ),
        )

    def factor_report(self) -> list[FactorIC]:
        """Return current IC statistics per factor."""
        with self._lock:
            return [
                self._factor_ics.get(
                    name,
                    FactorIC(name=name, ic=0.0, ic_ir=0.0, n_observations=0, is_significant=False),
                )
                for name in _FACTOR_NAMES
            ]

    def as_edge_estimator(
        self, default_price: float = 0.5,
    ) -> Callable[[str, str], tuple[float, float]]:
        """Return a callable compatible with ``ResearchPipeline.set_edge_estimator()``.

        The returned function accepts ``(market_id, exchange)`` and returns
        ``(edge, confidence)`` using the composite alpha as the edge estimate.

        Args:
            default_price: Price to use for factor computation when the
                actual market price is unavailable. Defaults to 0.5.
        """
        model = self
        price = default_price

        def _estimator(market_id: str, exchange: str) -> tuple[float, float]:
            estimate = model.estimate_alpha(market_id=market_id, price=price)
            return abs(estimate.composite_alpha), estimate.confidence

        return _estimator

    # ------------------------------------------------------------------
    # Internal helpers
    # ------------------------------------------------------------------

    def _compute_confidence(self) -> float:
        """Compute confidence from IC data. Must be called under lock."""
        if not self._factor_ics:
            return 0.3

        abs_ics = [abs(fic.ic) for fic in self._factor_ics.values()]
        mean_abs_ic = sum(abs_ics) / len(abs_ics) if abs_ics else 0.0

        # Coverage: fraction of observations relative to 100
        n_obs = max(
            (fic.n_observations for fic in self._factor_ics.values()), default=0,
        )
        coverage = min(n_obs / 100.0, 1.0)

        return mean_abs_ic * coverage
