"""
1-4-24 (Midnight) Game Implementation for PlayPalace v11.

Dice game where players roll 6 dice trying to get a 1 and a 4.
The other 4 dice are summed for points (max 24). Highest score wins the round.
"""

from dataclasses import dataclass, field
from datetime import datetime
import random

from ..base import Game, Player, GameOptions
from ..registry import register_game
from ...game_utils.action_guard_mixin import ActionGuardMixin
from ...game_utils.round_based_game_mixin import RoundBasedGameMixin
from ...game_utils.actions import Action, ActionSet, Visibility
from ...game_utils.bot_helper import BotHelper
from ...game_utils.dice import DiceSet
from ...game_utils.dice_game_mixin import DiceGameMixin
from ...game_utils.game_result import GameResult, PlayerResult
from ...game_utils.options import IntOption, option_field
from ...messages.localization import Localization
from server.core.ui.keybinds import KeybindState


@dataclass
class MidnightPlayer(Player):
    """Player state for 1-4-24 (Midnight) game."""

    dice: DiceSet = field(default_factory=lambda: DiceSet(num_dice=6, sides=6))
    round_score: int = 0  # Score for current round (0 if disqualified)
    round_wins: int = 0  # Number of rounds won
    qualified: bool = False  # Whether player has 1 and 4


@dataclass
class MidnightOptions(GameOptions):
    """Options for 1-4-24 (Midnight) game."""

    rounds: int = option_field(
        IntOption(
            default=5,
            min_val=1,
            max_val=20,
            value_key="rounds",
            label="midnight-set-rounds",
            prompt="midnight-enter-rounds",
            change_msg="midnight-option-changed-rounds",
        )
    )


