#!/usr/bin/env python3
"""Check a deliberately restricted TDA 2024 chip-action teaching casebook.

Standard library only; no speech, video, arbitrary history, or live rulings.
Default: verify JSON oracle and CSV. --write-csv: verify JSON then write CSV.
"""
import argparse
import copy
import csv
import io
import json
from pathlib import Path

HERE = Path(__file__).resolve().parent
PROFILE = "Poker TDA 2024 v1.0; restricted NLHE postflop opening-bet profile"
INPUT_KEYS = {
    "game", "street", "in_turn", "single_motion", "raise_rights",
    "prior_street_chips", "facing_opening_bet", "minimum_bet", "wager_faced",
    "total_stack", "chips_moved", "words", "word_timing", "declared_raise_to",
}
OUTPUT_KEYS = {"action", "final_wager", "change", "add_required", "stack_after", "full_raise"}
COLUMNS = [
    "id", "wager_faced", "minimum_bet", "total_stack", "chips_moved", "words",
    "word_timing", "declared_raise_to", "action", "final_wager", "change",
    "add_required", "stack_after", "full_raise", "rules",
]


def require(condition, message):
    if not condition:
        raise ValueError(message)


def integer(value, label, minimum=0):
    require(type(value) is int and value >= minimum, label + " must be an integer >= " + str(minimum))


def evaluate(inputs):
    """Reject outside-scope inputs instead of guessing an action."""
    require(isinstance(inputs, dict) and set(inputs) == INPUT_KEYS, "Unknown or missing input fields")
    require(inputs["game"] == "NLHE" and inputs["street"] == "postflop", "Only postflop NLHE is supported")
    for key in ("in_turn", "single_motion", "raise_rights"):
        require(inputs[key] is True, key + " must be established as true")
    require(type(inputs["prior_street_chips"]) is int and inputs["prior_street_chips"] == 0,
            "Prior street chips require a separate Rule 46 review")
    for key in ("minimum_bet", "total_stack"):
        integer(inputs[key], key, 1)
    integer(inputs["wager_faced"], "wager_faced")
    wager, minimum, stack = (inputs[k] for k in ("wager_faced", "minimum_bet", "total_stack"))
    require(type(inputs["facing_opening_bet"]) is bool, "Opening-bet flag must be boolean")
    require(inputs["facing_opening_bet"] == (wager > 0), "Only no wager or a full opening bet is supported")
    require(wager == 0 or wager >= minimum, "Short opening all-ins are unsupported")
    chips = inputs["chips_moved"]
    require(isinstance(chips, list) and len(chips) > 0, "A nonempty chip list is required")
    for chip in chips:
        integer(chip, "chip denomination", 1)
    moved = sum(chips)
    require(moved <= stack, "Chips moved exceed total stack")
    require(moved >= wager, "Undercalls are outside this casebook")
    words, timing, declared = (inputs[k] for k in ("words", "word_timing", "declared_raise_to"))
    if words == "":
        require(timing == "none" and declared is None, "Silent input has contradictory declaration fields")
    else:
        integer(declared, "declared_raise_to", 1)
        require(words == "Raise to " + format(declared, ","), "Only an exact, clear raise-to declaration is supported")
        require(timing == "before_chips_move", "Unclear, simultaneous or later words need staff review")
        require(wager > 0 and declared >= 2 * wager, "Declared underraises or opening declarations are unsupported")
        require(declared == moved, "This casebook requires declared total to equal chips moved")
        action, final = "raise", declared
    if words == "":
        if wager == 0:
            require(len(chips) == 1 and moved >= minimum, "Only a legal single-chip silent opening is supported")
            action, final = "bet", moved
        elif len(chips) == 1 or moved - min(chips) < wager:
            # Rule 44 or 45A comes before the last-chips branch.
            action, final = "call", wager
        elif moved == stack:
            action, final = "all-in", moved  # Rule 45B2; may be an incomplete raise.
        elif 2 * (moved - wager) < wager:
            action, final = "call", wager  # 45B1 -> 43, below halfway.
        else:
            action, final = "raise", max(moved, 2 * wager)
            require(final <= stack, "Stack cannot fund the required completion; outside this casebook")
    return {
        "action": action, "final_wager": final, "change": max(0, moved - final),
        "add_required": max(0, final - moved), "stack_after": stack - final,
        "full_raise": wager > 0 and final - wager >= wager,
    }


