"""
Dynamic dependency injection system for MCP Mesh.

Handles both initial injection and runtime updates when topology changes.
Focused purely on dependency injection - telemetry/tracing is handled at
the HTTP middleware layer for unified approach across MCP agents and FastAPI apps.
"""

import asyncio
import functools
import inspect
import logging
import weakref
from collections.abc import Callable
from typing import Any

from ..shared.logging_config import (
    format_log_value,
    format_result_summary,
    get_trace_prefix,
)
from .signature_analyzer import get_mesh_agent_positions, has_llm_agent_parameter

logger = logging.getLogger(__name__)


def _build_clean_signature(func: Any) -> inspect.Signature | None:
    """Build a signature excluding McpMeshTool parameters (detected by type).

    Uses type-based detection (not position-based) to avoid false positives
    from the injector heuristic that treats single/non-typed params as targets.

    Only excludes McpMeshTool here. MeshLlmAgent is excluded by the @mesh.llm
    decorator's __signature__ override, which runs AFTER @mesh.tool in the
    decoration chain (@mesh.tool → @mesh.llm → @app.tool).

    Returns None if no parameters need removal.
    """
    try:
        from .signature_analyzer import (
            _get_original_func,
            get_mesh_agent_parameter_names,
        )

        original = _get_original_func(func)
        injectable_names = set(get_mesh_agent_parameter_names(original))
        if not injectable_names:
            return None
        sig = inspect.signature(original)
        clean_params = [
            param
            for name, param in sig.parameters.items()
            if name not in injectable_names
        ]
        return sig.replace(parameters=clean_params)
    except Exception:
        return None


def analyze_injection_strategy(func: Callable, dependencies: list[str]) -> list[int]:
    """
    Analyze function signature and determine injection strategy.

    Rules:
    1. Single parameter: inject regardless of typing (with warning if not McpMeshTool)
    2. Multiple parameters: only inject into McpMeshTool typed parameters
    3. Log warnings for mismatches and edge cases

    Args:
        func: Function to analyze
        dependencies: List of dependency names to inject

    Returns:
        List of parameter positions to inject into
    """
    sig = inspect.signature(func)
    params = list(sig.parameters.values())
    param_count = len(params)
    mesh_positions = get_mesh_agent_positions(func)
    func_name = f"{func.__module__}.{func.__qualname__}"

    # No parameters at all
    if param_count == 0:
        if dependencies:
            logger.warning(
                f"Function '{func_name}' has no parameters but {len(dependencies)} "
                f"dependencies declared. Skipping injection."
            )
        return []

    # Single parameter rule: inject regardless of typing
    if param_count == 1:
        if not mesh_positions:
            param_name = params[0].name
            logger.warning(
                f"Single parameter '{param_name}' in function '{func_name}' found, "
                f"injecting {dependencies[0] if dependencies else 'dependency'} proxy "
                f"(consider typing as McpMeshTool for clarity)"
            )
        return [0]  # Inject into the single parameter

    # Multiple parameters rule: only inject into McpMeshTool typed parameters
    if param_count > 1:
        if not mesh_positions:
            logger.warning(
                f"⚠️ Function '{func_name}' has {param_count} parameters but none are "
                f"typed as McpMeshTool. Skipping injection of {len(dependencies)} dependencies. "
                f"Consider typing dependency parameters as McpMeshTool."
            )
            return []

        # Check for dependency/parameter count mismatches
        if len(dependencies) != len(mesh_positions):
            if len(dependencies) > len(mesh_positions):
                excess_deps = dependencies[len(mesh_positions) :]
                logger.warning(
                    f"Function '{func_name}' has {len(dependencies)} dependencies "
                    f"but only {len(mesh_positions)} McpMeshTool parameters. "
                    f"Dependencies {excess_deps} will not be injected."
                )
            else:
                excess_params = [
                    params[pos].name for pos in mesh_positions[len(dependencies) :]
                ]
                logger.warning(
                    f"Function '{func_name}' has {len(mesh_positions)} McpMeshTool parameters "
                    f"but only {len(dependencies)} dependencies declared. "
                    f"Parameters {excess_params} will remain None."
                )

        # Return positions we can actually inject into
        return mesh_positions[: len(dependencies)]

    return mesh_positions


