"""Main game screen — orchestrates the chess game loop in a Textual TUI."""

from __future__ import annotations

from typing import TYPE_CHECKING

import chess
import chess.engine
from textual.app import ComposeResult
from textual.screen import Screen
from textual.reactive import reactive
from textual.widgets import Static, Footer, Header, Input
from textual.containers import Horizontal, Vertical
from textual import work

from chess_cli.game import GameState
from chess_cli.tui.widgets.board import BoardWidget, compute_captured
from chess_cli.tui.widgets.eval_bar import EvalBarWidget
from chess_cli.tui.widgets.move_list import MoveListWidget
from chess_cli.tui.widgets.player_info import PlayerInfoWidget

if TYPE_CHECKING:
    from chess_cli.engine import EnginePlayer
    from chess_cli.lichess import BotInfo, LichessClient, LichessGameOver, LichessPlayer

CAPTURE_SYMBOLS = {
    chess.QUEEN: "\u265b",
    chess.ROOK: "\u265c",
    chess.BISHOP: "\u265d",
    chess.KNIGHT: "\u265e",
    chess.PAWN: "\u265f",
}


class GameScreen(Screen):
    """The main game screen — replaces the game_loop() from main.py."""

    DEFAULT_CSS = """
GameScreen {
    layout: vertical;
}

#opponent-info {
    height: auto;
    margin: 0 1;
}

#player-info {
    height: auto;
    margin: 0 1;
}

#board-area {
    height: 1fr;
    min-height: 14;
    margin: 1 1;
}

#eval-bar {
    width: 4;
    margin-right: 1;
}

#board {
    width: 28;
    height: 10;
}

#move-panel {
    width: 1fr;
    min-width: 22;
    height: 100%;
    margin-left: 2;
    border: round $primary-background;
    overflow-y: auto;
}

#move-panel-title {
    text-align: center;
    text-style: bold;
    color: $text-muted;
    width: 100%;
    height: 1;
    padding: 1 0 0 0;
}

#move-list {
    width: 100%;
    height: 1fr;
    overflow-y: auto;
}

#status-line {
    height: 1;
    margin: 1 2 0 2;
    color: $error;
}

#move-input {
    margin: 1 2 0 2;
}
"""

    def __init__(
        self,
        color: chess.Color,
        difficulty: str = "intermediate",
        threads: int = 1,
        hash_mb: int = 16,
        move_time: float | None = None,
        lichess_client: LichessClient | None = None,
        lichess_bot: BotInfo | None = None,
        clock_limit: int = 600,
        clock_increment: int = 0,
    ) -> None:
        self.player_color = color
        self.difficulty = difficulty
        self.threads = threads
        self.hash_mb = hash_mb
        self.move_time = move_time
        self.lichess_client = lichess_client
        self.lichess_bot = lichess_bot
        self.clock_limit = clock_limit
        self.clock_increment = clock_increment
        self.game: GameState | None = None
        self.engine: EnginePlayer | LichessPlayer | None = None
        super().__init__()

    @property
    def _is_lichess(self) -> bool:
        return self.lichess_client is not None

    def compose(self) -> ComposeResult:
        yield Header(show_clock=False)
        yield PlayerInfoWidget(id="opponent-info")
        with Horizontal(id="board-area"):
            yield EvalBarWidget(id="eval-bar")
            yield BoardWidget(id="board")
            with Vertical(id="move-panel"):
                yield Static("Moves", id="move-panel-title")
                yield MoveListWidget(id="move-list")
        yield PlayerInfoWidget(id="player-info")
        yield Static("", id="status-line")
        yield Input(placeholder="Your move (or 'help')...", id="move-input")
        yield Footer()

    # ------------------------------------------------------------------
    # Lifecycle
    # ------------------------------------------------------------------

    def on_mount(self) -> None:
        self.query_one("#move-input", Input).focus()
        self._start_engine()

    def on_unmount(self) -> None:
        self.workers.cancel_group(self, "engine")
        if self.engine:
            self.engine.quit()
            self.engine = None
        elif self._is_lichess:
            # Engine never created — close the client to avoid leak
            try:
                self.lichess_client.close()
            except Exception:
                pass

    # ------------------------------------------------------------------
    # Engine startup
    # ------------------------------------------------------------------

    @work(thread=True, exclusive=True, group="engine")
    def _start_engine(self) -> None:
        if self._is_lichess:
            self.app.call_from_thread(self._set_status, "Challenging bot...")
            try:
                from chess_cli.lichess import LichessPlayer

                color_map = {chess.WHITE: "white", chess.BLACK: "black"}
                color_str = color_map.get(self.player_color, "random")
                game_id = self.lichess_client.challenge(
                    self.lichess_bot.username,
                    clock_limit=self.clock_limit,
                    clock_increment=self.clock_increment,
                    color=color_str,
                )
                player = LichessPlayer(
                    client=self.lichess_client,
                    game_id=game_id,
                    player_color=self.player_color,
                    bot_username=self.lichess_bot.username,
                )
                player.elo = self.lichess_bot.rating
                player.start_stream()
                self.app.call_from_thread(self._on_engine_ready, player)
            except Exception as e:
                # Close client to avoid leak, enable input so user can quit
                try:
                    self.lichess_client.close()
                except Exception:
                    pass
                self.app.call_from_thread(
                    self._set_status, f"Lichess error: {e}"
                )
                self.app.call_from_thread(self._enable_input)
        else:
            self.app.call_from_thread(self._set_status, "Starting engine...")
            try:
                from chess_cli.engine import EnginePlayer

                engine = EnginePlayer(
                    difficulty=self.difficulty,
                    threads=self.threads,
                    hash_mb=self.hash_mb,
                    move_time=self.move_time,
                )
                self.app.call_from_thread(self._on_engine_ready, engine)
            except Exception as e:
                self.app.call_from_thread(
                    self._set_status, f"Engine error: {e}"
                )

    def _on_engine_ready(
        self, engine: EnginePlayer | LichessPlayer
    ) -> None:
        self.engine = engine
        difficulty = getattr(engine, "difficulty", "lichess")
        if self._is_lichess:
            opponent_name = self.lichess_bot.username
            site = "Lichess"
        else:
            opponent_name = "Stockfish"
            site = "Local"
        self.game = GameState(
            player_color=self.player_color,
            difficulty=difficulty,
            opponent_name=opponent_name,
            site=site,
        )
        self.game.opponent_elo = engine.elo
        self._setup_player_info()
        self._update_display()
        self._set_status("")
        # If player is Black, engine/bot moves first
        if not self.game.is_player_turn():
            self._request_engine_move()

    # ------------------------------------------------------------------
    # Player info setup
    # ------------------------------------------------------------------

    def _setup_player_info(self) -> None:
        player_name = (
            "You (White)" if self.player_color == chess.WHITE else "You (Black)"
        )
        if self._is_lichess:
            opponent_name = (
                f"{self.lichess_bot.username} ({self.lichess_bot.rating})"
            )
        else:
            opponent_name = (
                f"Stockfish ({self.difficulty.capitalize()}, "
                f"{self.game.opponent_elo or 'max'})"
            )

        opp_info = self.query_one("#opponent-info", PlayerInfoWidget)
        opp_info.player_name = opponent_name
        opp_info.is_white = self.player_color == chess.BLACK  # opponent is opposite

        player_info = self.query_one("#player-info", PlayerInfoWidget)
        player_info.player_name = player_name
        player_info.is_white = self.player_color == chess.WHITE

    # ------------------------------------------------------------------
    # Display helpers
    # ------------------------------------------------------------------

    def _update_display(self) -> None:
        """Refresh all widgets from the current game state."""
        if not self.game:
            return

        board_widget = self.query_one("#board", BoardWidget)
        board_widget.board_fen = self.game.board.fen()
        board_widget.perspective = self.player_color == chess.WHITE

        if self.game.move_history:
            last = self.game.move_history[-1]
            board_widget.highlight_from = last.from_square
            board_widget.highlight_to = last.to_square
        else:
            board_widget.highlight_from = -1
            board_widget.highlight_to = -1

        # Move list
        self.query_one("#move-list", MoveListWidget).update_moves(
            self.game.san_history
        )

        # Captured pieces
        white_cap, black_cap = compute_captured(self.game.board)
        opp_info = self.query_one("#opponent-info", PlayerInfoWidget)
        player_info = self.query_one("#player-info", PlayerInfoWidget)

        # The opponent's widget shows pieces THEY captured (our pieces that are gone)
        if self.player_color == chess.WHITE:
            opp_info.captured_text = "".join(
                CAPTURE_SYMBOLS.get(pt, "") for pt in white_cap
            )
            player_info.captured_text = "".join(
                CAPTURE_SYMBOLS.get(pt, "") for pt in black_cap
            )
        else:
            opp_info.captured_text = "".join(
                CAPTURE_SYMBOLS.get(pt, "") for pt in black_cap
            )
            player_info.captured_text = "".join(
                CAPTURE_SYMBOLS.get(pt, "") for pt in white_cap
            )

        # Active turn indicator
        is_player_turn = self.game.is_player_turn()
        player_info.is_active = is_player_turn
        opp_info.is_active = not is_player_turn

    def _set_status(self, msg: str) -> None:
        self.query_one("#status-line", Static).update(f"  {msg}" if msg else "")

    def _update_eval(
        self, eval_score: float, is_mate: bool, mate_in: int | None
    ) -> None:
        eval_bar = self.query_one("#eval-bar", EvalBarWidget)
        eval_bar.eval_score = eval_score
        eval_bar.is_mate = is_mate
        eval_bar.mate_in = mate_in or 0
        eval_bar.perspective = self.player_color == chess.WHITE

        # Update eval text on opponent info (shown from player's perspective)
        display_eval = (
            eval_score if self.player_color == chess.WHITE else -eval_score
        )
        if is_mate and mate_in is not None:
            display_mate = (
                mate_in if self.player_color == chess.WHITE else -mate_in
            )
            eval_text = f"Eval: #{display_mate}"
        else:
            sign = "+" if display_eval >= 0 else ""
            eval_text = f"Eval: {sign}{display_eval:.1f}"
        self.query_one("#opponent-info", PlayerInfoWidget).eval_text = eval_text

    def _disable_input(self) -> None:
        self.query_one("#move-input", Input).disabled = True

    def _enable_input(self) -> None:
        inp = self.query_one("#move-input", Input)
        inp.disabled = False
        inp.focus()

    # ------------------------------------------------------------------
    # Input handling
    # ------------------------------------------------------------------

    def on_input_submitted(self, event: Input.Submitted) -> None:
        user_input = event.value.strip()
        event.input.value = ""  # Clear input
        if not user_input or not self.game:
            return

        cmd = user_input.lower().split()

        if cmd[0] in ("help", "h", "?"):
            self.notify(
                "Commands: help, moves, undo, resign, draw, eval, pgn, save, quit\n"
                "Move notation: e4, Nf3, Bb5, O-O, O-O-O, exd5, e8=Q",
                title="Help",
                timeout=10,
            )
            return
        elif cmd[0] in ("quit", "q"):
            self._quit_game()
            return
        elif cmd[0] in ("undo", "u"):
            self._handle_undo()
            return
        elif cmd[0] == "resign":
            self._handle_resign()
            return
        elif cmd[0] == "moves":
            legal = sorted(
                self.game.board.san(m) for m in self.game.board.legal_moves
            )
            self.notify(", ".join(legal), title="Legal moves", timeout=10)
            return
        elif cmd[0] == "draw":
            self._handle_draw()
            return
        elif cmd[0] == "eval":
            eval_bar = self.query_one("#eval-bar", EvalBarWidget)
            # Show eval from player's perspective
            display_score = (
                eval_bar.eval_score
                if self.player_color == chess.WHITE
                else -eval_bar.eval_score
            )
            if eval_bar.is_mate:
                mate_display = (
                    eval_bar.mate_in
                    if self.player_color == chess.WHITE
                    else -eval_bar.mate_in
                )
                self.notify(f"Evaluation: #{mate_display}", title="Eval")
            else:
                sign = "+" if display_score >= 0 else ""
                self.notify(
                    f"Evaluation: {sign}{display_score:.2f}",
                    title="Eval",
                )
            return
        elif cmd[0] == "pgn":
            if self.game:
                self.notify(
                    str(self.game.to_pgn()), title="PGN", timeout=15
                )
            return
        elif cmd[0] == "save":
            self._handle_save(cmd)
            return

        # Try to parse as a move
        self._try_move(user_input)

    # ------------------------------------------------------------------
    # Move handling
    # ------------------------------------------------------------------

    def _try_move(self, user_input: str) -> None:
        move_input = user_input.replace("0-0-0", "O-O-O").replace("0-0", "O-O")
        try:
            move = self.game.make_move(move_input)
        except (
            chess.InvalidMoveError,
            chess.IllegalMoveError,
            chess.AmbiguousMoveError,
        ):
            if self._is_promotion_attempt(move_input):
                self._prompt_promotion(move_input)
                return
            self._set_status(
                f"Invalid move: {user_input}. Type 'moves' for legal moves."
            )
            return

        self._set_status("")
        self._update_display()

        # Send move to Lichess (must happen before game-over check
        # so that game-ending moves like checkmate are posted)
        if self._is_lichess:
            try:
                self.engine.send_player_move(move)
            except Exception as e:
                # Undo the local move that was never sent
                self.game.board.pop()
                self.game.move_history.pop()
                self.game.san_history.pop()
                self._update_display()
                self._set_status(f"Failed to send move: {e}")
                return

        # Check game over
        over_msg = self.game.get_game_over_message()
        if over_msg:
            self._show_game_over(over_msg)
            return

        # Disable input, engine's turn
        self._disable_input()
        self._after_player_move()

    @work(thread=True, exclusive=True, group="engine")
    def _after_player_move(self) -> None:
        status_msg = (
            "Waiting for opponent..."
            if self._is_lichess
            else "Stockfish is thinking..."
        )
        self.app.call_from_thread(self._set_status, status_msg)
        try:
            # Get eval after player move (use copy for thread safety)
            board_copy = self.game.board.copy()
            eval_score, is_mate, mate_in = self.engine.get_evaluation(board_copy)
            self.app.call_from_thread(self._update_eval, eval_score, is_mate, mate_in)

            # Check if game is over (engine might not need to move)
            if board_copy.is_game_over():
                over_msg = self.game.get_game_over_message()
                if over_msg:
                    self.app.call_from_thread(self._show_game_over, over_msg)
                    return

            # Engine/bot move — use board copy for thread safety
            board_copy = self.game.board.copy()
            engine_move = self.engine.get_move(board_copy)

            # Apply engine move on main thread, then get eval on the
            # post-move board. We compute the post-move board here in
            # the worker so we don't race with call_from_thread.
            board_copy.push(engine_move)
            eval_score, is_mate, mate_in = self.engine.get_evaluation(board_copy)

            self.app.call_from_thread(self._apply_engine_move, engine_move)
            self.app.call_from_thread(self._update_eval, eval_score, is_mate, mate_in)
            self.app.call_from_thread(self._set_status, "")
            self.app.call_from_thread(self._enable_input)

        except chess.engine.EngineTerminatedError:
            self.app.call_from_thread(self._set_status, "Engine crashed!")
            self.app.call_from_thread(self._enable_input)
            self.engine = None
        except Exception as e:
            from chess_cli.lichess import LichessGameOver

            if isinstance(e, LichessGameOver):
                self.app.call_from_thread(self._handle_lichess_game_over, e)
            else:
                self.app.call_from_thread(
                    self._set_status, f"Error: {e}"
                )
                self.app.call_from_thread(self._enable_input)

    def _apply_engine_move(self, move: chess.Move) -> None:
        self.game.make_engine_move(move)
        self._update_display()
        over_msg = self.game.get_game_over_message()
        if over_msg:
            self._show_game_over(over_msg)

    # ------------------------------------------------------------------
    # Engine-first move (player is Black)
    # ------------------------------------------------------------------

    @work(thread=True, exclusive=True, group="engine")
    def _request_engine_move(self) -> None:
        status_msg = (
            "Waiting for opponent..."
            if self._is_lichess
            else "Stockfish is thinking..."
        )
        self.app.call_from_thread(self._set_status, status_msg)
        self.app.call_from_thread(self._disable_input)
        try:
            board_copy = self.game.board.copy()
            move = self.engine.get_move(board_copy)

            # Compute eval on post-move board before applying
            board_copy.push(move)
            eval_score, is_mate, mate_in = self.engine.get_evaluation(board_copy)

            self.app.call_from_thread(self._apply_engine_move, move)
            self.app.call_from_thread(self._update_eval, eval_score, is_mate, mate_in)
            self.app.call_from_thread(self._set_status, "")
            self.app.call_from_thread(self._enable_input)
        except chess.engine.EngineTerminatedError:
            self.app.call_from_thread(self._set_status, "Engine crashed!")
            self.app.call_from_thread(self._enable_input)
            self.engine = None
        except Exception as e:
            from chess_cli.lichess import LichessGameOver

            if isinstance(e, LichessGameOver):
                self.app.call_from_thread(self._handle_lichess_game_over, e)
            else:
                self.app.call_from_thread(
                    self._set_status, f"Error: {e}"
                )
                self.app.call_from_thread(self._enable_input)

    # ------------------------------------------------------------------
    # Command handlers
    # ------------------------------------------------------------------

    def _handle_undo(self) -> None:
        if self._is_lichess:
            self._set_status("Undo not available in online games.")
            return
        count = self.game.undo_move()
        if count > 0:
            self._update_display()
            if self.engine:
                self._refresh_eval()
        else:
            self._set_status("No moves to undo.")

    @work(thread=True, exclusive=True, group="engine")
    def _refresh_eval(self) -> None:
        try:
            board_copy = self.game.board.copy()
            eval_score, is_mate, mate_in = self.engine.get_evaluation(board_copy)
            self.app.call_from_thread(self._update_eval, eval_score, is_mate, mate_in)
        except chess.engine.EngineTerminatedError:
            self.engine = None

    def _handle_resign(self) -> None:
        if self._is_lichess and self.engine:
            self.engine.resign_game()
        if self.player_color == chess.WHITE:
            self.game.result = "0-1"
        else:
            self.game.result = "1-0"
        opponent = (
            self.lichess_bot.username if self._is_lichess else "Stockfish"
        )
        self._show_game_over(f"You resigned. {opponent} wins.")

    def _handle_draw(self) -> None:
        if self._is_lichess and self.engine:
            self.engine.offer_draw_game()
            self._set_status("Draw offer sent.")
            return
        if self.game.board.can_claim_draw():
            self.game.result = "1/2-1/2"
            self._show_game_over("Draw claimed.")
        else:
            self._set_status("Draw cannot be claimed in this position.")

    def _handle_save(self, cmd: list[str]) -> None:
        from datetime import datetime

        filename = (
            cmd[1]
            if len(cmd) > 1
            else f"game_{datetime.now().strftime('%Y%m%d_%H%M%S')}.pgn"
        )
        try:
            self.game.save_pgn(filename)
            self.notify(f"Saved to {filename}", title="Game saved")
        except OSError as e:
            self._set_status(f"Error saving: {e}")

    def _quit_game(self) -> None:
        if self._is_lichess and self.engine:
            self.engine.resign_game()
            if self.game and not self.game.result:
                if self.player_color == chess.WHITE:
                    self.game.result = "0-1"
                else:
                    self.game.result = "1-0"
        self.workers.cancel_group(self, "engine")
        self.app.pop_screen()

    # ------------------------------------------------------------------
    # Lichess game over
    # ------------------------------------------------------------------

    def _handle_lichess_game_over(self, event: LichessGameOver) -> None:
        status_map = {
            "mate": "Checkmate!",
            "resign": "Resignation.",
            "stalemate": "Stalemate. Draw.",
            "draw": "Draw.",
            "timeout": "Time out.",
            "outoftime": "Out of time.",
            "aborted": "Game aborted.",
        }
        msg = status_map.get(event.status, f"Game over: {event.status}")
        # Set PGN result (guard against game not yet initialized)
        if self.game:
            if event.winner == "white":
                self.game.result = "1-0"
            elif event.winner == "black":
                self.game.result = "0-1"
            elif event.status in ("stalemate", "draw"):
                self.game.result = "1/2-1/2"
            elif event.status in ("aborted",):
                self.game.result = "*"
            else:
                self.game.result = "1/2-1/2"
        if event.winner:
            is_player_win = (
                event.winner == "white" and self.player_color == chess.WHITE
            ) or (
                event.winner == "black" and self.player_color == chess.BLACK
            )
            if is_player_win:
                msg += " You win!"
            else:
                bot_name = (
                    self.lichess_bot.username if self.lichess_bot else "Opponent"
                )
                msg += f" {bot_name} wins."
        self._show_game_over(msg)

    # ------------------------------------------------------------------
    # Game over
    # ------------------------------------------------------------------

    def _show_game_over(self, msg: str) -> None:
        self._set_status(msg)
        self.notify(msg, title="Game Over", timeout=30)
        # Re-enable input so user can type 'save' or 'quit'
        self._enable_input()
        # Update placeholder to hint at available actions
        inp = self.query_one("#move-input", Input)
        inp.placeholder = "Type 'save' to save or 'quit' to exit..."

    # ------------------------------------------------------------------
    # Promotion helpers
    # ------------------------------------------------------------------

    def _is_promotion_attempt(self, san: str) -> bool:
        for move in self.game.board.legal_moves:
            if move.promotion:
                move_san = self.game.board.san(move)
                base = move_san.split("=")[0]
                if san == base:
                    return True
        return False

    def _prompt_promotion(self, base_san: str) -> None:
        from chess_cli.tui.widgets.promotion import PromotionModal

        def on_promotion(piece: str) -> None:
            try:
                move = self.game.make_move(base_san + "=" + piece)
                self._set_status("")
                self._update_display()
                if self._is_lichess:
                    try:
                        self.engine.send_player_move(move)
                    except Exception as e:
                        self.game.board.pop()
                        self.game.move_history.pop()
                        self.game.san_history.pop()
                        self._update_display()
                        self._set_status(f"Failed to send move: {e}")
                        return
                over_msg = self.game.get_game_over_message()
                if over_msg:
                    self._show_game_over(over_msg)
                    return
                self._disable_input()
                self._after_player_move()
            except (
                chess.InvalidMoveError,
                chess.IllegalMoveError,
                chess.AmbiguousMoveError,
            ):
                self._set_status("Invalid promotion move.")

        self.app.push_screen(PromotionModal(), callback=on_promotion)
