"""Market universe management: discovery, filtering, tracking, and lifecycle."""

from __future__ import annotations

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

from horizon.fund._types import MarketScore

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


@dataclass
class UniverseEntry:
    """A market tracked in the active universe."""
    market_id: str
    exchange: str
    name: str
    added_at: float
    score: MarketScore | None = None
    assigned_strategy: str | None = None
    sector: str = ""
    last_refreshed: float = 0.0
    active: bool = True


@dataclass
class UniverseConfig:
    """Configuration for market universe management."""
    exchanges: list[str] = field(default_factory=lambda: ["polymarket"])
    min_volume_24h: float = 1000.0
    min_composite_score: float = 0.3
    max_markets: int = 100
    refresh_interval_secs: float = 300.0
    excluded_categories: list[str] = field(default_factory=list)
    excluded_market_ids: list[str] = field(default_factory=list)


class MarketUniverse:
    """Manages the set of markets the fund actively tracks and trades.

    Composes horizon.discovery for market discovery and the fund's
    MarketScorer for fitness evaluation. Maintains a filtered, scored
    universe that the research pipeline draws from.
    """

    def __init__(self, config: UniverseConfig | None = None) -> None:
        self._config = config or UniverseConfig()
        self._entries: dict[str, UniverseEntry] = {}
        self._lock = threading.RLock()
        self._last_refresh = 0.0
        self._removal_log: deque[dict[str, Any]] = deque(maxlen=5_000)

    @property
    def size(self) -> int:
        with self._lock:
            return sum(1 for e in self._entries.values() if e.active)

    def active_markets(self) -> list[UniverseEntry]:
        """Return all active markets in the universe."""
        with self._lock:
            return [e for e in self._entries.values() if e.active]

    def get(self, market_id: str) -> UniverseEntry | None:
        with self._lock:
            return self._entries.get(market_id)

    def refresh(self, scorer: Any = None) -> dict[str, Any]:
        """Refresh the universe: discover new markets, rescore existing, prune stale.

        Args:
            scorer: MarketScorer instance for fitness evaluation.

        Returns:
            Summary of changes: added, removed, rescored counts.
        """
        from horizon.discovery import discover_markets

        now = time.time()
        added = 0
        removed = 0
        rescored = 0

        for exchange in self._config.exchanges:
            try:
                markets = discover_markets(
                    exchange=exchange,
                    active=True,
                    limit=self._config.max_markets,
                    min_volume=self._config.min_volume_24h,
                    sort_by="volume",
                )
            except Exception as e:
                logger.warning("Universe refresh failed for %s: %s", exchange, e)
                continue

            for market in markets:
                mid = market.id
                if self._is_excluded(mid, getattr(market, "category", "")):
                    continue

                with self._lock:
                    if mid in self._entries:
                        self._entries[mid].last_refreshed = now
                        rescored += 1
                    else:
                        if self.size >= self._config.max_markets:
                            continue
                        entry = UniverseEntry(
                            market_id=mid,
                            exchange=exchange,
                            name=getattr(market, "name", mid),
                            added_at=now,
                            last_refreshed=now,
                            sector=getattr(market, "category", ""),
                        )
                        self._entries[mid] = entry
                        added += 1

        # Prune markets that weren't found in refresh (stale)
        stale_threshold = now - self._config.refresh_interval_secs * 3
        with self._lock:
            for mid, entry in list(self._entries.items()):
                if entry.active and entry.last_refreshed < stale_threshold:
                    entry.active = False
                    removed += 1
                    self._removal_log.append({
                        "market_id": mid,
                        "reason": "stale",
                        "timestamp": now,
                    })

        self._last_refresh = now
        summary = {"added": added, "removed": removed, "rescored": rescored, "total": self.size}
        logger.info("Universe refresh: +%d -%d ~%d = %d", added, removed, rescored, self.size)
        return summary

    def add_market(
        self,
        market_id: str,
        exchange: str,
        name: str = "",
        sector: str = "",
    ) -> None:
        """Manually add a market to the universe."""
        with self._lock:
            if market_id in self._entries:
                self._entries[market_id].active = True
                return
            if self.size >= self._config.max_markets:
                raise ValueError(f"Universe at capacity ({self._config.max_markets})")
            self._entries[market_id] = UniverseEntry(
                market_id=market_id,
                exchange=exchange,
                name=name or market_id,
                added_at=time.time(),
                last_refreshed=time.time(),
                sector=sector,
            )

    def remove_market(self, market_id: str, reason: str = "manual") -> None:
        """Remove a market from the active universe."""
        with self._lock:
            entry = self._entries.get(market_id)
            if entry is not None:
                entry.active = False
                self._removal_log.append({
                    "market_id": market_id,
                    "reason": reason,
                    "timestamp": time.time(),
                })

    def assign_strategy(self, market_id: str, strategy_name: str) -> None:
        """Assign a strategy to a market."""
        with self._lock:
            entry = self._entries.get(market_id)
            if entry is not None:
                entry.assigned_strategy = strategy_name

    def unassigned_markets(self) -> list[UniverseEntry]:
        """Return active markets with no assigned strategy."""
        with self._lock:
            return [
                e for e in self._entries.values()
                if e.active and e.assigned_strategy is None
            ]

    def by_sector(self) -> dict[str, list[UniverseEntry]]:
        """Group active markets by sector."""
        result: dict[str, list[UniverseEntry]] = {}
        with self._lock:
            for e in self._entries.values():
                if e.active:
                    result.setdefault(e.sector or "uncategorized", []).append(e)
        return result

    def update_scores(self, scores: dict[str, MarketScore]) -> None:
        """Batch update fitness scores for markets."""
        with self._lock:
            for mid, score in scores.items():
                entry = self._entries.get(mid)
                if entry is not None:
                    entry.score = score
                    if score.composite_score < self._config.min_composite_score:
                        entry.active = False
                        self._removal_log.append({
                            "market_id": mid,
                            "reason": "low_score",
                            "score": score.composite_score,
                            "timestamp": time.time(),
                        })

    def ranked(self, top_n: int = 20) -> list[UniverseEntry]:
        """Return active markets ranked by composite score."""
        active = self.active_markets()
        return sorted(
            active,
            key=lambda e: e.score.composite_score if e.score else 0.0,
            reverse=True,
        )[:top_n]

    def needs_refresh(self) -> bool:
        return time.time() - self._last_refresh > self._config.refresh_interval_secs

    def removal_history(self, limit: int = 50) -> list[dict[str, Any]]:
        return list(self._removal_log)[-limit:]

    def _is_excluded(self, market_id: str, category: str) -> bool:
        if market_id in self._config.excluded_market_ids:
            return True
        if category and category.lower() in [c.lower() for c in self._config.excluded_categories]:
            return True
        return False

    def digest(self) -> str:
        """Compute a SHA-256 digest of the current universe state for change detection."""
        with self._lock:
            ids = sorted(mid for mid, e in self._entries.items() if e.active)
        return hashlib.sha256("|".join(ids).encode()).hexdigest()[:16]
