import typing as t

from ellar.di.constants import (
    INJECTABLE_ATTRIBUTE,
    INJECTABLE_WATERMARK,
    Tag,
)
from ellar.di.exceptions import DIImproperConfiguration
from ellar.di.types import T
from ellar.reflect import fail_silently, reflect
from injector import (
    CallableT,
    ConstructorOrClassT,
    Scope,
    ScopeDecorator,
    SingletonScope,
    inject,
)
from injector import (
    is_decorated_with_inject as injector_is_decorated_with_inject,
)

if t.TYPE_CHECKING:  # pragma: no cover
    from .injector import Container

__all__ = (
    "ProviderConfig",
    "injectable",
    "is_decorated_with_injectable",
    "get_scope",
    "has_binding",
    "InjectByTag",
)


class ProviderConfig(t.Generic[T]):
    """
    ProviderConfig is a class that configures a provider for a service.

    Example:

    >>> class SomeClass:
    ...     def __init__(self, a: InjectByTag('A'), b: AnotherType):
    ...         self.a = a
    ...         self.b = b

    >>> provider_config = ProviderConfig(SomeClass)
    >>> provider_config.register(container)

    Example with string:

    >>> provider_config = ProviderConfig('path.to:SomeClass')
    >>> provider_config.register(container)

    Example with value:

    >>> provider_config = ProviderConfig(SomeClass, use_value=SomeValue)
    >>> provider_config.register(container)

    Example with class:

    >>> provider_config = ProviderConfig(SomeClass, use_class=AnotherType)
    >>> provider_config.register(container)

    Example with use_class as string:

    >>> provider_config = ProviderConfig(SomeClass, use_class='path.to:AnotherType')
    >>> provider_config.register(container)

    Example with scope:

    >>> provider_config = ProviderConfig(SomeClass, scope=request_scope)
    >>> provider_config.register(container)

    Example with tag (supports both callable and generic syntax):

    >>> provider_config = ProviderConfig(SomeClass, tag='some_tag')
    >>> provider_config.register(container)
    >>> instance = container.get(InjectByTag('some_tag'))  # Callable syntax
    >>> # or: instance = container.get(InjectByTag[T('some_tag')])  # Generic syntax
    >>> assert isinstance(instance, SomeClass)


    """

    __slots__ = (
        "base_type",
        "use_value",
        "use_class",
        "scope",
        "tag",
        "export",
        "core",
    )

    def __init__(
        self,
        base_type: t.Union[t.Type[T], t.Type, str],
        *,
        use_value: t.Optional[T] = None,
        use_class: t.Union[t.Type[T], t.Any] = None,
        scope: t.Optional[t.Union[t.Type[Scope], t.Any]] = None,
        tag: t.Optional[str] = None,
        export: bool = False,
        core: bool = False,
    ):
        self.scope = scope or SingletonScope
        if use_value and use_class:
            raise DIImproperConfiguration(
                "`use_class` and `use_value` can not be used at the same time."
            )

        self.base_type = base_type
        self.use_value = use_value
        self.use_class = use_class
        self.tag = tag
        self.export = export
        self.core = core

    def __repr__(self) -> str:
        """Developer-friendly representation showing all configuration details"""
        parts = [f"base_type={self._type_repr(self.base_type)}"]

        if self.use_value is not None:
            parts.append(f"use_value={self._type_repr(self.use_value)}")
        if self.use_class is not None:
            parts.append(f"use_class={self._type_repr(self.use_class)}")
        if self.scope != SingletonScope:
            parts.append(f"scope={self._type_repr(self.scope)}")
        if self.tag:
            parts.append(f"tag={self.tag!r}")
        if self.export:
            parts.append("export=True")
        if self.core:
            parts.append("core=True")

        return f"ProviderConfig({', '.join(parts)})"

    def __str__(self) -> str:
        """User-friendly string representation"""
        base = self._type_name(self.base_type)

        if self.use_class:
            impl = self._type_name(self.use_class)
            desc = f"{base} -> {impl}"
        elif self.use_value is not None:
            desc = f"{base} -> <value>"
        else:
            desc = base

        if self.tag:
            desc = f"{desc} [tag:{self.tag}]"

        return desc

    @staticmethod
    def _type_repr(obj: t.Any) -> str:
        """Get a repr-style string for a type or value"""
        if isinstance(obj, type):
            return (
                f"{obj.__module__}.{obj.__qualname__}"
                if hasattr(obj, "__module__")
                else obj.__qualname__
            )
        elif isinstance(obj, str):
            return repr(obj)
        else:
            return repr(obj)

    @staticmethod
    def _type_name(obj: t.Any) -> str:
        """Get a simple name for a type or value"""
        if isinstance(obj, type):
            return obj.__qualname__ if hasattr(obj, "__qualname__") else obj.__name__
        elif isinstance(obj, str):
            return obj
        else:
            return type(obj).__name__

    def get_type(self) -> t.Type:
        return self._resolve_type(self.base_type)

    def get_use_class(self) -> t.Type:
        return self._resolve_type(self.use_class)

    def _resolve_type(self, type_or_str: t.Union[t.Type, str]) -> t.Type:
        from ellar.utils.importer import import_from_string

        if isinstance(type_or_str, str):
            return t.cast(t.Type, import_from_string(type_or_str))
        return type_or_str

    def register(self, container: "Container") -> None:
        base_type = self.get_type()
        scope = get_scope(base_type) or self.scope
        use_class = self.get_use_class()

        if use_class:
            scope = get_scope(use_class) or scope
            container.register(
                base_type=base_type,
                concrete_type=use_class,
                scope=scope,
                tag=self.tag,
            )
        elif self.use_value:
            container.register(
                base_type=base_type,
                concrete_type=self.use_value,
                scope=scope,
                tag=self.tag,
            )
        elif not isinstance(base_type, type):
            raise DIImproperConfiguration(
                f"couldn't determine provider setup for {base_type}. "
                f"Please use `ProviderConfig` or `register_services` function in a "
                f"Module to configure the provider"
            )
        else:
            container.register(base_type=base_type, scope=scope, tag=self.tag)


