#!/usr/bin/env python3
"""Brute nibble/byte/spot permutations for SuperGold Bingo 0x30 card slots."""

from __future__ import annotations

import itertools
from dataclasses import dataclass
from typing import Callable

Grid = tuple[tuple[int | None, ...], ...]
CENTER = (2, 2)


def col_table_to_rows(b: tuple, i: tuple, n: tuple, g: tuple, o: tuple) -> Grid:
    cols = (b, i, n, g, o)
    rows: list[tuple[int | None, ...]] = []
    for r in range(5):
        row: list[int | None] = []
        for c in range(5):
            if (r, c) == CENTER:
                row.append(None)
            else:
                v = cols[c][r]
                row.append(None if v == "FR" else int(v))
        rows.append(tuple(row))
    return tuple(rows)


def grid_numbers(grid: Grid) -> frozenset[int]:
    return frozenset(v for row in grid for v in row if v is not None)


def spot_orders() -> dict[str, list[tuple[int, int]]]:
    row_m = [(divmod(s, 5)[0], divmod(s, 5)[1]) for s in range(25) if s != 12]
    col_m: list[tuple[int, int]] = []
    for c in range(5):
        for r in range(5):
            if (r, c) != CENTER:
                col_m.append((r, c))
    return {
        "row_major": row_m,
        "row_major_rev": list(reversed(row_m)),
        "col_major": col_m,
        "col_major_rev": list(reversed(col_m)),
    }


