"""Main container implementation for InjectQ dependency injection library."""

from __future__ import annotations

import logging
from contextlib import contextmanager
from typing import TYPE_CHECKING, Any

from injectq.utils import (
    BindingError,
    DependencyNotFoundError,
    ServiceFactory,
    ServiceKey,
)

from .registry import _UNSET, ServiceRegistry
from .resolver import DependencyResolver
from .scopes import ScopeManager, ScopeType
from .thread_safety import HybridLock


if TYPE_CHECKING:
    from collections.abc import Callable, Iterator

    from injectq.diagnostics import DependencyVisualizer


_logger = logging.getLogger("injectq.core")


class FactoryProxy:
    """Proxy object for managing factory bindings with dict-like interface."""

    def __init__(self, container: InjectQ) -> None:
        self._container = container

    def __setitem__(self, service_type: ServiceKey, factory: ServiceFactory) -> None:
        """Bind a factory function to a service type."""
        self._container.bind_factory(service_type, factory)

    def __getitem__(self, service_type: ServiceKey) -> ServiceFactory:
        """Get a factory function for a service type."""
        factory = self._container._registry.get_factory(service_type)  # noqa: SLF001
        if factory is None:
            msg = f"No factory registered for {service_type}"
            raise KeyError(msg)
        return factory

    def __delitem__(self, service_type: ServiceKey) -> None:
        """Remove a factory binding."""
        if not self._container._registry.remove_factory(service_type):  # noqa: SLF001
            msg = f"No factory registered for {service_type}"
            raise KeyError(msg)

    def __contains__(self, service_type: ServiceKey) -> bool:
        """Check if a factory is registered."""
        return self._container._registry.has_factory(service_type)  # noqa: SLF001


