import os
import sys
import configparser
import inspect
import argparse
import abc
import logging
import warnings
import subprocess
import re
import syslog
import socket
import getpass

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, config="~/.dput.cf"):
        self.config = os.path.expanduser(config)
        dput_cf = configparser.ConfigParser(interpolation=None)
        dput_cf.read(self.config)

        self.parsed = {}
        for section in dput_cf.sections():
            config = dput_cf[section]
            method = config.get("method")
            host, dummy, ftp_port = config.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 list(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 CLI():
    LOG_FORMAT = "%(levelname)s: %(message)s [%(name)s:%(lineno)d, thread=%(threadName)s]"

    def __init__(self, prog, description, epilog=None, allow_unknown=False):
        self.prog = prog
        self.args = None
        self.unknown_args = []
        self.allow_unknown = allow_unknown

        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.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.ROUTES["log"].path.join(mini_buildd.config.LOG_FILE),
            maxBytes=5000000,
            backupCount=9,
            encoding=mini_buildd.config.CHAR_ENCODING)
        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_logging(self):
        # 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 self.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}")

    @classmethod
    def loggers(cls):
        """Overwrite this method for custom subset of 'syslog', 'file', 'console'."""
        return ["console"]

    def setup(self):
        pass

    @abc.abstractmethod
    def runcli(self):
        pass

    def run(self):
        argcomplete.autocomplete(self.parser)
        if self.allow_unknown:
            self.args, self.unknown_args = self.parser.parse_known_args()
        else:
            self.args = self.parser.parse_args()

        self.setup()
        self._setup_logging()

        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.log_exception(LOG, f"{self.prog} failed (try '-vv' to debug)", e, level=logging.ERROR)
            sys.exit(1)


def auth_log(msg):
    """
    Uff. Dirty hack to get an 'auth' log including the ssh fingerprint.

    Needed (for now) by ``m-b-ssh-uploader-command``, ``m-b-ssh-client-command``.

    Needs sshd on loglevel=VERBOSE, and the user needs access to
    auth.log (i.e., add user to group 'adm' in standard Debian).
    """
    def getpppid():
        """Get grandparent PID."""
        return subprocess.check_output(f"/bin/ps -p {os.getppid()} -oppid=", shell=True).strip()

    def get_last_matching_line(file_name, regex):
        result = None
        with mini_buildd.fopen(file_name) as f:
            for line in f:
                if re.match(regex, line):
                    result = line
        return result

    def get_fingerprint():
        try:
            line = get_last_matching_line("/var/log/auth.log", rf"^.*sshd\[{getpppid()}\]: Found matching .* key: .*")
            fp = line.rpartition(" ")[2].strip()
        except BaseException:
            fp = "NO_FINGERPRINT_FOUND"
        return fp

    command = os.environ.get("SSH_ORIGINAL_COMMAND", "NO_COMMAND_FOUND")
    ip = os.environ.get("SSH_CONNECTION", "NO_IP_FOUND").partition(" ")[0]
    try:
        host = socket.gethostbyaddr(ip)[0]
    except BaseException:
        host = ip

    syslog.syslog(syslog.LOG_NOTICE, f"{msg}: [{getpass.getuser()}] {get_fingerprint()}@{host} \"{command}\"")
