"""C2 VPR DTOs (L1 cross-component layer; AZ-336).

The trio (:class:`VprQuery`, :class:`VprCandidate`, :class:`VprResult`)
is frozen by ``contracts/c2_vpr/vpr_strategy_protocol.md`` v1.0.0:
slotted, immutable, no defaults, and stamped with the producer's
``monotonic_ns`` so the C13 FDR record can correlate the embed→retrieve
hop without a wall-clock dependency.

C2.5 rerank DTOs live in :mod:`gps_denied_onboard._types.rerank` (AZ-342);
this module no longer re-exports them.
"""

from __future__ import annotations

from dataclasses import dataclass

__all__ = [
    "VprCandidate",
    "VprQuery",
    "VprResult",
]


@dataclass(frozen=True, slots=True)
class VprQuery:
    """Backbone embedding for one nav-camera frame.

    Produced by :meth:`VprStrategy.embed_query`; consumed by
    :meth:`VprStrategy.retrieve_topk` on the same instance (same
    ingest thread per INV-1) or — in the offline C10 corpus-build
    path — by the descriptor index builder.

    ``frame_id`` echoes :attr:`NavCameraFrame.frame_id` (int); the
    contract documented ``UUID`` in draft but every other in-pipeline
    DTO routed from a single frame carries the source ``int`` so
    consumers can correlate without an extra mapping step.
    ``embedding`` is L2-normalised by the strategy before return
    (INV-3); ``produced_at`` is ``monotonic_ns`` from an injected
    :class:`gps_denied_onboard.clock.Clock`.
    """

    frame_id: int
    embedding: object  # numpy.ndarray, shape (D,), dtype float16|float32
    produced_at: int


@dataclass(frozen=True, slots=True)
class VprCandidate:
    """One retrieval candidate from a top-K result.

    ``tile_id`` is the composite ``(zoom_level, lat, lon)`` tuple
    matching :class:`gps_denied_onboard.components.c6_tile_cache.TileId`;
    the tuple form keeps this L1 DTO free of an L3 component import
    (the contract spells it ``tuple`` per the architecture layering
    rule). Consumers reconstruct ``TileId`` at the C6 boundary.

    ``descriptor_distance`` is the backbone-specific metric (cosine
    on L2-normalised embeddings, Euclidean on raw); the strategy
    sorts the parent ``VprResult.candidates`` tuple ascending by
    this field (INV-4). ``descriptor_dim`` is echoed from the
    strategy so a downstream FDR audit can verify the strategy↔index
    dim match after the flight.
    """

    tile_id: tuple[int, float, float]
    descriptor_distance: float
    descriptor_dim: int


@dataclass(frozen=True, slots=True)
class VprResult:
    """Top-K candidates from :meth:`VprStrategy.retrieve_topk`.

    Consumed by C2.5 ``RerankStrategy``. ``candidates`` is a tuple
    (not a list) so the frozen+slotted invariant holds: a frozen
    dataclass can still receive a mutable list and let consumers
    mutate it; the tuple closes that door.

    ``backbone_label`` is the strategy's lowercase
    ``BUILD_VPR_<variant>`` form (INV-5) — non-empty for every
    successful retrieval; consumed by C13 FDR for provenance.
    """

    frame_id: int
    candidates: tuple[VprCandidate, ...]
    retrieved_at: int
    backbone_label: str
