"""Runtime dispatch and type-matching utilities with decorator-based
overload registration.

This module provides:

- `TypeMatch`: helpers to check values against type hints and to compute
  a specificity score used to rank overload candidates.
- `WizeDispatcher`: a builder that creates namespaced decorators (e.g.,
  `@dispatch.fn`) for registering overloads on free functions and on
  methods, including instance/class/static methods and property setters.

It keeps the original callable as a fallback, binds calls according to
the original signature, and selects the best overload using typing-aware
matching rules.

The public instance `dispatch` is used to decorate overloads.
"""

from __future__ import annotations

from collections.abc import Callable as ABCCallable
from collections.abc import Collection as ABCCollection
from collections.abc import Iterable as ABCIterable
from collections.abc import Mapping as ABCMapping
from collections.abc import MutableMapping, MutableSequence, Sequence
from contextlib import suppress
from dataclasses import dataclass
from functools import update_wrapper
from inspect import BoundArguments, Parameter, Signature, signature
from sys import modules
from types import EllipsisType, MappingProxyType, ModuleType
from typing import (
    Annotated,
    Any,
    Callable,
    ClassVar,
    Dict,
    Final,
    ForwardRef,
    FrozenSet,
    Iterable,
    List,
    Literal,
    Mapping,
    Optional,
    ParamSpec,
    Self,
    Tuple,
    Type,
    TypeVar,
    Union,
    get_args,
    get_origin,
    get_type_hints,
)

UnionType: Optional[Any] = None
with suppress(Exception):
    from types import UnionType
# Sentinel for "no type constraint".
WILDCARD: Final[object] = object()


