#!/usr/bin/env python3
"""Independent verifier for the dry-side-pot river casebook."""

from __future__ import annotations

import ast
import csv
import hashlib
import json
import re
import struct
import sys
import xml.etree.ElementTree as ET
from fractions import Fraction
from itertools import combinations
from pathlib import Path
from typing import Any


DATA_DIRECTORY = Path(__file__).resolve().parent
RANKS = "23456789TJQKA"
SUITS = "cdhs"
EXPECTED_FILES = (
    "README.md",
    "casebook.json",
    "cases.csv",
    "decision-tree.svg",
    "dry-side-pot-poker.webp",
    "generate.mjs",
    "thresholds.csv",
    "verification-spec.json",
    "verify.py",
)
EXPECTED_BRANCHES = {
    "dead-bluff": {
        "hero": "QhJh",
        "rows": (
            ("KdQd", "call"), ("KsQs", "call"), ("KdJd", "call"),
            ("KsJs", "call"), ("KdTd", "call"), ("KsTs", "call"),
            ("Kd8d", "fold"), ("Ks8s", "fold"), ("Kd6d", "fold"),
            ("Ks6s", "fold"),
        ),
    },
    "main-pot-isolation": {
        "hero": "Kh5h",
        "rows": (
            ("KdQd", "call"), ("KsQs", "call"), ("KdJd", "call"),
            ("KsJs", "call"), ("KdTd", "call"), ("KsTs", "call"),
            ("Kd8d", "fold"), ("Ks8s", "fold"), ("Kd6d", "fold"),
            ("Ks6s", "fold"),
        ),
    },
    "thin-value": {
        "hero": "KhQh",
        "rows": (
            ("KdJd", "call"), ("KsJs", "call"), ("KdTd", "call"),
            ("KsTs", "call"), ("Kd8d", "fold"), ("Ks8s", "fold"),
            ("Kd6d", "fold"), ("Ks6s", "fold"), ("Kd3d", "fold"),
            ("Ks3s", "fold"),
        ),
    },
}
CASES_COLUMNS = (
    "case_id", "hero", "villain", "response", "weight", "hero_beats_all_in",
    "hero_beats_villain", "check_main_share", "check_payoff_chips",
    "bet_main_share", "bet_side_share", "bet_payoff_chips",
)
THRESHOLD_COLUMNS = (
    "main_pot_chips", "bet_chips", "bet_as_main_pot_fraction",
    "conditional_main_pot_share_q", "break_even_fold_frequency",
    "break_even_fold_percent",
)


class VerificationError(Exception):
    """Raised when a public artifact fails the independent contract."""


def require(condition: bool, message: str) -> None:
    if not condition:
        raise VerificationError(message)


def load_json(name: str) -> dict[str, Any]:
    return json.loads((DATA_DIRECTORY / name).read_text(encoding="utf-8"))


def split_hand(value: str) -> list[str]:
    require(isinstance(value, str) and len(value) == 4, f"invalid hand syntax: {value!r}")
    return [value[:2], value[2:]]


def card_parts(card: str) -> tuple[int, str]:
    require(isinstance(card, str) and len(card) == 2, f"invalid card: {card!r}")
    require(card[0] in RANKS and card[1] in SUITS, f"invalid card: {card!r}")
    return RANKS.index(card[0]) + 2, card[1]


