#!/usr/bin/env python3
"""Independent verifier for the pocket-aces waiting-time release bundle."""

from __future__ import annotations

import csv
import hashlib
import json
import math
from fractions import Fraction
from pathlib import Path


ROOT = Path(__file__).resolve().parent


def choose(n: int, k: int) -> int:
    return math.comb(n, k)


def seen_by(hands: int, probability: float) -> float:
    return -math.expm1(hands * math.log1p(-probability))


def first_hit_quantile(probability: float, quantile: float) -> int:
    candidate = math.ceil(math.log1p(-quantile) / math.log1p(-probability))
    while candidate > 0 and seen_by(candidate - 1, probability) >= quantile:
        candidate -= 1
    while seen_by(candidate, probability) < quantile:
        candidate += 1
    return candidate


def binomial_probability(n: int, k: int, probability: float) -> float:
    return math.comb(n, k) * probability**k * (1 - probability) ** (n - k)


def reached_repetitions(hands: int, probability: float, target: int) -> float:
    return 1 - sum(binomial_probability(hands, k, probability) for k in range(target))


def repeat_quantile(probability: float, target: int, quantile: float) -> int:
    low = target
    high = max(low, math.ceil(target / probability))
    while reached_repetitions(high, probability, target) < quantile:
        high *= 2
    while low < high:
        middle = (low + high) // 2
        if reached_repetitions(middle, probability, target) >= quantile:
            high = middle
        else:
            low = middle + 1
    return low


def assert_close(actual: float, expected: float, tolerance: float = 5e-12) -> None:
    if not math.isclose(actual, expected, rel_tol=tolerance, abs_tol=tolerance):
        raise AssertionError(f"{actual} != {expected}")


def check(condition: bool, message: str) -> None:
    if not condition:
        raise AssertionError(message)


