"""C3.5 refiner strategy composition-root factory (AZ-348).

:func:`build_refiner_strategy` selects exactly one strategy by
``config.components['c3_5_adhop'].strategy``. Both concrete
strategies are linked into the production binary
**unconditionally** (NO ``BUILD_REFINER_*`` flag — this is NOT
ADR-002 territory). Runtime selection only per ADR-001.

Strategy resolution table — mirrors the contract's
``conditional_refiner_protocol.md`` v1.0.0 § Composition-root
factory table verbatim:

* ``"adhop"`` → ``gps_denied_onboard.components.c3_5_adhop.adhop_refiner.AdHoPRefiner`` (AZ-349; placeholder today).
* ``"passthrough"`` → ``gps_denied_onboard.components.c3_5_adhop.passthrough_refiner.PassthroughRefiner``.

The shared :class:`RansacFilter` and C7 :class:`InferenceRuntime`
handles are constructor-injected — the factory does NOT own their
lifecycles. The runtime root constructs ONE
:class:`RansacFilter` instance and identity-shares it across C3,
C3.5, and C4 (per ``ransac_filter.md`` v1.0.0).
"""

from __future__ import annotations

import logging
from typing import TYPE_CHECKING

from gps_denied_onboard.components.c3_5_adhop.errors import RefinerConfigError

if TYPE_CHECKING:
    from gps_denied_onboard.components.c3_5_adhop import (
        C3_5RefinerConfig,
        ConditionalRefiner,
    )
    from gps_denied_onboard.components.c7_inference import InferenceRuntime
    from gps_denied_onboard.config.schema import Config
    from gps_denied_onboard.helpers.ransac_filter import RansacFilter

__all__ = ["build_refiner_strategy"]


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


# Strategy resolution table — mirrors the contract verbatim. ANY
# mutation of this dict MUST be mirrored in the contract.
_STRATEGY_TO_MODULE: dict[str, tuple[str, str]] = {
    "adhop": (
        "gps_denied_onboard.components.c3_5_adhop.adhop_refiner",
        "AdHoPRefiner",
    ),
    "passthrough": (
        "gps_denied_onboard.components.c3_5_adhop.passthrough_refiner",
        "PassthroughRefiner",
    ),
}


def _c3_5_config(config: "Config") -> "C3_5RefinerConfig":
    """Pull the registered C3.5 config block.

    ``c3_5_adhop.__init__`` registers it on import; a missing
    registration is a developer error and surfaces as ``KeyError``
    rather than a silent fallback.
    """
    return config.components["c3_5_adhop"]


def build_refiner_strategy(
    config: "Config",
    *,
    ransac_filter: "RansacFilter",
    inference_runtime: "InferenceRuntime",
) -> "ConditionalRefiner":
    """Construct the :class:`ConditionalRefiner` impl selected by config.

    1. Reads ``config.components['c3_5_adhop'].{strategy, residual_threshold_px}``.
    2. Validates ``residual_threshold_px > 0`` — defensive
       redundancy on top of the config-load-time check
       (:class:`C3_5RefinerConfig.__post_init__`); raises
       :class:`RefinerConfigError` on failure.
    3. Imports the concrete strategy module via the resolution
       table (NOT lazy — both strategies are linked
       unconditionally).
    4. Constructs the strategy via its module-level
       ``create(config, ransac_filter, inference_runtime)``
       factory function.
    5. Emits ONE INFO log ``kind="c3_5.refiner.strategy_loaded"``
       with ``{strategy, residual_threshold_px}``.

    Raises:
        RefinerConfigError: unknown strategy label OR invalid
            threshold (``<= 0``).
    """
    block = _c3_5_config(config)
    strategy = block.strategy
    module_info = _STRATEGY_TO_MODULE.get(strategy)
    if module_info is None:
        _LOG.error(
            "c3_5.refiner.strategy_unknown",
            extra={"strategy": strategy},
        )
        raise RefinerConfigError(f"Unknown refiner strategy: {strategy}")
    if block.residual_threshold_px <= 0.0:
        # Config-load validation should have rejected this already;
        # defensive in case a caller constructed Config bypassing
        # __post_init__ (e.g., via dataclasses.replace on a partial
        # block).
        _LOG.error(
            "c3_5.refiner.invalid_threshold",
            extra={
                "strategy": strategy,
                "residual_threshold_px": block.residual_threshold_px,
            },
        )
        raise RefinerConfigError(
            "residual_threshold_px must be > 0; "
            f"got {block.residual_threshold_px}"
        )
    module_name, class_name = module_info
    module = __import__(module_name, fromlist=[class_name])
    create_fn = getattr(module, "create", None)
    if create_fn is None:
        strategy_cls = getattr(module, class_name)
        instance = strategy_cls(
            ransac_filter=ransac_filter,
            inference_runtime=inference_runtime,
        )
    else:
        instance = create_fn(
            config,
            ransac_filter=ransac_filter,
            inference_runtime=inference_runtime,
        )
    _LOG.info(
        "c3_5.refiner.strategy_loaded",
        extra={
            "strategy": strategy,
            "residual_threshold_px": block.residual_threshold_px,
        },
    )
    return instance
