#!/usr/bin/python3
# pylint: disable=invalid-name  # https://github.com/PyCQA/pylint/issues/516

import signal
import os
import pwd
import inspect
import contextlib
import logging

# PEP 3143
import daemon
import daemon.runner

import mini_buildd.misc
import mini_buildd.cli
import mini_buildd.net
import mini_buildd.config
import mini_buildd.httpd
import mini_buildd.django_settings


LOG = logging.getLogger("mini_buildd")

#: Needed for man page hack in setup.py
DESCRIPTION = "Once minimal Debian build and repository daemon"


class PIDFile():
    """
    Pidfile with automatic stale fixup.

    This uses code from the PEP 3143 reference
    implementation.
    """

    @classmethod
    def _is_pidfile_stale(cls, pidfile, name=None):
        """
        Improvement (linux specific) of daemon.runner.is_pidfile_stale: Also checks the name of the process.

        Fixes situations when another unrelated process has reclaimed the pid from the stale pidfile.
        """
        is_stale = False
        pidfile_pid = pidfile.read_pid()
        if pidfile_pid is not None:
            is_stale = daemon.runner.is_pidfile_stale(pidfile)
            if name and not is_stale:
                try:
                    with mini_buildd.fopen(f"/proc/{pidfile_pid}/comm") as proc_comm:
                        pidfile_name = proc_comm.read()
                    is_stale = pidfile_name != name
                except BaseException as e:
                    LOG.warning(f"Error in extra linux /proc-style stale check (ignoring): {e}")

        return is_stale

    def __init__(self, pidfile_path, acquire_timeout=5):
        self.pidfile = daemon.runner.make_pidlockfile(pidfile_path, acquire_timeout)
        if self._is_pidfile_stale(self.pidfile, name="mini-buildd"):
            LOG.warning(f"Fixing STALE PID file: {self}")
            self.pidfile.break_lock()
        self.pidfile.acquire(timeout=acquire_timeout)

    def __str__(self):
        return f"{self.pidfile.path} ({self.pidfile.read_pid()})"

    def close(self):
        self.pidfile.release()


class HttpDRun:
    """Small wrapper allowing us to use contextlib.closing()."""

    def __init__(self, pidfile, webapp):
        self.pidfile = PIDFile(pidfile)
        self.httpd = mini_buildd.httpd.HttpD(wsgi_app=webapp)
        self.httpd.start()

    def close(self):
        self.httpd.shutdown()
        self.httpd.join()
        self.pidfile.close()


