"""
Improve the docstrings of Django apps.

For example:

* List all model and form fields as parameters
  (see:mod:`~sphinxcontrib_django.docstrings.classes`)
* Improve field representations in the documentation
  (see :mod:`~sphinxcontrib_django.docstrings.attributes`)
* Add information about autogenerated methods
  (see :mod:`~sphinxcontrib_django.docstrings.methods`)
* Improve the appearance of static iterable data
  (see :mod:`~sphinxcontrib_django.docstrings.data`)
* Fix the intersphinx mappings to the Django documentation
  (see :mod:`~sphinxcontrib_django.docstrings.patches`)
"""
from __future__ import annotations

import importlib
import os
from typing import TYPE_CHECKING

import django
from sphinx.errors import ConfigError

from .. import __version__
from .attributes import improve_attribute_docstring
from .classes import improve_class_docstring
from .config import CHOICES_LIMIT, EXCLUDE_MEMBERS, INCLUDE_MEMBERS
from .data import improve_data_docstring
from .methods import improve_method_docstring

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


def setup(app: Sphinx) -> dict:
    """
    Allow this package to be used as Sphinx extension.

    This is also called from the top-level :meth:`~sphinxcontrib_django.setup`.

    It connects to the sphinx events :event:`autodoc-skip-member` and
    :event:`autodoc-process-docstring`.

    Additionally, the sphinx config value ``django_settings`` is added via
    :meth:`~sphinx.application.Sphinx.add_config_value` and
    :meth:`~sphinxcontrib_django.docstrings.setup_django` is called on the
    :event:`config-inited` event.

    :param app: The Sphinx application object
    """
    from .patches import patch_django_for_autodoc

    # When running, make sure Django doesn't execute querysets
    # Fix module paths for intersphinx mappings
    patch_django_for_autodoc()

    # Register custom event which can be emitted after Django has been set up
    app.add_event("django-configured")

    # Set default to environment variable to enable backwards compatibility
    app.add_config_value(
        "django_settings", os.environ.get("DJANGO_SETTINGS_MODULE"), True
    )

    # Django models tables names configuration.
    # Set default of django_show_db_tables to False
    app.add_config_value("django_show_db_tables", False, True)
    # Set default of django_show_db_tables_abstract to False
    app.add_config_value("django_show_db_tables_abstract", False, True)
    # Integer amount of model field choices to show
    app.add_config_value("django_choices_to_show", CHOICES_LIMIT, True)
    # Setup Django after config is initialized
    app.connect("config-inited", setup_django)

    # Load sphinx.ext.autodoc extension before registering events
    app.setup_extension("sphinx.ext.autodoc")

    # Generate docstrings for Django model fields
    # Register the docstring processor with sphinx
    app.connect("autodoc-process-docstring", improve_docstring)

    # influence skip rules
    app.connect("autodoc-skip-member", autodoc_skip)

    return {
        "version:": __version__,
        "parallel_read_safe": True,
        "parallel_write_safe": True,
    }


def setup_django(app: Sphinx, config: Config) -> None:
    """
    This function calls :func:`django.setup` so it doesn't have to be done in the app's
    ``conf.py``.

    Called on the :event:`config-inited` event.

    :param app: The Sphinx application object

    :param config: The Sphinx configuration

    :raises ~sphinx.errors.ConfigError: If setting ``django_settings`` is not set correctly
    """
    if not config.django_settings:
        raise ConfigError(
            "Please specify your Django settings in the configuration 'django_settings'"
            " in your conf.py"
        )
    try:
        importlib.import_module(config.django_settings)
    except ModuleNotFoundError as e:
        raise ConfigError(
            "The module you specified in the configuration 'django_settings' in your"
            " conf.py cannot be imported. Make sure the module path is correct and the"
            " source directory is added to sys.path."
        ) from e
    os.environ["DJANGO_SETTINGS_MODULE"] = config.django_settings
    django.setup()

    # Emit event to allow code which depends on Django to run
    app.emit("django-configured")


def autodoc_skip(
    app: Sphinx, what: str, name: str, obj: object, options: Options, lines: list[str]
) -> bool | None:
    """
    Hook to tell autodoc to include or exclude certain fields (see :event:`autodoc-skip-member`).

    Sadly, it doesn't give a reference to the parent object,
    so only the ``name`` can be used for referencing.

    :param app: The Sphinx application object
    :param what: The parent type, ``class`` or ``module``
    :param name: The name of the child method/attribute.
    :param obj: The child value (e.g. a method, dict, or module reference)
    :param options: The current autodoc settings.
    """
    if name in EXCLUDE_MEMBERS:
        return True

    if name in INCLUDE_MEMBERS:
        return False

    return None


def improve_docstring(
    app: Sphinx, what: str, name: str, obj: object, options: Options, lines: list[str]
) -> list[str]:
    """
    Hook to improve the autodoc docstrings for Django models
    (see :event:`autodoc-process-docstring`).

    :param what: The type of the object which the docstring belongs to (one of ``module``,
                 ``class``, ``exception``, ``function``, ``method`` and ``attribute``)
    :param name: The fully qualified name of the object
    :param obj: The documented object
    :param options: The options given to the directive: an object with attributes
                    ``inherited_members``, ``undoc_members``, ``show_inheritance`` and ``noindex``
                    that are ``True`` if the flag option of same name was given to the auto
                    directive
    :param lines: A list of strings – the lines of the processed docstring – that the event
                  handler can modify in place to change what Sphinx puts into the output.
    :return: The modified list of lines
    """
    if what == "class":
        improve_class_docstring(app, obj, lines)
    elif what == "attribute":
        improve_attribute_docstring(app, obj, name, lines)
    elif what == "method":
        improve_method_docstring(name, lines)
    elif what == "data":
        improve_data_docstring(obj, lines)

    # Return the extended docstring
    return lines
