"""Event-driven research triggers, cross-market detection, and research attribution."""

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.research_intel")

# Trigger type constants
TRIGGER_REGIME_CHANGE = "regime_change"
TRIGGER_CROSS_MARKET_DIVERGENCE = "cross_market_divergence"
TRIGGER_EDGE_REFRESH = "edge_refresh"
TRIGGER_NEW_MARKET = "new_market"
TRIGGER_VOLUME_SPIKE = "volume_spike"
TRIGGER_SETTLEMENT_CASCADE = "settlement_cascade"


@dataclass(frozen=True)
class ResearchTrigger:
    """An event that should prompt research activity."""
    trigger_type: str
    market_id: str
    priority: int  # 1=highest, 5=lowest
    timestamp: float
    source_data: dict[str, Any] = field(default_factory=dict)


@dataclass(frozen=True)
class CrossMarketOpportunity:
    """A detected divergence between two correlated markets."""
    market_a: str
    market_b: str
    divergence: float
    correlation: float
    confidence: float
    timestamp: float


@dataclass(frozen=True)
class ResearchAttribution:
    """Links a strategy outcome back to the research trigger that spawned it."""
    strategy_name: str
    hypothesis_id: str
    trigger_type: str
    signals_used: list[str]
    pnl: float
    information_ratio: float


