# -*- coding: utf-8 -*-
# Copyright 2014 Objectif Libre
#
#    Licensed under the Apache License, Version 2.0 (the "License"); you may
#    not use this file except in compliance with the License. You may obtain
#    a copy of the License at
#
#         http://www.apache.org/licenses/LICENSE-2.0
#
#    Unless required by applicable law or agreed to in writing, software
#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
#    License for the specific language governing permissions and limitations
#    under the License.
#
import decimal

from cloudkitty import dataframe
from cloudkitty import rating
from cloudkitty.rating.hash.controllers import root as root_api
from cloudkitty.rating.hash.db import api as hash_db_api


class HashMap(rating.RatingProcessorBase):
    """HashMap rating module.

    HashMap can be used to map arbitrary fields of a resource to different
    costs.
    """

    module_name = 'hashmap'
    description = 'HashMap rating module.'
    hot_config = True
    config_controller = root_api.HashMapConfigController

    db_api = hash_db_api.get_instance()

    def __init__(self, tenant_id=None):
        super(HashMap, self).__init__(tenant_id)
        self._entries = {}
        self._res = {}
        self._load_rates()

    def reload_config(self):
        """Reload the module's configuration.

        """
        self._load_rates()

    def _load_mappings(self, mappings_uuid_list):
        hashmap = hash_db_api.get_instance()
        mappings = {}
        for mapping_uuid in mappings_uuid_list:
            mapping_db = hashmap.get_mapping(uuid=mapping_uuid)
            if mapping_db.group_id:
                group_name = mapping_db.group.name
            else:
                group_name = '_DEFAULT_'
            if group_name not in mappings:
                mappings[group_name] = {}
            current_scope = mappings[group_name]

            mapping_value = mapping_db.value
            if mapping_value:
                current_scope[mapping_value] = {}
                current_scope = current_scope[mapping_value]
            current_scope['type'] = mapping_db.map_type
            current_scope['cost'] = mapping_db.cost
        return mappings

    def _load_thresholds(self, thresholds_uuid_list):
        hashmap = hash_db_api.get_instance()
        thresholds = {}
        for threshold_uuid in thresholds_uuid_list:
            threshold_db = hashmap.get_threshold(uuid=threshold_uuid)
            if threshold_db.group_id:
                group_name = threshold_db.group.name
            else:
                group_name = '_DEFAULT_'
            if group_name not in thresholds:
                thresholds[group_name] = {}
            current_scope = thresholds[group_name]

            threshold_level = threshold_db.level
            current_scope[threshold_level] = {}
            current_scope = current_scope[threshold_level]
            current_scope['type'] = threshold_db.map_type
            current_scope['cost'] = threshold_db.cost
        return thresholds

    def _update_entries(self,
                        entry_type,
                        root,
                        service_uuid=None,
                        field_uuid=None,
                        tenant_uuid=None):
        hashmap = hash_db_api.get_instance()
        list_func = getattr(hashmap, 'list_{}'.format(entry_type))
        entries_uuid_list = list_func(
            service_uuid=service_uuid,
            field_uuid=field_uuid,
            tenant_uuid=tenant_uuid)
        load_func = getattr(self, '_load_{}'.format(entry_type))
        entries = load_func(entries_uuid_list)
        if entry_type in root:
            res = root[entry_type]
            for group, values in entries.items():
                if group in res:
                    res[group].update(values)
                else:
                    res[group] = values
        else:
            root[entry_type] = entries

    def _load_service_entries(self, service_name, service_uuid):
        self._entries[service_name] = dict()
        for entry_type in ('mappings', 'thresholds'):
            for tenant in (None, self._tenant_id):
                self._update_entries(
                    entry_type,
                    self._entries[service_name],
                    service_uuid=service_uuid,
                    tenant_uuid=tenant)

    def _load_field_entries(self, service_name, field_name, field_uuid):
        if service_name not in self._entries:
            self._entries[service_name] = {}
        if 'fields' not in self._entries[service_name]:
            self._entries[service_name]['fields'] = {}
        scope = self._entries[service_name]['fields'][field_name] = {}
        for entry_type in ('mappings', 'thresholds'):
            for tenant in (None, self._tenant_id):
                self._update_entries(
                    entry_type,
                    scope,
                    field_uuid=field_uuid,
                    tenant_uuid=tenant)

    def _load_rates(self):
        self._entries = {}
        hashmap = hash_db_api.get_instance()
        services_uuid_list = hashmap.list_services()
        for service_uuid in services_uuid_list:
            service_db = hashmap.get_service(uuid=service_uuid)
            service_name = service_db.name
            self._load_service_entries(service_name, service_uuid)
            fields_uuid_list = hashmap.list_fields(service_uuid)
            for field_uuid in fields_uuid_list:
                field_db = hashmap.get_field(uuid=field_uuid)
                field_name = field_db.name
                self._load_field_entries(service_name, field_name, field_uuid)

    def add_rating_informations(self, point):
        for entry in self._res.values():
            rate = entry['rate']
            flat = entry['flat']
            if entry['threshold']['scope'] == 'field':
                if entry['threshold']['type'] == 'flat':
                    flat += entry['threshold']['cost']
                else:
                    rate *= entry['threshold']['cost']
            res = rate * flat
            res *= point.qty
            if entry['threshold']['scope'] == 'service':
                if entry['threshold']['type'] == 'flat':
                    res += entry['threshold']['cost']
                else:
                    res *= entry['threshold']['cost']
            point = point.set_price(point.price + res)
        return point

    def update_result(self,
                      group,
                      map_type,
                      cost,
                      level=0,
                      is_threshold=False,
                      threshold_scope='field'):
        if group not in self._res:
            self._res[group] = {'flat': 0,
                                'rate': 1,
                                'threshold': {
                                    'level': -1,
                                    'cost': 0,
                                    'type': 'flat',
                                    'scope': 'field'}}
        if is_threshold:
            best = self._res[group]['threshold']['level']
            if level > best:
                self._res[group]['threshold']['level'] = level
                self._res[group]['threshold']['cost'] = cost
                self._res[group]['threshold']['type'] = map_type
                self._res[group]['threshold']['scope'] = threshold_scope
        else:
            if map_type == 'rate':
                self._res[group]['rate'] *= cost
            elif map_type == 'flat':
                new_flat = cost
                cur_flat = self._res[group]['flat']
                if new_flat > cur_flat:
                    self._res[group]['flat'] = new_flat

    def process_mappings(self,
                         mapping_groups,
                         cmp_value):
        for group_name, mappings in mapping_groups.items():
            for mapping_value, mapping in mappings.items():
                if str(cmp_value) == mapping_value:

                    self.update_result(
                        group_name,
                        mapping['type'],
                        mapping['cost'])

    def process_thresholds(self,
                           threshold_groups,
                           cmp_level,
                           threshold_type):
        for group_name, thresholds in threshold_groups.items():
            for threshold_level, threshold in thresholds.items():
                if cmp_level >= threshold_level:
                    self.update_result(
                        group_name,
                        threshold['type'],
                        threshold['cost'],
                        threshold_level,
                        True,
                        threshold_type)

    def process_services(self, service_name, point):
        if service_name not in self._entries:
            return
        service_mappings = self._entries[service_name]['mappings']
        for group_name, mapping in service_mappings.items():
            self.update_result(group_name,
                               mapping['type'],
                               mapping['cost'])
        service_thresholds = self._entries[service_name]['thresholds']
        self.process_thresholds(service_thresholds,
                                point.qty,
                                'service')

    def process_fields(self, service_name, point):
        if service_name not in self._entries:
            return
        if 'fields' not in self._entries[service_name]:
            return
        desc_data = point.desc
        field_mappings = self._entries[service_name]['fields']
        for field_name, group_mappings in field_mappings.items():
            if field_name not in desc_data:
                continue
            cmp_value = desc_data[field_name]
            self.process_mappings(group_mappings['mappings'],
                                  cmp_value)
            if group_mappings['thresholds']:
                self.process_thresholds(group_mappings['thresholds'],
                                        decimal.Decimal(cmp_value),
                                        'field')

    def process(self, data):
        output = dataframe.DataFrame(start=data.start, end=data.end)

        for service_name, point in data.iterpoints():
            self._res = {}
            self.process_services(service_name, point)
            self.process_fields(service_name, point)
            output.add_point(self.add_rating_informations(point), service_name)

        return output
