# -*- coding: utf-8 -*-
#
# Copyright (C) 2014-2016 Avencall
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>


import logging
from concurrent.futures import ALL_COMPLETED
from concurrent.futures import ThreadPoolExecutor
from concurrent.futures import wait

from xivo.util import tokenize

from xivo_dird import BaseServicePlugin
from xivo_dird import helpers

logger = logging.getLogger(__name__)


class LookupServicePlugin(BaseServicePlugin):

    def __init__(self):
        self._service = None

    def load(self, args):
        try:
            self._service = _LookupService(args['config'], args['sources'])
            return self._service
        except KeyError:
            msg = ('%s should be loaded with "config" and "sources" but received: %s'
                   % (self.__class__.__name__, ','.join(args.keys())))
            raise ValueError(msg)

    def unload(self):
        if self._service:
            self._service.stop()
            self._service = None


class _LookupService(object):

    def __init__(self, config, sources):
        self._global_config = config
        self._sources = sources
        self._executor = ThreadPoolExecutor(max_workers=10)

    def stop(self):
        self._executor.shutdown()

    def _config(self, profile):
        return self._global_config.get('services', {}).get('lookup', {}).get(profile, {})

    def _async_search(self, source, term, args):
        raise_stopper = helpers.RaiseStopper(return_on_raise=[])
        future = self._executor.submit(raise_stopper.execute, source.search, term, args)
        future.name = source.name
        return future

    def _source_by_profile(self, profile):
        try:
            source_names = self._config(profile)['sources']
        except KeyError:
            logger.warning('Cannot find lookup sources for profile %s', profile)
            return []
        else:
            return [self._sources[name] for name in source_names if name in self._sources]

    def lookup(self, term, profile, xivo_user_uuid, args, token):
        futures = []
        for source in self._source_by_profile(profile):
            args['token'] = token
            args['xivo_user_uuid'] = xivo_user_uuid
            futures.append(self._async_search(source, term, args))

        params = {'return_when': ALL_COMPLETED}
        if 'timeout' in self._config(profile):
            params['timeout'] = self._config(profile)['timeout']

        done, _ = wait(futures, **params)
        results = []
        for future in done:
            for result in future.result():
                results.append(result)
        return self.sort_search_results(results, term)

    @staticmethod
    def sort_search_results(results, term):

        def sort(items):
            sortable = list()
            non_sortable = list()
            for i in items:
                if i.is_sortable:
                    sortable.append(i)
                else:
                    non_sortable.append(i)
            sortable = sorted(sortable, key=lambda _: _.sort_string)
            return sortable + non_sortable

        to_sort = ['word', 'start', 'other']
        not_sort = ['fuzzy']

        classified = {_: list() for _ in to_sort + not_sort}
        for result in [ResultForSort(_, term) for _ in results]:
            classified[result.classify()].append(result)
        final = list()
        for item in to_sort:
            final.extend(sort(classified[item]))
        for item in not_sort:
            final.extend(classified[item])
        return [_.result for _ in final]


class ResultForSort(object):

    def __init__(self, result, term):
        self.result = result
        self.term = term
        self.sort_string = self.find_sort_string()
        self.tokenized_values = self.tokenize_values()

    def __repr__(self):
        return '; '.join([f'{k}: {v}' for k, v in self.result.fields.items()])

    def find_sort_string(self):
        item = self.result.fields
        if 'display_name' in item.keys() and item['display_name']:
            return item['display_name'].lower()
        names = list()
        if 'firstname' in item.keys() and item['firstname']:
            names.append(item['firstname'])
        if 'lastname' in item.keys()and item['lastname']:
            names.append(item['lastname'])
        if len(names) > 0:
            return ' '.join(names).lower()
        return ''

    @property
    def is_sortable(self):
        return self.sort_string != ''

    def check_fuzzines(self, tokenized_term):
        for term in tokenized_term:
            for field in self.tokenized_values:
                if isinstance(field, str) and term in field:
                    return False
        return True

    def tokenize_values(self):
        values = set()
        for value in self.result.fields.values():
            values.update(set(tokenize(str(value))))
        return values

    def check_word_match(self, tokenized_term):
        for term in tokenized_term:
            if term in self.tokenized_values:
                return True
        return False

    def check_start_match(self, tokenized_term):
        for term in tokenized_term:
            for value in self.tokenized_values:
                if value.startswith(term):
                    return True
        return False

    def classify(self):
        classes = (
            (self.check_fuzzines, 'fuzzy'),
            (self.check_word_match, 'word'),
            (self.check_start_match, 'start')
        )
        tokenized_term = tokenize(self.term)
        for method, name in classes:
            if method(tokenized_term):
                return name
        return 'other'
