# coding: utf-8

from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals

import re
import six
import logging
import funcsigs

import yt.wrapper as yt

from copy import deepcopy
from collections import defaultdict
from saas.library.python.singleton import Singleton
from saas.library.python.byte_size import ByteSize
from saas.library.python.token_store import PersistentTokenStore


YT_TABLE_USAGE_PATH = '//home/saas/ssm/service_resources_usage'
YT_TABLE_QUOTAS_PATH = '//home/saas/ssm/quotas_data'


def yt_get_table(path, cluster='hahn'):
    """
    Load data from yt table
    :param path: type str
    :param cluster: type str
    :return: type list of dict
    """
    yt_client = yt.YtClient(proxy=cluster, token=PersistentTokenStore.get_token_from_store_env_or_file('yt'))
    return list(yt_client.read_table(path, format=yt.JsonFormat(encoding="utf-8")))


def resource_dict_sum(resources_1, resources_2):
    result = resources_1.copy()

    for location, value in resources_2.items():
        if location not in result.keys():
            result[location] = value
        else:
            result[location] = resources_1[location] + resources_2[location]
    return result


def resource_dict_diff(resources_1, resources_2):
    result = resources_1.copy()

    for location, value in resources_2.items():
        if location not in result.keys():
            result[location] = - value
        else:
            result[location] = resources_1[location] - resources_2[location]
    return result


class CpuResource(object):
    __slots__ = ('_value', )

    def __init__(self, ms=0):
        self._value = ms if ms is not None else 0

    @property
    def cores(self):
        return self._value/1000

    @property
    def milliseconds_per_second(self):
        return self._value

    def __str__(self):
        return str(self.cores)

    def __repr__(self):
        return 'CpuResource({})'.format(self._value)

    def __eq__(self, other):
        if isinstance(other, CpuResource):
            return self._value == other._value
        else:
            return NotImplemented

    def __gt__(self, other):
        if isinstance(other, CpuResource):
            return self._value > other._value
        else:
            return NotImplemented

    def __ge__(self, other):
        if isinstance(other, CpuResource):
            return self._value >= other._value
        else:
            return NotImplemented

    def __add__(self, other):
        if isinstance(other, CpuResource):
            return CpuResource(self._value + other._value)
        else:
            return NotImplemented

    def __iadd__(self, other):
        if isinstance(other, CpuResource):
            self._value += other._value
            return self
        else:
            return NotImplemented

    def __sub__(self, other):
        if isinstance(other, CpuResource):
            return CpuResource(self._value - other._value)
        else:
            return NotImplemented

    def __isub__(self, other):
        if isinstance(other, CpuResource):
            self._value -= other._value
            return self
        else:
            return NotImplemented

    def __neg__(self):
        return CpuResource(-self._value)


class WithTableView(object):  # TODO: use metaclass
    FIELD_ORDER = ['cpu_guarantee', 'ram_guarantee', 'hdd_volume', 'ssd_volume', 'hdd_bandwidth', 'ssd_bandwidth']
    FIELD_NAME_MAPPING = {'cpu_guarantee': 'CPU', 'cpu_limit': 'CPU_LIMIT', 'ram_guarantee': 'RAM', 'hdd_volume': 'HDD', 'ssd_volume': 'SSD',
                          'hdd_bandwidth': 'HDD_READ_BW', 'ssd_bandwidth': 'SSD_READ_BW'}

    def get_table_view(self, custom_field_order=None, custom_table_mapping=None):
        field_order = custom_field_order if custom_field_order is not None else self.FIELD_ORDER
        field_name_mapping = custom_table_mapping if custom_table_mapping is not None else self.FIELD_NAME_MAPPING
        location_table = defaultdict(dict)

        for res_type in field_order:
            for location, usage in getattr(self, res_type).items():
                location_table[location][res_type] = usage

        location_order = sorted(location_table.keys())
        if not location_order:
            return None

        header = ['', ] + sorted(location_table.keys()) + ['TOTAL', ]
        table = [header, ]
        for field_name in field_order:
            total = None
            row_header = [field_name_mapping[field_name], ]
            row = []
            for location in location_order:
                value = location_table[location].get(field_name, None)
                row.append(str(value))
                if value is not None:
                    if total is None:
                        total = deepcopy(value)
                    else:
                        total += value

            row_trailer = [str(total), ]

            table.append(row_header + row + row_trailer)
        return table


