from __future__ import annotations

from typing import TYPE_CHECKING

from docutils import nodes

from sphinx.ext.autodoc._directive_options import _process_documenter_options
from sphinx.ext.autodoc._generate import _auto_document_object
from sphinx.ext.autodoc._shared import LOGGER, _AutodocAttrGetter, _AutodocConfig
from sphinx.util.docutils import SphinxDirective, switch_source_input
from sphinx.util.parsing import nested_parse_to_nodes

if TYPE_CHECKING:
    from collections.abc import Callable

    from docutils.nodes import Node
    from docutils.parsers.rst.states import RSTState
    from docutils.statemachine import StringList

    from sphinx.ext.autodoc._property_types import _AutodocObjType


class DummyOptionSpec(dict[str, 'Callable[[str], str]']):  # NoQA: FURB189
    """An option_spec allows any options."""

    def __bool__(self) -> bool:
        """Behaves like some options are defined."""
        return True

    def __getitem__(self, _key: str) -> Callable[[str], str]:
        return lambda x: x


def parse_generated_content(
    state: RSTState, content: StringList, titles_allowed: bool
) -> list[Node]:
    """Parse an item of content generated by _auto_document_object()."""
    with switch_source_input(state, content):
        if titles_allowed:
            return nested_parse_to_nodes(state, content)

        node = nodes.paragraph()
        # necessary so that the child nodes get the right source/line set
        node.document = state.document
        state.nested_parse(content, 0, node, match_titles=False)
        return node.children


class AutodocDirective(SphinxDirective):
    """A directive class for all autodoc directives.

    It generates the directive lines for the given object,
    then parses and returns the generated content.
    """

    option_spec = DummyOptionSpec()
    has_content = True
    required_arguments = 1
    optional_arguments = 0
    final_argument_whitespace = True

    def run(self) -> list[Node]:
        source, lineno = self.get_source_info()
        LOGGER.debug('[autodoc] %s:%s: input:\n%s', source, lineno, self.block_text)

        # get target object type / strip prefix (auto-)
        assert self.name.startswith('auto')
        objtype: _AutodocObjType = self.name[4:]  # type: ignore[assignment]

        env = self.env

        #: true if the generated content may contain titles
        titles_allowed = True

        # process the options with the selected object type's option_spec
        try:
            documenter_options = _process_documenter_options(
                obj_type=objtype,
                default_options=self.config.autodoc_default_options,
                options=self.options,
            )
        except (KeyError, ValueError, TypeError) as exc:
            # an option is either unknown or has a wrong type
            LOGGER.error(  # NoQA: TRY400
                'An option to %s is either unknown or has an invalid value: %s',
                self.name,
                exc,
                exc_info=exc,
                location=(env.current_document.docname, lineno),
            )
            return []
        documenter_options._tab_width = self.state.document.settings.tab_width

        # record all filenames as dependencies -- this will at least
        # partially make automatic invalidation possible
        record_dependencies = self.state.document.settings.record_dependencies

        # generate the output
        content = _auto_document_object(
            name=self.arguments[0],
            obj_type=objtype,
            current_document=env.current_document,
            config=_AutodocConfig.from_config(env.config),
            events=env.events,
            get_attr=_AutodocAttrGetter(env._registry.autodoc_attrgetters),
            more_content=self.content,
            options=documenter_options,
            record_dependencies=record_dependencies,
            ref_context=env.ref_context,
            reread_always=env.reread_always,
        )
        if not content:
            return []

        LOGGER.debug('[autodoc] output:\n%s', '\n'.join(content))

        return parse_generated_content(self.state, content, titles_allowed)
