# -*- coding: utf-8 -*-
import requests
import logging
import time

from saas.library.python.token_store import PersistentTokenStore


def request(func):
    """
    Handle requests information.
    """
    def decorator(*args, **kwargs):

        def check_request(response):
            message = 'Status code: %d | URL: %s | Data: %s' % (resp.status_code, resp.url, str(data))
            if response.ok:
                logging.debug('Success request | %s', message)
                return True
            else:
                logging.error('Failed request | %s | Reason: %s | Response data: %s', message, resp.reason, resp.text)
                return False

        max_retry_cnt = 5
        cur_retry_cnt = 0
        if len(args) > 1:
            data = args[1]
        else:
            data = {}
        resp = func(*args, **kwargs)
        if not check_request(resp):
            while cur_retry_cnt < max_retry_cnt:
                time.sleep(cur_retry_cnt)
                resp = func(*args, **kwargs)
                if check_request(resp):
                    break
                cur_retry_cnt += 1
            else:
                raise requests.exceptions.RetryError('Failed request. Max attempts has reached.')
        return resp
    return decorator


class DispenserResource(object):
    """
    Class for describe Dispenser resources.
    Description: https://wiki.yandex-team.ru/dispenser/api/#resource
    """
    RESOURCE_TYPES = ['ENUMERABLE', 'MEMORY', 'POWER', 'STORAGE', 'TRAFFIC']
    QUOTING_MODES = ['DEFAULT', 'ENTITIES_ONLY', 'SYNCHRONIZATION']

    def __init__(self, key, name, description, service, resource_type, quoting_mode, priority=None):
        self.key = key
        self.name = name
        self.description = description
        self.service = service
        if type not in self.RESOURCE_TYPES:
            raise AttributeError('Unsupportable resource type {}. Supported types: {}'.format(type, self.RESOURCE_TYPES))
        self.type = resource_type
        if quoting_mode not in self.QUOTING_MODES:
            raise AttributeError('Unsupportable resource type {}. Supported types: {}'.format(type, self.RESOURCE_TYPES))
        self.quotingMode = quoting_mode
        self.priority = priority


class DispenserQuotaSpec(object):
    """
    Class for describe quotas spec
    """
    pass


