"""Component metadata and container assembly."""

import inspect
from collections.abc import Callable, Iterable
from dataclasses import dataclass, field
from enum import Enum
from types import ModuleType, UnionType
from typing import Annotated, Any, Union, get_args, get_origin, get_type_hints

from punq import Container, Scope  # type: ignore[import-untyped]

_COMPONENT_DEFINITION_ATTR = "__iisi_component_definition__"
_PORT_DEFINITION_ATTR = "__iisi_port_definition__"
_MISSING = object()
_VALIDATED_COMPONENT_KINDS = {
    "application",
    "driven_adapter",
    "driving_adapter",
}


class ComponentKind(str, Enum):
    COMPONENT = "component"
    DRIVING_ADAPTER = "driving_adapter"
    DRIVEN_ADAPTER = "driven_adapter"
    APPLICATION = "application"
    SEED = "seed"


class PortKind(str, Enum):
    DRIVING = "driving_port"
    DRIVEN = "driven_port"


class ComponentLifetime(str, Enum):
    TRANSIENT = "transient"
    SINGLETON = "singleton"


@dataclass(slots=True, frozen=True)
class PortDefinition:
    port: type[Any]
    kind: PortKind
    module: str = ""
    qualname: str = ""

    def __post_init__(self) -> None:
        object.__setattr__(self, "module", self.module or self.port.__module__)
        object.__setattr__(self, "qualname", self.qualname or self.port.__qualname__)


@dataclass(slots=True, frozen=True)
class ComponentDefinition:
    implementation: type[Any]
    interface: type[Any]
    port: type[Any] | None = None
    kind: ComponentKind = ComponentKind.COMPONENT
    lifetime: ComponentLifetime = ComponentLifetime.TRANSIENT
    module: str = ""
    qualname: str = ""

    def __post_init__(self) -> None:
        object.__setattr__(self, "module", self.module or self.implementation.__module__)
        object.__setattr__(self, "qualname", self.qualname or self.implementation.__qualname__)


class DuplicateComponentError(ValueError):
    """Raised when the same component is registered with conflicting metadata."""


class PortsAndAdaptersViolationError(ValueError):
    """Raised when component wiring violates the ports-and-adapters constraints."""


def infer_interface(target: type[Any]) -> type[Any]:
    for base in target.__bases__:
        if base is object:
            continue
        if isinstance(base, type) and getattr(base, "_is_protocol", False):
            return base
        return base
    return target


def component_definition_for(target: type[Any]) -> ComponentDefinition | None:
    """Return component metadata for a class."""

    value = getattr(target, _COMPONENT_DEFINITION_ATTR, None)
    if isinstance(value, ComponentDefinition):
        return value
    return None


def port_definition_for(target: type[Any]) -> PortDefinition | None:
    """Return port metadata for a class or protocol."""

    value = getattr(target, _PORT_DEFINITION_ATTR, None)
    if isinstance(value, PortDefinition):
        return value
    return None


def _decorate_port(target: type[Any], *, kind: PortKind) -> type[Any]:
    definition = PortDefinition(port=target, kind=kind)
    existing = port_definition_for(target)
    if existing is not None and existing != definition:
        raise PortsAndAdaptersViolationError(f"Port '{target.__qualname__}' already has port metadata.")
    setattr(target, _PORT_DEFINITION_ATTR, definition)
    return target


def _decorate_component(
    target: type[Any],
    *,
    interface: type[Any] | None,
    port: type[Any] | None,
    kind: ComponentKind,
    lifetime: ComponentLifetime,
) -> type[Any]:
    definition = ComponentDefinition(
        implementation=target,
        interface=interface or infer_interface(target),
        port=port,
        kind=kind,
        lifetime=lifetime,
    )
    existing = component_definition_for(target)
    if existing is not None and existing != definition:
        raise DuplicateComponentError(f"Component '{target.__qualname__}' already has component metadata.")
    setattr(target, _COMPONENT_DEFINITION_ATTR, definition)
    return target


def register(
    cls: type[Any] | None = None,
    *,
    interface: type[Any] | None = None,
    kind: ComponentKind = ComponentKind.COMPONENT,
    lifetime: ComponentLifetime = ComponentLifetime.TRANSIENT,
) -> type[Any] | Callable[[type[Any]], type[Any]]:
    """Attach component metadata to a class."""

    def decorator(target: type[Any]) -> type[Any]:
        return _decorate_component(target, interface=interface, port=None, kind=kind, lifetime=lifetime)

    if cls is None:
        return decorator
    return decorator(cls)