class ResearchIntelligence:
    """Manages event-driven research triggers, cross-market divergence
    detection, and attribution of strategy outcomes back to research signals.

    Trigger types:
    - regime_change: market regime transition detected (priority 1)
    - cross_market_divergence: correlated markets diverged (priority 2)
    - edge_refresh: existing edge may have decayed (priority 3)
    - new_market: new tradeable market appeared (priority 3)
    - volume_spike: unusual volume detected (priority 2)
    - settlement_cascade: significant settlement triggers review (priority 2)
    """

    def __init__(
        self,
        divergence_threshold: float = 0.05,
        correlation_lookback: int = 50,
    ) -> None:
        self._divergence_threshold = divergence_threshold
        self._correlation_lookback = correlation_lookback

        self._triggers: deque[ResearchTrigger] = deque(maxlen=10_000)
        self._consumed: deque[ResearchTrigger] = deque(maxlen=10_000)
        self._cross_market_prices: dict[str, deque[float]] = {}
        self._market_pairs: list[tuple[str, str]] = []
        self._attributions: deque[ResearchAttribution] = deque(maxlen=10_000)
        self._trigger_pnl: dict[str, list[float]] = {}
        self._lock = threading.Lock()

    # ------------------------------------------------------------------
    # Event handlers
    # ------------------------------------------------------------------

    def on_regime_change(self, old_regime: str, new_regime: str) -> None:
        """Emit a regime_change trigger with highest priority."""
        trigger = ResearchTrigger(
            trigger_type=TRIGGER_REGIME_CHANGE,
            market_id="*",
            priority=1,
            timestamp=time.time(),
            source_data={"old_regime": old_regime, "new_regime": new_regime},
        )
        self.emit_trigger(trigger)
        logger.info(
            "Regime change trigger: %s -> %s",
            old_regime, new_regime,
        )

    def on_settlement(self, market_id: str, pnl: float) -> None:
        """Emit a settlement_cascade trigger if PnL is significant."""
        if abs(pnl) < 1e-9:
            return
        trigger = ResearchTrigger(
            trigger_type=TRIGGER_SETTLEMENT_CASCADE,
            market_id=market_id,
            priority=2,
            timestamp=time.time(),
            source_data={"pnl": pnl},
        )
        self.emit_trigger(trigger)
        logger.info(
            "Settlement cascade trigger: market=%s pnl=%.4f",
            market_id, pnl,
        )

    def on_new_market(self, market_id: str) -> None:
        """Emit a new_market trigger."""
        trigger = ResearchTrigger(
            trigger_type=TRIGGER_NEW_MARKET,
            market_id=market_id,
            priority=3,
            timestamp=time.time(),
            source_data={},
        )
        self.emit_trigger(trigger)
        logger.info("New market trigger: %s", market_id)

    def on_volume_spike(
        self,
        market_id: str,
        volume: float,
        avg_volume: float,
    ) -> None:
        """Emit a volume_spike trigger if volume exceeds 2x the average."""
        if avg_volume <= 0 or volume <= 2.0 * avg_volume:
            return
        trigger = ResearchTrigger(
            trigger_type=TRIGGER_VOLUME_SPIKE,
            market_id=market_id,
            priority=2,
            timestamp=time.time(),
            source_data={
                "volume": volume,
                "avg_volume": avg_volume,
                "ratio": volume / avg_volume,
            },
        )
        self.emit_trigger(trigger)
        logger.info(
            "Volume spike trigger: market=%s ratio=%.1fx",
            market_id, volume / avg_volume,
        )

    # ------------------------------------------------------------------
    # Cross-market monitoring
    # ------------------------------------------------------------------

    def register_pair(self, market_a: str, market_b: str) -> None:
        """Register a cross-market pair to monitor for divergences."""
        with self._lock:
            pair = (market_a, market_b)
            if pair not in self._market_pairs:
                self._market_pairs.append(pair)
                logger.debug("Registered cross-market pair: %s / %s", market_a, market_b)

    def update_prices(self, prices: dict[str, float]) -> None:
        """Update rolling prices and check registered pairs for divergences.

        If the absolute price difference between two paired markets exceeds
        the divergence threshold, a cross_market_divergence trigger is emitted
        with a CrossMarketOpportunity in source_data.
        """
        with self._lock:
            # Update rolling price windows
            for market_id, price in prices.items():
                if not math.isfinite(price):
                    continue
                if market_id not in self._cross_market_prices:
                    self._cross_market_prices[market_id] = deque(
                        maxlen=self._correlation_lookback,
                    )
                self._cross_market_prices[market_id].append(price)

            # Check registered pairs for divergences
            now = time.time()
            for market_a, market_b in self._market_pairs:
                if market_a not in self._cross_market_prices:
                    continue
                if market_b not in self._cross_market_prices:
                    continue

                prices_a = self._cross_market_prices[market_a]
                prices_b = self._cross_market_prices[market_b]

                if len(prices_a) == 0 or len(prices_b) == 0:
                    continue

                price_a = prices_a[-1]
                price_b = prices_b[-1]
                divergence = abs(price_a - price_b)

                if divergence <= self._divergence_threshold:
                    continue

                # Compute correlation if we have enough history
                correlation = self._pearson(
                    list(prices_a), list(prices_b),
                )
                if correlation is None:
                    correlation = 0.0

                # Higher correlation + higher divergence = higher confidence
                confidence = min(1.0, abs(correlation) * (divergence / self._divergence_threshold) * 0.5)

                opportunity = CrossMarketOpportunity(
                    market_a=market_a,
                    market_b=market_b,
                    divergence=divergence,
                    correlation=correlation,
                    confidence=confidence,
                    timestamp=now,
                )

                trigger = ResearchTrigger(
                    trigger_type=TRIGGER_CROSS_MARKET_DIVERGENCE,
                    market_id=f"{market_a}:{market_b}",
                    priority=2,
                    timestamp=now,
                    source_data={
                        "opportunity": {
                            "market_a": opportunity.market_a,
                            "market_b": opportunity.market_b,
                            "divergence": opportunity.divergence,
                            "correlation": opportunity.correlation,
                            "confidence": opportunity.confidence,
                            "timestamp": opportunity.timestamp,
                        },
                    },
                )
                # Emit without lock to avoid deadlock (emit_trigger acquires lock)
                self._triggers.append(trigger)
                logger.info(
                    "Cross-market divergence: %s / %s divergence=%.4f corr=%.2f",
                    market_a, market_b, divergence, correlation,
                )

    # ------------------------------------------------------------------
    # Trigger queue
    # ------------------------------------------------------------------

    def emit_trigger(self, trigger: ResearchTrigger) -> None:
        """Add a trigger to the pending queue, maintaining priority order."""
        with self._lock:
            self._triggers.append(trigger)

    def consume_triggers(self, max_count: int = 5) -> list[ResearchTrigger]:
        """Pop the highest-priority pending triggers.

        Returns up to max_count triggers sorted by priority (1=highest).
        Consumed triggers are moved to the consumed deque.
        """
        with self._lock:
            # Sort all pending by priority (stable sort preserves insertion order for ties)
            pending = sorted(self._triggers, key=lambda t: t.priority)
            to_consume = pending[:max_count]
            remaining = pending[max_count:]

            self._triggers.clear()
            self._triggers.extend(remaining)

            for t in to_consume:
                self._consumed.append(t)

            return to_consume

    def pending_count(self) -> int:
        """Return the number of pending triggers."""
        with self._lock:
            return len(self._triggers)

    # ------------------------------------------------------------------
    # Attribution
    # ------------------------------------------------------------------

    def record_attribution(
        self,
        strategy_name: str,
        hypothesis_id: str,
        trigger_type: str,
        signals_used: list[str],
        pnl: float,
        information_ratio: float,
    ) -> None:
        """Record a research attribution for tracking effectiveness."""
        attr = ResearchAttribution(
            strategy_name=strategy_name,
            hypothesis_id=hypothesis_id,
            trigger_type=trigger_type,
            signals_used=signals_used,
            pnl=pnl,
            information_ratio=information_ratio,
        )
        with self._lock:
            self._attributions.append(attr)
            if trigger_type not in self._trigger_pnl:
                self._trigger_pnl[trigger_type] = []
            self._trigger_pnl[trigger_type].append(pnl)
        logger.debug(
            "Attribution recorded: strategy=%s trigger=%s pnl=%.4f",
            strategy_name, trigger_type, pnl,
        )

    def trigger_effectiveness(self) -> dict[str, dict[str, float]]:
        """Return effectiveness metrics per trigger type.

        Returns:
            {trigger_type: {count, avg_pnl, total_pnl}}
        """
        with self._lock:
            result: dict[str, dict[str, float]] = {}
            for trigger_type, pnls in self._trigger_pnl.items():
                count = len(pnls)
                total_pnl = sum(pnls)
                avg_pnl = total_pnl / count if count > 0 else 0.0
                result[trigger_type] = {
                    "count": count,
                    "avg_pnl": avg_pnl,
                    "total_pnl": total_pnl,
                }
            return result

    # ------------------------------------------------------------------
    # Queries
    # ------------------------------------------------------------------

    def recent_triggers(self, limit: int = 20) -> list[ResearchTrigger]:
        """Return the most recent pending triggers."""
        with self._lock:
            return list(self._triggers)[-limit:]

    def recent_attributions(self, limit: int = 20) -> list[ResearchAttribution]:
        """Return the most recent attributions."""
        with self._lock:
            return list(self._attributions)[-limit:]

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

    @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)