class DispenserAPI(object):
    """
    Class for Dispenser API requests.
    Documentation: https://wiki.yandex-team.ru/dispenser/api/
    """
    API_HOST_TESTING = 'https://dispenser-test.yandex-team.ru/'
    API_HOST = 'https://dispenser.yandex-team.ru/'
    API_PATH = '/api/v1'

    def __init__(self, service, auth_token=None, cluster_type='testing', cluster_key='common'):
        # Basic parameters
        self._service_name = service
        self._cluster_type = cluster_type
        self._cluster_key = cluster_key

        # Auth
        if auth_token:
            self._oauth_token = auth_token
        else:
            self._oauth_token = PersistentTokenStore.get_token_from_store_env_or_file('dispenser')

        # API url
        self._api_url = (self.API_HOST_TESTING if 'testing' in cluster_type else self.API_HOST) + cluster_key + self.API_PATH

        # Create connection
        self._connection = requests.session()
        self._connection.headers = {
            'Content-Type': 'application/json',
            'Authorization': 'OAuth %s' % self._oauth_token
        }

    # Project API methods
    @request
    def get_projects(self, project=None, leaf=False, member=None, responsible=None, show_persons=False):
        """
        Get projects by query params.
        :param project: type str or list
        :param leaf: type boolean
        :param member: type srt or list
        :param responsible:  type srt or list
        :param show_persons:  type boolean
        :return: type requests.model.Response
        """
        request_url = self._api_url + '/projects'
        request_query_params = {}
        if project:
            request_query_params['project'] = project
        if leaf:
            request_query_params['leaf'] = leaf
        if member:
            request_query_params['member'] = member
        if responsible:
            request_query_params['responsible'] = responsible
        if show_persons:
            request_query_params['showPersons'] = show_persons
        return self._connection.get(request_url, json=request_query_params)

    @request
    def get_project(self, project):
        """
        Get project information by project name.
        :param project: type str
        :return: type requests.model.Response
        """
        request_url = self._api_url + '/projects/' + project
        return self._connection.get(request_url)

    # Services API methods
    @request
    def get_service(self, all_services=False):
        """
        Get service by service name. Or get all services.
        :param all_services type boolean
        :return: type requests.model.Response
        """
        request_url = self._api_url + '/services/' + ('all' if all_services else self._service_name)
        return self._connection.get(request_url)

    @request
    def add_service_admin(self, admin):
        """
        Method for add service administrators
        :param admin: type str or list
        :return: type requests.model.Response
        """
        request_url = self._api_url + '/services/' + self._service_name + '/attach-admins'
        request_query_params = [admin] if type(admin) is str else admin
        return self._connection.post(request_url, json=request_query_params)

    @request
    def remove_service_admin(self, admin):
        """
        Method for add service administrators
        :param admin: type str or list
        :return: type requests.model.Response
        """
        request_url = self._api_url + '/services/' + self._service_name + '/detach-admins'
        request_query_params = [admin] if type(admin) is str else admin
        return self._connection.post(request_url, json=request_query_params)

    # Service resources API methods
    @request
    def get_service_resource(self, resource_key=None):
        """
        Method to get service resources. If
        :param resource_key:  type str or None
        :return: type requests.model.Response
        """
        request_url = self._api_url + '/services/' + self._service_name + '/resources/' + (resource_key if resource_key else '')
        return self._connection.get(request_url)

    @request
    def create_service_resource(self, resource_key, resource_name, resource_desc, resource_type, quoting_mode):
        """
        Method for create service resources. Based on object "service resources".
        :param resource_key:
        :param resource_name:
        :param resource_desc:
        :param resource_type:
        :param quoting_mode:
        :return: type requests.model.Response
        """
        request_url = self._api_url + '/services/' + self._service_name + '/resources/' + resource_key
        service_resource = {
            'name': resource_name,
            'description': resource_desc,
            'type': resource_type,
            'quotingMode': quoting_mode,
        }
        return self._connection.put(request_url, json=service_resource)

    @request
    def update_service_resource(self, resource_key, resource_name=None, resource_desc=None,
                                resource_type=None, quoting_mode=None):
        """
        Method for update service resources. Based on object "service resources".
        :param resource_key:
        :param resource_name:
        :param resource_desc:
        :param resource_type:
        :param quoting_mode:
        :return: type requests.model.Response
        """
        request_url = self._api_url + '/services/' + self._service_name + '/resources/' + resource_key
        service_resource_data = self.get_service_resource(resource_key=resource_key).json()
        if quoting_mode:
            service_resource_data['quotingMode'] = quoting_mode
        if resource_type:
            service_resource_data['type'] = resource_type
        if resource_name:
            service_resource_data['name'] = resource_name
        if resource_desc:
            service_resource_data['description'] = resource_desc
        return self._connection.post(request_url, json=service_resource_data)

    @request
    def remove_service_resource(self, resource_key):
        """
        Method for remove service resources.
        :param resource_key: type str
        :return: type requests.model.Response
        """
        request_url = self._api_url + '/services/' + self._service_name + '/resources/' + resource_key
        return self._connection.delete(request_url)

    # Resource segmentation API methods
    @request
    def get_resource_segmentations(self, resource_key):
        """
        Method for get resource segmentations.
        :param resource_key: type str
        :return: type requests.model.Response
        """
        request_url = self._api_url + '/services/' + self._service_name + '/resources/' + resource_key + '/segmentations'
        return self._connection.get(request_url)

    @request
    def set_resource_segmentations(self, resource_key, segmentations):
        """
        Method for get resource segmentations. Require permissions from Dispenser.
        :param resource_key: type str
        :param segmentations: type dict
        :return: type requests.model.Response
        """
        request_url = self._api_url + '/services/' + self._service_name + '/resources/' + resource_key + '/segmentations'
        return self._connection.put(request_url, json=segmentations)

    # Service quota specification API methods
    @request
    def get_quota_spec(self, resource_key, quota_spec_key):
        """
        Method for get quota specification.
        :param resource_key: type syt
        :param quota_spec_key: type str
        :return: type requests.model.Response
        """
        request_url = self._api_url + '/quota-specifications/' + self._service_name + '/' + resource_key + '/' + quota_spec_key
        return self._connection.get(request_url)

    @request
    def create_quota_spec(self, resource_key, quota_spec_key, spec_description, spec_type='ABSOLUTE'):
        """
        Method for create quota specification.
        :param resource_key: type syt
        :param quota_spec_key: type str
        :param spec_description: type str
        :param spec_type:  type str in ['ABSOLUTE' ,'PERCENT'[
        :return: type requests.model.Response
        """
        request_url = self._api_url + '/quota-specifications/' + self._service_name + '/' + resource_key + '/' + quota_spec_key
        quota_spec = {
            'type': spec_type,
            'description': spec_description,
        }
        return self._connection.put(request_url, json=quota_spec)

    @request
    def update_quota_spec(self, resource_key, quota_spec_key, spec_description='', spec_type='ABSOLUTE'):
        """
        Method for update quota specification.
        :param resource_key: type syt
        :param quota_spec_key: type str
        :param spec_description: type str
        :param spec_type:  type str in ['ABSOLUTE' ,'PERCENT'[
        :return: type requests.model.Response
        """
        request_url = self._api_url + '/quota-specifications/' + self._service_name + '/' + resource_key + '/' + quota_spec_key
        quota_spec = self.get_quota_spec(resource_key, quota_spec_key)
        if spec_type:
            quota_spec['type'] = spec_type
        if spec_description:
            quota_spec['description'] = spec_description
        return self._connection.post(request_url, json=quota_spec)

    @request
    def remove_quota_spec(self, resource_key, quota_spec_key):
        """
        Method for remove quota specification.
        :param resource_key: type syt
        :param quota_spec_key: type str
        :return: type requests.model.Response
        """
        request_url = self._api_url + '/quota-specifications/' + self._service_name + '/' + resource_key + '/' + quota_spec_key
        return self._connection.delete(request_url)

    # Service quota API methods
    @request
    def get_qoutas(self, project=None, leaf=False, member=None, resource=None, entity_spec=None, order='ABS'):
        """
        Get projects by query params.
        :param project: type str or list
        :param leaf: type boolean
        :param member: type srt or list
        :param resource:  type srt or list
        :param entity_spec:  type str
        :param order: type str in ['ABS', 'DESC']
        :return: type requests.model.Response
        """
        request_url = self._api_url + '/quotas'
        request_query_params = {}
        if project:
            request_query_params['project'] = project
        if leaf:
            request_query_params['leaf'] = leaf
        if member:
            request_query_params['member'] = member
        if resource:
            request_query_params['resource'] = resource
        if entity_spec:
            request_query_params['entitySpec'] = entity_spec
        if order:
            request_query_params['order'] = order
        return self._connection.get(request_url, json=request_query_params)

    @request
    def update_quotas(self, project_name, resource_name, quota_spec, quota_max_value, quota_unit, segments):
        """
        Update maximal amount of quota.
        :param project_name:  type str
        :param resource_name:  type str
        :param quota_spec:  type str
        :param quota_max_value:  type int
        :param quota_unit:  type str
        :param segments: type str or [str]
        :return: type requests.model.Response
        """
        request_url = self._api_url + '/quotas/' + project_name + '/' + resource_name + '/' + quota_spec
        request_query_params = {
            'maxValue': quota_max_value,
            'unit': quota_unit,
            'segments': segments
        }
        return self._connection.post(request_url, json=request_query_params)

    # Entity specification API methods
    @request
    def get_entity_specs(self, service_name=None):
        """
        Get entity specifications for service.
        :return: type requests.model.Response
        """
        request_url = self._api_url + '/entity-specifications'
        request_query_params = {
            'service': service_name if service_name else self._service_name
        }
        return self._connection.get(request_url, json=request_query_params)

    @request
    def get_entity_spec(self, entity_spec, service_name=None):
        """
        Get entity specification for service.
        :return: type requests.model.Response
        """
        service_name = service_name if service_name else self._service_name
        request_url = self._api_url + '/entity-specifications/' + service_name + '/' + entity_spec
        return self._connection.get(request_url)

    # Entity API methods
    @request
    def get_entities(self, entity_spec, service_name=None, limit=None, unused=False):
        """
        Get entities by entity params.
        :param entity_spec: type str
        :param service_name: type str
        :param limit: type int
        :param unused: type boolean
        :return: type requests.model.Response
        """
        request_url = self._api_url + '/entities'
        request_query_params = {
            'service': service_name if service_name else self._service_name,
            'limit': limit if limit else '',
            'entitySpec': '/' + self._service_name + '/' + entity_spec,
            'trash': unused
        }
        return self._connection.get(request_url, json=request_query_params)

    @request
    def get_entity(self, entity_spec, entity_name, service_name=None):
        """
        Get entity.
        :param entity_spec: type str
        :param entity_name: type str
        :param service_name: type str
        :return: type requests.model.Response
        """
        service_name = service_name if service_name else self._service_name
        request_url = self._api_url + '/entities/' + service_name + '/' + entity_spec + '/' + entity_name
        return self._connection.get(request_url)

    # Change quotas API method
    @request
    def change_quotas_batch(self, operations, mode='ROLLBACK_ON_ERROR'):
        """
        Batch acquire/release interface quotas/
        :param mode: type str
        :param operations: type list of dict
        :return: type requests.model.Response
        """
        if mode not in ['IGNORE_UNKNOWN_ENTITIES_AND_USAGES', 'ROLLBACK_ON_ERROR']:
            raise AttributeError('Unsupported mode')
        ops = []
        if type(operations) is dict:
            ops.append({
                'id': 1,
                'operation': operations,
            })
        elif type(operations) is list:
            operation_id = 1
            for operation in operations:
                op = {
                    'id': operation_id,
                    'operation': operation,
                }
                ops.append(op)
                operation_id += 1
        request_url = self._api_url + '/change-quotas?reqId=uniqId'
        request_query_params = {
            'mode': mode,
            'serviceKey': self._service_name,
            'operations': ops
        }
        return self._connection.post(request_url, json=request_query_params)

    @staticmethod
    def prepare_operation(operation_type, entity_data, login, project_name):
        """
        Prepare operation method.
        :param operation_type: type str in ['CREATE_ENTITY', 'RELEASE_ENTITY']
        :param entity_data: type dict
        :param login:
        :param project_name:
        :return:
        """
        if operation_type not in ['CREATE_ENTITY', 'RELEASE_ENTITY']:
            raise AttributeError('Unsupported operation type')
        opedation_data = {
            'type': operation_type,
            'action': entity_data,
            'performer': {
                'login': login,
                'projectKey': project_name
            }
        }
        return opedation_data

    @staticmethod
    def prepare_entity_reference(entity_name, entity_spec):
        """
        Prepare entity reference method
        :param entity_name: type str
        :param entity_spec: type str
        :return: type dict
        """
        entity_ref = {
            'key': entity_name,
            'specificationKey': entity_spec
        }
        return entity_ref

    @staticmethod
    def prepare_entity_data(entity_name, entity_spec, dimensions):
        """
        Prepare entity data method
        :param entity_name: type str
        :param entity_spec: type str
        :param dimensions:  type list of resource
        :return: type dict
        """
        entity_data = {
            'key': entity_name,
            'specificationKey': entity_spec,
            'dimensions': dimensions
        }
        return entity_data

    @staticmethod
    def prepare_dimension(resource_name, resource_value, resource_unit, locations):
        """
        Prepare resource dimension
        :param resource_name: type str
        :param resource_value:  type str
        :param resource_unit: type str
        :param locations: type list
        :return: return type dict
        """
        dimension_data = {
            'resourceKey': resource_name,
            'amount': {
                'value': resource_value,
                'unit': resource_unit
            },
            'segments': locations
        }
        return dimension_data


