#!/usr/bin/env python3
"""Independent verification for the PLO Monte Carlo study.

This checker uses a pure-Python five-card rank tuple and a separate dictionary-
based Omaha enumerator. It does not import the generator's ranking or board-
scoring functions.
"""

from __future__ import annotations

import csv
import json
import random
from collections import Counter
from itertools import combinations
from pathlib import Path

from phevaluator.evaluator import evaluate_5cards

RANKS = "23456789TJQKA"
SUITS = "cdhs"
DECK = tuple(range(52))
SCRIPT_PATH = Path(__file__).resolve()
BUNDLE = SCRIPT_PATH.parent
PUBLISHED_BUNDLE = SCRIPT_PATH.name == "plo-monte-carlo-verify-study.py"
OUTPUT = BUNDLE / ("plo-monte-carlo-output" if PUBLISHED_BUNDLE else "outputs")
PUBLISHED_EXACT_CSV = BUNDLE / "plo-monte-carlo-exact-matchups.csv"
EXACT_CSV = (
    PUBLISHED_EXACT_CSV
    if PUBLISHED_BUNDLE and PUBLISHED_EXACT_CSV.is_file()
    else OUTPUT / "exact-matchups.csv"
)
VERIFY_JSON = OUTPUT / "independent-verification.json"

PANEL = {
    "P5-A": (("As", "Ah", "Ks", "Kh", "Qc"), ("Jc", "Tc", "9d", "8d", "7h")),
    "P5-B": (("As", "Ks", "Qh", "Jh", "Tc"), ("Ad", "Kd", "Qc", "Jc", "9s")),
    "P5-D": (("As", "Ah", "Ks", "Kh", "Qc"), ("2c", "2d", "2h", "3s", "4s")),
    "P6-A": (("As", "Ah", "Ks", "Kh", "Qc", "Jc"), ("Tc", "9c", "8d", "7d", "6h", "5h")),
    "P6-B": (("As", "Ks", "Qs", "Jh", "Th", "9d"), ("Ad", "Kd", "Qd", "Jc", "Tc", "8c")),
    "P6-D": (("As", "Ah", "Ks", "Kh", "Qc", "Jc"), ("2c", "2d", "2h", "2s", "3d", "4d")),
}

INDEPENDENT_EXPECTED_COUNTS = {
    "P5-A": (469_214, 66, 381_388),
    "P5-B": (294_553, 298_784, 257_331),
    "P5-D": (669_638, 0, 181_030),
    "P6-A": (359_603, 0, 298_405),
    "P6-B": (225_107, 255_322, 177_579),
    "P6-D": (533_331, 0, 124_677),
}


def card_id(card: str) -> int:
    if len(card) != 2 or card[0] not in RANKS or card[1] not in SUITS:
        raise ValueError(card)
    return RANKS.index(card[0]) * 4 + SUITS.index(card[1])


