"""Versioned, replayable Lost Cities match records."""

from __future__ import annotations

import json
from collections import Counter
from collections.abc import Iterable, Mapping, Sequence
from dataclasses import dataclass
from pathlib import Path
from typing import Any

from .game import GameState

FORMAT_NAME = "coolrl.lost-cities.match"
FORMAT_VERSION = 1
FORMAT_ID = f"{FORMAT_NAME}.v{FORMAT_VERSION}"


def encode_public_hands(
    counts: Sequence[Mapping[tuple[int, int], int]],
) -> list[list[dict[str, int]]]:
    return [
        [
            {"color": color, "value": value, "count": count}
            for (color, value), count in sorted(player.items())
            if count > 0
        ]
        for player in counts
    ]


def decode_public_hands(
    payload: Sequence[Sequence[Mapping[str, Any]]],
) -> list[Counter[tuple[int, int]]]:
    if len(payload) != 2:
        raise ValueError("public_hands must contain exactly two players")
    result: list[Counter[tuple[int, int]]] = []
    for cards in payload:
        counter: Counter[tuple[int, int]] = Counter()
        for card in cards:
            color = int(card["color"])
            value = int(card["value"])
            count = int(card["count"])
            if color < 0 or value < 0 or count <= 0:
                raise ValueError("invalid public hand entry")
            counter[(color, value)] += count
        result.append(counter)
    return result


def make_step(
    *,
    index: int,
    state: GameState,
    public_hands: Sequence[Mapping[tuple[int, int], int]],
    actor: int | None,
    phase_before: str | None,
    action_id: int | None,
) -> dict[str, Any]:
    return {
        "type": "step",
        "index": index,
        "actor": actor,
        "phase_before": phase_before,
        "action_id": action_id,
        "state": state.to_snapshot(),
        "public_hands": encode_public_hands(public_hands),
    }


@dataclass
class MatchRecord:
    metadata: dict[str, Any]
    steps: list[dict[str, Any]]

    def validate(self) -> None:
        if self.metadata.get("type") != "metadata":
            raise ValueError("first record must be metadata")
        if self.metadata.get("format") != FORMAT_ID:
            raise ValueError(f"unsupported match format: {self.metadata.get('format')!r}")
        if not self.steps:
            raise ValueError("match record must contain at least the initial step")
        for expected, step in enumerate(self.steps):
            if step.get("type") != "step" or step.get("index") != expected:
                raise ValueError("steps must be contiguous and zero-indexed")
            state = step.get("state")
            if not isinstance(state, dict):
                raise ValueError(f"step {expected} has no state snapshot")
            GameState.from_snapshot(state)
            decode_public_hands(step.get("public_hands", []))
            if expected == 0 and any(
                step.get(key) is not None for key in ("actor", "phase_before", "action_id")
            ):
                raise ValueError("initial step cannot contain an action")
            if expected > 0:
                previous = GameState.from_snapshot(self.steps[expected - 1]["state"])
                if step.get("actor") != previous.current_player:
                    raise ValueError(f"step {expected} actor does not match previous state")
                if step.get("phase_before") != previous.phase:
                    raise ValueError(f"step {expected} phase does not match previous state")
                try:
                    previous.apply_unified_action(int(step["action_id"]))
                except (KeyError, TypeError, ValueError) as exc:
                    raise ValueError(f"step {expected} has an invalid action") from exc
                if previous.to_snapshot() != state:
                    raise ValueError(f"step {expected} state does not follow its action")

    def write_jsonl(self, path: str | Path) -> Path:
        self.validate()
        destination = Path(path)
        destination.parent.mkdir(parents=True, exist_ok=True)
        with destination.open("w", encoding="utf-8") as handle:
            for row in (self.metadata, *self.steps):
                handle.write(json.dumps(row, ensure_ascii=False, separators=(",", ":")) + "\n")
        return destination

    @classmethod
    def read_jsonl(cls, path: str | Path) -> MatchRecord:
        with Path(path).open(encoding="utf-8") as handle:
            rows = [json.loads(line) for line in handle if line.strip()]
        if not rows:
            raise ValueError("empty match record")
        record = cls(metadata=rows[0], steps=rows[1:])
        record.validate()
        return record


def record_from_rows(rows: Iterable[dict[str, Any]]) -> MatchRecord:
    materialized = list(rows)
    if not materialized:
        raise ValueError("empty match record")
    record = MatchRecord(materialized[0], materialized[1:])
    record.validate()
    return record


__all__ = [
    "FORMAT_ID",
    "FORMAT_NAME",
    "FORMAT_VERSION",
    "MatchRecord",
    "decode_public_hands",
    "encode_public_hands",
    "make_step",
    "record_from_rows",
]