@dataclass
@register_game
class MidnightGame(ActionGuardMixin, RoundBasedGameMixin, Game, DiceGameMixin):
    """
    1-4-24 (Midnight) dice game.

    Players roll 6 dice and must keep at least one die after each roll.
    Once kept, dice are locked and can't be changed.
    To score, you need a 1 and a 4. The other 4 dice sum for points (max 24).
    Highest score wins the round. First to win the most rounds wins the game.
    """

    relevant_preferences = ["dice_keeping_style"]

    # Game-specific state
    players: list[MidnightPlayer] = field(default_factory=list)
    options: MidnightOptions = field(default_factory=MidnightOptions)

    @classmethod
    def get_name(cls) -> str:
        return "1-4-24"

    @classmethod
    def get_type(cls) -> str:
        return "midnight"

    @classmethod
    def get_category(cls) -> str:
        return "category-dice-games"

    @classmethod
    def get_min_players(cls) -> int:
        return 2

    @classmethod
    def get_max_players(cls) -> int:
        return 6

    def create_player(self, player_id: str, name: str, is_bot: bool = False) -> MidnightPlayer:
        """Create a new player with Midnight-specific state."""
        return MidnightPlayer(
            id=player_id,
            name=name,
            is_bot=is_bot,
            dice=DiceSet(num_dice=6, sides=6),
            round_score=0,
            round_wins=0,
            qualified=False,
        )

    # ==========================================================================
    # Dice toggle methods (required by DiceGameMixin)
    # ==========================================================================

    def _is_dice_toggle_enabled(self, player: Player, die_index: int) -> str | None:
        """Check if toggling die at index is enabled."""
        if self.status != "playing":
            return "action-not-playing"
        if player.is_spectator:
            return "action-spectator"
        if self.current_player != player:
            return "action-not-your-turn"
        midnight_player: MidnightPlayer = player  # type: ignore
        if not midnight_player.dice.has_rolled:
            return "midnight-need-to-roll"
        if midnight_player.dice.is_locked(die_index):
            return "dice-locked"
        # Allow toggling (keeping/unkeeping) until dice are locked
        return None

    def _is_dice_toggle_hidden(self, player: Player, die_index: int) -> Visibility:
        """Dice toggles are visible when dice are rolled and die is not locked/kept."""
        if self.status != "playing":
            return Visibility.HIDDEN
        if player.is_spectator:
            return Visibility.HIDDEN
        if self.current_player != player:
            return Visibility.HIDDEN
        midnight_player: MidnightPlayer = player  # type: ignore
        if not midnight_player.dice.has_rolled:
            return Visibility.HIDDEN
        # Hide only locked dice (kept dice are still toggleable)
        if midnight_player.dice.is_locked(die_index):
            return Visibility.HIDDEN
        return Visibility.VISIBLE

    def _get_dice_toggle_label(self, player: Player, die_index: int) -> str:
        """Get label for dice toggle action."""
        midnight_player: MidnightPlayer = player  # type: ignore

        # Safety check - if dice haven't been rolled or index out of range
        if not midnight_player.dice.has_rolled or die_index >= len(midnight_player.dice.values):
            return f"Die {die_index + 1}"

        die_val = midnight_player.dice.values[die_index]
        if midnight_player.dice.is_locked(die_index):
            return f"{die_val} (locked)"
        if midnight_player.dice.is_kept(die_index):
            return f"{die_val} (kept)"
        return str(die_val)

    # Dice toggle handlers provided by DiceGameMixin (no override needed)

    # ==========================================================================
    # Roll action
    # ==========================================================================

    def _is_roll_enabled(self, player: Player) -> str | None:
        """Check if roll action is enabled."""
        error = self.guard_turn_action_enabled(player)
        if error:
            return error
        midnight_player: MidnightPlayer = player  # type: ignore
        # Must keep at least one die per roll (except first roll)
        if midnight_player.dice.has_rolled and midnight_player.dice.kept_unlocked_count == 0:
            return "midnight-must-keep-one"
        if midnight_player.dice.has_rolled and midnight_player.dice.all_decided:
            return "action-not-available"
        if midnight_player.dice.unlocked_count == 0:
            return "midnight-no-dice-to-keep"
        return None

    def _is_roll_hidden(self, player: Player) -> Visibility:
        """Roll is visible during play for current player."""
        midnight_player: MidnightPlayer = player  # type: ignore
        can_reroll = not (midnight_player.dice.has_rolled and midnight_player.dice.all_decided)
        return self.turn_action_visibility(
            player, extra_condition=midnight_player.dice.unlocked_count > 0 and can_reroll
        )

    def _action_roll(self, player: Player, action_id: str) -> None:
        """Handle roll action."""
        midnight_player: MidnightPlayer = player  # type: ignore

        had_rolled = midnight_player.dice.has_rolled
        locked_before = set(midnight_player.dice.locked)
        kept_before = set(midnight_player.dice.kept)
        if had_rolled:
            rolled_indices = [
                i
                for i in range(midnight_player.dice.num_dice)
                if i not in locked_before and i not in kept_before
            ]
        else:
            rolled_indices = list(range(midnight_player.dice.num_dice))

        self.play_sound("game_pig/roll.ogg")

        # Roll dice (locks kept dice)
        midnight_player.dice.roll(lock_kept=True, clear_kept=True)
        self._apply_dice_values_defaults(midnight_player)

        # Format rerolled dice only (first roll announces all dice).
        result_text = ", ".join(str(midnight_player.dice.values[i]) for i in rolled_indices)

        # Announce results
        self.broadcast_personal_l(
            player, "midnight-you-rolled", "midnight-player-rolled", dice=result_text
        )

        # Check if auto-score needed (all locked or only 1 unlocked)
        if midnight_player.dice.unlocked_count <= 1:
            self._score_turn(midnight_player)
            return

        # Give bot time to think about next action
        if player.is_bot:
            BotHelper.jolt_bot(player, ticks=random.randint(10, 20))  # nosec B311

        self.rebuild_all_menus()

    # ==========================================================================
    # Bank action
    # ==========================================================================

    def _is_bank_enabled(self, player: Player) -> str | None:
        """Check if bank action is enabled."""
        if self.status != "playing":
            return "action-not-playing"
        if player.is_spectator:
            return "action-spectator"
        if self.current_player != player:
            return "action-not-your-turn"
        midnight_player: MidnightPlayer = player  # type: ignore
        if not midnight_player.dice.has_rolled:
            return "midnight-must-roll-first"
        if not midnight_player.dice.all_decided:
            return "midnight-keep-all-first"
        return None

    def _is_bank_hidden(self, player: Player) -> Visibility:
        """Bank is visible during play for current player after first roll."""
        if self.status != "playing":
            return Visibility.HIDDEN
        if player.is_spectator:
            return Visibility.HIDDEN
        if self.current_player != player:
            return Visibility.HIDDEN
        midnight_player: MidnightPlayer = player  # type: ignore
        if not midnight_player.dice.has_rolled:
            return Visibility.HIDDEN
        return Visibility.VISIBLE

    def _action_bank(self, player: Player, action_id: str) -> None:
        """Bank score and end turn."""
        midnight_player: MidnightPlayer = player  # type: ignore
        self._score_turn(midnight_player)

    def _score_turn(self, player: MidnightPlayer) -> None:
        """Calculate and apply turn score."""
        # Check if player has both 1 and 4
        has_one = 1 in player.dice.values
        has_four = 4 in player.dice.values

        if has_one and has_four:
            # Qualified! Calculate score from other 4 dice
            # Remove first occurrence of 1 and first occurrence of 4
            values_copy = player.dice.values.copy()
            values_copy.remove(1)
            values_copy.remove(4)

            player.round_score = sum(values_copy)
            player.qualified = True

            self.play_sound("game_pig/bank.ogg")
            self.broadcast_personal_l(
                player,
                "midnight-you-scored",
                "midnight-scored",
                score=player.round_score,
            )
        else:
            # Disqualified
            player.round_score = 0
            player.qualified = False

            self.broadcast_personal_l(
                player, "midnight-you-disqualified", "midnight-player-disqualified"
            )

        # Jolt all bots to pause for the turn change
        BotHelper.jolt_bots(self, ticks=random.randint(20, 30))  # nosec B311

        self._on_turn_end()

    # ==========================================================================
    # Action set creation
    # ==========================================================================

    def create_turn_action_set(self, player: MidnightPlayer) -> ActionSet:
        """Create the turn action set for a player."""
        user = self.get_user(player)
        locale = user.locale if user else "en"

        action_set = ActionSet(name="turn")

        # Add dice toggle actions from mixin (handles 1-6 keys and menu items)
        self.add_dice_toggle_actions(action_set, num_dice=6)

        # Roll action
        action_set.add(
            Action(
                id="roll",
                label=Localization.get(locale, "midnight-roll"),
                handler="_action_roll",
                is_enabled="_is_roll_enabled",
                is_hidden="_is_roll_hidden",
            )
        )

        # Bank action (end turn voluntarily)
        action_set.add(
            Action(
                id="bank",
                label=Localization.get(locale, "midnight-bank"),
                handler="_action_bank",
                is_enabled="_is_bank_enabled",
                is_hidden="_is_bank_hidden",
            )
        )

        return action_set

    def setup_keybinds(self) -> None:
        """Define all keybinds for the game."""
        # Call parent for lobby/standard keybinds
        super().setup_keybinds()

        # Dice keybinds from mixin (1-6 keys)
        self.setup_dice_keybinds(num_dice=6)

        # Roll action keybind
        self.define_keybind("r", "Roll the dice", ["roll"], state=KeybindState.ACTIVE)

        # Bank action keybind
        self.define_keybind("b", "Bank", ["bank"], state=KeybindState.ACTIVE)

    def should_reset_all_scores(self) -> bool:
        return True

    def _reset_player_for_game(self, player: MidnightPlayer) -> None:
        player.dice.reset()
        player.round_score = 0
        player.round_wins = 0
        player.qualified = False

    def _reset_player_for_round(self, player: MidnightPlayer) -> None:
        player.dice.reset()
        player.round_score = 0
        player.qualified = False

    def _reset_player_for_turn(self, player: MidnightPlayer) -> None:
        player.dice.reset()
        player.round_score = 0
        player.qualified = False

    def _announce_turn_start(self, player) -> None:
        self.play_sound("game_pig/turn.ogg")
        self.broadcast_l("midnight-turn-start", player=player.name)

    def _setup_bot_for_turn(self, player: MidnightPlayer) -> None:
        if player.is_bot:
            BotHelper.set_target(player, 24)

    def on_tick(self) -> None:
        """Called every tick. Handle bot AI."""
        super().on_tick()

        if not self.game_active:
            return

        BotHelper.on_tick(self)

    def bot_think(self, player: MidnightPlayer) -> str | None:
        """Bot AI decision making. Called by BotHelper."""
        if self._should_bot_roll(player):
            return "roll"

        decision = self._bot_lock_target_value(player, 1)
        if decision:
            return decision

        decision = self._bot_lock_target_value(player, 4)
        if decision:
            return decision

        decision = self._bot_lock_highest_available(player)
        if decision:
            return decision

        return self._bot_finalize_turn(player)

    def _should_bot_roll(self, player: MidnightPlayer) -> bool:
        if not player.dice.has_rolled:
            return True
        if player.dice.kept_unlocked_count > 0:
            return True
        return False

    def _bot_lock_target_value(self, player: MidnightPlayer, target: int) -> str | None:
        locked_values = [player.dice.values[i] for i in player.dice.locked]
        if target in locked_values:
            return None
        for i in range(6):
            if not player.dice.is_locked(i) and player.dice.values[i] == target:
                return f"toggle_die_{i}"
        return None

    def _bot_lock_highest_available(self, player: MidnightPlayer) -> str | None:
        best_index = -1
        best_value = 0
        for i in range(6):
            if not player.dice.is_locked(i) and not player.dice.is_kept(i):
                if player.dice.values[i] > best_value:
                    best_value = player.dice.values[i]
                    best_index = i
        if best_index >= 0:
            return f"toggle_die_{best_index}"
        return None

    def _bot_finalize_turn(self, player: MidnightPlayer) -> str:
        locked_values = [player.dice.values[i] for i in player.dice.locked]
        has_one = 1 in locked_values
        has_four = 4 in locked_values

        if has_one and has_four:
            return "bank"

        if player.dice.kept_unlocked_count == 0:
            return "bank"

        return "roll"

    def _on_round_end(self) -> None:
        """Handle end of a round."""
        active_players = self.get_active_players()

        # Find highest score among qualified players
        qualified_players = [p for p in active_players if p.qualified]

        if not qualified_players:
            # No one qualified
            self.broadcast_l("midnight-all-disqualified")
        else:
            high_score = max(p.round_score for p in qualified_players)
            winners = [p for p in qualified_players if p.round_score == high_score]

            if len(winners) == 1:
                # Single round winner
                winner = winners[0]
                winner.round_wins += 1
                self._team_manager.add_to_team_score(winner.name, 1)
                self.play_sound("game_pig/bank.ogg")
                self.broadcast_l("midnight-round-winner", player=winner.name)
            else:
                # Tie
                names = [w.name for w in winners]
                # Each tied player gets a win
                for w in winners:
                    w.round_wins += 1
                    self._team_manager.add_to_team_score(w.name, 1)
                for player in self.players:
                    user = self.get_user(player)
                    if user:
                        names_str = Localization.format_list_and(user.locale, names)
                        user.speak_l("midnight-round-tie", players=names_str)

        # Check if game is over
        if self.round >= self.options.rounds:
            self._end_game()
        else:
            # Next round
            self._start_round()

    def _end_game(self) -> None:
        """End the game and determine overall winner."""
        active_players = self.get_active_players()

        # Find highest round wins
        max_wins = max(p.round_wins for p in active_players)
        winners = [p for p in active_players if p.round_wins == max_wins]

        if len(winners) == 1:
            # Single game winner
            self.play_sound("game_pig/win.ogg")
            self.broadcast_l("midnight-game-winner", player=winners[0].name, wins=max_wins)
        else:
            # Game tie
            names = [w.name for w in winners]
            for player in self.players:
                user = self.get_user(player)
                if user:
                    names_str = Localization.format_list_and(user.locale, names)
                    user.speak_l(
                        "midnight-game-tie", players=names_str, wins=max_wins, buffer="table"
                    )

        self.finish_game()

    def build_game_result(self) -> GameResult:
        """Build the game result with Midnight-specific data."""
        active_players = self.get_active_players()
        sorted_players = sorted(active_players, key=lambda p: p.round_wins, reverse=True)
        winner = sorted_players[0] if sorted_players else None

        # Build final standings
        final_standings = {}
        for player in sorted_players:
            final_standings[player.name] = player.round_wins

        return GameResult(
            game_type=self.get_type(),
            timestamp=datetime.now().isoformat(),
            duration_ticks=self.sound_scheduler_tick,
            player_results=[
                PlayerResult(
                    player_id=p.id,
                    player_name=p.name,
                    is_bot=p.is_bot,
                    is_virtual_bot=getattr(p, "is_virtual_bot", False),
                )
                for p in active_players
            ],
            custom_data={
                "winner_name": winner.name if winner else None,
                "winner_rounds": winner.round_wins if winner else 0,
                "final_standings": final_standings,
                "rounds_played": self.round,
                "total_rounds": self.options.rounds,
            },
        )

    def format_end_screen(self, result: GameResult, locale: str) -> list[str]:
        """Format the end screen for Midnight game."""
        lines = [Localization.get(locale, "game-final-scores")]

        final_standings = result.custom_data.get("final_standings", {})
        for i, (name, wins) in enumerate(final_standings.items(), 1):
            lines.append(f"{i}. {name}: {wins} round wins")

        return lines
