#!/usr/bin/env python3
"""Independently verify the public pot-commitment current-state examples."""

from __future__ import annotations

import itertools
import json
import math
import sys
from collections import Counter
from pathlib import Path


DEFAULT_DATA_PATH = Path(__file__).with_name("pot-committed-current-state-cases.json")
RANKS = {rank: value for value, rank in enumerate("23456789TJQKA", start=2)}
SUITS = set("cdhs")
TOLERANCE = 1e-9


def close(actual: float, expected: float, label: str) -> None:
    if not math.isclose(actual, expected, rel_tol=TOLERANCE, abs_tol=TOLERANCE):
        raise AssertionError(f"{label}: expected {expected}, received {actual}")


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


def five_card_rank(cards: tuple[str, ...]) -> tuple[int, ...]:
    parsed = [parse_card(card) for card in cards]
    ranks = sorted((rank for rank, _ in parsed), reverse=True)
    counts = Counter(ranks)
    groups = sorted(((count, rank) for rank, count in counts.items()), reverse=True)
    flush = len({suit for _, suit in parsed}) == 1
    unique = sorted(set(ranks), reverse=True)
    if 14 in unique:
        unique.append(1)
    straight_high = next(
        (unique[index] for index in range(len(unique) - 4) if unique[index] - unique[index + 4] == 4),
        0,
    )
    if flush and straight_high:
        return (8, straight_high)
    if groups[0][0] == 4:
        return (7, groups[0][1], groups[1][1])
    if groups[0][0] == 3 and groups[1][0] == 2:
        return (6, groups[0][1], groups[1][1])
    if flush:
        return (5, *ranks)
    if straight_high:
        return (4, straight_high)
    if groups[0][0] == 3:
        kickers = sorted((rank for rank in ranks if rank != groups[0][1]), reverse=True)
        return (3, groups[0][1], *kickers[:2])
    pairs = sorted((rank for count, rank in groups if count == 2), reverse=True)
    if len(pairs) >= 2:
        kicker = max(rank for rank in ranks if rank not in pairs[:2])
        return (2, pairs[0], pairs[1], kicker)
    if len(pairs) == 1:
        kickers = sorted((rank for rank in ranks if rank != pairs[0]), reverse=True)
        return (1, pairs[0], *kickers[:3])
    return (0, *ranks)


def seven_card_rank(cards: list[str]) -> tuple[int, ...]:
    if len(cards) != 7 or len(set(cards)) != 7:
        raise AssertionError(f"Expected seven distinct cards, received {cards}")
    return max(five_card_rank(combo) for combo in itertools.combinations(cards, 5))


def verify_range(name: str, range_data: dict, board: list[str], hero: list[str]) -> None:
    hero_rank = seven_card_rank(board + hero)
    wins = ties = 0
    combinations = range_data["value"] + range_data["bluffs"]
    if len(combinations) != range_data["total"] or len(set(combinations)) != len(combinations):
        raise AssertionError(f"{name}: duplicate or miscounted combinations")
    for hand in combinations:
        if len(hand) != 4:
            raise AssertionError(f"{name}: invalid physical combination {hand}")
        villain = [hand[:2], hand[2:]]
        if len(set(board + hero + villain)) != 9:
            raise AssertionError(f"{name}: card collision in {hand}")
        villain_rank = seven_card_rank(board + villain)
        if hero_rank > villain_rank:
            wins += 1
        elif hero_rank == villain_rank:
            ties += 1
    if (wins, ties) != (range_data["heroWins"], range_data["ties"]):
        raise AssertionError(f"{name}: evaluator found {wins} wins and {ties} ties")
    equity = (wins + ties / 2) / len(combinations)
    close(equity, range_data["heroEquity"], f"{name}.heroEquity")


