import re
import socket
import email.utils
import inspect
import html
import logging

import django.db
import django.core.exceptions
import django.core.mail

import mini_buildd.misc
import mini_buildd.net
import mini_buildd.gnupg

import mini_buildd.models.base
import mini_buildd.models.repository
import mini_buildd.models.chroot
import mini_buildd.models.gnupg

from mini_buildd.models.msglog import MsgLog
LOG = logging.getLogger(__name__)


class Daemon(mini_buildd.models.base.StatusModel):
    # Basics
    identity = django.db.models.CharField(max_length=50, default=socket.gethostname(),
                                          help_text=("Daemon's identity; this will be used to identify this\n"
                                                     "mini-buildd instance in various places.\n"
                                                     "\n"
                                                     "It will occur in the 'Name-Real' part of the GnuPG key,\n"
                                                     "determines the name of the automated keyring packages, and will\n"
                                                     "also occur in the 'Origin' tag of repository indices.\n"
                                                     "\n"
                                                     "In most cases, just name of the host we are running on is a good\n"
                                                     "choice.\n"))

    hostname = django.db.models.CharField(
        max_length=200,
        default=socket.getfqdn(),
        help_text="Obsoleted: Use command line arg '--hostname' instead.")

    email_address = django.db.models.EmailField(max_length=255, default=f"mini-buildd@{socket.getfqdn()}",
                                                help_text=("Daemon's email address; this will be used to identify this\n"
                                                           "mini-buildd instance in various places.\n"
                                                           "\n"
                                                           "It will occur in the 'Name-Email' part of the GnuPG key, and used as\n"
                                                           "maintainer email on various occations when packages are build\n"
                                                           "automatically.\n"))

    # GnuPG options
    gnupg_template = django.db.models.TextField(default=("Key-Type: RSA\n"
                                                         "Key-Length: 4096\n"
                                                         "Expire-Date: 0\n"),
                                                help_text=("Template as accepted by 'gpg --batch --gen-key' (see 'man gpg').\n"
                                                           "\n"
                                                           "You should not give 'Name-Real' and 'Name-Email', as these are\n"
                                                           "automatically added.)\n"))

    gnupg_keyserver = django.db.models.CharField(
        max_length=200,
        default="pool.sks-keyservers.net",
        help_text="GnuPG key server to use (as fill-in helper).")

    ftpd_bind = django.db.models.CharField(
        max_length=200,
        default="tcp:port=8067",
        help_text=f"FTP Server Endpoint: <pre>{html.escape(inspect.getdoc(mini_buildd.net.ServerEndpoint))}</pre>")

    ftpd_options = django.db.models.CharField(max_length=255, default="", blank=True, help_text="For future use.")

    # Load options
    build_queue_size = django.db.models.IntegerField(
        default=mini_buildd.misc.get_cpus(),
        help_text="Maximum number of parallel builds.")

    sbuild_jobs = django.db.models.IntegerField(
        default=1,
        help_text="Degree of parallelism per build (via sbuild's '--jobs' option).")

    # EMail options
    # DEPRECATED/UNUSED: With the switch to django mail framework, this is now configured via the --smtp command line argument.
    smtp_server = django.db.models.CharField(
        max_length=254,
        default="localhost:25",
        help_text="DEPRECATED/UNUSED: Replaced by '--smtp' command line option.")

    notify = django.db.models.ManyToManyField(mini_buildd.models.repository.EmailAddress,
                                              blank=True,
                                              help_text="Addresses that get all notification emails unconditionally.")
    allow_emails_to = django.db.models.CharField(
        max_length=254,
        default=f".*@{socket.getfqdn()}",
        help_text=("Regex to allow sending emails to automatically computed\n"
                   "addresses (currently the 'Changed-By' or 'Maintainer' addresses\n"
                   "when this feature is enabled for the resp. repository).\n"
                   "\n"
                   "Use '.*' to allow all -- it's however recommended to put this to\n"
                   "s.th. like '.*@myemail.domain', to avoid original package\n"
                   "maintainers to be accidentally spammed.\n"))

    custom_hooks_directory = django.db.models.CharField(max_length=255, default="", blank=True, help_text="For future use.")

    show_last_packages = django.db.models.IntegerField(
        default=100,
        help_text="How many packager events to keep (2.0: this value will just be added to the max size of the generic event log).")
    show_last_builds = django.db.models.IntegerField(
        default=100,
        help_text="How many builder events to keep (2.0: this value will just be added to the max size of the generic event log).")

    wait_for_build_results = django.db.models.IntegerField(
        default=5,
        help_text="Future use: How many days to wait for build results until finishing a package.")
    keep_build_results = django.db.models.IntegerField(
        default=5,
        help_text="Future use: How many days to keep build results that cannot be uploaded.")

    LETHAL_DEPENDENCIES = False

    class Meta(mini_buildd.models.base.StatusModel.Meta):
        verbose_name_plural = "Daemon"

    class Admin(mini_buildd.models.base.StatusModel.Admin):
        fieldsets = (
            (None, {"fields": (), "description": ("The daemon instance. There is always exactly one instance of this.\n"
                                                  "\n"
                                                  "prepare/remove actions will generate/remove the GnuPG key.\n")}),
            ("Archive identity", {"fields": (("identity", "hostname", "email_address"), "gnupg_template")}),
            ("FTP (incoming) Options", {"fields": ("ftpd_bind", "ftpd_options")}),
            ("Load Options", {"fields": ("build_queue_size", "sbuild_jobs")}),
            ("E-Mail Options", {"fields": ("smtp_server", "notify", "allow_emails_to")}),
            ("Other Options", {"fields": ("gnupg_keyserver", "custom_hooks_directory", "show_last_packages", "show_last_builds")}))

        filter_horizontal = ("notify",)

        # These are depcrecated or not used yet
        readonly_fields = ["smtp_server", "ftpd_options", "custom_hooks_directory"]

        def save_model(self, request, obj, form, change):
            """Save model -- always update from daemon object first."""
            obj.mbd_get_daemon().update_to_model()
            super().save_model(request, obj, form, change)

    def __str__(self):
        return (f"{self.identity}: Serving {len(mini_buildd.models.repository.Repository.mbd_get_active())} repositories, "
                f"{len(mini_buildd.models.chroot.Chroot.mbd_get_active())} chroots, "
                f"using {len(mini_buildd.models.gnupg.Remote.mbd_get_active())} remotes")

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._mbd_fullname = f"mini-buildd archive {self.identity}"
        self._mbd_gnupg = mini_buildd.gnupg.GnuPG(self.gnupg_template, self._mbd_fullname, self.email_address)
        self._mbd_gnupg_long_id = self._mbd_gnupg.get_first_sec_key()
        self._mbd_gnupg_fingerprint = self._mbd_gnupg.get_first_sec_key_fingerprint()
        self._mbd_ftp_server_endpoint = mini_buildd.net.ServerEndpoint(self.ftpd_bind, protocol="ftp")

    @property
    def mbd_fullname(self):
        return self._mbd_fullname

    @property
    def mbd_gnupg(self):
        return self._mbd_gnupg

    @property
    def mbd_gnupg_fingerprint(self):
        return self._mbd_gnupg_fingerprint

    @property
    def mbd_gnupg_long_id(self):
        return self._mbd_gnupg_long_id

    # Note: pylint false-positive: https://github.com/PyCQA/pylint/issues/1553
    def clean(self, *args, **kwargs):  # pylint: disable=arguments-differ
        super().clean(*args, **kwargs)

        self.mbd_validate_regex(r"^[a-zA-Z0-9\-]+$", self.identity, "Identity")

        if Daemon.objects.count() > 0 and self.id != Daemon.objects.get().id:
            raise django.core.exceptions.ValidationError("You can only create one Daemon instance!")

    def mbd_prepare(self, request):
        self._mbd_gnupg.prepare()
        self._mbd_gnupg_long_id = self._mbd_gnupg.get_first_sec_key()
        self._mbd_gnupg_fingerprint = self._mbd_gnupg.get_first_sec_key_fingerprint()
        MsgLog(LOG, request).info(f"Daemon GnuPG key generated: {self._mbd_gnupg_long_id}: {self._mbd_gnupg_fingerprint}")

    @classmethod
    def mbd_sync(cls, request):
        MsgLog(LOG, request).warning("The GnuPG key will never be updated automatically. Explicitly run remove+prepare to achieve this.")

    def mbd_remove(self, request):
        self._mbd_gnupg.remove()
        MsgLog(LOG, request).info("Daemon GnuPG key removed.")

    def mbd_get_dependencies(self):
        """All active or to-be active repositories, remotes and chroots."""
        result = []
        for model in [mini_buildd.models.repository.Repository, mini_buildd.models.chroot.Chroot, mini_buildd.models.gnupg.Remote]:
            for o in model.mbd_get_active_or_auto_reactivate():
                result.append(o)
        return result

    def mbd_check(self, request):
        """Just warn in case there are no repos and no chroots."""
        if not self.mbd_get_daemon().get_active_repositories() and not self.mbd_get_daemon().get_active_chroots():
            MsgLog(LOG, request).warning("No active chroot or repository.")

    def mbd_get_ftp_endpoint(self):
        return self._mbd_ftp_server_endpoint

    def mbd_get_ftp_url(self):
        return self._mbd_ftp_server_endpoint.geturl()

    def mbd_get_archive_origin(self):
        return f"Mini-Buildd archive {self.identity} on {mini_buildd.config.HOSTNAME}"

    def mbd_get_pub_key(self):
        return self._mbd_gnupg.get_pub_key()

    def mbd_get_dput_conf(self):
        return f"""\
[mini-buildd-{self.identity}]
method   = ftp
fqdn     = {self.mbd_get_ftp_endpoint().hopo()}
login    = anonymous
incoming = /incoming
"""

    def _mbd_notify_signature(self, typ):
        reason = {"daemon": "Your address is configured to get any notifications (contact administrators if you don't want this).",
                  "repository": "Your address is configured to get any notifications for this repository (contact administrators if you don't want this).",
                  "changed-by": "Your address is the uploader of the package ('Changed-By' in changes).",
                  "maintainer": "Your address is the maintainer of the package ('Maintainer' in changes).",
                  "subscriber": "Your user account has a matching subscription.",
                  }.get(typ, "Unknown")

        return (f"--\N{SPACE}\n"
                f"mini-buildd instance '{self.identity}' at {mini_buildd.config.HOSTNAME} <{self.email_address}>\n"
                f"Reason for this mail: {reason}\n"
                f"Visit mini-buildd   : {mini_buildd.config.primary_http_endpoint().geturl()}\n"
                f"Manage your account : {mini_buildd.config.primary_http_endpoint().geturl(path=mini_buildd.config.HTTP_PATH_LOGIN)}\n")

    def mbd_notify(self, subject, body, repository=None, changes=None, distribution=None, msglog=LOG):
        subject_prefix = f"[mini-buildd-{self.identity}] "
        m_to = []
        m_to_raw = []
        m_automatic_to_allow = re.compile(self.allow_emails_to)
        # Include all plain 'msglogs' so far
        m_msglog = getattr(msglog, "plain", "")
        if m_msglog:
            m_msglog = f"Daemon messages:\n{m_msglog}---\n"

        def add_to(address, typ, is_automatic):
            address_raw = email.utils.parseaddr(address)[1]
            if not is_automatic or m_automatic_to_allow.search(address_raw):
                if address_raw in m_to_raw:
                    msglog.debug(f"Notify: Skipping {typ} address: {address}: Duplicate")
                else:
                    m_to.append((subject_prefix + subject, m_msglog + body + self._mbd_notify_signature(typ), self.email_address, [address]))
                    m_to_raw.append(address_raw)
                    msglog.info(f"Notify: Adding {typ} address: {address}")
            else:
                msglog.warning(f"Notify: Skipping {typ} address: {address}: Not allowed (only '{self.allow_emails_to}')")

        def get_subscriptions():
            package = changes.get("Source", None)
            real_distribution = distribution
            if real_distribution is None:
                # If distribution was not given explicitly, try from changes, resolving meta dists if needed.
                changes_dist = changes.get("Distribution", "")
                real_distribution = mini_buildd.models.repository.map_incoming_distribution(changes_dist)
            return self.mbd_get_daemon().get_subscription_objects().filter(package__in=[package, ""], distribution__in=[real_distribution, ""])

        # Add hardcoded addresses from daemon
        for m in self.notify.all():
            add_to(m.__str__(), "daemon", is_automatic=False)

        # Add hardcoded addresses from repository
        if repository:
            for m in repository.notify.all():
                add_to(m.__str__(), "repository", is_automatic=False)

        if changes:
            # Add package uploader (Changed-By): Add even if we do not have a repo, so uploader is informed on such error cases too.
            changed_by = changes.get("X-Mini-Buildd-Originally-Changed-By", changes.get("Changed-By"))
            if not repository or (repository.notify_changed_by and changed_by):
                add_to(changed_by, "changed-by", is_automatic=True)

            # Add package maintainer: Add only when we have a repo, and it's configured to do so
            maintainer = changes.get("Maintainer")
            if repository and (repository.notify_maintainer and maintainer):
                add_to(maintainer, "maintainer", is_automatic=True)

            # Add user subscriptions
            for s in get_subscriptions():
                address = f"{s.subscriber.get_full_name()} <{s.subscriber.email}>"
                if s.subscriber.is_active:
                    add_to(address, "subscriber", is_automatic=False)
                else:
                    msglog.debug(f"Notify: Skipping subscription address: {address}: Account disabled")

        try:
            django.core.mail.send_mass_mail(m_to)
            msglog.info(f"Notify: Sent '{subject}'")
        except BaseException as e:
            mini_buildd.config.log_exception(msglog, f"Notify: Mail '{subject}' failed to '{m_to}'", e)