def evaluate_five(cards: list[str] | tuple[str, ...]) -> tuple[int, ...]:
    require(len(cards) == 5 and len(set(cards)) == 5, "five distinct cards required")
    parsed = [card_parts(card) for card in cards]
    ranks = sorted((rank for rank, _ in parsed), reverse=True)
    counts = {rank: ranks.count(rank) for rank in set(ranks)}
    groups = sorted(counts.items(), key=lambda item: (-item[1], -item[0]))
    unique = sorted(set(ranks), reverse=True)
    if 14 in unique:
        unique.append(1)
    straight_high = 0
    for index in range(len(unique) - 4):
        window = unique[index:index + 5]
        if all(rank == window[0] - offset for offset, rank in enumerate(window)):
            straight_high = window[0]
            break
    flush = len({suit for _, suit in parsed}) == 1
    if flush and straight_high:
        return (8, straight_high)
    if groups[0][1] == 4:
        return (7, groups[0][0], groups[1][0])
    if groups[0][1] == 3 and groups[1][1] == 2:
        return (6, groups[0][0], groups[1][0])
    if flush:
        return (5, *ranks)
    if straight_high:
        return (4, straight_high)
    if groups[0][1] == 3:
        kickers = [rank for rank, count in groups if count == 1][:2]
        return (3, groups[0][0], *kickers)
    if groups[0][1] == 2 and groups[1][1] == 2:
        pairs = sorted((groups[0][0], groups[1][0]), reverse=True)
        kicker = next(rank for rank, count in groups if count == 1)
        return (2, *pairs, kicker)
    if groups[0][1] == 2:
        kickers = [rank for rank, count in groups if count == 1][:3]
        return (1, groups[0][0], *kickers)
    return (0, *ranks)


def best_of_seven(cards: list[str]) -> tuple[int, ...]:
    require(len(cards) == 7 and len(set(cards)) == 7, f"seven distinct cards required: {cards}")
    return max(evaluate_five(combo) for combo in combinations(cards, 5))


def hero_share(hero_score: tuple[int, ...], opponent_scores: list[tuple[int, ...]]) -> Fraction:
    scores = [hero_score, *opponent_scores]
    best = max(scores)
    if hero_score != best:
        return Fraction(0)
    return Fraction(1, sum(score == best for score in scores))


