from __future__ import annotations

import logging

from pokerkit import Automation, NoLimitTexasHoldem, State

from openpoker.events.emitter import EventEmitter
from openpoker.types import (
    ActionAppliedPayload,
    ActionHistoryEntry,
    ActionType,
    AvailableAction,
    EngineEventType,
    EventPayload,
    GameConfig,
    GameSnapshot,
    GameVariant,
    HandStartedPayload,
    Player,
    PlayerSnapshot,
    PlayerStatus,
    PokerAction,
    ShowdownPayload,
    ShowdownResult,
    Street,
    StreetChangedPayload,
)

logger = logging.getLogger(__name__)


class PokerGame:
    def __init__(
        self,
        game_id: str,
        game_name: str,
        config: GameConfig,
        emitter: EventEmitter | None = None,
    ):
        self.game_id = game_id
        self.game_name = game_name
        self.config = config
        self.players: list[Player] = []
        self.state: State | None = None
        self.hand_number = 0
        self.dealer_position = 0
        self.action_history: list[ActionHistoryEntry] = []
        self.conversation_history: dict[str, list[dict[str, str | list[dict[str, str]] | None]]] = {}
        self.showdown_results: list[ShowdownResult] = []
        self.players_in_hand: list[Player] = []
        self.emitter = emitter

        self.automations = (
            Automation.ANTE_POSTING,
            Automation.BET_COLLECTION,
            Automation.BLIND_OR_STRADDLE_POSTING,
            Automation.CARD_BURNING,
            Automation.HOLE_DEALING,
            Automation.BOARD_DEALING,
            Automation.HOLE_CARDS_SHOWING_OR_MUCKING,
            Automation.HAND_KILLING,
            Automation.CHIPS_PUSHING,
            Automation.CHIPS_PULLING,
        )

    def _emit(
        self,
        event_type: EngineEventType,
        payload: EventPayload,
        include_snapshot: bool = False,
    ) -> None:
        if not self.emitter:
            return
        snapshot = self.to_snapshot() if include_snapshot else None
        self.emitter.emit(
            event_type=event_type,
            game_id=self.game_id,
            hand_number=self.hand_number,
            payload=payload,
            snapshot=snapshot,
        )

    def add_player(self, player: Player) -> bool:
        if len(self.players) >= self.config.max_players:
            return False

        player.position = len(self.players)
        if player.stack <= 0:
            player.stack = self.config.starting_stack
        self.players.append(player)
        return True

    def can_start_game(self) -> bool:
        return len(self.players) >= 2

    def _get_active_players(self) -> list[Player]:
        return [p for p in self.players if p.status != PlayerStatus.SITTING_OUT and p.stack > 0]

    def _blind_indices(self, active_players: list[Player]) -> tuple[int, int]:
        active_positions = [p.position for p in active_players]
        dealer_idx_in_active = active_positions.index(self.dealer_position)
        if len(active_players) == 2:
            sb_index = dealer_idx_in_active
            bb_index = (dealer_idx_in_active + 1) % len(active_players)
        else:
            sb_index = (dealer_idx_in_active + 1) % len(active_players)
            bb_index = (dealer_idx_in_active + 2) % len(active_players)
        return sb_index, bb_index

    def _rotate_dealer_to_next_active(self) -> None:
        active_players = self._get_active_players()
        if len(active_players) < 2:
            return

        attempts = 0
        max_attempts = len(self.players)

        while attempts < max_attempts:
            self.dealer_position = (self.dealer_position + 1) % len(self.players)
            dealer_player = self.players[self.dealer_position]

            if dealer_player.stack > 0 and dealer_player.status != PlayerStatus.SITTING_OUT:
                break

            attempts += 1

    def start_new_hand(self) -> None:
        if not self.can_start_game():
            raise ValueError("Need at least 2 players to start")

        self.hand_number += 1
        self.showdown_results = []

        if self.hand_number > 1:
            self._rotate_dealer_to_next_active()

        current_dealer = self.players[self.dealer_position]
        if current_dealer.stack == 0 or current_dealer.status == PlayerStatus.SITTING_OUT:
            self._rotate_dealer_to_next_active()

        active_players = sorted(
            self._get_active_players(),
            key=lambda p: p.position,
        )
        self.players_in_hand = active_players

        if len(active_players) < 2:
            raise ValueError("Need at least 2 active players")

        starting_stacks = [p.stack for p in active_players]

        if self.config.variant == GameVariant.NO_LIMIT_HOLDEM:
            sb_index, bb_index = self._blind_indices(active_players)

            blinds_dict: dict[int, int] = {}
            blinds_dict[sb_index] = self.config.small_blind
            blinds_dict[bb_index] = self.config.big_blind

            game = NoLimitTexasHoldem(
                self.automations,
                True,
                self.config.ante,
                blinds_dict,
                self.config.big_blind,
            )
            self.state = game(starting_stacks, len(active_players))
        else:
            raise NotImplementedError(f"Variant {self.config.variant} not yet implemented")

        for player in self.players:
            player.current_bet = 0
            player.hole_cards = None
            player.last_action = None
            if player in active_players:
                player.status = PlayerStatus.ACTIVE

        self.action_history = []
        self.conversation_history = {}

        self._deal_hole_cards()
        self._update_button_positions()
        self._update_stacks()

        dealer = next((p for p in active_players if p.is_dealer), None)
        sb = next((p for p in active_players if p.is_small_blind), None)
        bb = next((p for p in active_players if p.is_big_blind), None)

        self._emit(
            EngineEventType.HAND_STARTED,
            payload=HandStartedPayload(
                players=[p.name for p in active_players],
                dealer=dealer.name if dealer else None,
                small_blind_player=sb.name if sb else None,
                small_blind_amount=self.config.small_blind if sb else None,
                big_blind_player=bb.name if bb else None,
                big_blind_amount=self.config.big_blind if bb else None,
            ),
            include_snapshot=True,
        )

    def _deal_hole_cards(self) -> None:
        if not self.state:
            return

        for player_idx, player in enumerate(self.players_in_hand):
            if player_idx < len(self.state.hole_cards):
                hole_cards = self.state.hole_cards[player_idx]
                player.hole_cards = [repr(card) for card in hole_cards]

    def _update_button_positions(self) -> None:
        for player in self.players:
            player.is_dealer = False
            player.is_small_blind = False
            player.is_big_blind = False

        if len(self.players_in_hand) >= 2:
            dealer = next((p for p in self.players_in_hand if p.position == self.dealer_position), None)
            if dealer:
                dealer.is_dealer = True

            sb_index, bb_index = self._blind_indices(self.players_in_hand)
            sb_player = self.players_in_hand[sb_index]
            bb_player = self.players_in_hand[bb_index]

            sb_player.is_small_blind = True
            bb_player.is_big_blind = True

    def get_current_street(self) -> Street:
        if not self.state or not self.state.status:
            return Street.SHOWDOWN

        board_card_count = len(self.state.board_cards)

        if board_card_count == 0:
            return Street.PREFLOP
        if board_card_count == 3:
            return Street.FLOP
        if board_card_count == 4:
            return Street.TURN
        if board_card_count == 5:
            return Street.RIVER
        return Street.SHOWDOWN

    def get_current_actor(self) -> Player | None:
        if not self.state:
            return None
        if not hasattr(self.state, "actor_index"):
            return None
        if self.state.actor_index is None:
            return None
        if self.state.actor_index < len(self.players_in_hand):
            return self.players_in_hand[self.state.actor_index]
        return None

    def get_available_actions(self, player: Player) -> list[AvailableAction]:
        if not self.state:
            return []

        current_actor = self.get_current_actor()
        if not current_actor or current_actor.id != player.id:
            return []

        actions: list[AvailableAction] = []

        call_amount = 0
        if self.state.can_check_or_call():
            call_amount = self.state.checking_or_calling_amount
            if call_amount == 0:
                actions.append(AvailableAction(action_type=ActionType.CHECK))
            else:
                actions.append(
                    AvailableAction(
                        action_type=ActionType.CALL,
                        min_amount=call_amount,
                        max_amount=call_amount,
                    )
                )

        if self.state.can_fold() and call_amount > 0:
            actions.append(AvailableAction(action_type=ActionType.FOLD))

        try:
            min_raise = self.state.min_completion_betting_or_raising_to_amount
            max_raise = self.state.max_completion_betting_or_raising_to_amount

            if min_raise is not None and max_raise is not None:
                if self.state.checking_or_calling_amount == 0:
                    actions.append(
                        AvailableAction(
                            action_type=ActionType.BET,
                            min_amount=min_raise,
                            max_amount=max_raise,
                        )
                    )
                else:
                    actions.append(
                        AvailableAction(
                            action_type=ActionType.RAISE,
                            min_amount=min_raise,
                            max_amount=max_raise,
                        )
                    )
        except (AttributeError, ValueError) as exc:
            logger.warning("could not determine raise/bet range: %s", exc)

        return actions

    def execute_action(self, player: Player, action: PokerAction) -> bool:
        if not self.state:
            return False

        current_actor = self.get_current_actor()
        if not current_actor or current_actor.id != player.id:
            return False

        board_before = self.get_board_cards()
        hand_was_active = self.state.status
        street_before = self.get_current_street()

        try:
            resolved_amount: int | None = None

            if action.action_type == ActionType.FOLD:
                self.state.fold()
                player.status = PlayerStatus.FOLDED
            elif action.action_type == ActionType.CHECK:
                self.state.check_or_call()
            elif action.action_type == ActionType.CALL:
                resolved_amount = self.state.checking_or_calling_amount
                self.state.check_or_call()
            elif action.action_type == ActionType.BET:
                if action.amount is None:
                    return False
                self.state.complete_bet_or_raise_to(action.amount)
                resolved_amount = action.amount
            elif action.action_type == ActionType.RAISE:
                if action.amount is None:
                    return False
                self.state.complete_bet_or_raise_to(action.amount)
                resolved_amount = action.amount
            else:
                return False

            player.last_action = action.action_type
            self._update_stacks()

            self.action_history.append(
                ActionHistoryEntry(
                    player_name=player.name,
                    player_id=player.id,
                    action_type=action.action_type,
                    amount=resolved_amount,
                    street=street_before,
                    pot_after=self.get_pot_size(),
                )
            )

            board_after = self.get_board_cards()
            if board_before != board_after:
                new_cards = board_after[len(board_before) :]
                self._emit(
                    EngineEventType.STREET_CHANGED,
                    payload=StreetChangedPayload(
                        street=self.get_current_street(),
                        new_cards=new_cards,
                    ),
                    include_snapshot=True,
                )
                for p in self.players_in_hand:
                    p.last_action = None

            self._emit(
                EngineEventType.ACTION_APPLIED,
                payload=ActionAppliedPayload(
                    player_id=player.id,
                    player_name=player.name,
                    action_type=action.action_type,
                    amount=resolved_amount,
                ),
                include_snapshot=True,
            )

            if not self.state.status and hand_was_active:
                self._capture_showdown_results()
                self._handle_hand_completion()

            return True

        except (ValueError, AttributeError):
            return False

    def _update_stacks(self) -> None:
        if not self.state:
            return

        for i, player in enumerate(self.players_in_hand):
            if i < len(self.state.stacks):
                player.stack = self.state.stacks[i]
            if i < len(self.state.bets):
                player.current_bet = self.state.bets[i]

            if i < len(self.state.statuses):
                is_in_hand = self.state.statuses[i]
                if is_in_hand:
                    if player.stack == 0:
                        player.status = PlayerStatus.ALL_IN
                    else:
                        player.status = PlayerStatus.ACTIVE
                else:
                    player.status = PlayerStatus.FOLDED

    def _capture_showdown_results(self) -> None:
        self.showdown_results = []
        if not self.state:
            return

        try:
            from pokerkit.state import ChipsPulling

            went_to_showdown = (
                getattr(self.state, "all_in_show_status", False) or len(self.state.board_cards) >= 5
            )

            for operation in reversed(self.state.operations):
                if not isinstance(operation, ChipsPulling):
                    break

                player_idx = operation.player_index
                amount = operation.amount
                if player_idx >= len(self.players_in_hand):
                    continue

                player = self.players_in_hand[player_idx]
                hand_desc = "Winner"

                try:
                    if player_idx < len(self.state.hole_cards):
                        hole_cards = self.state.hole_cards[player_idx]
                        board_cards = self.state.board_cards

                        if went_to_showdown and len(board_cards) >= 5:
                            hand = self.state.get_hand(player_idx, 0, 0)
                            hand_str = str(hand)
                            hand_desc = hand_str.split("(")[0].strip() if "(" in hand_str else hand_str
                        elif went_to_showdown and len(hole_cards) == 2:
                            hand_desc = f"{repr(hole_cards[0])}, {repr(hole_cards[1])}"
                        else:
                            hand_desc = "Won by fold"
                except (AttributeError, IndexError, ValueError, TypeError) as exc:
                    logger.warning("failed to resolve hand description for player %s: %s", player.name, exc)

                self.showdown_results.append(ShowdownResult(name=player.name, amount=amount, hand=hand_desc))

            if self.showdown_results:
                self._emit(
                    EngineEventType.SHOWDOWN,
                    payload=ShowdownPayload(
                        results=self.showdown_results,
                    ),
                    include_snapshot=True,
                )

        except (AttributeError, ImportError) as exc:
            logger.warning("showdown capture failed: %s", exc)

    def _handle_hand_completion(self) -> None:
        self._update_stacks()
        for player in self.players:
            player.current_bet = 0
            if player.status in (PlayerStatus.ALL_IN,):
                player.status = PlayerStatus.ACTIVE

    def get_pot_size(self) -> int:
        if not self.state:
            return 0
        return self.state.total_pot_amount

    def get_board_cards(self) -> list[str]:
        if not self.state:
            return []
        return [repr(card) for card in self.state.get_board_cards(0)]

    def get_current_bet(self) -> int:
        if not self.state:
            return 0
        try:
            return max(self.state.bets) if self.state.bets else 0
        except (AttributeError, ValueError):
            return 0

    def is_hand_active(self) -> bool:
        return self.state is not None and self.state.status

    def to_snapshot(
        self,
        viewer_id: str | None = None,
        reveal_all_hole_cards: bool = False,
    ) -> GameSnapshot:
        current_actor = self.get_current_actor()
        player_snapshots: list[PlayerSnapshot] = []
        for player in self.players:
            hole_cards = None
            if reveal_all_hole_cards:
                hole_cards = player.hole_cards
            elif viewer_id and player.id == viewer_id:
                hole_cards = player.hole_cards

            player_snapshots.append(
                PlayerSnapshot(
                    id=player.id,
                    name=player.name,
                    player_type=player.player_type,
                    provider=player.provider,
                    model=player.model,
                    position=player.position,
                    stack=player.stack,
                    status=player.status,
                    current_bet=player.current_bet,
                    is_dealer=player.is_dealer,
                    is_small_blind=player.is_small_blind,
                    is_big_blind=player.is_big_blind,
                    last_action=player.last_action,
                    hole_cards=hole_cards,
                )
            )

        return GameSnapshot(
            game_id=self.game_id,
            game_name=self.game_name,
            hand_number=self.hand_number,
            street=self.get_current_street(),
            pot=self.get_pot_size(),
            current_bet=self.get_current_bet(),
            board_cards=self.get_board_cards(),
            players=player_snapshots,
            current_actor_id=current_actor.id if current_actor else None,
            current_actor_name=current_actor.name if current_actor else None,
            hand_active=self.is_hand_active(),
            showdown_results=self.showdown_results,
        )
