#!/usr/bin/env python3
"""Separately implemented verifier for the mixed-strategy rounding release."""

from __future__ import annotations

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


ROOT = Path(__file__).resolve().parent
POT = Fraction(100)
GRIDS = (1, 5, 10, 25, 50)
FEATURED = (
    ("quarter-pot", Fraction(25)),
    ("one-third-pot", Fraction(100, 3)),
    ("half-pot", Fraction(50)),
    ("three-quarters-pot", Fraction(75)),
    ("pot", Fraction(100)),
    ("one-and-half-pot", Fraction(150)),
    ("double-pot", Fraction(200)),
)
RELEASE_FILES = (
    "README.md",
    "ablations.csv",
    "exploitability-rounding-ja.svg",
    "exploitability-rounding.svg",
    "explorer.html",
    "generate.mjs",
    "results.json",
    "scenarios.csv",
    "sweep.csv",
    "verify.py",
)
SCENARIO_COLUMNS = (
    "id", "labelEn", "labelJa", "betPotRatio", "gridPoints",
    "equilibriumQBetFrequency", "equilibriumCallFrequency",
    "roundedQBetFrequency", "roundedCallFrequency", "equilibriumPayoff",
    "roundedProfilePayoff", "profileDelta", "bettorBestResponseGain",
    "defenderBestResponseGain", "exploitability", "exploitabilityPctPot", "exact",
)
SWEEP_COLUMNS = (
    "betPotRatio", "gridPoints", "equilibriumQBetFrequency",
    "equilibriumCallFrequency", "roundedQBetFrequency", "roundedCallFrequency",
    "profileDelta", "bettorBestResponseGain", "defenderBestResponseGain",
    "exploitability", "exploitabilityPctPot", "exact",
)
ABLATION_COLUMNS = (
    "id", "labelEn", "labelJa", "qBetFrequency", "callFrequency",
    "profilePayoff", "profileDelta", "bettorBestResponseGain",
    "defenderBestResponseGain", "nashConv", "exploitability",
)
EVALUATION_KEYS = {
    "betPotRatio", "betChips", "gridPoints", "equilibriumQBetFrequency",
    "equilibriumCallFrequency", "roundedQBetFrequency", "roundedCallFrequency",
    "equilibriumPayoff", "roundedProfilePayoff", "profileDelta",
    "bettorBestResponseGain", "defenderBestResponseGain", "nashConv",
    "exploitability", "exploitabilityPctPot", "exact", "fractions",
}


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


def fraction_text(value: Fraction) -> str:
    return str(value.numerator) if value.denominator == 1 else f"{value.numerator}/{value.denominator}"


