#!/usr/bin/env python3
"""
HyperTwist IPC wrapper for brownan GPL Rubik's Cube solver oracle.

Input: Either a 54-character facelet string (kociemba order: U R F D L B)
       or a 120-character brownan raw cube string.

Output: Space-separated WCA move notation (U, D, F, B, L, R, with ' and 2 suffixes).
        Brownan native output uses faces F, T, L, B, D, R where T=U in WCA.
"""
import argparse
import os
import subprocess
import sys


# Map brownan native face letters to WCA face letters
BROWNAN_TO_WCA = {
    "T": "U",  # Top in brownan = Up in WCA
    "D": "D",
    "F": "F",
    "B": "B",
    "L": "L",
    "R": "R",
}


def find_brownan_exe():
    """Locate the brownan solver executable next to this wrapper."""
    script_dir = os.path.dirname(os.path.abspath(__file__))
    candidates = [
        os.path.join(script_dir, "..", "..", "UnrealHyperTwist", "Binaries", "Win64", "Solvers", "brownan-solver.exe"),
        os.path.join(script_dir, "brownan-solver.exe"),
        os.path.join(script_dir, "..", "brownan-solver.exe"),
        "brownan-solver.exe",
        "brownan-solver",
    ]
    for candidate in candidates:
        resolved = os.path.abspath(candidate)
        if os.path.isfile(resolved):
            return resolved
    return None


def ensure_tables(brownan_exe: str) -> bool:
    """Check that brownan heuristic tables exist next to the executable."""
    exe_dir = os.path.dirname(os.path.abspath(brownan_exe))
    tables = ["table_corner.rht", "table_edge1.rht", "table_edge2.rht"]
    missing = [t for t in tables if not os.path.exists(os.path.join(exe_dir, t))]
    if missing:
        print(
            f"WARNING: Brownan tables missing: {missing}. "
            "Solving without tables will be very slow. "
            "Run table generation first by invoking brownan-solver.exe interactively "
            "and selecting options 2, 3, and 4.",
            file=sys.stderr,
        )
        return False
    return True


def convert_move(brownan_move: str) -> str:
    """Convert a single brownan move token to WCA notation."""
    token = brownan_move.strip()
    if not token:
        return ""

    if token.startswith("2"):
        face = token[1]
        suffix = "2"
    elif token.endswith("'"):
        face = token[0]
        suffix = "'"
    else:
        face = token[0]
        suffix = ""

    wca_face = BROWNAN_TO_WCA.get(face, face)
    return f"{wca_face}{suffix}"