class CLI(mini_buildd.cli.CLI):
    SHUTDOWN = [signal.SIGTERM, signal.SIGINT, signal.SIGHUP]

    def _setup(self):
        """Set global variables that really make no sense to propagate through."""
        mini_buildd.config.DEBUG = self.args.debug.split(",")
        mini_buildd.config.FOREGROUND = self.args.foreground
        mini_buildd.config.HOSTNAME_FQDN = self.args.hostname
        mini_buildd.config.HOSTNAME = mini_buildd.config.HOSTNAME_FQDN.partition(".")[0]

        # FIXME: 'default' option in add_argument() can't be used, see https://bugs.python.org/issue16399
        if self.args.http_endpoint is not None:
            mini_buildd.config.HTTP_ENDPOINTS = self.args.http_endpoint
        mini_buildd.config.ROUTES = mini_buildd.config.Routes(self.args.home)

        # Overwrite default shutdown signal handlers with no-ops (else, SIGHUP would just crash it).
        for s in self.SHUTDOWN:
            signal.signal(s, lambda x, y: None)

    def _setup_environment(self):
        os.environ.clear()
        os.environ["HOME"] = self.args.home
        os.environ["PATH"] = "/usr/bin:/bin:/usr/sbin:/sbin"
        os.environ["LANG"] = "C.UTF-8"
        for name in ["USER", "LOGNAME"]:
            os.environ[name] = self._user

        mini_buildd.config.python37_systemcert_workaround()

    def is_extra_run(self):
        """Return True if in a non-daemon, non-HTTP server extra run."""
        return self.args.set_admin_password or self.args.remove_system_artifacts or self.args.loaddata or self.args.dumpdata

    def __init__(self):
        self._user = pwd.getpwuid(os.getuid())[0]

        super().__init__("mini-buildd", DESCRIPTION,
                         "See also: Online manual, usually at http://localhost:8066/doc/.")

        group_conf = self.parser.add_argument_group("daemon arguments")

        group_conf.add_argument("-N", "--hostname",
                                action="store",
                                default=mini_buildd.config.HOSTNAME_FQDN,
                                help="Public (fully qualified) hostname used for all services.")

        group_conf.add_argument("-E", "--http-endpoint", action="store", nargs="+",
                                help=f"""\
HTTP Server Endpoint:
{inspect.getdoc(mini_buildd.net.ServerEndpoint)}
May be given multiple times (to server multiple endpoints). The first
endpoint given will be the primary.

(default: {mini_buildd.config.HTTP_ENDPOINTS}), not""")
        group_conf.add_argument("-W", "--httpd-bind", action="store", default=":::8066",
                                help="DEPRECATED (use '--http-endpoint' instead): Web Server IP/Hostname and port to bind to.")
        group_conf.add_argument("-S", "--smtp", action="store", default=":@smtp://localhost:25",
                                help="SMTP credentials in format '[USER]:[PASSWORD]@smtp|ssmtp://HOST:PORT'.")
        group_conf.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').")
        group_conf.add_argument("-H", "--home", action="store", default="~",
                                help="Run with this home dir (you may use '~' for user expansion). The only use case to change this for debugging, really.")
        group_conf.add_argument("-F", "--pidfile", action="store", default="~/.mini-buildd.pid",
                                help="Set pidfile path (you may use '~' for user expansion).")
        group_conf.add_argument("-f", "--foreground", action="store_true",
                                help="Don't daemonize, log to console.")

        group_log = self.parser.add_argument_group("logging and debugging arguments")
        group_log.add_argument("-l", "--loggers", action="store", default="file,syslog",
                               help="Comma-separated list of loggers (file,syslog,console) to use.")
        group_log.add_argument("-d", "--debug", action="store", default="", metavar="OPTION,..",
                               help="""\
Comma-separated list of special debugging options:
'warnings' (show all warnings from python's warnings module in log),
'exception' (log tracebacks in exception handlers),
'http' (put http server in debug mode),
'webapp' (put web application [django] in debug mode),
'sbuild' (run sbuild in debug mode),
'keep' (keep spool and temporary directories).""")

        group_db = self.parser.add_argument_group("database arguments")
        group_db.add_argument("-P", "--set-admin-password", action="store", metavar="PASSWORD",
                              help="Update password for superuser ('admin'); user is created if non-existent yet.")
        group_db.add_argument("-D", "--dumpdata", action="store", metavar="APP[.MODEL]",
                              help="Dump database contents for app[.MODEL] as JSON file (see 'django-admin dumpdata').")
        group_db.add_argument("-L", "--loaddata", action="store", metavar="FILE",
                              help="INTERNAL USE ONLY, use with care! Load JSON file into database (see 'django-admin loaddata').")
        group_db.add_argument("-R", "--remove-system-artifacts", action="store_true",
                              help="INTERNAL USE ONLY, use with care! Bulk-remove associated data of all objects that might have produced artifacts on the system.")

    def setup(self):
        # Arguments that imply foreground mode
        if self.args.set_admin_password or self.args.loaddata or self.args.dumpdata:
            self.args.foreground = True

        # reproducible builds: Expand these for run, leave static for usage (as this is used to build man pages) (thx to Chris Lamb, see Debian Bug #833340)
        self.args.home = os.path.expanduser(self.args.home)
        self.args.pidfile = os.path.expanduser(self.args.pidfile)

        # Warn when deprecated --httpd-bind is still used (unless you use the default)
        if self.parser.get_default("httpd_bind") != self.args.httpd_bind:
            self.args.http_endpoint = [self.args.httpd_bind]
            LOG.warning("Option '--httpd-bind' is deprecated. Please use '--http-endpoint' instead (see 'mini-buildd --help').")

        # User sanity check
        if self.args.dedicated_user != self._user:
            raise Exception(f"Run as dedicated user only (use '--dedicated-user={self._user}' if you really want this, will write to that user's $HOME!)")

        if not self.is_extra_run():
            # Pre-daemonize check if shm is usable on the system (else pyftpdlib fails strangely later)
            mini_buildd.misc.check_multiprocessing()

        # Daemonize early
        if not self.args.foreground:
            daemon.DaemonContext(working_directory=self.args.home, umask=0o022).open()
        self._setup()
        self._setup_environment()

        # Configure django
        mini_buildd.django_settings.configure(self.args.smtp)

    def loggers(self):
        loggers = self.args.loggers.split(",")
        if self.args.foreground:
            loggers.append("console")
        return loggers

    def runcli(self):
        # import here: We cannot import anything 'django' prior to django's configuration
        from mini_buildd.webapp import WebApp

        webapp = WebApp()

        # Extra options that exit without running as daemon
        if self.args.set_admin_password:
            webapp.set_admin_password(self.args.set_admin_password)
        elif self.args.remove_system_artifacts:
            webapp.remove_system_artifacts()
        elif self.args.loaddata:
            webapp.loaddata(self.args.loaddata)
        elif self.args.dumpdata:
            webapp.dumpdata(self.args.dumpdata)
        else:
            with contextlib.closing(HttpDRun(self.args.pidfile, webapp)):
                mini_buildd.misc.attempt(mini_buildd.get_daemon().mbd_start)
                signal.sigwait(self.SHUTDOWN)
                mini_buildd.get_daemon().mbd_stop()


CLI().run()
