"""
Description
-----------

:func:`flasket.cli.make_flasket` allows a user to quickly create a Flasket application. It's an all-in-one command-line oriented function.

Functions
---------

make_flasket
^^^^^^

.. autofunction:: make_flasket


"""
import argparse
import os
import sys
import typing as t

from torxtools import xdgtools
from torxtools.cfgtools import which
from torxtools.pathtools import expandpath
from yaml import safe_load

from ._utils import deepmerge
from .defaults import CFGFILE_SEARCH_PATHS, default_configuration
from .middleware import FlaskFlasketCreator, GunicornFlasketCreator

__all__: t.List[str] = [
    "make_flasket",
]


def _read(cfgfile: str) -> t.Dict:
    """
    Convenience function in order to be mocked.

    Parameters
    ----------
    cfgfile: str

        a single path representing a yaml file.

    Returns
    -------
    dict:

        a dictionary
    """
    with open(cfgfile, encoding="UTF-8") as fd:
        data = safe_load(fd)
    return data or {}


def _parse_arguments(
    *,
    argv,
    cfgfile,
    cfgfilepaths,
    creator_class,
    defaults,
    description,
):
    """ """
    if not argv:
        return {}

    # argument_default=None does not set the default to None for boolean options,
    # so we'll specifically set default=None for those values
    #
    # Default values aren't actually added/set here, but in the FlasketSettings,
    # We only care about values that were specified.
    parser = argparse.ArgumentParser(
        description=description,
        argument_default=None,
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    # Create helptext for UI option
    b_ui = {True: "enabled", False: "disabled"}[defaults["server"]["ui"]]
    if cfgfilepaths:
        # Keep on two lines, otherwise line continuation will make
        # an obsure argparse bug appear
        helpmsg_cfgfile = "Use CFGFILE as configuration file, "
        helpmsg_cfgfile += f"otherwise first file found in search path is used. (default search path: {cfgfilepaths})"
    else:
        helpmsg_cfgfile = "Use CFGFILE as configuration file."

    # fmt: off
    parser.add_argument(
        "-l", "--listen", metavar="HOST",
        help=f'The ip to listen on (default: {defaults["server"]["listen"]})',
    )
    parser.add_argument(
        "-p", "--port", metavar="PORT", type=int,
        help=f'The port to listen on (default: {defaults["server"]["port"]})',
    )
    # Is configuration file already specified?
    if not cfgfile:
        parser.add_argument(
        "-c", "--cfgfile", metavar="CFGFILE",
        help=helpmsg_cfgfile,
        )
    parser.add_argument(
        "--ui", action="store_true", default=None,
        help=f"Enable the OpenAPI UI. Disable with --no-ui. (default: {b_ui})",
    )
    parser.add_argument(
        "--no-ui", action="store_false", default=None, dest="ui",
        help=argparse.SUPPRESS,
    )
    # fmt: on
    creator_class.add_arguments(defaults["server"], parser)
    args = parser.parse_args(argv)
    return vars(args)


def make_flasket(
    rootpath: str,
    middleware: str = "gunicorn",
    *,
    argv: t.List[str] = None,
    cfgfile=None,
    cfgfilename=None,
    cfgfilepaths=None,
    default_cfg: t.Dict[str, t.Any] = None,
    description="Flasket server",
    options=None,
):
    """
    Parameters
    ----------
    rootpath: str

        path of location where the 'api', 'htdocs' and 'app' directories will be served.

    middleware: str

        Middleware to use: 'gunicorn' for production, 'flask' for debug.

    argv: list[str]

        Uses :code:`sys.argv[1:]` if None. Use :code:`[]` if you desire to deactive argument parsing.

    cfgfile: str

        Configuration file to use. Overrides searching and asking for a configuration file.

    cfgfilename: str

        Configuration filename to use. Typically this is your packagename with a 'yml' extension.

    cfgfilepaths: str

        Paths to search for in order to find the configuration file. Every value of the list must contain the placeholder '{cfgfilename}' such as :code:`["/home/jdoe/{cfgfilename}"]`

    default_cfg: dict, default: :meth:`flasket.defaults.default_configuration`

        Dictionary containing the defaults for the command line arguments and configuration.
        Passed value will be merged with the default factory configuration, command line arguments
        and the configuration file that was read.

    description: str

        Text description to use in argparse.

    options: dict

        Options to be passed down to backends.

        .. code-block:: python

          {"static": {"redirect_to": "/app/", "redirect_code": 308}}
    """
    # Verify input
    assert True in [argv is None, isinstance(argv, list)], "error: argv must be None or a list"

    # rootpath was probably passed by "__file__"
    if rootpath and os.path.isfile(rootpath):
        rootpath = os.path.dirname(rootpath)

    # Sets environment variables for XDG paths
    xdgtools.setenv()

    # Verify middleware exists
    middleware = middleware.lower().strip()
    if middleware not in ["flask", "gunicorn"]:
        raise ValueError('middleware argument must be in ["flask", "gunicorn", "test"]')
    if middleware == "flask":
        creator_class = FlaskFlasketCreator()
    if middleware == "gunicorn":
        creator_class = GunicornFlasketCreator()

    # Prepare search path for configuration file. Disable it if we're looking
    # for a impossible (None) file name
    if cfgfilepaths is None:
        cfgfilepaths = CFGFILE_SEARCH_PATHS
    cfgfilepaths = [e.format(cfgfilename=cfgfilename) for e in cfgfilepaths]

    # Create a default configuration from what was passed
    # and what we set. Other values are filtered
    defaults = default_configuration(default_cfg)

    # Parse arguments if they exist
    if argv is None:
        argv = sys.argv[1:]

    arguments = _parse_arguments(
        argv=argv,
        cfgfile=cfgfile,
        cfgfilepaths=cfgfilepaths,
        creator_class=creator_class,
        defaults=defaults,
        description=description,
    )

    # Copy arguments over to an dict with server
    arguments = {k: v for k, v in arguments.items() if v is not None}
    cfgfile = arguments.pop("cfgfile", cfgfile)
    arguments = {"server": arguments}

    # Search for the configuration file
    if not cfgfile and cfgfilepaths:
        # Search for cfgfile
        cfgfilepaths = [e.format(cfgfilename=cfgfilename) for e in cfgfilepaths]
        cfgfile = which(cfgfile, expandpath(cfgfilepaths))

    filedata = _read(cfgfile) or {}

    # Merge in the reverse order of priority
    cfg = defaults
    cfg = deepmerge(cfg, filedata)
    cfg = deepmerge(cfg, arguments)
    creator_class.force_cfg(defaults["server"])

    return creator_class.flasket(rootpath=rootpath, cfg=cfg, options=options)