def facelet_to_brownan_raw(facelet: str) -> str:
    """Convert a 54-character kociemba facelet string to brownan 120-char raw format."""
    if len(facelet) != 54:
        raise ValueError(f"Facelet string must be 54 characters, got {len(facelet)}")

    # Map input face labels/colors to brownan's fixed color scheme based on centers.
    # Brownan fixed centers: U=r, L=b, F=w, R=g, B=y, D=o
    color_map = {
        facelet[4]: "r",    # U center
        facelet[13]: "g",   # R center
        facelet[22]: "w",   # F center
        facelet[31]: "o",   # D center
        facelet[40]: "b",   # L center
        facelet[49]: "y",   # B center
    }

    if len(set(color_map.keys())) != 6:
        raise ValueError("Invalid facelet string: center pieces are not all distinct")

    def remap(s: str) -> str:
        return "".join(color_map.get(c, c) for c in s)

    # Build 48-position brownan layout string (1-indexed internally).
    # brownan layout (centers fixed):
    #          1  2  3
    #          4  R  5
    #          6  7  8
    # 9 10 11  12 13 14  15 16 17   18 19 20
    # 21 B 22  23 W 24  25 G 26    27 Y 28
    # 29 30 31  32 33 34  35 36 37   38 39 40
    #         41  42 43
    #         44  O 45
    #         46  47 48
    layout = [""] * 49

    u = remap(facelet[0:9])
    layout[1], layout[2], layout[3] = u[0], u[1], u[2]
    layout[4], layout[5] = u[3], u[5]
    layout[6], layout[7], layout[8] = u[6], u[7], u[8]

    r = remap(facelet[9:18])
    layout[15], layout[16], layout[17] = r[0], r[1], r[2]
    layout[25], layout[26] = r[3], r[5]
    layout[35], layout[36], layout[37] = r[6], r[7], r[8]

    f = remap(facelet[18:27])
    layout[12], layout[13], layout[14] = f[0], f[1], f[2]
    layout[23], layout[24] = f[3], f[5]
    layout[32], layout[33], layout[34] = f[6], f[7], f[8]

    d = remap(facelet[27:36])
    layout[41], layout[42], layout[43] = d[0], d[1], d[2]
    layout[44], layout[45] = d[3], d[5]
    layout[46], layout[47], layout[48] = d[6], d[7], d[8]

    l = remap(facelet[36:45])
    layout[9], layout[10], layout[11] = l[0], l[1], l[2]
    layout[21], layout[22] = l[3], l[5]
    layout[29], layout[30], layout[31] = l[6], l[7], l[8]

    # Back face may need mirroring/rotation in the unfolded net.
    # Direct mapping is used here; adjust if brownan rejects valid cubes.
    b = remap(facelet[45:54])
    layout[18], layout[19], layout[20] = b[0], b[1], b[2]
    layout[27], layout[28] = b[3], b[5]
    layout[38], layout[39], layout[40] = b[6], b[7], b[8]

    # Original cube_convert.py prepends '@' because the author used wrong indexes.
    cc = "@" + "".join(layout[1:49])

    # Extract 20 cubies exactly as cube_convert.py does.
    cubies = [
        "nn%s%s%sn" % (cc[29], cc[40], cc[46]),
        "nnn%s%sn" % (cc[39], cc[47]),
        "nnn%s%s%s" % (cc[38], cc[48], cc[37]),
        "nn%s%snn" % (cc[21], cc[28]),
        "nnn%sn%s" % (cc[27], cc[26]),
        "n%s%s%snn" % (cc[1], cc[9], cc[20]),
        "n%sn%snn" % (cc[2], cc[19]),
        "n%sn%sn%s" % (cc[3], cc[18], cc[17]),
        "nn%sn%sn" % (cc[30], cc[44]),
        "nnnn%s%s" % (cc[45], cc[36]),
        "n%s%snnn" % (cc[4], cc[10]),
        "n%snnn%s" % (cc[5], cc[16]),
        "%sn%sn%sn" % (cc[32], cc[31], cc[41]),
        "%snnn%sn" % (cc[33], cc[42]),
        "%snnn%s%s" % (cc[34], cc[43], cc[35]),
        "%sn%snnn" % (cc[23], cc[22]),
        "%snnnn%s" % (cc[24], cc[25]),
        "%s%s%snnn" % (cc[12], cc[6], cc[11]),
        "%s%snnnn" % (cc[13], cc[7]),
        "%s%snnn%s" % (cc[14], cc[8], cc[15]),
    ]

    def select(pred):
        matches = [c for c in cubies if pred(c)]
        if not matches:
            raise ValueError("Invalid cube: not all cubies accounted for")
        return matches[0]

    fc = ""
    fc += select(lambda a: "y" in a and "b" in a and "o" in a)
    fc += select(lambda a: "y" in a and "o" in a and a.count("n") == 4)
    fc += select(lambda a: "y" in a and "o" in a and "g" in a)
    fc += select(lambda a: "y" in a and "b" in a and a.count("n") == 4)
    fc += select(lambda a: "y" in a and "g" in a and a.count("n") == 4)
    fc += select(lambda a: "y" in a and "b" in a and "r" in a)
    fc += select(lambda a: "y" in a and "r" in a and a.count("n") == 4)
    fc += select(lambda a: "y" in a and "r" in a and "g" in a)
    fc += select(lambda a: "o" in a and "b" in a and a.count("n") == 4)
    fc += select(lambda a: "o" in a and "g" in a and a.count("n") == 4)
    fc += select(lambda a: "b" in a and "r" in a and a.count("n") == 4)
    fc += select(lambda a: "g" in a and "r" in a and a.count("n") == 4)
    fc += select(lambda a: "w" in a and "b" in a and "o" in a)
    fc += select(lambda a: "w" in a and "o" in a and a.count("n") == 4)
    fc += select(lambda a: "w" in a and "o" in a and "g" in a)
    fc += select(lambda a: "w" in a and "b" in a and a.count("n") == 4)
    fc += select(lambda a: "w" in a and "g" in a and a.count("n") == 4)
    fc += select(lambda a: "w" in a and "b" in a and "r" in a)
    fc += select(lambda a: "w" in a and "r" in a and a.count("n") == 4)
    fc += select(lambda a: "w" in a and "r" in a and "g" in a)

    if len(fc) != 120:
        raise ValueError(f"Conversion error: final code length is {len(fc)}, expected 120")

    return fc


def solve(cube_input: str, brownan_exe: str = None) -> str:
    """Solve a cube from either a 54-char facelet or 120-char brownan raw string."""
    cube_input = cube_input.strip()

    if len(cube_input) == 54:
        raw_cube = facelet_to_brownan_raw(cube_input)
    elif len(cube_input) == 120:
        raw_cube = cube_input
    else:
        print(f"ERROR: Input must be 54-char facelet or 120-char brownan raw, got {len(cube_input)}", file=sys.stderr)
        sys.exit(1)

    if brownan_exe is None:
        brownan_exe = find_brownan_exe()
    if not brownan_exe or not os.path.exists(brownan_exe):
        print("ERROR: brownan-solver executable not found", file=sys.stderr)
        sys.exit(1)

    ensure_tables(brownan_exe)

    exe_dir = os.path.dirname(os.path.abspath(brownan_exe))
    try:
        result = subprocess.run(
            [brownan_exe, raw_cube],
            cwd=exe_dir,
            text=True,
            capture_output=True,
            timeout=300,
        )
    except subprocess.TimeoutExpired:
        print("ERROR: Brownan solver timed out (consider generating heuristic tables)", file=sys.stderr)
        sys.exit(1)
    except Exception as e:
        print(f"ERROR: {e}", file=sys.stderr)
        sys.exit(1)

    lines = result.stdout.splitlines()
    in_solution = False
    moves = []
    for line in lines:
        stripped = line.strip()
        if stripped == "Solution found!":
            in_solution = True
            continue
        if in_solution:
            if not stripped:
                continue
            # Skip the "Continuing to look..." status line; the actual move
            # sequence is on the following line.
            if stripped.startswith("Continuing"):
                continue
            for token in stripped.split():
                wca = convert_move(token)
                if wca:
                    moves.append(wca)
            break

    return " ".join(moves)


if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="HyperTwist Brownan Solver Wrapper")
    parser.add_argument("cube_input", help="54-char facelet string or 120-char brownan raw string")
    parser.add_argument("--exe", help="Path to brownan-solver executable")
    args = parser.parse_args()

    result = solve(args.cube_input, args.exe)
    print(result)
