#!/usr/bin/env python3
"""Independent verifier for the PLO final-board freeroll casebook."""

from __future__ import annotations

import argparse
import csv
import itertools
import json
import platform
import sys
import time
from collections import Counter
from pathlib import Path

RANKS = "23456789TJQKA"
SUITS = "cdhs"
CATEGORIES = (
    "high card",
    "one pair",
    "two pair",
    "three of a kind",
    "straight",
    "flush",
    "full house",
    "four of a kind",
    "straight flush",
)
DECK = tuple(rank + suit for rank in RANKS for suit in SUITS)
SUIT_DISPLAY = {"c": "♣", "d": "♦", "h": "♥", "s": "♠"}


def parse_card(token: str) -> tuple[int, str]:
    if len(token) != 2 or token[0] not in RANKS or token[1] not in SUITS:
        raise AssertionError(f"Invalid card token: {token}")
    return RANKS.index(token[0]) + 2, token[1]


def display_card(token: str) -> str:
    parse_card(token)
    rank = "10" if token[0] == "T" else token[0]
    return rank + SUIT_DISPLAY[token[1]]


def straight_high(ranks: list[int]) -> int:
    unique = set(ranks)
    if 14 in unique:
        unique.add(1)
    for high in range(14, 4, -1):
        if all(high - offset in unique for offset in range(5)):
            return high
    return 0


def evaluate_five(tokens: tuple[str, ...]) -> tuple[int, ...]:
    parsed = [parse_card(token) for token in tokens]
    ranks = sorted((rank for rank, _ in parsed), reverse=True)
    grouped = sorted(Counter(ranks).items(), key=lambda item: (item[1], item[0]), reverse=True)
    flush = len({suit for _, suit in parsed}) == 1
    run = straight_high(ranks)

    if flush and run:
        return 8, run
    if grouped[0][1] == 4:
        return 7, grouped[0][0], grouped[1][0]
    if grouped[0][1] == 3 and grouped[1][1] == 2:
        return 6, grouped[0][0], grouped[1][0]
    if flush:
        return 5, *ranks
    if run:
        return 4, run
    if grouped[0][1] == 3:
        kickers = sorted((rank for rank, count in grouped if count == 1), reverse=True)
        return 3, grouped[0][0], *kickers
    pairs = sorted((rank for rank, count in grouped if count == 2), reverse=True)
    if len(pairs) == 2:
        kicker = next(rank for rank, count in grouped if count == 1)
        return 2, *pairs, kicker
    if len(pairs) == 1:
        kickers = sorted((rank for rank, count in grouped if count == 1), reverse=True)
        return 1, pairs[0], *kickers
    return 0, *ranks


def evaluate_omaha(hole: list[str], board: list[str]) -> tuple[tuple[int, ...], tuple[str, ...]]:
    assert len(hole) == 4
    assert len(board) in (3, 4, 5)
    candidates = []
    for hole_pair in itertools.combinations(hole, 2):
        for board_triple in itertools.combinations(board, 3):
            cards = (*hole_pair, *board_triple)
            candidates.append((evaluate_five(cards), cards))
    return max(candidates, key=lambda item: item[0])


def nut_straight_high(board: list[str]) -> int:
    """Find the highest straight rank possible with exactly two holes and three board cards."""
    available = [card for card in DECK if card not in set(board)]
    best = 0
    for hole_pair in itertools.combinations(available, 2):
        for board_triple in itertools.combinations(board, 3):
            ranks = [parse_card(card)[0] for card in (*hole_pair, *board_triple)]
            best = max(best, straight_high(ranks))
    return best


def verify_evaluator() -> int:
    fixtures = [
        (("As", "Ks", "Qs", "Js", "Ts"), (8, 14)),
        (("9c", "9d", "9h", "9s", "2c"), (7, 9, 2)),
        (("Ac", "Ad", "Ah", "Kc", "Kd"), (6, 14, 13)),
        (("Ac", "Jc", "9c", "5c", "2c"), (5, 14, 11, 9, 5, 2)),
        (("As", "2d", "3c", "4h", "5s"), (4, 5)),
        (("Qc", "Qd", "Qh", "8s", "2c"), (3, 12, 8, 2)),
        (("Jc", "Jd", "8h", "8s", "Ac"), (2, 11, 8, 14)),
        (("Tc", "Td", "Ah", "8s", "3c"), (1, 10, 14, 8, 3)),
        (("Ac", "Jd", "9h", "5s", "2c"), (0, 14, 11, 9, 5, 2)),
    ]
    for cards, expected in fixtures:
        actual = evaluate_five(cards)
        assert actual == expected, (cards, expected, actual)
    return len(fixtures)


