"""C6 tile cache DTOs + enums (AZ-303).

This module is the single source of truth for the data shapes the three
``c6_tile_cache`` Protocols (``TileStore``, ``TileMetadataStore``,
``DescriptorIndex``) exchange with consumers. Every DTO is
``@dataclass(frozen=True)`` per PEP 557; every enum is ``str, Enum``
so JSON / Postgres / FDR serialisation is trivial.

The contract files at ``_docs/02_document/contracts/c6_tile_cache/``
are the authoritative human-readable shape — this module mirrors them
1:1.
"""

from __future__ import annotations

from dataclasses import dataclass
from datetime import datetime
from enum import Enum
from pathlib import Path

__all__ = [
    "Bbox",
    "FreshnessLabel",
    "HnswParams",
    "IndexMetadata",
    "SectorBoundary",
    "SectorClassification",
    "TileId",
    "TileMetadata",
    "TileMetadataPersistent",
    "TileQualityMetadata",
    "TileSource",
    "VotingStatus",
]


_MAX_ZOOM_LEVEL = 21  # satellite-provider legal range upper bound


class TileSource(str, Enum):
    """Where a tile originated.

    ``GOOGLEMAPS``: pre-flight download via ``satellite-provider``.
    ``ONBOARD_INGEST``: produced mid-flight by C5 orthorectification
    (F4 path) and uploaded post-landing by C11.
    """

    GOOGLEMAPS = "googlemaps"
    ONBOARD_INGEST = "onboard_ingest"


class FreshnessLabel(str, Enum):
    """C6 freshness gate output.

    See ``tile_metadata_store.md`` Invariants I-2 / I-3 for the
    transitions; this enum only exposes the four legal states.
    """

    FRESH = "fresh"
    STALE_ACTIVE_CONFLICT = "stale_active_conflict"
    STALE_REAR = "stale_rear"
    DOWNGRADED = "downgraded"


class VotingStatus(str, Enum):
    """C6 voting state for an onboard-ingested tile.

    Forward-only transitions per Invariant I-8 of
    ``tile_metadata_store.md``: ``PENDING → TRUSTED``,
    ``PENDING → REJECTED``, ``TRUSTED → REJECTED``. The
    impl (NOT this task) enforces the transition table.
    """

    PENDING = "pending"
    TRUSTED = "trusted"
    REJECTED = "rejected"


class SectorClassification(str, Enum):
    """Operator-set classification of a geographic sector.

    Drives the C6 freshness gate. Set pre-flight by C12 against the
    ``sector_boundaries`` table; the metadata store reads sector rows
    at insert-time only.
    """

    ACTIVE_CONFLICT = "active_conflict"
    STABLE_REAR = "stable_rear"


@dataclass(frozen=True)
class TileId:
    """Spatial identity of a Web-Mercator tile.

    ``(zoom_level, lat, lon)`` is the composite identity. ``(x, y)``
    integer tile coordinates are derived by the same function
    ``satellite-provider`` uses (Invariant I-1 of ``tile_store.md``);
    this DTO carries the WGS84 form because it is the consumer-facing
    shape.
    """

    zoom_level: int
    lat: float
    lon: float

    def __post_init__(self) -> None:
        if not 0 <= self.zoom_level <= _MAX_ZOOM_LEVEL:
            raise ValueError(
                f"TileId.zoom_level must be in [0, {_MAX_ZOOM_LEVEL}]; "
                f"got {self.zoom_level}"
            )
        if not -90.0 <= self.lat <= 90.0:
            raise ValueError(
                f"TileId.lat must be in [-90.0, 90.0]; got {self.lat}"
            )
        if not -180.0 <= self.lon <= 180.0:
            raise ValueError(
                f"TileId.lon must be in [-180.0, 180.0]; got {self.lon}"
            )


@dataclass(frozen=True)
class Bbox:
    """Axis-aligned WGS84 bounding box, inclusive on min, exclusive on max."""

    min_lat: float
    min_lon: float
    max_lat: float
    max_lon: float

    def __post_init__(self) -> None:
        if self.min_lat >= self.max_lat:
            raise ValueError(
                f"Bbox.min_lat ({self.min_lat}) must be < max_lat "
                f"({self.max_lat})"
            )
        if self.min_lon >= self.max_lon:
            raise ValueError(
                f"Bbox.min_lon ({self.min_lon}) must be < max_lon "
                f"({self.max_lon})"
            )


@dataclass(frozen=True)
class TileQualityMetadata:
    """Quality metadata attached to an onboard-ingested tile.

    Set at write-time by C5; consumed by C6's freshness gate (insert
    path) and by C11's voting-status updater (post-landing). Field
    semantics are documented in the D-PROJ-2 ingest contract.
    """

    estimator_label: str
    covariance_2x2: tuple[tuple[float, float], tuple[float, float]]
    last_anchor_age_ms: int
    mre_px: float
    imu_bias_norm: float


@dataclass(frozen=True)
class TileMetadata:
    """Per-row tile metadata as understood by both ``TileStore`` and
    ``TileMetadataStore``.

    ``flight_id`` / ``companion_id`` / ``quality_metadata`` are set
    only for :attr:`TileSource.ONBOARD_INGEST`; for
    :attr:`TileSource.GOOGLEMAPS` they are ``None``. ``voting_status``
    defaults to :attr:`VotingStatus.PENDING` for onboard ingest and
    :attr:`VotingStatus.TRUSTED` for googlemaps (the impl applies the
    default; this DTO does not).
    """

    tile_id: TileId
    tile_size_meters: float
    tile_size_pixels: int
    capture_timestamp: datetime
    source: TileSource
    content_sha256_hex: str
    freshness_label: FreshnessLabel
    flight_id: str | None
    companion_id: str | None
    quality_metadata: TileQualityMetadata | None
    voting_status: VotingStatus


@dataclass(frozen=True)
class TileMetadataPersistent:
    """In-process view of LRU + disk-budget bookkeeping.

    Returned ONLY by ``TileMetadataStore.lru_candidates`` and
    constructed inside the impl when responding to
    ``record_lru_access`` / ``total_disk_bytes``. Consumers reading
    by ``tile_id`` / by ``bbox`` get the plain :class:`TileMetadata`.
    """

    metadata: TileMetadata
    accessed_at: datetime
    uploaded_at: datetime | None
    disk_bytes: int


@dataclass(frozen=True)
class SectorBoundary:
    """A single operator-defined sector polygon (axis-aligned bbox today)."""

    bbox: Bbox
    classification: SectorClassification


@dataclass(frozen=True)
class HnswParams:
    """HNSW build hyperparameters.

    Defaults track the FAISS-team baseline (``HNSW32`` + M=32 +
    efConstruction=200 + efSearch=64); overriding any value is a
    research-time concern not exercised in production.
    """

    m: int = 32
    ef_construction: int = 200
    ef_search: int = 64
    metric: str = "L2"


@dataclass(frozen=True)
class IndexMetadata:
    """Sidecar metadata block written next to the ``.index`` file.

    Read by ``DescriptorIndex.index_metadata()`` at runtime; produced
    by C10's pre-flight ``rebuild_from_descriptors`` together with the
    AZ-280 ``.sha256`` sidecar.
    """

    descriptor_dim: int
    n_vectors: int
    backbone_label: str
    backbone_sha256_hex: str
    built_at: datetime
    hnsw_params: HnswParams
    sidecar_sha256_hex: str
    file_path: Path
