"""C6 storage composition-root factories (AZ-303).

Three factories selected by ``config.components['c6_tile_cache']``:

- :func:`build_tile_store` — JPEG body store.
- :func:`build_tile_metadata_store` — Postgres metadata + LRU + voting.
- :func:`build_descriptor_index` — FAISS HNSW vector index.

The factories lazily import concrete impl modules. A ``BUILD_*`` flag
that is OFF MUST NOT cause the concrete module to be imported — that
is the AC-5 / Risk-2 invariant. We honour it by reading the flag
from :mod:`os.environ` BEFORE the ``import`` statement.

The concrete impls (``postgres_filesystem_store``, ``faiss_descriptor_index``)
are produced by separate downstream tasks (AZ-305 / AZ-306). Until they
land, requesting any runtime raises :class:`RuntimeNotAvailableError`
with a message that names the missing impl module — the failure is
explicit, not silent.
"""

from __future__ import annotations

import os
from typing import TYPE_CHECKING

from gps_denied_onboard.runtime_root.errors import RuntimeNotAvailableError

if TYPE_CHECKING:
    from gps_denied_onboard.components.c6_tile_cache import (
        C6TileCacheConfig,
        DescriptorIndex,
        TileMetadataStore,
        TileStore,
    )
    from gps_denied_onboard.config.schema import Config

__all__ = [
    "build_descriptor_index",
    "build_tile_metadata_store",
    "build_tile_store",
]


def _is_build_flag_on(flag_name: str) -> bool:
    """Read a compile-time ``BUILD_*`` flag from the environment.

    ``ON`` / ``1`` / ``true`` / ``yes`` (case-insensitive) → ``True``.
    Anything else (including unset) → ``False``. Defaults to OFF to
    keep test envs honest about which runtimes they advertise.
    """
    raw = os.environ.get(flag_name, "")
    return raw.strip().lower() in {"on", "1", "true", "yes"}


def _c6_config(config: "Config") -> "C6TileCacheConfig":
    """Pull the registered C6 config block.

    ``c6_tile_cache.__init__`` registers it on import; if the package
    has not been imported, :class:`KeyError` here is the right
    failure mode — silent fallback would mask a missing import.
    """
    return config.components["c6_tile_cache"]


def build_tile_store(config: "Config") -> "TileStore":
    """Construct the :class:`TileStore` impl selected by config.

    Today only ``"postgres_filesystem"`` is wired; the runtime label
    is validated at config-load time so unknown labels never reach
    here. Concrete impl is produced by AZ-305.
    """
    block = _c6_config(config)
    runtime = block.store_runtime
    if runtime == "postgres_filesystem":
        try:
            from gps_denied_onboard.components.c6_tile_cache.postgres_filesystem_store import (  # noqa: PLC0415
                PostgresFilesystemStore,
            )
        except ModuleNotFoundError as exc:
            raise RuntimeNotAvailableError(
                f"TileStore runtime {runtime!r} is configured but its "
                "concrete impl module "
                "'c6_tile_cache.postgres_filesystem_store' has not been "
                "built into this binary yet (AZ-305 pending)."
            ) from exc
        return PostgresFilesystemStore(config)
    raise RuntimeNotAvailableError(
        f"TileStore runtime {runtime!r} is not buildable in this binary."
    )


def build_tile_metadata_store(config: "Config") -> "TileMetadataStore":
    """Construct the :class:`TileMetadataStore` impl selected by config.

    Today the same ``PostgresFilesystemStore`` class implements both
    :class:`TileStore` and :class:`TileMetadataStore` (single-row
    transactional store across the two surfaces). The factories
    return the same instance shape but stay separate so a future
    SQLite Tier-0 variant can swap one without the other.
    """
    block = _c6_config(config)
    runtime = block.metadata_runtime
    if runtime == "postgres_filesystem":
        try:
            from gps_denied_onboard.components.c6_tile_cache.postgres_filesystem_store import (  # noqa: PLC0415
                PostgresFilesystemStore,
            )
        except ModuleNotFoundError as exc:
            raise RuntimeNotAvailableError(
                f"TileMetadataStore runtime {runtime!r} is configured "
                "but its concrete impl module "
                "'c6_tile_cache.postgres_filesystem_store' has not been "
                "built into this binary yet (AZ-305 pending)."
            ) from exc
        return PostgresFilesystemStore(config)
    raise RuntimeNotAvailableError(
        f"TileMetadataStore runtime {runtime!r} is not buildable in this binary."
    )


def build_descriptor_index(config: "Config") -> "DescriptorIndex":
    """Construct the :class:`DescriptorIndex` impl selected by config.

    Gated by ``BUILD_FAISS_INDEX``: if the flag is OFF, the concrete
    module is NOT imported (sys.modules invariant; AC-5) and
    :class:`RuntimeNotAvailableError` is raised at composition time.
    """
    block = _c6_config(config)
    runtime = block.descriptor_index_runtime
    if runtime == "faiss_hnsw":
        if not _is_build_flag_on("BUILD_FAISS_INDEX"):
            raise RuntimeNotAvailableError(
                f"DescriptorIndex runtime {runtime!r} requires "
                "BUILD_FAISS_INDEX=ON in this binary; the flag is OFF."
            )
        try:
            from gps_denied_onboard.components.c6_tile_cache.faiss_descriptor_index import (  # noqa: PLC0415
                FaissDescriptorIndex,
            )
        except ModuleNotFoundError as exc:
            raise RuntimeNotAvailableError(
                f"DescriptorIndex runtime {runtime!r} is configured but "
                "its concrete impl module "
                "'c6_tile_cache.faiss_descriptor_index' has not been "
                "built into this binary yet (AZ-306 pending)."
            ) from exc
        return FaissDescriptorIndex(config)
    raise RuntimeNotAvailableError(
        f"DescriptorIndex runtime {runtime!r} is not buildable in this binary."
    )
