# All the code here is extracted and shortened from openpgp.py Gemato
# See https://github.com/mgorny/gemato


import datetime
import errno
import logging
import os
import os.path
import shutil
import subprocess
import tempfile
import email

GNUPG = os.environ.get('GNUPG', 'gpg')
GNUPGCONF = os.environ.get('GNUPGCONF', 'gpgconf')


def _rmtree_error_handler(func, path, exc_info):
    if (not isinstance(exc_info[1], OSError)
            or exc_info[1].errno not in (errno.ENOENT,
                                         errno.ENOTEMPTY,
                                         errno.EEXIST,
                                         errno.EBUSY)):
        raise exc_info[1]


class OpenPGPException(Exception):
    pass


class OpenPGPSignatureData:
    __slots__ = ['fingerprint', 'timestamp', 'expire_timestamp',
                 'primary_key_fingerprint']

    def __init__(self, fingerprint, timestamp, expire_timestamp,
                 primary_key_fingerprint):
        self.fingerprint = fingerprint
        self.timestamp = timestamp
        self.expire_timestamp = expire_timestamp
        self.primary_key_fingerprint = primary_key_fingerprint


class OpenPGPEnvironment:
    __slots__ = ['_home', 'debug', 'proxy']

    def __init__(self, debug=False, proxy=None):
        self.debug = debug
        self.proxy = proxy
        self._home = tempfile.mkdtemp(prefix='openpgp.')

        with open(os.path.join(self._home, 'dirmngr.conf'), 'w') as f:
            f.write(f'''# autogenerated

# honor user's http_proxy setting
honor-http-proxy

# enable debugging, in case we needed it
log-file {os.path.join(self._home, 'dirmngr.log')}
debug-level guru
''')
        with open(os.path.join(self._home, 'gpg.conf'), 'w') as f:
            f.write('''# autogenerated

# we set validity directly on keys
trust-model direct
''')
        with open(os.path.join(self._home, 'gpg-agent.conf'), 'w') as f:
            f.write(f'''# autogenerated

# avoid any smartcard operations, we are running in isolation
disable-scdaemon

# enable debugging, in case we needed it
log-file {os.path.join(self._home, 'gpg-agent.log')}
debug-level guru
''')

    def __exit__(self, exc_type, exc_value, exc_cb):
        if self._home is not None:
            self.close()

    def clone(self):
        return OpenPGPEnvironment(debug=self.debug, proxy=self.proxy)

    def close(self):
        if self._home is not None:
            ret, sout, serr = self._spawn_gpg(
                [GNUPGCONF, '--kill', 'all'])
            if ret != 0:
                logging.warning(
                    f'{GNUPGCONF} --kill failed:\n'
                    f'{serr.decode("utf8", errors="backslashescape")}')
            if not self.debug:
                # we need to loop due to ENOTEMPTY potential
                while os.path.isdir(self._home):
                    shutil.rmtree(self._home,
                                  onerror=_rmtree_error_handler)
            else:
                logging.debug(f'GNUPGHOME left for debug purposes: '
                              f'{self._home}')
            self._home = None

    def _spawn_gpg(self, argv, stdin=b'', **kwargs):
        env_override = {'GNUPGHOME': self.home}
        if self.proxy is not None:
            env_override['http_proxy'] = self.proxy
        assert 'env_override' not in kwargs
        kwargs['env_override'] = env_override

        env = os.environ.copy()
        env['TZ'] = 'UTC'
        env.update(env_override)

        try:
            p = subprocess.Popen(argv,
                                 stdin=subprocess.PIPE,
                                 stdout=subprocess.PIPE,
                                 stderr=subprocess.PIPE,
                                 env=env)
        except FileNotFoundError:
            raise OpenPGPException('install gpg')

        out, err = p.communicate(stdin)
        if p.wait() != 0:
            raise OpenPGPException(
                err.decode('utf8', errors='backslashreplace'))
        return p.wait(), out, err

    def _parse_gpg_ts(self, ts):
        if 'T' in ts:
            # TODO: is this correct for all cases? is it localtime?
            return datetime.datetime.strptime(ts, '%Y%m%dT%H%M%S')
        elif ts == '0':
            # no timestamp
            return None
        else:
            return datetime.datetime.utcfromtimestamp(int(ts))

    @property
    def home(self):
        assert self._home is not None
        return self._home

    def import_key(self, keyfile, trust=True):
        with open(keyfile, 'rb') as fd:
            exitst, out, err = self._spawn_gpg(
                [GNUPG, '--batch', '--import', '--status-fd', '1'], fd.read())

        if trust:
            fprs = set()
            for line in out.splitlines():
                if line.startswith(b'[GNUPG:] IMPORT_OK'):
                    fprs.add(line.split(b' ')[3].decode('ASCII'))

            ownertrust = ''.join(f'{fpr}:6:\n' for fpr in fprs).encode('utf8')
            exitst, out, err = self._spawn_gpg(
                [GNUPG, '--batch', '--import-ownertrust'],
                ownertrust)

    def verify_file(self, f):
        with open(f, 'r') as fd:
            exitst, out, err = self._spawn_gpg(
                [GNUPG, '--batch', '--status-fd', '1', '--verify'],
                fd.read().encode('utf8'))

        is_good = False
        is_trusted = False
        sig_data = None

        # process the output of gpg to find the exact result
        for line in out.splitlines():
            if line.startswith(b'[GNUPG:] GOODSIG'):
                is_good = True
            elif line.startswith(b'[GNUPG:] EXPKEYSIG'):
                raise OpenPGPException(
                    err.decode('utf8', errors='backslashreplace'))
            elif line.startswith(b'[GNUPG:] REVKEYSIG'):
                raise OpenPGPException(
                    err.decode('utf8', errors='backslashreplace'))
            elif line.startswith(b'[GNUPG:] VALIDSIG'):
                spl = line.split(b' ')
                assert len(spl) >= 12
                fp = spl[2].decode('utf8')
                ts = self._parse_gpg_ts(spl[4].decode('utf8'))
                expts = self._parse_gpg_ts(spl[5].decode('utf8'))
                pkfp = spl[11].decode('utf8')

                sig_data = OpenPGPSignatureData(fp, ts, expts, pkfp)
            elif line.startswith(b'[GNUPG:] TRUST_'):
                spl = line.split(b' ', 2)
                if spl[1] in (b'TRUST_MARGINAL',
                              b'TRUST_FULL',
                              b'TRUST_ULTIMATE'):
                    is_trusted = True

        # require both GOODSIG and VALIDSIG
        if not is_good or sig_data is None:
            raise OpenPGPException(
                err.decode('utf8', errors='backslashreplace'))
        if not is_trusted:
            raise OpenPGPException(
                err.decode('utf8', errors='backslashreplace'))
        return sig_data

    def clear_sign_file(self, f, outf, keyid=None):
        args = []
        if keyid is not None:
            args += ['--local-user', keyid]
        exitst, out, err = self._spawn_gpg(
            [GNUPG, '--batch', '--clearsign'] + args,
            f.read().encode('utf8'))

        outf.write(out.decode('utf8'))

    def list_keys(self):
        # list all keys in the keyring
        exitst, out, err = self._spawn_gpg(
            ['gpg', '--batch', '--with-colons', '--list-keys'])
        if exitst != 0:
            raise OpenPGPException(err.decode('utf8'))

        # find keys and UIDs
        addrs = set()
        addrs_key = set()
        keys = set()
        prev_pub = None
        for l in out.splitlines():
            # were we expecting a fingerprint?
            if prev_pub is not None:
                if l.startswith(b'fpr:'):
                    fpr = l.split(b':')[9].decode('ASCII')
                    assert fpr.endswith(prev_pub)
                    keys.add(fpr)
                    prev_pub = None
                else:
                    return False
            elif l.startswith(b'pub:'):
                if keys:
                    # every key must have at least one UID
                    if not addrs_key:
                        return False
                    addrs.update(addrs_key)
                    addrs_key = set()

                # wait for the fingerprint
                prev_pub = l.split(b':')[4].decode('ASCII')
            elif l.startswith(b'uid:'):
                uid = l.split(b':')[9]
                name, addr = email.utils.parseaddr(uid.decode('utf8'))
                if '@' in addr:
                    logging.debug('refresh_keys_wkd(): UID: {}'
                                  .format(addr))
                    addrs_key.add(addr)

        addrs.update(addrs_key)
        return list(keys)
