from __future__ import annotations

import re
import sys

from .fen import (
    ADVANCED_POSITION,
    BEGINNER_POSITION,
    INTERMEDIATE_POSITION,
    INTRO_POSITION,
    ParsedFEN,
    encode_fen,
    validate_fen,
)
from .move_gen import (
    generate_arata,
    generate_moves_for_square,
    is_square_attacked,
    would_be_in_check_after_move,
)
from .move_gen import (
    in_check as check_detection,
)
from .pgn import PGNOptions, encode_pgn, parse_pgn
from .piece_ids import assign_piece_ids_with_state
from .utils import (
    ARATA,
    ARCHER,
    BETRAY,
    BLACK,
    CANNON,
    CANONICAL_NAMES,
    ENGLISH_NAMES,
    FEN_CODES,
    FORTRESS,
    GENERAL,
    LANCER,
    LIEUTENANT_GENERAL,
    MAJOR_GENERAL,
    MARSHAL,
    MUSKETEER,
    RIDER,
    SOLDIER,
    SPY,
    SQUARES,
    TACTICIAN,
    TAKE,
    TSUKE,
    WARRIOR,
    WHITE,
    Board,
    Color,
    HandPiece,
    Move,
    SetupMode,
    get,
    get_top,
    is_game_over,
    piece,
    piece_to_fen_code,
    symbol_to_name,
)


def _color(text: str, color: str) -> str:
    codes = {"yellow": "33", "blue": "34", "green": "32"}
    return f"\033[{codes[color]}m{text}\033[39m"


def _dim(text: str) -> str:
    return f"\033[2m{text}\033[22m"


