from __future__ import annotations

from dataclasses import dataclass

from .utils import (
    Board,
    Color,
    HandPiece,
    Piece,
    SetupMode,
    create_hand_piece_from_fen_code,
    create_piece_from_fen_code,
    setup_code_to_mode,
    setup_mode_to_code,
)

INTRO_POSITION = "3img3/1s2n2s1/d1fwdwf1d/9/9/9/D1FWDWF1D/1S2N2S1/3GMI3 J2N2R2D1/j2n2r2d1 w 0 - 1"
BEGINNER_POSITION = (
    "3img3/1ra1n1as1/d1fwdwf1d/9/9/9/D1FWDWF1D/1SA1N1AR1/3GMI3 J2N2S1R1D1/j2n2s1r1d1 w 1 - 1"
)
INTERMEDIATE_POSITION = (
    "9/9/9/9/9/9/9/9/9 M1G1I1J2W2N3R2S2F2D4C1A2K1T1/m1g1i1j2w2n3r2s2f2d4c1a2k1t1 w 2 wb 1"
)
ADVANCED_POSITION = (
    "9/9/9/9/9/9/9/9/9 M1G1I1J2W2N3R2S2F2D4C1A2K1T1/m1g1i1j2w2n3r2s2f2d4c1a2k1t1 w 3 wb 1"
)


@dataclass
class ParsedFEN:
    board: Board
    hand: list[HandPiece]
    turn: Color
    mode: SetupMode
    drafting: dict[Color, bool]
    move_number: int


def parse_fen(fen: str) -> ParsedFEN:
    result = validate_fen(fen)
    if not result["ok"]:
        raise ValueError(f"Invalid FEN: {result['error']}")

    placement, handpieces, turn, mode_code, drafting_code, move_number = fen.split(" ")

    board: Board = []
    for y, rank in enumerate(placement.split("/")):
        outer: list[list[Piece | None]] = []
        for partial in rank.split("|"):
            if ":" in partial:
                tower: list[Piece] = []
                for z, piece_code in enumerate(partial.split(":")):
                    tower.append(
                        create_piece_from_fen_code(piece_code, (y + 1, 9 - len(outer), z + 1))
                    )
                outer.append(tower)
            else:
                partial_pieces: list[Piece | None] = []
                for ch in partial:
                    if ch.isdigit():
                        partial_pieces.extend([None] * int(ch))
                    else:
                        partial_pieces.append(
                            create_piece_from_fen_code(
                                ch, (y + 1, 9 - len(outer) - len(partial_pieces), 1)
                            )
                        )
                outer.extend([[sq] for sq in partial_pieces])
        board.append(outer)

    hand: list[HandPiece] = []
    for player_hand in handpieces.split("/"):
        if player_hand == "-":
            continue
        for i in range(0, len(player_hand), 2):
            piece_code = player_hand[i]
            count = int(player_hand[i + 1])
            hand.append(create_hand_piece_from_fen_code(piece_code, count))

    return ParsedFEN(
        board=board,
        hand=hand,
        turn=turn,  # type: ignore[arg-type]
        mode=setup_code_to_mode[int(mode_code)],
        drafting={"w": "w" in drafting_code, "b": "b" in drafting_code},
        move_number=int(move_number),
    )


def encode_fen(parsed: ParsedFEN) -> str:
    placement = ""
    empty_count = 0

    for file_index in range(9):
        for rank_index in range(9):
            square = parsed.board[file_index][rank_index]
            if square[0] is None:
                empty_count += 1
                continue

            if empty_count:
                placement += str(empty_count)
                empty_count = 0

            if len(square) > 1:
                placement += (
                    "|"
                    + ":".join(
                        (p and _piece_to_fen_char(p) or "")
                        for p in square  # type: ignore[arg-type]
                    )
                    + "|"
                )
            else:
                piece = square[0]
                assert piece is not None
                placement += _piece_to_fen_char(piece)
        placement += f"{empty_count}/" if empty_count else "/"
        empty_count = 0

    hand = ""
    white_hand = [p for p in parsed.hand if p.color == "w"]
    black_hand = [p for p in parsed.hand if p.color == "b"]

    for hp in white_hand:
        hand += f"{_piece_to_fen_char(Piece(square='', tier=0, type=hp.type, color=hp.color))}{hp.count}"
    hand += "/" if white_hand else "-/"
    for hp in black_hand:
        hand += f"{_piece_to_fen_char(Piece(square='', tier=0, type=hp.type, color=hp.color))}{hp.count}"
    hand += "" if black_hand else "-"

    draft_keys = [c for c, allowed in parsed.drafting.items() if allowed]
    draft_str = "".join(draft_keys) if draft_keys else "-"

    return (
        f"{placement[:-1]} {hand} {parsed.turn} {setup_mode_to_code[parsed.mode]} "
        f"{draft_str} {parsed.move_number}"
    )