def evaluate_closed_action(case: dict) -> dict:
    history = case["history"]
    state = case["currentState"]
    contributed_pot = (
        history["heroEarlierContribution"]
        + history["opponentEarlierContribution"]
        + history["foldedPlayersEarlierContribution"]
        + history["opponentCurrentBet"]
    )
    close(contributed_pot, state["potBeforeDecision"], f"{case['id']} contribution ledger")
    close(
        history["heroStartingStack"] - history["heroEarlierContribution"],
        state["heroStackBeforeDecision"],
        f"{case['id']} current Hero stack",
    )
    if not state["actionClosedAfterCall"] or not state["noRemainingRake"]:
        raise AssertionError(f"{case['id']}: closed-action assumptions are missing")
    pot = state["potBeforeDecision"]
    call_cost = state["callCost"]
    equity = state["equity"]
    prior = history["heroEarlierContribution"]
    final_pot = pot + call_cost
    gross_receipt = equity * final_pot
    call_ev = gross_receipt - call_cost
    return {
        "finalPotIfCall": final_pot,
        "requiredEquity": call_cost / final_pot,
        "expectedGrossReceipt": gross_receipt,
        "foldEvFromDecision": 0,
        "callEvFromDecision": call_ev,
        "deltaCallMinusFold": call_ev,
        "wholeHandFoldResult": -prior,
        "wholeHandExpectedCallResult": gross_receipt - prior - call_cost,
        "preferredAction": "call" if call_ev > 0 else "fold" if call_ev < 0 else "indifferent",
    }


def verify_history(case: dict, ranges: dict) -> None:
    close(
        case["currentState"]["equity"],
        ranges[case["currentState"]["conditionalRangeId"]]["heroEquity"],
        f"{case['id']} range equity",
    )
    recomputed = evaluate_closed_action(case)
    for key, expected in recomputed.items():
        actual = case["result"][key]
        if isinstance(expected, (float, int)):
            close(actual, expected, f"{case['id']}.{key}")
        elif actual != expected:
            raise AssertionError(f"{case['id']}.{key}: expected {expected}, received {actual}")


def verify_future_tree(section: dict) -> None:
    values = section["inputs"]
    result = section["result"]
    pot_after_call = values["potBeforeDecision"] + values["callCost"]
    bet_probability = 1 - values["riverCheckbackProbability"]
    naive_equity = (
        values["riverCheckbackProbability"] * values["equityVersusCheckbackRange"]
        + bet_probability * values["equityVersusBetRange"]
    )
    naive_ev = naive_equity * pot_after_call - values["callCost"]
    final_pot_after_river_call = pot_after_call + 2 * values["riverBet"]
    river_threshold = values["riverBet"] / final_pot_after_river_call
    river_call_ev = values["equityVersusBetRange"] * final_pot_after_river_call - values["riverBet"]
    optimal_river_ev = max(0, river_call_ev)
    full_tree_ev = (
        -values["callCost"]
        + values["riverCheckbackProbability"] * values["equityVersusCheckbackRange"] * pot_after_call
        + bet_probability * optimal_river_ev
    )
    always_call_ev = (
        -values["callCost"]
        + values["riverCheckbackProbability"] * values["equityVersusCheckbackRange"] * pot_after_call
        + bet_probability * river_call_ev
    )
    expected = {
        "potAfterCurrentCall": pot_after_call,
        "riverBetProbability": bet_probability,
        "naiveUnconditionalEquity": naive_equity,
        "naiveOneStepEv": naive_ev,
        "finalPotAfterRiverCall": final_pot_after_river_call,
        "riverBreakEvenEquity": river_threshold,
        "riverCallEv": river_call_ev,
        "riverFoldEv": 0,
        "optimalRiverAction": "call" if river_call_ev > 0 else "fold" if river_call_ev < 0 else "indifferent",
        "fullTreeCurrentCallEv": full_tree_ev,
        "fullTreeCurrentCallEvIfRiverAlwaysCalled": always_call_ev,
        "callVersusFoldResult": "call" if full_tree_ev > 0 else "fold" if full_tree_ev < 0 else "indifferent",
    }
    for key, expected_value in expected.items():
        actual = result[key]
        if isinstance(expected_value, (float, int)):
            close(actual, expected_value, f"futureActionCounterexample.result.{key}")
        elif actual != expected_value:
            raise AssertionError(f"futureActionCounterexample.result.{key}: expected {expected_value}, received {actual}")


