"""
Caprail IoC Service Container.

Inspired by Laravel's service container — supports binding, singletons,
instances, contextual resolution, and tagging.
"""
from __future__ import annotations

import inspect
from collections.abc import Callable
from typing import Any, TypeVar

T = TypeVar("T")


class BindingResolutionError(Exception):
    """Raised when the container cannot resolve a binding."""


class Container:
    """
    The Caprail IoC service container.

    Manages class bindings, singletons, and dependency resolution.
    """

    def __init__(self) -> None:
        self._bindings: dict[str, dict[str, Any]] = {}
        self._instances: dict[str, Any] = {}
        self._aliases: dict[str, str] = {}
        self._tags: dict[str, list[str]] = {}

    # ------------------------------------------------------------------
    # Binding
    # ------------------------------------------------------------------

    def bind(
        self,
        abstract: str | type,
        concrete: Callable[..., Any] | None = None,
        shared: bool = False,
    ) -> None:
        """Register a binding in the container."""
        key = self._key(abstract)
        if concrete is None:
            concrete = abstract if callable(abstract) else None
        self._bindings[key] = {"concrete": concrete, "shared": shared}

    def singleton(self, abstract: str | type, concrete: Callable[..., Any] | None = None) -> None:
        """Register a shared (singleton) binding."""
        self.bind(abstract, concrete, shared=True)

    def instance(self, abstract: str | type, value: Any) -> None:
        """Register an existing object as a singleton instance."""
        key = self._key(abstract)
        self._instances[key] = value

    def alias(self, abstract: str | type, alias: str) -> None:
        """Alias an abstract type to a shorter name."""
        self._aliases[alias] = self._key(abstract)

    def tag(self, abstracts: list[str | type], tag: str) -> None:
        """Tag multiple abstracts under a single label."""
        self._tags.setdefault(tag, [])
        for abstract in abstracts:
            self._tags[tag].append(self._key(abstract))

    # ------------------------------------------------------------------
    # Resolution
    # ------------------------------------------------------------------

    def make(self, abstract: str | type, parameters: dict[str, Any] | None = None) -> Any:
        """Resolve a binding from the container."""
        key = self._resolve_alias(self._key(abstract))

        if key in self._instances:
            return self._instances[key]

        binding = self._bindings.get(key)
        if binding is None:
            return self._build(abstract, parameters or {})

        concrete = binding["concrete"]
        obj = self._build(concrete, parameters or {})

        if binding["shared"]:
            self._instances[key] = obj

        return obj

    def tagged(self, tag: str) -> list[Any]:
        """Resolve all abstracts registered under a tag."""
        return [self.make(abstract) for abstract in self._tags.get(tag, [])]

    def bound(self, abstract: str | type) -> bool:
        """Determine if the given abstract has a binding."""
        key = self._key(abstract)
        return key in self._bindings or key in self._instances

    def resolved(self, abstract: str | type) -> bool:
        """Determine if the given abstract has been resolved."""
        return self._key(abstract) in self._instances

    def __getitem__(self, abstract: str | type) -> Any:
        return self.make(abstract)

    def __setitem__(self, abstract: str | type, concrete: Any) -> None:
        self.instance(abstract, concrete)

    def __contains__(self, abstract: str | type) -> bool:
        return self.bound(abstract)

    # ------------------------------------------------------------------
    # Internal helpers
    # ------------------------------------------------------------------

    def _build(self, concrete: Any, parameters: dict[str, Any]) -> Any:
        if concrete is None:
            raise BindingResolutionError("Concrete is None — cannot build.")

        if not callable(concrete):
            return concrete

        if isinstance(concrete, type):
            return self._instantiate(concrete, parameters)

        # Plain callable / factory
        return concrete(self)

    def _instantiate(self, cls: type, parameters: dict[str, Any]) -> Any:
        try:
            sig = inspect.signature(cls.__init__)
        except (ValueError, TypeError):
            return cls()

        kwargs: dict[str, Any] = {}
        for name, param in sig.parameters.items():
            if name == "self":
                continue
            if param.kind in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD):
                continue
            if name in parameters:
                kwargs[name] = parameters[name]
                continue
            annotation = param.annotation
            if annotation is inspect.Parameter.empty:
                if param.default is not inspect.Parameter.empty:
                    continue
                raise BindingResolutionError(
                    f"Cannot resolve parameter '{name}' of {cls.__name__} — "
                    "no type hint and no default value."
                )
            try:
                kwargs[name] = self.make(annotation)
            except BindingResolutionError:
                if param.default is not inspect.Parameter.empty:
                    continue
                raise

        return cls(**kwargs)

    @staticmethod
    def _key(abstract: str | type) -> str:
        if isinstance(abstract, str):
            return abstract
        return f"{abstract.__module__}.{abstract.__qualname__}"

    def _resolve_alias(self, key: str) -> str:
        return self._aliases.get(key, key)
