import os
import sys
import configparser
import inspect
import argparse
import abc
import logging
import logging.handlers
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__)

#: Generic log format
LOG_FORMAT = "%(levelname).1s: %(message)s"
LOG_FORMAT_DEBUG = LOG_FORMAT + " [%(name)s:%(lineno)d, thread=%(threadName)s]"

#: Log levels that we use throughout the code (python defaults), mapped to numerical counterparts used in syslog/systemd
LOG_LEVELS = {
    logging.getLevelName(logging.CRITICAL): "2",
    logging.getLevelName(logging.ERROR): "3",
    logging.getLevelName(logging.WARNING): "4",
    logging.getLevelName(logging.INFO): "6",
    logging.getLevelName(logging.DEBUG): "7",
}


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


def log_clone(dst, level=None, src="mini_buildd"):
    """Set up logger named 'dst' with the same handlers and loglevel as the logger named 'src'."""
    src_log = logging.getLogger(src)
    dst_log = logging.getLogger(dst)
    dst_log.handlers = []
    for h in src_log.handlers:
        dst_log.addHandler(h)
    dst_log.setLevel(src_log.getEffectiveLevel() if level is None else level)


class ConsoleHandler(logging.StreamHandler):
    def __init__(self):
        super().__init__(stream=sys.stderr)
        self.setFormatter(logging.Formatter(LOG_FORMAT))


class ConsoleSystemDHandler(ConsoleHandler):
    def __init__(self):
        super().__init__()
        self.setFormatter(logging.Formatter(LOG_FORMAT_DEBUG))

    def emit(self, record):
        try:
            self.stream.write(f"<{LOG_LEVELS[record.levelname]}>{self.format(record)}" + self.terminator)
            self.flush()
        except Exception:
            self.handleError(record)


class DaemonLogHandler(logging.handlers.RotatingFileHandler):
    def __init__(self):
        super().__init__(mini_buildd.config.ROUTES["log"].path.join(mini_buildd.config.LOG_FILE), maxBytes=5000000, backupCount=9, encoding=mini_buildd.config.CHAR_ENCODING)
        self.setFormatter(logging.Formatter("%(asctime)s " + LOG_FORMAT_DEBUG))


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 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():
    def __init__(self, prog, description, epilog=None, allow_unknown=False):
        self.prog = prog
        self.args = None
        self.unknown_args = []
        self.allow_unknown = allow_unknown

        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("-l", "--log-level", choices=LOG_LEVELS.keys(), action="store", default="INFO", help="set log level")
        self.parser.add_argument("-v", "--verbose", dest="verbosity", action="count", default=0,
                                 help="DEPRECATED (use --log-level): increase log level. Give twice for max logs")
        self.parser.add_argument("-q", "--quiet", dest="terseness", action="count", default=0,
                                 help="DEPRECATED (use --log-level): 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)

    @classmethod
    def loggers(cls):
        """Overwrite this method for custom subset of handler classes from this module."""
        return [ConsoleHandler()]

    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()

        # .. attention: **compat 1.x**: --verbose, --quiet
        if (self.args.verbosity != 0 or self.args.terseness != 0):
            LOG.warning("Deprecated command line options ``--verbose, --quiet``: Use ``--log-level <level>`` instead")
            self.args.log_level = logging.WARNING - (10 * (min(2, self.args.verbosity) - min(2, self.args.terseness)))

        self.setup()

        self.logger.handlers = self.loggers()
        self.wlogger.handlers = self.loggers()

        self.logger.setLevel(self.args.log_level)
        self.wlogger.setLevel(self.args.log_level)
        logging.captureWarnings(True)
        if not sys.warnoptions:
            warnings.simplefilter("default")
        logging.raiseExceptions = "exception" in mini_buildd.config.DEBUG  # Properly set raiseException based "exception" debug value given (see https://docs.python.org/3.5/howto/logging.html#exceptions-raised-during-logging)

        try:
            self.runcli()
        except BaseException as e:
            mini_buildd.log_exception(LOG, f"{self.prog} failed (try '--log-level 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}\"")
