"""Importer utilities for autodoc"""

from __future__ import annotations

import re
from typing import TYPE_CHECKING

from sphinx.ext.autodoc._shared import LOGGER
from sphinx.locale import __

if TYPE_CHECKING:
    from collections.abc import Mapping, Sequence

    from sphinx.environment import _CurrentDocument
    from sphinx.ext.autodoc._property_types import _AutodocObjType

#: extended signature RE: with explicit module name separated by ::
py_ext_sig_re = re.compile(
    r"""^ ([\w.]+::)?            # explicit module name
          ([\w.]+\.)?            # module and/or class name(s)
          (\w+)  \s*             # thing name
          (?: \[\s*(.*?)\s*])?   # optional: type parameters list
          (?: \((.*)\)           # optional: arguments
           (?:\s* -> \s* (.*))?  #           return annotation
          )? $                   # and nothing more
    """,
    re.VERBOSE,
)


def _parse_name(
    *,
    name: str,
    objtype: _AutodocObjType,
    current_document: _CurrentDocument,
    ref_context: Mapping[str, str | None],
) -> tuple[str, tuple[str, ...], str | None, str | None] | None:
    """Parse *name* into module name, path, arguments, and return annotation."""
    # Parse the definition in *name*.
    # autodoc directives for classes and functions can contain a signature,
    # which overrides the autogenerated one.
    matched = py_ext_sig_re.match(name)
    if matched is None:
        LOGGER.warning(
            __('invalid signature for auto%s (%r)'),
            objtype,
            name,
            type='autodoc',
        )
        # need a module to import
        LOGGER.warning(
            __(
                "don't know which module to import for autodocumenting "
                '%r (try placing a "module" or "currentmodule" directive '
                'in the document, or giving an explicit module name)'
            ),
            name,
            type='autodoc',
        )
        return None

    explicit_modname, path, base, _tp_list, args, retann = matched.groups()
    if args is not None:
        args = f'({args})'

    # Support explicit module and class name separation via ``::``
    if explicit_modname is not None:
        module_name = explicit_modname.removesuffix('::')
        parents = path.rstrip('.').split('.') if path else ()
    else:
        module_name = None
        parents = ()

    resolved = _resolve_name(
        objtype=objtype,
        module_name=module_name,
        path=path,
        base=base,
        parents=parents,
        current_document=current_document,
        ref_context_py_module=ref_context.get('py:module'),
        ref_context_py_class=ref_context.get('py:class', ''),  # type: ignore[arg-type]
    )
    if resolved is None:
        return None
    module_name, parts = resolved

    if objtype == 'module' and args:
        msg = __("signature arguments given for automodule: '%s'")
        LOGGER.warning(msg, name, type='autodoc')
        return None
    if objtype == 'module' and retann:
        msg = __("return annotation given for automodule: '%s'")
        LOGGER.warning(msg, name, type='autodoc')
        return None

    if not module_name:
        # Could not resolve a module to import
        LOGGER.warning(
            __(
                "don't know which module to import for autodocumenting "
                '%r (try placing a "module" or "currentmodule" directive '
                'in the document, or giving an explicit module name)'
            ),
            name,
            type='autodoc',
        )
        return None

    return module_name, parts, args, retann


def _resolve_name(
    *,
    objtype: _AutodocObjType,
    module_name: str | None,
    path: str | None,
    base: str,
    parents: Sequence[str],
    current_document: _CurrentDocument,
    ref_context_py_module: str | None,
    ref_context_py_class: str,
) -> tuple[str | None, tuple[str, ...]] | None:
    """Resolve the module and name of the object to document given by the
    arguments and the current module/class.

    Must return a pair of the module name and a chain of attributes; for
    example, it would return ``('zipfile', ('ZipFile', 'open'))`` for the
    ``zipfile.ZipFile.open`` method.
    """
    if objtype == 'module':
        if module_name is not None:
            LOGGER.warning(
                __('"::" in automodule name doesn\'t make sense'), type='autodoc'
            )
        return (path or '') + base, ()

    if objtype in {'class', 'exception', 'function', 'decorator', 'data', 'type'}:
        if module_name is not None:
            return module_name, (*parents, base)
        if path:
            module_name = path.rstrip('.')
            return module_name, (*parents, base)

        # if documenting a toplevel object without explicit module,
        # it can be contained in another auto directive ...
        module_name = current_document.autodoc_module
        # ... or in the scope of a module directive
        if not module_name:
            module_name = ref_context_py_module
        # ... else, it stays None, which means invalid
        return module_name, (*parents, base)

    if objtype in {'method', 'property', 'attribute'}:
        if module_name is not None:
            return module_name, (*parents, base)

        if path:
            mod_cls = path.rstrip('.')
        else:
            # if documenting a class-level object without path,
            # there must be a current class, either from a parent
            # auto directive ...
            mod_cls = current_document.autodoc_class
            # ... or from a class directive
            if not mod_cls:
                mod_cls = ref_context_py_class
                # ... if still falsy, there's no way to know
                if not mod_cls:
                    return None, ()
        module_name, _sep, cls = mod_cls.rpartition('.')
        parents = [cls]
        # if the module name is still missing, get it like above
        if not module_name:
            module_name = current_document.autodoc_module
        if not module_name:
            module_name = ref_context_py_module
        # ... else, it stays None, which means invalid
        return module_name, (*parents, base)

    return None