def verify_casebook(casebook: dict[str, Any]) -> None:
    require(casebook.get("recordType") == "dry-side-pot-river-casebook", "wrong record type")
    require(casebook.get("publicationDate") == "2026-09-05", "publication date drift")
    setup = casebook["setup"]
    require(setup["board"] == "Kc 9d 7s 4h 2c" and setup["allInHand"] == "As9c", "card setup drift")
    require(setup["mainPotChips"] == 300 and setup["currentSidePotChips"] == 0, "pot setup drift")
    require(setup["heroBetChips"] == 150, "bet size drift")
    require(
        "retrospective full-state analysis" in " ".join(setup.get("assumptions", []))
        and "conditional range" in " ".join(setup.get("assumptions", [])),
        "hidden-information boundary missing",
    )
    formula = casebook.get("specialCaseFormula", {})
    require(
        formula == {
            "scope": "Hero has zero check EV, loses every main and side pot when called, and q is expected main-pot share conditional on Villain folding.",
            "expression": "EV(bet) = f*q*M - (1-f)*B",
            "checkEv": "0",
            "breakEvenForPositiveQ": "f* = B / (q*M + B)",
            "zeroShareBoundary": "If q = 0, the bet is never strictly better than checking; it ties only when f = 1.",
        },
        "special-case formula drift",
    )
    board = setup["board"].split()
    all_in = split_hand(setup["allInHand"])
    expected = {
        "dead-bluff": (Fraction(0), Fraction(-90), Fraction(-90), Fraction(2, 5)),
        "main-pot-isolation": (Fraction(0), Fraction(30), Fraction(30), Fraction(2, 5)),
        "thin-value": (Fraction(300), Fraction(360), Fraction(60), Fraction(3, 5)),
    }
    require(len(casebook["cases"]) == 3, "expected three cases")
    require({item.get("id") for item in casebook["cases"]} == set(EXPECTED_BRANCHES), "case id set drift")
    for model_case in casebook["cases"]:
        pinned = EXPECTED_BRANCHES[model_case["id"]]
        require(model_case["hero"] == pinned["hero"], f"hero drift: {model_case['id']}")
        require(model_case["board"] == setup["board"], f"case board drift: {model_case['id']}")
        require(model_case["allIn"] == setup["allInHand"], f"case all-in hand drift: {model_case['id']}")
        require(model_case.get("rangeWeights") == "equal", f"range-weight policy drift: {model_case['id']}")
        require(model_case["villainCombinationCount"] == 10, f"published branch count drift: {model_case['id']}")
        require(
            tuple((branch["villain"], branch["response"]) for branch in model_case["branches"])
            == pinned["rows"],
            f"range/response policy drift: {model_case['id']}",
        )
        hero = split_hand(model_case["hero"])
        hero_score = best_of_seven([*board, *hero])
        all_in_score = best_of_seven([*board, *all_in])
        branches = model_case["branches"]
        require(len(branches) == 10, f"expected ten branches: {model_case['id']}")
        total_weight = Fraction(0)
        weighted_check = Fraction(0)
        weighted_bet = Fraction(0)
        fold_weight = Fraction(0)
        for branch in branches:
            villain = split_hand(branch["villain"])
            known = [*board, *all_in, *hero, *villain]
            require(len(known) == len(set(known)), f"card collision: {model_case['id']} {branch['villain']}")
            villain_score = best_of_seven([*board, *villain])
            weight = Fraction(str(branch["weight"]))
            require(weight == 1, f"branch weight must equal one: {model_case['id']} {branch['villain']}")
            total_weight += weight
            check_share = hero_share(hero_score, [all_in_score, villain_score])
            check_payoff = check_share * setup["mainPotChips"]
            require(Fraction(str(branch["checkMainShare"])) == check_share, "check share mismatch")
            require(Fraction(str(branch["checkPayoffChips"])) == check_payoff, "check payoff mismatch")
            response = branch["response"]
            require(response in {"fold", "call"}, "invalid response")
            if response == "fold":
                fold_weight += weight
                bet_main_share = hero_share(hero_score, [all_in_score])
                bet_side_share: Fraction | None = None
                bet_payoff = bet_main_share * setup["mainPotChips"]
            else:
                bet_main_share = check_share
                bet_side_share = hero_share(hero_score, [villain_score])
                bet_payoff = (
                    bet_main_share * setup["mainPotChips"]
                    + bet_side_share * (2 * setup["heroBetChips"])
                    - setup["heroBetChips"]
                )
            require(Fraction(str(branch["betMainShare"])) == bet_main_share, "bet main share mismatch")
            if bet_side_share is None:
                require(branch["betSideShare"] is None, "fold branch has a side-pot share")
            else:
                require(Fraction(str(branch["betSideShare"])) == bet_side_share, "side share mismatch")
            require(Fraction(str(branch["betPayoffChips"])) == bet_payoff, "bet payoff mismatch")
            require(branch["heroBeatsAllIn"] == (hero_score > all_in_score), "all-in comparison mismatch")
            require(branch["heroBeatsVillain"] == (hero_score > villain_score), "villain comparison mismatch")
            weighted_check += weight * check_payoff
            weighted_bet += weight * bet_payoff
        check_ev = weighted_check / total_weight
        bet_ev = weighted_bet / total_weight
        fold_frequency = fold_weight / total_weight
        expected_check, expected_bet, expected_delta, expected_fold = expected[model_case["id"]]
        require(check_ev == expected_check and bet_ev == expected_bet, f"fixed EV mismatch: {model_case['id']}")
        require(bet_ev - check_ev == expected_delta, f"fixed delta mismatch: {model_case['id']}")
        require(fold_frequency == expected_fold, f"fold frequency mismatch: {model_case['id']}")
        require(Fraction(str(model_case["foldFrequency"])) == fold_frequency, "published fold frequency mismatch")
        require(Fraction(str(model_case["callFrequency"])) == 1 - fold_frequency, "published call frequency mismatch")
        require(Fraction(str(model_case["checkEvChips"])) == check_ev, "published check EV mismatch")
        require(Fraction(str(model_case["betEvChips"])) == bet_ev, "published bet EV mismatch")
        require(Fraction(str(model_case["betMinusCheckChips"])) == bet_ev - check_ev, "published delta mismatch")


