"""Spatial pre-spend validator — REC-111 PR-3 Phase 5.

The consult's locked BLOCK/WARN sublocation rules, applied to an r2v_multi
``ShotPrimitive`` at the dispatch_payload build point — before any payload is
assembled and dispatched (i.e. before money moves).

Reuses ``prose_validator.Severity`` / ``ValidationResult`` so dispatch_payload's
existing ``_log_verify_results`` / BLOCK-raise plumbing handles the output
without a second result type.

A location WITHOUT a registry (``load_location_registry`` returned ``None``) is
*undecomposed*: nothing is constrained and ``verify_spatial`` returns ``[]``.
"""

from __future__ import annotations

from pathlib import Path
from typing import Any

from recoil.pipeline._lib.prose_validator import Severity, ValidationResult
from recoil.pipeline._lib.sublocation_registry import validate_ref_file


def _segment_sublocation(segment: Any) -> str | None:
    """Read a segment's ``sublocation`` defensively (dict OR object entry)."""
    if isinstance(segment, dict):
        value = segment.get("sublocation")
    else:
        value = getattr(segment, "sublocation", None)
    text = str(value).strip() if value is not None else ""
    return text or None


def verify_spatial(
    primitive,
    *,
    registry: dict | None,
    base_dir: "Path | None",
) -> list[ValidationResult]:
    """Validate per-segment sublocations against a location registry.

    Rules (consult-locked):
      - BLOCK ``spatial_unknown_sublocation`` — a segment names a sublocation
        not in ``registry["sublocations"]``.
      - BLOCK ``spatial_stub_ref`` — a known sublocation's establishing ref
        (resolved as ``base_dir / entry["ref"]``) fails ``validate_ref_file``.
      - WARN ``spatial_midbatch_jump`` — consecutive segments carry different
        sublocations.
      - WARN ``spatial_nonadjacent_transition`` — that pair is absent from
        ``adjacency`` (order-insensitive).

    ``registry is None`` ⇒ undecomposed location ⇒ return ``[]`` (silent).
    """
    if registry is None:
        return []

    pass_id = str(getattr(primitive, "shot_id", None) or "spatial")
    sublocations = registry.get("sublocations") or {}
    adjacency = {
        frozenset(pair)
        for pair in (registry.get("adjacency") or [])
        if isinstance(pair, (list, tuple)) and len(pair) == 2
    }

    results: list[ValidationResult] = []
    prev_subloc: str | None = None

    for segment in getattr(primitive, "timing_segments", None) or []:
        subloc = _segment_sublocation(segment)
        if subloc is None:
            prev_subloc = None
            continue

        if subloc not in sublocations:
            results.append(
                ValidationResult(
                    Severity.BLOCK,
                    pass_id,
                    "spatial_unknown_sublocation",
                    f"sublocation {subloc!r} is not in the location registry "
                    f"(known: {sorted(sublocations)})",
                )
            )
        else:
            ref = (sublocations.get(subloc) or {}).get("ref")
            if base_dir is not None:
                err = (
                    validate_ref_file(Path(base_dir) / ref)
                    if ref
                    else "registry entry has no ref"
                )
                if err:
                    results.append(
                        ValidationResult(
                            Severity.BLOCK,
                            pass_id,
                            "spatial_stub_ref",
                            f"sublocation {subloc!r} establishing ref invalid: {err}",
                        )
                    )

        if prev_subloc is not None and prev_subloc != subloc:
            results.append(
                ValidationResult(
                    Severity.WARN,
                    pass_id,
                    "spatial_midbatch_jump",
                    f"consecutive segments change sublocation "
                    f"{prev_subloc!r} -> {subloc!r} within one batch",
                )
            )
            if frozenset((prev_subloc, subloc)) not in adjacency:
                results.append(
                    ValidationResult(
                        Severity.WARN,
                        pass_id,
                        "spatial_nonadjacent_transition",
                        f"transition {prev_subloc!r} -> {subloc!r} is not in the "
                        f"location adjacency graph",
                    )
                )

        prev_subloc = subloc

    return results
