import os
import sys
import gnupg
from typing import Union, List
from subprocess import Popen, PIPE
from rkd.api.inputoutput import IO
from .exception import CryptographyKeysAlreadyCreated
from .inputoutput import StreamableBuffer
from .model import Encryption


class EncryptionService(object):
    _io: IO

    def __init__(self, io: IO):
        self._io = io

    def create_decryption_stream(self, encryption: Encryption, stdin: Union[int, 'StreamableBuffer'] = PIPE) \
            -> StreamableBuffer:

        """Takes incoming encrypted buffer and returns a buffer that outputs a decrypted data"""

        gpg_command = [
            'gpg',
            '--homedir', encryption.get_home_dir(),
            '--decrypt',
            '--recipient', encryption.recipient(),
            '--armor',
            '--passphrase', encryption.get_passphrase(),
            '--batch',
            '--yes',
            '--pinentry-mode', 'loopback',
            '--verbose',
        ]

        return self._create_stream(encryption, gpg_command, stdin)

    def create_encryption_stream(self, encryption: Encryption, stdin: Union[int, 'StreamableBuffer'] = PIPE) \
            -> StreamableBuffer:

        gpg_command = [
            'gpg',
            '--homedir', encryption.get_home_dir(),
            '--encrypt',
            '--recipient', encryption.recipient(),
            '--armor'
        ]

        return self._create_stream(encryption, gpg_command, stdin)

    def _create_stream(self, encryption: Encryption, gpg_command: List[str],
                       stdin: Union[int, 'StreamableBuffer'] = PIPE) -> StreamableBuffer:

        self._io.debug('popen({})'.format(gpg_command))
        proc = Popen(gpg_command,
                     stdout=PIPE,
                     stdin=PIPE if stdin == PIPE else stdin.get_buffer(),
                     stderr=sys.stderr.fileno(),
                     close_fds=True
                     )

        return StreamableBuffer(
            io=self._io,
            read_callback=proc.stdout.read,
            close_callback=proc.terminate,
            eof_callback=lambda: proc.poll() is not None,
            is_success_callback=lambda: proc.poll() == 0,
            has_exited_with_failure=lambda: proc.poll() is not None and proc.poll() >= 1,
            description='Encryption stream <{}>'.format(str(gpg_command)),
            buffer=proc.stdout,
            in_buffer=proc.stdin,
            parent=stdin
        )

    def create_keys(self, enc: Encryption):
        """Generates GPG keys usable for backup encryption & decryption"""

        gpg = self._get_pgp(enc)

        for key in gpg.list_keys():
            if enc.get_userid() in str(key['uids']):
                raise CryptographyKeysAlreadyCreated.from_keys_already_created(enc.get_userid())

        result = gpg.gen_key(
            gpg.gen_key_input(
                key_type=enc.get_key_type(),
                key_length=enc.get_key_length(),
                name_real=enc.get_username(),
                name_comment="Generated by Bahub (https://github.com/riotkit-org/backup-repository)",
                name_email=enc.get_userid(),
                expire_date=0,
                passphrase=enc.get_passphrase()
            )
        )

        self._io.success_msg('Keys created, fingerprint: {fingerprint}'.format(fingerprint=result))

    def list_keys(self, enc: Encryption) -> List[dict]:
        """Lists GPG keys for given Encryption entry"""

        gpg = self._get_pgp(enc)

        return list(map(
            lambda key: {'email': key['uids'][0], 'fingerprint': key['fingerprint'], 'gpg_home': enc.get_home_dir()},
            gpg.list_keys()
        ))

    @staticmethod
    def _get_pgp(enc: Encryption) -> gnupg.GPG:
        if not os.path.isdir(enc.get_home_dir()):
            os.mkdir(enc.get_home_dir())

        gpg = gnupg.GPG(gnupghome=enc.get_home_dir())
        gpg.encoding = 'utf-8'

        return gpg

