"""C2.5 ReRank strategy composition-root factory (AZ-342).

:func:`build_rerank_strategy` selects exactly one strategy by
``config.components['c2_5_rerank'].strategy`` and respects
compile-time ``BUILD_RERANK_<variant>`` gating: requesting a
strategy whose flag is OFF raises
:class:`StrategyNotAvailableError` at composition time (NOT at
first frame).

The shared :class:`LightGlueRuntime` is constructor-injected — the
factory does NOT own its lifecycle. The runtime root constructs ONE
``LightGlueRuntime`` instance and passes the same reference to both
this factory (C2.5) and the future C3 matcher factory (R14 fix; see
``description.md`` § 6).

Concrete strategy modules are imported lazily — a Tier-0 workstation
build with ``BUILD_RERANK_INLIER_COUNT=OFF`` MUST NOT load
``c2_5_rerank.inlier_based_reranker`` (ADR-002 / I-5; verifiable via
``sys.modules``).
"""

from __future__ import annotations

import logging
import os
from typing import TYPE_CHECKING

from gps_denied_onboard.runtime_root.errors import StrategyNotAvailableError

if TYPE_CHECKING:
    from gps_denied_onboard.clock import Clock
    from gps_denied_onboard.components.c2_5_rerank import (
        C2_5RerankConfig,
        ReRankStrategy,
    )
    from gps_denied_onboard.components.c6_tile_cache import TileStore
    from gps_denied_onboard.config.schema import Config
    from gps_denied_onboard.fdr_client import FdrClient
    from gps_denied_onboard.helpers.feature_extractor import FeatureExtractor
    from gps_denied_onboard.helpers.lightglue_runtime import LightGlueRuntime

__all__ = ["build_rerank_strategy"]


_LOG = logging.getLogger("gps_denied_onboard.c2_5_rerank")


# Strategy resolution table — mirrors the contract's
# ``rerank_strategy_protocol.md`` v1.0.0 § Composition-Root Factory
# table verbatim. ANY mutation here MUST be mirrored in the contract.
_STRATEGY_TO_BUILD_FLAG: dict[str, str] = {
    "inlier_count": "BUILD_RERANK_INLIER_COUNT",
}

_STRATEGY_TO_MODULE: dict[str, tuple[str, str]] = {
    "inlier_count": (
        "gps_denied_onboard.components.c2_5_rerank.inlier_based_reranker",
        "InlierCountReRanker",
    ),
}


def _is_build_flag_on(flag_name: str) -> bool:
    raw = os.environ.get(flag_name, "")
    return raw.strip().lower() in {"on", "1", "true", "yes"}


def _c2_5_config(config: "Config") -> "C2_5RerankConfig":
    return config.components["c2_5_rerank"]


def build_rerank_strategy(
    config: "Config",
    *,
    tile_store: "TileStore",
    lightglue_runtime: "LightGlueRuntime",
    feature_extractor: "FeatureExtractor",
    clock: "Clock",
    fdr_client: "FdrClient | None" = None,
) -> "ReRankStrategy":
    """Construct the :class:`ReRankStrategy` impl selected by config.

    1. Reads ``config.components['c2_5_rerank'].strategy``.
    2. Checks the matching ``BUILD_RERANK_<variant>`` flag — if OFF,
       raises :class:`StrategyNotAvailableError` BEFORE any import.
    3. Lazily imports the concrete strategy module.
    4. Constructs the strategy via its module-level
       ``create(config, tile_store, lightglue_runtime, feature_extractor, fdr_client)``
       factory function (each concrete strategy module exports
       ``create`` as its public entry-point; concrete constructors
       stay private).
    5. Emits ONE INFO log ``kind="c2_5.rerank.strategy_loaded"`` with
       structured fields ``{strategy, top_n}``.

    ``feature_extractor`` is a shared L1 helper (AZ-343 scope
    expansion) used by the concrete strategy to extract keypoints +
    descriptors from each per-frame nav image AND from each
    candidate's tile pixels. ``clock`` is the composition-root
    :class:`Clock` (AZ-398) — strategies stamp
    :attr:`RerankResult.reranked_at` via ``clock.monotonic_ns()``
    rather than calling stdlib ``time`` directly (Invariant 2 of
    the replay contract). ``fdr_client`` is optional — passed
    through to strategies that emit FDR records; ``None`` lets the
    strategy run without FDR emission (useful for tests).

    Raises:
        StrategyNotAvailableError: compile-time flag OFF or
            concrete module not yet built.
    """
    block = _c2_5_config(config)
    strategy = block.strategy
    flag_name = _STRATEGY_TO_BUILD_FLAG.get(strategy)
    module_info = _STRATEGY_TO_MODULE.get(strategy)
    if flag_name is None or module_info is None:
        # Defensive — config validation rejects unknown strategy labels
        # at load (C2_5RerankConfig.__post_init__).
        _LOG.error(
            "c2_5.rerank.build_flag_off",
            extra={"strategy": strategy, "reason": "unknown_strategy"},
        )
        raise StrategyNotAvailableError(
            f"ReRankStrategy {strategy!r} is not buildable in this binary."
        )
    if not _is_build_flag_on(flag_name):
        _LOG.error(
            "c2_5.rerank.build_flag_off",
            extra={"strategy": strategy, "flag": flag_name},
        )
        raise StrategyNotAvailableError(
            f"BUILD_RERANK_{strategy.upper()} is OFF for this binary; "
            f"cannot select strategy={strategy}."
        )
    module_name, class_name = module_info
    try:
        module = __import__(module_name, fromlist=[class_name])
    except ModuleNotFoundError as exc:
        raise StrategyNotAvailableError(
            f"ReRankStrategy {strategy!r} is configured but its concrete impl "
            f"module {module_name!r} has not been built into this binary "
            "yet (AZ-343 pending)."
        ) from exc
    create_fn = getattr(module, "create", None)
    if create_fn is None:
        strategy_cls = getattr(module, class_name)
        instance = strategy_cls(
            config,
            tile_store=tile_store,
            lightglue_runtime=lightglue_runtime,
            feature_extractor=feature_extractor,
            clock=clock,
            fdr_client=fdr_client,
        )
    else:
        instance = create_fn(
            config,
            tile_store=tile_store,
            lightglue_runtime=lightglue_runtime,
            feature_extractor=feature_extractor,
            clock=clock,
            fdr_client=fdr_client,
        )
    _LOG.info(
        "c2_5.rerank.strategy_loaded",
        extra={"strategy": strategy, "top_n": block.top_n},
    )
    return instance
