"""Agent ratings from Python benchmarks.

Tracks and manages ELO ratings for ML agents, generated by:
    essence-wars benchmark

Agent ratings represent the relative strength of different AI implementations
(e.g., PPO, AlphaZero, Behavioral Cloning) against baseline opponents.
"""

from __future__ import annotations

import json
from dataclasses import dataclass, field
from datetime import datetime
from pathlib import Path
from typing import Any

from .base import BaseRating, RatingCategory, expected_score

# Default path for agent ratings
DEFAULT_AGENT_RATINGS_PATH = Path("data/ratings/agent_elo.json")


@dataclass
class AgentRating(BaseRating):
    """ELO rating for a single agent.

    Attributes:
        agent_id: Unique identifier (e.g., "ppo_v1", "mcts_100")
        rating: Current ELO rating
        games: Total games played (games_played in original)
        wins: Number of wins
        losses: Number of losses
        draws: Number of draws
        agent_type: Type of agent (ppo, alphazero, mcts, greedy, random)
        model_path: Path to model checkpoint (if applicable)
    """

    agent_id: str = ""
    agent_type: str | None = None
    model_path: str | None = None
    metadata: dict[str, Any] = field(default_factory=dict)

    @property
    def display_name(self) -> str:
        """Human-readable name for display."""
        return self.agent_id

    @property
    def category(self) -> RatingCategory:
        """Rating category."""
        return RatingCategory.AGENT

    @property
    def identifier(self) -> str:
        """Unique identifier."""
        return self.agent_id


