#!/usr/bin/env python3
"""Generate match states and their observations so TypeScript can be checked against JAX.

The two observation builders must agree to the bit. A mismatch does not throw --
the ONNX policy consumes a wrong vector quite happily and plays worse for reasons
nobody can see. So the port is not trusted; it is checked.

States are drawn from real random play so the fixture covers the awkward parts:
mid-round, both seats to move, past a round roll-over, with a non-zero carry.
"""

from __future__ import annotations

import json
from pathlib import Path

import jax
import numpy as np

from lost_cities_jax.match import MatchState, match_reset_from, match_step
from lost_cities_jax.match_obs import MATCH_OBS_DIM, match_observation
from lost_cities_jax.opponents import random_legal_action
from lost_cities_jax.types import N_CARDS

OUTPUT = Path(__file__).resolve().parents[1] / "web" / "src" / "game" / "match-parity-fixture.json"
N_ROUNDS = 3


def match_json(match: MatchState) -> dict:
    round_state = match.round
    return {
        "round": {
            "deckOrder": np.asarray(round_state.deck_order).astype(int).tolist(),
            "drawPtr": int(round_state.draw_ptr),
            "cardLoc": np.asarray(round_state.card_loc).astype(int).tolist(),
            "handPublic": np.asarray(round_state.hand_public).astype(bool).tolist(),
            "colTop": np.asarray(round_state.col_top).astype(int).tolist(),
            "colHandshakes": np.asarray(round_state.col_hs).astype(int).tolist(),
            "colLength": np.asarray(round_state.col_len).astype(int).tolist(),
            "piles": [
                np.asarray(round_state.pile[color, : int(round_state.pile_len[color])])
                .astype(int)
                .tolist()
                for color in range(5)
            ],
            "toMove": int(round_state.to_move),
            "stepCount": int(round_state.step_count),
            "done": bool(round_state.done),
        },
        "deckOrders": np.asarray(match.deck_orders).astype(int).tolist(),
        "coinFlips": np.asarray(match.coin_flips).astype(int).tolist(),
        "roundIdx": int(match.round_idx),
        "carry": np.asarray(match.carry).astype(int).tolist(),
        "done": bool(match.done),
    }


def main() -> None:
    rng = np.random.default_rng(20260715)
    key = jax.random.PRNGKey(7)
    rows = []

    for match_index in range(6):
        decks = np.stack([rng.permutation(N_CARDS) for _ in range(N_ROUNDS)])
        coins = rng.integers(0, 2, size=(N_ROUNDS,))
        match = match_reset_from(decks.astype(np.int8), coins.astype(np.int8))

        # Sample the opening position and then every 17th ply, which lands in all
        # three rounds and on both seats without hand-picking anything.
        ply = 0
        while not bool(match.done) and ply < 400:
            if ply % 17 == 0 or ply == 0:
                for player in (0, 1):
                    obs = np.asarray(match_observation(match, player), dtype=np.float64)
                    assert obs.shape == (MATCH_OBS_DIM,)
                    rows.append(
                        {
                            "match": match_json(match),
                            "player": player,
                            "observation": [round(float(v), 7) for v in obs],
                        }
                    )
            key, step_key = jax.random.split(key)
            action = int(random_legal_action(match.round, match.round.to_move, step_key))
            match, _, _ = match_step(match, action)
            ply += 1
        del match_index

    OUTPUT.write_text(
        json.dumps({"format": "jax-web-match-parity-v1", "obsDim": MATCH_OBS_DIM, "rows": rows})
        + "\n"
    )
    print(f"wrote {len(rows)} rows -> {OUTPUT}")


if __name__ == "__main__":
    main()
