"""Composition-manifest schema — the board's ordered-pointer composition PRIMITIVE (REC-235).

A ``CompositionManifest`` is the board's data SHAPE: an ordered list of atom-version URN
members + a layout hint + a ``kind``. The grid/strip PNG is a DERIVED, on-demand VIEW
(rendered by ``board_builder.render_composition_view`` — a downstream CONSUMER, not part of
this module). This module owns ONLY the data shape — never rendering, generation, or I/O
beyond serialization. A reorder is a NEW manifest with ``members`` permuted.

Kept dependency-free exactly like ``atom.py``: this schema does NOT import
``recoil.pipeline.core.take`` (the URN parser) NOR ``board_builder`` (the layout
vocabulary). Member URNs are shape-checked here against a LOCAL module-level regex — a copy
of the atom-version URN grammar Phase 0 pinned, NOT an import. Full parse-resolution and
slot-vs-``GRID_LAYOUTS`` validation happen DOWNSTREAM, where the renderer/readmodel consume
the manifest (``workspace.readmodel`` is the layer that imports both schema + pipeline
parser — not the schema importing up into pipeline).
"""

from __future__ import annotations

import re
from typing import Any, Literal

from pydantic import Field, field_validator

from recoil.api.schemas._base import _Versioned


# The 6 board kinds (SYNTHESIS Decision 2). An out-of-set kind raises at validation.
CompositionKind = Literal["CONT", "ONER", "COVERAGE", "BROLL", "PICKUP", "CUT"]

# Local copy of the atom-version URN grammar (``atom:{episode}/beat/{beat_id}@tN``) that
# Phase 0 pinned in ``take.py::_ATOM_VERSION_URN_RE`` — copied, NOT imported, to keep this
# API schema dependency-free (mirrors ``atom.py``). The renderer/readmodel that CONSUME a
# member run the full ``parse_atom_version_urn`` resolution downstream.
_ATOM_VERSION_URN_RE = re.compile(r"^atom:[A-Za-z0-9_]+/beat/[A-Za-z0-9_]+@t\d+$")


class CompositionManifest(_Versioned):
    """The board's composition primitive: ordered atom-version pointers + layout + kind.

    ``members`` is an ordered list of atom-version URNs (order is editorial — a reorder is a
    new manifest with ``members`` permuted, same atoms in a new order). ``layout`` is a
    ``GRID_LAYOUTS``-compatible hint (e.g. ``{"slots": 4}``) validated here ONLY as a dict —
    slot-vs-``GRID_LAYOUTS`` validation is the renderer's job (avoids a board_builder import).
    Pure data: no rendering, no generation, no I/O beyond serialization.
    """

    kind: CompositionKind
    members: list[str] = Field(default_factory=list)
    layout: dict[str, Any] = Field(default_factory=dict)

    @field_validator("members")
    @classmethod
    def _members_must_be_atom_version_urns(cls, members: list[str]) -> list[str]:
        for urn in members:
            if not isinstance(urn, str) or not _ATOM_VERSION_URN_RE.match(urn):
                raise ValueError(
                    "composition member must be an atom-version URN "
                    f"'atom:{{episode}}/beat/{{beat_id}}@t{{N}}', got {urn!r}"
                )
        return members
