import gnupg
import gzip
import hashlib
import json
import logging
import os
import requests
import time
import xmltodict

from datetime import datetime
from io import BytesIO
from margaritashotgun.exceptions import *
from prompt_toolkit import prompt
from margaritashotgun.settings import *

logger = logging.getLogger(__name__)


class Repository():
    """
    Lime-compiler repository client
    https://github.com/ThreatResponse/lime-compiler
    """

    def __init__(self, url, gpg_verify):
        """
        :type url: str
        :param url: repository url
        :type gpg_verify: bool
        :param gpg_verify: enable/disable gpg signature verification
        """
        self.url = url.rstrip('/')
        self.gpg_verify = gpg_verify
        self.metadata_dir = 'repodata'
        self.metadata_file = 'repomd.xml'
        self.repo_signing_key = PUBLIC_SIGNING_KEY_FILE
        self.key_metadata = PUBLIC_SIGNING_KEY_METADATA

        self.gpg = None
        self.key_path = None
        self.key_info = None
        self.raw_key = None

    def init_gpg(self):
        """
        Initialize gpg object and check if repository signing key is trusted
        """
        if self.gpg_verify:
            logger.debug("gpg verification enabled, initializing gpg")
            gpg_home = os.path.expanduser('~/.gnupg')
            self.gpg = gnupg.GPG(gnupghome=gpg_home)
            self.key_path, self.key_info = self.get_signing_key()
            logger.debug("{0} {1}".format(self.key_path, self.key_info))
            self.check_signing_key()

    def get_signing_key(self):
        """
        Download a local copy of repo signing key for installation
        """

        """
        Download a local copy of repo signing key key metadata.
        Fixes #17 Scan Keys no available in all GPG versions.
        """
        tmp_key_path = "/tmp/{0}".format(self.repo_signing_key)
        tmp_metadata_path = "/tmp/{0}".format(self.key_metadata)

        repo_key_path = "{0}/{1}".format(self.url, self.repo_signing_key)
        repo_metadata_path = "{0}/{1}".format(self.url, self.key_metadata)


        req_key = requests.get(repo_key_path)
        req_metadata = requests.get(repo_metadata_path)

        # Fetch the key to disk
        if req_key.status_code is 200:
            logger.debug(("found repository signing key at "
                          "{0}".format(repo_key_path)))
            self.raw_key = req_key.content
            with open(tmp_key_path, 'wb') as f:
                f.write(self.raw_key)
        else:
            raise RepositoryMissingSigningKeyError(repo_key_path)

        # Fetch the fingerprint from the metadata
        if req_metadata.status_code is 200:
            logger.debug(("found key metadata at "
                          "{0}".format(repo_metadata_path)))
            print(req_metadata.content)
            key_info = json.loads(req_metadata.content.decode('utf-8'))
        else:
            RepositoryMissingKeyMetadataError

        return (tmp_key_path, key_info)

    def check_signing_key(self):
        """
        Check that repo signing key is trusted by gpg keychain
        """
        user_keys = self.gpg.list_keys()
        if len(user_keys) > 0:
            trusted = False
            for key in user_keys:
                if key['fingerprint'] == self.key_info['fingerprint']:
                    trusted = True
                    logger.debug(("repo signing key trusted in user keyring, "
                                  "fingerprint {0}".format(key['fingerprint'])))
        else:
            trusted = False
        if trusted is False:
            repo_key_url = "{0}/{1}".format(self.url, self.repo_signing_key)
            raise RepositoryUntrustedSigningKeyError(repo_key_url,
                                                     self.key_info['fingerprint'])

    def prompt_for_install(self):
        """
        Prompt user to install untrusted repo signing key
        """
        print(self.key_info)
        repo_key_url = "{0}/{1}".format(self.url, self.repo_signing_key)
        print(("warning: Repository key untrusted \n"
               "Importing GPG key 0x{0}:\n"
               "  Userid: \"{1}\"\n"
               "  From  : {2}".format(self.key_info['fingerprint'],
                                      self.key_info['uids'][0],
                                      repo_key_url)))
        response = prompt(u'Is this ok: [y/N] ')
        if response == 'y':
            self.install_key(self.raw_key)
            return True
        else:
            return False

    def install_key(self, key_data):
        """
        Install untrusted repo signing key
        """
        logger.info(("importing repository signing key {0} "
                     "{1}".format(self.key_info['fingerprint'],
                                  self.key_info['uids'][0])))
        import_result = self.gpg.import_keys(key_data)
        logger.debug("import results: {0}".format(import_result.results))

    def fetch(self, kernel_version, manifest_type):
        """
        Search repository for kernel module matching kernel_version

        :type kernel_version: str
        :param kernel_version: kernel version to search repository on
        :type manifest_type: str
        :param manifest_type: kernel module manifest to search on
        """
        metadata = self.get_metadata()
        logger.debug("parsed metadata: {0}".format(metadata))
        manifest = self.get_manifest(metadata['manifests'][manifest_type])

        try:
            module = manifest[kernel_version]
            logger.debug("found module {0}".format(module))
        except KeyError:
            raise KernelModuleNotFoundError(kernel_version, self.url)
        path = self.fetch_module(module)

        return path

    def get_metadata(self):
        """
        Fetch repository repomd.xml file
        """
        metadata_path = "{}/{}/{}".format(self.url,
                                          self.metadata_dir,
                                          self.metadata_file)
        metadata_sig_path = "{}/{}/{}.sig".format(self.url.rstrip('/'),
                                                  self.metadata_dir,
                                                  self.metadata_file)
        # load metadata
        req = requests.get(metadata_path)
        if req.status_code is 200:
            raw_metadata = req.content
        else:
            raise RepositoryError(metadata_path, ("status code not 200: "
                                                  "{}".format(req.status_code)))

        if self.gpg_verify:
            self.verify_data_signature(metadata_sig_path, metadata_path,
                                       raw_metadata)

        return self.parse_metadata(raw_metadata)

    def parse_metadata(self, metadata_xml):
        """
        Parse repomd.xml file

        :type metadata_xml: str
        :param metadata_xml: raw xml representation of repomd.xml
        """
        try:
            metadata = dict()
            mdata = xmltodict.parse(metadata_xml)['metadata']
            metadata['revision'] = mdata['revision']
            metadata['manifests'] = dict()

            # check if multiple manifests are present
            if type(mdata['data']) is list:
                manifests = mdata['data']
            else:
                manifests = [mdata['data']]

            for manifest in manifests:
                manifest_dict = dict()
                manifest_dict['type'] = manifest['@type']
                manifest_dict['checksum'] = manifest['checksum']
                manifest_dict['open_checksum'] = manifest['open_checksum']
                manifest_dict['location'] = manifest['location']['@href']
                manifest_dict['timestamp'] = datetime.fromtimestamp(
                                                 int(manifest['timestamp']))
                manifest_dict['size'] = int(manifest['size'])
                manifest_dict['open_size'] = int(manifest['open_size'])
                metadata['manifests'][manifest['@type']] = manifest_dict

        except Exception as e:
            raise RepositoryError("{0}/{1}".format(self.url,self.metadata_dir,
                                                   self.metadata_file), e)

        return metadata

    def get_manifest(self, metadata):
        """
        Get latest manifest as specified in repomd.xml

        :type metadata: dict
        :param metadata: dictionary representation of repomd.xml
        """
        manifest_path = "{0}/{1}".format(self.url, metadata['location'])
        req = requests.get(manifest_path, stream=True)
        if req.status_code is 200:
            gz_manifest = req.raw.read()

        self.verify_checksum(gz_manifest, metadata['checksum'],
                             metadata['location'])
        manifest = self.unzip_manifest(gz_manifest)
        self.verify_checksum(manifest, metadata['open_checksum'],
                             metadata['location'].rstrip('.gz'))

        return self.parse_manifest(manifest)

    def unzip_manifest(self, raw_manifest):
        """
        Decompress gzip encoded manifest

        :type raw_manifest: str
        :param raw_manifest: compressed gzip manifest file content
        """
        buf = BytesIO(raw_manifest)
        f = gzip.GzipFile(fileobj=buf)
        manifest = f.read()

        return manifest

    def parse_manifest(self, manifest_xml):
        """
        Parse manifest xml file

        :type manifest_xml: str
        :param manifest_xml: raw xml content of manifest file
        """

        manifest = dict()
        try:
            mdata = xmltodict.parse(manifest_xml)['modules']['module']
            for module in mdata:
                mod = dict()
                mod['type'] = module['@type']
                mod['name'] = module['name']
                mod['arch'] = module['arch']
                mod['checksum'] = module['checksum']
                mod['version'] = module['version']
                mod['packager'] = module['packager']
                mod['location'] = module['location']['@href']
                mod['signature'] = module['signature']['@href']
                mod['platform'] = module['platform']
                manifest[mod['version']] = mod

        except Exception as e:
            raise

        return manifest

    def fetch_module(self, module):
        """
        Download and verify kernel module

        :type module: str
        :param module: kernel module path
        """
        tm = int(time.time())
        datestamp = datetime.utcfromtimestamp(tm).isoformat()
        filename = "lime-{0}-{1}.ko".format(datestamp, module['version'])
        url = "{0}/{1}".format(self.url, module['location'])
        logger.info("downloading {0} as {1}".format(url, filename))
        req = requests.get(url, stream=True)

        with open(filename, 'wb') as f:
            f.write(req.raw.read())

        self.verify_module(filename, module, self.gpg_verify)
        return filename

    def verify_module(self, filename, module, verify_signature):
        """
        Verify kernel module checksum and signature

        :type filename: str
        :param filename: downloaded kernel module path
        :type module: dict
        :param module: kernel module metadata
        :type verify_signature: bool
        :param verify_signature: enable/disable signature verification
        """
        with open(filename, 'rb') as f:
            module_data = f.read()
        self.verify_checksum(module_data, module['checksum'],
                             module['location'])

        if self.gpg_verify:
            signature_url = "{0}/{1}".format(self.url, module['signature'])
            file_url = "{0}/{1}".format(self.url, module['location'])
            self.verify_file_signature(signature_url, file_url, filename)

    def verify_checksum(self, data, checksum, filename):
        """
        Verify sha256 checksum vs calculated checksum

        :type data: str
        :param data: data used to calculate checksum
        :type checksum: str
        :param checksum: expected checksum of data
        :type filename: str
        :param checksum: original filename
        """
        calculated_checksum = hashlib.sha256(data).hexdigest()
        logger.debug("calculated checksum {0} for {1}".format(calculated_checksum,
                                                              filename))
        if calculated_checksum != checksum:
            raise RepositoryError("{0}/{1}".format(self.url, filename),
                                  ("checksum verification failed, expected "
                                   "{0} got {1}".format(checksum,
                                                        calculated_checksum)))

    def verify_data_signature(self, signature_url, data_url, data):
        """
        Verify data against it's remote signature

        :type signature_url: str
        :param signature_url: remote path to signature for data_url
        :type data_url: str
        :param data_url: url from which data was fetched
        :type data: str
        :param data: content of remote file at file_url
        """

        req = requests.get(signature_url)
        if req.status_code is 200:
            tm = int(time.time())
            datestamp = datetime.utcfromtimestamp(tm).isoformat()
            sigfile = "repo-{0}-tmp.sig".format(datestamp)
            logger.debug("writing {0} to {1}".format(signature_url, sigfile))
            with open(sigfile, 'wb') as f:
                f.write(req.content)
        else:
            raise RepositoryMissingSignatureError(signature_url)

        verified = self.gpg.verify_data(sigfile, data)
        try:
            os.remove(sigfile)
        except OSError:
            pass

        if verified.valid is True:
            logger.debug("verified {0} against {1}".format(data_url,
                                                           signature_url))
        else:
            raise RepositorySignatureError(data_url, signature_url)

    def verify_file_signature(self, signature_url, file_url, filename):
        """
        Verify a local file against it's remote signature

        :type signature_url: str
        :param signature_url: remote path to signature for file_url
        :type file_url: str
        :param file_url: url from which file at filename was fetched
        :type filename: str
        :param filename: filename of local file downloaded from file_url
        """

        req = requests.get(signature_url, stream=True)
        if req.status_code is 200:
            sigfile = req.raw
        else:
            raise RepositoryMissingSignatureError(signature_url)

        verified = self.gpg.verify_file(sigfile, filename)

        if verified.valid is True:
            logger.debug("verified {0} against {1}".format(filename, signature_url))
        else:
            raise RepositorySignatureError(file_url, signature_url)