class WithTotalResource(object):
    _total_resource_regexp = re.compile(r'^total_(?P<resource>\w+(_\w+)*)$')

    def __getattr__(self, item):
        match = self._total_resource_regexp.match(item)
        if match:
            result = None
            for value in object.__getattribute__(self, match.group('resource')).values():
                if result is None:
                    result = deepcopy(value)
                else:
                    result += value
            return result
        else:
            return object.__getattribute__(self, item)


class AbcService(six.with_metaclass(Singleton)):
    @classmethod
    def _get_instance_id(cls, args, kwargs):
        """
        Singleton interface
        """
        signature = funcsigs.signature(cls.__init__)
        bound_params = signature.bind(cls, *args, **kwargs)
        return bound_params.arguments['service_id']

    @classmethod
    def _extra_actions(cls, instance, args, kwargs):
        """
        Singleton customisation
        """
        signature = funcsigs.signature(cls.__init__)
        bound_params = signature.bind(cls, *args, **kwargs)
        if 'slug' in bound_params.arguments and not instance.slug:
            instance.slug = bound_params.arguments['slug']
        if 'hr_name' in bound_params.arguments and not instance.hr_name:
            instance.hr_name = bound_params.arguments['hr_name']

    def __init__(self, service_id, slug=None, hr_name=None):
        self.service_id = service_id
        self.slug = slug
        self.hr_name = hr_name

    def __repr__(self):
        return 'AbcService({}, {}, {})'.format(self.service_id, self.slug, self.hr_name)


