import os
import sys
import configparser
import inspect
import argparse
import abc
import logging
import warnings

import argcomplete

import mini_buildd.misc
import mini_buildd.net
import mini_buildd.config

LOG = logging.getLogger(__name__)


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 DputCf():
    """Guess possible mini-buildd targets and their URL endpoints."""

    def __init__(self, path="~/.dput.cf"):
        dput_cf = configparser.ConfigParser(interpolation=None)
        dput_cf.read(os.path.expanduser(path))

        self.parsed = {}
        for section in dput_cf.sections():
            config = dput_cf[section]
            host, dummy, ftp_port = config.get("fqdn").partition(":")
            if ftp_port and config.get("method") == "ftp":  # method 'ftp' and explicit port is our heuristic
                http_port = int(ftp_port) - 1
                self.parsed[section] = {}
                self.parsed[section]["http_url"] = f"http://{host}:{http_port}"
                self.parsed[section]["ftp_url"] = f"ftp://{host}:{ftp_port}"

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

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

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

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

    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 CLI():
    LOG_FORMAT = "%(levelname)s: %(message)s [%(name)s:%(lineno)d, thread=%(threadName)s]"

    def __init__(self, prog, description, epilog=None):
        self.prog = prog
        mini_buildd.misc.setup_console_logging(logging.DEBUG)

        self.parser = argparse.ArgumentParser(prog=prog, description=description, epilog=epilog,
                                              formatter_class=ArgumentDefaultsRawTextHelpFormatter)
        self.parser.add_argument("--version", action="version", version=mini_buildd.__version__)
        self.parser.add_argument("-v", "--verbose", dest="verbosity", action="count", default=0,
                                 help="increase log level. Give twice for max logs")
        self.parser.add_argument("-q", "--quiet", dest="terseness", action="count", default=0,
                                 help="decrease log level. Give twice for min logs")
        self.args = None
        self.logger = logging.getLogger("mini_buildd")
        self.wlogger = logging.getLogger("py.warnings")

    @classmethod
    def _add_endpoint(cls, parser):
        parser.add_argument("endpoint", action="store",
                            metavar="ENDPOINT",
                            help=f"HTTP target endpoint: {inspect.getdoc(mini_buildd.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)

    def _loglevel(self):
        return logging.WARNING - (10 * (min(2, self.args.verbosity) - min(2, self.args.terseness)))

    @staticmethod
    def _daemon_log_handler_file():
        handler = logging.handlers.RotatingFileHandler(
            mini_buildd.config.LOG_FILE,
            maxBytes=5000000,
            backupCount=9,
            encoding="UTF-8")
        handler.setFormatter(logging.Formatter("%(asctime)s " + CLI.LOG_FORMAT))
        return handler

    @staticmethod
    def _daemon_log_handler_syslog():
        handler = logging.handlers.SysLogHandler(
            address="/dev/log",
            facility=logging.handlers.SysLogHandler.LOG_USER)
        handler.setFormatter(logging.Formatter(CLI.LOG_FORMAT))
        return handler

    @staticmethod
    def _daemon_log_handler_console():
        handler = logging.StreamHandler()
        handler.setFormatter(logging.Formatter("%(asctime)s " + CLI.LOG_FORMAT))
        return handler

    def _setup_daemon_logging(self, loggers):
        if self.args.foreground:
            loggers.append("console")

        # Clear all loggers now.
        self.logger.handlers = []

        # Try to add all loggers; collect exceptions to be able
        # to do error reporting later, when hopefully one valid
        # handler is set up.
        loggers_failed = {}
        for typ in loggers:
            try:
                handler_func = getattr(self, "_daemon_log_handler_" + typ)
                self.logger.addHandler(handler_func())
            except BaseException as e:
                loggers_failed[typ] = e

        # Properly set raiseException based "exception" debug value given (see https://docs.python.org/3.5/howto/logging.html#exceptions-raised-during-logging)
        logging.raiseExceptions = "exception" in mini_buildd.config.DEBUG

        # Finally, log all errors now that occurred while setting up loggers
        for typ, err in list(loggers_failed.items()):
            LOG.critical(f"Logger {typ} failed: {err}")

    def setup(self):
        pass

    @abc.abstractmethod
    def runcli(self):
        pass

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

        self.setup()

        self.logger.setLevel(self._loglevel())
        self.wlogger.setLevel(self._loglevel())
        if self.logger.getEffectiveLevel() <= logging.DEBUG:
            self.wlogger.handlers = self.logger.handlers
            logging.captureWarnings(True)
            if not sys.warnoptions:
                warnings.simplefilter("default")
            mini_buildd.config.DEBUG = ["exception"]

        try:
            self.runcli()
        except BaseException as e:
            mini_buildd.config.log_exception(LOG, f"{self.prog} failed (try '-vv' to debug)", e)
            sys.exit(1)