def _kind_alias(kind: ComponentKind) -> Callable[..., type[Any] | Callable[[type[Any]], type[Any]]]:
    def alias(
        cls: type[Any] | None = None,
        *,
        interface: type[Any] | None = None,
        lifetime: ComponentLifetime = ComponentLifetime.TRANSIENT,
    ) -> type[Any] | Callable[[type[Any]], type[Any]]:
        return register(cls, interface=interface, kind=kind, lifetime=lifetime)

    return alias


def _port_alias(kind: PortKind) -> Callable[[type[Any]], type[Any]]:
    def decorator(target: type[Any]) -> type[Any]:
        return _decorate_port(target, kind=kind)

    return decorator


def _implements_contract(target: type[Any], contract: type[Any]) -> bool:
    return any(base is contract for base in inspect.getmro(target)[1:])


def _validated_port(
    target: type[Any],
    *,
    port: type[Any],
    expected_kind: PortKind,
    decorator_name: str,
) -> PortDefinition:
    port_definition = port_definition_for(port)
    if port_definition is None:
        raise TypeError(
            f"{decorator_name}() requires port '{port.__qualname__}' "
            f"to be decorated with @{expected_kind.value}."
        )
    if port_definition.kind is not expected_kind:
        raise TypeError(
            f"{decorator_name}() requires a @{expected_kind.value} port, "
            f"got @{port_definition.kind.value} '{port.__qualname__}'."
        )
    if not _implements_contract(target, port):
        raise TypeError(f"{target.__qualname__} must inherit from declared port '{port.__qualname__}'.")
    return port_definition


def application(
    cls: type[Any] | None = None,
    *,
    port: type[Any] | object = _MISSING,
    lifetime: ComponentLifetime = ComponentLifetime.TRANSIENT,
) -> type[Any] | Callable[[type[Any]], type[Any]]:
    """Attach application metadata to a class with an explicit driving port."""

    if port is _MISSING:
        raise TypeError("application() requires port=...")
    assert isinstance(port, type)

    def decorator(target: type[Any]) -> type[Any]:
        _validated_port(target, port=port, expected_kind=PortKind.DRIVING, decorator_name="application")
        return _decorate_component(
            target,
            interface=port,
            port=port,
            kind=ComponentKind.APPLICATION,
            lifetime=lifetime,
        )

    if cls is None:
        return decorator
    return decorator(cls)


def driven_adapter(
    cls: type[Any] | None = None,
    *,
    port: type[Any] | object = _MISSING,
    lifetime: ComponentLifetime = ComponentLifetime.TRANSIENT,
) -> type[Any] | Callable[[type[Any]], type[Any]]:
    """Attach driven-adapter metadata to a class with an explicit driven port."""

    if port is _MISSING:
        raise TypeError("driven_adapter() requires port=...")
    assert isinstance(port, type)

    def decorator(target: type[Any]) -> type[Any]:
        _validated_port(target, port=port, expected_kind=PortKind.DRIVEN, decorator_name="driven_adapter")
        return _decorate_component(
            target,
            interface=port,
            port=port,
            kind=ComponentKind.DRIVEN_ADAPTER,
            lifetime=lifetime,
        )

    if cls is None:
        return decorator
    return decorator(cls)


driving_port = _port_alias(PortKind.DRIVING)
driven_port = _port_alias(PortKind.DRIVEN)
driving_adapter = _kind_alias(ComponentKind.DRIVING_ADAPTER)
seed = _kind_alias(ComponentKind.SEED)


@dataclass(slots=True)
class ComponentRegistry:
    """Collect component metadata from modules."""

    _definitions: dict[type[Any], ComponentDefinition] = field(default_factory=dict)

    def add(self, definition: ComponentDefinition) -> ComponentDefinition:
        existing = self._definitions.get(definition.implementation)
        if existing is None:
            self._definitions[definition.implementation] = definition
            return definition
        if existing != definition:
            raise DuplicateComponentError(
                f"Component '{definition.module}.{definition.qualname}' was registered with conflicting metadata."
            )
        return existing

    def collect_module(self, module: ModuleType) -> None:
        for _, value in sorted(vars(module).items()):
            if not inspect.isclass(value) or value.__module__ != module.__name__:
                continue
            definition = component_definition_for(value)
            if definition is not None:
                self.add(definition)

    def collect_modules(self, modules: Iterable[ModuleType]) -> None:
        for module in sorted(modules, key=lambda item: item.__name__):
            self.collect_module(module)

    @classmethod
    def from_modules(cls, modules: Iterable[ModuleType]) -> "ComponentRegistry":
        registry = cls()
        registry.collect_modules(modules)
        return registry

    def definitions(self) -> tuple[ComponentDefinition, ...]:
        return tuple(
            sorted(
                self._definitions.values(),
                key=lambda definition: (
                    definition.module,
                    definition.qualname,
                    definition.interface.__module__,
                    definition.interface.__qualname__,
                ),
            )
        )


