from ..search.things import Agent
from ..agent.environment import XYEnvironment
from ..search.things import *
from numpy import inf, min, max
from typing import override, Literal, Generic, TypeVar

from copy import deepcopy

Agent = Agent
Player = Literal["player", "opponent", "terminal"]  # player identifier type
State = TypeVar("State")  # type alias for game state representation
Numeric = int | float # type alias for numerical scores


class Tile(Thing):
    """ Utility class representing a tile on the Tic-Tac-Toe board. """
    def __init__(self, player:str | None=None) -> None:
        """ Constructor for Tile.
        
        :param player: The player identifier ("X" or "O") occupying this tile, or None if empty.
        """
        self.player = player
        self.min_val, self.max_val = inf, -inf  # can't remember what this is for
    
    @override
    def __repr__(self) -> str:
        """Player occupying the tile or a blank space if empty."""
        return self.player if self.player else " "


Board = list[list[Tile]]  # type alias for Tic-Tac-Toe board representation


def is_terminal(board:Board, return_winner:bool=False) -> bool | str:
    """ Checks if the given Tic-Tac-Toe board state is terminal (win or draw). 
    
    :param board: The current game board state, as list of lists of Tiles.
    :return: True if the game is over (win or draw), False otherwise.
    """
    # check rows, columns, diagonals for win
    for i in range(3):
        if board[i][0].player and all(board[i][j].player == board[i][0].player for j in range(3)):
            return True if not return_winner else board[i][0].player
        if board[0][i].player and all(board[j][i].player == board[0][i].player for j in range(3)):
            return True if not return_winner else board[0][i].player
    if board[0][0].player and all(board[i][i].player == board[0][0].player for i in range(3)):
        return True if not return_winner else board[0][0].player
    if board[0][2].player and all(board[i][2-i].player == board[0][2].player for i in range(3)):
        return True if not return_winner else board[0][2].player
    
    # check for draw (no empty tiles)
    if all(board[i][j].player for i in range(3) for j in range(3)):
        return True if not return_winner else "draw"
    
    return False


class TicTacToeGame(XYEnvironment):
    """ Noughts and Crosses (Tic-Tac-Toe) game environment. """
    def __init__(self, *args, **kwargs) -> None:
        """ Constructor for TicTacToeGame. 
        
        :param args: Positional arguments for the XYEnvironment base class.
        :param kwargs: Keyword arguments for the XYEnvironment base class.
        """
        super().__init__(*args, width=3, height=3, **kwargs)
        self.board:Board = [[Tile() for j in range(3)] for i in range(3)]
        self.in_play = True  # game is ongoing


    @property  # override
    def is_done(self) -> bool:
        """ Checks if the game is over. """
        if len(self.agents) == 0: return True
        return (not self.in_play) or is_terminal(self.board)

    
    def make_move(self) -> None:
        """ Makes a move on the board. """
        raise NotImplementedError
    
    @override
    def step(self) -> None:
        """ Advances the game by one step, processing the player's move. 
        
        Receives input from the user for their move, updates the board state,
        and checks for game termination.
        """
        if self.is_done: return  # game over
        
        self.make_move()  # get player move
        
        super().step()  # progress agent move
        
        if self.is_done: 
            self.show_board()
            winner = is_terminal(self.board, return_winner=True)
            print(f"{self}: "+("draw" if winner=="draw" else f"{winner} wins"))
    
    def show_board(self) -> None:
        """ Utility function to print the current board state. """
        board = ""
        for i in (0,1):
            board += f"_{self.board[i][0]}_|_{self.board[i][1]}_|_{self.board[i][2]}_"
            board += "\n"
        board += f" {self.board[2][0]} | {self.board[2][1]} | {self.board[2][2]} "
        print(board[:-1])
    
    @override
    def __repr__(self) -> str:
        """ String representation of the current board state. """
        return "♟️"
    
    @override
    def add_agent(self, agent: Agent, player:str="X") -> None:
        if not isinstance(agent, Agent):
            raise TypeError(f"{self}: {agent} is not an Agent")
        self.agents.add(agent)
        self.agent = agent
        self.agent.player = player # default "X"


    @override
    def percept(self, agent: Agent) -> Board:
        """ Returns the current board state as the agent's percept. 
        
        :param agent: The agent receiving the percept.
        :return board: The current game board state, as list of lists of Tiles.
        """
        return self.board


    @override
    def execute_action(self, agent: Agent, action: tuple[str, Board]) -> None:
        """ Executes the given action by the agent on the game state.
        
        :param agent: The agent performing the action.
        :param action: A tuple containing the action command and the resulting state. Actions can be: ("move", state) or ("done", state).
        """
        command, state = action
        
        match command:
            case "move":
                self.board = agent.move(state)
            case "done":
                self.in_play = False


