"""Base types for the effect handler system.

This module defines the core types used by all effect handlers:
- EffectContext: The context object passed to every effect handler
- EffectResult: The result returned by effect handlers
- EffectType: Categorization of effects for UI/logging

EffectContext provides a clean interface for effect handlers to interact with
the game state without needing to know the internal structure. It includes
helper methods for common operations like finding cards and getting definitions.

Usage:
    @effect_handler("deal_damage")
    def handle_deal_damage(ctx: EffectContext) -> EffectResult:
        target = ctx.get_target_pokemon()
        if target is None:
            return EffectResult.failure("No target specified")

        amount = ctx.params.get("amount", 0)
        target.damage += amount

        return EffectResult.success(f"Dealt {amount} damage")
"""

from enum import StrEnum
from typing import TYPE_CHECKING, Any

from pydantic import BaseModel, Field

if TYPE_CHECKING:
    from app.core.models.card import CardDefinition, CardInstance
    from app.core.models.game_state import GameState, PlayerState
    from app.core.rng import RandomProvider


class EffectType(StrEnum):
    """Categories of effects for logging and UI purposes.

    These help the game engine and UI understand what kind of effect
    occurred without parsing the effect_id.
    """

    DAMAGE = "damage"  # Deals damage to a Pokemon
    HEAL = "heal"  # Heals damage from a Pokemon
    DRAW = "draw"  # Draws cards from deck
    DISCARD = "discard"  # Discards cards
    SEARCH = "search"  # Searches deck for cards
    SHUFFLE = "shuffle"  # Shuffles deck
    STATUS = "status"  # Applies or removes status conditions
    ENERGY = "energy"  # Attaches, moves, or discards energy
    BENCH = "bench"  # Affects bench (swap, damage, etc.)
    MODIFIER = "modifier"  # Modifies stats (HP, damage, costs)
    COIN_FLIP = "coin_flip"  # Coin flip with conditional effect
    SPECIAL = "special"  # Unique effects that don't fit other categories


class EffectResult(BaseModel):
    """Result returned by an effect handler.

    Effect handlers return this to indicate success/failure and provide
    information for logging, UI updates, and debugging.

    Attributes:
        success: Whether the effect was successfully applied.
        message: Human-readable description of what happened.
        effect_type: Category of the effect for UI/logging.
        details: Additional structured data about the effect (for logging/replay).
        state_changes: List of state changes made (for undo/replay support).
    """

    success: bool
    message: str
    effect_type: EffectType | None = None
    details: dict[str, Any] = Field(default_factory=dict)
    state_changes: list[dict[str, Any]] = Field(default_factory=list)

    @classmethod
    def success_result(
        cls,
        message: str,
        effect_type: EffectType | None = None,
        details: dict[str, Any] | None = None,
    ) -> "EffectResult":
        """Create a successful result.

        Args:
            message: Description of what happened.
            effect_type: Category of the effect.
            details: Additional structured data.

        Returns:
            An EffectResult with success=True.
        """
        return cls(
            success=True,
            message=message,
            effect_type=effect_type,
            details=details or {},
        )

    @classmethod
    def failure(cls, message: str) -> "EffectResult":
        """Create a failure result.

        Args:
            message: Description of why the effect failed.

        Returns:
            An EffectResult with success=False.
        """
        return cls(success=False, message=message)