def _prepare_injection_kwargs(
    func: Callable,
    kwargs: dict,
    mesh_positions: list[int],
    dependencies: list[str],
    injected_deps_array: list,
    get_dependency_fn: Callable[[str], Any | None],
    log: logging.Logger,
) -> tuple[dict, int]:
    """Prepare kwargs with injected dependencies and LLM agent.

    This contains the shared logic between async and sync dependency wrappers:
    dependency resolution, LLM agent injection, and invocation logging.

    Args:
        func: The original function being wrapped
        kwargs: Caller-supplied keyword arguments
        mesh_positions: Parameter positions that accept mesh dependencies
        dependencies: Dependency names declared on the function
        injected_deps_array: Wrapper's mutable array of cached dependency instances
        get_dependency_fn: Fallback lookup for dependencies not in the array
        log: Logger instance for debug output

    Returns:
        Tuple of (final_kwargs dict ready for invocation, injected_count)
    """
    tp = get_trace_prefix()

    # Log tool invocation
    arg_keys = list(kwargs.keys()) if kwargs else []
    log.debug(f"{tp}🔧 Tool '{func.__name__}' called with kwargs={arg_keys}")
    log.debug(f"{tp}🔧 Tool '{func.__name__}' args: {format_log_value(kwargs)}")

    # Build final kwargs with injected dependencies
    sig = inspect.signature(func)
    params = list(sig.parameters.keys())
    final_kwargs = kwargs.copy()

    injected_count = 0
    injected_deps: list[str] = []

    for dep_index, param_position in enumerate(mesh_positions):
        if dep_index < len(dependencies):
            dep_name = dependencies[dep_index]
            param_name = params[param_position]

            if param_name not in final_kwargs or final_kwargs.get(param_name) is None:
                dependency = None
                if dep_index < len(injected_deps_array):
                    dependency = injected_deps_array[dep_index]

                if dependency is None:
                    dep_key = f"{func.__module__}.{func.__qualname__}:dep_{dep_index}"
                    dependency = get_dependency_fn(dep_key)

                final_kwargs[param_name] = dependency
                injected_count += 1
                proxy_type = type(dependency).__name__ if dependency else "None"
                injected_deps.append(f"{dep_name} → {proxy_type}")

    if injected_count > 0:
        log.debug(
            f"{tp}🔧 Injected {injected_count} dependencies: {', '.join(injected_deps)}"
        )

    # Inject LLM agent if the function has @mesh.llm metadata
    if hasattr(func, "_mesh_llm_param_name"):
        llm_param = func._mesh_llm_param_name
        if llm_param not in final_kwargs or final_kwargs.get(llm_param) is None:
            llm_agent = getattr(func, "_mesh_llm_agent", None)
            final_kwargs[llm_param] = llm_agent
            log.debug(f"{tp}🤖 LLM_INJECTION: Injected {llm_param}={llm_agent}")

    return final_kwargs, injected_count


def _log_wrapper_result(func: Callable, result: Any, log: logging.Logger) -> None:
    """Log the result of a dependency-injected function call."""
    tp = get_trace_prefix()
    log.debug(
        f"{tp}🔧 Tool '{func.__name__}' returned: {format_result_summary(result)}"
    )
    log.debug(f"{tp}🔧 Tool '{func.__name__}' result: {format_log_value(result)}")


