"""
Proof: Generalized ordinal potentials imply FIP and a pure NE; exact-potential
maximizers are pure Nash equilibria. (Monderer & Shapley, 1996)
Generated: 2026-04-28

This is a deductive theorem proof. The verdict is established by the
argument written in proof.md's `## Proof` section. The computations below
are *implementation regression checks* — they spot-check the code that
decides whether a given finite instance satisfies the formal hypotheses
(GOP, exact potential, FIP, pure NE), not the deductive argument itself.

proof.md ordering (sentence case as written; loader normalizes to title):
  ## Theorem statement
  ## Proof
  ## Corollaries
  ## Scope
  ## Relation to prior work
  ## What could challenge this verdict?
  ## Conclusion

Sampling counts must NOT appear in proof.md body prose; they live in
proof_audit.md under `## Implementation regression checks`.
"""
import os
import random
import sys

PROOF_ENGINE_ROOT = os.environ.get("PROOF_ENGINE_ROOT")
if not PROOF_ENGINE_ROOT:
    _d = os.path.dirname(os.path.abspath(__file__))
    while _d != os.path.dirname(_d):
        if os.path.isdir(os.path.join(_d, "proof-engine", "skills", "proof-engine", "scripts")):
            PROOF_ENGINE_ROOT = os.path.join(_d, "proof-engine", "skills", "proof-engine")
            break
        _d = os.path.dirname(_d)
    if not PROOF_ENGINE_ROOT:
        raise RuntimeError("PROOF_ENGINE_ROOT not set and skill dir not found via walk-up from proof.py")
sys.path.insert(0, PROOF_ENGINE_ROOT)

from itertools import product

from scripts.computations import prove_holds
from scripts.proof_summary import ProofSummaryBuilder


# 1. CLAIM INTERPRETATION (Rule 4)
CLAIM_NATURAL = (
    "Let G be a finite strategic-form game. (A) If G admits a generalized "
    "ordinal potential P, then every better-response path is finite, G has "
    "the finite improvement property, and G admits a pure Nash equilibrium. "
    "(B) If G admits an exact potential P, then every global maximizer of P "
    "is a pure Nash equilibrium."
)
CLAIM_FORMAL = {
    "subject": (
        "finite strategic-form games admitting either a generalized ordinal "
        "potential (Part A) or an exact potential (Part B)"
    ),
    "property": (
        "Part A: every better-response path is finite, the finite improvement "
        "property holds, and a pure Nash equilibrium exists. "
        "Part B: every global maximizer of the exact potential is a pure NE."
    ),
    "operator": "holds",
    "claim_type": "theorem",
    "purpose": "methodology_demonstration",
    "attribution": (
        "Monderer & Shapley (1996), \"Potential Games,\" "
        "Games and Economic Behavior 14(1), 124–143"
    ),
    "operator_note": (
        "Universally quantified over the (unbounded) class of finite "
        "strategic-form games, separately under each potential hypothesis. "
        "Both parts are established by the deductive argument of Monderer & "
        "Shapley (1996) — Theorem (A) follows their Lemmas 2.3 + 2.5 (strict "
        "monotonicity of the GOP along better-response edges + finiteness of "
        "the strategy space → no profile can repeat → paths terminate at a "
        "pure NE), and Theorem (B) is their Lemma 4.2 specialized (at a "
        "global maximizer of the exact potential, by exactness no deviation "
        "can strictly increase any player's payoff). The deductive argument "
        "in proof.md's `## Proof (after Monderer & Shapley, 1996)` section "
        "is a re-exposition of their published proof; this artifact is a "
        "verifiable companion, not a substitute for citing the primary "
        "source. The computations are implementation regression checks; "
        "they cannot establish a 'for all' claim and we do not claim they do."
    ),
}

# 2. FACT REGISTRY — A-types only; all are regression, not primary evidence.
FACT_REGISTRY = {
    "A1": {"label": "GOP-detector regression spot-check (Part A)",
           "method": None, "result": None},
    "A2": {"label": "FIP / pure-NE termination regression (Part A)",
           "method": None, "result": None},
    "A3": {"label": "Exact-potential maximizer is pure NE regression (Part B)",
           "method": None, "result": None},
}


