"""Custom patches to make the world work."""

from __future__ import annotations

from functools import lru_cache
from typing import TYPE_CHECKING, Any

from docutils import nodes
from docutils.parsers.rst.directives.admonitions import BaseAdmonition
from docutils.parsers.rst.states import Body, Text
from sphinx.ext.napoleon.docstring import GoogleDocstring

from .attributes_patch import patch_attribute_handling

if TYPE_CHECKING:
    from sphinx.application import Sphinx
    from sphinx.ext.autodoc import Options


def napoleon_numpy_docstring_return_type_processor(  # ruff:ignore[too-many-arguments, too-many-positional-arguments]
    app: Sphinx,
    what: str,
    name: str,  # ruff:ignore[unused-function-argument]
    obj: Any,  # ruff:ignore[unused-function-argument]
    options: Options | None,  # ruff:ignore[unused-function-argument]
    lines: list[str],
) -> None:
    """Insert a : under Returns: to tell napoleon not to look for a return type."""
    if what not in {"function", "method"}:
        return
    if not getattr(app.config, "napoleon_numpy_docstring", False):
        return

    # Search for the returns header:
    # Returns:
    # --------
    for pos, line in enumerate(lines[:-2]):
        if line.lower().strip(":") not in {"return", "returns"}:
            continue
        # Underline detection.
        chars = set(lines[pos + 1].strip())
        # Napoleon allows the underline to consist of a bunch of weirder things...
        if len(chars) != 1 or next(iter(chars)) not in "=-~_*+#":
            continue
        pos += 2  # ruff:ignore[redefined-loop-name]
        break
    else:
        return

    lines.insert(pos, ":")


def fix_napoleon_numpy_docstring_return_type(app: Sphinx) -> None:
    """If no return type is explicitly set, numpy docstrings will use the return type text as return types."""
    # standard priority is 500. Setting priority to 499 ensures this runs before
    # napoleon's docstring processor.
    app.connect("autodoc-process-docstring", napoleon_numpy_docstring_return_type_processor, priority=499)


def _patched_lookup_annotation(*_args: Any) -> str:
    """
    GoogleDocstring._lookup_annotation sometimes adds incorrect type annotations to constructor parameters.

    Disable it so we can handle this on our own.
    """
    return ""


def _patch_google_docstring_lookup_annotation() -> None:
    """Fix issue https://github.com/tox-dev/sphinx-autodoc-typehints/issues/308."""
    GoogleDocstring._lookup_annotation = _patched_lookup_annotation  # type: ignore[assignment] # ruff:ignore[private-member-access]


orig_base_admonition_run = BaseAdmonition.run


def _patched_base_admonition_run(self: BaseAdmonition) -> Any:
    result = orig_base_admonition_run(self)
    result[0].line = self.lineno
    return result


orig_text_indent = Text.indent


def _patched_text_indent(self: Text, *args: Any) -> Any:
    _, line = self.state_machine.get_source_and_line()  # ty: ignore[unresolved-attribute]
    result = orig_text_indent(self, *args)
    node = self.parent[-1]  # ty: ignore[not-subscriptable]
    if node.tagname == "system_message":
        node = self.parent[-2]  # ty: ignore[not-subscriptable]
    node.line = line
    return result


def _patched_body_doctest(
    self: Body, _match: None, _context: None, next_state: str | None
) -> tuple[list[Any], str | None, list[Any]]:
    assert self.document.current_line is not None  # ruff:ignore[assert]
    line = self.document.current_line + 1
    data = "\n".join(self.state_machine.get_text_block())  # ty: ignore[unresolved-attribute]
    n = nodes.doctest_block(data, data)
    n.line = line
    self.parent += n  # ty: ignore[unsupported-operator]
    return [], next_state, []


def _patch_line_numbers() -> None:
    """
    Make the rst parser put line numbers on more nodes.

    When the line numbers are missing, we have a hard time placing the :rtype:.
    """
    Text.indent = _patched_text_indent  # type: ignore[method-assign]
    BaseAdmonition.run = _patched_base_admonition_run  # type: ignore[method-assign,assignment]
    Body.doctest = _patched_body_doctest  # type: ignore[method-assign]


_OVERLOADS_CACHE: dict[str, dict[Any, Any]] = {}


@lru_cache
def fix_directive_based_signature_formatting() -> None:
    """
    Patch Sphinx 9's new directive-based autodoc to cache and clear overload detection.

    Overloads are cached before being cleared so we can render them in docstrings with
    proper type formatting instead of in signatures where types can't be cross-referenced.
    """
    from sphinx.pycode import ModuleAnalyzer  # ruff:ignore[import-outside-top-level]

    original_analyze = ModuleAnalyzer.analyze

    def patched_analyze(self: Any) -> None:
        original_analyze(self)
        if self.overloads:
            _OVERLOADS_CACHE[self.modname] = dict(self.overloads)
        self.overloads = {}

    ModuleAnalyzer.analyze = patched_analyze  # type: ignore[method-assign]


def install_patches(app: Sphinx) -> None:
    """
    Install the patches.

    :param app: the Sphinx app
    """
    # For Sphinx 9+ directive-based architecture
    fix_directive_based_signature_formatting()
    patch_attribute_handling(app)
    _patch_google_docstring_lookup_annotation()
    fix_napoleon_numpy_docstring_return_type(app)
    _patch_line_numbers()


__all__ = [
    "_OVERLOADS_CACHE",
    "install_patches",
]