def verify(root: Path) -> dict:
    if not __debug__:
        raise RuntimeError("verification refuses Python optimized mode because -O removes assertion checks")
    started = time.perf_counter()
    cases = json.loads((root / "cases.json").read_text(encoding="utf-8"))
    summary = json.loads((root / "summary.json").read_text(encoding="utf-8"))
    csv_rows = list(csv.DictReader((root / "final-board-outcomes.csv").open(encoding="utf-8", newline="")))
    csv_by_key = {(row["case_id"], row["completion"]): row for row in csv_rows}
    summary_by_id = {row["id"]: row for row in summary["cases"]}
    checks = verify_evaluator()
    observed_rows = 0

    assert cases["schemaVersion"] == summary["schemaVersion"] == 1
    assert len(cases["cases"]) == len(summary["cases"]) == 6
    assert len(csv_rows) == 1_020
    checks += 3

    for fixture in cases["cases"]:
        board = fixture.get("board", cases["board"])
        starting_street = fixture.get("street", cases["street"])
        assert len(board) == {"flop": 3, "turn": 4, "river": 5}[starting_street]
        known = [*board, *fixture["hero"], *fixture["villain"]]
        assert len(known) == len(set(known)) == len(board) + 8
        for card in known:
            parse_card(card)

        hero_turn, _ = evaluate_omaha(fixture["hero"], board)
        villain_turn, _ = evaluate_omaha(fixture["villain"], board)
        assert hero_turn == villain_turn
        assert CATEGORIES[hero_turn[0]] == "straight"
        expected_nut_high = nut_straight_high(board)
        assert hero_turn[1] == expected_nut_high

        unseen = [card for card in DECK if card not in set(known)]
        cards_to_come = 5 - len(board)
        completions = list(itertools.combinations(unseen, cards_to_come))
        assert len(completions) == (40 if cards_to_come == 1 else 820)
        counts = Counter()

        for completion in completions:
            hero_score, hero_cards = evaluate_omaha(fixture["hero"], [*board, *completion])
            villain_score, villain_cards = evaluate_omaha(fixture["villain"], [*board, *completion])
            outcome = "win" if hero_score > villain_score else "loss" if hero_score < villain_score else "tie"
            reverse = "win" if villain_score > hero_score else "loss" if villain_score < hero_score else "tie"
            assert (outcome, reverse) in (("win", "loss"), ("loss", "win"), ("tie", "tie"))
            counts[outcome] += 1

            completion_key = " ".join(completion)
            row = csv_by_key[(fixture["id"], completion_key)]
            assert row["starting_street"] == fixture.get("street", cases["street"])
            assert row["completion_display"] == " ".join(display_card(card) for card in completion)
            assert row["hero_outcome"] == outcome
            assert row["hero_category"] == CATEGORIES[hero_score[0]]
            assert row["villain_category"] == CATEGORIES[villain_score[0]]
            assert set(row["hero_best_five"].split()) == set(hero_cards)
            assert set(row["villain_best_five"].split()) == set(villain_cards)
            observed_rows += 1

        equity = ((2 * counts["win"] + counts["tie"]) * 50) / len(completions)
        true_freeroll = counts["win"] > 0 and counts["loss"] == 0
        expected = fixture["expected"]
        frozen = summary_by_id[fixture["id"]]
        for label, actual in (
            ("wins", counts["win"]),
            ("ties", counts["tie"]),
            ("losses", counts["loss"]),
            ("equityPct", equity),
            ("trueFreeroll", true_freeroll),
        ):
            assert actual == expected[label] == frozen[label], (fixture["id"], label, actual)
        assert counts["win"] + counts["tie"] + counts["loss"] == len(completions)
        assert frozen["legalCompletions"] == len(completions)
        assert frozen["cardsToCome"] == cards_to_come
        assert frozen["nutStraightHigh"] == expected_nut_high
        checks += 15 + len(completions) * 8

    assert observed_rows == len(csv_rows) == len(csv_by_key)
    checks += 1
    return {
        "status": "pass",
        "verifiedAt": "2026-09-04",
        "implementation": "separate Python standard-library Omaha reimplementation",
        "python": platform.python_version(),
        "pythonOptimizeFlag": sys.flags.optimize,
        "platform": platform.platform(),
        "cases": len(cases["cases"]),
        "completionRows": observed_rows,
        "checks": checks,
        "elapsedSeconds": round(time.perf_counter() - started, 6),
    }


def main() -> None:
    parser = argparse.ArgumentParser()
    parser.add_argument("--root", type=Path, default=Path(__file__).resolve().parent)
    parser.add_argument("--report", type=Path)
    args = parser.parse_args()
    report = verify(args.root.resolve())
    output = json.dumps(report, indent=2) + "\n"
    if args.report:
        args.report.parent.mkdir(parents=True, exist_ok=True)
        args.report.write_text(output, encoding="utf-8", newline="\n")
    print(output, end="")


if __name__ == "__main__":
    try:
        main()
    except (AssertionError, KeyError, RuntimeError, ValueError) as error:
        print(f"verification failed: {error}", file=sys.stderr)
        raise SystemExit(1) from error