def round_probability(value: Fraction, grid_points: int) -> Fraction:
    units = value * 100 / grid_points
    rounded_units = (2 * units.numerator + units.denominator) // (2 * units.denominator)
    rounded_units = max(0, min(100 // grid_points, rounded_units))
    return Fraction(rounded_units * grid_points, 100)


def profile(pot: Fraction, bet: Fraction, q_bet: Fraction, call: Fraction) -> dict[str, Fraction]:
    value_branch = pot + call * bet
    q_bet_branch = (1 - call) * pot - call * bet
    payoff = Fraction(1, 2) * value_branch + Fraction(1, 2) * q_bet * q_bet_branch
    bettor_br = Fraction(1, 2) * value_branch + Fraction(1, 2) * max(Fraction(0), q_bet_branch)
    defender_fold = Fraction(1, 2) * pot * (1 + q_bet)
    defender_call = Fraction(1, 2) * (pot + bet * (1 - q_bet))
    defender_br_to_bettor = min(defender_fold, defender_call)
    gain_bettor = bettor_br - payoff
    gain_defender = payoff - defender_br_to_bettor
    return {
        "payoff": payoff,
        "bettor_gain": gain_bettor,
        "defender_gain": gain_defender,
        "nash_conv": gain_bettor + gain_defender,
        "exploitability": (gain_bettor + gain_defender) / 2,
    }


def evaluate(bet: Fraction, grid_points: int) -> dict:
    q_bet = bet / (POT + bet)
    call = POT / (POT + bet)
    rounded_q_bet = round_probability(q_bet, grid_points)
    rounded_call = round_probability(call, grid_points)
    equilibrium = profile(POT, bet, q_bet, call)
    rounded = profile(POT, bet, rounded_q_bet, rounded_call)
    return {
        "betPotRatio": bet / POT,
        "betChips": bet,
        "gridPoints": grid_points,
        "equilibriumQBetFrequency": q_bet,
        "equilibriumCallFrequency": call,
        "roundedQBetFrequency": rounded_q_bet,
        "roundedCallFrequency": rounded_call,
        "equilibriumPayoff": equilibrium["payoff"],
        "roundedProfilePayoff": rounded["payoff"],
        "profileDelta": rounded["payoff"] - equilibrium["payoff"],
        "bettorBestResponseGain": rounded["bettor_gain"],
        "defenderBestResponseGain": rounded["defender_gain"],
        "nashConv": rounded["nash_conv"],
        "exploitability": rounded["exploitability"],
        "exploitabilityPctPot": rounded["exploitability"] / POT * 100,
        "exact": rounded["exploitability"] == 0,
        "fractions": {
            "equilibriumQBetFrequency": fraction_text(q_bet),
            "equilibriumCallFrequency": fraction_text(call),
            "roundedQBetFrequency": fraction_text(rounded_q_bet),
            "roundedCallFrequency": fraction_text(rounded_call),
            "exploitability": fraction_text(rounded["exploitability"]),
        },
    }


def close(actual: float | str, expected: Fraction, field: str, tolerance: float = 1e-9) -> None:
    delta = abs(float(actual) - float(expected))
    require(delta <= tolerance, f"{field}: {actual} != {float(expected)} (delta {delta})")


def verify_row(row: dict, bet: Fraction, grid_points: int, label: str) -> None:
    expected = evaluate(bet, grid_points)
    for field in set(row).intersection(expected):
        value = expected[field]
        if field == "fractions":
            require(row[field] == value, f"{label} fractions")
        elif isinstance(value, bool):
            actual = row[field]
            if isinstance(actual, str):
                actual = actual.lower() == "true"
            require(actual is value, f"{label} {field}: {actual} != {value}")
        elif field == "gridPoints":
            require(int(row[field]) == value, f"{label} gridPoints")
        else:
            tolerance = 5e-7 if field == "exploitabilityPctPot" else 5e-12
            close(row[field], value, f"{label} {field}", tolerance)


def verify_csv_schema(path: Path, expected: tuple[str, ...]) -> list[dict[str, str]]:
    with path.open(encoding="utf-8", newline="") as handle:
        reader = csv.DictReader(handle)
        require(tuple(reader.fieldnames or ()) == expected, f"CSV schema: {path.name}")
        return list(reader)


def verify_chart(filename: str, language: str, sweep_by_grid: dict[int, list[tuple[int, dict]]]) -> None:
    svg = (ROOT / filename).read_text(encoding="utf-8")
    require('width="1200" height="700"' in svg, f"SVG dimensions: {filename}")
    require('role="img"' in svg and f'lang="{language}"' in svg, f"SVG accessibility: {filename}")
    require("<title id=" in svg and "<desc id=" in svg, f"SVG title/description: {filename}")
    paths = re.findall(r'<path data-grid-points="(\d+)" d="([^"]+)"', svg)
    require([int(grid) for grid, _ in paths] == [5, 10, 25, 50], f"Chart series: {filename}")
    require(svg.count("stroke-dasharray=") >= 6, f"Non-color chart encoding: {filename}")
    for grid_text, path_data in paths:
        grid = int(grid_text)
        points = [(float(x), float(y)) for x, y in re.findall(r'[ML] ([\d.]+) ([\d.]+)', path_data)]
        rows = sweep_by_grid[grid]
        require(len(points) == len(rows) == 191, f"Chart point count: {filename}/{grid}")
        for (actual_x, actual_y), (bet_percent, row) in zip(points, rows, strict=True):
            expected_x = 105 + (((bet_percent / 100) - 0.1) / 1.9) * 1010
            expected_y = 120 + 460 - (min(float(row["exploitabilityPctPot"]), 15) / 15) * 460
            require(abs(actual_x - expected_x) <= 0.011, f"Chart x: {filename}/{grid}/{bet_percent}")
            require(abs(actual_y - expected_y) <= 0.011, f"Chart y: {filename}/{grid}/{bet_percent}")


def main() -> None:
    payload = json.loads((ROOT / "results.json").read_text(encoding="utf-8"))
    require(set(payload) == {"schemaVersion", "generatedAt", "model", "headline", "halfPotAblation", "featured", "sweepSummary"}, "Top-level JSON schema")
    require(payload["schemaVersion"] == 2, "Unexpected schema version")
    require(payload["model"]["potBeforeBet"] == 100, "Unexpected pot")
    require("conditional probability that Q bets" in payload["model"]["qBetFrequencyDefinition"], "Q-bet denominator declaration")
    require("defender payoff equals" in payload["model"]["constantSumPayoff"], "Constant-sum payoff declaration")

    featured = payload["featured"]
    require(len(featured) == len(FEATURED) * len(GRIDS), "Featured row count")
    expected_featured_keys = EVALUATION_KEYS | {"id", "labelEn", "labelJa"}
    require(all(set(row) == expected_featured_keys for row in featured), "Featured JSON row schema")
    by_key = {(row["id"], row["gridPoints"]): row for row in featured}
    for identifier, bet in FEATURED:
        for grid in GRIDS:
            verify_row(by_key[(identifier, grid)], bet, grid, f"featured {identifier}/{grid}")

    headline = payload["headline"]
    require(set(headline) == expected_featured_keys, "Headline JSON schema")
    require(headline["id"] == "three-quarters-pot" and headline["gridPoints"] == 25, "Headline fixture")
    close(headline["profileDelta"], Fraction(25, 56), "headline profileDelta")
    close(headline["bettorBestResponseGain"], Fraction(25, 8), "headline bettor gain")
    close(headline["defenderBestResponseGain"], Fraction(25, 8), "headline defender gain")
    close(headline["exploitability"], Fraction(25, 8), "headline exploitability")
    require(headline["fractions"]["equilibriumQBetFrequency"] == "3/7", "Headline Q-bet fraction")
    require(headline["fractions"]["equilibriumCallFrequency"] == "4/7", "Headline call fraction")

    scenarios = verify_csv_schema(ROOT / "scenarios.csv", SCENARIO_COLUMNS)
    require(len(scenarios) == 35, "Scenario CSV row count")
    for row in scenarios:
        bet = dict(FEATURED)[row["id"]]
        verify_row(row, bet, int(row["gridPoints"]), f"scenarios.csv {row['id']}/{row['gridPoints']}")

    sweep = verify_csv_schema(ROOT / "sweep.csv", SWEEP_COLUMNS)
    require(len(sweep) == 191 * len(GRIDS), "Sweep CSV row count")
    sweep_by_grid: dict[int, list[tuple[int, dict]]] = {grid: [] for grid in GRIDS}
    for row in sweep:
        bet_percent = round(float(row["betPotRatio"]) * 100)
        verify_row(row, Fraction(bet_percent), int(row["gridPoints"]), f"sweep.csv {row['betPotRatio']}/{row['gridPoints']}")
        sweep_by_grid[int(row["gridPoints"])].append((bet_percent, row))

    summaries = payload["sweepSummary"]
    require([summary["gridPoints"] for summary in summaries] == list(GRIDS), "Sweep-summary grid order")
    for summary in summaries:
        require(set(summary) == {"gridPoints", "exactIntegerBetSizes", "maximumObserved"}, "Sweep-summary schema")
        grid = summary["gridPoints"]
        rows = [(percent, evaluate(Fraction(percent), grid)) for percent in range(10, 201)]
        exact_sizes = [percent for percent, row in rows if row["exact"]]
        require(summary["exactIntegerBetSizes"] == exact_sizes, f"Sweep exact sizes: {grid}")
        maximum_percent, maximum_row = max(rows, key=lambda item: item[1]["exploitability"])
        require(set(summary["maximumObserved"]) == EVALUATION_KEYS, f"Sweep maximum schema: {grid}")
        verify_row(summary["maximumObserved"], Fraction(maximum_percent), grid, f"sweep maximum/{grid}")
        close(summary["maximumObserved"]["exploitability"], maximum_row["exploitability"], f"sweep maximum exploitability/{grid}")

    ablations = payload["halfPotAblation"]
    require([row["id"] for row in ablations] == ["equilibrium", "bluff-only", "call-only", "both"], "Ablation order")
    require(all(set(row) == set(ABLATION_COLUMNS) for row in ablations), "Ablation JSON schema")
    expected_ablations = {
        "equilibrium": (Fraction(1, 3), Fraction(2, 3)),
        "bluff-only": (Fraction(1, 4), Fraction(2, 3)),
        "call-only": (Fraction(1, 3), Fraction(3, 4)),
        "both": (Fraction(1, 4), Fraction(3, 4)),
    }
    equilibrium_payoff = profile(POT, Fraction(50), Fraction(1, 3), Fraction(2, 3))["payoff"]
    for row in ablations:
        q_bet, call = expected_ablations[row["id"]]
        result = profile(POT, Fraction(50), q_bet, call)
        close(row["qBetFrequency"], q_bet, f"ablation {row['id']} Q bet")
        close(row["callFrequency"], call, f"ablation {row['id']} call")
        close(row["profilePayoff"], result["payoff"], f"ablation {row['id']} payoff")
        close(row["profileDelta"], result["payoff"] - equilibrium_payoff, f"ablation {row['id']} delta")
        close(row["bettorBestResponseGain"], result["bettor_gain"], f"ablation {row['id']} bettor gain")
        close(row["defenderBestResponseGain"], result["defender_gain"], f"ablation {row['id']} defender gain")
        close(row["nashConv"], result["nash_conv"], f"ablation {row['id']} NashConv")
        close(row["exploitability"], result["exploitability"], f"ablation {row['id']} exploitability")
    ablation_csv = verify_csv_schema(ROOT / "ablations.csv", ABLATION_COLUMNS)
    require(len(ablation_csv) == 4, "Ablation CSV row count")
    for json_row, csv_row in zip(ablations, ablation_csv, strict=True):
        require(json_row["id"] == csv_row["id"], "Ablation CSV id")
        for field in ABLATION_COLUMNS[3:]:
            close(csv_row[field], Fraction(str(json_row[field])), f"ablations.csv {json_row['id']}/{field}")

    verify_chart("exploitability-rounding.svg", "en", sweep_by_grid)
    verify_chart("exploitability-rounding-ja.svg", "ja", sweep_by_grid)
    explorer = (ROOT / "explorer.html").read_text(encoding="utf-8")
    require("class R{" in explorer and "BigInt" in explorer, "Exact-rational calculator implementation")
    require("丸め後の対戦EV差" in explorer and "ベッターのBR改善量" in explorer, "Calculator terminology")

    require({path.name for path in ROOT.iterdir() if path.is_file()} == set(RELEASE_FILES) | {"MANIFEST.sha256"}, "Release allow-list")
    manifest_lines = (ROOT / "MANIFEST.sha256").read_text(encoding="utf-8").strip().splitlines()
    require([line.split("  ", 1)[1] for line in manifest_lines] == list(RELEASE_FILES), "Manifest order")
    for line in manifest_lines:
        digest, filename = line.split("  ", 1)
        actual = hashlib.sha256((ROOT / filename).read_bytes()).hexdigest()
        require(actual == digest, f"Manifest digest: {filename}")

    print("Separate verification passed: 35 scenarios, 4 ablations, 955 sweep rows, 5 summaries, 2 chart paths, calculator, and 10 manifest files.")


if __name__ == "__main__":
    main()
