"""Batch selector parsing + grouping cross-check for the reroll surfaces (REC-111).

Shared by the ``--batch`` CLI path (``cli/generate.py``) and ``POST /reroll``
(``api/routes/reroll.py``) so both map an ``EP{ep:03d}_{CONT|ONER}_{ordinal:03d}``
selector to the SAME persisted scene file and apply the SAME grouping-metadata
cross-check before any spend.

Selector → persisted scene id (matches ``grouping.py``):
  - ``CONT`` → continuity grouping; persisted scene id is ``BATCH_{N:03d}``
    (``cluster_shots_into_batches`` names the batch, even though the continuity
    grouping identity in filenames/provenance is ``CONT_{N:03d}``).
  - ``ONER`` → oner grouping; persisted scene id is ``ONER_{N:03d}``.
"""
from __future__ import annotations

import re
from dataclasses import dataclass
from pathlib import Path

from recoil.core.paths import ProjectPaths


class SelectorResolutionError(ValueError):
    """Raised when a selector cannot be resolved in the selector domain."""

# {ep:03d}/{ordinal:03d} are minimum-3-digit zero-padded, so accept 3+ digits.
_BATCH_SELECTOR_RE = re.compile(r"^EP(\d{3})_(CONT|ONER)_(\d{3})$")

# selector token → (grouping strategy, persisted scene-id prefix)
_SELECTOR_STRATEGY = {"CONT": "continuity", "ONER": "oner"}
_SELECTOR_SCENE_PREFIX = {"CONT": "BATCH", "ONER": "ONER"}


@dataclass(frozen=True)
class BatchSelector:
    episode: int
    strategy: str       # "continuity" | "oner"
    ordinal: int
    scene_id: str       # "BATCH_004" | "ONER_002"


@dataclass(frozen=True)
class BeatRef:
    project: str
    episode: int            # 1
    strategy: str           # "continuity" | "oner"
    ordinal: int            # 1
    scene_id: str           # "BATCH_001"  (persisted scene-id, from BatchSelector)
    selector: str           # "EP001_CONT_001"  (canonical user-facing)
    scene_path: Path        # canonical ep_{NNN}_* orchestration scene file


def parse_batch_selector(batch_id: str) -> BatchSelector | None:
    """Parse a batch selector; return None when it does not match the locked format."""
    match = _BATCH_SELECTOR_RE.match(batch_id or "")
    if not match:
        return None
    token = match.group(2)
    ordinal = int(match.group(3))
    return BatchSelector(
        episode=int(match.group(1)),
        strategy=_SELECTOR_STRATEGY[token],
        ordinal=ordinal,
        scene_id=f"{_SELECTOR_SCENE_PREFIX[token]}_{ordinal:03d}",
    )


def resolve(any_id: str, project: str) -> BeatRef:
    """Resolve a canonical batch selector to the persisted orchestration scene."""
    selector = parse_batch_selector(any_id)
    if selector is None:
        raise SelectorResolutionError(f"unresolvable selector: {any_id!r}")

    project = project.lower()
    scenes_dir = ProjectPaths.for_project(project).orchestration_scenes_dir
    expected_name = f"ep_{selector.episode:03d}_{selector.scene_id}.json"
    expected_path = scenes_dir / expected_name
    matches = [
        path
        for path in scenes_dir.glob(expected_name)
        if not path.name.endswith(".bak") and "__ab_runA_" not in path.name
    ]
    if not matches:
        raise FileNotFoundError(f"no canonical scene for {any_id!r} at {expected_path}")

    token_by_strategy = {strategy: token for token, strategy in _SELECTOR_STRATEGY.items()}
    token = token_by_strategy[selector.strategy]
    canonical_selector = f"EP{selector.episode:03d}_{token}_{selector.ordinal:03d}"
    return BeatRef(
        project=project,
        episode=selector.episode,
        strategy=selector.strategy,
        ordinal=selector.ordinal,
        scene_id=selector.scene_id,
        selector=canonical_selector,
        scene_path=matches[0],
    )


def verify_scene_grouping_metadata(scene, selector: BatchSelector, beat) -> None:
    """Cross-check persisted grouping metadata against the selector before dispatch.

    Both the scene-level grouping and the target beat's grouping must carry the
    selector's strategy + ordinal. On any mismatch raise
    ``RerollPreflightError("batch_selector_metadata_mismatch", ...)``.
    """
    scene_grouping = (scene.scene_metadata or {}).get("grouping")
    beat_grouping = (beat.beat_metadata or {}).get("grouping")
    if not _grouping_matches(scene_grouping, selector) or not _grouping_matches(
        beat_grouping, selector
    ):
        from recoil.pipeline.orchestrator.episode_runner import RerollPreflightError

        raise RerollPreflightError(
            "batch_selector_metadata_mismatch",
            "--batch selector does not match persisted scene grouping metadata.",
        )


def _grouping_matches(grouping, selector: BatchSelector) -> bool:
    if not isinstance(grouping, dict):
        return False
    if grouping.get("strategy") != selector.strategy:
        return False
    try:
        return int(grouping.get("ordinal")) == selector.ordinal
    except (TypeError, ValueError):
        return False