def extract_nibbles(data: bytes, *, nibble_order: str) -> list[int]:
    out: list[int] = []
    if nibble_order == "low_first":
        for b in data:
            out.append(b & 0x0F)
            out.append((b >> 4) & 0x0F)
    elif nibble_order == "high_first":
        for b in data:
            out.append((b >> 4) & 0x0F)
            out.append(b & 0x0F)
    elif nibble_order == "legacy_pair_low":
        for j in range(24):
            out.append((data[j // 2] >> ((j & 1) * 4)) & 0x0F)
    else:
        raise ValueError(nibble_order)
    return out[:24]


def bit_rev4(n: int) -> int:
    n &= 0x0F
    return int(f"{n:04b}"[::-1], 2)


NUM_MAPS: dict[str, Callable[[int, int], int | None]] = {
    "col_local+1": lambda col, nib: col * 15 + nib + 1,
    "col_local+0": lambda col, nib: col * 15 + nib,
    "row_local+1": lambda col, nib: 0 * 15 + nib + 1,  # placeholder patched below
    "abs_nib+1": lambda _col, nib: nib + 1,
    "abs_nib": lambda _col, nib: nib,
}


def num_from(row: int, col: int, nib: int, num_map: str) -> int | None:
    if num_map == "row_local+1":
        val = row * 15 + nib + 1
    else:
        val = NUM_MAPS[num_map](col, nib)
    if 1 <= val <= 75:
        return val
    return None


def decode_block(
    block: bytes,
    *,
    byte_rev: bool,
    nibble_order: str,
    spot_name: str,
    num_map: str,
    bit_rev: bool,
) -> Grid | None:
    if len(block) != 12 or all(b == 0 for b in block):
        return None
    data = bytes(reversed(block)) if byte_rev else block
    nibbles = extract_nibbles(data, nibble_order=nibble_order)
    if len(nibbles) != 24:
        return None
    spots = spot_orders()[spot_name]
    grid: list[list[int | None]] = [[0] * 5 for _ in range(5)]
    grid[CENTER[0]][CENTER[1]] = None
    for nib, (r, c) in zip(nibbles, spots):
        if bit_rev:
            nib = bit_rev4(nib)
        val = num_from(r, c, nib, num_map)
        if val is None:
            return None
        grid[r][c] = val
    return tuple(tuple(row) for row in grid)


def decode_block_raw_bytes(block: bytes, *, byte_rev: bool, spot_name: str) -> Grid | None:
    """Hypothesis: each byte is a ball number 1–75 (only 12 spots — rest invalid)."""
    if len(block) != 12 or all(b == 0 for b in block):
        return None
    data = list(reversed(block)) if byte_rev else list(block)
    vals = [b for b in data if 1 <= b <= 75]
    if len(vals) < 12:
        return None
    spots = spot_orders()[spot_name][:12]
    grid: list[list[int | None]] = [[None] * 5 for _ in range(5)]
    grid[CENTER[0]][CENTER[1]] = None
    for v, (r, c) in zip(vals[:12], spots):
        grid[r][c] = v
    return tuple(tuple(row) for row in grid)


def decode_block_byte_pairs(block: bytes, *, byte_rev: bool, spot_name: str, pair_endian: str) -> Grid | None:
    """Hypothesis: 6 big-endian or little-endian 16-bit values map to 6 spots (partial)."""
    if len(block) != 12:
        return None
    data = bytes(reversed(block)) if byte_rev else block
    nums: list[int] = []
    for i in range(0, 12, 2):
        if pair_endian == "be":
            n = (data[i] << 8) | data[i + 1]
        else:
            n = data[i] | (data[i + 1] << 8)
        if 1 <= n <= 75:
            nums.append(n)
        elif 1 <= data[i] <= 75 and 1 <= data[i + 1] <= 75:
            nums.extend([data[i], data[i + 1]])
        else:
            return None
    if len(nums) < 6:
        return None
    spots = spot_orders()[spot_name][: min(len(nums), 24)]
    grid: list[list[int | None]] = [[None] * 5 for _ in range(5)]
    grid[CENTER[0]][CENTER[1]] = None
    for v, (r, c) in zip(nums[: len(spots)], spots):
        grid[r][c] = v
    return tuple(tuple(row) for row in grid)


def overlap(a: Grid, b: Grid) -> int:
    n = 0
    for r in range(5):
        for c in range(5):
            if (r, c) == CENTER:
                continue
            av, bv = a[r][c], b[r][c]
            if av is not None and av == bv:
                n += 1
    return n


@dataclass(frozen=True)
class Oracle:
    name: str
    payload_hex: str
    cards: tuple[Grid, ...]


ORACLES: tuple[Oracle, ...] = (
    Oracle(
        "jul3_night",
        "540c99018310142cab00104d142aaa0082324445190c2123"
        "a240181905700e41d208001d4a0a2c060000000000000000",
        (
            ((11, 19, 43, 59, 68), (5, 16, 31, 50, 62), (12, 24, None, 48, 61), (7, 23, 38, 56, 66), (3, 20, 32, 57, 64)),
            ((15, 20, 36, 59, 75), (5, 25, 34, 53, 67), (12, 27, None, 55, 63), (11, 18, 32, 47, 71), (9, 29, 38, 58, 69)),
            ((5, 16, 32, 50, 75), (1, 24, 45, 57, 73), (4, 21, None, 54, 74), (12, 29, 38, 58, 61), (11, 25, 36, 49, 63)),
            ((9, 27, 41, 49, 70), (15, 20, 43, 55, 64), (3, 23, None, 57, 71), (2, 22, 39, 47, 66), (4, 17, 42, 52, 63)),
        ),
    ),
    Oracle(
        "jul4_1127",
        "9c106a040c214c0533102e080a2a20618426c14865204d040d025230a8508e01220e014c9406a4120000000000000000",
        (
            col_table_to_rows((4, 5, 8, 3, 13), (21, 17, 19, 26, 22), (34, 44, "FR", 33, 39), (52, 49, 54, 56, 48), (73, 66, 61, 62, 65)),
            col_table_to_rows((3, 12, 2, 6, 4), (17, 25, 29, 27, 19), (44, 36, "FR", 39, 45), (56, 53, 55, 48, 59), (61, 67, 72, 68, 75)),
            col_table_to_rows((14, 7, 6, 1, 3), (19, 22, 16, 26, 18), (34, 31, "FR", 33, 40), (50, 59, 47, 52, 58), (75, 66, 64, 68, 73)),
            col_table_to_rows((4, 2, 8, 3, 9), (17, 21, 26, 25, 27), (41, 42, "FR", 45, 31), (55, 48, 53, 50, 56), (63, 66, 73, 68, 70)),
        ),
    ),
    Oracle(
        "jul4_setB_seq6",
        "e840204e9202884310330a4a0c4c801c44468d40c2242a113030046a0b033430a0490912d00624190000000000000000",
        (
            col_table_to_rows((15, 4, 7, 8, 6), (21, 26, 27, 30, 25), (40, 35, "FR", 38, 32), (60, 55, 53, 49, 54), (74, 65, 69, 73, 70)),
            col_table_to_rows((10, 4, 15, 12, 2), (27, 18, 30, 26, 19), (42, 43, "FR", 38, 41), (60, 55, 52, 56, 48), (68, 64, 75, 61, 63)),
            col_table_to_rows((7, 11, 2, 8, 14), (19, 28, 17, 24, 21), (35, 44, "FR", 43, 36), (57, 55, 60, 48, 59), (62, 69, 70, 64, 61)),
            col_table_to_rows((6, 13, 5, 14, 3), (21, 23, 27, 24, 30), (40, 34, "FR", 31, 43), (52, 55, 53, 50, 56), (72, 63, 73, 66, 69)),
        ),
    ),
    Oracle(
        "jul4_setA_seq7",
        "c94044522038ac02186824230513846034098131830682681444203512258036e22068208928084e0000000000000000",
        (
            col_table_to_rows((4, 7, 8, 1, 15), (22, 30, 28, 18, 25), (42, 36, "FR", 43, 44), (51, 48, 53, 55, 49), (75, 74, 64, 65, 72)),
            col_table_to_rows((3, 10, 6, 14, 9), (25, 16, 28, 24, 18), (44, 33, "FR", 38, 45), (57, 48, 51, 50, 54), (74, 69, 61, 68, 73)),
            col_table_to_rows((8, 2, 11, 1, 10), (17, 30, 23, 29, 27), (35, 41, "FR", 45, 33), (51, 58, 54, 59, 56), (62, 69, 65, 74, 71)),
            col_table_to_rows((10, 14, 8, 13, 11), (29, 23, 22, 21, 17), (44, 36, "FR", 37, 34), (53, 49, 46, 59, 57), (75, 70, 71, 64, 72)),
        ),
    ),
)


def search_oracle(oracle: Oracle) -> None:
    payload = bytes.fromhex(oracle.payload_hex)
    blocks = tuple(payload[i * 12 : (i + 1) * 12] for i in range(4))
    active = [(i, b) for i, b in enumerate(blocks) if any(b != 0 for b in b)]

    best: list[tuple[int, str, tuple[int, ...], int, int]] = []

    for byte_rev in (False, True):
        for nibble_order in ("low_first", "high_first", "legacy_pair_low"):
            for spot_name in spot_orders():
                for num_map in NUM_MAPS:
                    for bit_rev in (False, True):
                        decoded = []
                        ok = True
                        for _, block in active:
                            g = decode_block(
                                block,
                                byte_rev=byte_rev,
                                nibble_order=nibble_order,
                                spot_name=spot_name,
                                num_map=num_map,
                                bit_rev=bit_rev,
                            )
                            if g is None:
                                ok = False
                                break
                            decoded.append(g)
                        if not ok or len(decoded) != len(oracle.cards):
                            continue
                        for perm in itertools.permutations(range(len(decoded))):
                            score = sum(
                                overlap(decoded[perm[i]], oracle.cards[i])
                                for i in range(len(oracle.cards))
                            )
                            key = (
                                byte_rev,
                                nibble_order,
                                spot_name,
                                num_map,
                                bit_rev,
                            )
                            label = (
                                f"byte_rev={byte_rev} nib={nibble_order} spot={spot_name} "
                                f"map={num_map} bit_rev={bit_rev} perm={[p + 1 for p in perm]}"
                            )
                            best.append((score, label, perm, len(decoded), 24 * len(decoded)))
                            if score == 24 * len(decoded):
                                print(f"\n=== EXACT MATCH {oracle.name} ===")
                                print(label)
                                for i, p in enumerate(perm):
                                    print(f"  wire slot {p + 1} -> ref card {i + 1}")
                                return

    best.sort(key=lambda x: -x[0])
    top = best[:8]
    print(f"\n--- {oracle.name} nibble model (best of {len(best)} valid) ---")
    if not top:
        print("  (no valid full-card decodes in nibble model)")
    else:
        for score, label, *_ in top:
            print(f"  {score}/{24 * len(oracle.cards)}  {label}")


def main() -> None:
    print("Nibble decode uses col_local+1 => col*15 + nibble + 1 (zero-origin nibble correction)")
    print(f"Variants: 2 byte × 3 nibble × 4 spot × {len(NUM_MAPS)} map × 2 bit_rev × 24 slot perm\n")
    for oracle in ORACLES:
        search_oracle(oracle)


if __name__ == "__main__":
    main()