def verify(data: dict) -> None:
    if data["schemaVersion"] != 1 or data["modelVersion"] != "1.0.0":
        raise AssertionError("Unsupported public data version")
    board = data["cards"]["board"]
    hero = data["cards"]["hero"]
    if len(set(board + hero)) != 7:
        raise AssertionError("Board and Hero cards collide")
    for range_name, range_data in data["ranges"].items():
        verify_range(range_name, range_data, board, hero)

    histories = {case["id"]: case for case in data["closedActionHistories"]}
    expected_ids = {
        "history-a-eight-handed-dead-money",
        "history-b-heads-up-hero-invested-more",
        "history-c-range-information-changed",
    }
    if set(histories) != expected_ids:
        raise AssertionError("Unexpected history IDs")
    for case in histories.values():
        verify_history(case, data["ranges"])

    history_a = histories["history-a-eight-handed-dead-money"]
    history_b = histories["history-b-heads-up-hero-invested-more"]
    history_c = histories["history-c-range-information-changed"]
    expected_matching_fields = [
        "potBeforeDecision",
        "callCost",
        "equity",
        "conditionalRangeId",
        "boardId",
        "heroHand",
        "heroStackBeforeDecision",
        "actionClosedAfterCall",
        "noRemainingRake",
    ]
    if data["pairedHistoryProof"]["matchingDecisionFields"] != expected_matching_fields:
        raise AssertionError("A/B proof does not declare the complete expected current-state field set")
    if history_a["currentState"] != history_b["currentState"]:
        raise AssertionError("A/B current states are not identical")
    close(history_a["result"]["callEvFromDecision"], 50, "History A call EV")
    close(history_b["result"]["callEvFromDecision"], 50, "History B call EV")
    close(history_a["result"]["wholeHandExpectedCallResult"], -30, "History A whole-hand result")
    close(history_b["result"]["wholeHandExpectedCallResult"], -100, "History B whole-hand result")
    proof = data["pairedHistoryProof"]
    if proof["earlierHeroContributions"] != [
        history_a["history"]["heroEarlierContribution"],
        history_b["history"]["heroEarlierContribution"],
    ]:
        raise AssertionError("A/B proof contribution array does not match the histories")
    if proof["callEvFromDecision"] != [
        history_a["result"]["callEvFromDecision"],
        history_b["result"]["callEvFromDecision"],
    ]:
        raise AssertionError("A/B proof EV array does not match the histories")
    if proof["wholeHandExpectedCallResults"] != [
        history_a["result"]["wholeHandExpectedCallResult"],
        history_b["result"]["wholeHandExpectedCallResult"],
    ]:
        raise AssertionError("A/B proof whole-hand array does not match the histories")

    different_state_fields = sorted(
        key
        for key in history_b["currentState"]
        if history_b["currentState"][key] != history_c["currentState"][key]
    )
    if different_state_fields != ["conditionalRangeId", "equity"]:
        raise AssertionError(f"Unexpected B/C state differences: {different_state_fields}")
    counterexample = data["historyChangesInformationCounterexample"]
    if counterexample["changedDecisionInputs"] != ["conditionalRangeId", "equity"]:
        raise AssertionError("B/C counterexample does not declare its complete changed-input set")
    if counterexample["callEvFromDecision"] != [
        history_b["result"]["callEvFromDecision"],
        history_c["result"]["callEvFromDecision"],
    ]:
        raise AssertionError("B/C counterexample EV array does not match the histories")
    if counterexample["preferredActions"] != [
        history_b["result"]["preferredAction"],
        history_c["result"]["preferredAction"],
    ]:
        raise AssertionError("B/C counterexample action array does not match the histories")
    close(history_c["result"]["callEvFromDecision"], -25, "History C call EV")
    if [history_b["result"]["preferredAction"], history_c["result"]["preferredAction"]] != ["call", "fold"]:
        raise AssertionError("B/C decisions must flip from call to fold")

    verify_future_tree(data["futureActionCounterexample"])
    close(data["futureActionCounterexample"]["result"]["naiveOneStepEv"], 4, "future one-step proxy")
    close(data["futureActionCounterexample"]["result"]["fullTreeCurrentCallEv"], -20, "future full-tree EV")


def main() -> None:
    path = Path(sys.argv[1]) if len(sys.argv) > 1 else DEFAULT_DATA_PATH
    data = json.loads(path.read_text(encoding="utf-8"))
    verify(data)
    print(
        "Verified card legality, exact listed-range equity, three contribution ledgers, "
        "the A/B invariance proof, the B/C decision flip, and the future-action proxy reversal."
    )


if __name__ == "__main__":
    main()