def _piece_to_fen_char(piece: Piece) -> str:
    from .utils import piece_to_fen_code, symbol_to_name

    code = piece_to_fen_code[symbol_to_name[piece.type]]
    return code.upper() if piece.color == "w" else code


def validate_fen(fen: str) -> dict[str, object]:
    parts = fen.split(" ")
    if len(parts) != 6:
        return {"ok": False, "error": f"expected 6 fields, received {len(parts)}"}

    placement, handpieces, turn, mode_code, drafting_code, move_number = parts

    ranks = placement.split("/")
    if len(ranks) != 9:
        return {
            "ok": False,
            "error": f"1st field (piece positions) is invalid [expected 9 ranks, received {len(ranks)}]",
        }

    for i, rank in enumerate(ranks):
        file_parts = rank.split("|")
        file_count = 0
        for file_part in file_parts:
            if ":" in file_part:
                file_count += 1
                tower_pieces = file_part.split(":")
                if len(tower_pieces) > 3:
                    return {
                        "ok": False,
                        "error": f"1st field (piece positions) is invalid [tower size] @({i + 1}-{file_count})",
                    }
                if not all(ch in "mgijwnrsfdcaktMGIJWNRSFDCAKT" for ch in tower_pieces):
                    return {
                        "ok": False,
                        "error": f"1st field (piece positions) is invalid [invalid piece] @({i + 1}-{file_count})",
                    }
            else:
                for ch in file_part:
                    if ch.isdigit():
                        file_count += int(ch)
                    else:
                        file_count += 1
                        if ch not in "mgijwnrsfdcaktMGIJWNRSFDCAKT":
                            return {
                                "ok": False,
                                "error": f"1st field (piece positions) is invalid [invalid piece] @({i + 1}-{file_count})",
                            }
        if file_count != 9:
            return {
                "ok": False,
                "error": f"1st field (piece positions) is invalid [expected 9 squares, received {file_count}] in rank: {i + 1}",
            }

    hands = handpieces.split("/")
    if len(hands) != 2:
        return {
            "ok": False,
            "error": f"2nd field (hand pieces) is invalid [expected 2 hands, received {len(hands)}]",
        }

    white_hand, black_hand = hands
    if (len(white_hand) == 1 and white_hand != "-") or (len(black_hand) == 1 and black_hand != "-"):
        return {"ok": False, "error": "2nd field (hand pieces) is invalid [invalid piece]"}

    if (len(white_hand) > 1 and len(white_hand) % 2 != 0) or (
        len(black_hand) > 1 and len(black_hand) % 2 != 0
    ):
        return {"ok": False, "error": "2nd field (hand pieces) is invalid [invalid piece count]"}

    if len(white_hand) > 1:
        for i in range(0, len(white_hand), 2):
            if white_hand[i] not in "MGIJWNRSFDCAKT":
                return {"ok": False, "error": "2nd field (hand pieces) is invalid [invalid piece]"}
            if not white_hand[i + 1].isdigit():
                return {"ok": False, "error": "2nd field (hand pieces) is invalid [invalid count]"}

    if len(black_hand) > 1:
        for i in range(0, len(black_hand), 2):
            if black_hand[i] not in "mgijwnrsfdcakt":
                return {"ok": False, "error": "2nd field (hand pieces) is invalid [invalid piece]"}
            if not black_hand[i + 1].isdigit():
                return {"ok": False, "error": "2nd field (hand pieces) is invalid [invalid count]"}

    if turn not in ("w", "b"):
        return {
            "ok": False,
            "error": f"3rd field (active player) is invalid [expected 'w' or 'b', received {turn}]",
        }

    if len(mode_code) != 1 or mode_code not in "0123":
        return {
            "ok": False,
            "error": f"4th field (setup mode) is invalid [expected '0', '1', '2', or '3', received {mode_code}]",
        }

    if drafting_code not in {"-", "w", "b", "wb"}:
        return {
            "ok": False,
            "error": f"5th field (drafting availability) is invalid [expected '-', 'w', 'b', or 'wb', received {drafting_code}]",
        }

    try:
        int(move_number)
    except ValueError:
        return {
            "ok": False,
            "error": f"6th field (full move number) is invalid [expected an integer, received {move_number}]",
        }

    return {"ok": True}


# Aliases for JS-style naming
parseFEN = parse_fen
encodeFEN = encode_fen
validateFen = validate_fen
ParsedFENType = ParsedFEN
