import dataclasses
import inspect
import pathlib
from typing import Any
from typing import Dict
from typing import List
from typing import Literal
from typing import Optional
from typing import Union
from unittest import mock

from sphinx.application import Sphinx
from sphinx.cmd.build import main as sphinx_build


@dataclasses.dataclass
class SphinxConfig:
    """Configuration values to pass to the Sphinx application instance."""

    src_dir: str
    """The directory containing the project's source."""

    conf_dir: str
    """The directory containing the project's ``conf.py``."""

    build_dir: str
    """The directory to write build outputs into."""

    builder_name: str
    """The currently used builder name."""

    doctree_dir: str
    """The directory to write doctrees into."""

    config_overrides: Dict[str, Any] = dataclasses.field(default_factory=dict)
    """Any overrides to configuration values."""

    force_full_build: bool = dataclasses.field(default=False)
    """Force a full build on startup."""

    keep_going: bool = dataclasses.field(default=False)
    """Continue building when errors (from warnings) are encountered."""

    num_jobs: Union[Literal["auto"], int] = dataclasses.field(default=1)
    """The number of jobs to use for parallel builds."""

    quiet: bool = dataclasses.field(default=False)
    """Hide standard Sphinx output messages"""

    silent: bool = dataclasses.field(default=False)
    """Hide all Sphinx output."""

    tags: List[str] = dataclasses.field(default_factory=list)
    """Tags to enable during a build."""

    verbosity: int = dataclasses.field(default=0)
    """The verbosity of Sphinx's output."""

    version: Optional[str] = dataclasses.field(default=None)
    """Sphinx's version number."""

    warning_is_error: bool = dataclasses.field(default=False)
    """Treat any warning as an error"""

    @property
    def parallel(self) -> int:
        """The parsed value of the ``num_jobs`` field."""

        if self.num_jobs == "auto":
            import multiprocessing

            return multiprocessing.cpu_count()

        return self.num_jobs

    @classmethod
    def fromcli(cls, args: List[str]):
        """Return the ``SphinxConfig`` instance that's equivalent to the given arguments.

        Parameters
        ----------
        args
           The cli arguments you would normally pass to ``sphinx-build``

        Returns
        -------
        Optional[SphinxConfig]
           ``None`` if the arguments could not be parsed, otherwise the set configuration
           options derived from the sphinx build command.
        """

        if args[0] == "sphinx-build":
            args = args[1:]

        # The easiest way to handle this is to just call sphinx-build but with
        # the Sphinx app object patched out - then we just use all the args it
        # was given!
        with mock.patch("sphinx.cmd.build.Sphinx") as m_Sphinx:
            sphinx_build(args)

        if m_Sphinx.call_args is None:
            return None

        signature = inspect.signature(Sphinx)
        keys = signature.parameters.keys()

        values = m_Sphinx.call_args[0]
        sphinx_args = {k: v for k, v in zip(keys, values)}

        if sphinx_args is None:
            return None

        return cls(
            src_dir=sphinx_args["srcdir"],
            conf_dir=sphinx_args["confdir"],
            build_dir=sphinx_args["outdir"],
            builder_name=sphinx_args["buildername"],
            doctree_dir=sphinx_args["doctreedir"],
            config_overrides=sphinx_args.get("confoverrides", {}),
            force_full_build=sphinx_args.get("freshenv", False),
            keep_going=sphinx_args.get("keep_going", False),
            num_jobs=sphinx_args.get("parallel", 1),
            quiet=sphinx_args.get("status", 1) is None,
            silent=sphinx_args.get("warning", 1) is None,
            tags=sphinx_args.get("tags", []),
            verbosity=sphinx_args.get("verbosity", 0),
            warning_is_error=sphinx_args.get("warningiserror", False),
        )

    def to_application_args(self) -> Dict[str, Any]:
        """Convert this into the equivalent Sphinx application arguments."""

        # On OSes like Fedora Silverblue, `/home` is symlinked to `/var/home`.  This
        # causes issues, since, depending on the origin of any path given to us `/home`
        # may or may not be the true location of the file - which introduces consistency
        # problems throughout the codebase.
        #
        # Resolving these paths here, should ensure that the agent always
        # reports the true location of any given directory.
        conf_dir = pathlib.Path(self.conf_dir).resolve()
        build_dir = pathlib.Path(self.build_dir).resolve()
        doctree_dir = pathlib.Path(self.doctree_dir).resolve()
        src_dir = pathlib.Path(self.src_dir).resolve()

        return {
            "buildername": self.builder_name,
            "confdir": str(conf_dir),
            "confoverrides": self.config_overrides,
            "doctreedir": str(doctree_dir),
            "freshenv": self.force_full_build,
            "keep_going": self.keep_going,
            "outdir": str(build_dir),
            "parallel": self.parallel,
            "srcdir": str(src_dir),
            "status": None,
            "tags": self.tags,
            "verbosity": self.verbosity,
            "warning": None,
            "warningiserror": self.warning_is_error,
        }
