"""Public contracts for the Connect 4 backend.

Interfaces use an ``I`` prefix and live here so ``ux`` can depend on stable
APIs without importing ``game``.
"""

from abc import ABC, abstractmethod
from typing import Optional, Tuple

from base_models import Game, Gamestats, Player


class IGame(ABC):
    """In-progress match: grid, turn order, and completion state.

    All moves that change the board go through this object. Concrete
    implementations may also define ``advance_bot_if_needed`` for bot plies;
    that hook is intentionally **not** part of this abstract interface.
    """

    @abstractmethod
    def place_disc(self, player: Player, column: int) -> bool:
        """Drop ``player``'s disc in ``column``.

        Args:
            player: The player making the move; must be the side to move.
            column: Zero-based target column.

        Returns:
            True if the placement ended the game (win or draw); False if the
            game continues.

        Raises:
            GameException: If the move is illegal, no landing row exists for the
                selected column, or it is not this player's turn.
        """
        pass

    @abstractmethod
    def reset(self) -> None:
        """Reset the grid and opening state (turn, disc counts, status)."""
        pass

    @abstractmethod
    def data(self) -> Game:
        """Return the current game snapshot (shared schema)."""
        pass


class IGameSession(ABC):
    """Session for the same two players across multiple games.

    Each application start creates a new session. Implementations track
    cumulative :class:`Gamestats` and which :class:`IGame` is current.

    A session **must not** mutate board or turn state on an :class:`IGame`
    instance (e.g. no wrapping ``place_disc`` or bot advancement here).
    Creating or replacing the current game reference and updating session
    aggregates when a game finishes is allowed.
    """

    @abstractmethod
    def set_players(self, players: Tuple[Player, Player]) -> None:
        """Configure the two session players (ids 1 and 2).

        Args:
            players: ``(player_one, player_two)`` in seat order.

        Raises:
            ValueError: If ids or configuration are invalid.
        """
        pass

    @abstractmethod
    def start_new_game(self) -> IGame:
        """Create a new in-progress game for this session's players.

        If a game was already in progress, it is abandoned in favor of the new
        one (per product rules).

        Returns:
            The new :class:`IGame` instance.
        """
        pass

    @abstractmethod
    def get_current_game(self) -> Optional[IGame]:
        """Return the active game, or ``None`` if none has been started."""
        pass

    @abstractmethod
    def get_player_session_stats(self) -> Tuple[Gamestats, Gamestats]:
        """Cumulative session statistics for player one and player two."""
        pass


def create_players(players: Tuple[Player, Player]) -> IGameSession:
    """Create players on a new implicit session.

    This is the only action required for step 1. Every call creates a fresh
    session so the UI does not need an explicit ``create_session`` action.

    Args:
        players: Player one and player two in seat order.

    Returns:
        The active game session with players initialized.
    """
    from game.session import create_players_in_new_session

    return create_players_in_new_session(players)


def get_active_session() -> IGameSession:
    """Return the currently active implicit session."""
    from game.session import get_active_session_impl

    return get_active_session_impl()


def advance_bot_if_needed(game: IGame) -> Optional[bool]:
    """If the side to move is a bot, apply one bot ply.

    Bot advancement is implemented on concrete game classes only; it is not
    declared on :class:`IGame` so callers depend on this module helper rather
    than the abstract type.

    Args:
        game: The active game (typically a concrete class from ``game``).

    Returns:
        ``True`` if a bot move completed the game, ``False`` if a bot move was
        made and the game continues, or ``None`` if no bot move was made.

    Raises:
        TypeError: If ``game`` does not provide ``advance_bot_if_needed``.
    """
    runner = getattr(game, "advance_bot_if_needed", None)
    if not callable(runner):
        raise TypeError(
            "Game implementation does not expose advance_bot_if_needed(); "
            "only concrete classes implement bot plies."
        )
    return runner()
