"""ExecutorRegistry for managing flow step executors.

This module provides the central registry for executor resolution. Executors are
resolved by step type (e.g., "apiCall", "conditional") and instantiated on demand.

Architecture Note - Pipeline Injection for Composite Executors:
    Composite executors (conditional, switch, loop) contain nested blocks that must
    be executed through the same pipeline as top-level steps. This creates a circular
    dependency: Pipeline -> ExecutorStage -> Registry -> CompositeExecutor -> Pipeline.

    Resolution: The registry accepts the pipeline via `set_pipeline()` after pipeline
    construction. The execution strategy (ProductionStrategy/TestStrategy) is responsible
    for calling this after building the pipeline. See `strategy.py` for the initialization
    sequence.
"""

from __future__ import annotations

import inspect
import re
import threading
from importlib import import_module
from typing import TYPE_CHECKING, Any

import structlog

from agent_flows.exceptions import ExecutorNotFoundError
from agent_flows.executors.base import BaseExecutor

if TYPE_CHECKING:
    from agent_flows.core.flow_engine.pipeline import ExecutionPipeline

logger = structlog.get_logger(__name__)

# Mapping of step types to their executor module and class names.
# Executors are lazily loaded on first access to reduce startup time.
_BUILT_IN_EXECUTORS: dict[str, tuple[str, str]] = {
    "flow_variables": ("agent_flows.executors.flow_variables", "FlowVariablesExecutor"),
    "apiCall": ("agent_flows.executors.api_call", "ApiCallExecutor"),
    "codeInterpreter": ("agent_flows.executors.code_interpreter", "CodeInterpreterExecutor"),
    "llmInstruction": ("agent_flows.executors.llm_instruction", "LlmInstructionExecutor"),
    "setVariables": ("agent_flows.executors.set_variables", "SetVariablesExecutor"),
    "webScraping": ("agent_flows.executors.web_scraping", "WebScrapingExecutor"),
    "webSearch": ("agent_flows.executors.web_search", "WebSearchExecutor"),
    "conditional": ("agent_flows.executors.conditional", "ConditionalExecutor"),
    "switch": ("agent_flows.executors.switch", "SwitchExecutor"),
    "loop": ("agent_flows.executors.loop", "LoopExecutor"),
    "mcpServerAction": ("agent_flows.executors.mcp_server_action", "McpServerActionExecutor"),
    "streamUI": ("agent_flows.executors.stream_ui", "StreamUIExecutor"),
    "finish": ("agent_flows.executors.finish", "FinishExecutor"),
    "runCommand": ("agent_flows.executors.run_command", "RunCommandExecutor"),
}

# Composite executors execute nested blocks and require a BlockExecutor dependency.
# See module docstring for the rationale behind the deferred pipeline injection pattern.
_COMPOSITE_EXECUTORS = {"conditional", "switch", "loop"}


class ExecutorRegistry:
    """Thread-safe registry for resolving and instantiating flow step executors.

    Simple executors are instantiated with no arguments. Composite executors receive
    a BlockExecutor that wraps the pipeline for nested block execution.
    """

    def __init__(self) -> None:
        self._executors: dict[str, type[BaseExecutor]] = {}
        self._lock = threading.RLock()
        self._pipeline: ExecutionPipeline | None = None

    def set_pipeline(self, pipeline: ExecutionPipeline) -> None:
        """Configure the pipeline for composite executor block execution.

        Must be called after pipeline construction but before any composite
        executors are instantiated. Typically called by the execution strategy.

        Args:
            pipeline: The execution pipeline that composite executors will use
                      to execute their nested blocks.
        """
        with self._lock:
            self._pipeline = pipeline
            logger.debug("Pipeline configured for composite executors")

    def register_executor(
        self,
        step_type: str,
        executor_class: type[BaseExecutor],
        override: bool = False,
    ) -> None:
        """Register a custom executor."""
        with self._lock:
            if not self._is_valid_step_type(step_type):
                raise ValueError(
                    f"Invalid step type '{step_type}'. Must be alphanumeric with optional underscores/hyphens"
                )

            if not self._is_valid_executor_class(executor_class):
                raise TypeError(
                    f"Executor class {executor_class.__name__} must inherit from BaseExecutor"
                )

            if step_type in self._executors and not override:
                raise ValueError(f"Executor for step type '{step_type}' already registered")

            self._executors[step_type] = executor_class

            logger.debug(
                "Executor registered",
                step_type=step_type,
                executor_class=executor_class.__name__,
            )

    def get_executor(self, step_type: str) -> BaseExecutor:
        """Get executor instance for a step type.

        For composite executors (conditional, switch, loop), this passes the
        configured block executor to enable nested block execution.
        """
        with self._lock:
            if step_type not in self._executors:
                self._load_built_in(step_type)

            if step_type not in self._executors:
                raise ExecutorNotFoundError(f"No executor registered for step type: {step_type}")

            executor_class = self._executors[step_type]
            try:
                if step_type in _COMPOSITE_EXECUTORS:
                    # Composite executors receive a BlockExecutor to run nested blocks
                    if self._pipeline is None:
                        raise ExecutorNotFoundError(
                            f"Composite executor '{step_type}' requires pipeline to be configured. "
                            "Call set_pipeline() after building the pipeline."
                        )
                    from agent_flows.core.flow_engine.block_executor import BlockExecutor

                    block_executor = BlockExecutor(self._pipeline)
                    return executor_class(block_executor)
                else:
                    # Simple executors have no dependencies
                    return executor_class()
            except Exception as e:  # pragma: no cover - defensive guard
                logger.error(
                    "Failed to instantiate executor",
                    step_type=step_type,
                    error=str(e),
                )
                raise ExecutorNotFoundError(
                    f"Failed to create executor instance for step type: {step_type}"
                ) from e

    def is_registered(self, step_type: str) -> bool:
        """Check if a step type is registered."""
        with self._lock:
            if step_type not in self._executors:
                self._load_built_in(step_type)
            return step_type in self._executors

    def validate_step_config(self, step_type: str, config: dict[str, Any]) -> bool:
        """Validate the configuration for a given step type."""
        executor = self.get_executor(step_type)
        return executor.validate_config(config)

    def _load_built_in(self, step_type: str) -> None:
        module_info = _BUILT_IN_EXECUTORS.get(step_type)
        if not module_info:
            return
        module_name, attr = module_info
        try:
            module = import_module(module_name)
            executor_cls = getattr(module, attr)
            self._executors[step_type] = executor_cls
        except Exception as e:  # pragma: no cover - defensive guard
            logger.warning(
                "Failed to load built-in executor",
                step_type=step_type,
                module=module_name,
                error=str(e),
            )

    def _is_valid_step_type(self, step_type: str) -> bool:
        """Validate step type naming conventions."""
        if not isinstance(step_type, str) or not step_type:
            return False

        # Allow alphanumeric characters, underscores, and hyphens
        return bool(re.match(r"^[a-zA-Z][a-zA-Z0-9_-]*$", step_type))

    def _is_valid_executor_class(self, executor_class: type[Any]) -> bool:
        """Validate that executor class implements BaseExecutor."""
        if not inspect.isclass(executor_class):
            return False

        # Check if it's a subclass of BaseExecutor
        try:
            return issubclass(executor_class, BaseExecutor)
        except TypeError:  # pragma: no cover - defensive guard
            return False