class TypeMatch:
    """Type-hint matching, scoring, and function selection helpers.

    (Docstring shortened in package version for brevity.)
    """

    @staticmethod
    def _resolve_hints(
        *,
        func: Callable[..., Any],
        g: Optional[Mapping[str, Any]] = None,
        localns: Optional[Mapping[str, Any]] = None,
    ) -> Dict[str, Any]:
        return get_type_hints(
            obj=func,
            globalns=(func.__globals__ if g is None else
                      (g if isinstance(g, dict) else dict(g))),
            localns=(None if localns is None else (
                localns if isinstance(localns, dict) else dict(localns))),
        )

    @staticmethod
    def _resolve_hint(hint: object) -> object:
        with suppress(Exception):
            g: Dict[str, Any] = vars(modules[__name__])
            if isinstance(hint, str):
                return eval(hint, g, g)
            if isinstance(hint, ForwardRef):
                return eval(hint.__forward_arg__, g, g)
        return hint

    @staticmethod
    def _is_typevar_like(hint: object) -> bool:
        return isinstance(hint, (TypeVar, ParamSpec))

    @staticmethod
    def _class_distance(a: type, b: type) -> int:
        with suppress(Exception):
            return a.__mro__.index(b)
        return 10_000

    @classmethod
    def _is_union_origin(cls, origin: object) -> bool:
        return origin is Union or (UnionType is not None
                                   and origin is UnionType)

    @classmethod
    def _kwargs_value_type_from_varkw(cls, annotation: object) -> object:
        ann: object = cls._resolve_hint(annotation)
        if get_origin(ann) in (dict, Mapping, ABCMapping, MutableMapping):
            args: Tuple[Any, ...] = get_args(ann)
            if len(args) == 2:
                return args[1]
        return Any

    @classmethod
    def _is_match(cls, value: object, hint: object) -> bool:
        hint = cls._resolve_hint(hint)
        if hint in (Any, object) or hint is WILDCARD:
            return True
        supertype: Optional[object] = getattr(hint, "__supertype__", None)
        if callable(hint) and supertype is not None:
            return cls._is_match(value, supertype)
        if (isinstance(hint, type) and issubclass(hint, dict)
                and hasattr(hint, "__annotations__")
                and hasattr(hint, "__total__")):
            if not isinstance(value, dict):
                return False
            ann: Dict[str, object] = hint.__annotations__
            for k in getattr(hint, "__required_keys__", set()):
                if k not in value or not cls._is_match(value[k], ann[k]):
                    return False
            return all(not (k in value and not cls._is_match(value[k], ann[k]))
                       for k in getattr(hint, "__optional_keys__", set()))
        if isinstance(hint, type) and getattr(hint, "_is_protocol", False):
            return (isinstance(value, hint)
                    if getattr(hint, "_is_runtime_protocol", False) else False)
        if cls._is_typevar_like(hint):
            if isinstance(hint, TypeVar):
                if hint.__constraints__:
                    return any(
                        cls._is_match(value, c) for c in hint.__constraints__)
                if hint.__bound__ is not None:
                    return cls._is_match(value, hint.__bound__)
            return True
        origin: Optional[type] = get_origin(hint)
        args: Tuple[Any, ...] = get_args(hint)
        if origin is Annotated:
            return cls._is_match(value, args[0])
        if origin is ClassVar:
            return cls._is_match(value, args[0]) if args else True
        if origin is Literal:
            return any(value == lit for lit in args)
        if isinstance(value, type):
            if origin in (Type, type):
                return cls._is_match(value, args[0] if args else Any)
            if origin is None and isinstance(hint, type):
                return issubclass(value, hint)
            return False
        if origin is None:
            if hint in (Tuple, tuple):
                return isinstance(value, tuple)
            if hint in (List, list):
                return isinstance(value, list)
            if hint in (Dict, dict):
                return isinstance(value, dict)
            if hint in (set, frozenset):
                return isinstance(value, (set, frozenset))
            if hint in (type, Type):
                return isinstance(value, type)
            return isinstance(value, hint) if isinstance(hint, type) else False
        if cls._is_union_origin(origin):
            return any(cls._is_match(value, t) for t in args)
        if origin in (Type, type):
            return False
        if origin is ABCCallable:
            if not callable(value):
                return False
            if not args:
                return True
            params_spec: Union[EllipsisType,
                               Tuple[object, ...]] = (args[0] if len(args) >= 1
                                                      else Ellipsis)
            if isinstance(params_spec, EllipsisType):
                return True
            try:
                parameters: MappingProxyType[str, Parameter] = signature(
                    value).parameters
            except Exception:
                return True
            declared: list[object] = []
            has_varargs: bool = False
            for p in parameters.values():
                if p.kind in (
                        Parameter.POSITIONAL_ONLY,
                        Parameter.POSITIONAL_OR_KEYWORD,
                ):
                    declared.append(p.annotation if p.
                                    annotation is not Parameter.empty else Any)
                elif p.kind == Parameter.VAR_POSITIONAL:
                    has_varargs = True
            declared_n: int = len(declared)
            if declared_n < len(params_spec) and not has_varargs:
                return False
            for i, expected_t in enumerate(params_spec):
                if i < declared_n:
                    actual_t: object = declared[i]
                    if (actual_t is not Any and actual_t is not Parameter.empty
                            and not cls._is_match(actual_t, expected_t)):
                        return False
                else:
                    break
            return True
        if origin in (dict, ABCMapping, MutableMapping):
            return (all(
                cls._is_match(k, args[0] if len(args) > 0 else Any)
                and cls._is_match(v, args[1] if len(args) > 1 else Any)
                for k, v in value.items())
                    if isinstance(value, ABCMapping) else False)
        if origin in (Sequence, MutableSequence):
            if not isinstance(value, Sequence):
                return False
            if not args:
                return True
            with suppress(TypeError):
                return all(cls._is_match(x, args[0]) for x in value)
            return False
        if origin in (ABCIterable, ABCCollection) and isinstance(
                value, Iterable):
            return (all(cls._is_match(x, args[0])
                        for x in iter(value)) if args else True)
        if origin in (tuple, list, dict, set, frozenset) and not isinstance(
                value, origin if origin is not frozenset else (frozenset, )):
            return (cls._is_match(value, args[0])
                    if origin is list and args else False)
        if origin is tuple:
            if not isinstance(value, tuple):
                return False
            if len(args) == 2 and args[1] is Ellipsis:
                return all(cls._is_match(v, args[0]) for v in value)
            if len(args) != len(value):
                return False
            return all(
                cls._is_match(v, t) for v, t in zip(value, args, strict=True))
        if origin is list:
            return ((all(
                cls._is_match(x, args[0])
                for x in value) if isinstance(value, list) else cls._is_match(
                    value, args[0])) if args else isinstance(value, list))
        if origin is dict:
            return (all(
                cls._is_match(k, args[0] if len(args) > 0 else Any)
                and cls._is_match(v, args[1] if len(args) > 1 else Any)
                for k, v in value.items())
                    if isinstance(value, dict) else False)
        if origin in (set, frozenset):
            if not isinstance(value, origin) or not isinstance(
                    value, Iterable):
                return False
            if not args:
                return True
            return all(cls._is_match(x, args[0]) for x in value)
        return isinstance(value, origin) if isinstance(origin, type) else False

    @classmethod
    def _type_specificity_score(cls, value: object, hint: object) -> int:
        hint = cls._resolve_hint(hint)
        if hint in (Any, object) or hint is WILDCARD:
            return 0
        supertype: Optional[object] = getattr(hint, "__supertype__", None)
        if callable(hint) and supertype is not None:
            return cls._type_specificity_score(value, supertype) + 1
        if (isinstance(hint, type) and issubclass(hint, dict)
                and hasattr(hint, "__annotations__")
                and hasattr(hint, "__total__")):
            return (25 + 2 * len(getattr(hint, "__required_keys__", set())) +
                    sum(
                        cls._type_specificity_score(value, t)
                        for t in hint.__annotations__.values()))
        if isinstance(hint, type) and getattr(hint, "_is_protocol", False):
            return 14 if getattr(hint, "_is_runtime_protocol", False) else 6
        if cls._is_typevar_like(hint):
            if isinstance(hint, TypeVar):
                if hint.__constraints__:
                    return (max(
                        cls._type_specificity_score(value, c)
                        for c in hint.__constraints__) - 1)
                if hint.__bound__ is not None:
                    return (
                        cls._type_specificity_score(value, hint.__bound__) - 1)
            return 1
        origin: Optional[type] = get_origin(hint)
        args: Tuple[Any, ...] = get_args(hint)
        if origin is Literal:
            return 100
        if origin is Annotated:
            return 1 + cls._type_specificity_score(value, args[0])
        if origin is ClassVar:
            return cls._type_specificity_score(value, args[0]) if args else 1
        if cls._is_union_origin(origin):
            return max([cls._type_specificity_score(value, t)
                        for t in args]) - len(args)
        if origin in (Type, type):
            return (8 if not args else 15 +
                    cls._type_specificity_score(value, args[0]))
        if origin is ABCCallable:
            params_spec: Union[EllipsisType,
                               Tuple[object, ...]] = (args[0] if len(
                                   args or ()) >= 1 else Ellipsis)
            return 12 + (0 if isinstance(params_spec, EllipsisType) else sum(
                cls._type_specificity_score(value, p) for p in params_spec))
        if origin in (dict, ABCMapping, MutableMapping):
            return (20 +
                    sum(cls._type_specificity_score(value, a)
                        for a in args) if isinstance(value, dict) else -50)
        if origin in (Sequence, MutableSequence, ABCIterable, ABCCollection):
            return ((18 +
                     cls._type_specificity_score(value, args[0]) if args else
                     16) if isinstance(value,
                                       (list, tuple, set, frozenset)) else -50)
        if origin and origin in (tuple, list, dict, set, frozenset):
            return ((20 +
                     sum(cls._type_specificity_score(value, a)
                         for a in args)) if isinstance(value, origin) else -50)
        if hint in (Tuple, List, Dict, tuple, list, dict, set, frozenset):
            return 10
        if isinstance(hint, type):
            return 5 + max(
                0,
                50 - cls._class_distance(
                    value if isinstance(value, type) else type(value), hint),
            )
        return 1

    def __new__(
        cls,
        match: Dict[str, object],
        options: list[Callable[..., Any]],
    ) -> list[Callable[..., Any]]:
        if not match or not options:
            return []
        keys: Tuple[str, ...] = tuple(match.keys())
        ranked: list[Tuple[Callable[..., Any], int]] = []

        def _key_hint(
            k: str,
            params_map: Mapping[str, Parameter],
            kw_param: Optional[Parameter],
            tmap_local: Optional[Mapping[str, Any]],
        ) -> object:
            if tmap_local and k in tmap_local:
                return cls._resolve_hint(tmap_local[k])
            p: Optional[Parameter] = params_map.get(k)
            if p is None:
                return (cls._kwargs_value_type_from_varkw(kw_param.annotation)
                        if kw_param and kw_param is not Parameter.empty else
                        Any)
            return p.annotation if p.annotation is not Parameter.empty else Any

        for fn in options:
            params: MappingProxyType[str, Parameter] = signature(fn).parameters
            varkw: Optional[Parameter] = next(
                (p
                 for p in params.values() if p.kind == Parameter.VAR_KEYWORD),
                None,
            )
            tmap: Optional[Mapping[str,
                                   Any]] = getattr(fn, "__dispatch_type_map__",
                                                   None)
            if not all(
                    cls._is_match(match[k], _key_hint(k, params, varkw, tmap))
                    for k in keys):
                continue
            score: int = sum(
                cls._type_specificity_score(match[k],
                                            _key_hint(k, params, varkw, tmap))
                for k in keys)
            for k in keys:
                p: Optional[Parameter] = params.get(k)
                score += (40 if (cls._resolve_hint(tmap[k] if (
                    tmap and k in tmap) else (p.annotation if (
                        p and p.annotation is not Parameter.empty) else Any)))
                          not in (Any, object) else 20)
            score -= 1000 * sum(1
                                for k in keys if k not in params and not varkw)
            if varkw:
                score -= 1
            if any(p.kind == Parameter.VAR_POSITIONAL
                   for p in params.values()):
                score -= 2
            ranked.append((fn, score))
        return ([fn for fn, s in ranked
                 if s == max(s for _, s in ranked)] if ranked else [])