class InteractiveTicTacToeGame(TicTacToeGame):
    """ Interactive Tic-Tac-Toe game environment for human players. """
    def get_position(self) -> tuple[int,int]:
        """ Utility function to return user-inputted board position.
        
        :return: A tuple (i,j) representing the row and column indices of the move.
        """
        self.show_board()
        
        def _read_move_safe(prompt: str,
                            _counter:int=5,
                            bounds:tuple[int,int] = (0,2)) -> int:
            """ Utility to read integer input safely within bounds.
            
            :param prompt: The input prompt string.
            :param _counter: Number of attempts before giving up.
            :param bounds: Tuple of (min, max) valid integer bounds.
            :return: The valid integer input from the user.
            """
            try:
                _unsafe_input = input(prompt)
                i = int(_unsafe_input)-1
                if i < bounds[0] or i > bounds[1]:
                    raise ValueError(f"Out of bounds, ensure it number is integer [{bounds[0]+1}-{bounds[1]+1}]")
            except Exception as e:
                print(e)
                if _counter > 0:
                    return _read_move_safe(prompt, _counter-1, bounds)
                else:
                    raise SystemError("I give up.")             
            return i
        
        i = _read_move_safe("choose move row [1-3]  ")
        j = _read_move_safe("choose move column [1-3]  ")
        return i,j
    
    @override
    def make_move(self) -> None:
        """ Makes a move on the board. """
        i,j = self.get_position()  # needs defining
        
        placed = False  # flag for valid move
        if not self.board[i][j].player:  # places mark if valid
            self.board[i][j].player = "X" if self.agent.player == "O" else "O"
            placed = True
            
        while not placed:
            print("not a valid move !")
            i,j = self.get_position()
            if (i < 0 or i > 2 or j < 0 or j > 2):
                continue
            if not self.board[i][j].player:
                self.board[i][j].player = "X" if self.agent.player == "O" else "O"
                placed = True

    
    @override
    def execute_action(self, agent: Agent, action: tuple[str, Board]) -> None:
        print(f"{agent}: executing action: {action}")
        return super().execute_action(agent, action)


class AdversarialAgent(UtilityBasedAgent, Generic[State]):
    """ Agent for adversarial search problems. """
    def to_move(self, state:State) -> Player:
        """ Returns which player is to move in the given state.
        
        Receives the game state and determines which player's turn it is.
        Must be implemented by subclasses.
        
        :param state: The current game state.
        :return player: The player whose turn it is to move.
        """
        raise NotImplementedError
    
    def moves(self, state:State) -> list[State]:
        """ Returns the collection of possible moves in the given state.
        
        Receives the game state and returns a list of possible resulting states
        after all legal moves for the current player.
        Must be implemented by subclasses.
        
        :param state: The current game state.
        :return moves: A list of possible resulting game states.
        """
        raise NotImplementedError

    def score(self, state:State) -> Numeric:
        """ Returns the score for the given state.
        
        Receives the game state and returns a numerical score indicating the
        desirability of that state for the agent.
        Must be implemented by subclasses.
        
        :param state: The current game state.
        :return score: A numerical score for the state.
        """
        raise NotImplementedError