@t.overload
def injectable(
    scope: t.Union[t.Type[Scope], ScopeDecorator, CallableT] = SingletonScope,
) -> t.Union[CallableT, t.Callable[[t.Any], CallableT]]:  # pragma: no cover
    pass


@t.overload
def injectable(
    scope: t.Union[t.Type[Scope], ScopeDecorator, t.Type[T]] = SingletonScope,
) -> t.Union[t.Type[T], t.Callable[[t.Any], t.Type[T]]]:  # pragma: no cover
    pass


def injectable(
    scope: t.Union[t.Type[Scope], ScopeDecorator, ConstructorOrClassT] = SingletonScope,
) -> t.Union[ConstructorOrClassT, t.Callable[[t.Any], ConstructorOrClassT]]:
    """Decorates a callable or Type with inject and Defines Type or callable scope injection scope

    Example use:

    >>> @injectable # default is 'singleton_scope'
    ... class InjectableType2:
    ...     def __init__(self):
    ...         pass
    >>>
    >>> @injectable(scope=request_scope)
    ... class InjectableType3:
    ...     def __init__(self):
    ...         pass
    """

    def _decorator(func_or_class: ConstructorOrClassT) -> ConstructorOrClassT:
        fail_silently(inject, constructor_or_class=func_or_class)
        setattr(func_or_class, INJECTABLE_ATTRIBUTE, scope)

        reflect.define_metadata(INJECTABLE_WATERMARK, True, func_or_class)

        return func_or_class

    if not (
        isinstance(scope, ScopeDecorator)
        or isinstance(scope, type)
        and issubclass(scope, Scope)
    ):
        func_ = scope
        scope = SingletonScope
        return _decorator(func_)

    return _decorator


def is_decorated_with_injectable(func_or_class: ConstructorOrClassT) -> bool:
    """See if given Type or Call is declared with a scope.

    Example use:
    >>> class NonInjectableType:
    ...     def __init__(self):
    ...         pass

    >>> @injectable
    ... class InjectableType2:
    ...     def __init__(self):
    ...         pass

    >>> is_decorated_with_injectable(NonInjectableType)
    True
    >>>
    >>> is_decorated_with_injectable(InjectableType2)
    True
    """

    return hasattr(func_or_class, INJECTABLE_ATTRIBUTE)


def has_binding(func_or_class: ConstructorOrClassT) -> bool:
    """See if given a Type __init__ or callable has __binding__."""
    if isinstance(func_or_class, type) and hasattr(func_or_class, "__init__"):
        return injector_is_decorated_with_inject(func_or_class.__init__)  # type: ignore[misc]
    return injector_is_decorated_with_inject(func_or_class)


def get_scope(
    func_or_class: ConstructorOrClassT,
) -> t.Optional[t.Union[t.Type[Scope], ScopeDecorator]]:
    """Get scope declared scope if available in a type or callable"""
    return t.cast(
        t.Optional[t.Union[t.Type[Scope], ScopeDecorator]],
        getattr(
            func_or_class,
            INJECTABLE_ATTRIBUTE,
            getattr(func_or_class, "__scope__", None),
        ),
    )


class _InjectByTagClass:
    """
    Inject a provider/service by tag.

    Supports both callable and generic syntax:
    - InjectByTag('A')  # callable syntax
    - InjectByTag['A']  # generic syntax

    For example:

    class A:
        name = 'A'

    class AnotherType:
        name = 'AnotherType'

    class SomeClass:
        def __init__(self, a: InjectByTag('A'), b: AnotherType):
            self.a = a
            self.b = b

    # Or using generic syntax:
    class SomeClass:
        def __init__(self, a: InjectByTag['A'], b: AnotherType):
            self.a = a
            self.b = b

    injector = EllarInjector()
    injector.container.register_exact_scoped(A, tag='A')
    injector.container.register_exact_scoped(AnotherType)
    injector.container.register_exact_scoped(SomeClass)

    instance = injector.get(SomeClass)
    assert instance.a.name == 'A'
    assert instance.b.name == 'AnotherType'
    """

    def __call__(self, tag: str) -> t.Any:
        """
        Callable syntax: InjectByTag('tag_name')

        :param tag: Registered Provider/Service tag name
        :return: A NewType for tagged dependency injection
        """
        return t.NewType(Tag(tag), Tag)

    def __getitem__(self, tag: str) -> t.Any:
        """
        Generic syntax: InjectByTag['tag_name']

        :param tag: Registered Provider/Service tag name
        :return: A NewType for tagged dependency injection
        """
        return t.NewType(Tag(tag), Tag)

    def __repr__(self) -> str:
        """Return a string representation for debugging"""
        return "InjectByTag"


# Create singleton instance to support both syntaxes
InjectByTag = _InjectByTagClass()
