"""Snapshot registry, alias resolution, and canonical path helpers."""
from __future__ import annotations

import json
from dataclasses import dataclass
from pathlib import Path
from typing import Any

from libs.common.config import get_settings

_REGISTRY_PATH = Path("configs/snapshots/registry.json")
_LEGACY_SNAPSHOT_FALLBACK_ROOT = Path("data/datasets/snapshots")


class UnknownSnapshotError(ValueError):
    """Raised when a snapshot id cannot be resolved."""


@dataclass(frozen=True)
class SnapshotResolution:
    requested_snapshot_id: str
    canonical_snapshot_id: str
    refresh_policy: str | None
    purpose: str | None
    expected_feature_set: str | None
    is_alias: bool
    is_registry_managed: bool
    is_legacy_passthrough: bool


def _load_registry_payload() -> dict[str, Any]:
    if not _REGISTRY_PATH.exists():
        raise FileNotFoundError(f"Snapshot registry not found: {_REGISTRY_PATH}")
    return json.loads(_REGISTRY_PATH.read_text())


def load_snapshot_registry() -> dict[str, Any]:
    """Return the raw snapshot registry payload."""
    return _load_registry_payload()


def get_canonical_spec(canonical_snapshot_id: str) -> dict[str, Any]:
    """Return the registry spec for a managed canonical snapshot."""
    payload = _load_registry_payload()
    spec = payload.get("canonicals", {}).get(canonical_snapshot_id)
    if spec is None:
        raise UnknownSnapshotError(f"Unknown canonical snapshot id: {canonical_snapshot_id}")
    return dict(spec)


def get_requested_aliases(canonical_snapshot_id: str) -> list[str]:
    """Return all legacy aliases that resolve to *canonical_snapshot_id*."""
    payload = _load_registry_payload()
    aliases = payload.get("aliases", {})
    return sorted(
        alias
        for alias, alias_spec in aliases.items()
        if alias_spec.get("canonical_id") == canonical_snapshot_id
    )


def _snapshot_root_candidates(snapshot_dir: str | Path | None = None) -> list[Path]:
    if snapshot_dir is not None:
        return [Path(snapshot_dir)]
    settings = get_settings()
    roots = [Path(settings.parquet_dir), _LEGACY_SNAPSHOT_FALLBACK_ROOT]
    deduped: list[Path] = []
    seen: set[Path] = set()
    for root in roots:
        root = root.resolve()
        if root in seen:
            continue
        seen.add(root)
        deduped.append(root)
    return deduped


def snapshot_exists(snapshot_id: str, snapshot_dir: str | Path | None = None) -> bool:
    """Return True when the raw snapshot directory exists in any known root."""
    for root in _snapshot_root_candidates(snapshot_dir):
        if (root / snapshot_id).exists():
            return True
    return False


def resolve_snapshot(
    requested_snapshot_id: str,
    *,
    snapshot_dir: str | Path | None = None,
    allow_legacy_passthrough: bool = True,
) -> SnapshotResolution:
    """Resolve a requested snapshot id to its canonical snapshot id.

    Registry-managed aliases map to one of the two canonicals. For legacy
    directories outside the managed alias set, phase-1 keeps a passthrough
    fallback so old experiments do not break immediately.
    """
    payload = _load_registry_payload()
    canonicals = payload.get("canonicals", {})
    aliases = payload.get("aliases", {})

    if requested_snapshot_id in canonicals:
        spec = canonicals[requested_snapshot_id]
        return SnapshotResolution(
            requested_snapshot_id=requested_snapshot_id,
            canonical_snapshot_id=requested_snapshot_id,
            refresh_policy=spec.get("refresh_policy"),
            purpose=spec.get("purpose"),
            expected_feature_set=None,
            is_alias=False,
            is_registry_managed=True,
            is_legacy_passthrough=False,
        )

    if requested_snapshot_id in aliases:
        alias_spec = aliases[requested_snapshot_id]
        canonical_snapshot_id = str(alias_spec["canonical_id"])
        canonical_spec = canonicals.get(canonical_snapshot_id, {})
        return SnapshotResolution(
            requested_snapshot_id=requested_snapshot_id,
            canonical_snapshot_id=canonical_snapshot_id,
            refresh_policy=canonical_spec.get("refresh_policy"),
            purpose=alias_spec.get("purpose") or canonical_spec.get("purpose"),
            expected_feature_set=alias_spec.get("expected_feature_set"),
            is_alias=True,
            is_registry_managed=True,
            is_legacy_passthrough=False,
        )

    if allow_legacy_passthrough and snapshot_exists(requested_snapshot_id, snapshot_dir=snapshot_dir):
        return SnapshotResolution(
            requested_snapshot_id=requested_snapshot_id,
            canonical_snapshot_id=requested_snapshot_id,
            refresh_policy="legacy_passthrough",
            purpose="legacy_passthrough",
            expected_feature_set=None,
            is_alias=False,
            is_registry_managed=False,
            is_legacy_passthrough=True,
        )

    raise UnknownSnapshotError(f"Unknown snapshot id: {requested_snapshot_id}")


def resolve_snapshot_path(
    requested_snapshot_id: str,
    *,
    snapshot_dir: str | Path | None = None,
    allow_legacy_passthrough: bool = True,
) -> Path | None:
    """Resolve the on-disk snapshot directory.

    Prefer the canonical directory. If it does not exist yet, fall back to the
    requested legacy alias directory so phase-1 remains parity-safe until the
    canonical snapshot has been rebuilt.
    """
    resolution = resolve_snapshot(
        requested_snapshot_id,
        snapshot_dir=snapshot_dir,
        allow_legacy_passthrough=allow_legacy_passthrough,
    )
    candidate_ids = [resolution.canonical_snapshot_id]
    if resolution.requested_snapshot_id != resolution.canonical_snapshot_id:
        candidate_ids.append(resolution.requested_snapshot_id)

    seen: set[Path] = set()
    for root in _snapshot_root_candidates(snapshot_dir):
        for snapshot_id in candidate_ids:
            candidate = (root / snapshot_id).resolve()
            if candidate in seen:
                continue
            seen.add(candidate)
            if candidate.exists():
                return candidate
    return None


def canonical_snapshot_dir(canonical_snapshot_id: str) -> Path:
    """Return the managed canonical directory under data/parquet."""
    settings = get_settings()
    return Path(settings.parquet_dir).resolve() / canonical_snapshot_id