class SaaSDispenserAPI(DispenserAPI):
    """
    SaaS Dispenser interface
    """

    def __init__(self, service, auth_token=None, cluster_type='testing', cluster_key='common'):
        super(SaaSDispenserAPI, self).__init__(service, auth_token, cluster_type, cluster_key)
        self.projects_data = self.get_projects().json()['result']

    def get_project_name_by_abc(self, abc_id):
        """
        Get project name by ABC service id
        :param abc_id: type int
        :return: type str
        """
        for project in self.projects_data:
            if int(abc_id) == project['abcServiceId']:
                return project['key']

    def create_service(self, service_name, service_ctype, instances_count, memory, cpu, ssd_storage,
                       hdd_storage, service_owner, project_name, locations=['MAN', 'SAS', 'VLA']):
        """
        Register SaaS service in Dispenser
        :param service_name: type str
        :param service_ctype: type str
        :param instances_count: type int
        :param memory: type int (Gb)
        :param cpu: type int (Cores)
        :param ssd_storage: type int (Gb)
        :param hdd_storage: type int (Gb)
        :param service_owner: type str
        :param project_name: type str
        :param locations: type list
        :return: type requests.model.Response
        """
        memory_total = memory * instances_count * len(locations)
        cpu_total = cpu * instances_count * len(locations)
        ssd_total = ssd_storage * instances_count * len(locations)
        hdd_total = hdd_storage * instances_count * len(locations)
        dimensions = []
        for location in locations:
            dimensions.append(self.prepare_dimension('ram', memory_total, 'GIBIBYTE', [location]))
            dimensions.append(self.prepare_dimension('cpu', cpu_total, 'CORES', [location]))
            dimensions.append(self.prepare_dimension('hdd', hdd_total, 'GIBIBYTE', [location]))
            dimensions.append(self.prepare_dimension('ssd', ssd_total, 'GIBIBYTE', [location]))

        entity_data = self.prepare_entity_data(service_name, service_ctype, dimensions)
        operation = self.prepare_operation('CREATE_ENTITY', entity_data, service_owner, project_name)
        result = self.change_quotas_batch(operation)
        return result

    def update_service(self, service_name, service_ctype, instances_count, memory, cpu, ssd_storage,
                       hdd_storage, service_owner, project_name, locations=['MAN', 'SAS', 'VLA']):
        """
        Modify SaaS service in Dispenser
        :param service_name: type str
        :param service_ctype: type str
        :param instances_count: type int
        :param memory: type int (Gb)
        :param cpu: type int (Cores)
        :param ssd_storage: type int (Gb)
        :param hdd_storage: type int (Gb)
        :param service_owner: type str
        :param project_name: type str
        :param locations: type list
        :return: type requests.model.Response
        """
        self.remove_service(service_name, service_ctype,  service_owner, project_name)
        self.create_service(service_name, service_ctype, instances_count, memory, cpu,
                            ssd_storage, hdd_storage, service_owner, project_name, locations=locations)

    def remove_service(self, service_name, service_ctype, service_owner, project_name):
        """
        Remove SaaS service from Dispenser.
        :param service_name: type str
        :param service_ctype: type str
        :param service_owner: type str
        :param project_name: type str
        :return: type requests.model.Response
        """
        entity_ref = self.prepare_entity_reference(service_name, service_ctype)
        operation = self.prepare_operation('RELEASE_ENTITY', entity_ref, service_owner, project_name)
        result = self.change_quotas_batch(operation)
        return result
