"""Cross-strategy correlation monitoring and detection."""

from __future__ import annotations

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

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


@dataclass(frozen=True)
class CorrelationPair:
    """Correlation measurement between two strategies."""
    strategy_a: str
    strategy_b: str
    correlation: float
    n_observations: int
    timestamp: float


@dataclass(frozen=True)
class ConcentrationAlert:
    """Alert when strategies are too correlated."""
    strategy_a: str
    strategy_b: str
    correlation: float
    threshold: float
    timestamp: float
    message: str


class CorrelationMonitor:
    """Monitors pairwise correlation between strategy returns.

    Maintains rolling return windows for each strategy and computes
    pairwise Pearson correlations. Raises alerts when correlation
    exceeds the configured threshold, indicating hidden concentration.
    """

    def __init__(
        self,
        max_correlation: float = 0.85,
        lookback: int = 100,
    ) -> None:
        self._max_correlation = max_correlation
        self._lookback = lookback
        self._returns: dict[str, deque[float]] = {}
        self._last_nav: dict[str, float] = {}
        self._lock = threading.Lock()
        self._alerts: deque[ConcentrationAlert] = deque(maxlen=10_000)

    def update(self, strategy_navs: dict[str, float]) -> list[ConcentrationAlert]:
        """Update with current strategy NAVs and compute correlations.

        Args:
            strategy_navs: Mapping of strategy_name -> current NAV.

        Returns:
            New correlation alerts (if any pairs exceed threshold).
        """
        new_alerts: list[ConcentrationAlert] = []
        now = time.time()

        with self._lock:
            # Compute returns
            for name, nav in strategy_navs.items():
                if name not in self._returns:
                    self._returns[name] = deque(maxlen=self._lookback)
                    self._last_nav[name] = nav
                    continue

                prev = self._last_nav[name]
                if prev > 0:
                    ret = (nav - prev) / prev
                    self._returns[name].append(ret)
                self._last_nav[name] = nav

            # Check pairwise correlations
            names = [n for n in self._returns if len(self._returns[n]) >= 10]
            for i in range(len(names)):
                for j in range(i + 1, len(names)):
                    a, b = names[i], names[j]
                    corr = self._pearson(
                        list(self._returns[a]),
                        list(self._returns[b]),
                    )
                    if corr is not None and abs(corr) >= self._max_correlation:
                        alert = ConcentrationAlert(
                            strategy_a=a,
                            strategy_b=b,
                            correlation=corr,
                            threshold=self._max_correlation,
                            timestamp=now,
                            message=(
                                f"Strategies '{a}' and '{b}' have correlation "
                                f"{corr:.2f} (threshold: {self._max_correlation:.2f})"
                            ),
                        )
                        new_alerts.append(alert)
                        logger.warning(
                            "Correlation alert: %s / %s = %.2f",
                            a, b, corr,
                        )

        self._alerts.extend(new_alerts)
        return new_alerts

    def correlation_matrix(self) -> dict[str, dict[str, float]]:
        """Compute full pairwise correlation matrix."""
        with self._lock:
            names = [n for n in self._returns if len(self._returns[n]) >= 10]
            matrix: dict[str, dict[str, float]] = {}
            for a in names:
                matrix[a] = {}
                for b in names:
                    if a == b:
                        matrix[a][b] = 1.0
                    else:
                        corr = self._pearson(
                            list(self._returns[a]),
                            list(self._returns[b]),
                        )
                        matrix[a][b] = corr if corr is not None else 0.0
        return matrix

    def pairs(self) -> list[CorrelationPair]:
        """Return all unique pairwise correlations."""
        now = time.time()
        result: list[CorrelationPair] = []
        with self._lock:
            names = [n for n in self._returns if len(self._returns[n]) >= 10]
            for i in range(len(names)):
                for j in range(i + 1, len(names)):
                    a, b = names[i], names[j]
                    ra, rb = list(self._returns[a]), list(self._returns[b])
                    n_obs = min(len(ra), len(rb))
                    corr = self._pearson(ra, rb)
                    if corr is not None:
                        result.append(CorrelationPair(
                            strategy_a=a, strategy_b=b,
                            correlation=corr, n_observations=n_obs,
                            timestamp=now,
                        ))
        return result

    def remove_strategy(self, name: str) -> None:
        """Remove a strategy from tracking."""
        with self._lock:
            self._returns.pop(name, None)
            self._last_nav.pop(name, None)

    def recent_alerts(self, limit: int = 20) -> list[ConcentrationAlert]:
        return list(self._alerts)[-limit:]

    @staticmethod
    def _pearson(xs: list[float], ys: list[float]) -> float | None:
        """Compute Pearson correlation coefficient."""
        n = min(len(xs), len(ys))
        if n < 5:
            return None

        xs, ys = xs[-n:], ys[-n:]
        mx = sum(xs) / n
        my = sum(ys) / n

        cov = sum((x - mx) * (y - my) for x, y in zip(xs, ys))
        sx = math.sqrt(sum((x - mx) ** 2 for x in xs))
        sy = math.sqrt(sum((y - my) ** 2 for y in ys))

        if sx < 1e-12 or sy < 1e-12:
            return None

        return cov / (sx * sy)