# 3. CONSTRUCTIVE EXAMPLES AND REGRESSION DETECTORS
#
# Strategy profiles are tuples; payoffs and potentials are dict-of-tuple maps.
# A "better-response" edge from s to s' exists when exactly one player i
# changes strategy and u_i(s') > u_i(s); a profile is a pure NE iff it has
# no outgoing better-response edge.

def enumerate_profiles(shape):
    """Enumerate every pure strategy profile for a game with the given shape.
    `shape[i]` is the size of player i's strategy set; profiles are tuples
    of length len(shape) with entry i in range(shape[i])."""
    return list(product(*[range(n) for n in shape]))


def unilateral_neighbors(profile, shape):
    """Yield (i, profile') pairs reachable from `profile` by a unilateral
    deviation of player i to a different strategy."""
    for i, n_i in enumerate(shape):
        for a in range(n_i):
            if a == profile[i]:
                continue
            yield i, profile[:i] + (a,) + profile[i + 1:]


def is_pure_ne(profile, shape, payoffs):
    """True iff no player has a strictly profitable unilateral deviation."""
    base = payoffs[profile]
    for i, neighbor in unilateral_neighbors(profile, shape):
        if payoffs[neighbor][i] > base[i]:
            return False
    return True


def has_generalized_ordinal_potential(shape, payoffs, P):
    """True iff P is a generalized ordinal potential: for every unilateral
    deviation by player i, sign of player i's payoff change is matched by
    the sign of P's change. The standard GOP condition is one-directional —
    u_i(s') > u_i(s) implies P(s') > P(s) — which is what we check."""
    for s in enumerate_profiles(shape):
        for i, s_prime in unilateral_neighbors(s, shape):
            if payoffs[s_prime][i] > payoffs[s][i] and not P[s_prime] > P[s]:
                return False
    return True


def has_exact_potential(shape, payoffs, P):
    """True iff P is an exact potential: for every unilateral deviation by
    player i, u_i(s') - u_i(s) equals P(s') - P(s)."""
    for s in enumerate_profiles(shape):
        for i, s_prime in unilateral_neighbors(s, shape):
            if (payoffs[s_prime][i] - payoffs[s][i]) != (P[s_prime] - P[s]):
                return False
    return True


def better_response_paths_terminate(shape, payoffs, max_steps=None):
    """Run a finite simulation of the better-response dynamic from every
    starting profile, picking the lexicographically first improving deviation
    at each step. Returns True iff every run reaches a pure NE within
    max_steps. With a strict generalized ordinal potential present, no
    profile can repeat along a better-response path, so the dynamic must
    terminate in at most |S| - 1 steps where |S| is the number of profiles."""
    profiles = enumerate_profiles(shape)
    if max_steps is None:
        max_steps = len(profiles)
    for start in profiles:
        s = start
        for _ in range(max_steps + 1):
            found = None
            for i, s_prime in unilateral_neighbors(s, shape):
                if payoffs[s_prime][i] > payoffs[s][i]:
                    found = s_prime
                    break
            if found is None:
                break  # reached a pure NE
            s = found
        else:
            return False  # ran out of steps without reaching a NE
    return True


def two_player_coordination_game():
    """Minimal exact-potential example: 2x2 coordination game with payoffs
    u_i(C, C) = 2, u_i(D, D) = 1, u_i(C, D) = u_i(D, C) = 0. The exact
    potential P(s) equals the common payoff at s (it agrees with both
    players' payoff differences along any unilateral deviation)."""
    shape = (2, 2)
    payoffs = {
        (0, 0): (2, 2),
        (0, 1): (0, 0),
        (1, 0): (0, 0),
        (1, 1): (1, 1),
    }
    P_exact = {(0, 0): 2, (0, 1): 0, (1, 0): 0, (1, 1): 1}
    return shape, payoffs, P_exact