class AbcServiceQuota(WithTableView, WithTotalResource):
    QUOTA_FIELD_ORDER = ['cpu', 'ram', 'hdd', 'ssd']
    QUOTA_FIELD_NAME_MAPPING = {'cpu': 'CPU', 'ram': 'RAM', 'hdd': 'HDD', 'ssd': 'SSD'}
    QUOTA_FIELD_CLASS_MAPPING = {'cpu': CpuResource, 'ram': ByteSize, 'hdd': ByteSize, 'ssd': ByteSize}
    SPARE_FIELD_ORDER = ['cpu_free', 'ram_free', 'hdd_free', 'ssd_free']
    SPARE_FIELD_NAME_MAPPING = {'cpu_free': 'CPU', 'ram_free': 'RAM', 'hdd_free': 'HDD', 'ssd_free': 'SSD'}

    @classmethod
    def load_from_yt(cls, quota_service_id):
        quotas_data = yt_get_table(YT_TABLE_QUOTAS_PATH)
        services_data = yt_get_table(YT_TABLE_USAGE_PATH)

        quotas = [cls.from_table_row(table_row) for table_row in quotas_data if table_row['quota_abc_id'] == quota_service_id]
        if len(quotas) == 0:
            logging.warning('No quota info for quota_service_id %d', quota_service_id)
            quota = cls(AbcService(quota_service_id), {}, {}, {}, {})
        elif len(quotas) == 1:
            quota = quotas[0]
        else:
            raise RuntimeError('Multiple quotas for quota_service_id {}'.format(quota_service_id))

        services = [SaasResourceUsage.from_table_row(table_row) for table_row in services_data if table_row['quota_abc']['id'] == quota_service_id]
        for service in services:
            quota.add_service(service)
        return quota

    @classmethod
    def load_all_from_yt(cls):
        quotas_data = yt_get_table(YT_TABLE_QUOTAS_PATH)
        services_data = yt_get_table(YT_TABLE_USAGE_PATH)

        quotas = [cls.from_table_row(table_row) for table_row in quotas_data]
        services = [SaasResourceUsage.from_table_row(table_row) for table_row in services_data]

        quotas_dict = {quota.abc_service.service_id: quota for quota in quotas}
        for service in services:
            if service.abc_quota.service_id not in quotas_dict.keys():
                quotas_dict[service.abc_quota.service_id] = cls(service.abc_quota, {}, {}, {}, {})

            quotas_dict[service.abc_quota.service_id].add_service(service)
            # print(quotas_dict[service.abc_quota.service_id].total_cpu)
        # print(quotas_dict.values())
        return sorted(quotas_dict.values(), key=lambda quota: (quota.total_cpu or CpuResource(0)), reverse=True)

    def __init__(self, abc_service, cpu_quotas, ram_quotas, hdd_quotas, ssd_quotas):
        """
        @type abc_service: AbcService
        @type cpu_quotas: Dict[AnyStr, int]
        @type ram_quotas: Dict[AnyStr, int]
        @type hdd_quotas: Dict[AnyStr, int]
        @type ssd_quotas: Dict[AnyStr, int]
        """
        self.abc_service = abc_service

        for resource_type, cls in self.QUOTA_FIELD_CLASS_MAPPING.items():
            self.__setattr__(resource_type, {})

        self.cpu.update({location: self.QUOTA_FIELD_CLASS_MAPPING['cpu'](value) for location, value in cpu_quotas.items()})
        self.ram.update({location: self.QUOTA_FIELD_CLASS_MAPPING['ram'](value) for location, value in ram_quotas.items()})
        self.hdd.update({location: self.QUOTA_FIELD_CLASS_MAPPING['hdd'](value) for location, value in hdd_quotas.items()})
        self.ssd.update({location: self.QUOTA_FIELD_CLASS_MAPPING['ssd'](value) for location, value in ssd_quotas.items()})

        self._projects = {}  # type: Dict[int, ServiceGroup]

    def __eq__(self, other):
        if isinstance(other, AbcServiceQuota):
            return self.abc_service == other.abc_service
        else:
            return NotImplemented

    @classmethod
    def from_table_row(cls, row):
        """
        See table schema at https://yt.yandex-team.ru/hahn/navigation?path=//home/saas/ssm/quotas_data
        """
        abc_service = AbcService(row['quota_abc_id'], row['quota_abc_name'])
        return AbcServiceQuota(abc_service, cpu_quotas=row['CPU'], ram_quotas=row['RAM'], hdd_quotas=row['HDD'], ssd_quotas=row['SSD'])

    def add_service(self, service):
        # type: (SaasResourceUsage) -> None
        if service.abc_quota != self.abc_service:
            logging.error('Adding service with wrong abc quota: self: %s, service: %s', self.abc_service.service_id, service.abc_quota.service_id)
        if service.abc_service.service_id not in self._projects.keys():
            self._projects[service.abc_service.service_id] = ServiceGroup(service.abc_service)
        self._projects[service.abc_service.service_id].add_service(service)

    @property
    def projects(self):
        return sorted(self._projects.values(), key=lambda p: p.total_cpu_guarantee, reverse=True)

    def _compute_resource_usage(self, resource_type):
        result = {}
        for project in self._projects.values():
            result = resource_dict_sum(result, getattr(project, resource_type))
        return result

    @property
    def cpu_guarantee(self):
        return self._compute_resource_usage('cpu_guarantee')

    @property
    def ram_guarantee(self):
        return self._compute_resource_usage('ram_guarantee')

    @property
    def hdd_volume(self):
        return self._compute_resource_usage('hdd_volume')

    @property
    def ssd_volume(self):
        return self._compute_resource_usage('ssd_volume')

    @property
    def hdd_bandwidth(self):
        return self._compute_resource_usage('hdd_bandwidth')

    @property
    def ssd_bandwidth(self):
        return self._compute_resource_usage('ssd_bandwidth')

    @property
    def cpu_free(self):
        return resource_dict_diff(self.cpu, self.cpu_guarantee)

    @property
    def ram_free(self):
        return resource_dict_diff(self.ram, self.ram_guarantee)

    @property
    def hdd_free(self):
        return resource_dict_diff(self.hdd, self.hdd_volume)

    @property
    def ssd_free(self):
        return resource_dict_diff(self.ssd, self.ssd_volume)

    def get_quota_table_view(self):
        return self.get_table_view(self.QUOTA_FIELD_ORDER, self.QUOTA_FIELD_NAME_MAPPING)

    def get_free_table_view(self):
        return self.get_table_view(self.SPARE_FIELD_ORDER, self.SPARE_FIELD_NAME_MAPPING)


