import contextlib
import inspect
import logging
import html

import django.db.models
import django.contrib.admin
import django.contrib.messages

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

import mini_buildd.models.base

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


class GnuPGPublicKey(mini_buildd.models.base.StatusModel):
    key_id = django.db.models.CharField(max_length=100, blank=True, default="",
                                        help_text="Give a key id here to retrieve the actual key automatically per configured key server.")
    key = django.db.models.TextField(blank=True, default="",
                                     help_text="ASCII-armored GnuPG public key. Leave the key id blank if you fill this manually.")

    key_long_id = django.db.models.CharField(max_length=254, blank=True, default="")
    key_created = django.db.models.CharField(max_length=254, blank=True, default="")
    key_expires = django.db.models.CharField(max_length=254, blank=True, default="")
    key_name = django.db.models.CharField(max_length=254, blank=True, default="")
    key_fingerprint = django.db.models.CharField(max_length=254, blank=True, default="")

    class Meta(mini_buildd.models.base.StatusModel.Meta):
        abstract = True
        app_label = "mini_buildd"

    class Admin(mini_buildd.models.base.StatusModel.Admin):
        search_fields = ["key_id", "key_long_id", "key_name", "key_fingerprint"]
        readonly_fields = ["key_long_id", "key_created", "key_expires", "key_name", "key_fingerprint"]
        exclude = ("extra_options",)

    def __str__(self):
        return f"{self.key_long_id if self.key_long_id else self.key_id}: {self.key_name}"

    # Note: pylint false-positive: https://github.com/PyCQA/pylint/issues/1553
    def clean(self, *args, **kwargs):  # pylint: disable=arguments-differ
        super().clean(*args, **kwargs)
        if self.key_id and len(self.key_id) < 8:
            raise django.core.exceptions.ValidationError("The key id, if given, must be at least 8 bytes  long")

    @classmethod
    def mbd_filter_key(cls, key_id):
        regex = rf"{key_id[-8:]}$"
        return cls.objects.filter(django.db.models.Q(key_long_id__iregex=regex) | django.db.models.Q(key_id__iregex=regex))

    def mbd_prepare(self, _request):
        with contextlib.closing(mini_buildd.gnupg.TmpGnuPG()) as gpg:
            if self.key:
                # Add key given explicitly
                gpg.add_pub_key(self.key)
            elif self.key_id:
                # Import key
                gpg.import_pub_key(self.mbd_get_daemon().model.gnupg_keyserver, self.key_id)
                self.key = gpg.get_pub_key(self.key_id)

            for key, info in gpg.get_pub_keys().items():  # Iterate seems the most pythonic, though we always have only one item here
                self.key_long_id = key
                self.key_name, self.key_fingerprint, self.key_created, self.key_expires = info["user_id"], info["fingerprint"], info["created"], info["expires"]

            # Update the user-given key id by it's long version
            self.key_id = self.key_long_id

    def mbd_remove(self, _request):
        self.key_long_id = ""
        self.key_created = ""
        self.key_expires = ""
        self.key_name = ""
        self.key_fingerprint = ""

    def mbd_sync(self, request):
        self._mbd_remove_and_prepare(request)

    def mbd_check(self, _request):
        """Check that we actually have the key and long_id. This should always be true after "prepare"."""
        if not self.key and not self.key_long_id:
            raise Exception("GnuPG key with inconsistent state -- try remove,prepare to fix.")


class AptKey(GnuPGPublicKey):
    def clean(self, *args, **kwargs):
        super().clean(*args, **kwargs)

        if self.key_id:
            matching_key = self.mbd_filter_key(self.key_id)
            if matching_key.count() > 0:
                raise django.core.exceptions.ValidationError(f"Another such key id already exists: {matching_key[0]}")