def main() -> None:
    checks = 0
    experiment = json.loads((ROOT / "experiment.json").read_text(encoding="utf-8"))
    total = choose(52, 2)
    check(total == 1326, "52 choose 2 must equal 1326")
    checks += 1

    expected_fractions = {
        "exact_suited_combo": Fraction(1, total),
        "pocket_aces": Fraction(choose(4, 2), total),
        "ace_king_any_suits": Fraction(16, total),
        "queens_or_better": Fraction(18, total),
        "any_pocket_pair": Fraction(13 * choose(4, 2), total),
        "pocket_pair_flop_rank_match": Fraction(13 * choose(4, 2), total)
        * Fraction(choose(50, 3) - choose(48, 3), choose(50, 3)),
    }
    events = {event["id"]: event for event in experiment["events"]}
    check(set(events) == set(expected_fractions), "event identifiers do not match")
    checks += 1
    for event_id, fraction in expected_fractions.items():
        event = events[event_id]
        check(
            Fraction(int(event["numerator"]), int(event["denominator"])) == fraction,
            f"fraction mismatch for {event_id}",
        )
        probability = float(fraction)
        assert_close(event["probability"], probability)
        assert_close(event["percent"], probability * 100)
        assert_close(event["oneIn"], 1 / probability)
        assert_close(event["meanHands"], 1 / probability)
        check(event["medianHands"] == first_hit_quantile(probability, 0.5), f"median mismatch for {event_id}")
        check(event["percentile90Hands"] == first_hit_quantile(probability, 0.9), f"p90 mismatch for {event_id}")
        check(event["percentile95Hands"] == first_hit_quantile(probability, 0.95), f"p95 mismatch for {event_id}")
        checks += 8

    pocket_aces_probability = float(Fraction(1, 221))
    pocket_aces = events["pocket_aces"]
    check(pocket_aces["meanHands"] == 221, "AA mean must be exactly 221")
    check(pocket_aces["medianHands"] == 153, "AA median must be 153")
    check(pocket_aces["percentile90Hands"] == 508, "AA p90 must be 508")
    check(pocket_aces["percentile95Hands"] == 661, "AA p95 must be 661")
    assert_close(seen_by(500, pocket_aces_probability), 0.8964372513358113)
    checks += 5

    conditional = Fraction(choose(50, 3) - choose(48, 3), choose(50, 3))
    check(conditional == Fraction(144, 1225), "conditional flop fraction mismatch")
    check(expected_fractions["pocket_pair_flop_rank_match"] == Fraction(144, 20825), "combined flop fraction mismatch")
    checks += 2

    with (ROOT / "aa-cdf.csv").open(encoding="utf-8", newline="") as handle:
        cdf_rows = list(csv.DictReader(handle))
    check(len(cdf_rows) == 1001, "AA CDF must contain 1,001 data rows")
    checks += 1
    for row in cdf_rows:
        hands = int(row["hands"])
        expected_seen = seen_by(hands, pocket_aces_probability)
        assert_close(float(row["seen_at_least_once"]), expected_seen, 6e-12)
        assert_close(float(row["no_pocket_aces"]), 1 - expected_seen, 6e-12)
        checks += 2

    with (ROOT / "event-waits.csv").open(encoding="utf-8", newline="") as handle:
        event_rows = list(csv.DictReader(handle))
    check(len(event_rows) == len(expected_fractions), "event CSV row count mismatch")
    check({row["id"] for row in event_rows} == set(expected_fractions), "event CSV identifiers do not match")
    checks += 2
    for row in event_rows:
        event = events[row["id"]]
        check(row["label_en"] == event["labelEn"], f"English label mismatch for {row['id']}")
        check(row["label_ja"] == event["labelJa"], f"Japanese label mismatch for {row['id']}")
        check(row["success_numerator"] == event["numerator"], f"numerator mismatch for {row['id']}")
        check(row["success_denominator"] == event["denominator"], f"denominator mismatch for {row['id']}")
        check(row["probability_per_hand"] == f"{event['probability']:.12f}", f"probability mismatch for {row['id']}")
        check(row["percent_per_hand"] == f"{event['percent']:.9f}", f"percent mismatch for {row['id']}")
        check(row["one_in_hands"] == f"{event['oneIn']:.6f}", f"one-in mismatch for {row['id']}")
        check(row["mean_hands"] == f"{event['meanHands']:.6f}", f"mean mismatch for {row['id']}")
        check(int(row["median_hands"]) == event["medianHands"], f"median mismatch for {row['id']}")
        check(int(row["p90_hands"]) == event["percentile90Hands"], f"p90 mismatch for {row['id']}")
        check(int(row["p95_hands"]) == event["percentile95Hands"], f"p95 mismatch for {row['id']}")
        check(row["scope_en"] == event["scopeEn"], f"English scope mismatch for {row['id']}")
        check(row["scope_ja"] == event["scopeJa"], f"Japanese scope mismatch for {row['id']}")
        checks += 13

    repeat_lookup = {row["targetOccurrences"]: row for row in experiment["pocketAcesRepeatWaits"]}
    expected_repeats = {
        1: (221, 153, 508, 661),
        5: (1105, 1032, 1765, 2021),
        10: (2210, 2137, 3137, 3468),
        20: (4420, 4347, 5722, 6157),
    }
    for target, expected in expected_repeats.items():
        row = repeat_lookup[target]
        actual = (
            int(row["meanHands"]),
            row["medianHands"],
            row["percentile90Hands"],
            row["percentile95Hands"],
        )
        check(actual == expected, f"repeat-wait fixture mismatch for {target} AA occurrences")
        check(row["medianHands"] == repeat_quantile(pocket_aces_probability, target, 0.5), f"repeat median mismatch for {target}")
        check(row["percentile90Hands"] == repeat_quantile(pocket_aces_probability, target, 0.9), f"repeat p90 mismatch for {target}")
        check(row["percentile95Hands"] == repeat_quantile(pocket_aces_probability, target, 0.95), f"repeat p95 mismatch for {target}")
        checks += 4

    with (ROOT / "aa-repeat-waits.csv").open(encoding="utf-8", newline="") as handle:
        repeat_rows = list(csv.DictReader(handle))
    check(len(repeat_rows) == len(expected_repeats), "repeat-wait CSV row count mismatch")
    check(
        {int(row["target_aa_occurrences"]) for row in repeat_rows} == set(expected_repeats),
        "repeat-wait CSV target identifiers do not match",
    )
    checks += 2
    for csv_row in repeat_rows:
        target = int(csv_row["target_aa_occurrences"])
        row = repeat_lookup[target]
        check(csv_row["mean_hands"] == f"{row['meanHands']:.6f}", f"repeat mean mismatch for target {target}")
        check(int(csv_row["median_hands"]) == row["medianHands"], f"repeat median mismatch for target {target}")
        check(int(csv_row["p90_hands"]) == row["percentile90Hands"], f"repeat p90 mismatch for target {target}")
        check(int(csv_row["p95_hands"]) == row["percentile95Hands"], f"repeat p95 mismatch for target {target}")
        checks += 4

    for locale in ("en", "ja"):
        name = "waiting-curve-ja.svg" if locale == "ja" else "waiting-curve.svg"
        svg = (ROOT / name).read_text(encoding="utf-8")
        check("viewBox=\"0 0 1200 700\"" in svg, f"missing SVG viewBox in {name}")
        check("role=\"img\"" in svg, f"missing SVG role in {name}")
        check(f"lang=\"{locale}\"" in svg, f"missing SVG language in {name}")
        check("<title" in svg and "<desc" in svg, f"missing SVG accessible text in {name}")
        check("153" in svg and "221" in svg and "508" in svg, f"missing markers in {name}")
        checks += 5

    for line in (ROOT / "MANIFEST.sha256").read_text(encoding="utf-8").splitlines():
        expected_hash, filename = line.split("  ", maxsplit=1)
        actual_hash = hashlib.sha256((ROOT / filename).read_bytes()).hexdigest()
        check(actual_hash == expected_hash, f"manifest mismatch for {filename}")
        checks += 1

    print(f"Verified pocket-aces waiting-time bundle: {checks} independent checks passed.")


if __name__ == "__main__":
    main()