class EffectContext:
    """Context object passed to effect handlers.

    Provides access to the game state and helper methods for common operations.
    Effect handlers should use this rather than directly manipulating game state
    when possible, as it provides a cleaner interface and enables logging.

    The context is created fresh for each effect execution and is not persisted.

    Attributes:
        game: The current game state (mutable - handlers modify this).
        source_player_id: ID of the player who triggered the effect.
        source_card_id: Instance ID of the card that triggered the effect (if any).
        target_player_id: ID of the target player (if applicable).
        target_card_id: Instance ID of the target card (if applicable).
        params: Effect-specific parameters from the card/attack definition.
        rng: Random number generator for coin flips, etc.

    Example:
        ctx = EffectContext(
            game=game_state,
            source_player_id="player1",
            source_card_id="pikachu-uuid",
            target_card_id="charmander-uuid",
            params={"amount": 30},
            rng=rng,
        )
        result = deal_damage_handler(ctx)
    """

    def __init__(
        self,
        game: "GameState",
        source_player_id: str,
        rng: "RandomProvider",
        source_card_id: str | None = None,
        target_player_id: str | None = None,
        target_card_id: str | None = None,
        params: dict[str, Any] | None = None,
    ) -> None:
        """Initialize an effect context.

        Args:
            game: The current game state.
            source_player_id: ID of the player triggering the effect.
            rng: Random number generator for this effect.
            source_card_id: Instance ID of the source card (optional).
            target_player_id: ID of the target player (optional).
            target_card_id: Instance ID of the target card (optional).
            params: Effect-specific parameters (optional).
        """
        self.game = game
        self.source_player_id = source_player_id
        self.source_card_id = source_card_id
        self.target_player_id = target_player_id
        self.target_card_id = target_card_id
        self.params = params or {}
        self.rng = rng

    # =========================================================================
    # Player Access
    # =========================================================================

    def get_source_player(self) -> "PlayerState":
        """Get the player who triggered this effect.

        Returns:
            The source player's state.

        Raises:
            KeyError: If source_player_id is not in the game.
        """
        return self.game.players[self.source_player_id]

    def get_target_player(self) -> "PlayerState | None":
        """Get the target player, if specified.

        Returns:
            The target player's state, or None if no target specified.
        """
        if self.target_player_id is None:
            return None
        return self.game.players.get(self.target_player_id)

    def get_opponent(self) -> "PlayerState":
        """Get the opponent of the source player.

        Convenience method for 2-player games.

        Returns:
            The opponent's player state.
        """
        return self.game.get_opponent(self.source_player_id)

    def get_opponent_id(self) -> str:
        """Get the opponent's player ID.

        Returns:
            The opponent's player ID.
        """
        return self.game.get_opponent_id(self.source_player_id)

    # =========================================================================
    # Card Access
    # =========================================================================

    def get_source_card(self) -> "CardInstance | None":
        """Get the card that triggered this effect.

        Returns:
            The source CardInstance, or None if no source card.
        """
        if self.source_card_id is None:
            return None
        card, _ = self.game.find_card_instance(self.source_card_id)
        return card

    def get_target_card(self) -> "CardInstance | None":
        """Get the target card, if specified.

        Returns:
            The target CardInstance, or None if no target specified.
        """
        if self.target_card_id is None:
            return None
        card, _ = self.game.find_card_instance(self.target_card_id)
        return card

    def get_source_pokemon(self) -> "CardInstance | None":
        """Get the source card if it's the active Pokemon.

        Convenience method for attack effects where the source is the attacker.

        Returns:
            The source player's active Pokemon, or None.
        """
        return self.get_source_player().get_active_pokemon()

    def get_target_pokemon(self) -> "CardInstance | None":
        """Get the target Pokemon.

        If target_card_id is specified, returns that card.
        Otherwise, returns the opponent's active Pokemon (common default).

        Returns:
            The target CardInstance, or None if not found.
        """
        if self.target_card_id:
            return self.get_target_card()
        # Default to opponent's active
        opponent = self.get_opponent()
        return opponent.get_active_pokemon()

    def get_card_definition(self, card: "CardInstance") -> "CardDefinition | None":
        """Look up the definition for a card instance.

        Args:
            card: The CardInstance to look up.

        Returns:
            The CardDefinition, or None if not found in registry.
        """
        return self.game.get_card_definition(card.definition_id)

    def find_card(self, instance_id: str) -> tuple["CardInstance | None", str | None]:
        """Find a card anywhere in the game.

        Args:
            instance_id: The instance ID to search for.

        Returns:
            Tuple of (CardInstance, zone_name) or (None, None) if not found.
        """
        return self.game.find_card_instance(instance_id)

    # =========================================================================
    # Parameter Access
    # =========================================================================

    def get_param(self, key: str, default: Any = None) -> Any:
        """Get an effect parameter with optional default.

        Args:
            key: The parameter key.
            default: Default value if key not present.

        Returns:
            The parameter value or default.
        """
        return self.params.get(key, default)

    def get_int_param(self, key: str, default: int = 0) -> int:
        """Get an integer parameter.

        Args:
            key: The parameter key.
            default: Default value if key not present or not an int.

        Returns:
            The parameter as an integer.
        """
        value = self.params.get(key, default)
        if isinstance(value, int):
            return value
        try:
            return int(value)
        except (TypeError, ValueError):
            return default

    def get_str_param(self, key: str, default: str = "") -> str:
        """Get a string parameter.

        Args:
            key: The parameter key.
            default: Default value if key not present.

        Returns:
            The parameter as a string.
        """
        value = self.params.get(key, default)
        return str(value) if value is not None else default

    # =========================================================================
    # Random Operations
    # =========================================================================

    def flip_coin(self) -> bool:
        """Flip a coin.

        Returns:
            True for heads, False for tails.
        """
        return self.rng.coin_flip()

    def flip_coins(self, count: int) -> list[bool]:
        """Flip multiple coins.

        Args:
            count: Number of coins to flip.

        Returns:
            List of results (True=heads, False=tails).
        """
        return [self.rng.coin_flip() for _ in range(count)]

    def count_heads(self, count: int) -> int:
        """Flip coins and count heads.

        Args:
            count: Number of coins to flip.

        Returns:
            Number of heads.
        """
        return sum(self.flip_coins(count))

    # =========================================================================
    # Rules Access
    # =========================================================================

    @property
    def rules(self):
        """Get the game's rules configuration.

        Returns:
            The RulesConfig for this game.
        """
        return self.game.rules

    @property
    def is_first_turn(self) -> bool:
        """Check if this is the first turn of the game.

        Returns:
            True if this is the first turn.
        """
        return self.game.is_first_turn()