def verify_casebook(book):
    require(isinstance(book, dict) and book.get("schema_version") == 1, "Unsupported casebook schema")
    require(book.get("profile") == PROFILE, "Unsupported rules profile")
    cases = book.get("cases")
    require(isinstance(cases, list) and cases, "No cases supplied")
    seen = set()
    for case in cases:
        require(set(case) == {"id", "description", "inputs", "expected", "rules"}, "Unknown or missing case fields")
        require(isinstance(case["id"], str) and case["id"] and case["id"] not in seen, "Invalid or duplicate case id")
        seen.add(case["id"])
        require(isinstance(case["description"], str) and case["description"], "Missing case description")
        require(isinstance(case["rules"], list) and case["rules"] and all(isinstance(r, str) for r in case["rules"]), "Missing rule map")
        expected = case["expected"]
        require(isinstance(expected, dict) and set(expected) == OUTPUT_KEYS, "Unknown or missing expected result fields")
        for key in ("final_wager", "change", "add_required", "stack_after"):
            integer(expected[key], "expected " + key)
        require(type(expected["full_raise"]) is bool, "full_raise must be boolean")
        require(evaluate(case["inputs"]) == expected, "Model/oracle mismatch in " + case["id"])
    return cases


def csv_text(cases):
    output = io.StringIO(newline="")
    writer = csv.DictWriter(output, fieldnames=COLUMNS, lineterminator="\n")
    writer.writeheader()
    for case in cases:
        inp, expected = case["inputs"], case["expected"]
        row = {key: inp[key] for key in COLUMNS if key in inp}
        row.update(expected)
        row.update(id=case["id"], chips_moved="+".join(map(str, inp["chips_moved"])),
                   declared_raise_to="" if inp["declared_raise_to"] is None else inp["declared_raise_to"],
                   full_raise=str(expected["full_raise"]).lower(), rules=";".join(case["rules"]))
        writer.writerow(row)
    return output.getvalue()


def controls(cases):
    """Literal boundary oracles and explicit rejection controls; no random data."""
    base = copy.deepcopy(cases[0]["inputs"])
    base.update(wager_faced=400, minimum_bet=200, total_stack=5000)
    boundary = [([500, 99], "call", 400), ([500, 100], "raise", 800),
                ([500, 101], "raise", 800), ([400, 400], "raise", 800),
                ([500, 500], "raise", 1000)]
    for chips, action, final in boundary:
        trial = dict(base, chips_moved=chips)
        result = evaluate(trial)
        require((result["action"], result["final_wager"]) == (action, final), "Halfway/equality boundary failed")
    trial = dict(base, wager_faced=501, chips_moved=[500, 500])
    require(evaluate(trial)["action"] == "call", "All-chips-needed strict boundary failed")
    trial["wager_faced"] = 500
    require(evaluate(trial)["action"] == "raise", "Smallest-chip equality boundary failed")
    invalid = [
        {"prior_street_chips": 100}, {"in_turn": False}, {"single_motion": False},
        {"raise_rights": False}, {"street": "preflop"}, {"game": "PLO"},
        {"wager_faced": 100}, {"facing_opening_bet": False}, {"chips_moved": []},
        {"chips_moved": [0, 500]}, {"chips_moved": [-1, 501]}, {"chips_moved": [True]},
        {"chips_moved": [500.0]}, {"chips_moved": [6000]}, {"chips_moved": [100]},
        {"words": "maybe raise"}, {"word_timing": "after_chips_hit_table"},
        {"declared_raise_to": 1000}, {"total_stack": 700, "chips_moved": [500, 100]},
        {"unknown_fact": True}, {"in_turn": 1},
    ]
    for changes in invalid:
        try:
            evaluate(dict(base, **changes))
        except ValueError:
            continue
        raise ValueError("Unsupported input was accepted: " + repr(changes))
    bad = {"schema_version": 1, "profile": PROFILE, "cases": copy.deepcopy(cases)}
    bad["cases"][0]["expected"]["final_wager"] += 1
    try:
        verify_casebook(bad)
    except ValueError:
        pass
    else:
        raise ValueError("Corrupted oracle was accepted")
    return len(boundary) + 2, len(invalid) + 1


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--write-csv", action="store_true", help="After JSON verification, regenerate CSV with explicit LF newlines")
    args = parser.parse_args()
    book = json.loads((HERE / "cases.json").read_text(encoding="utf-8"))
    cases = verify_casebook(book)
    boundaries, rejected = controls(cases)
    expected_csv = csv_text(cases).encode("utf-8")
    if args.write_csv:
        (HERE / "cases.csv").write_bytes(expected_csv)
    require((HERE / "cases.csv").read_bytes() == expected_csv, "CSV differs from verified JSON or LF serialization")
    print(f"PASS: {len(cases)} reviewed cases; {boundaries} boundary checks; {rejected} invalid/tamper rejections; CSV byte parity.")


if __name__ == "__main__":
    try:
        main()
    except (ValueError, OSError, KeyError, TypeError, json.JSONDecodeError) as exc:
        raise SystemExit("FAIL: " + str(exc))