class Gungi:
    def __init__(self, fen: str | None = None) -> None:
        self._init_position = fen or INTRO_POSITION
        self._board: Board
        self._hand: list[HandPiece]
        self._turn: Color
        self._move_number: int
        self._drafting: dict[Color, bool]
        self._mode: SetupMode
        self._history: list[Move] = []
        self._previous_state: ParsedFEN | None = None
        self._comments: dict[str, str] = {}
        self._initialize_state(self._init_position)

    def _initialize_state(self, fen: str, move: Move | None = None) -> None:
        state_with_ids = assign_piece_ids_with_state(fen, self._previous_state, move)
        self._board = state_with_ids.board
        self._hand = state_with_ids.hand
        self._turn = state_with_ids.turn
        self._move_number = state_with_ids.move_number
        self._drafting = state_with_ids.drafting
        self._mode = state_with_ids.mode
        self._previous_state = state_with_ids

    def ascii(self, english: bool = False) -> str:
        tier_to_color = {1: "yellow", 2: "blue", 3: "green"}

        s = "\u3000\uff19\uff18\uff17\uff16\uff15\uff14\uff13\uff12\uff11\u3000\n"
        s += "\uff0b\u30fc\u30fc\u30fc\u30fc\u30fc\u30fc\u30fc\u30fc\u30fc\uff0b\n"
        base_digit = ord("\uff11")
        for rank in range(1, 10):
            s += "\uff5c"
            for file in range(9, 0, -1):
                square = self.get_top(f"{rank}-{file}")
                if not square:
                    s += "\u30fb"
                    continue

                code = piece_to_fen_code[symbol_to_name[square.type]]
                en = code.upper() if square.color == "w" else code
                raw_piece = chr(ord(en) + 0xFEE0) if english else square.type
                colored = _color(raw_piece, tier_to_color[square.tier])
                s += colored if square.color == "w" else _dim(colored)
            s += f"\uff5c{chr(base_digit + rank - 1)}\n"
        s += "\uff0b\u30fc\u30fc\u30fc\u30fc\u30fc\u30fc\u30fc\u30fc\u30fc\uff0b\n"
        return s

    def board(self) -> Board:
        return self._board

    def captured(self, color: Color | None = None) -> list[HandPiece]:
        all_captured: list[HandPiece] = []
        for move in self._history:
            if move.captured:
                for captured_piece in move.captured:
                    existing = next(
                        (
                            hand_piece
                            for hand_piece in all_captured
                            if hand_piece.type == captured_piece.type
                            and hand_piece.color == captured_piece.color
                        ),
                        None,
                    )
                    if existing:
                        existing.count += 1
                    else:
                        all_captured.append(
                            HandPiece(type=captured_piece.type, color=captured_piece.color, count=1)
                        )
        return (
            [hand_piece for hand_piece in all_captured if hand_piece.color == color]
            if color
            else all_captured
        )

    def clear(self) -> None:
        self._initialize_state(ADVANCED_POSITION)
        self._history = []
        self._comments.clear()

    def fen(self) -> str:
        return encode_fen(
            ParsedFEN(
                board=self._board,
                hand=self._hand,
                turn=self._turn,
                mode=self._mode,
                drafting=self._drafting,
                move_number=self._move_number,
            )
        )

    def get(self, square: str):
        return get(square, self._board)

    def get_drafting_rights(self, color: Color | None = None):
        return self._drafting[color] if color else self._drafting

    def get_top(self, square: str):
        return get_top(square, self._board)

    def hand(self, color: Color | None = None) -> list[HandPiece]:
        return (
            [hand_piece for hand_piece in self._hand if hand_piece.color == color]
            if color
            else self._hand
        )

    def history(self, verbose: bool = False):
        return self._history if verbose else [move.san for move in self._history]

    def in_draft(self) -> bool:
        return self._drafting["b"] or self._drafting["w"]

    def is_fourfold_repetition(self) -> bool:
        states = [move.before.split(" ")[0] for move in self._history]
        if states:
            states.append(self._history[-1].after.split(" ")[0])

        frequency: dict[str, int] = {}
        for state in states:
            frequency[state] = frequency.get(state, 0) + 1
        return max(frequency.values(), default=0) == 4

    def _has_escaping_move(self) -> bool:
        fen = self.fen()

        for square in SQUARES:
            for move in generate_moves_for_square(square, fen):
                if not would_be_in_check_after_move(move):
                    return True

        for hand_piece in self._hand:
            for move in generate_arata(hand_piece, fen):
                if not would_be_in_check_after_move(move):
                    return True

        return False

    def in_check(self, color: Color | None = None) -> bool:
        check_color = color or self.turn()
        return check_detection(check_color, self.fen())

    def is_checkmate(self) -> bool:
        if self.in_draft():
            return False
        if is_game_over(self._board):
            return False
        return self.in_check() and not self._has_escaping_move()

    def is_stalemate(self) -> bool:
        if self.in_draft():
            return False
        if is_game_over(self._board):
            return False
        return (not self.in_check()) and not self._has_escaping_move()

    def is_insufficient_material(self) -> bool:
        if self.in_draft():
            return False

        if any(hand_piece.type != piece["marshal"] for hand_piece in self._hand):
            return False

        pieces = [
            board_piece
            for rank in self._board
            for file in rank
            for board_piece in file
            if board_piece
        ]
        non_marshals = [
            board_piece for board_piece in pieces if board_piece.type != piece["marshal"]
        ]
        if non_marshals:
            return False

        marshals = [board_piece for board_piece in pieces if board_piece.type == piece["marshal"]]
        if len(marshals) != 2:
            return False

        marshal_1, marshal_2 = marshals
        rank_1, file_1 = map(int, marshal_1.square.split("-"))
        rank_2, file_2 = map(int, marshal_2.square.split("-"))

        rank_diff = abs(rank_1 - rank_2)
        file_diff = abs(file_1 - file_2)
        is_adjacent = rank_diff <= 1 and file_diff <= 1
        return not is_adjacent

    def is_draw(self) -> bool:
        return (
            self.is_stalemate() or self.is_fourfold_repetition() or self.is_insufficient_material()
        )

    def is_game_over(self) -> bool:
        if self.in_draft():
            return False
        if is_game_over(self._board):
            return True
        if self.is_fourfold_repetition():
            return True
        if self.is_insufficient_material():
            return True
        return self.is_checkmate() or self.is_stalemate()

    def load(self, fen: str) -> None:
        self._initialize_state(fen)

    def load_pgn(
        self, pgn: str, fen: str = ADVANCED_POSITION, opts: PGNOptions | None = None
    ) -> None:
        self._initialize_state(fen)
        self._history = []
        self._comments.clear()

        parsed = parse_pgn(pgn, opts=opts)
        for index, move in enumerate(parsed.moves):
            self.move(move)
            comment = parsed.comments.get(index)
            if comment:
                self.set_comment(comment)

    def move(self, move: str | Move) -> Move:
        moves = self.moves(verbose=True)

        def _field(obj, attr: str, alt: str | None = None):
            if isinstance(obj, dict):
                if attr in obj:
                    return obj[attr]
                if alt and alt in obj:
                    return obj[alt]
                return None
            return getattr(obj, attr, None)

        normalized_input = (
            move[:-1] if isinstance(move, str) and re.search(r"[=#]$", move) else None
        )

        found: Move | None = None
        if isinstance(move, str):
            found = next(
                (
                    candidate
                    for candidate in moves
                    if candidate.san == normalized_input or candidate.san == move
                ),
                None,
            )
        else:
            found = next(
                (
                    candidate
                    for candidate in moves
                    if _field(move, "piece") == candidate.piece
                    and _field(move, "type") == candidate.type
                    and (_field(move, "from_") or _field(move, "from")) == candidate.from_
                    and _field(move, "to") == candidate.to
                    and _field(move, "captured") is candidate.captured
                    and _field(move, "draft_finished", "draftFinished") == candidate.draft_finished
                ),
                None,
            )

        if not found:
            raise ValueError(f"Illegal move: {move}")

        self._history.append(found)
        self._initialize_state(found.after, found)

        if self.is_checkmate():
            self._history[-1].san += "#"
        elif (
            self.is_stalemate() or self.is_fourfold_repetition() or self.is_insufficient_material()
        ):
            self._history[-1].san += "="

        return found

    def moves(
        self,
        square: str | None = None,
        arata: HandPiece | None = None,
        verbose: bool = False,
    ) -> list[Move | str]:
        if self.is_game_over():
            return []

        if square:
            moves = generate_moves_for_square(square, self.fen())
        elif arata:
            moves = generate_arata(arata, self.fen())
        elif self.in_draft():
            moves = [
                move
                for hand_piece in self.hand()
                for move in generate_arata(hand_piece, self.fen())
            ]
        else:
            board_moves = [
                move
                for square_name in SQUARES
                for move in generate_moves_for_square(square_name, self.fen())
            ]
            hand_moves = [
                move
                for hand_piece in self.hand()
                for move in generate_arata(hand_piece, self.fen())
            ]
            moves = [*board_moves, *hand_moves]

        return moves if verbose else [move.san for move in moves]

    def move_number(self) -> int:
        return self._move_number

    def pgn(self, opts: PGNOptions | None = None) -> str:
        return encode_pgn(self._history, opts, comments=self._comments)

    def print(self, english: bool = False) -> None:
        sys.stdout.write(f"{self.ascii(english=english)}\n")

    def reset(self) -> None:
        self._initialize_state(self._init_position)
        self._history = []
        self._comments.clear()

    def turn(self) -> Color:
        return self._turn

    def undo(self) -> Move | None:
        if not self._history:
            return None

        last_move = self._history.pop()
        self._initialize_state(last_move.before)
        return last_move

    def set_comment(self, comment: str) -> None:
        self._comments[self.fen()] = comment

    def get_comment(self) -> str | None:
        return self._comments.get(self.fen())

    def remove_comment(self) -> str | None:
        return self._comments.pop(self.fen(), None)

    def get_comments(self) -> list[dict[str, str]]:
        return [{"fen": fen, "comment": comment} for fen, comment in self._comments.items()]

    def remove_comments(self) -> list[dict[str, str]]:
        comments = self.get_comments()
        self._comments.clear()
        return comments

    def validate_fen(self, fen: str):
        return validate_fen(fen)

    # CamelCase aliases for close JS API parity
    getDraftingRights = get_drafting_rights
    getTop = get_top
    inDraft = in_draft
    isFourfoldRepetition = is_fourfold_repetition
    inCheck = in_check
    isCheckmate = is_checkmate
    isStalemate = is_stalemate
    isInsufficientMaterial = is_insufficient_material
    isDraw = is_draw
    isGameOver = is_game_over
    loadPgn = load_pgn
    moveNumber = move_number
    setComment = set_comment
    getComment = get_comment
    removeComment = remove_comment
    getComments = get_comments
    removeComments = remove_comments
    validateFen = validate_fen


# Re-exports
__all__ = [
    "Gungi",
    "WHITE",
    "BLACK",
    "MARSHAL",
    "GENERAL",
    "LIEUTENANT_GENERAL",
    "MAJOR_GENERAL",
    "WARRIOR",
    "LANCER",
    "RIDER",
    "SPY",
    "FORTRESS",
    "SOLDIER",
    "CANNON",
    "ARCHER",
    "MUSKETEER",
    "TACTICIAN",
    "TSUKE",
    "TAKE",
    "BETRAY",
    "ARATA",
    "INTRO_POSITION",
    "BEGINNER_POSITION",
    "INTERMEDIATE_POSITION",
    "ADVANCED_POSITION",
    "SQUARES",
    "CANONICAL_NAMES",
    "ENGLISH_NAMES",
    "FEN_CODES",
    "validate_fen",
    "is_square_attacked",
]
