import os
import re
import tempfile
import shutil
import glob
import logging

import mini_buildd.misc
import mini_buildd.call
import mini_buildd.config

LOG = logging.getLogger(__name__)


class Colons():
    """
    Provide a colon->name mapping for the gpg script-parsable '--with-colons' output.

    See /usr/share/doc/gnupg/DETAILS.gz.
    """

    def __init__(self, colons_line):
        self._colons = colons_line.split(":")

    def __str__(self):
        return f"{self.type}: {self.key_id}: {self.user_id}"

    def _get(self, index):
        return mini_buildd.misc.list_get(self._colons, index, "")

    @property
    def type(self):
        return self._get(0)

    @property
    def key_id(self):
        return self._get(4)

    @property
    def creation_date(self):
        return self._get(5)

    @property
    def expiration_date(self):
        return self._get(6)

    @property
    def user_id(self):
        """Fingerprint for 'fpr' type."""
        return self._get(9)


class BaseGnuPG():
    @classmethod
    def get_flavor(cls):
        """
        Ugly-parse GPG binary flavor(=major.minor).

        "1.4" ("classic"), "2.0" ("stable") or "2.1" ("modern") from
        "gpg --version" output (like "gpg (GnuPG) 2.1.14"). Don't fail
        but return "unknown" if anything nasty happens.

        """
        try:
            version_info = mini_buildd.call.Call(["gpg", "--version"]).check().stdout.splitlines()
            version_line = version_info[0].split(" ")
            return version_line[2][0:3]
        except BaseException as e:
            LOG.warning(f"Can't parse GPG flavor: {e}")
            return "unknown"

    def __init__(self, home):
        self.flavor = self.get_flavor()
        self.home = home
        self.gpg_cmd = ["gpg",
                        "--homedir", home,
                        "--display-charset", mini_buildd.config.CHAR_ENCODING,
                        "--batch"]
        LOG.info(f"GPG {self.flavor}: {self.gpg_cmd}")

    def gen_secret_key(self, template):
        flavor_additions = {"2.1": "\n%no-protection\n",
                            "2.2": "\n%no-protection\n"}

        with tempfile.TemporaryFile() as t:
            t.write(template.encode(mini_buildd.config.CHAR_ENCODING))
            t.write(flavor_additions.get(self.flavor, "").encode(mini_buildd.config.CHAR_ENCODING))
            t.seek(0)
            mini_buildd.call.Call(self.gpg_cmd + ["--gen-key"], stdin=t).log().check()

    def export(self, dest_file, identity=""):
        with open(dest_file, "w") as f:
            mini_buildd.call.Call(self.gpg_cmd + ["--export"] + ([identity] if identity else []), stdout=f).check()

    def get_pub_key(self, identity):
        return mini_buildd.call.Call(self.gpg_cmd + ["--armor", "--export", identity]).log().check().stdout

    def _get_colons(self, type_regex, list_arg="--list-public-keys", identity=None):
        for line in mini_buildd.call.Call(self.gpg_cmd + [list_arg, "--with-colons", "--fixed-list-mode", "--with-fingerprint"] + ([] if identity is None else [identity])).log().check().stdout.splitlines():
            colons = Colons(line)
            LOG.debug(f"{colons}")
            if re.match(type_regex, colons.type):
                yield colons

    def get_pub_keys(self):
        result = {}
        for pub_colons in self._get_colons("^pub$"):
            fpr_colons = next(self._get_colons("^fpr$", identity=pub_colons.key_id))
            uid_colons = next(self._get_colons("^uid$", identity=pub_colons.key_id))
            result[pub_colons.key_id] = {"user_id": uid_colons.user_id, "fingerprint": fpr_colons.user_id, "created": pub_colons.creation_date, "expires": pub_colons.expiration_date}
            return result

    def get_first_sec_colon(self, type_regex):
        try:
            return next(self._get_colons(type_regex, list_arg="--list-secret-keys"))
        except StopIteration:
            return Colons("")

    def get_first_sec_key(self):
        return self.get_first_sec_colon("^sec$").key_id

    def get_first_sec_key_fingerprint(self):
        return self.get_first_sec_colon("^fpr$").user_id

    def get_first_sec_key_user_id(self):
        return self.get_first_sec_colon("^uid$").user_id

    def import_pub_key(self, key_server, identity):
        # 1st, try keyrings on local system
        for keyring in glob.glob("/usr/share/keyrings/*.gpg"):
            try:
                # Note that gpg --export succeeds even if nothing can be exported, --import however fails on empty input
                key = mini_buildd.call.Call(self.gpg_cmd + ["--armor", "--keyring", keyring, "--export", identity]).log().check().stdout
                mini_buildd.call.Call(self.gpg_cmd + ["--armor", "--import", "-"], input=bytes(key, encoding="ascii")).log().check()
                LOG.info(f"{identity}: Imported from keyring: {keyring}")
                return
            except BaseException as e:
                LOG.info(f"{identity}: Can't import from keyring {keyring}: {e}")

        # 2nd, try key_server (with retry)
        mini_buildd.call.call_with_retry(self.gpg_cmd + ["--armor", "--keyserver", key_server, "--recv-keys", identity], retry_max_tries=5, retry_sleep=5)
        LOG.info(f"{identity}: Imported from key server: {key_server}")

    def add_pub_key(self, key):
        with tempfile.TemporaryFile() as t:
            t.write(key.encode(mini_buildd.config.CHAR_ENCODING))
            t.seek(0)
            mini_buildd.call.Call(self.gpg_cmd + ["--import"], stdin=t).log().check()

    def add_keyring(self, keyring):
        if os.path.exists(keyring):
            self.gpg_cmd += ["--keyring", keyring]
        else:
            LOG.warning(f"Skipping non-existing keyring file: {keyring}")

    def verify(self, signature, data=None):
        try:
            mini_buildd.call.Call(self.gpg_cmd + ["--verify", signature] + ([data] if data else [])).check()
        except BaseException:
            raise Exception("GnuPG authorization failed.")

    def sign(self, file_name, identity=None):
        # 1st: copy the unsigned file and add an extra new line
        # (Like 'debsign' from devscripts does: dpkg-source <= squeeze will have problems without the newline)
        unsigned_file = file_name + ".asc"
        shutil.copyfile(file_name, unsigned_file)
        with open(unsigned_file, "a") as unsigned:
            unsigned.write("\n")

        # 2nd: Sign the file copy
        signed_file = file_name + ".signed"

        def failed_cleanup():
            if os.path.exists(signed_file):
                os.remove(signed_file)

        # Retrying sign call; workaround for mystery https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=849551
        mini_buildd.call.call_with_retry(self.gpg_cmd
                                         + ["--armor", "--textmode", "--clearsign", "--output", signed_file]
                                         + (["--local-user", identity] if identity else []) + [unsigned_file],
                                         retry_max_tries=5,
                                         retry_sleep=1,
                                         retry_failed_cleanup=failed_cleanup)

        # 3rd: Success, move to orig file and cleanup
        os.rename(signed_file, file_name)
        os.remove(unsigned_file)