def rank_five(cards: tuple[int, int, int, int, int]) -> tuple[int, ...]:
    ranks = [card // 4 + 2 for card in cards]
    suits = [card % 4 for card in cards]
    counts = Counter(ranks)
    groups = sorted(((count, rank) for rank, count in counts.items()), reverse=True)
    unique = sorted(counts, reverse=True)
    straight_high = 5 if {14, 2, 3, 4, 5}.issubset(counts) else 0
    for high in range(14, 5 - 1, -1):
        if all(rank in counts for rank in range(high - 4, high + 1)):
            straight_high = high
            break
    flush = len(set(suits)) == 1
    if flush and straight_high:
        return (8, straight_high)
    if groups[0][0] == 4:
        quad = groups[0][1]
        kicker = max(rank for rank in ranks if rank != quad)
        return (7, quad, kicker)
    if groups[0][0] == 3 and groups[1][0] == 2:
        return (6, groups[0][1], groups[1][1])
    if flush:
        return (5, *sorted(ranks, reverse=True))
    if straight_high:
        return (4, straight_high)
    if groups[0][0] == 3:
        trip = groups[0][1]
        kickers = sorted((rank for rank in ranks if rank != trip), reverse=True)
        return (3, trip, *kickers)
    pairs = sorted((rank for rank, count in counts.items() if count == 2), reverse=True)
    if len(pairs) == 2:
        kicker = max(rank for rank, count in counts.items() if count == 1)
        return (2, *pairs, kicker)
    if len(pairs) == 1:
        pair = pairs[0]
        kickers = sorted((rank for rank, count in counts.items() if count == 1), reverse=True)
        return (1, pair, *kickers)
    return (0, *unique)


def best_for_triple(hole: tuple[int, ...], triple: tuple[int, int, int]) -> tuple[int, ...]:
    return max(rank_five((*pair, *triple)) for pair in combinations(hole, 2))


def build_dictionary(remaining: tuple[int, ...], hole: tuple[int, ...]) -> dict[tuple[int, int, int], tuple[int, ...]]:
    return {triple: best_for_triple(hole, triple) for triple in combinations(remaining, 3)}


def independent_exact(hero_codes: tuple[str, ...], villain_codes: tuple[str, ...]) -> tuple[int, int, int]:
    hero = tuple(card_id(card) for card in hero_codes)
    villain = tuple(card_id(card) for card in villain_codes)
    known = hero + villain
    if len(set(known)) != len(known):
        raise ValueError("Duplicate cards")
    remaining = tuple(card for card in DECK if card not in set(known))
    hero_lookup = build_dictionary(remaining, hero)
    villain_lookup = build_dictionary(remaining, villain)
    wins = ties = losses = 0
    for board in combinations(remaining, 5):
        hero_rank = max(hero_lookup[triple] for triple in combinations(board, 3))
        villain_rank = max(villain_lookup[triple] for triple in combinations(board, 3))
        if hero_rank > villain_rank:
            wins += 1
        elif hero_rank == villain_rank:
            ties += 1
        else:
            losses += 1
    return wins, ties, losses


def crosscheck_all_five_card_hands() -> dict[str, int | bool]:
    python_to_ph: dict[tuple[int, ...], int] = {}
    ph_to_python: dict[int, tuple[int, ...]] = {}
    suit_cycle_checks = 0
    for index, cards in enumerate(combinations(DECK, 5)):
        python_rank = rank_five(cards)
        ph_rank = evaluate_5cards(*cards)
        if python_rank in python_to_ph and python_to_ph[python_rank] != ph_rank:
            raise AssertionError(f"One Python rank maps to multiple PH ranks: {python_rank}")
        if ph_rank in ph_to_python and ph_to_python[ph_rank] != python_rank:
            raise AssertionError(f"One PH rank maps to multiple Python ranks: {ph_rank}")
        python_to_ph[python_rank] = ph_rank
        ph_to_python[ph_rank] = python_rank
        if index % 257 == 0:
            cycled = tuple((card // 4) * 4 + ((card % 4 + 1) % 4) for card in cards)
            if rank_five(cycled) != python_rank or evaluate_5cards(*cycled) != ph_rank:
                raise AssertionError(f"Suit-cycle mismatch: {cards}")
            suit_cycle_checks += 1
    ordered = sorted(python_to_ph)
    ph_sequence = [python_to_ph[rank] for rank in ordered]
    if any(left <= right for left, right in zip(ph_sequence, ph_sequence[1:], strict=False)):
        raise AssertionError("PH ordering does not reverse the stronger-is-larger Python tuple ordering")
    if len(python_to_ph) != 7_462 or len(ph_to_python) != 7_462:
        raise AssertionError("Expected 7,462 distinct five-card ranks")
    return {
        "five_card_combinations_checked": 2_598_960,
        "distinct_rank_classes": len(python_to_ph),
        "suit_cycle_samples_checked": suit_cycle_checks,
        "bijection_and_ordering_passed": True,
    }


def direct_omaha_samples() -> dict[str, int | bool]:
    rng = random.Random(20260829)
    checks = 0
    for key, (hero_codes, villain_codes) in PANEL.items():
        hero = tuple(card_id(card) for card in hero_codes)
        villain = tuple(card_id(card) for card in villain_codes)
        remaining = tuple(card for card in DECK if card not in set(hero + villain))
        hero_lookup = build_dictionary(remaining, hero)
        villain_lookup = build_dictionary(remaining, villain)
        for _ in range(200):
            board = tuple(sorted(rng.sample(remaining, 5)))
            cached_h = max(hero_lookup[triple] for triple in combinations(board, 3))
            cached_v = max(villain_lookup[triple] for triple in combinations(board, 3))
            direct_h = max(rank_five((*pair, *triple)) for pair in combinations(hero, 2) for triple in combinations(board, 3))
            direct_v = max(rank_five((*pair, *triple)) for pair in combinations(villain, 2) for triple in combinations(board, 3))
            if cached_h != direct_h or cached_v != direct_v:
                raise AssertionError(f"Cached/direct mismatch in {key} on {board}")
            shuffled = list(board)
            rng.shuffle(shuffled)
            permuted_h = max(best_for_triple(hero, tuple(sorted(triple))) for triple in combinations(shuffled, 3))
            if permuted_h != direct_h:
                raise AssertionError(f"Board-order mismatch in {key}")
            checks += 1
    return {"cached_direct_and_board_order_checks": checks, "passed": True}


def unit_checks() -> dict[str, bool]:
    def ids(text: str) -> tuple[int, int, int, int, int]:
        return tuple(card_id(card) for card in text.split())  # type: ignore[return-value]

    wheel = rank_five(ids("Ac 2d 3h 4s 5c"))
    six_high = rank_five(ids("2c 3d 4h 5s 6c"))
    full_house = rank_five(ids("Ac Ad Ah Kc Kd"))
    flush = rank_five(ids("Ac Jc 8c 5c 2c"))
    pair_ace = rank_five(ids("Ac Ad Qh Js 9c"))
    pair_king = rank_five(ids("Kc Kd Ah Qs Jc"))
    if not (six_high > wheel and full_house > flush and pair_ace > pair_king):
        raise AssertionError("Five-card unit ordering failed")
    try:
        independent_exact(("As",) * 5, ("2c", "2d", "2h", "3s", "4s"))
    except ValueError:
        duplicate_rejected = True
    else:
        duplicate_rejected = False
    if not duplicate_rejected:
        raise AssertionError("Duplicate cards were not rejected")
    return {
        "wheel_ordering": True,
        "full_house_over_flush": True,
        "pair_kicker_ordering": True,
        "duplicate_cards_rejected": True,
    }


def main() -> None:
    with EXACT_CSV.open(newline="", encoding="utf-8") as handle:
        primary = {row["spot"]: (int(row["wins"]), int(row["ties"]), int(row["losses"])) for row in csv.DictReader(handle)}

    crosscheck = crosscheck_all_five_card_hands()
    samples = direct_omaha_samples()
    units = unit_checks()
    exact_results: dict[str, dict[str, object]] = {}
    for key, (hero, villain) in PANEL.items():
        result = independent_exact(hero, villain)
        if result != primary[key] or result != INDEPENDENT_EXPECTED_COUNTS[key]:
            raise AssertionError(
                f"Exact mismatch {key}: independent={result}, primary={primary[key]}, pinned={INDEPENDENT_EXPECTED_COUNTS[key]}"
            )
        wins, ties, losses = result
        exact_results[key] = {
            "wins": wins,
            "ties": ties,
            "losses": losses,
            "player_swap_expected": {"wins": losses, "ties": ties, "losses": wins},
            "passed": True,
        }
        print(f"{key}: independent exact counts match ({wins:,}/{ties:,}/{losses:,})")

    suit_map = str.maketrans({"c": "d", "d": "h", "h": "s", "s": "c"})
    hero, villain = PANEL["P5-A"]
    suit_result = independent_exact(
        tuple(card.translate(suit_map) for card in hero),
        tuple(card.translate(suit_map) for card in villain),
    )
    if suit_result != INDEPENDENT_EXPECTED_COUNTS["P5-A"]:
        raise AssertionError(f"Suit-isomorphic exact mismatch: {suit_result}")

    swap_result = independent_exact(PANEL["P5-A"][1], PANEL["P5-A"][0])
    expected_swap = (
        INDEPENDENT_EXPECTED_COUNTS["P5-A"][2],
        INDEPENDENT_EXPECTED_COUNTS["P5-A"][1],
        INDEPENDENT_EXPECTED_COUNTS["P5-A"][0],
    )
    if swap_result != expected_swap:
        raise AssertionError(f"Player-swap exact mismatch: {swap_result} != {expected_swap}")

    report = {
        "run_date": "2026-08-29",
        "independence": "Pure-Python tuple evaluator and dictionary-based board scorer; primary generator functions not imported",
        "five_card_crosscheck": crosscheck,
        "omaha_sample_checks": samples,
        "unit_checks": units,
        "exact_panel": exact_results,
        "full_suit_isomorph_rerun": {
            "spot": "P5-A",
            "counts": {"wins": suit_result[0], "ties": suit_result[1], "losses": suit_result[2]},
            "passed": True,
        },
        "full_player_swap_rerun": {
            "spot": "P5-A",
            "counts": {"wins": swap_result[0], "ties": swap_result[1], "losses": swap_result[2]},
            "passed": True,
        },
        "all_checks_passed": True,
    }
    VERIFY_JSON.parent.mkdir(parents=True, exist_ok=True)
    VERIFY_JSON.write_text(json.dumps(report, indent=2) + "\n", encoding="utf-8")
    print("All independent checks passed")


if __name__ == "__main__":
    main()
