#
#-*- coding:utf-8 -*-

"""
    Gentoo-keys - seedhandler.py

    Seed handling interface module

    @copyright: 2012-2015 by Brian Dolbec <dol-sen@gentoo.org>
    @copyright: 2014-2015 by Pavlos Ratis <dastergon@gentoo.org>
    @license: GNU GPL2, see COPYING for details.
"""

import os
import re

from snakeoil.demandload import demandload

from gkeys.gkey import GKEY
from gkeys.lock import LockDir
from gkeys.seed import Seeds, decoder

demandload(
    "json:load",
    "gkeys.exception:UpdateDbError",
    "gkeys.fileops:ensure_dirs",
    "gkeys.fileops:updateseeds",
    "gkeys.fetch:Fetch",
    "sslfetch.connections:get_timestamp",
)


class SeedHandler(object):

    def __init__(self, logger, config):
        self.config = config
        self.logger = logger
        self.fingerprint_re = re.compile('[0-9A-Fa-f]{40}')
        self.finerprint_re2 = re.compile('[0-9A-Fa-f]{4}( [0-9A-Fa-f]{4}){9}')
        self.seeds = None
        self.seedsdir_lock = None
        self.update_lock = None


    def new(self, args, checkgkey=False):
        newgkey = self.build_gkeydict(args)
        if checkgkey:
            newgkey, is_good = self.check_gkey(newgkey)
            if is_good:
                newgkey = GKEY(**newgkey)
                self.logger.debug("SeedHandler: new; new gkey: %s" % str(newgkey))
            else:
                return None
        else:
            newgkey = GKEY(**newgkey)
            self.logger.debug("SeedHandler: new; NON-checked new gkey: %s" % str(newgkey))
        return newgkey


    @staticmethod
    def build_gkeydict(args):
        keyinfo = {}
        for attr in GKEY._fields:
            try:
                value = getattr(args, attr)
                if attr == 'name' and isinstance(value, list):
                    value = " ".join(value)
                if attr in ['fingerprint', 'keys'] and value:
                    value = [x.replace(" ", "") for x in value]
                    keyinfo[attr] = value
                if value is not None and attr in ['keydir', 'name', 'nick']:
                    keyinfo[attr] = decoder(value)
                else:
                    keyinfo[attr] = value
            except AttributeError:
                if attr in GKEY._fields:
                    keyinfo[attr] = None
        return keyinfo

    def compare_seeds(self, seeds1, seeds2) :
        '''Compares two seed lists and returns the differences

        @param seeds1: set of seeds to be compared
        @param seeds2: set of seeds to be compared
        @return added_gkeys: list of keys that are included in seed2 but not seed1
        @return changed_gkeys: list of keys that are included in seed1 and seed2 but have been altered
        @return removed_gkeys: list of keys that are included in seed1 but not in seed2
        '''
        old_gkeys = seeds1[1]
        new_gkeys = seeds2[1]
        changed_gkeys = []
        old_changed_gkeys = []
        removed_gkeys = []
        added_gkeys = []
        if old_gkeys:
            for new_gkey in new_gkeys:
                for old_gkey in old_gkeys:
                    if new_gkey.nick == old_gkey.nick and new_gkey != old_gkey:
                        changed_gkeys.append(new_gkey)
                        old_changed_gkeys.append(old_gkey)
                if new_gkey not in old_gkeys and new_gkey not in changed_gkeys:
                    added_gkeys.append(new_gkey)
            for old_gkey in old_gkeys:
                if old_gkey not in new_gkeys and old_gkey not in old_changed_gkeys:
                    removed_gkeys.append(old_gkey)
        else:
            added_gkeys = new_gkeys
        return(added_gkeys, changed_gkeys, removed_gkeys)

    def compare_seeds(self, seeds1, seeds2) :
        '''Compares two seed lists and returns the differences

        @param seeds1: set of seeds to be compared
        @param seeds2: set of seeds to be compared
        @return added_gkeys: list of keys that are included in seed2 but not seed1
        @return changed_gkeys: list of keys that are included in seed1 and seed2 but have been altered
        @return removed_gkeys: list of keys that are included in seed1 but not in seed2
        '''
        old_gkeys = seeds1[1]
        new_gkeys = seeds2[1]
        changed_gkeys = []
        old_changed_gkeys = []
        removed_gkeys = []
        added_gkeys = []
        if old_gkeys:
            for new_gkey in new_gkeys:
                for old_gkey in old_gkeys:
                    if new_gkey.nick == old_gkey.nick and new_gkey != old_gkey:
                        changed_gkeys.append(new_gkey)
                        old_changed_gkeys.append(old_gkey)
                if new_gkey not in old_gkeys and new_gkey not in changed_gkeys:
                    added_gkeys.append(new_gkey)
            for old_gkey in old_gkeys:
                if old_gkey not in new_gkeys and old_gkey not in old_changed_gkeys:
                    removed_gkeys.append(old_gkey)
        else:
            added_gkeys = new_gkeys
        return(added_gkeys, changed_gkeys, removed_gkeys)

    def load_seeds(self, seedfile=None, filepath=None, refresh=False):
        '''Load seed file

        @param seeds: string of the short name seed file
        @param seedfile: string filepath of the file to load
        @return Seeds class instance of the file loaded
        '''
        if not seedfile and not filepath:
            self.logger.error("SeedHandler: load_seeds; no filename to load: "
            "setting = %s.  Please use the -S or -F option to indicate: which seed "
            "file to use." % seedfile)
            return False
        if seedfile:
            filepath = self.config.get_key('seeds', seedfile)
        elif not filepath:
            self.logger.error("SeedHandler: load_seeds; No filepath to load")
        self.logger.debug("SeedHandler: load_seeds; seeds filepath to load: "
            "%s" % filepath)
        seeds = Seeds(config=self.config, _logger=self.logger)
        seeds.load(filepath, refresh=refresh)
        self.seeds = seeds
        return seeds

    def load_category(self, category, nicks=None, refresh=False):
        '''Loads the designated key directories

        @param category: string
        @param nicks: list of string nick ids to load
        @return Seeds class object
        '''
        seeds = Seeds(config=self.config, _logger=self.logger)
        if category == 'sign':
            catdir = self.config.get_key('sign-keydir')
        else:
            #keyrings = self.config.get_key('keyring')
            #catdir = os.path.join(keyrings, category)
            catdir = self.config.get_key('keyrings', category)
        self.logger.debug("SeedHandler: load_category; catdir = %s", catdir)
        try:
            if not nicks:
                nicks = os.listdir(catdir)
            for nick in nicks:
                seed_path = os.path.join(catdir, nick)
                if not os.path.isdir(seed_path):
                    continue
                gkey_path = os.path.join(seed_path, 'gkey.seeds')
                seed = None
                try:
                    with open(gkey_path, 'r') as fileseed:
                        seed = load(fileseed)
                    self.logger.debug("SeedHandler: load_category; loaded seed file %s.",
                                      gkey_path)
                except IOError as error:
                    self.logger.debug("SeedHandler: load_category; IOError loading seed file %s.",
                                      gkey_path)
                    self.logger.debug("Error was: %s" % str(error))
                if seed:
                    self.logger.debug("SeedHandler: load_category; processing seeds: %s", seed)
                    for nick in sorted(seed):
                        key = seed[nick]
                        # GKEY class change auto-update
                        if not 'uid' in list(key):
                            if not refresh:
                                raise UpdateDbError(category)
                            key['uid'] = []
                        if not 'keys' in list(key):
                            if not refresh:
                                raise UpdateDbError(category)
                            key['keys'] = key['fingerprint'][:]
                        seeds.add(nick, GKEY(**key))
        except OSError as error:
            self.logger.debug("SeedHandler: load_category; OSError for %s" % catdir)
            self.logger.exception("Error was: %s" % str(error))
        self.seeds = seeds
        self.logger.debug("SeedHandler: load_category; seeds loaded: %s", seeds)
        return seeds

    def fetch_seeds(self, seeds, args, verified_dl=None):
        '''Fetch new seed files

        @param seeds: list of seed nicks to download
        @param args: argparse namespace instance
        @param verified_dl: Function pointer to the Actions.verify()
                instance needed to do the download and verification
        '''
        http_check = re.compile(r'^(http|https)://')
        urls = []
        messages = []
        try:
            for seed in [seeds]:
                seedurl = self.config.get_key('seedurls', seed)
                seedpath = self.config.get_key('seeds', seed)
                self.logger.debug("Seed URL/seed path...: %s, %s" % (seedurl, seedpath))
                if seedurl and seedpath and http_check.match(seedurl):
                    urls.extend([(seed, seedurl, seedpath)])
                else:
                    self.logger.info("Wrong seed file URLs/seed path... Skipping: %s" % seed)
        except KeyError:
            pass
        succeeded = []
        seedsdir = os.path.dirname(self.config.get_key('seeds', seeds))
        updatedir = os.path.join(seedsdir, "__updates__")
        mode = int(self.config.get_key('permissions', 'directories'),0)
        ensure_dirs(updatedir, mode=mode)
        self.update_lock = LockDir(updatedir)
        self.update_lock.write_lock()
        for (seed, url, filepath) in urls:
            tmppath = os.path.join(updatedir, os.path.split(filepath)[-1])
            # use the real timestamp file for the dl timestamp
            tpath = filepath + ".timestamp"
            verify_info = self.config.get_key('verify-seeds', seed).split()
            args.category = verify_info[0]
            args.nick = verify_info[1]
            args.filename = url
            args.signature = tmppath + ".sig"
            args.timestamp = tpath
            args.destination = tmppath
            args.fetchonly = False
            verified, messages_ = verified_dl(args)
            messages.append(messages_)
            if verified and not args.fetchonly:
                self.seedsdir_lock = LockDir(seedsdir)
                if (updateseeds(self.config, self.logger, tmppath, filepath) and
                    updateseeds(self.config, self.logger, args.signature, tpath)
                ):
                    self.logger.info("Updated seed file...: %s ... OK" % (filepath))
                    succeeded.append(verified)
                else:
                    self.logger.info("Updating seed file...: %s ... Failed" % (filepath))
                    succeeded.append(False)
                self.seedsdir_lock.unlock()
            else:
                # sha512sum the 2 files
                pass
        self.update_lock.unlock()
        return (succeeded, messages)

    def check_gkey(self, args):
        # assume it's good until an error is found
        is_good = True
        try:
            args['keydir'] = args.get('keydir', args['nick'])
            fprs = []
            keys = []
            if args['keys'] or args['fingerprint']:
                for fpr in args['keys']:
                    is_good, fingerprint = self._check_fingerprint_integrity(fpr)
                    if is_good:
                        keys.append(fingerprint)
                    else:
                        self.logger.error('Bad key from command line args: %s' % fpr)
                if is_good:
                    args['keys'] = keys
                    for fpr in args['fingerprint']:
                        is_good, fingerprint = self._check_fingerprint_integrity(fpr)
                        if is_good:
                            fprs.append(fingerprint)
                        else:
                            self.logger.error('Bad fingerprint from command line args: %s' % fpr)
                    if is_good:
                        args['fingerprint'] = fprs
        except KeyError:
            self.logger.error('GPG fingerprint not found.')
            is_good = False
        if not is_good:
            self.logger.error('An invalid key or fingerprint '
                  'was found for %s' % args['name'])
        return args, is_good

    def _check_fingerprint_integrity(self, fpr):
        # assume it's good unti an error is found
        is_good = True
        fingerprint = fpr.replace(" ", "")
        # check fingerprint integrity
        if len(fingerprint) != 40:
            self.logger.error('  GPGKey incorrect fingerprint ' +
                    'length (%s) for fingerprint: %s' % (len(fingerprint), fingerprint))
            is_good = False
        if not self.fingerprint_re.match(fingerprint):
            self.logger.error('  GPGKey: Non hexadecimal digits in ' + 'fingerprint for fingerprint: ' + fingerprint)
            is_good = False
        return is_good, fingerprint

    def key_search(self, args, search_args):
        '''Performs a search for all listed args in the seeds'''
        search_args.sort()
        self.logger.debug("SeedHandler.key_search() search_args: %s" % str(search_args))
        self.logger.debug("SeedHandler.key_search() search_args values: %s" % str(args))
        results = []
        found = {}
        if isinstance(args, dict):
            exact = args.get('exact', False)
            _all = args.get('all', False)
        else:
            exact = getattr(args, 'exact', False)
            _all = getattr(args, 'all', False)
        for arg in search_args:
            if isinstance(args, dict):
                value = args.get(arg, '')
            else:
                value = getattr(args, arg)
            seeds = self.seeds.field_search(arg, value, exact)
            for seed in seeds:
                if seed.nick in found:
                    found[seed.nick]['args'].append(arg)
                else:
                    found[seed.nick] = {'args': [arg], 'seed': seed}
        if _all:
            for possible in sorted(found):
                if search_args == found[possible]['args']:
                    results.append(found[possible]['seed'])
        else:
            for nick in sorted(found):
                results.append(found[nick]['seed'])
        return results