class AgentRatings:
    """Manager for agent ratings from Python benchmarks.

    Provides both loading from file and runtime tracking capabilities.

    Example:
        >>> # Load existing ratings
        >>> ratings = AgentRatings.load()
        >>> print(ratings.get_ranked())

        >>> # Track new games
        >>> ratings = AgentRatings()
        >>> ratings.update("my_ppo", "greedy", winner=0)  # my_ppo won
        >>> ratings.update("my_ppo", "mcts_100", winner=1)  # mcts won
        >>> ratings.save()
    """

    def __init__(
        self,
        ratings: dict[str, AgentRating] | None = None,
        k_factor: float = 32.0,
        initial_rating: float = 1500.0,
    ):
        """Initialize agent ratings.

        Args:
            ratings: Dictionary of agent_id -> AgentRating
            k_factor: Maximum rating change per game
            initial_rating: Starting rating for new agents
        """
        self.ratings = ratings or {}
        self.k_factor = k_factor
        self.initial_rating = initial_rating
        self.last_updated = datetime.now()

    @classmethod
    def load(cls, path: Path | str | None = None) -> AgentRatings:
        """Load agent ratings from JSON file.

        Args:
            path: Path to agent_elo.json. Defaults to data/ratings/agent_elo.json.

        Returns:
            AgentRatings instance with loaded data.

        Raises:
            FileNotFoundError: If ratings file not found.
        """
        path = Path(path) if path else DEFAULT_AGENT_RATINGS_PATH

        if not path.exists():
            raise FileNotFoundError(f"Agent ratings file not found: {path}")

        with path.open() as f:
            data = json.load(f)

        ratings = {}
        for agent_id, rating_data in data.get("ratings", {}).items():
            ratings[agent_id] = AgentRating(
                rating=rating_data.get("rating", 1500.0),
                games=rating_data.get("games_played", rating_data.get("games", 0)),
                wins=rating_data.get("wins", 0),
                losses=rating_data.get("losses", 0),
                draws=rating_data.get("draws", 0),
                agent_id=agent_id,
                agent_type=rating_data.get("agent_type"),
                model_path=rating_data.get("model_path"),
                metadata=rating_data.get("metadata", {}),
            )

        instance = cls(
            ratings=ratings,
            k_factor=data.get("k_factor", 32.0),
            initial_rating=data.get("initial_rating", 1500.0),
        )

        # Parse last_updated if present
        last_updated_str = data.get("last_updated", "")
        try:
            instance.last_updated = datetime.fromisoformat(
                last_updated_str.replace("Z", "+00:00")
            )
        except (ValueError, AttributeError):
            instance.last_updated = datetime.now()

        return instance

    @classmethod
    def empty(cls) -> AgentRatings:
        """Create an empty AgentRatings instance."""
        return cls()

    def _ensure_agent(
        self,
        agent_id: str,
        agent_type: str | None = None,
        model_path: str | None = None,
    ) -> None:
        """Ensure agent exists in ratings dict."""
        if agent_id not in self.ratings:
            self.ratings[agent_id] = AgentRating(
                rating=self.initial_rating,
                games=0,
                wins=0,
                losses=0,
                draws=0,
                agent_id=agent_id,
                agent_type=agent_type,
                model_path=model_path,
            )

    def update(
        self,
        agent1: str,
        agent2: str,
        winner: int,
        agent1_type: str | None = None,
        agent2_type: str | None = None,
    ) -> tuple[float, float]:
        """Update ratings after a game.

        Args:
            agent1: Name of first agent
            agent2: Name of second agent
            winner: 0 if agent1 won, 1 if agent2 won, -1 for draw
            agent1_type: Optional agent type for agent1
            agent2_type: Optional agent type for agent2

        Returns:
            Tuple of (agent1_new_rating, agent2_new_rating)
        """
        self._ensure_agent(agent1, agent1_type)
        self._ensure_agent(agent2, agent2_type)

        r1 = self.ratings[agent1]
        r2 = self.ratings[agent2]

        # Calculate expected scores
        e1 = expected_score(r1.rating, r2.rating)

        # Determine actual scores
        if winner == 0:
            s1, s2 = 1.0, 0.0
            r1.wins += 1
            r2.losses += 1
        elif winner == 1:
            s1, s2 = 0.0, 1.0
            r1.losses += 1
            r2.wins += 1
        else:  # Draw
            s1, s2 = 0.5, 0.5
            r1.draws += 1
            r2.draws += 1

        # Update ratings
        r1.rating += self.k_factor * (s1 - e1)
        r2.rating += self.k_factor * (s2 - (1.0 - e1))

        # Update game counts
        r1.games += 1
        r2.games += 1

        self.last_updated = datetime.now()

        return r1.rating, r2.rating

    def get_ranked(self) -> list[AgentRating]:
        """Get all agents sorted by rating (descending)."""
        return sorted(self.ratings.values(), key=lambda a: a.rating, reverse=True)

    def get_by_type(self, agent_type: str) -> list[AgentRating]:
        """Get agents of a specific type, sorted by rating."""
        return sorted(
            [a for a in self.ratings.values() if a.agent_type == agent_type],
            key=lambda a: a.rating,
            reverse=True,
        )

    def get(self, agent_id: str) -> AgentRating | None:
        """Get rating for a specific agent."""
        return self.ratings.get(agent_id)

    def predict_matchup(self, agent1_id: str, agent2_id: str) -> float:
        """Predict win probability for agent1 vs agent2 based on ELO.

        Returns:
            Expected win probability for agent1 (0 to 1).
        """
        a1 = self.ratings.get(agent1_id)
        a2 = self.ratings.get(agent2_id)

        if not a1 or not a2:
            return 0.5

        return expected_score(a1.rating, a2.rating)

    def save(self, path: Path | str | None = None) -> None:
        """Save ratings to JSON file.

        Args:
            path: Output path. Defaults to data/ratings/agent_elo.json.
        """
        path = Path(path) if path else DEFAULT_AGENT_RATINGS_PATH
        path.parent.mkdir(parents=True, exist_ok=True)

        data = {
            "version": 1,
            "last_updated": self.last_updated.isoformat(),
            "k_factor": self.k_factor,
            "initial_rating": self.initial_rating,
            "ratings": {
                agent_id: {
                    "rating": r.rating,
                    "games_played": r.games,
                    "wins": r.wins,
                    "losses": r.losses,
                    "draws": r.draws,
                    "agent_type": r.agent_type,
                    "model_path": r.model_path,
                    "metadata": r.metadata,
                }
                for agent_id, r in self.ratings.items()
            },
        }

        with path.open("w") as f:
            json.dump(data, f, indent=2)

    def leaderboard(self) -> list[tuple[str, float, int]]:
        """Get sorted leaderboard.

        Returns:
            List of (name, rating, games_played) tuples.
        """
        return sorted(
            [(agent_id, r.rating, r.games) for agent_id, r in self.ratings.items()],
            key=lambda x: x[1],
            reverse=True,
        )

    def __len__(self) -> int:
        return len(self.ratings)

    def __repr__(self) -> str:
        total_games = sum(r.games for r in self.ratings.values()) // 2
        return f"AgentRatings({len(self.ratings)} agents, {total_games} games)"


def agent_ratings_exist(path: Path | str | None = None) -> bool:
    """Check if agent ratings file exists."""
    path = Path(path) if path else DEFAULT_AGENT_RATINGS_PATH
    return path.exists()