class TicTacToeAgent[Board](AdversarialAgent):
    def __init__(self, player:Literal["X","O"]="X") -> None:
        """ Constructor for TicTacToeAgent.
        
        :param player: The player identifier ("X" or "O") for this agent.
        """
        super().__init__()
        self.player = player
    
    
    @override
    def __repr__(self) -> str:
        """ String representation of the TicTacToeAgent.
         :return: The player identifier if set, otherwise the default representation.
        """
        if hasattr(self, "player"):
            return self.player
        return super().__repr__()  # fallback
    
    @override
    def score(self, state:Board) -> int | None:
        """ Determines the score of the given Tic-Tac-Toe state for the agent.
        
        If not terminal, returns None.
        If a winning game state for the agent, returns 1.
        If a losing game state for the agent, returns -1.
        If a draw, returns 0.
        
        :param state: The current game state as a list of lists representing the board
        :return score: Numerical score for the state or None if non-terminal.
        """
        draw = True  # check
        index = lambda x,i,j: x[i][j]
        
        def is_win(check:list[tuple[int,int]]) -> bool:
            """ Checks if the given positions constitute a win for a player. 
            
            :param check: List of (i,j) tuples representing board positions to check, either row, diagonal, or column.
            :return: True if all positions are occupied by the same player, False otherwise.
            """
            if all(index(state, *idx).player == tile.player for idx in check):
                return True
            return False
        
        for i in range(3):
            for j in range(3):
                tile = index(state,i,j)
                if not tile.player:  # is empty
                    draw = False  # so cannot be a draw
                    continue # check next tile
                check = [ # rows and columns
                    [((i_+1)%3, j) for i_ in range(i, i+2)],
                    [(i,(j_+1)%3) for j_ in range(j, j+2)]]
                if i==j: # diagonal
                    check.append([((i_+1)%3, (i_+1)%3) for i_ in range(i, i+2)])
                if i+j == 2: # anti-diagonal
                    check.append([((i_+1)%3, (2-(i_+1)%3)%3) for i_ in range(i, i+2)])
                if any(is_win(idxs) for idxs in check):
                    return 1 if tile.player == self.player else -1
        
        return 0 if draw else None

    @override
    def moves(self, state:Board) -> list[Board]:
        """ Moves available from the given Tic-Tac-Toe state.
        
        :param state: The current game state as a list of lists representing the board and possible moves
        :return: A list of possible resulting game states with a legal moves.
        """
        to_move = self.to_move(state)
        
        if self.player == "X":
            player = "X" if to_move == "player" else "O"
        else:
            player = "X" if to_move == "opponent" else "O"
        possible_moves = []
        for i in range(3):
            for j in range(3):
                if not state[i][j].player:
                    move = deepcopy(state)#.copy()
                    move[i][j] = Tile(player)
                    possible_moves.append(move)
        return possible_moves


    @override
    def move(self, state:Board) -> Board:
        """ Returns the state after making a move.
        :param state: The current game state as a list of lists representing the board and possible moves
        :return: The new game state after the move has been made.
        """
        return state
    
    @override
    def to_move(self, state:Board) -> Player:
        """ Determines which player's turn it is in the given state.
        
        :param state: The current game state as a list of lists representing the board and possible moves
        :return: The player whose turn it is to move, or "terminal" if the game is over.
        """
        if self.score(state) is not None:
            return "terminal"  # game over
        
        moves_made = {"X":0, "O":0} # count moves made by each player
        for i in range(3):
            for j in range(3):
                if state[i][j].player is not None:
                    moves_made[state[i][j].player] += 1
        
        player = self.player
        opponent = "O" if self.player == "X" else "X"
        
        if moves_made[player] + moves_made[opponent] == 0:
            self.first_move = "player"
            return "player"  # take first move if available
        
        if moves_made[player] < moves_made[opponent] \
                and moves_made[opponent] == 1:
            self.first_move = "opponent"
            return "player"  # take second move if available
        
        if moves_made[player] > moves_made[opponent]:
            return "opponent"
        elif moves_made[player] < moves_made[opponent]:
            return "player"
        else:
            return self.first_move
          
    #     match self.player:
    #         case "X":
    #             return "player" if moves_made["X"] < moves_made["O"] else "opponent"
    #         case "O":
    #             return "player" if moves_made["O"] < moves_made["X"] else "opponent"
    
    # # @override
    # # def program(self, percepts:list[Board]) -> tuple[str, Board]:
    # #     """ Agent program to select an move based on percepts. """
    # #     raise NotImplementedError

