from contextlib import redirect_stdout
from io import StringIO
import json
import re
import skor_estetika as scorer


def mirror_move(move):
    return move.translate(str.maketrans("12345678", "87654321"))


def playback(board, moves, white_to_move=True):
    def notation(pos):
        return chr(97 + pos[0]) + str(pos[1] + 1)

    def position():
        return "/".join("".join(board.get((file, rank), "1") for file in range(8))
                        for rank in range(7, -1, -1))

    frames = [position()]
    san = []
    for index, move in enumerate(moves):
        white = (index % 2 == 0) == white_to_move
        source, target = scorer.square(move[:2]), scorer.square(move[2:4])
        kind = board[source].upper()
        capture = target in board
        prefix = kind if kind != "P" else (notation(source)[0] if capture else "")
        if kind != "P":
            rivals = [origin for origin, dest in scorer.legal_moves(board, white)
                      if dest == target and origin != source and board[origin] == board[source]]
            if rivals:
                square = notation(source)
                if all(origin[0] != source[0] for origin in rivals):
                    prefix += square[0]
                elif all(origin[1] != source[1] for origin in rivals):
                    prefix += square[1]
                else:
                    prefix += square
        promotion = move[4:].upper()
        move_san = prefix + ("x" if capture else "") + notation(target)
        if promotion:
            move_san += "=" + promotion
        board = scorer.play(board, source, target,
                            (promotion if white else promotion.lower()) or None)
        if scorer.in_check(board, not white):
            move_san += "+" if any(scorer.legal_moves(board, not white)) else "#"
        san.append(move_san)
        frames.append(position())
    return san, frames


def analyze_json(request):
    try:
        data = json.loads(request)
        fen = data["fen"].split()
        if len(fen) != 6:
            raise ValueError("Enter a complete six-field FEN.")
        rows = fen[0].split("/")
        if len(rows) != 8 or any(
            not re.fullmatch(r"[prnbqkPRNBQK1-8]+", row)
            or sum(int(c) if c.isdigit() else 1 for c in row) != 8 for row in rows
        ):
            raise ValueError("The FEN piece placement is invalid.")
        if fen[1] not in ("w", "b") or fen[2:4] != ["-", "-"]:
            raise ValueError("Use a White- or Black-to-move FEN without castling or en passant rights.")
        if not fen[4].isdigit() or not fen[5].isdigit() or int(fen[5]) < 1:
            raise ValueError("The FEN move counters are invalid.")
        original_fen = " ".join(fen)
        mirrored = fen[1] == "b"
        first_move = int(fen[5])
        if mirrored:
            fen[0] = "/".join(row.swapcase() for row in reversed(rows))
            fen[1] = "w"
        fen = " ".join(fen)
        board = scorer.scoring_position(fen)
        if scorer.in_check(board, False):
            raise ValueError("The side waiting to move cannot already be in check.")
        mate_moves = data.get("mate_moves", 3)
        if type(mate_moves) is not int or mate_moves not in (2, 3, 4, 5):
            raise ValueError("Choose mate in 2, 3, 4, or 5.")
        model = data.get("model", "2008")
        if model not in scorer.MODELS:
            raise ValueError("Choose model 2008, 2010, or 2012.")
        preset = scorer.MODELS[model]
        moves = data["moves"].lower().split()
        if any(not re.fullmatch(r"[a-h][1-8][a-h][1-8][qrbn]?", move) for move in moves):
            raise ValueError("Enter legal moves in UCI notation, including any promotion suffix.")
        if mirrored:
            moves = [mirror_move(move) for move in moves]
        depth_error = f"This position has no forced mate whose shortest length is exactly {mate_moves} moves."

        def evaluate(line, **options):
            return scorer.evaluate(fen, line, 2, dissertation=True,
                                   overlap_rule="pairs", mate_moves=mate_moves,
                                   p2_2012=preset.get("p2_2012", False), **options)

        if data.get("action") == "tree":
            key = moves[0] if moves else None
            tree = scorer.mate_tree(board, mate_moves, exact=True, key=key,
                                    node_limit=2_000_000, line_limit=10_000)
            if tree[0] != mate_moves or not tree[1]:
                reason = (depth_error if tree[0] != mate_moves else
                          "The first move of this line is not a forced-mate key.")
                return json.dumps({"tree": {"index": None, "reason": reason}})
            summaries = {}
            with redirect_stdout(StringIO()):
                scorer.print_solution_tree(fen, evaluate, tree=tree, summaries=summaries,
                                           mate_moves=mate_moves)
            summary = summaries[key]
            summary["key"] = mirror_move(key) if mirrored else key
            if summary["index"] is None:
                summary["reason"] = "At least two defenses are needed to compare motifs."
            return json.dumps({"tree": summary})
        automatic = not moves
        if automatic:
            minimum, lines, _ = scorer.mate_tree(board, mate_moves, exact=True,
                                                 single_line=True, node_limit=2_000_000)
            if minimum != mate_moves:
                raise ValueError(depth_error)
            moves = lines[0]
        elif len(moves) != 2 * mate_moves - 1:
            raise ValueError(f"Enter {2 * mate_moves - 1} UCI moves, "
                             f"{mate_moves} moves by the mating side and {mate_moves - 1} replies.")
        try:
            scores = evaluate(moves)
        except ValueError:
            raise ValueError("The line is illegal or does not end in checkmate.") from None
        if mirrored:
            moves = [mirror_move(move) for move in moves]
        san, frames = playback(scorer.read_position(original_fen), moves, white_to_move=not mirrored)
        if "split" in preset:
            total = scorer.expected_stochastic(scores, scorer.SPLITS[preset["split"]],
                                               cap=preset.get("cap", False))
        else:
            total = sum(scores.values())
        return json.dumps({"san": san, "frames": frames, "first_move": first_move, "mirrored": mirrored, "model": model, "total": float(total), "moves": moves,
                           "scores": {k: float(v) for k,v in scores.items()},
                           "automatic": automatic})
    except ValueError as error:
        return json.dumps({"error": str(error)})