class ServiceGroup(WithTableView, WithTotalResource):
    """
    subproject
    """
    def __init__(self, abc_service, saas_services=None):
        """
        @type abc_service: AbcService
        @type saas_services: List[SaasResourceUsage]
        """
        self.abc_service = abc_service
        self._services = []
        self.cpu_guarantee = {}
        self.ram_guarantee = {}
        self.hdd_volume = {}
        self.ssd_volume = {}
        self.hdd_bandwidth = {}
        self.ssd_bandwidth = {}

        if saas_services is not None:
            for service in saas_services:
                self.add_service(service)

    def add_service(self, service):
        logging.debug('Resources before add service: cpu:%s, ram:%s, hdd:%s, ssd:%s', self.cpu_guarantee, self.ram_guarantee, self.hdd_volume, self.ssd_volume)
        logging.debug('Service resource: cpu:%s, ram:%s, hdd:%s, ssd:%s', service.cpu_guarantee, service.ram_guarantee, service.hdd_volume, service.ssd_volume)
        self._services.append(service)
        self.cpu_guarantee = resource_dict_sum(self.cpu_guarantee, service.cpu_guarantee)
        self.ram_guarantee = resource_dict_sum(self.ram_guarantee, service.ram_guarantee)
        self.hdd_volume = resource_dict_sum(self.hdd_volume, service.hdd_volume)
        self.ssd_volume = resource_dict_sum(self.ssd_volume, service.ssd_volume)
        self.hdd_bandwidth = resource_dict_sum(self.hdd_bandwidth, service.hdd_bandwidth)
        self.ssd_bandwidth = resource_dict_sum(self.ssd_bandwidth, service.ssd_bandwidth)
        logging.debug('Resources after add service: cpu:%s, ram:%s, hdd:%s, ssd:%s', self.cpu_guarantee, self.ram_guarantee, self.hdd_volume, self.ssd_volume)

    @property
    def services(self):
        return sorted(self._services, key=lambda s: s.total_cpu_guarantee, reverse=True)


class SaasResourceUsage(WithTableView, WithTotalResource):
    FIELD_ORDER = ['cpu_guarantee', 'cpu_limit', 'ram_guarantee', 'hdd_volume', 'ssd_volume', 'hdd_bandwidth', 'ssd_bandwidth']
    __slots__ = ('ctype', 'name', 'abc_service', 'abc_quota', 'owner', ) + tuple(FIELD_ORDER)

    def __init__(self, service_ctype, service_name, service_owner, service_abc, quota_abc, usage):
        self.ctype = service_ctype
        self.name = service_name
        self.abc_service = service_abc
        self.abc_quota = quota_abc
        self.owner = service_owner
        for usage_type in self.FIELD_ORDER:
            self.__setattr__(usage_type, {})
        for location, usage_info in usage.items():
            self.cpu_guarantee[location] = CpuResource(usage_info.get('CPU', 0))
            self.cpu_limit[location] = CpuResource(usage_info.get('CPU_LIMIT', 0))
            self.ram_guarantee[location] = ByteSize(usage_info.get('RAM', 0))
            self.hdd_volume[location] = ByteSize(usage_info.get('HDD', 0))
            self.ssd_volume[location] = ByteSize(usage_info.get('SSD', 0))
            self.hdd_bandwidth[location] = ByteSize(usage_info.get('HDD_READ_BW'))
            self.ssd_bandwidth[location] = ByteSize(usage_info.get('SSD_READ_BW'))

    def __eq__(self, other):
        return self.name == other.name and self.ctype == other.ctype

    @classmethod
    def from_table_row(cls, row):
        """
        See table schema at https://yt.yandex-team.ru/hahn/navigation?path=//home/saas/ssm/service_resources_usage
        """
        abc_quota = AbcService(row['quota_abc']['id'], row['quota_abc']['name'], row['quota_abc']['hr_name'])
        abc_service = AbcService(row['service_abc']['id'], row['service_abc']['name'], row['service_abc']['hr_name'])
        return cls(row['service_ctype'], row['service_name'], row['service_owner'], service_abc=abc_service, quota_abc=abc_quota, usage=row['usage'])
