import abc
import argparse
import configparser
import inspect
import logging
import logging.handlers
import os
import pwd
import sys
import warnings

import argcomplete

from mini_buildd import net, util

LOG = logging.getLogger(__name__)


warnings.formatwarning = lambda message, category, filename, lineno, line=None: f"{filename}:{lineno}: {category.__name__}: {message}"


def log_test():
    """For debugging only"""
    LOG.debug("debug")
    LOG.info("info")
    LOG.warning("warning")
    LOG.error("error")
    LOG.critical("critical")


def log_info():
    """For debugging only"""
    for logger in [logging.getLogger()] + [logging.getLogger(name) for name in logging.root.manager.loggerDict]:  # pylint:disable=no-member
        print(f"{logger}: {logger.handlers}")


class StdErrHandler(logging.StreamHandler):
    #: Log levels that we use throughout the code (python defaults) mapped to numerical counterparts used in syslog/systemd
    SYSTEMD_PREFIX = {} if sys.stderr.isatty() else {
        logging.getLevelNamesMapping()["CRITICAL"]: "<2>",
        logging.getLevelNamesMapping()["ERROR"]: "<3>",
        logging.getLevelNamesMapping()["WARNING"]: "<4>",
        logging.getLevelNamesMapping()["INFO"]: "<6>",
        logging.getLevelNamesMapping()["DEBUG"]: "<7>",
    }

    #: Generic log format
    LOG_FORMAT = "{systemd_prefix}{levelname[0]}: {message} [{threadName}({thread}), {name}({filename}:{lineno})]"

    def __init__(self):
        super().__init__(stream=sys.stderr)
        self.setFormatter(logging.Formatter(self.LOG_FORMAT, style="{"))

    def emit(self, record):
        record.systemd_prefix = self.SYSTEMD_PREFIX.get(record.levelno, "")
        return super().emit(record)


class DputCf():
    """Guess possible mini-buildd targets and their URL endpoints"""

    DEFAULT_PATH = "~/.dput.cf"

    def __init__(self, config_path=DEFAULT_PATH):
        self.config = os.path.expanduser(config_path)
        dput_cf = configparser.ConfigParser(interpolation=None)
        dput_cf.read(self.config)

        self.parsed = {}
        for section in dput_cf.sections():
            cfg = dput_cf[section]
            method = cfg.get("method")
            host, dummy, ftp_port = cfg.get("fqdn").partition(":")
            if section.startswith("mini-buildd") and method in ["ftp", "ftps"]:
                http_port = int(ftp_port) - 1
                http_method = "https" if method == "ftps" else "http"
                self.parsed[section] = {}
                self.parsed[section]["http_url"] = f"{http_method}://{host}:{http_port}"
                self.parsed[section]["ftp_url"] = f"{method}://{host}:{ftp_port}"

    def first_target(self):
        return next(iter(self.parsed))

    def target_completer(self, **_kwargs):
        return self.parsed.keys()

    def first_http_url(self):
        return self.parsed[self.first_target()]["http_url"]

    def http_url_completer(self, **_kwargs):
        return [target["http_url"] for target in self.parsed.values()]

    def get_target_ftp_url(self, target):
        return self.parsed[target]["ftp_url"]

    def get_target_http_url(self, target):
        return self.parsed[target]["http_url"]


class ArgumentDefaultsRawTextHelpFormatter(argparse.RawTextHelpFormatter, argparse.ArgumentDefaultsHelpFormatter):
    """Custom argparse (for mini-buildd[-tool]) help formatter (mixin): We like to use raw text, but also have default values shown"""


class CLI():
    LOG_LEVEL_DEFAULT = ["WARNING", "mini_buildd.INFO"]

    @classmethod
    def log_level_parse(cls, value, set_level=False):
        logger, dummy, level = value.rpartition(".")
        if level not in logging.getLevelNamesMapping():
            raise argparse.ArgumentTypeError(f"{level}: Invalid log level")
        # print(f"PSD: {logger or 'root'}={level}")
        if set_level:
            logging.getLogger(logger).setLevel(level)
            # print(f"SET: {logger or 'root'}={level}")
        return value

    @classmethod
    def log_level_completer(cls, **_kwargs):
        return logging.getLevelNamesMapping().keys()

    def __init__(self, prog, description, epilog=None, run_as_mini_buildd=False):
        self.prog = prog
        self.args = None
        self._user = pwd.getpwuid(os.getuid())[0]
        self.run_as_mini_buildd = run_as_mini_buildd

        self.parser = argparse.ArgumentParser(prog=prog, description=description, epilog=epilog,
                                              formatter_class=ArgumentDefaultsRawTextHelpFormatter)
        self.parser.add_argument("--version", action="version", version=util.__version__)
        self.parser.add_argument("-l", "--log-level", action="append", type=self.log_level_parse,
                                 help=(
                                     f"set log level (if none given, {self.LOG_LEVEL_DEFAULT} is applied by default).\n"
                                     "May also be given as ``<logger>.<level>`` to effect individual loggers only,\n"
                                     "and multiple times (check existing logs for actual logger names)."
                                 )).completer = CLI.log_level_completer
        if run_as_mini_buildd:
            self.parser.add_argument("-U", "--dedicated-user", action="store", default="mini-buildd",
                                     help="Force a custom dedicated user name (to run as a different user than 'mini-buildd').")

    @classmethod
    def _add_endpoint(cls, parser):
        parser.add_argument("endpoint", action="store",
                            metavar="ENDPOINT",
                            help=f"HTTP target endpoint: {inspect.getdoc(net.ClientEndpoint)}").completer = DputCf().http_url_completer

    @classmethod
    def _add_subparser(cls, subparser, cmd, doc):
        return subparser.add_parser(cmd, help=doc, formatter_class=ArgumentDefaultsRawTextHelpFormatter)

    @abc.abstractmethod
    def runcli(self):
        pass

    def run(self):
        argcomplete.autocomplete(self.parser)
        self.args = self.parser.parse_args()

        # As 'append' would append to default, we handle default values manually (https://bugs.python.org/issue16399)
        if self.args.log_level is None:
            self.args.log_level = self.LOG_LEVEL_DEFAULT

        logging.root.handlers = [StdErrHandler()]
        for log_level in self.args.log_level:
            self.log_level_parse(log_level, set_level=True)

        # Raise exceptions in a debug setup only (see https://docs.python.org/3.5/howto/logging.html#exceptions-raised-during-logging)
        logging.raiseExceptions = logging.root.isEnabledFor(logging.DEBUG)

        # Always capture possible python warnings to py.warning logger
        logging.captureWarnings(True)

        # If not set explicitly otherwise (PYTHONWARNINGS, -W): Default to ignore python warnings
        if not sys.warnoptions:
            warnings.simplefilter("ignore")

        try:
            if self.run_as_mini_buildd and self.args.dedicated_user != self._user:
                raise util.HTTPUnauthorized(f"Run as user 'mini-buildd' only (use '--dedicated-user={self._user}' if you really want this, will write to that user's $HOME!)")

            self.runcli()
        except BaseException as e:
            util.log_exception(LOG, f"{self.prog} failed (try '--log-level DEBUG')", e, level=logging.ERROR)
            sys.exit(1)
