"""
Base class and function utilities for cli
=========================================
"""
import argparse
import logging
import os
import sys
import textwrap

import orion
import orion.core
from orion.core.io.database import DatabaseError
from orion.core.utils.exceptions import (
    BranchingEvent,
    InexecutableUserScript,
    MissingResultFile,
    NoConfigurationError,
    NoNameError,
)

logger = logging.getLogger(__name__)

CLI_DOC_HEADER = "Oríon CLI for asynchronous distributed optimization"


class OrionArgsParser:
    """Parser object handling the upper-level parsing of Oríon's arguments."""

    def __init__(self, description=CLI_DOC_HEADER):
        """Create the pre-command arguments"""
        self.description = description

        self.parser = argparse.ArgumentParser(
            formatter_class=argparse.RawDescriptionHelpFormatter,
            description=textwrap.dedent(description),
        )

        self.parser.add_argument(
            "-V",
            "--version",
            action="version",
            version="orion " + orion.core.__version__,
        )

        self.parser.add_argument(
            "-v",
            "--verbose",
            action="count",
            default=0,
            help="logging levels of information about the process (-v: INFO. -vv: DEBUG)",
        )

        self.parser.add_argument(
            "--logdir",
            type=str,
            default=None,
            help="Path to a directory to store logs",
        )

        self.parser.add_argument(
            "-d",
            "--debug",
            action="store_true",
            help="Use debugging mode with EphemeralDB.",
        )

        self.subparsers = self.parser.add_subparsers(dest="command")

    def get_subparsers(self):
        """Return the subparser object for this parser."""
        return self.subparsers

    def parse(self, argv):
        """Call argparse and generate a dictionary of arguments' value"""
        args = vars(self.parser.parse_args(argv))

        verbose = args.pop("verbose", 0)
        levels = {0: logging.WARNING, 1: logging.INFO, 2: logging.DEBUG}

        logdir = args.pop("logdir")
        logfile = None
        if logdir is not None:
            os.makedirs(logdir, exist_ok=True)

            pid = os.getpid()
            logfile = os.path.join(logdir, f"orion_{pid}.log")

        logging.basicConfig(
            format="%(asctime)-15s::%(levelname)s::%(name)s::%(message)s",
            level=levels.get(verbose, logging.DEBUG),
            filename=logfile,
        )
        logger.debug("Orion version : %s", orion.core.__version__)

        if args["command"] is None:
            self.parser.parse_args(["--help"])

        function = args.pop("func", None)
        empty_command = (argv[-1] if argv else sys.argv[-1]) == args["command"]
        if function is None or (empty_command and args.pop("help_empty", False)):
            self.parser.parse_args([args["command"], "--help"])

        return args, function

    def execute(self, argv):
        """Execute main function of the subparser"""
        try:
            args, function = self.parse(argv)
            returncode = function(args)
        except (
            NoConfigurationError,
            NoNameError,
            DatabaseError,
            MissingResultFile,
            BranchingEvent,
            InexecutableUserScript,
        ) as e:
            print("Error:", e, file=sys.stderr)

            if args.get("verbose", 0) >= 2:
                raise e

            return 1

        except KeyboardInterrupt:
            print("Orion is interrupted.")
            return 130

        return 0 if returncode is None else returncode


def get_basic_args_group(
    parser,
    group_name="Oríon arguments",
    group_help="These arguments determine orion's behaviour",
):
    """Return the basic arguments for any command."""
    basic_args_group = parser.add_argument_group(group_name, description=group_help)

    basic_args_group.add_argument(
        "-n",
        "--name",
        type=str,
        metavar="stringID",
        help="experiment's unique name; "
        "(default: None - specified either here or in a config)",
    )

    basic_args_group.add_argument(
        "-u",
        "--user",
        type=str,
        help="user associated to experiment's unique name; "
        "(default: $USER - can be overridden either here or in a config)",
    )

    basic_args_group.add_argument(
        "-v",
        "--version",
        type=int,
        help="specific version of experiment to fetch; "
        "(default: None - latest experiment.)",
    )

    basic_args_group.add_argument(
        "-c",
        "--config",
        type=argparse.FileType("r"),
        metavar="path-to-config",
        help="user provided " "orion configuration file",
    )

    return basic_args_group


def get_user_args_group(parser):
    """
    Return the user group arguments for any command.
    User group arguments are composed of the user script and the user args
    """
    usergroup = parser.add_argument_group(
        "User script related arguments",
        description="These arguments determine user's script behaviour "
        "and they can serve as orion's parameter declaration.",
    )

    usergroup.add_argument(
        "user_args",
        nargs=argparse.REMAINDER,
        metavar="...",
        help="Command line of user script. A configuration "
        "file intended to be used with 'userscript' must be given as a path "
        "in the **first positional** argument OR using `--config=<path>` "
        "keyword argument.",
    )

    return usergroup