@dataclass(slots=True, frozen=True)
class _FactoryBinding:
    interface: type[Any]
    factory: Callable[..., Any]
    lifetime: ComponentLifetime


class ContainerBuilder:
    """Build a punq container from component metadata."""

    def __init__(self, registry: ComponentRegistry | None = None) -> None:
        self.registry = registry or ComponentRegistry()
        self._instance_bindings: list[tuple[type[Any], Any]] = []
        self._factory_bindings: list[_FactoryBinding] = []

    def add_modules(self, modules: Iterable[ModuleType]) -> "ContainerBuilder":
        self.registry.collect_modules(modules)
        return self

    def add_instance(self, interface: type[Any], instance: Any) -> "ContainerBuilder":
        self._instance_bindings.append((interface, instance))
        return self

    def add_factory(
        self,
        interface: type[Any],
        factory: Callable[..., Any],
        *,
        lifetime: ComponentLifetime = ComponentLifetime.TRANSIENT,
    ) -> "ContainerBuilder":
        self._factory_bindings.append(_FactoryBinding(interface=interface, factory=factory, lifetime=lifetime))
        return self

    def build(self) -> Container:
        definitions = self.registry.definitions()
        _validate_ports_and_adapters(definitions)
        container = Container()
        for definition in definitions:
            scope = _scope_for(definition.lifetime)
            container.register(definition.interface, definition.implementation, scope=scope)
            if definition.interface is not definition.implementation:
                container.register(definition.implementation, definition.implementation, scope=scope)
        for binding in self._factory_bindings:
            container.register(binding.interface, binding.factory, scope=_scope_for(binding.lifetime))
        for interface, instance in self._instance_bindings:
            container.register(interface, instance=instance)
        container.register(Container, instance=container)
        container.register(ComponentRegistry, instance=self.registry)
        return container


def _validate_ports_and_adapters(definitions: tuple[ComponentDefinition, ...]) -> None:
    _validate_unique_port_binding(definitions, ComponentKind.APPLICATION, PortKind.DRIVING)
    _validate_unique_port_binding(definitions, ComponentKind.DRIVEN_ADAPTER, PortKind.DRIVEN)

    for definition in definitions:
        if definition.kind.value not in _VALIDATED_COMPONENT_KINDS:
            continue
        _validate_component_port(definition)
        _validate_constructor_dependencies(definition)


def _validate_unique_port_binding(
    definitions: tuple[ComponentDefinition, ...],
    component_kind: ComponentKind,
    expected_port_kind: PortKind,
) -> None:
    owners: dict[type[Any], ComponentDefinition] = {}
    for definition in definitions:
        if definition.kind is not component_kind:
            continue
        port = definition.port
        if port is None:
            raise PortsAndAdaptersViolationError(
                f"{_component_kind_label(component_kind)} '{definition.qualname}' must declare an explicit port."
            )
        port_definition = port_definition_for(port)
        if port_definition is None or port_definition.kind is not expected_port_kind:
            raise PortsAndAdaptersViolationError(
                f"{_component_kind_label(component_kind)} '{definition.qualname}' "
                f"must use a @{expected_port_kind.value}."
            )
        existing = owners.get(port)
        if existing is not None:
            raise PortsAndAdaptersViolationError(
                f"{_port_label(port_definition)} already has multiple "
                f"{_component_kind_label(component_kind)} implementations: "
                f"'{existing.qualname}' and '{definition.qualname}'."
            )
        owners[port] = definition


def _validate_component_port(definition: ComponentDefinition) -> None:
    if definition.kind is ComponentKind.APPLICATION:
        _validate_explicit_component_port(definition, PortKind.DRIVING)
        return
    if definition.kind is ComponentKind.DRIVEN_ADAPTER:
        _validate_explicit_component_port(definition, PortKind.DRIVEN)


def _validate_explicit_component_port(definition: ComponentDefinition, expected_kind: PortKind) -> None:
    port = definition.port
    if port is None:
        raise PortsAndAdaptersViolationError(
            f"{_component_label(definition)} must declare an explicit @{expected_kind.value}."
        )
    port_definition = port_definition_for(port)
    if port_definition is None or port_definition.kind is not expected_kind:
        raise PortsAndAdaptersViolationError(
            f"{_component_label(definition)} must declare an explicit @{expected_kind.value}."
        )
    if definition.interface is not port:
        raise PortsAndAdaptersViolationError(
            f"{_component_label(definition)} must register its declared port as interface."
        )
    if not _implements_contract(definition.implementation, port):
        raise PortsAndAdaptersViolationError(
            f"{_component_label(definition)} must inherit from declared port '{port.__qualname__}'."
        )