def two_player_gop_only_game():
    """Minimal GOP-but-not-exact 2x2 game. Player 0 receives payoff 3 at
    (0,0) and 2 at (1,1); player 1 receives 5 at (0,0) and 4 at (1,1); both
    players receive 0 off-diagonal. The asymmetric pair-payoffs at the two
    diagonal profiles make exactness fail (matching the differences along
    deviations into and out of (0,0) forces inconsistent values for the
    potential at (1,1)). The supplied P is a generalized ordinal potential:
    it strictly increases along every improving unilateral deviation, which
    is the only condition Theorem (A) requires."""
    shape = (2, 2)
    payoffs = {
        (0, 0): (3, 5),
        (0, 1): (0, 0),
        (1, 0): (0, 0),
        (1, 1): (2, 4),
    }
    P_gop = {(0, 0): 2, (0, 1): 0, (1, 0): 0, (1, 1): 1}
    return shape, payoffs, P_gop


# 4. IMPLEMENTATION REGRESSION CHECKS
#
# These spot-check the code that decides whether an instance satisfies the
# formal hypotheses. They do not establish the theorem; sampling cannot.
# Method/label text for the corresponding `add_computed_fact` calls is
# role-disclosed (Rule 10).

N_SAMPLES = 600
RANDOM_SEED = 20260428


def _global_maximizer(P):
    """Return any profile attaining max P. Ties are broken by sort order;
    Part B's claim is "every global maximizer," so the construction below
    iterates over the full argmax set rather than picking one."""
    best = max(P.values())
    return [s for s, v in P.items() if v == best]


def gop_regression_pass(shape, payoffs, P_gop):
    """Confirm the GOP-detector accepts the constructed GOP and that every
    better-response path from every starting profile terminates."""
    if not has_generalized_ordinal_potential(shape, payoffs, P_gop):
        return False
    return better_response_paths_terminate(shape, payoffs)


def exact_potential_regression_pass(shape, payoffs, P_exact):
    """Confirm the exact-potential detector accepts P_exact, and that every
    global maximizer of P_exact is a pure NE."""
    if not has_exact_potential(shape, payoffs, P_exact):
        return False
    for s in _global_maximizer(P_exact):
        if not is_pure_ne(s, shape, payoffs):
            return False
    return True


def _random_two_player_game(rng, n=2, payoff_range=10):
    """Sample a random 2-player game with strategy-set size n on each side,
    integer payoffs uniform on [-payoff_range, payoff_range]. Used only for
    regression sampling — not for theorem evidence."""
    shape = (n, n)
    payoffs = {
        s: (rng.randint(-payoff_range, payoff_range),
            rng.randint(-payoff_range, payoff_range))
        for s in enumerate_profiles(shape)
    }
    return shape, payoffs


def _identifies_gop(shape, payoffs):
    """Heuristic GOP candidate: take P(s) = sum of player payoffs at s and
    accept iff the GOP-detector confirms it. The goal is spot-checking the
    detector on games where a candidate is easy to find, not deciding
    GOP-membership in general."""
    base = {s: sum(payoffs[s]) for s in enumerate_profiles(shape)}
    if has_generalized_ordinal_potential(shape, payoffs, base):
        return base
    return None


def run_regression_samples(n_samples=N_SAMPLES, seed=RANDOM_SEED):
    """Run the implementation regression suite over `n_samples` random
    2-player games. For each sampled game, if the heuristic finds a candidate
    GOP, verify that the GOP-detector accepts it and that the better-response
    dynamic terminates. Record any disagreement. Returns the disagreement
    counts; theorems remain established by the deductive argument regardless
    of these counts."""
    rng = random.Random(seed)
    gop_disagreements = 0
    fip_disagreements = 0
    for _ in range(n_samples):
        shape, payoffs = _random_two_player_game(rng)
        P_candidate = _identifies_gop(shape, payoffs)
        if P_candidate is None:
            continue
        if not has_generalized_ordinal_potential(shape, payoffs, P_candidate):
            gop_disagreements += 1
        if not better_response_paths_terminate(shape, payoffs):
            fip_disagreements += 1
    return gop_disagreements, fip_disagreements


