"""Research pipeline: scan markets, score opportunities, rank for deployment."""

from __future__ import annotations

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

from horizon.fund._types import MarketScore

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


@dataclass(frozen=True)
class Opportunity:
    """A scored trading opportunity."""
    market_id: str
    exchange: str
    market_name: str
    fitness_score: float
    edge_estimate: float
    confidence: float
    strategy_type: str
    rationale: str
    data: dict[str, Any] = field(default_factory=dict)


@dataclass(frozen=True)
class ResearchScan:
    """Results of a research scan."""
    timestamp: float
    markets_scanned: int
    opportunities_found: int
    opportunities: list[Opportunity]
    scan_duration_secs: float


class ResearchPipeline:
    """Scans the market universe for trading opportunities.

    Composes:
    - MarketUniverse for the set of markets to evaluate
    - MarketScorer for fitness scoring
    - Optional LLM edge estimation (via callback)
    - Optional signal aggregation (via callback)

    The pipeline outputs ranked Opportunity objects that the
    autopilot can evaluate for deployment.
    """

    def __init__(
        self,
        min_fitness: float = 0.3,
        min_edge: float = 0.03,
        min_confidence: float = 0.5,
        max_opportunities: int = 20,
    ) -> None:
        self._min_fitness = min_fitness
        self._min_edge = min_edge
        self._min_confidence = min_confidence
        self._max_opportunities = max_opportunities
        self._edge_estimator: Callable[[str, str], tuple[float, float]] | None = None
        self._strategy_matcher: Callable[[MarketScore], str] | None = None
        self._scans: deque[ResearchScan] = deque(maxlen=1_000)

    def set_edge_estimator(
        self,
        fn: Callable[[str, str], tuple[float, float]],
    ) -> None:
        """Set the edge estimation function.

        Args:
            fn: Callable(market_id, exchange) -> (edge, confidence).
                Edge is the estimated mispricing (0-1).
                Confidence is how confident we are (0-1).
        """
        self._edge_estimator = fn

    def set_strategy_matcher(
        self,
        fn: Callable[[MarketScore], str],
    ) -> None:
        """Set the strategy-type matching function.

        Args:
            fn: Callable(MarketScore) -> strategy_type string.
        """
        self._strategy_matcher = fn

    def scan(
        self,
        universe_entries: list[Any],
        scorer: Any = None,
        regime: Any = None,
    ) -> ResearchScan:
        """Scan the universe for opportunities.

        Args:
            universe_entries: List of UniverseEntry objects.
            scorer: MarketScorer instance.
            regime: Optional RegimeSnapshot for regime-aware filtering.

        Returns:
            ResearchScan with ranked opportunities.
        """
        start = time.time()
        opportunities: list[Opportunity] = []

        for entry in universe_entries:
            try:
                opp = self._evaluate_market(entry, scorer, regime=regime)
                if opp is not None:
                    opportunities.append(opp)
            except Exception as e:
                logger.debug("Research scan error for %s: %s", entry.market_id, e)

        # Rank by composite: alpha, fitness, ensemble confidence, regime fit
        def _rank_key(o: Opportunity) -> float:
            regime_fit = 0.5  # default
            if regime is not None:
                rname = getattr(regime, "regime", "")
                if rname in ("trending_up", "trending_down") and o.strategy_type == "directional":
                    regime_fit = 1.0
                elif rname in ("quiet", "mean_reverting") and o.strategy_type == "market_maker":
                    regime_fit = 1.0
                elif rname == "volatile":
                    regime_fit = 0.2  # penalize new entries in volatile regime
            return o.edge_estimate * 0.35 + o.fitness_score * 0.25 + o.confidence * 0.25 + regime_fit * 0.15

        opportunities.sort(key=_rank_key, reverse=True)
        opportunities = opportunities[: self._max_opportunities]

        scan = ResearchScan(
            timestamp=time.time(),
            markets_scanned=len(universe_entries),
            opportunities_found=len(opportunities),
            opportunities=opportunities,
            scan_duration_secs=time.time() - start,
        )
        self._scans.append(scan)
        logger.info(
            "Research scan: %d markets -> %d opportunities in %.1fs",
            len(universe_entries), len(opportunities), scan.scan_duration_secs,
        )
        return scan

    def _evaluate_market(
        self,
        entry: Any,
        scorer: Any,
        regime: Any = None,
    ) -> Opportunity | None:
        """Evaluate a single market for opportunity."""
        market_id = entry.market_id
        exchange = entry.exchange

        # Get fitness score
        fitness = 0.5
        if entry.score is not None:
            fitness = entry.score.composite_score
        elif scorer is not None:
            # Score on the fly
            score = scorer.score_market(
                market_id=market_id,
                exchange=exchange,
                price=0.5,
                volume_24h=0.0,
                spread=0.1,
            )
            fitness = score.composite_score

        if fitness < self._min_fitness:
            return None

        # Estimate edge
        edge = 0.0
        confidence = 0.5
        if self._edge_estimator is not None:
            try:
                edge, confidence = self._edge_estimator(market_id, exchange)
            except Exception:
                edge, confidence = 0.0, 0.3
        else:
            # Default edge estimator: small positive edge based on fitness
            # Higher fitness markets are more likely to have exploitable spreads
            edge = 0.05 * fitness
            confidence = 0.3 + 0.2 * fitness  # 0.3-0.5 range

        if edge < self._min_edge:
            return None
        if confidence < self._min_confidence:
            return None

        # Determine strategy type (regime-aware)
        strategy_type = "market_maker"  # default
        if self._strategy_matcher is not None and entry.score is not None:
            try:
                strategy_type = self._strategy_matcher(entry.score)
            except Exception:
                pass
        elif regime is not None:
            rname = getattr(regime, "regime", "")
            if rname in ("trending_up", "trending_down"):
                strategy_type = "directional"
            elif rname in ("quiet", "mean_reverting"):
                strategy_type = "market_maker"
            elif edge > 0.10:
                strategy_type = "directional"
        elif edge > 0.10:
            strategy_type = "directional"
        elif fitness > 0.7:
            strategy_type = "market_maker"

        rationale = (
            f"Fitness={fitness:.2f}, Edge={edge:.3f}, "
            f"Confidence={confidence:.2f}, Type={strategy_type}"
        )

        return Opportunity(
            market_id=market_id,
            exchange=exchange,
            market_name=getattr(entry, "name", market_id),
            fitness_score=fitness,
            edge_estimate=edge,
            confidence=confidence,
            strategy_type=strategy_type,
            rationale=rationale,
        )

    def recent_scans(self, limit: int = 10) -> list[ResearchScan]:
        return list(self._scans)[-limit:]

    def best_opportunities(self, limit: int = 5) -> list[Opportunity]:
        """Return best opportunities from the most recent scan."""
        if not self._scans:
            return []
        return self._scans[-1].opportunities[:limit]
