"""Runtime-path and build-identity helpers for the packaged camera worker.

The source generator hashes Python source as part of its cache and provenance
contracts.  A PyInstaller process does not necessarily expose those modules as
ordinary ``.py`` files, so the onedir build ships a read-only source snapshot
and a build-time identity manifest.  Normal source-tree execution keeps the
historical paths and identifiers unchanged.
"""

from __future__ import annotations

import json
import os
import sys
from functools import lru_cache
from pathlib import Path
from typing import Any, Iterable


BUILD_IDENTITY_SCHEMA_VERSION = "cw-camera-worker-build-identity-v1"
BUILD_IDENTITY_FILE = "cw_camera_worker_build_identity.json"
WORKER_VERSION = "0.1.9"


def is_frozen_runtime() -> bool:
    """Return whether the worker is running from a frozen application."""

    return bool(getattr(sys, "frozen", False)) or os.environ.get(
        "CWAI_FORCE_FROZEN_IDENTITY",
        "",
    ).strip() == "1"


def frozen_bundle_root() -> Path | None:
    value = getattr(sys, "_MEIPASS", None)
    if value:
        return Path(value).resolve()
    override = os.environ.get("CWAI_FROZEN_BUNDLE_ROOT", "").strip()
    return Path(override).resolve() if override else None


def _identity_candidates() -> list[Path]:
    candidates: list[Path] = []
    override = os.environ.get("CWAI_WORKER_BUILD_IDENTITY", "").strip()
    if override:
        candidates.append(Path(override).resolve())
    bundle_root = frozen_bundle_root()
    if bundle_root is not None:
        candidates.extend(
            (
                bundle_root / "cwai_runtime" / BUILD_IDENTITY_FILE,
                bundle_root / BUILD_IDENTITY_FILE,
            )
        )
    return candidates


@lru_cache(maxsize=1)
def load_build_identity() -> dict[str, Any]:
    """Load and validate the optional frozen-build identity manifest."""

    for path in _identity_candidates():
        if not path.is_file():
            continue
        payload = json.loads(path.read_text(encoding="utf-8"))
        if not isinstance(payload, dict):
            raise ValueError("Camera worker build identity must be a JSON object.")
        if payload.get("schemaVersion") != BUILD_IDENTITY_SCHEMA_VERSION:
            raise ValueError(
                "Unsupported camera worker build identity schema: "
                + repr(payload.get("schemaVersion"))
            )
        return payload
    return {}


def frozen_identity_value(name: str) -> str | None:
    """Return one build-time identity only while running frozen."""

    if not is_frozen_runtime():
        return None
    value = load_build_identity().get(name)
    if isinstance(value, str) and value:
        return value
    return None


def source_tree_roots(
    default_repository_root: Path,
    default_camera_director_root: Path,
) -> tuple[Path, Path]:
    """Resolve the source snapshot used by cache identity calculations."""

    if is_frozen_runtime():
        bundle_root = frozen_bundle_root()
        identity = load_build_identity()
        relative = identity.get(
            "sourceRootRelative",
            "cwai_sources/repository",
        )
        if bundle_root is not None and isinstance(relative, str):
            repository_root = (bundle_root / relative).resolve()
            camera_director_root = (
                repository_root / "MachineLearning" / "CameraDirector"
            )
            if camera_director_root.is_dir():
                return repository_root, camera_director_root
    return (
        Path(default_repository_root).resolve(),
        Path(default_camera_director_root).resolve(),
    )


def source_snapshot_path(path: Path) -> Path:
    """Map a module path to its packaged source snapshot when available."""

    candidate = Path(path)
    if not is_frozen_runtime():
        return candidate
    bundle_root = frozen_bundle_root()
    identity = load_build_identity()
    relative = identity.get("sourceRootRelative", "cwai_sources/repository")
    if bundle_root is None or not isinstance(relative, str):
        return candidate
    snapshot = (
        bundle_root
        / relative
        / "MachineLearning"
        / "CameraDirector"
        / candidate.name
    )
    return snapshot if snapshot.is_file() else candidate


def frozen_code_identifier(
    paths: Iterable[Path],
) -> str | None:
    """Return the build-time aggregate code ID for the known generator set."""

    if not is_frozen_runtime():
        return None
    identity = load_build_identity()
    expected = identity.get("generationCodeFiles")
    value = identity.get("generationCodeIdentifier")
    actual = sorted(Path(path).name for path in paths)
    if (
        isinstance(expected, list)
        and all(isinstance(item, str) for item in expected)
        and actual == sorted(expected)
        and isinstance(value, str)
        and value
    ):
        return value
    return None


def clear_identity_cache_for_tests() -> None:
    """Reset process-local manifest state for isolated unit tests."""

    load_build_identity.cache_clear()