class DependencyInjector:
    """
    Manages dynamic dependency injection for mesh agents.

    This class:
    1. Maintains a registry of available dependencies (McpMeshTool)
    2. Coordinates with MeshLlmAgentInjector for LLM agent injection
    3. Tracks which functions depend on which services
    4. Updates function bindings when topology changes
    5. Handles graceful degradation when dependencies unavailable
    """

    def __init__(self):
        self._dependencies: dict[str, Any] = {}
        self._function_registry: weakref.WeakValueDictionary = (
            weakref.WeakValueDictionary()
        )
        self._dependency_mapping: dict[str, set[str]] = (
            {}
        )  # dep_name -> set of function_ids
        self._lock = asyncio.Lock()

        # LLM agent injector for MeshLlmAgent parameters
        from .mesh_llm_agent_injector import get_global_llm_injector

        self._llm_injector = get_global_llm_injector()
        logger.debug("🤖 DependencyInjector initialized with MeshLlmAgentInjector")

    async def register_dependency(self, name: str, instance: Any) -> None:
        """Register a new dependency or update existing one.

        Args:
            name: Composite key in format "function_id:dep_N" or legacy capability name
            instance: Proxy instance to register
        """
        async with self._lock:
            logger.debug(f"📦 Registering dependency: {name}")
            self._dependencies[name] = instance

            # Notify all functions that depend on this (using composite keys)
            if name in self._dependency_mapping:
                for func_id in self._dependency_mapping[name]:
                    if func_id in self._function_registry:
                        func = self._function_registry[func_id]
                        logger.debug(
                            f"🔄 UPDATING dependency '{name}' for {func_id} -> {func} at {hex(id(func))}"
                        )
                        if hasattr(func, "_mesh_update_dependency"):
                            # Extract dep_index from composite key (format: "function_id:dep_N")
                            if ":dep_" in name:
                                dep_index_str = name.split(":dep_")[-1]
                                try:
                                    dep_index = int(dep_index_str)
                                    func._mesh_update_dependency(dep_index, instance)
                                except ValueError:
                                    logger.warning(
                                        f"⚠️ Invalid dep_index in key '{name}', skipping update"
                                    )
                            else:
                                # Legacy format (shouldn't happen with new code)
                                logger.warning(
                                    f"⚠️ Legacy dependency key format '{name}' not supported in array-based injection"
                                )

    async def unregister_dependency(self, name: str) -> None:
        """Remove a dependency (e.g., service went down).

        Args:
            name: Composite key in format "function_id:dep_N" or legacy capability name
        """
        async with self._lock:
            logger.info(f"🗑️ INJECTOR: Unregistering dependency: {name}")
            if name in self._dependencies:
                del self._dependencies[name]
                logger.info(f"🗑️ INJECTOR: Removed {name} from dependencies registry")

                # Notify all functions that depend on this
                if name in self._dependency_mapping:
                    affected_functions = self._dependency_mapping[name]
                    logger.info(
                        f"🗑️ INJECTOR: Updating {len(affected_functions)} functions affected by {name} removal"
                    )

                    for func_id in affected_functions:
                        if func_id in self._function_registry:
                            func = self._function_registry[func_id]
                            if hasattr(func, "_mesh_update_dependency"):
                                logger.info(
                                    f"🗑️ INJECTOR: Removing {name} from function {func_id}"
                                )
                                # Extract dep_index from composite key
                                if ":dep_" in name:
                                    dep_index_str = name.split(":dep_")[-1]
                                    try:
                                        dep_index = int(dep_index_str)
                                        func._mesh_update_dependency(dep_index, None)
                                    except ValueError:
                                        logger.warning(
                                            f"⚠️ Invalid dep_index in key '{name}', skipping removal"
                                        )
                                else:
                                    # Legacy format
                                    logger.warning(
                                        f"⚠️ Legacy dependency key format '{name}' not supported in array-based injection"
                                    )
                            else:
                                logger.warning(
                                    f"🗑️ INJECTOR: Function {func_id} has no _mesh_update_dependency method"
                                )
                        else:
                            logger.warning(
                                f"🗑️ INJECTOR: Function {func_id} not found in registry"
                            )
                else:
                    logger.info(f"🗑️ INJECTOR: No functions mapped to dependency {name}")
            else:
                logger.info(f"🗑️ INJECTOR: Dependency {name} was not registered (no-op)")

    def get_dependency(self, name: str) -> Any | None:
        """Get current instance of a dependency."""
        return self._dependencies.get(name)

    def find_original_function(self, function_name: str) -> Any | None:
        """Find the original function by name from wrapper registry or decorator registry.

        This is used for self-dependency proxy creation to get the cached
        original function reference for direct calls.

        Args:
            function_name: Name of the function to find

        Returns:
            Original function if found, None otherwise
        """
        logger.debug(f"🔍 Searching for original function: '{function_name}'")

        # First, search through wrapper registry (functions with dependencies)
        for func_id, wrapper_func in self._function_registry.items():
            if hasattr(wrapper_func, "_mesh_original_func"):
                original = wrapper_func._mesh_original_func

                # Match by function name
                if hasattr(original, "__name__") and original.__name__ == function_name:
                    logger.debug(
                        f"✅ Found original function '{function_name}' in wrapper registry: {func_id}"
                    )
                    return original

        # If not found in wrapper registry, search in decorator registry (all functions)
        try:
            from .decorator_registry import DecoratorRegistry

            # Search through mesh tools (functions decorated with @mesh.tool)
            mesh_tools = DecoratorRegistry.get_mesh_tools()
            for tool_name, decorated_func in mesh_tools.items():
                original_func = decorated_func.function  # Get the original function
                if (
                    hasattr(original_func, "__name__")
                    and original_func.__name__ == function_name
                ):
                    logger.debug(
                        f"✅ Found original function '{function_name}' in decorator registry: {tool_name}"
                    )
                    return original_func

        except Exception as e:
            logger.warning(f"⚠️ Error searching decorator registry: {e}")

        # List available functions for debugging
        available_functions = []
        for wrapper_func in self._function_registry.values():
            if hasattr(wrapper_func, "_mesh_original_func"):
                original = wrapper_func._mesh_original_func
                if hasattr(original, "__name__"):
                    available_functions.append(original.__name__)

        # Also list functions from decorator registry
        try:
            from .decorator_registry import DecoratorRegistry

            mesh_tools = DecoratorRegistry.get_mesh_tools()
            for tool_name, decorated_func in mesh_tools.items():
                if hasattr(decorated_func.function, "__name__"):
                    available_functions.append(decorated_func.function.__name__)
        except (AttributeError, KeyError, TypeError):
            pass

        logger.warning(
            f"❌ Original function '{function_name}' not found. "
            f"Available functions: {list(set(available_functions))}"
        )
        return None

    def process_llm_tools(self, llm_tools: dict[str, list[dict[str, Any]]]) -> None:
        """
        Process llm_tools from registry response and delegate to MeshLlmAgentInjector.

        Args:
            llm_tools: Dict mapping function_id -> list of tool metadata
                      Format: {"function_id": [{"function_name": "...", "endpoint": {...}, ...}]}
        """
        logger.info(
            f"🤖 DependencyInjector processing llm_tools for {len(llm_tools)} functions"
        )
        self._llm_injector.process_llm_tools(llm_tools)

    def process_llm_providers(self, llm_providers: dict[str, dict[str, Any]]) -> None:
        """
        Process llm_providers from registry response and delegate to MeshLlmAgentInjector (v0.6.1).

        Args:
            llm_providers: Dict mapping function_name -> ResolvedLLMProvider
                          Format: {"function_name": {"agent_id": "...", "endpoint": "...", ...}}
        """
        logger.info(
            f"🔌 DependencyInjector processing llm_providers for {len(llm_providers)} functions"
        )
        self._llm_injector.process_llm_providers(llm_providers)

    def update_llm_tools(self, llm_tools: dict[str, list[dict[str, Any]]]) -> None:
        """
        Update llm_tools when topology changes (heartbeat updates).

        Args:
            llm_tools: Updated llm_tools dict from registry
        """
        logger.info(
            f"🔄 DependencyInjector updating llm_tools for {len(llm_tools)} functions"
        )
        self._llm_injector.update_llm_tools(llm_tools)

    def create_llm_injection_wrapper(
        self, func: Callable, function_id: str
    ) -> Callable:
        """
        Create wrapper for function with MeshLlmAgent parameter.

        Delegates to MeshLlmAgentInjector.

        Args:
            func: Function to wrap
            function_id: Unique function ID from @mesh.llm decorator

        Returns:
            Wrapped function with MeshLlmAgent injection
        """
        logger.debug(f"🤖 Creating LLM injection wrapper for {function_id}")
        return self._llm_injector.create_injection_wrapper(func, function_id)

    def initialize_direct_llm_agents(self) -> None:
        """
        Initialize LLM agents that use direct LiteLLM (no mesh delegation).

        This should be called during agent startup to initialize agents that
        don't need to wait for registry response.
        """
        self._llm_injector.initialize_direct_llm_agents()

    def create_injection_wrapper(
        self, func: Callable, dependencies: list[str]
    ) -> Callable:
        """
        Create in-place dependency injection by modifying the original function.

        This approach:
        1. Preserves the original function pointer for FastMCP
        2. Adds dynamic dependency injection capability
        3. Can be updated when topology changes
        4. Handles missing dependencies gracefully
        5. Logs warnings for configuration issues
        """
        func_id = f"{func.__module__}.{func.__qualname__}"

        # Use new smart injection strategy
        mesh_positions = analyze_injection_strategy(func, dependencies)

        # Track which dependencies this function needs (using composite keys)
        for dep_index, dep in enumerate(dependencies):
            dep_key = f"{func_id}:dep_{dep_index}"
            if dep_key not in self._dependency_mapping:
                self._dependency_mapping[dep_key] = set()
            self._dependency_mapping[dep_key].add(func_id)

        # Store current dependency values as array (indexed by position)
        if not hasattr(func, "_mesh_injected_deps"):
            func._mesh_injected_deps = [None] * len(dependencies)

        # Store original implementation if not already stored
        if not hasattr(func, "_mesh_original_func"):
            func._mesh_original_func = func

        # Create a wrapper function that handles dependency injection
        # Capture logger in local scope to avoid NameError
        wrapper_logger = logger

        # If no mesh positions to inject, create minimal wrapper for tracking
        if not mesh_positions:
            logger.debug(
                f"🔧 No injection positions for {func.__name__}, creating minimal wrapper for tracking"
            )

            # Check if we need async wrapper for minimal case
            if inspect.iscoroutinefunction(func):

                @functools.wraps(func)
                async def minimal_wrapper(*args, **kwargs):
                    # Use ExecutionTracer for functions without dependencies (v0.4.0 style)
                    from ..tracing.execution_tracer import ExecutionTracer

                    wrapper_logger.debug(
                        f"🔧 DI: Executing async function {func.__name__} (no dependencies)"
                    )

                    # For async functions without dependencies, use the async tracer
                    return await ExecutionTracer.trace_function_execution_async(
                        func, args, kwargs, [], [], 0, wrapper_logger
                    )

            else:

                @functools.wraps(func)
                def minimal_wrapper(*args, **kwargs):
                    # Use ExecutionTracer for functions without dependencies (v0.4.0 style)
                    from ..tracing.execution_tracer import ExecutionTracer

                    wrapper_logger.debug(
                        f"🔧 DI: Executing sync function {func.__name__} (no dependencies)"
                    )

                    # Use original function tracer for functions without dependencies
                    return ExecutionTracer.trace_original_function(
                        func, args, kwargs, wrapper_logger
                    )

            # Add minimal metadata for compatibility (use array for consistency)
            minimal_wrapper._mesh_injected_deps = [None] * len(dependencies)
            minimal_wrapper._mesh_dependencies = dependencies
            minimal_wrapper._mesh_positions = mesh_positions
            minimal_wrapper._mesh_original_func = func

            # Override signature to hide injectable parameters from FastMCP
            clean_sig = _build_clean_signature(func)
            if clean_sig is not None:
                minimal_wrapper.__signature__ = clean_sig

            def update_dependency(dep_index: int, instance: Any | None) -> None:
                """No-op update for functions without injection positions."""
                pass

            minimal_wrapper._mesh_update_dependency = update_dependency

            # Register this wrapper for dependency updates (even though it won't use them)
            logger.debug(
                f"🔧 REGISTERING minimal wrapper: {func_id} -> {minimal_wrapper} at {hex(id(minimal_wrapper))}"
            )
            self._function_registry[func_id] = minimal_wrapper

            return minimal_wrapper

        # Determine if we need async wrapper
        need_async_wrapper = inspect.iscoroutinefunction(func)

        if need_async_wrapper:

            @functools.wraps(func)
            async def dependency_wrapper(*args, **kwargs):
                final_kwargs, injected_count = _prepare_injection_kwargs(
                    func,
                    kwargs,
                    mesh_positions,
                    dependencies,
                    dependency_wrapper._mesh_injected_deps,
                    self.get_dependency,
                    wrapper_logger,
                )

                from ..tracing.execution_tracer import ExecutionTracer

                result = await ExecutionTracer.trace_function_execution_async(
                    func._mesh_original_func,
                    args,
                    final_kwargs,
                    dependencies,
                    mesh_positions,
                    injected_count,
                    wrapper_logger,
                )

                _log_wrapper_result(func, result, wrapper_logger)
                return result

        else:

            @functools.wraps(func)
            def dependency_wrapper(*args, **kwargs):
                final_kwargs, injected_count = _prepare_injection_kwargs(
                    func,
                    kwargs,
                    mesh_positions,
                    dependencies,
                    dependency_wrapper._mesh_injected_deps,
                    self.get_dependency,
                    wrapper_logger,
                )

                from ..tracing.execution_tracer import ExecutionTracer

                result = ExecutionTracer.trace_function_execution(
                    func._mesh_original_func,
                    args,
                    final_kwargs,
                    dependencies,
                    mesh_positions,
                    injected_count,
                    wrapper_logger,
                )

                _log_wrapper_result(func, result, wrapper_logger)
                return result

        # Override signature to hide injectable parameters from FastMCP schema generation
        clean_sig = _build_clean_signature(func)
        if clean_sig is not None:
            dependency_wrapper.__signature__ = clean_sig

        # Store dependency state on wrapper as array (indexed by position)
        dependency_wrapper._mesh_injected_deps = [None] * len(dependencies)

        # Add update method to wrapper (now uses index-based updates)
        def update_dependency(dep_index: int, instance: Any | None) -> None:
            """Called when a dependency changes (index-based for duplicate capability support)."""
            if dep_index < len(dependency_wrapper._mesh_injected_deps):
                dependency_wrapper._mesh_injected_deps[dep_index] = instance
                if instance is None:
                    wrapper_logger.debug(
                        f"Removed dependency at index {dep_index} from {func_id}"
                    )
                else:
                    wrapper_logger.debug(
                        f"Updated dependency at index {dep_index} for {func_id}"
                    )
                    wrapper_logger.debug(
                        f"🔗 Wrapper pointer receiving dependency: {dependency_wrapper} at {hex(id(dependency_wrapper))}"
                    )
            else:
                wrapper_logger.warning(
                    f"⚠️ Attempted to update dependency at index {dep_index} but wrapper only has {len(dependency_wrapper._mesh_injected_deps)} dependencies"
                )

        # Store update method on wrapper
        dependency_wrapper._mesh_update_dependency = update_dependency
        dependency_wrapper._mesh_dependencies = dependencies
        dependency_wrapper._mesh_positions = mesh_positions
        dependency_wrapper._mesh_original_func = func

        # Register this wrapper for dependency updates
        logger.debug(
            f"🔧 REGISTERING in function_registry: {func_id} -> {dependency_wrapper} at {hex(id(dependency_wrapper))}"
        )
        self._function_registry[func_id] = dependency_wrapper

        # Return the wrapper (which FastMCP will register)
        return dependency_wrapper


# Global injector instance
_global_injector = DependencyInjector()


def get_global_injector() -> DependencyInjector:
    """Get the global dependency injector instance."""
    return _global_injector
