"""Bot move selection strategies."""

from __future__ import annotations

import math
import random
from typing import Optional

from base_models import BotDifficulty, Game, GameOutcome
from game.board import get_winner, is_draw


def choose_bot_column(game_data: Game, player_id: int) -> int:
    """Choose a column for the bot based on configured difficulty."""
    bot_player = (
        game_data.players[0]
        if game_data.players[0].id == player_id
        else game_data.players[1]
    )
    difficulty = bot_player.difficulty or BotDifficulty.EASY
    available_columns = _get_available_columns(game_data.grid)
    if not available_columns:
        raise ValueError("No available columns for bot move.")

    if difficulty == BotDifficulty.EASY:
        return _choose_easy_column(game_data, player_id, available_columns)
    if difficulty == BotDifficulty.MEDIUM:
        return _choose_medium_column(game_data, player_id, available_columns)
    return _choose_hard_column(game_data, player_id, available_columns)


def _get_available_columns(grid: list[list[int]]) -> list[int]:
    return [col for col in range(len(grid[0])) if grid[0][col] == 0]


def _landing_row(grid: list[list[int]], column: int) -> Optional[int]:
    for row in range(len(grid) - 1, -1, -1):
        if grid[row][column] == 0:
            return row
    return None


def _simulate_move(
    grid: list[list[int]], column: int, player_id: int
) -> Optional[list[list[int]]]:
    row = _landing_row(grid, column)
    if row is None:
        return None
    clone = [r[:] for r in grid]
    clone[row][column] = player_id
    return clone


def _winning_column(
    grid: list[list[int]], player_id: int, columns: list[int]
) -> Optional[int]:
    target = GameOutcome.PLAYER_1_WIN if player_id == 1 else GameOutcome.PLAYER_2_WIN
    for column in columns:
        simulated = _simulate_move(grid, column, player_id)
        if simulated is None:
            continue
        if get_winner(simulated) == target:
            return column
    return None


def _choose_easy_column(game_data: Game, player_id: int, columns: list[int]) -> int:
    # Easy mode intentionally makes mistakes and does not proactively block.
    if random.random() < 0.7:
        return random.choice(columns)
    center_first = sorted(columns, key=lambda col: abs(col - 3))
    return center_first[0]


def _choose_medium_column(game_data: Game, player_id: int, columns: list[int]) -> int:
    winning = _winning_column(game_data.grid, player_id, columns)
    if winning is not None:
        return winning

    opponent_id = 1 if player_id == 2 else 2
    block = _winning_column(game_data.grid, opponent_id, columns)
    if block is not None:
        return block

    return sorted(columns, key=lambda col: abs(col - 3))[0]


def _choose_hard_column(game_data: Game, player_id: int, columns: list[int]) -> int:
    _, chosen = _minimax(
        grid=game_data.grid,
        depth=5,
        maximizing=True,
        bot_player_id=player_id,
        current_player_id=player_id,
        alpha=-math.inf,
        beta=math.inf,
    )
    # Fallback is defensive only; there is always at least one available column here.
    return chosen if chosen is not None else columns[0]


def _minimax(
    grid: list[list[int]],
    depth: int,
    maximizing: bool,
    bot_player_id: int,
    current_player_id: int,
    alpha: float,
    beta: float,
) -> tuple[float, Optional[int]]:
    winner = get_winner(grid)
    bot_win = (
        GameOutcome.PLAYER_1_WIN if bot_player_id == 1 else GameOutcome.PLAYER_2_WIN
    )
    opp_win = (
        GameOutcome.PLAYER_2_WIN if bot_player_id == 1 else GameOutcome.PLAYER_1_WIN
    )

    if winner == bot_win:
        return (1000 + depth, None)
    if winner == opp_win:
        return (-1000 - depth, None)
    if is_draw(grid):
        return (0, None)
    if depth == 0:
        return (_heuristic_score(grid, bot_player_id), None)

    columns = _get_available_columns(grid)
    if maximizing:
        value = -math.inf
        best_column: Optional[int] = None
        for column in columns:
            nxt = _simulate_move(grid, column, current_player_id)
            if nxt is None:
                continue
            score, _ = _minimax(
                nxt,
                depth - 1,
                False,
                bot_player_id,
                1 if current_player_id == 2 else 2,
                alpha,
                beta,
            )
            if score > value:
                value = score
                best_column = column
            alpha = max(alpha, value)
            if beta <= alpha:
                break
        return (value, best_column)

    value = math.inf
    best_column = None
    for column in columns:
        nxt = _simulate_move(grid, column, current_player_id)
        if nxt is None:
            continue
        score, _ = _minimax(
            nxt,
            depth - 1,
            True,
            bot_player_id,
            1 if current_player_id == 2 else 2,
            alpha,
            beta,
        )
        if score < value:
            value = score
            best_column = column
        beta = min(beta, value)
        if beta <= alpha:
            break
    return (value, best_column)


def _heuristic_score(grid: list[list[int]], bot_player_id: int) -> float:
    """Lightweight board evaluation for hard mode search."""
    score = 0.0
    center_col = len(grid[0]) // 2
    center_count = sum(1 for row in grid if row[center_col] == bot_player_id)
    score += center_count * 3
    return score
