"""Fund-level performance attribution: alpha/beta decomposition, strategy contribution, cost attribution."""

from __future__ import annotations

import logging
import math
import threading
import time
from collections import deque
from dataclasses import dataclass, field

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


@dataclass(frozen=True)
class AlphaBetaDecomposition:
    """Alpha/beta decomposition against a benchmark."""
    total_return: float
    alpha: float
    beta: float
    r_squared: float
    information_ratio: float


@dataclass(frozen=True)
class StrategyAttribution:
    """Return attribution for a single strategy."""
    strategy_name: str
    gross_return: float
    net_return: float
    contribution: float
    sharpe: float
    win_rate: float
    n_trades: int


@dataclass(frozen=True)
class CostAttribution:
    """Aggregated cost breakdown across all strategies."""
    total_cost: float
    spread_cost: float
    slippage_cost: float
    fee_cost: float
    cost_pct: float


@dataclass(frozen=True)
class PerformanceSnapshot:
    """Point-in-time performance attribution snapshot."""
    timestamp: float
    alpha_beta: AlphaBetaDecomposition
    strategy_attributions: list[StrategyAttribution]
    cost: CostAttribution


class PerformanceAttribution:
    """Computes fund-level alpha/beta decomposition, strategy contribution,
    and cost attribution.

    Maintains rolling return windows for portfolio and benchmark, plus
    per-strategy returns and costs. All state is thread-safe.
    """

    def __init__(self, risk_free_rate: float = 0.0) -> None:
        self._risk_free_rate = risk_free_rate
        self._portfolio_returns: deque[float] = deque(maxlen=500)
        self._benchmark_returns: deque[float] = deque(maxlen=500)
        self._strategy_returns: dict[str, deque[float]] = {}
        self._strategy_costs: dict[str, list[float]] = {}
        self._snapshots: deque[PerformanceSnapshot] = deque(maxlen=1_000)
        self._lock = threading.Lock()

    # ------------------------------------------------------------------
    # Data ingestion
    # ------------------------------------------------------------------

    def update(
        self,
        portfolio_return: float,
        benchmark_return: float,
        strategy_returns: dict[str, float] | None = None,
    ) -> None:
        """Append period returns to rolling windows.

        Args:
            portfolio_return: Fund-level return for the period.
            benchmark_return: Benchmark return for the period.
            strategy_returns: Optional per-strategy returns for the period.
        """
        with self._lock:
            self._portfolio_returns.append(portfolio_return)
            self._benchmark_returns.append(benchmark_return)
            if strategy_returns:
                for name, ret in strategy_returns.items():
                    if name not in self._strategy_returns:
                        self._strategy_returns[name] = deque(maxlen=500)
                    self._strategy_returns[name].append(ret)

    def record_cost(
        self,
        strategy_name: str,
        spread: float = 0.0,
        slippage: float = 0.0,
        fee: float = 0.0,
    ) -> None:
        """Accumulate trading costs for a strategy.

        Args:
            strategy_name: Name of the strategy incurring the cost.
            spread: Bid-ask spread cost.
            slippage: Execution slippage cost.
            fee: Exchange/broker fee.
        """
        with self._lock:
            costs = self._strategy_costs.setdefault(strategy_name, [])
            costs.append(spread + slippage + fee)
            # Also store breakdown for aggregation -- use a second key
            breakdown_key = strategy_name + "::breakdown"
            breakdown = self._strategy_costs.setdefault(breakdown_key, [0.0, 0.0, 0.0])
            breakdown[0] += spread
            breakdown[1] += slippage
            breakdown[2] += fee

    # ------------------------------------------------------------------
    # Computations
    # ------------------------------------------------------------------

    def compute_alpha_beta(self) -> AlphaBetaDecomposition | None:
        """Compute alpha/beta decomposition via simple OLS.

        Requires >= 20 observations. Returns None if insufficient data.
        """
        with self._lock:
            rp = list(self._portfolio_returns)
            rm = list(self._benchmark_returns)

        n = min(len(rp), len(rm))
        if n < 20:
            return None

        rp, rm = rp[-n:], rm[-n:]

        mean_p = sum(rp) / n
        mean_m = sum(rm) / n

        cov_pm = sum((rp[i] - mean_p) * (rm[i] - mean_m) for i in range(n))
        var_m = sum((rm[i] - mean_m) ** 2 for i in range(n))

        if var_m < 1e-18:
            return AlphaBetaDecomposition(
                total_return=sum(rp),
                alpha=sum(rp),
                beta=0.0,
                r_squared=0.0,
                information_ratio=0.0,
            )

        beta = cov_pm / var_m
        alpha = mean_p - beta * mean_m
        total_return = sum(rp)

        # R-squared = correlation^2
        var_p = sum((rp[i] - mean_p) ** 2 for i in range(n))
        if var_p < 1e-18:
            r_squared = 0.0
        else:
            corr = cov_pm / (math.sqrt(var_p) * math.sqrt(var_m))
            r_squared = corr ** 2

        # Information ratio = alpha / tracking_error
        residuals = [rp[i] - beta * rm[i] for i in range(n)]
        mean_resid = sum(residuals) / n
        var_resid = sum((r - mean_resid) ** 2 for r in residuals) / max(n - 1, 1)
        std_resid = math.sqrt(var_resid)

        if std_resid > 1e-12:
            information_ratio = alpha / std_resid
        else:
            information_ratio = 0.0

        return AlphaBetaDecomposition(
            total_return=total_return,
            alpha=alpha,
            beta=beta,
            r_squared=r_squared,
            information_ratio=information_ratio,
        )

    def compute_strategy_attributions(self) -> list[StrategyAttribution]:
        """Compute return attribution for each tracked strategy."""
        with self._lock:
            strat_data = {
                name: list(rets) for name, rets in self._strategy_returns.items()
            }
            strat_costs = {
                name: sum(costs)
                for name, costs in self._strategy_costs.items()
                if not name.endswith("::breakdown")
            }
            portfolio_rets = list(self._portfolio_returns)

        total_portfolio = sum(portfolio_rets) if portfolio_rets else 0.0
        attributions: list[StrategyAttribution] = []

        for name, rets in strat_data.items():
            if not rets:
                continue

            gross = sum(rets)
            cost = strat_costs.get(name, 0.0)
            net = gross - cost
            n_trades = len(rets)

            # Contribution to portfolio
            if abs(total_portfolio) > 1e-18:
                contribution = net / total_portfolio
            else:
                contribution = 0.0

            # Annualized Sharpe
            mean_r = sum(rets) / len(rets)
            excess = [r - self._risk_free_rate / 252.0 for r in rets]
            mean_excess = sum(excess) / len(excess)
            var_excess = sum((e - mean_excess) ** 2 for e in excess) / max(len(excess) - 1, 1)
            std_excess = math.sqrt(var_excess)
            if std_excess > 1e-12:
                sharpe = mean_excess / std_excess * math.sqrt(252)
            else:
                sharpe = 0.0

            # Win rate
            wins = sum(1 for r in rets if r > 0)
            win_rate = wins / len(rets)

            attributions.append(StrategyAttribution(
                strategy_name=name,
                gross_return=gross,
                net_return=net,
                contribution=contribution,
                sharpe=sharpe,
                win_rate=win_rate,
                n_trades=n_trades,
            ))

        # Sort by absolute contribution descending
        attributions.sort(key=lambda a: abs(a.contribution), reverse=True)
        return attributions

    def compute_cost_attribution(self) -> CostAttribution:
        """Aggregate all costs across strategies."""
        with self._lock:
            total_spread = 0.0
            total_slippage = 0.0
            total_fee = 0.0
            for key, vals in self._strategy_costs.items():
                if key.endswith("::breakdown"):
                    total_spread += vals[0]
                    total_slippage += vals[1]
                    total_fee += vals[2]

            portfolio_rets = list(self._portfolio_returns)

        total_cost = total_spread + total_slippage + total_fee
        gross_return = sum(portfolio_rets) if portfolio_rets else 0.0

        if abs(gross_return) > 1e-18:
            cost_pct = total_cost / abs(gross_return)
        else:
            cost_pct = 0.0

        return CostAttribution(
            total_cost=total_cost,
            spread_cost=total_spread,
            slippage_cost=total_slippage,
            fee_cost=total_fee,
            cost_pct=cost_pct,
        )

    def compute(self, benchmark_return: float | None = None) -> PerformanceSnapshot:
        """Run full attribution and store a snapshot.

        Args:
            benchmark_return: Optional new benchmark return to record
                before computing. If None, uses existing data only.

        Returns:
            A PerformanceSnapshot combining alpha/beta, strategy, and cost attribution.
        """
        if benchmark_return is not None:
            # Record a zero portfolio return just to keep windows aligned
            # (caller should have already called update() for real data)
            pass

        alpha_beta = self.compute_alpha_beta()
        if alpha_beta is None:
            alpha_beta = AlphaBetaDecomposition(
                total_return=0.0, alpha=0.0, beta=0.0,
                r_squared=0.0, information_ratio=0.0,
            )

        strategy_attributions = self.compute_strategy_attributions()
        cost = self.compute_cost_attribution()

        snapshot = PerformanceSnapshot(
            timestamp=time.time(),
            alpha_beta=alpha_beta,
            strategy_attributions=strategy_attributions,
            cost=cost,
        )

        with self._lock:
            self._snapshots.append(snapshot)

        logger.debug(
            "Performance snapshot: alpha=%.4f beta=%.3f r2=%.3f IR=%.3f cost=%.6f",
            alpha_beta.alpha, alpha_beta.beta,
            alpha_beta.r_squared, alpha_beta.information_ratio,
            cost.total_cost,
        )
        return snapshot

    def recent_snapshots(self, limit: int = 10) -> list[PerformanceSnapshot]:
        """Return the most recent performance snapshots."""
        with self._lock:
            return list(self._snapshots)[-limit:]
