"""Concrete IGame implementation for Connect 4."""

from datetime import UTC, datetime
from uuid import uuid4
from typing import Optional

from api import IGame
from base_models import (
    DISCS_PER_PLAYER,
    ExceptionType,
    Game,
    GameException,
    GameOutcome,
    GameStatus,
    Player,
    PlayerType,
)
from game.bots import choose_bot_column
from game.board import create_empty_grid, find_landing_row, find_winning_line, is_draw


class Connect4Game(IGame):
    """Concrete game engine implementing the IGame contract."""

    def __init__(self, players: tuple[Player, Player]) -> None:
        """Initialize a new game with the provided players."""
        player_one, player_two = players
        if player_one.id != 1 or player_two.id != 2:
            raise ValueError("Players must be provided in seat order with ids 1 and 2.")

        now = datetime.now(UTC)
        self._state = Game(
            game_id=str(uuid4()),
            position_1_player_id=player_one.id,
            position_2_player_id=player_two.id,
            grid=create_empty_grid(),
            players=players,
            started_at=now,
            completed_at=None,
            status=GameStatus.RUNNING,
            outcome=None,
            next_move_player_id=player_one.id,
            winner_id=None,
            discs_remaining=(DISCS_PER_PLAYER, DISCS_PER_PLAYER),
        )

    def place_disc(self, player: Player, column: int) -> bool:
        """Place a disc and advance game state.

        Args:
            player: Player making the move.
            column: Zero-based target column.

        Returns:
            True if the move completes the game; False otherwise.

        Raises:
            GameException: If placement is not allowed.
        """
        if self._state.status != GameStatus.RUNNING:
            raise GameException(
                message="Cannot place a disc because the game has already completed.",
                code=2001,
                type=ExceptionType.PLACEMENT_NOT_ALLOWED,
            )

        if player.id != self._state.next_move_player_id:
            raise GameException(
                message=f"It is not player {player.id}'s turn.",
                code=2002,
                type=ExceptionType.PLACEMENT_NOT_ALLOWED,
            )

        if player.id not in (
            self._state.position_1_player_id,
            self._state.position_2_player_id,
        ):
            raise GameException(
                message=f"Player {player.id} is not part of this game.",
                code=2003,
                type=ExceptionType.PLACEMENT_NOT_ALLOWED,
            )

        landing_row = find_landing_row(self._state.grid, column)
        self._state.grid[landing_row][column] = player.id
        self._update_discs_remaining_after_move(player.id)

        line = find_winning_line(self._state.grid)
        if line is not None:
            player_id, win_line = line
            self._state.status = GameStatus.COMPLETED
            self._state.completed_at = datetime.now(UTC)
            self._state.outcome = (
                GameOutcome.PLAYER_1_WIN if player_id == 1 else GameOutcome.PLAYER_2_WIN
            )
            self._state.winner_id = player_id
            self._state.winning_line = win_line
            return True

        if is_draw(self._state.grid):
            self._state.status = GameStatus.COMPLETED
            self._state.completed_at = datetime.now(UTC)
            self._state.outcome = GameOutcome.DRAW
            self._state.winner_id = None
            self._state.winning_line = None
            return True

        self._state.next_move_player_id = (
            self._state.position_2_player_id
            if player.id == self._state.position_1_player_id
            else self._state.position_1_player_id
        )
        return False

    def reset(self) -> None:
        """Reset the match to its initial running state."""
        self._state.grid = create_empty_grid()
        self._state.started_at = datetime.now(UTC)
        self._state.completed_at = None
        self._state.status = GameStatus.RUNNING
        self._state.outcome = None
        self._state.winner_id = None
        self._state.winning_line = None
        self._state.next_move_player_id = self._state.position_1_player_id
        self._state.discs_remaining = (DISCS_PER_PLAYER, DISCS_PER_PLAYER)

    def data(self) -> Game:
        """Return current game snapshot."""
        return self._state

    def advance_bot_if_needed(self) -> Optional[bool]:
        """Advance one move when it is a bot player's turn.

        Returns:
            ``True`` if bot moved and completed the game, ``False`` if bot moved
            and the game continues, or ``None`` when no bot move is made.
        """
        if self._state.status != GameStatus.RUNNING:
            return None

        current_player = self._player_by_id(self._state.next_move_player_id)
        if current_player.type != PlayerType.BOT:
            return None

        column = choose_bot_column(self._state, current_player.id)
        return self.place_disc(current_player, column)

    def _update_discs_remaining_after_move(self, player_id: int) -> None:
        """Update remaining discs for the player that just moved."""
        p1_remaining, p2_remaining = self._state.discs_remaining
        if player_id == self._state.position_1_player_id:
            self._state.discs_remaining = (p1_remaining - 1, p2_remaining)
        else:
            self._state.discs_remaining = (p1_remaining, p2_remaining - 1)

    def _player_by_id(self, player_id: int) -> Player:
        """Return player model by id for this game."""
        if self._state.players[0].id == player_id:
            return self._state.players[0]
        return self._state.players[1]