def verify_csvs(casebook: dict[str, Any]) -> None:
    with (DATA_DIRECTORY / "cases.csv").open(newline="", encoding="utf-8") as handle:
        reader = csv.DictReader(handle)
        require(tuple(reader.fieldnames or ()) == CASES_COLUMNS, "cases.csv header drift")
        rows = list(reader)
    require(len(rows) == 30, "cases.csv must contain 30 branches")
    expected_rows = {
        (model_case["id"], branch["villain"]): branch
        for model_case in casebook["cases"] for branch in model_case["branches"]
    }
    require(len(expected_rows) == 30, "branch identities must be unique within cases")
    actual_keys = [(row["case_id"], row["villain"]) for row in rows]
    require(len(set(actual_keys)) == 30, "cases.csv contains a duplicate branch")
    require(set(actual_keys) == set(expected_rows), "cases.csv branch set mismatch")
    for row in rows:
        key = (row["case_id"], row["villain"])
        require(key in expected_rows, f"unexpected CSV branch: {key}")
        branch = expected_rows[key]
        model_case = next(item for item in casebook["cases"] if item["id"] == row["case_id"])
        expected_fields = {
            "hero": model_case["hero"],
            "response": branch["response"],
            "weight": str(branch["weight"]),
            "hero_beats_all_in": str(branch["heroBeatsAllIn"]).lower(),
            "hero_beats_villain": str(branch["heroBeatsVillain"]).lower(),
            "check_main_share": str(branch["checkMainShare"]),
            "check_payoff_chips": str(branch["checkPayoffChips"]),
            "bet_main_share": str(branch["betMainShare"]),
            "bet_side_share": "" if branch["betSideShare"] is None else str(branch["betSideShare"]),
            "bet_payoff_chips": str(branch["betPayoffChips"]),
        }
        for field, expected_value in expected_fields.items():
            require(row[field] == expected_value, f"CSV {field} mismatch: {key}")

    with (DATA_DIRECTORY / "thresholds.csv").open(newline="", encoding="utf-8") as handle:
        reader = csv.DictReader(handle)
        require(tuple(reader.fieldnames or ()) == THRESHOLD_COLUMNS, "thresholds.csv header drift")
        thresholds = list(reader)
    require(len(thresholds) == 12, "thresholds.csv must contain 12 rows")
    published = casebook["thresholdRows"]
    require(len(published) == 12, "casebook threshold row count mismatch")
    for row, json_row in zip(thresholds, published, strict=True):
        main_pot = int(row["main_pot_chips"])
        bet = int(row["bet_chips"])
        q = Fraction(row["conditional_main_pot_share_q"])
        require(main_pot == json_row["mainPotChips"], "threshold main-pot input mismatch")
        require(bet == json_row["betChips"], "threshold bet input mismatch")
        require(Fraction(row["bet_as_main_pot_fraction"]) == Fraction(json_row["betAsMainPotFraction"]), "bet fraction mismatch")
        require(q == Fraction(json_row["conditionalMainPotShare"]), "conditional share input mismatch")
        threshold = Fraction(bet, 1) / (q * main_pot + bet)
        require(Fraction(row["break_even_fold_frequency"]) == threshold, "CSV threshold mismatch")
        require(Fraction(json_row["breakEvenFoldFrequency"]) == threshold, "JSON threshold mismatch")
        require(abs(float(row["break_even_fold_percent"]) - float(threshold * 100)) < 0.00005, "threshold percent mismatch")
        require(abs(float(json_row["breakEvenFoldPercent"]) - float(threshold * 100)) < 0.00005, "JSON threshold percent mismatch")


def webp_dimensions(path: Path) -> tuple[int, int]:
    data = path.read_bytes()
    require(len(data) >= 30 and data[:4] == b"RIFF" and data[8:12] == b"WEBP", "invalid WebP")
    offset = 12
    while offset + 8 <= len(data):
        chunk_type = data[offset:offset + 4]
        chunk_size = struct.unpack_from("<I", data, offset + 4)[0]
        payload = offset + 8
        require(payload + chunk_size <= len(data), "truncated WebP")
        if chunk_type == b"VP8X":
            width = 1 + int.from_bytes(data[payload + 4:payload + 7], "little")
            height = 1 + int.from_bytes(data[payload + 7:payload + 10], "little")
            return width, height
        if chunk_type == b"VP8 ":
            require(data[payload + 3:payload + 6] == b"\x9d\x01\x2a", "invalid VP8 header")
            width = struct.unpack_from("<H", data, payload + 6)[0] & 0x3FFF
            height = struct.unpack_from("<H", data, payload + 8)[0] & 0x3FFF
            return width, height
        if chunk_type == b"VP8L":
            bits = int.from_bytes(data[payload + 1:payload + 5], "little")
            return (bits & 0x3FFF) + 1, ((bits >> 14) & 0x3FFF) + 1
        offset = payload + chunk_size + (chunk_size % 2)
    raise VerificationError("WebP has no dimension-bearing chunk")