def _validate_constructor_dependencies(definition: ComponentDefinition) -> None:
    for parameter_name, dependency in _constructor_dependencies(definition.implementation):
        dependency_definition = component_definition_for(dependency)
        if dependency_definition is not None:
            raise PortsAndAdaptersViolationError(
                f"{_component_label(definition)} parameter '{parameter_name}' depends on decorated implementation "
                f"'{dependency.__qualname__}'. Depend on its port instead."
            )

        port_definition = port_definition_for(dependency)
        if port_definition is None:
            continue

        if definition.kind is ComponentKind.DRIVING_ADAPTER and port_definition.kind is not PortKind.DRIVING:
            raise PortsAndAdaptersViolationError(
                f"{_component_label(definition)} parameter '{parameter_name}' "
                f"depends on {_port_label(port_definition)}. "
                "Driving adapters may depend only on @driving_port ports."
            )
        if definition.kind is ComponentKind.APPLICATION and port_definition.kind is not PortKind.DRIVEN:
            raise PortsAndAdaptersViolationError(
                f"{_component_label(definition)} parameter '{parameter_name}' "
                f"depends on {_port_label(port_definition)}. "
                "Applications may depend only on @driven_port ports."
            )


def _constructor_dependencies(implementation: type[Any]) -> tuple[tuple[str, type[Any]], ...]:
    init = implementation.__init__
    if init is object.__init__:
        return ()

    signature = inspect.signature(init)
    type_hints = _safe_type_hints(init)
    dependencies: list[tuple[str, type[Any]]] = []

    for parameter_name, parameter in signature.parameters.items():
        if parameter_name == "self":
            continue
        if parameter.kind in {inspect.Parameter.VAR_KEYWORD, inspect.Parameter.VAR_POSITIONAL}:
            continue

        for dependency in _dependency_types(type_hints.get(parameter_name)):
            dependencies.append((parameter_name, dependency))

    return tuple(dependencies)


def _safe_type_hints(init: Callable[..., Any]) -> dict[str, Any]:
    try:
        return get_type_hints(init, include_extras=True)
    except (AttributeError, NameError, TypeError):
        return dict(getattr(init, "__annotations__", {}))


def _dependency_types(annotation: Any) -> tuple[type[Any], ...]:
    if annotation is None:
        return ()
    if isinstance(annotation, type):
        return (annotation,)

    origin = get_origin(annotation)
    if origin is Annotated:
        args = get_args(annotation)
        if not args:
            return ()
        return _dependency_types(args[0])

    args = get_args(annotation)
    if origin in {Union, UnionType}:
        return tuple(
            dict.fromkeys(
                dependency
                for candidate in args
                if candidate is not type(None)
                for dependency in _dependency_types(candidate)
            )
        )

    if origin is list:
        return tuple(
            dict.fromkeys(
                dependency
                for candidate in args
                if candidate is not Ellipsis
                for dependency in _dependency_types(candidate)
            )
        )

    return ()


def _component_kind_label(kind: ComponentKind) -> str:
    if kind is ComponentKind.APPLICATION:
        return "Application"
    if kind is ComponentKind.DRIVEN_ADAPTER:
        return "Driven adapter"
    if kind is ComponentKind.DRIVING_ADAPTER:
        return "Driving adapter"
    if kind is ComponentKind.SEED:
        return "Seed"
    return "Component"


def _component_label(definition: ComponentDefinition) -> str:
    return f"{_component_kind_label(definition.kind)} '{definition.qualname}'"


def _port_label(definition: PortDefinition) -> str:
    prefix = "@driving_port" if definition.kind is PortKind.DRIVING else "@driven_port"
    return f"{prefix} '{definition.qualname}'"


def _scope_for(lifetime: ComponentLifetime) -> Scope:
    return Scope.singleton if lifetime is ComponentLifetime.SINGLETON else Scope.transient


__all__ = [
    "ComponentDefinition",
    "ComponentKind",
    "ComponentLifetime",
    "ComponentRegistry",
    "ContainerBuilder",
    "DuplicateComponentError",
    "PortDefinition",
    "PortKind",
    "PortsAndAdaptersViolationError",
    "application",
    "component_definition_for",
    "driven_adapter",
    "driven_port",
    "driving_adapter",
    "driving_port",
    "infer_interface",
    "port_definition_for",
    "register",
    "seed",
]