class InjectQ:
    """Main dependency injection container.

    Provides multiple API styles:
    - Dict-like interface: container[Type] = instance
    - Binding methods: container.bind(Type, Implementation)
    - Factory methods: container.factories[Type] = factory_func
    """

    _instance: InjectQ | None = None

    def __init__(
        self,
        modules: list[Any] | None = None,
        use_async_scopes: bool = True,
        thread_safe: bool = True,
        allow_override: bool = True,
    ) -> None:
        """Initialize the InjectQ container.

        Args:
            modules: List of modules to register
            use_async_scopes: Whether to use async scope management
            thread_safe: Whether to use thread-safe operations
            allow_override: Whether to allow overriding existing registrations
                          (default: True)
        """
        _logger.info(
            "Initializing InjectQ container "
            "(async_scopes=%s, thread_safe=%s, allow_override=%s)",
            use_async_scopes,
            thread_safe,
            allow_override,
        )
        self._allow_override = allow_override
        self._registry = ServiceRegistry()
        self._resolver = DependencyResolver(self._registry)

        # Choose scope manager based on async support requirement
        if use_async_scopes:
            from .async_scopes import create_enhanced_scope_manager

            self._scope_manager = create_enhanced_scope_manager()
        else:
            self._scope_manager = ScopeManager()

        self._resolver.scope_manager = self._scope_manager
        self._factories = FactoryProxy(self)

        # Thread safety support
        self._thread_safe = thread_safe
        if thread_safe:
            self._lock = HybridLock()
        else:
            self._lock = None

        # Install modules if provided
        if modules:
            for module in modules:
                self.install_module(module)

    @classmethod
    def get_instance(cls) -> InjectQ:
        """Returns the current instance of the InjectQ container.

        This method first attempts to retrieve the current container context using
        `ContainerContext.get_current()`. If a context is available, it returns that
        context. Otherwise, it checks if a singleton instance of the container exists;
        if not, it creates one and returns it.

        Returns:
            InjectQ: The current container instance, either from the context
            or as a singleton.
        """
        from .context import ContainerContext

        ctx = ContainerContext.get_current()
        if ctx is not None:
            return ctx

        if cls._instance is None:
            cls._instance = cls()
        return cls._instance

    @classmethod
    def reset_instance(cls) -> None:
        """Reset the global singleton instance (mainly for testing)."""
        cls._instance = None

    def _ensure_thread_safe(self, operation: Callable) -> Any:
        """Execute operation with thread safety if enabled."""
        if self._thread_safe and self._lock:
            with self._lock:
                return operation()
        else:
            return operation()

    # Dict-like interface
    def __setitem__(self, service_type: ServiceKey, implementation: Any) -> None:
        """Bind a service type to an implementation using dict syntax."""
        # Auto-detect if None should be allowed based on implementation value
        allow_none = implementation is None

        # If implementation is a class type, use bind() for proper validation
        # If implementation is an instance, use bind_instance()
        if isinstance(implementation, type):
            self._ensure_thread_safe(
                lambda: self.bind(
                    service_type,
                    implementation,
                    scope=ScopeType.SINGLETON,
                    allow_none=allow_none,
                    allow_concrete=True,
                )
            )
        else:
            self._ensure_thread_safe(
                lambda: self.bind_instance(
                    service_type,
                    implementation,
                    allow_none,
                    allow_concrete=True,
                )
            )

    def __getitem__(self, service_type: ServiceKey) -> Any:
        """Get a service instance using dict syntax."""
        _logger.debug("Getting service via __getitem__: %s", service_type)
        return self._ensure_thread_safe(lambda: self.get(service_type))

    def __delitem__(self, service_type: ServiceKey) -> None:
        """Remove a service binding using dict syntax."""

        def remove_binding() -> None:
            _logger.debug("Removing binding: %s", service_type)
            if not self._registry.remove_binding(service_type):
                msg = f"No binding registered for {service_type}"
                raise KeyError(msg)

        self._ensure_thread_safe(remove_binding)

    def __contains__(self, service_type: ServiceKey) -> bool:
        """Check if a service is registered."""
        _logger.debug("Checking if service registered: %s", service_type)
        return self._ensure_thread_safe(lambda: service_type in self._registry)

    def bind(
        self,
        service_type: ServiceKey,
        implementation: Any = _UNSET,
        scope: str | ScopeType = ScopeType.SINGLETON,
        to: Any = None,
        allow_none: bool = False,
        allow_concrete: bool = True,
    ) -> None:
        """Bind a service type to an implementation.

        Args:
            service_type: The service type or key to bind
            implementation: The implementation (class, instance, or factory)
            scope: The scope for the service
            to: Alternative parameter for implementation (fluent API)
            allow_none: Whether to allow None as a valid implementation
            allow_concrete: Whether to auto-register concrete types when
                          registering instances (default: True)
        """
        scope_str = scope.value if isinstance(scope, ScopeType) else scope
        _logger.debug(
            "Binding service: %s -> %s (scope: %s)",
            service_type,
            implementation if implementation is not _UNSET else "self",
            scope_str,
        )
        self._ensure_thread_safe(
            lambda: self._registry.bind(
                service_type,
                implementation,
                scope,
                to,
                allow_none,
                allow_concrete,
                self._allow_override,
            )
        )

    def bind_instance(
        self,
        service_type: ServiceKey,
        instance: Any,
        allow_none: bool = False,
        allow_concrete: bool = True,
    ) -> None:
        """Bind a service type to a specific instance.

        Args:
            service_type: The service type or key to bind
            instance: The instance to bind
            allow_none: Whether to allow None as a valid instance
            allow_concrete: Whether to auto-register concrete types when
                          registering instances (default: True)
        """
        instance_type = type(instance).__name__ if instance else "None"
        _logger.debug("Binding instance: %s (%s)", service_type, instance_type)
        self._ensure_thread_safe(
            lambda: self._registry.bind_instance(
                service_type,
                instance,
                allow_none,
                allow_concrete,
                self._allow_override,
            )
        )

    def bind_factory(
        self,
        service_type: ServiceKey,
        factory: ServiceFactory,
    ) -> None:
        """Bind a service type to a factory function.

        Args:
            service_type: The service type or key to bind
            factory: The factory function
            allow_concrete: Whether to auto-register concrete types when
                          registering instances (default: True)
        """
        factory_name = (
            factory.__name__ if hasattr(factory, "__name__") else str(factory)
        )
        _logger.debug("Binding factory: %s -> %s", service_type, factory_name)
        self._ensure_thread_safe(
            lambda: self._registry.bind_factory(
                service_type,
                factory,
                self._allow_override,
            )
        )

    @property
    def factories(self) -> FactoryProxy:
        """Get the factory proxy for dict-like factory bindings."""
        return self._factories

    # Resolution methods
    def get(self, service_type: ServiceKey) -> Any:
        """Get a service instance."""
        _logger.debug("Resolving service: %s", service_type)
        return self._ensure_thread_safe(lambda: self._resolver.resolve(service_type))

    async def aget(self, service_type: ServiceKey) -> Any:
        """Get a service instance asynchronously."""
        _logger.debug("Resolving service async: %s", service_type)
        return await self._ensure_thread_safe(
            lambda: self._resolver.resolve_async(service_type)
        )

    async def atry_get(self, service_type: ServiceKey, default: Any = None) -> Any:
        """Try to get a service instance, returning default if not found."""

        async def try_resolve() -> Any:
            try:
                return await self.aget(service_type)
            except DependencyNotFoundError:
                return default

        return await try_resolve()

    def try_get(self, service_type: ServiceKey, default: Any = None) -> Any:
        """Try to get a service instance, returning default if not found."""

        def try_resolve() -> Any:
            try:
                return self.get(service_type)
            except DependencyNotFoundError:
                return default

        return try_resolve()

    def has(self, service_type: ServiceKey) -> bool:
        """Check if a service type can be resolved."""
        return self._ensure_thread_safe(lambda: service_type in self._registry)

    def get_factory(self, service_type: ServiceKey) -> ServiceFactory:
        """Get the raw factory function without invoking it.

        This method returns the factory function itself, allowing you to call it
        with custom arguments instead of relying on dependency injection.

        Args:
            service_type: The service type or key for the factory

        Returns:
            The factory function

        Raises:
            DependencyNotFoundError: If no factory is registered for the service type

        Example:
            >>> injector.bind_factory("data_store", lambda key: data[key])
            >>> factory = injector.get_factory("data_store")
            >>> result = factory("key1")  # Call with custom argument
        """

        def _get_factory() -> ServiceFactory:
            factory = self._registry.get_factory(service_type)
            if factory is None:
                raise DependencyNotFoundError(service_type)  # type: ignore  # noqa: PGH003
            return factory

        return self._ensure_thread_safe(_get_factory)

    def call_factory(self, service_type: ServiceKey, *args: Any, **kwargs: Any) -> Any:
        """Get and call a factory function with custom arguments.

        This is a convenience method that combines get_factory() and calling it
        in a single step. It allows you to invoke a factory with your own arguments
        instead of relying on dependency injection.

        Args:
            service_type: The service type or key for the factory
            *args: Positional arguments to pass to the factory
            **kwargs: Keyword arguments to pass to the factory

        Returns:
            The result of calling the factory function

        Raises:
            DependencyNotFoundError: If no factory is registered for the service type

        Example:
            >>> injector.bind_factory("data_store", lambda key: data[key])
            >>> result = injector.call_factory("data_store", "key1")
        """
        factory = self.get_factory(service_type)
        return factory(*args, **kwargs)

    async def aget_factory(self, service_type: ServiceKey) -> ServiceFactory:
        """Get the raw factory function without invoking it (async version).

        This is the async equivalent of get_factory(). It returns the factory
        function itself, allowing you to call it with custom arguments.

        Args:
            service_type: The service type or key for the factory

        Returns:
            The factory function

        Raises:
            DependencyNotFoundError: If no factory is registered for the service type

        Example:
            >>> injector.bind_factory("data_store", lambda key: data[key])
            >>> factory = await injector.aget_factory("data_store")
            >>> result = factory("key1")  # or await factory("key1") if async
        """
        return self.get_factory(service_type)  # Factory lookup is sync

    async def acall_factory(
        self, service_type: ServiceKey, *args: Any, **kwargs: Any
    ) -> Any:
        """Get and call a factory function with custom arguments (async version).

        This is the async equivalent of call_factory(). It gets the factory
        and calls it, handling both sync and async factories appropriately.

        Args:
            service_type: The service type or key for the factory
            *args: Positional arguments to pass to the factory
            **kwargs: Keyword arguments to pass to the factory

        Returns:
            The result of calling the factory function

        Raises:
            DependencyNotFoundError: If no factory is registered for the service type

        Example:
            >>> injector.bind_factory("data_store", async_loader)
            >>> result = await injector.acall_factory("data_store", "key1")
        """
        import asyncio

        factory = await self.aget_factory(service_type)
        if asyncio.iscoroutinefunction(factory):
            return await factory(*args, **kwargs)
        return factory(*args, **kwargs)

    def invoke(self, service_type: ServiceKey, *args: Any, **kwargs: Any) -> Any:
        """Invoke a factory with custom args, injecting remaining dependencies.

        This is a hybrid method that combines manual argument provision with
        automatic dependency injection. You provide some arguments explicitly,
        and the container injects the rest based on type hints.

        Note: Use keyword arguments for explicit parameters to avoid ambiguity.
        Positional args are matched in order and may not trigger DI as expected.

        Injection Strategy:
        1. Parameters provided via args/kwargs are used as-is
        2. Try to inject by parameter name (string key)
        3. Try to inject by type annotation (only for non-primitive types)
        4. Use default value if available
        5. Raise error if required param cannot be resolved

        Args:
            service_type: The service type or key for the factory
            *args: Positional arguments to pass to the factory
            **kwargs: Keyword arguments to pass to the factory

        Returns:
            The result of calling the factory with mixed args

        Raises:
            DependencyNotFoundError: If no factory is registered or a required
                                   dependency cannot be resolved

        Example:
            >>> # Factory signature: create_service(db: Database, user_id: str)
            >>> injector.bind_factory("service", create_service)
            >>> # Provide user_id, db will be injected automatically
            >>> service = injector.invoke("service", user_id="123")
        """
        import inspect

        factory = self.get_factory(service_type)
        sig = inspect.signature(factory)

        # Build complete kwargs dict with DI for missing params
        complete_kwargs = dict(kwargs)

        # Track which params are already provided by positional args
        param_list = list(sig.parameters.keys())
        provided_by_position = set(param_list[: len(args)])

        # Primitive types that we shouldn't auto-inject by type
        # (to avoid ambiguous injections like int, str, bool)
        primitive_types = (str, int, float, bool, bytes, type(None))

        # Inject missing dependencies
        for param_name, param in sig.parameters.items():
            # Skip if already provided via args or kwargs
            if param_name in provided_by_position or param_name in complete_kwargs:
                continue

            # Try to inject this parameter
            injected = False
            try:
                # First try by parameter name (string key)
                if self.has(param_name):
                    complete_kwargs[param_name] = self.get(param_name)
                    injected = True
                # Then try by type annotation (but not for primitive types)
                elif (
                    param.annotation != inspect.Parameter.empty
                    and param.annotation not in primitive_types
                ):
                    if self.has(param.annotation):
                        complete_kwargs[param_name] = self.get(param.annotation)
                        injected = True
            except DependencyNotFoundError:
                pass

            # If not injected and required, raise error
            if (
                not injected
                and param.default == inspect.Parameter.empty
                and param.kind
                not in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD)
            ):
                raise DependencyNotFoundError(param.annotation)  # type: ignore  # noqa: PGH003

        # Call the factory with positional args and complete kwargs
        return factory(*args, **complete_kwargs)

    async def ainvoke(self, service_type: ServiceKey, *args: Any, **kwargs: Any) -> Any:
        """Invoke a factory with custom args, injecting remaining dependencies (async).

        This is the async equivalent of invoke(). It combines manual argument
        provision with automatic dependency injection, and handles both sync
        and async factories appropriately.

        Note: Use keyword arguments for explicit parameters to avoid ambiguity.
        Positional args are matched in order and may not trigger DI as expected.

        Injection Strategy:
        1. Parameters provided via args/kwargs are used as-is
        2. Try to inject by parameter name (string key)
        3. Try to inject by type annotation (only for non-primitive types)
        4. Use default value if available
        5. Raise error if required param cannot be resolved

        Args:
            service_type: The service type or key for the factory
            *args: Positional arguments to pass to the factory
            **kwargs: Keyword arguments to pass to the factory

        Returns:
            The result of calling the factory with mixed args

        Raises:
            DependencyNotFoundError: If no factory is registered or a required
                                   dependency cannot be resolved

        Example:
            >>> # Factory: async def create_service(db: Database, user_id: str)
            >>> injector.bind_factory("service", create_service)
            >>> service = await injector.ainvoke("service", user_id="123")
        """
        import asyncio
        import inspect

        factory = await self.aget_factory(service_type)
        sig = inspect.signature(factory)

        # Build complete kwargs dict with DI for missing params
        complete_kwargs = dict(kwargs)

        # Track which params are already provided by positional args
        param_list = list(sig.parameters.keys())
        provided_by_position = set(param_list[: len(args)])

        # Primitive types that we shouldn't auto-inject by type
        primitive_types = (str, int, float, bool, bytes, type(None))

        # Inject missing dependencies
        for param_name, param in sig.parameters.items():
            # Skip if already provided via args or kwargs
            if param_name in provided_by_position or param_name in complete_kwargs:
                continue

            # Try to inject this parameter
            injected = False
            try:
                # First try by parameter name (string key)
                if self.has(param_name):
                    complete_kwargs[param_name] = await self.aget(param_name)
                    injected = True
                # Then try by type annotation (but not for primitive types)
                elif (
                    param.annotation != inspect.Parameter.empty
                    and param.annotation not in primitive_types
                ):
                    if self.has(param.annotation):
                        complete_kwargs[param_name] = await self.aget(param.annotation)
                        injected = True
            except DependencyNotFoundError:
                pass

            # If not injected and required, raise error
            if (
                not injected
                and param.default == inspect.Parameter.empty
                and param.kind
                not in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD)
            ):
                raise DependencyNotFoundError(param.annotation)  # type: ignore  # noqa: PGH003

        # Call the factory with positional args and complete kwargs
        if asyncio.iscoroutinefunction(factory):
            return await factory(*args, **complete_kwargs)
        return factory(*args, **complete_kwargs)

    # Scope management
    def scope(self, scope_name: str | ScopeType) -> Any:
        """Enter a scope context."""
        if isinstance(scope_name, ScopeType):
            scope_name = scope_name.value
        return self._scope_manager.scope_context(scope_name)

    def async_scope(self, scope_name: str | ScopeType) -> Any:
        """Enter an async scope context."""
        if isinstance(scope_name, ScopeType):
            scope_name = scope_name.value
        # Check if scope manager supports async contexts
        if hasattr(self._scope_manager, "async_scope_context"):
            return self._scope_manager.async_scope_context(scope_name)  # type: ignore  # noqa: PGH003
        # Fallback to regular scope context
        return self._scope_manager.scope_context(scope_name)

    def clear_scope(self, scope_name: str | ScopeType) -> None:
        """Clear all instances in a scope."""
        if isinstance(scope_name, ScopeType):
            scope_name = scope_name.value
        self._ensure_thread_safe(lambda: self._scope_manager.clear_scope(scope_name))

    def clear_all_scopes(self) -> None:
        """Clear all instances in all scopes."""
        self._ensure_thread_safe(lambda: self._scope_manager.clear_all_scopes())

    # Context management for multi-container support
    @contextmanager
    def context(self) -> Iterator[None]:
        """Use this container as the active context for dependency resolution.

        This allows the container to be used without the global singleton pattern.

        Example:
            container = InjectQ()
            with container.context():
                # Dependencies resolved using this container
                my_function()
        """
        from .context import ContainerContext

        old_container = ContainerContext.get_current()
        ContainerContext.set_current(self)
        try:
            yield
        finally:
            if old_container is not None:
                ContainerContext.set_current(old_container)
            else:
                ContainerContext.clear_current()

    def activate(self) -> None:
        """Activate this container as the current default context.

        This sets the container as the active context for all subsequent
        dependency resolution calls that don't specify a container explicitly.

        Note: Use context() manager for temporary activation instead.
        """
        from .context import ContainerContext

        ContainerContext.set_current(self)

    # Module installation
    def install_module(self, module: Any) -> None:
        """Install a module into the container."""

        def install() -> None:
            module_name = getattr(module, "__name__", str(module))
            _logger.info("Installing module: %s", module_name)
            if hasattr(module, "configure"):
                binder = ModuleBinder(self)
                module.configure(binder)
            else:
                msg = f"Module {module} does not have a configure method"
                raise BindingError(msg)

        self._ensure_thread_safe(install)

    # Validation and diagnostics
    def validate(self) -> None:
        """Validate all dependencies for consistency and resolvability."""

        def validate() -> None:
            _logger.info("Validating container dependencies")
            self._registry.validate()
            self._resolver.validate_dependencies()

        self._ensure_thread_safe(validate)

    def get_dependency_graph(self) -> dict[ServiceKey, list[ServiceKey]]:
        """Get the dependency graph for all registered services."""
        return self._ensure_thread_safe(lambda: self._resolver.get_dependency_graph())

    def visualize_dependencies(self) -> DependencyVisualizer:
        """Get a dependency visualizer for this container."""
        from injectq.diagnostics import DependencyVisualizer

        return DependencyVisualizer(self)

    def compile(self) -> None:
        """Pre-compile dependency graphs for performance optimization."""

        def compile_dependencies() -> None:
            # Pre-resolve dependency graphs and cache resolution plans
            self._resolver.compile_resolution_plans()

        self._ensure_thread_safe(compile_dependencies)

    # Cleanup methods
    def clear(self) -> None:
        """Clear all bindings and cached instances."""

        def clear() -> None:
            self._registry.clear()
            self.clear_all_scopes()

        self._ensure_thread_safe(clear)

    def __repr__(self) -> str:
        """String representation of the container."""
        return (
            f"InjectQ(services={len(self._registry)}, thread_safe={self._thread_safe})"
        )

    # Testing support
    @contextmanager
    def override(self, service_type: ServiceKey, override_value: Any) -> Iterator[None]:
        """Temporarily override a service binding for testing."""

        def setup_override() -> Any:
            # Store original binding
            original_binding = self._registry.get_binding(service_type)
            original_factory = self._registry.get_factory(service_type)
            return original_binding, original_factory

        def restore_override(
            original_binding: Any,
            original_factory: Any,
        ) -> None:
            # Clear cached instances again before restoring
            self._scope_manager.clear_scope("singleton")
            # Restore original binding
            self._registry.remove_binding(service_type)
            if original_factory:
                self._registry.bind_factory(service_type, original_factory)
            elif original_binding:
                self._registry._bindings[service_type] = original_binding  # noqa: SLF001

        original_binding, original_factory = self._ensure_thread_safe(setup_override)

        try:
            # Clear any cached instances for this service type
            self._ensure_thread_safe(
                lambda: self._scope_manager.clear_scope("singleton")
            )
            # Set override
            self._ensure_thread_safe(
                lambda: self.bind_instance(service_type, override_value)
            )
            yield
        finally:
            self._ensure_thread_safe(
                lambda: restore_override(original_binding, original_factory)
            )

    @classmethod
    @contextmanager
    def test_mode(cls) -> Iterator[InjectQ]:
        """Create a temporary container for testing."""
        original_instance = cls._instance
        try:
            cls._instance = None  # Force new instance
            test_container = cls()
            cls._instance = test_container
            yield test_container
        finally:
            cls._instance = original_instance


class ModuleBinder:
    """Binder interface for modules to configure the container."""

    def __init__(self, container: InjectQ) -> None:
        self._container = container

    def bind(
        self,
        service_type: ServiceKey,
        implementation: Any = None,
        scope: str | ScopeType = ScopeType.SINGLETON,
        to: Any = None,
    ) -> None:
        """Bind a service type to an implementation."""
        self._container.bind(service_type, implementation, scope, to)

    def bind_instance(self, service_type: ServiceKey, instance: Any) -> None:
        """Bind a service type to a specific instance."""
        self._container.bind_instance(service_type, instance)

    def bind_factory(self, service_type: ServiceKey, factory: ServiceFactory) -> None:
        """Bind a service type to a factory function."""
        self._container.bind_factory(service_type, factory)