def verify_visuals() -> None:
    require(webp_dimensions(DATA_DIRECTORY / "dry-side-pot-poker.webp") == (1200, 800), "hero must be 1200x800")
    svg_path = DATA_DIRECTORY / "decision-tree.svg"
    try:
        root = ET.fromstring(svg_path.read_text(encoding="utf-8"))
    except (OSError, ET.ParseError) as error:
        raise VerificationError(f"invalid SVG: {error}") from error
    require(root.attrib.get("viewBox") == "0 0 1200 700", "SVG viewBox drift")
    svg_text = svg_path.read_text(encoding="utf-8")
    require("<title id=\"title\">" in svg_text and "<desc id=\"desc\">" in svg_text, "SVG text alternative missing")
    require("Uncalled 150 returns" in svg_text and "Side pot = 300" in svg_text, "SVG branch labels missing")


def verify_specification(casebook: dict[str, Any]) -> None:
    spec = load_json("verification-spec.json")
    require(spec.get("recordType") == "verification-specification", "wrong verification spec type")
    require(spec.get("status") == "not-executed", "spec must not impersonate an execution receipt")
    require("not proof" in spec.get("statusBoundary", ""), "spec status boundary missing")
    expected = spec["expected"]
    require(expected["caseCount"] == 3 and expected["branchCount"] == 30, "spec case count drift")
    require(expected["thresholdRows"] == 12 and expected["manifestArtifacts"] == 9, "spec artifact count drift")
    actual_results = {
        "deadBluff": casebook["cases"][0],
        "mainPotIsolation": casebook["cases"][1],
        "thinValue": casebook["cases"][2],
    }
    for key, model_case in actual_results.items():
        require(
            expected["fixedCaseResults"][key] == {
                "checkEvChips": model_case["checkEvChips"],
                "betEvChips": model_case["betEvChips"],
                "deltaChips": model_case["betMinusCheckChips"],
            },
            f"verification-spec fixed result drift: {key}",
        )


def verify_manifest() -> None:
    lines = (DATA_DIRECTORY / "MANIFEST.sha256").read_text(encoding="ascii").splitlines()
    require(len(lines) == len(EXPECTED_FILES), "manifest entry count mismatch")
    pattern = re.compile(r"([0-9a-f]{64})  ([A-Za-z0-9._-]+)\Z", re.ASCII)
    parsed: list[tuple[str, str]] = []
    for line in lines:
        match = pattern.fullmatch(line)
        require(match is not None, f"invalid manifest row: {line!r}")
        parsed.append((match.group(1), match.group(2)))
    require(tuple(name for _, name in parsed) == EXPECTED_FILES, "manifest file order or set drift")
    for expected_hash, name in parsed:
        path = DATA_DIRECTORY / name
        require(path.is_file(), f"manifest target missing: {name}")
        require(hashlib.sha256(path.read_bytes()).hexdigest() == expected_hash, f"hash mismatch: {name}")


def verify_no_optimized_mode_gap() -> None:
    tree = ast.parse(Path(__file__).read_text(encoding="utf-8"), filename=str(__file__))
    require(not any(isinstance(node, ast.Assert) for node in ast.walk(tree)), "verifier contains assert")


def main() -> int:
    casebook = load_json("casebook.json")
    verify_casebook(casebook)
    verify_csvs(casebook)
    verify_visuals()
    verify_specification(casebook)
    verify_no_optimized_mode_gap()
    verify_manifest()
    print("Verified dry-side-pot bundle: 3 cases, 30 branches, 12 thresholds, 2 evaluators, and 9 manifest hashes.")
    return 0


if __name__ == "__main__":
    try:
        raise SystemExit(main())
    except VerificationError as error:
        print(f"VERIFICATION FAILED: {error}", file=sys.stderr)
        raise SystemExit(1) from error