class WizeDispatcher:
    """Create namespaced decorators to register method/function overloads."""

    _pending: ClassVar[Dict[str, "WizeDispatcher._OverloadDescriptor"]] = {}

    @dataclass(frozen=True)
    class _Overload:
        _func: Callable[..., Any]
        _type_map: Mapping[str, Any]
        _param_order: Tuple[str, ...]
        _dec_keys: FrozenSet[str]
        _is_original: bool
        _reg_index: int

    class _BaseRegistry:
        _target_name: str
        _original: Callable[..., Any]
        _sig: Signature
        _param_order: Tuple[str, ...]
        _overloads: list["WizeDispatcher._Overload"]
        _cache: Dict[Tuple[Type[Any], ...], Callable[..., Any]]
        _reg_counter: int
        _skip_first: bool

        def __init__(
            self,
            *,
            target_name: str,
            original: Callable[..., Any],
            skip_first: bool,
        ) -> None:
            self._target_name = target_name
            self._original = original
            self._sig = signature(obj=original)
            self._skip_first = skip_first
            self._param_order = WizeDispatcher._param_order(
                sig=self._sig, skip_first=skip_first)
            self._overloads = []
            self._cache = {}
            self._reg_counter = 0

        def _bind(
            self,
            instance: Any | None,
            args: Tuple[Any, ...],
            kwargs: Dict[str, Any],
        ) -> BoundArguments:
            bound: BoundArguments = (self._sig.bind(instance, *args, **kwargs)
                                     if self._skip_first else self._sig.bind(
                                         *args, **kwargs))
            bound.apply_defaults()
            return bound

        def _arg_types(self, bound: BoundArguments) -> Tuple[Type[Any], ...]:
            return tuple(
                type(bound.arguments[name]) for name in self._param_order)

        def _invoke(self, func: Callable[..., Any],
                    bound: BoundArguments) -> Any:
            return func(**dict(bound.arguments))

        def _dispatch(
            self,
            *,
            instance: Any | None,
            args: Tuple[Any, ...],
            kwargs: Dict[str, Any],
        ) -> Any:
            bound: BoundArguments = self._bind(instance, args, kwargs)
            types_key: Tuple[Type[Any], ...] = self._arg_types(bound)
            if (chosen := self._cache.get(types_key)) is not None:
                return self._invoke(chosen, bound)
            chosen: Any = next(
                iter(
                    TypeMatch(
                        {n: bound.arguments[n]
                         for n in self._param_order},
                        [ov._func
                         for ov in self._overloads] + [self._original],
                    )),
                self._original,
            )
            self._cache[types_key] = chosen
            return self._invoke(chosen, bound)

        def register(
            self,
            *,
            func: Callable[..., Any],
            type_map: Mapping[str, Any],
            dec_keys: FrozenSet[str],
            is_original: bool,
            reg_index_override: Optional[int] = None,
        ) -> None:
            attr_str: str = "__dispatch_type_map__"
            setattr(func, attr_str, dict(type_map))
            self._overloads.append(
                WizeDispatcher._Overload(
                    _func=func,
                    _type_map=type_map,
                    _param_order=self._param_order,
                    _dec_keys=dec_keys,
                    _is_original=is_original,
                    _reg_index=(reg_index_override if reg_index_override
                                is not None else self._reg_counter),
                ))
            self._reg_counter += 1
            self._cache.clear()

    class _MethodRegistry(_BaseRegistry):

        def __init__(
            self,
            *,
            target_name: str,
            original: Callable[..., Any],
            has_receiver: bool,
        ) -> None:
            super().__init__(
                target_name=target_name,
                original=original,
                skip_first=has_receiver,
            )

    class _FunctionRegistry(_BaseRegistry):

        def __init__(self, *, target_name: str,
                     original: Callable[..., Any]) -> None:
            super().__init__(target_name=target_name,
                             original=original,
                             skip_first=False)

    class _OverloadDescriptor:
        _queues: Dict[
            str,
            list[Tuple[Callable[..., Any], Dict[str, Any], Tuple[Any, ...]]],
        ]

        def __init__(self) -> None:
            self._queues = {}

        def __set_name__(self, owner: Type[Any], _own_name: str) -> None:
            reg_attr: str = "__dispatch_registry__"
            reg_map: Dict[str, WizeDispatcher._MethodRegistry] = getattr(
                owner, reg_attr, {})
            if not hasattr(owner, reg_attr):
                setattr(owner, reg_attr, reg_map)
            reg: WizeDispatcher._MethodRegistry
            for target_name, items in self._queues.items():
                if target_name not in reg_map:
                    original_attr: Any = owner.__dict__.get(target_name)
                    has_receiver: bool = True
                    if (isinstance(original_attr, property)
                            and original_attr.fget):
                        original_fn: Callable[...,
                                              Any] = (original_attr.fset
                                                      or original_attr.fget)
                    elif isinstance(original_attr,
                                    (classmethod, staticmethod)):
                        original_fn = original_attr.__func__
                        has_receiver = isinstance(original_attr, classmethod)
                    elif callable(original_attr):
                        original_fn = original_attr
                    else:
                        original_fn = items[-1][0]
                    reg = reg_map[target_name] = (
                        WizeDispatcher._MethodRegistry(
                            target_name=target_name,
                            original=original_fn,
                            has_receiver=has_receiver,
                        ))
                    reg.register(
                        func=original_fn,
                        type_map={
                            n:
                            WizeDispatcher._resolve_hints(
                                func=original_fn,
                                g=getattr(original_fn, "__wrapped__",
                                          original_fn).__globals__,
                                localns=owner.__dict__,
                            ).get(n, WILDCARD)
                            for n in reg._param_order
                        },
                        dec_keys=frozenset(),
                        is_original=True,
                        reg_index_override=-1,
                    )

                    def _wrap_inst(self_: Any,
                                   *a: Any,
                                   _reg=reg,
                                   **k: Any) -> Any:
                        return _reg._dispatch(instance=self_, args=a, kwargs=k)

                    selected_func: Any = _wrap_inst
                    if isinstance(original_attr, property):
                        selected_func = original_attr.setter(
                            lambda self_, value, _reg=reg: _reg._dispatch(
                                instance=self_, args=(value, ), kwargs={}))
                    elif isinstance(original_attr, classmethod):
                        selected_func = classmethod(selected_func)
                    elif isinstance(original_attr, staticmethod):
                        selected_func = staticmethod(
                            lambda *a, _reg=reg, **k: _reg._dispatch(
                                instance=None, args=a, kwargs=k))
                    setattr(owner, target_name, selected_func)
                reg = getattr(owner, reg_attr)[target_name]
                for func, decorator_types, decorator_pos in items:
                    if not getattr(func, "__qualname__",
                                   "").startswith(owner.__qualname__ + "."):
                        continue
                    ldec: int = len(decorator_pos)
                    dec_types: Dict[str, Any] = {
                        **{
                            v: decorator_pos[i]
                            for i, v in enumerate(reg._param_order) if i < ldec
                        },
                        **decorator_types,
                    }
                    reg.register(
                        func=func,
                        type_map=WizeDispatcher._merge_types(
                            order=reg._param_order,
                            decorator_types=dec_types,
                            fn_ann=WizeDispatcher._resolve_hints(
                                func=func,
                                g=func.__globals__,
                                localns=owner.__dict__,
                            ),
                        ),
                        dec_keys=frozenset(dec_types.keys()),
                        is_original=False,
                    )
            WizeDispatcher._pending.pop(owner.__qualname__, None)

        def __get__(self, instance: Any, owner: Optional[type] = None) -> Self:
            return self

        def _add(
            self,
            *,
            target_name: str,
            func: Callable[..., Any],
            decorator_types: Dict[str, Any],
            decorator_pos: Tuple[Any, ...],
        ) -> None:
            self._queues.setdefault(target_name, []).append(
                (func, dict(decorator_types), tuple(decorator_pos)))

    @staticmethod
    def _param_order(*, sig: Signature, skip_first: bool) -> Tuple[str, ...]:
        params: list[Parameter] = list(sig.parameters.values())
        if skip_first and params:
            params = params[1:]
        return tuple(p.name for p in params)

    @staticmethod
    def _register_function_overload(
            *,
            target_name: str,
            func: Callable[..., Any],
            decorator_types: Mapping[str, Any],
            decorator_pos: Tuple[Any, ...] = (),
    ) -> Callable[..., Any]:
        mod: ModuleType = modules[func.__module__]
        g: Dict[str, Any] = mod.__dict__
        reg_attr: str = "__fdispatch_registry__"
        wrap_attr: str = "__fdispatch_wrapper__"
        if not hasattr(mod, reg_attr):
            setattr(mod, reg_attr, {})
        regmap: Dict[str, WizeDispatcher._FunctionRegistry] = getattr(
            mod, reg_attr)
        target: Optional[Callable[..., Any]] = g.get(target_name)
        if target is None or not callable(target):
            raise AttributeError(f"Function '{target_name}' "
                                 "must exist before registering overloads")
        reg: WizeDispatcher._FunctionRegistry
        if target_name not in regmap:
            regmap[target_name] = WizeDispatcher._FunctionRegistry(
                target_name=target_name, original=target)
            wrapped: Callable[..., Any] = update_wrapper(
                lambda *a, **k: regmap[target_name]._dispatch(
                    instance=None, args=a, kwargs=k),
                target,
            )
            setattr(wrapped, wrap_attr, True)
            g[target_name] = wrapped
        else:
            current: Callable[..., Any] = g[target_name]
            if not getattr(current, wrap_attr, False):
                reg = regmap[target_name]
                reg._original = current
                reg._sig = signature(obj=current)
                reg._param_order = tuple(
                    p.name for p in signature(obj=current).parameters.values())
                if not reg._overloads:
                    reg._overloads = []
                    reg._cache = {}
                    reg._reg_counter = 0
                reg.register(
                    func=current,
                    type_map={
                        n:
                        WizeDispatcher._resolve_hints(func=current,
                                                      g=g).get(n, WILDCARD)
                        for n in reg._param_order
                    },
                    dec_keys=frozenset(),
                    is_original=True,
                    reg_index_override=-1,
                )
                wrapped: Callable[..., Any] = update_wrapper(
                    lambda *a, **k: reg._dispatch(
                        instance=None, args=a, kwargs=k),
                    current,
                )
                setattr(wrapped, wrap_attr, True)
                g[target_name] = wrapped
        reg = regmap[target_name]
        ldec: int = len(decorator_pos)
        dec_types: Dict[str, Any] = {
            **{
                name: decorator_pos[i]
                for i, name in enumerate(reg._param_order) if i < ldec
            },
            **decorator_types,
        }
        reg.register(
            func=func,
            type_map=WizeDispatcher._merge_types(
                order=reg._param_order,
                decorator_types=dec_types,
                fn_ann=WizeDispatcher._resolve_hints(func=func, g=g),
            ),
            dec_keys=frozenset(dec_types.keys()),
            is_original=False,
        )
        return g[target_name] if func.__name__ == target_name else func

    @staticmethod
    def _resolve_hints(
        *,
        func: Callable[..., Any],
        g: Optional[Mapping[str, Any]] = None,
        localns: Optional[Mapping[str, Any]] = None,
    ) -> Dict[str, Any]:
        return get_type_hints(
            obj=func,
            globalns=(func.__globals__ if g is None else
                      (g if isinstance(g, dict) else dict(g))),
            localns=(None if localns is None else (
                localns if isinstance(localns, dict) else dict(localns))),
        )

    @staticmethod
    def _merge_types(
        *,
        order: Tuple[str, ...],
        decorator_types: Mapping[str, Any],
        fn_ann: Mapping[str, Any],
    ) -> Dict[str, Any]:
        return {
            name: (decorator_types[name]
                   if name in decorator_types else fn_ann.get(name, WILDCARD))
            for name in order
        }

    def __getattr__(self, target_name: str):
        """Decorator factory for registering overloads under a given name.

        Allows usage like:
          @dispatch.fn            # use function annotations
          @dispatch.fn(int, str)  # positional types in parameter order
          @dispatch.fn(a=int)     # keyword types by parameter name
        """

        def _extract_func(obj):
            # unwrap classmethod/staticmethod to the underlying function
            return (obj.__func__ if isinstance(obj, (classmethod,
                                                     staticmethod)) else obj)

        def _decorator_factory(*decorator_args, **decorator_kwargs):

            def _queue_or_register(*, func, decorator_types, decorator_pos):
                qual: str = getattr(func, "__qualname__", "")
                # If we're inside a class body, queue for registration at
                # class creation
                if "." in qual:
                    owner_qual: str = qual.split(".", 1)[0]
                    desc: Any = WizeDispatcher._pending.get(owner_qual)
                    if desc is None:
                        desc = self._OverloadDescriptor()
                        WizeDispatcher._pending[owner_qual] = desc
                    desc._add(
                        target_name=target_name,
                        func=func,
                        decorator_types=dict(decorator_types),
                        decorator_pos=tuple(decorator_pos),
                    )
                    return desc
                # Free function: register immediately (requires base function
                # to exist)
                return self._register_function_overload(
                    target_name=target_name,
                    func=func,
                    decorator_types=dict(decorator_types),
                    decorator_pos=tuple(decorator_pos),
                )

            # Bare decorator form: @dispatch.name applied directly to
            # a function
            if (len(decorator_args) == 1 and not decorator_kwargs
                    and (hasattr(decorator_args[0], "__code__")
                         or isinstance(decorator_args[0],
                                       (classmethod, staticmethod)))):
                return _queue_or_register(
                    func=_extract_func(decorator_args[0]),
                    decorator_types={},
                    decorator_pos=(),
                )

            # Otherwise, return a decorator that will receive the function
            return lambda func: _queue_or_register(
                func=_extract_func(func),
                decorator_types=decorator_kwargs,
                decorator_pos=tuple(decorator_args),
            )

        return _decorator_factory


dispatch: Final[WizeDispatcher] = WizeDispatcher()