def exact_maximizer_regression_samples(n_samples=N_SAMPLES, seed=RANDOM_SEED + 1):
    """For each sample, build a candidate exact potential P from a random
    common-payoff game (where exactness is automatic), then verify every
    global maximizer of P is a pure NE in that game."""
    rng = random.Random(seed)
    disagreements = 0
    for _ in range(n_samples):
        shape = (2, 2)
        common = {s: rng.randint(-5, 5) for s in enumerate_profiles(shape)}
        payoffs = {s: (common[s], common[s]) for s in enumerate_profiles(shape)}
        if not has_exact_potential(shape, payoffs, common):
            disagreements += 1
            continue
        for s in _global_maximizer(common):
            if not is_pure_ne(s, shape, payoffs):
                disagreements += 1
                break
    return disagreements


# 5. ADVERSARIAL CHECKS (Rule 5)
adversarial_checks = [
    {
        "question": (
            "Does the deductive argument silently rely on finiteness in a way "
            "the statement does not make explicit?"
        ),
        "verification_performed": (
            "Re-read the argument: termination of better-response paths uses "
            "strict monotonicity of P along edges plus finiteness of the "
            "profile set (no profile repeats; the strategy space is finite, "
            "hence paths cannot extend beyond |S| profiles). Both finiteness "
            "assumptions are explicit hypotheses of the theorem."
        ),
        "finding": (
            "The reliance on finiteness is explicit: 'finite strategic-form "
            "game' is the first hypothesis. The argument fails for infinite "
            "strategy spaces (Part A's termination would require an "
            "additional well-ordering or compactness assumption)."
        ),
        "breaks_proof": False,
    },
    {
        "question": (
            "Is 'better-response path is finite' equivalent to FIP, or is "
            "there an asymmetry the proof glosses over?"
        ),
        "verification_performed": (
            "Cross-check the standard definition: FIP = every better-response "
            "improvement path terminates after finitely many steps. The two "
            "phrasings are textbook-equivalent in Monderer & Shapley (1996); "
            "we keep both in the theorem statement to mirror the natural-"
            "language claim."
        ),
        "finding": (
            "No asymmetry. 'Every better-response path is finite' and 'FIP' "
            "name the same property; including both in the conclusion is a "
            "redundancy of phrasing, not of substance."
        ),
        "breaks_proof": False,
    },
    {
        "question": (
            "Could a global maximizer of an exact potential fail to be a "
            "pure NE because of a tie-breaking subtlety?"
        ),
        "verification_performed": (
            "Re-verified the Part B argument: at a global maximizer s*, no "
            "neighbor s' satisfies P(s') > P(s*) by definition of maximizer; "
            "exactness gives u_i(s') - u_i(s*) = P(s') - P(s*) ≤ 0 for every "
            "deviation by player i. The argument uses ≥/≤ at the max; it "
            "does not require uniqueness of the maximizer."
        ),
        "finding": (
            "No subtlety. Multiple global maximizers all qualify as pure "
            "NE, including ties — the regression sweep over the full "
            "argmax set confirms this for each sampled game."
        ),
        "breaks_proof": False,
    },
    {
        "question": (
            "Does the formalization of 'generalized ordinal potential' match "
            "Monderer & Shapley's definition?"
        ),
        "verification_performed": (
            "Cross-checked our definition (sign of u_i payoff change "
            "matches sign of P change for every unilateral deviation) "
            "against Monderer & Shapley (1996), Definition 2.4. The "
            "one-directional implication used in the proof — strictly "
            "improving deviations strictly raise P — is sufficient for "
            "termination and is what we encode in the detector."
        ),
        "finding": (
            "Definitions agree. The detector implements the same condition "
            "the deductive argument uses; the regression spot-checks below "
            "are consistent with this formalization."
        ),
        "breaks_proof": False,
    },
]


