from __future__ import annotations

import logging
import uuid
from collections.abc import Callable

from openpoker.ai.ai_agent import AIAgent
from openpoker.core.poker_game import PokerGame
from openpoker.events.emitter import EventCallback, EventEmitter
from openpoker.types import (
    ActionHistoryEntry,
    ActionRequiredPayload,
    AIContext,
    AIDecisionPayload,
    AvailableAction,
    EngineErrorPayload,
    EngineEvent,
    EngineEventType,
    EngineOptions,
    GameConfig,
    GameCreatedPayload,
    GameOverPayload,
    GameSnapshot,
    OpponentView,
    Player,
    PlayerConfig,
    PlayerEliminatedPayload,
    PlayerStatus,
    PlayerType,
    PokerAction,
)

logger = logging.getLogger(__name__)


class PokerEngine:
    def __init__(self, options: EngineOptions | None = None):
        self.options = options or EngineOptions()
        self.emitter = EventEmitter()
        self.games: dict[str, PokerGame] = {}
        self.ai_agent = AIAgent(
            enable_memory=self.options.enable_memory,
            enable_monte_carlo=self.options.enable_monte_carlo,
            debug=self.options.debug,
        )

    def subscribe(self, callback: EventCallback) -> Callable[[], None]:
        return self.emitter.subscribe(callback)

    def get_events(self) -> list[EngineEvent]:
        return self.emitter.get_history()

    def create_game(
        self,
        game_name: str,
        config: GameConfig,
        players: list[PlayerConfig],
        game_id: str | None = None,
    ) -> str:
        resolved_game_id = game_id or str(uuid.uuid4())
        game = PokerGame(
            game_id=resolved_game_id,
            game_name=game_name,
            config=config,
            emitter=self.emitter,
        )

        for idx, player_cfg in enumerate(players):
            stack = (
                player_cfg.starting_stack if player_cfg.starting_stack is not None else config.starting_stack
            )
            game.add_player(
                Player(
                    id=player_cfg.id,
                    name=player_cfg.name,
                    player_type=player_cfg.player_type,
                    provider=player_cfg.provider,
                    model=player_cfg.model,
                    personality=player_cfg.personality,
                    playing_style=player_cfg.playing_style,
                    position=idx,
                    stack=stack,
                    status=PlayerStatus.ACTIVE,
                )
            )

        self.games[resolved_game_id] = game
        self.emitter.emit(
            event_type=EngineEventType.GAME_CREATED,
            game_id=resolved_game_id,
            hand_number=0,
            payload=GameCreatedPayload(
                game_name=game_name,
                players=[player.name for player in game.players],
            ),
            snapshot=game.to_snapshot() if self.options.include_snapshot_in_events else None,
        )
        return resolved_game_id

    def start_hand(self, game_id: str) -> GameSnapshot:
        game = self._get_game(game_id)
        game.start_new_hand()
        self._emit_action_required_if_human(game)
        return game.to_snapshot()

    def get_snapshot(
        self,
        game_id: str,
        viewer_id: str | None = None,
        reveal_all_hole_cards: bool = False,
    ) -> GameSnapshot:
        return self._get_game(game_id).to_snapshot(
            viewer_id=viewer_id,
            reveal_all_hole_cards=reveal_all_hole_cards,
        )

    def get_available_actions(self, game_id: str, player_id: str) -> list[AvailableAction]:
        game = self._get_game(game_id)
        player = self._get_player(game, player_id)
        return game.get_available_actions(player)

    async def submit_action(self, game_id: str, player_id: str, action: PokerAction) -> GameSnapshot:
        game = self._get_game(game_id)
        player = self._get_player(game, player_id)
        success = game.execute_action(player, action)
        if not success:
            self.emitter.emit(
                event_type=EngineEventType.ENGINE_ERROR,
                game_id=game.game_id,
                hand_number=game.hand_number,
                payload=EngineErrorPayload(message="Action execution failed", player_id=player_id),
                snapshot=game.to_snapshot() if self.options.include_snapshot_in_events else None,
            )
            return game.to_snapshot()

        await self._post_action_maintenance(game)
        self._emit_action_required_if_human(game)
        return game.to_snapshot()

    async def step(self, game_id: str) -> GameSnapshot:
        game = self._get_game(game_id)
        if not game.is_hand_active():
            if game.can_start_game():
                game.start_new_hand()
            return game.to_snapshot()

        current_actor = game.get_current_actor()
        if not current_actor:
            return game.to_snapshot()

        if current_actor.player_type == PlayerType.HUMAN:
            self._emit_action_required_if_human(game)
            return game.to_snapshot()

        available_actions = game.get_available_actions(current_actor)
        if not available_actions:
            return game.to_snapshot()

        context = self._build_ai_context(game, current_actor)
        action, details = await self.ai_agent.get_action(
            current_actor,
            context,
            available_actions,
            game,
        )
        if action:
            success = game.execute_action(current_actor, action)
            if not success:
                logger.warning(
                    "AI action failed for player=%s action=%s, game=%s",
                    current_actor.id,
                    action.action_type.value,
                    game_id,
                )
            if details:
                self.emitter.emit(
                    event_type=EngineEventType.AI_DECISION,
                    game_id=game.game_id,
                    hand_number=game.hand_number,
                    payload=AIDecisionPayload(decision=details),
                    snapshot=game.to_snapshot() if self.options.include_snapshot_in_events else None,
                )

            await self._post_action_maintenance(game)
            self._emit_action_required_if_human(game)
        return game.to_snapshot()

    async def run_until_human_turn_or_hand_end(
        self,
        game_id: str,
        max_steps: int = 100,
    ) -> GameSnapshot:
        game = self._get_game(game_id)
        steps = 0
        while steps < max_steps and game.is_hand_active():
            current_actor = game.get_current_actor()
            if not current_actor:
                break
            if current_actor.player_type == PlayerType.HUMAN:
                self._emit_action_required_if_human(game)
                break
            await self.step(game_id)
            steps += 1
        return game.to_snapshot()

    def _build_ai_context(self, game: PokerGame, player: Player) -> AIContext:
        all_players = [p for p in game.players if p.status != PlayerStatus.SITTING_OUT]
        opponents = [
            OpponentView(
                name=p.name,
                position=p.position,
                stack=p.stack,
                current_bet=p.current_bet,
                status=p.status,
                is_dealer=p.is_dealer,
                is_small_blind=p.is_small_blind,
                is_big_blind=p.is_big_blind,
            )
            for p in all_players
            if p.id != player.id
        ]

        call_amount = 0
        min_raise: int | None = None
        max_raise: int | None = None
        if game.state:
            call_amount = game.state.checking_or_calling_amount
            try:
                min_raise = game.state.min_completion_betting_or_raising_to_amount
                max_raise = game.state.max_completion_betting_or_raising_to_amount
            except (AttributeError, ValueError) as exc:
                logger.warning("could not determine raise range for AI context: %s", exc)

        history: list[ActionHistoryEntry] = list(game.action_history)

        return AIContext(
            hole_cards=player.hole_cards or [],
            board_cards=game.get_board_cards(),
            position=player.position,
            stack=player.stack,
            pot=game.get_pot_size(),
            current_bet=player.current_bet,
            call_amount=call_amount,
            min_raise=min_raise,
            max_raise=max_raise,
            opponents=opponents,
            action_history=history,
            street=game.get_current_street(),
            is_dealer=player.is_dealer,
            is_small_blind=player.is_small_blind,
            is_big_blind=player.is_big_blind,
            playing_style=player.playing_style,
        )

    def _emit_action_required_if_human(self, game: PokerGame) -> None:
        current_actor = game.get_current_actor()
        if not current_actor:
            return
        if current_actor.player_type != PlayerType.HUMAN:
            return
        available_actions = game.get_available_actions(current_actor)
        self.emitter.emit(
            event_type=EngineEventType.ACTION_REQUIRED,
            game_id=game.game_id,
            hand_number=game.hand_number,
            payload=ActionRequiredPayload(
                player_id=current_actor.id,
                player_name=current_actor.name,
                available_actions=available_actions,
            ),
            snapshot=game.to_snapshot() if self.options.include_snapshot_in_events else None,
        )

    async def _post_action_maintenance(self, game: PokerGame) -> None:
        if game.is_hand_active():
            return

        eliminations: list[Player] = []
        for player in game.players:
            if player.stack <= 0 and player.status != PlayerStatus.SITTING_OUT:
                player.status = PlayerStatus.SITTING_OUT
                eliminations.append(player)

        for eliminated in eliminations:
            self.emitter.emit(
                event_type=EngineEventType.PLAYER_ELIMINATED,
                game_id=game.game_id,
                hand_number=game.hand_number,
                payload=PlayerEliminatedPayload(player_id=eliminated.id, player_name=eliminated.name),
                snapshot=game.to_snapshot() if self.options.include_snapshot_in_events else None,
            )

        if self.options.enable_memory and game.action_history:
            hand_summary = self._build_hand_summary(game)
            for player in game.players:
                if player.player_type == PlayerType.AI:
                    await self.ai_agent.reflect_on_hand(player=player, hand_summary=hand_summary)

        players_with_chips = [p for p in game.players if p.stack > 0 and p.status != PlayerStatus.SITTING_OUT]
        if len(players_with_chips) == 1:
            winner = players_with_chips[0]
            self.emitter.emit(
                event_type=EngineEventType.GAME_OVER,
                game_id=game.game_id,
                hand_number=game.hand_number,
                payload=GameOverPayload(
                    winner_id=winner.id,
                    winner_name=winner.name,
                    winner_stack=winner.stack,
                ),
                snapshot=game.to_snapshot() if self.options.include_snapshot_in_events else None,
            )

    def _build_hand_summary(self, game: PokerGame) -> str:
        lines = [f"Hand #{game.hand_number}"]
        for entry in game.action_history:
            amount_text = f" {entry.amount}" if entry.amount is not None else ""
            lines.append(
                f"[{entry.street.value}] {entry.player_name}: {entry.action_type.value}{amount_text}"
            )
        if game.showdown_results:
            lines.append("Showdown:")
            for result in game.showdown_results:
                lines.append(f"{result.name} won {result.amount} with {result.hand}")
        return "\n".join(lines)

    def _get_game(self, game_id: str) -> PokerGame:
        game = self.games.get(game_id)
        if not game:
            raise ValueError(f"Game not found: {game_id}")
        return game

    def _get_player(self, game: PokerGame, player_id: str) -> Player:
        for player in game.players:
            if player.id == player_id:
                return player
        raise ValueError(f"Player not found: {player_id}")