class KeyringKey(GnuPGPublicKey):
    """
    Abstract class for GnuPG keys that influence the daemon's keyring.

    This basically means changes to remotes and users may be
    done on the fly (without stopping the daemon), to make this
    maintenance practically usable.
    """

    class Meta(mini_buildd.models.base.StatusModel.Meta):
        abstract = True
        app_label = "mini_buildd"

    class Admin(GnuPGPublicKey.Admin):
        @classmethod
        def _mbd_on_change(cls, request, obj):
            """Notify the daemon keyring to update itself."""
            if obj.mbd_get_daemon().keyrings:
                MsgLog(LOG, request).info("Scheduling keyrings update...")
                obj.mbd_get_daemon().keyrings.set_needs_update()

        @classmethod
        def _mbd_on_activation(cls, request, obj):
            cls._mbd_on_change(request, obj)

        @classmethod
        def _mbd_on_deactivation(cls, request, obj):
            cls._mbd_on_change(request, obj)


class Uploader(KeyringKey):
    user = django.db.models.OneToOneField(django.contrib.auth.models.User,
                                          on_delete=django.db.models.CASCADE)
    may_upload_to = django.db.models.ManyToManyField("Repository", blank=True)

    class Admin(KeyringKey.Admin):
        search_fields = KeyringKey.Admin.search_fields + ["user__username"]
        readonly_fields = KeyringKey.Admin.readonly_fields + ["user"]
        filter_horizontal = ("may_upload_to",)

    def __str__(self):
        return f"'{self.user}' may upload to '{','.join([r.identity for r in self.may_upload_to.all()])}' with key '{super().__str__()}'"


def cb_create_user_profile(sender, instance, created, **kwargs):
    """Automatically create a user profile with every user that is created."""
    if created:
        Uploader.objects.create(user=instance)


django.db.models.signals.post_save.connect(cb_create_user_profile, sender=django.contrib.auth.models.User)


class Remote(KeyringKey):
    http = django.db.models.CharField(primary_key=True, max_length=255, default="tcp:host=YOUR_REMOTE_HOST:port=8066",
                                      help_text=f"HTTP Remote Endpoint: <pre>{html.escape(inspect.getdoc(mini_buildd.net.ClientEndpoint))}</pre>")

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

    class Admin(KeyringKey.Admin):
        search_fields = KeyringKey.Admin.search_fields + ["http"]
        readonly_fields = KeyringKey.Admin.readonly_fields + ["key", "key_id", "pickled_data"]

    def mbd_url(self):
        return mini_buildd.net.ClientEndpoint(self.http, protocol="http").geturl()

    def __str__(self):
        status = self.mbd_get_status()
        chroots = ", ".join([f"{arch}: {' '.join(codenames)}" for arch, codenames in list(status.get("chroots", {}).items())])
        return f"{self.mbd_url()}: {chroots}"

    def mbd_get_status(self, update=False):
        if update:
            try:
                self.mbd_set_pickled_data(mini_buildd.api.Client(self.mbd_url()).call("status"))
            except Exception as e:
                raise Exception(f"Failed to update status for remote '{self.mbd_url()}': {e}")
        return self.mbd_get_pickled_data(default={})

    def mbd_prepare(self, request):
        # We prepare the GPG data from downloaded key data, so key_id _must_ be empty (see super(mbd_prepare))
        self.key_id = ""
        self.key = mini_buildd.api.Command.plain(mini_buildd.api.Client(self.mbd_url()).call("getkey"))

        if self.key:
            MsgLog(LOG, request).warning("Downloaded remote key integrated: Please check key manually before activation!")
        else:
            raise Exception(f"Empty remote key from '{self.mbd_url()}' -- maybe the remote is not prepared yet?")
        super().mbd_prepare(request)

    def mbd_remove(self, request):
        super().mbd_remove(request)
        self.mbd_set_pickled_data(mini_buildd.api.Status({}))
        MsgLog(LOG, request).info("Remote key and state removed.")

    def mbd_check(self, request):
        """Check whether the remote mini-buildd is up, running and serving for us."""
        super().mbd_check(request)

        url = self.mbd_url()
        status = self.mbd_get_status(update=True)
        if mini_buildd.config.primary_http_endpoint().geturl() not in status["remotes"]:
            raise Exception(f"Remote '{url}': does not know us (connect other remote with us, then retry).")

        if not status["running"]:
            raise Exception(f"Remote '{url}': is down.")
