"""Helpers for loading OmniLink use-case configurations.

The Python engine can be extended by providing *use case* modules that
configure an :class:`~omnilink.OmniLinkEngine` with domain specific templates
and handlers. This module centralises the discovery of those modules so that
bridge scripts (MQTT, remote polling, TCP forwarding, …) can remain generic.

A use case module must expose either a ``build_use_case()`` function returning a
:class:`UseCaseConfig` or a module level ``USE_CASE`` constant with the same
structure. The module to load is selected via the ``OmniLink_USE_CASE``
environment variable and defaults to ``"omnilink.examples.robot.use_case"``
which ships with the engine as a reference implementation. For backward
compatibility the loader also accepts the legacy ``"examples.robot.use_case"``
value and automatically prefixes it with ``"omnilink."``.
"""

from __future__ import annotations

import importlib
import os
from dataclasses import dataclass, field
from typing import Callable, Optional, Sequence

from . import OmniLinkEngine, TypeRegistry


def _noop_configure(_engine: OmniLinkEngine) -> None:
    """Default ``configure`` callback used when a use case does not provide one."""


@dataclass
class UseCaseConfig:
    """Container describing how a concrete use case integrates with the engine."""

    types: TypeRegistry
    templates: Sequence[str]
    configure: Callable[[OmniLinkEngine], None] = field(default=_noop_configure)
    context_provider: Optional[Callable[[], str]] = None

    def apply(self, engine: OmniLinkEngine) -> None:
        """Execute the ``configure`` callback for convenience."""

        self.configure(engine)


_DEFAULT_MODULE = "omnilink.examples.robot.use_case"


def load_use_case(module_name: Optional[str] = None) -> UseCaseConfig:
    """Return the configured :class:`UseCaseConfig` for the current deployment.

    ``module_name`` can override the discovery logic during tests. When not
    provided we respect the ``OmniLink_USE_CASE`` environment variable, falling
    back to the bundled robot example.
    """

    configured = module_name or os.environ.get("OmniLink_USE_CASE", _DEFAULT_MODULE)
    candidates = [configured]
    if not configured.startswith("omnilink."):
        candidates.append(f"omnilink.{configured}")

    module = None
    last_error: Optional[ModuleNotFoundError] = None
    target = configured
    for candidate in candidates:
        try:
            module = importlib.import_module(candidate)
        except ModuleNotFoundError as exc:
            last_error = exc
        else:
            target = candidate
            break
    else:
        if last_error is not None:
            raise last_error
        raise ModuleNotFoundError(configured)

    if hasattr(module, "build_use_case"):
        config = module.build_use_case()  # type: ignore[assignment]
    elif hasattr(module, "USE_CASE"):
        config = module.USE_CASE  # type: ignore[assignment]
    else:  # pragma: no cover - defensive programming
        raise AttributeError(
            f"Use case module {target!r} must define build_use_case() or USE_CASE"
        )

    if not isinstance(config, UseCaseConfig):  # pragma: no cover - validation
        raise TypeError(
            f"Use case module {target!r} returned unsupported configuration type"
        )

    return config


__all__ = ["UseCaseConfig", "load_use_case"]