class GnuPG(BaseGnuPG):
    def __init__(self, template, fullname, email):
        super().__init__(home=os.path.join(mini_buildd.config.HOME_DIR, ".gnupg"))
        self.template = f"""{template}
Name-Real: {fullname}
Name-Email: {email}
"""

    def prepare(self):
        if not self.get_pub_key():
            LOG.info("Generating GnuPG secret key (this might take some time)...")
            self.gen_secret_key(self.template)
            LOG.info("New GnuPG secret key prepared...")
        else:
            LOG.info("GnuPG key already prepared...")

    def remove(self):
        if os.path.exists(self.home):
            shutil.rmtree(self.home)
            LOG.info(f"GnuPG setup removed: {self.home}")

    def get_pub_key(self, identity=None):
        return super().get_pub_key("mini-buildd")


class TmpGnuPG(BaseGnuPG, mini_buildd.misc.TmpDir):
    r"""
    Temporary GnuPG.

    >>> # mini_buildd.config.DEBUG.append("keep")  # Enable 'keep' for debugging only
    >>> gnupg_home = mini_buildd.misc.TmpDir()
    >>> dummy = shutil.copy2("test-data/gpg/secring.gpg", gnupg_home.tmpdir)
    >>> dummy = shutil.copy2("test-data/gpg/pubring.gpg", gnupg_home.tmpdir)
    >>> gnupg = BaseGnuPG(home=gnupg_home.tmpdir)

    >>> gnupg.get_first_sec_colon("sec").type
    'sec'
    >>> gnupg.get_first_sec_key_user_id()
    'Üdo Ümlaut <test@key.org>'
    >>> gnupg.get_first_sec_key()  #doctest: +ELLIPSIS
    'AF95FC80FC40A82E'
    >>> gnupg.get_first_sec_key_fingerprint()  #doctest: +ELLIPSIS
    '4FB13BDD777C046D72D4E7D3AF95FC80FC40A82E'

    >>> export = tempfile.NamedTemporaryFile()
    >>> gnupg.export(export.name)

    >>> t = tempfile.NamedTemporaryFile()
    >>> t.write(b"A test file\n")
    12
    >>> t.flush()
    >>> gnupg.sign(file_name=t.name, identity="test@key.org")
    >>> gnupg.verify(t.name)
    >>> pub_key = gnupg.get_pub_key(identity="test@key.org")
    >>> tgnupg = TmpGnuPG()
    >>> tgnupg.add_pub_key(pub_key)
    >>> tgnupg.verify(t.name)

    >>> tgnupg.close()
    >>> gnupg_home.close()
    """

    def __init__(self):
        mini_buildd.misc.TmpDir.__init__(self)
        super().__init__(home=self.tmpdir)
