"""ActionLoader — dynamic Action plugin discovery and loading.

Scans ``{data_dir}/actions/`` for Python files that export an
``ActionClass`` inheriting from :class:`Action`, and provides
factory methods to instantiate them at runtime.  See design doc §7.2.
"""

from __future__ import annotations

import importlib.util
import logging
import os
import sys
from typing import Any, Optional

from oedix.core.action import Action

logger = logging.getLogger("oedix.loader")


class ActionLoader:
    """Discover, load, and instantiate Action plugins from a directory.

    Usage::

        loader = ActionLoader("/path/to/data/actions")
        loader.discover()
        actions = loader.list_actions()
        tool_action = loader.create("tool", tool_name="search")
    """

    def __init__(self, actions_dir: str) -> None:
        self.actions_dir = actions_dir
        self._registry: dict[str, type[Action]] = {}

    def discover(self) -> list[type[Action]]:
        """Scan the actions directory and register all valid Action classes.

        Each ``.py`` file must export an ``ActionClass`` variable that is
        a subclass of :class:`Action` with a non-empty ``type`` attribute.

        Returns:
            List of discovered Action classes.
        """
        self._registry.clear()
        if not os.path.isdir(self.actions_dir):
            logger.warning("Actions directory not found: %s", self.actions_dir)
            return []

        discovered: list[type[Action]] = []
        for filename in sorted(os.listdir(self.actions_dir)):
            if not filename.endswith(".py") or filename.startswith("_"):
                continue
            filepath = os.path.join(self.actions_dir, filename)
            if not os.path.isfile(filepath):
                continue

            action_cls = self._load_file(filepath)
            if action_cls is not None:
                self._registry[action_cls.type] = action_cls
                discovered.append(action_cls)
                logger.debug(
                    "Discovered action: type=%s from %s",
                    action_cls.type,
                    filename,
                )

        logger.info(
            "ActionLoader discovered %d actions: %s",
            len(discovered),
            [c.type for c in discovered],
        )
        return discovered

    def load(self, action_type: str) -> type[Action]:
        """Load a specific Action class by type.

        If the type is already registered, returns the cached class.
        Otherwise, attempts to find and load the corresponding file.

        Args:
            action_type: The ``type`` identifier of the Action.

        Returns:
            The Action subclass.

        Raises:
            KeyError: If no Action with the given type can be found.
        """
        if action_type in self._registry:
            return self._registry[action_type]

        # Try loading from file named after the type
        filepath = os.path.join(self.actions_dir, f"{action_type}.py")
        if os.path.isfile(filepath):
            cls = self._load_file(filepath)
            if cls is not None:
                self._registry[cls.type] = cls
                return cls

        raise KeyError(
            f"Action type '{action_type}' not found in {self.actions_dir}"
        )

    def list_actions(self) -> list[dict[str, str]]:
        """Return metadata for all registered actions.

        Used by the thinking action to build the available actions
        list for LLM decision-making.

        Returns:
            List of ``{"type": ..., "describe": ..., "usage": ...}`` dicts.
        """
        if not self._registry:
            self.discover()
        result = []
        for cls in self._registry.values():
            instance = cls()
            result.append({
                "type": cls.type,
                "describe": instance.describe(),
                "usage": instance.usage(),
            })
        return result

    def create(self, action_type: str, **params: Any) -> Action:
        """Create an Action instance by type.

        Args:
            action_type: The ``type`` identifier.
            **params: Keyword arguments passed to ``Action.__init__``.

        Returns:
            A new Action instance.

        Raises:
            KeyError: If the action type is not found.
        """
        cls = self.load(action_type)
        return cls(**params)

    def get_types(self) -> list[str]:
        """Return list of registered action type names."""
        if not self._registry:
            self.discover()
        return list(self._registry.keys())

    # ------------------------------------------------------------------
    # Internal
    # ------------------------------------------------------------------

    def _load_file(self, filepath: str) -> Optional[type[Action]]:
        """Load a single Action plugin file.

        The file must export ``ActionClass`` which must be a subclass
        of :class:`Action` with a non-empty ``type`` class variable.
        Uses ``importlib.util`` to avoid module cache issues in
        WIZ's ``exec()`` environment.
        """
        module_name = (
            f"oedix_plugin_{os.path.basename(filepath).removesuffix('.py')}"
        )
        try:
            # Clear cached module to pick up changes
            if module_name in sys.modules:
                del sys.modules[module_name]

            spec = importlib.util.spec_from_file_location(
                module_name, filepath
            )
            if spec is None or spec.loader is None:
                logger.warning("Cannot create spec for %s", filepath)
                return None

            module = importlib.util.module_from_spec(spec)
            sys.modules[module_name] = module
            spec.loader.exec_module(module)

            action_cls = getattr(module, "ActionClass", None)
            if action_cls is None:
                logger.warning(
                    "No ActionClass export in %s", filepath
                )
                return None

            if not (
                isinstance(action_cls, type)
                and issubclass(action_cls, Action)
            ):
                logger.warning(
                    "ActionClass in %s is not an Action subclass",
                    filepath,
                )
                return None

            if not getattr(action_cls, "type", ""):
                logger.warning(
                    "ActionClass in %s has no 'type' attribute",
                    filepath,
                )
                return None

            return action_cls

        except Exception:
            logger.exception("Failed to load action from %s", filepath)
            return None