# 6. VERDICT
if __name__ == "__main__":
    # Run the constructive-example regressions first; these are deterministic.
    shape_a, payoffs_a, P_gop = two_player_gop_only_game()
    shape_b, payoffs_b, P_exact = two_player_coordination_game()

    # A1 covers the GOP-only construction; A2 covers the exact-potential
    # coordination game's better-response termination — independent of A1.
    a1_pass = gop_regression_pass(shape_a, payoffs_a, P_gop)
    a2_pass = better_response_paths_terminate(shape_b, payoffs_b)
    a3_pass = exact_potential_regression_pass(shape_b, payoffs_b, P_exact)

    # Closes the Corollary 1 sketch ("exact potential is in particular a GOP")
    # by asserting the GOP-detector accepts the constructed exact potential.
    exact_implies_gop = has_generalized_ordinal_potential(shape_b, payoffs_b, P_exact)

    # Run the random-sample regression sweep.
    gop_disagree, fip_disagree = run_regression_samples()
    max_disagree = exact_maximizer_regression_samples()

    sampling_clean = (
        gop_disagree == 0 and fip_disagree == 0 and max_disagree == 0
    )
    constructive_clean = a1_pass and a2_pass and a3_pass and exact_implies_gop

    # The verdict is established by the deductive argument in proof.md.
    # Regression failure would indicate a build issue, not a counter-
    # example to the theorem; we surface it as UNDETERMINED so a human
    # can investigate before publishing.
    any_breaks = any(ac.get("breaks_proof") for ac in adversarial_checks)
    regression_ok = constructive_clean and sampling_clean
    verdict_holds = prove_holds(
        regression_ok and not any_breaks,
        label="theorem established by deductive argument",
    )
    if verdict_holds:
        verdict = "PROVED"
    else:
        verdict = "UNDETERMINED"

    builder = ProofSummaryBuilder(CLAIM_NATURAL, CLAIM_FORMAL)

    builder.add_computed_fact(
        "A1",
        label="GOP-detector regression spot-check (Part A)",
        method=(
            f"Implementation regression spot-check: {N_SAMPLES} sampled "
            f"random 2-player games plus a hand-constructed 2x2 GOP-only "
            f"game, used to spot-check that the GOP-detector agrees with "
            f"the formal definition."
        ),
        result=(a1_pass and gop_disagree == 0),
    )
    builder.add_computed_fact(
        "A2",
        label="FIP / pure-NE termination regression (Part A) and exact-implies-GOP closure (Corollary 1)",
        method=(
            f"Implementation regression sanity check: {N_SAMPLES} sampled "
            f"random 2-player games plus the constructive coordination game "
            f"(distinct from A1's GOP-only example), used to confirm "
            f"better-response paths terminate as the deductive argument "
            f"requires; also asserts the GOP-detector accepts the "
            f"constructed exact potential, closing Corollary 1's "
            f"'exact-implies-GOP' sketch."
        ),
        result=(a2_pass and fip_disagree == 0 and exact_implies_gop),
    )
    builder.add_computed_fact(
        "A3",
        label="Exact-potential maximizer is pure NE regression (Part B)",
        method=(
            f"Implementation regression spot-check: {N_SAMPLES} sampled "
            f"random common-payoff 2-player games plus a constructive "
            f"coordination game, used to spot-check that every global "
            f"maximizer of the exact potential is a pure NE."
        ),
        result=(a3_pass and max_disagree == 0),
    )

    for ac in adversarial_checks:
        builder.add_adversarial_check(
            question=ac["question"],
            verification_performed=ac["verification_performed"],
            finding=ac["finding"],
            breaks_proof=ac["breaks_proof"],
        )

    builder.set_verdict(verdict)
    builder.set_key_results(
        part_a_regression_clean=(a1_pass and a2_pass and gop_disagree == 0
                                  and fip_disagree == 0),
        part_b_regression_clean=(a3_pass and max_disagree == 0),
    )
    builder.emit()
