# coding: utf-8

"""
Simple client for Deploy Manager
"""

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

import binascii
import logging
import re
import requests
import simplejson
import six
import time

from six.moves.urllib.parse import urljoin

from saas.library.python.saas_slot import Slot
from saas.library.python.sane_requests import get_session

from .errors import DeployManagerApiError, NotEnoughSlots


class DeployManagerApiClient(object):
    LOGGER = logging.getLogger(__name__)
    NANNY_SERVICE_TAG_REGEXP = re.compile(r'(?P<name>\w+)@SAAS@')

    def __init__(self, dm_url='http://saas-dm.yandex.net', **kwargs):
        self.base_url = dm_url
        self._ctypes = []
        self.session = get_session(**kwargs)

    def get_slots_nanny_services(self, ctype, service):
        nanny_services = set()
        slots_by_interval = self.get_slots_by_interval(ctype, service, filter='result.slot.properties.NANNY_SERVICE_ID')
        for shard in slots_by_interval:
            for slot in shard['slots']:
                nanny_service = slot.get('result.slot.properties.NANNY_SERVICE_ID', None)
                if nanny_service:
                    nanny_services.add(nanny_service)
        return set(nanny_services)

    @property
    def ctypes(self):
        if not self._ctypes:
            self._ctypes = [ctype for ctype in self._get_dm_url_json(url='get_services').keys() if ctype != 'unused']
        return self._ctypes

    @property
    def stable_ctypes(self):
        return [ctype for ctype in self.ctypes if ctype.startswith('stable') and 'gemini' not in ctype]

    def get_proxy_endpoint_sets(self, ctype):
        return self.get_tags_info(ctype, 'searchproxy').get('endpointsets', [])

    def get_services(self, ctype, service_type='rtyserver'):
        from .saas_service import SaasService
        return [SaasService(ctype=ctype, service=s) for s in self._get_dm_url_json(url='get_services')[ctype][service_type].keys()]

    def get_sla(self, ctype, service):
        params = {'ctype': ctype, 'service': service}
        return self._get_dm_url_json(url='process_sla_description', params=params)

    def get_tags_info(self, ctype, service):
        params = {
            'ctype': ctype,
            'service': service
        }
        return self._get_dm_url_json(url='modify_tags_info', params=params)

    def modify_tags_info(self, ctype, service, params):
        params.update({
            'ctype': ctype,
            'service': service,
        })
        self.LOGGER.debug('Modify tags info with params %s', params)
        return self._get_dm_url_json(url='modify_tags_info', params=params, action='set')

    def check_configs(self, ctype, service):
        params = {
            'ctype': ctype,
            'service': service,
        }
        return self._get_dm_url_json(url='check_configs', params=params)

    def clear_cache(self, ctype, service):
        params = {
            'ctype': ctype,
            'service': service,
        }
        return self._get_dm_url_json(url='modify_tags_info', params=params, action='invalidate')

    def get_slots_by_interval(self, ctype, service, **params):
        base_params = {'ctype': ctype, 'service': service}
        params.update(base_params)
        return self._get_dm_api_url_json(url='slots_by_interval/', params=params)

    def slots_by_interval(self, ctype, service, **additional_params):
        result = {}
        for shard_info in self.get_slots_by_interval(ctype, service, **additional_params):
            slots = [
                Slot.from_id(
                    s['id'],
                    physical_host=s['$real_host$'],
                    geo=s['$datacenter$'],
                    shards_min=s['$shards_min$'],
                    shards_max=s['$shards_max$']
                )
                for s in shard_info['slots'] if not s['is_sd']
            ]
            result[shard_info['id']] = slots
        return result

    def get_cluster_map(self, ctype, service, service_type='rtyserver'):
        # type: (AnyStr, AnyStr, AnyStr) -> Dict[str, Dict[str, List[Slot]]]
        """
        :param ctype: saas ctype
        :param service: saas service
        :param service_type: you know what it is if you want
        :return: Dict, keys are shard ids, values are dicts with locations as keys and lists of Slots as values. ex: {}
        """
        result = {}
        params = {'ctype': ctype, 'service': service}
        cluster_map = self._get_dm_url_json(
            url='get_cluster_map', service_type=service_type, params=params
        )['cluster'][service]['config_types']['default']['sources']
        for shard, replicas in six.iteritems(cluster_map):
            result[shard] = {}
            for location, slots in six.iteritems(replicas):
                result[shard][location] = [
                    Slot.from_id(
                        slot, geo=location, shards_min=prop['shards_min'], shards_max=prop['shards_max']
                    ) for slot, prop in six.iteritems(slots)
                ]
        return result

    def search_map(self, ctype, service, **additional_params):
        result = {}
        for shard_info in self.get_slots_by_interval(ctype, service, **additional_params):
            slots_info = {
                Slot.from_id(
                    s['id'],
                    physical_host=s['$real_host$'],
                    geo=s['$datacenter$'],
                    shards_min=s['$shards_min$'],
                    shards_max=s['$shards_max$']
                ): {'disable_search': s['disable_search'], 'disable_indexing': s['disable_indexing']}
                for s in shard_info['slots'] if not s['is_sd']
            }
            result[shard_info['id']] = slots_info
        return result

    def get_per_dc_search(self, ctype, service):
        params = {'ctype': ctype, 'service': service}
        return self._get_dm_url_json(url='get_cluster_map', params=params)['cluster'][service].get('per_dc_search', False)

    def get_free_slots(self, ctype, service):
        """
        :param ctype:
        :param service:
        :return: List of slots
        :rtype: List[Slot]
        """
        result = []
        params = {'ctype': ctype, 'service': service}
        raw_slots_info = self._get_dm_url_json(url='free_slots', params=params)[ctype]
        for location, geo_slots_hosts in six.iteritems(raw_slots_info):
            for _, raw_slots in six.iteritems(geo_slots_hosts):
                result.extend([
                    Slot(
                        host=slot_info['host'],
                        port=slot_info['port'],
                        geo=slot_info['dc']
                    ) for slot_id, slot_info in six.iteritems(raw_slots)
                ])
        return result

    def get_free_slots_in_geo(self, ctype, service, geo):
        """
        :param ctype: saas ctype
        :param service: saas service name
        :param geo: geo code ex: SAS, MAN, VLA
        :return: List of slots
        :rtype: List[Slot]
        """
        geo = geo.upper()
        params = {'ctype': ctype, 'service': service}
        raw_slots_info = self._get_dm_url_json(url='free_slots', params=params)[ctype].get(geo, {})
        result = []
        for _, raw_slots in six.iteritems(raw_slots_info):
            result.extend([
                Slot(
                    host=slot_info['host'],
                    port=slot_info['port'],
                    geo=slot_info['dc']
                ) for slot_id, slot_info in six.iteritems(raw_slots)
            ])
        return result

    def allocate_same_slots(self, ctype, service, geo, intervals, new_slots_pool=None):
        """
        :param ctype: saas ctype "stable, stable_kv, etc...
        :param service: saas service name
        :param geo: saas geo location
        :param intervals: List of intervals, ex: [{'min':48048,'max':50231},{'min':50232,'max':52415}]
        :type ctype: AnyStr
        :type service: AnyStr
        :type geo: AnyStr
        :type new_slots_pool: List[Slot]
        """
        geo = geo.upper()
        result = []
        new_slots_pool = new_slots_pool if new_slots_pool else self.get_free_slots_in_geo(ctype, service, geo)
        if len(new_slots_pool) < len(intervals):
            self.LOGGER.error('Got only %d slots as replacement for %d slots', len(new_slots_pool), len(intervals))
            raise NotEnoughSlots()
        params = {
            'ctype': ctype,
            'service': service,
            'intervals': simplejson.dumps({geo: intervals}, separators=(',', ':')),
            'new_slots_pool': ','.join([s.id for s in new_slots_pool])
        }
        allocated_slots = self._get_dm_url_json(url='allocate_same_slots', params=params)
        self.LOGGER.error('added_slots = %s', allocated_slots)
        new_slots_pool_dict = {s.id: s for s in new_slots_pool}
        for s in allocated_slots:
            added_slot = new_slots_pool_dict['{}:{}'.format(s['host'], s['search_port'])]  # type: Slot
            added_slot.interval = {'min': s.get('shard_min', None), 'max': s.get('shard_max', None)}
            result.append(added_slot)
        return result

    def release_slots(self, ctype, service, slots):
        params = {
            'ctype': ctype,
            'service': service,
            'slots': ','.join([s.id for s in slots]),
        }
        return self._get_dm_url_raw(url='release_slots', params=params)

    def restore_replica(self, saas_service, slots, deploy_proxies=False):
        """
        :param saas_service:  Saas service
        :param slots: Slots to restore
        :param deploy_proxies: Only if you know what are you doing
        :type saas_service: saas.tools.devops.lib23.saas_service.SaasService
        :type slots: List[Slot]
        :type deploy_proxies: bool
        :return: DM task ID
        :rtype: str
        """
        assert len(slots) > 0
        params = {
            'ctype': saas_service.ctype,
            'service': saas_service.name,
            'deploy_proxies': 1 if deploy_proxies is True or deploy_proxies == 1 else 0,
            'restore_slots': ','.join([str(s) for s in slots])
        }
        dm_response = self._get_dm_url_raw(url='cluster_control', params=params)
        if dm_response.ok:
            return dm_response.text.strip()
        else:
            self.LOGGER.error('Cluster control task for service {}:{} creation failed'.format(saas_service.ctype, saas_service.name))
            dm_response.raise_for_status()

    def get_slots_and_stats(self, ctype, service, **kwargs):
        """
        :param ctype: Service ctype: stable, prestable, stable_kv, ...
        :param service: Service name
        :param filter: See dm documentation
        :param groupings: See dm documentation
        :param slots_filters: See dm documentation
        :rtype: dict
        """
        allowed_query_params = {'filter', 'groupings', 'slots_filters'}
        params = {'ctype': ctype, 'service': service}
        for k, v in six.iteritems(kwargs):
            if k in allowed_query_params:
                params[k] = v
            else:
                self.LOGGER.error('Unknown query param "%s" for slots_and_stat DM api method', k)
        return self._get_dm_api_url_json('slots_and_stat/', params=params, service_type='rtyserver')

    def get_deploy_info(self, deploy_id):
        params = {'id': deploy_id}
        return self._get_dm_url_json(url='deploy_info', params=params)

    def get_storage_file(self, path):
        params = {
            'path': path,
            'download': 1
        }
        return self._get_dm_url_raw(url='process_storage', params=params).content

    def wait_for_dm_task(self, task_id):
        sleep_delay = 0
        task_status = self.get_deploy_info(task_id)
        self.LOGGER.debug('Deploy task status: %s', task_status)
        while not task_status['is_finished']:
            self.LOGGER.debug('Deploy task status: %s', task_status)
            sleep_delay = min(sleep_delay + 1, 60)
            time.sleep(sleep_delay)
            task_status = self.get_deploy_info(task_id)

        if task_status['result_code'] == 'FINISHED':
            return True
        else:
            self.LOGGER.error('DM task %s failed', task_id)
            return False

    def _create_deploy_task(self, service, ctype, service_type, only_save=False, degrade_level=None):
        degrade_level = degrade_level or 0.01
        params = {
            'ctype': ctype,
            'service': service,
            'may_be_dead_procentage': degrade_level,
        }

        if only_save:
            params['slots'] = '[]'

        result = self._get_dm_url_raw('deploy', service_type=service_type, params=params)
        return result.text.strip()

    def _create_proxy_deploy_task(self, ctype, proxy_type, only_save=False, degrade_level=None):
        return self._create_deploy_task(
            proxy_type, ctype, proxy_type,
            only_save=only_save,
            degrade_level=degrade_level
        )

    def modify_searchmap(self, ctype, service, slots, action, deploy_proxies=False, deploy_backends=False):
        params = {
            'ctype': ctype,
            'service': service,
            'deploy_proxies': 1 if deploy_proxies else 0,
            'deploy': 1 if deploy_backends else 0,
            'slots_vector': ','.join([s.id for s in slots])
        }
        return self._get_dm_url_raw(url='modify_searchmap', params=params, action=action)

    def add_endpoint_sets(self, ctype, service, shards_endpoint_sets):
        processed_endpoint_sets = []
        for shard, endpoint_sets in six.iteritems(shards_endpoint_sets):
            processed_endpoint_sets.extend(['{}:{}'.format(str(endpoint_set), shard) for endpoint_set in endpoint_sets])
        if not processed_endpoint_sets:
            return "{}"
        params = {
            'ctype': ctype,
            'service': service,
            'eps': ','.join(processed_endpoint_sets)
        }
        try:
            return self._get_dm_url_json(url='add_endpointsets', params=params, action=None, service_type=None)
        except requests.HTTPError as e:
            self.LOGGER.error('Endpointset addition failed. Error: %s; DM Response: %s', e, e.response.text)
            return None

    def deploy_proxy(self, ctype, proxy_type, only_save=False, degrade_level=None):

        assert proxy_type in {'searchproxy', 'indexerproxy', }, 'Proxy type must be either searchproxy or indexerproxy, got {}'.format(proxy_type)

        deploy_proxy_task = self._create_proxy_deploy_task(
            ctype, proxy_type,
            only_save=only_save,
            degrade_level=degrade_level
        )

        if not self.wait_for_dm_task(deploy_proxy_task):
            self.LOGGER.error('Deploy %s incomplete', proxy_type)
            return False
        else:
            return True

    def deploy_indexerproxy(self, ctype, degrade_level=None):
        return self.deploy_proxy(ctype, 'indexerproxy', degrade_level=degrade_level)

    def deploy_searchproxy(self, ctype, degrade_level=None):
        return self.deploy_proxy(ctype, 'searchproxy', degrade_level=degrade_level)

    def deploy_rtyserver(self, service, ctype, only_save=False, degrade_level=None):
        deploy_task = self._create_deploy_task(
            service, ctype, 'rtyserver',
            only_save=only_save,
            degrade_level=degrade_level
        )
        return self.wait_for_dm_task(deploy_task)

    def deploy_proxies(self, ctype, searchproxy_degrade_level=0.01, indexerproxy_degrade_level=0.15):
        return self.deploy_indexerproxy(ctype, indexerproxy_degrade_level), self.deploy_searchproxy(ctype, searchproxy_degrade_level)

    def is_deploy_blocked(self, service, ctype):
        params = {
            'ctype': ctype,
            'service': service,
        }
        url = urljoin(self.base_url, 'deploy_block')
        result = self._get_dm_url_json(url, params)
        return result['blocked']

    def _get_dm_url_raw(self, url, params=None, action='get', service_type='rtyserver', raise_on_error=True):
        # type: (AnyStr, MutableMapping[AnyStr, Any], AnyStr, AnyStr, bool) -> requests.Response
        """
        :param action: action for dm (get, set)
        :param service_type: rtyserver, searchproxy, indexerproxy
        :type url: AnyStr
        :type params: Mutab[str, Any]
        :rtype: requests.Response
        """
        request_url = urljoin(self.base_url, url)
        if params:
            params['action'] = action
            params['service_type'] = service_type
            params = {k: v for k, v in six.iteritems(params)}
        self.LOGGER.debug('URL: %s, PARAMS: %s', request_url, params)
        dm_response = self.session.get(request_url, params=params)
        if raise_on_error:
            dm_response.raise_for_status()
        return dm_response

    def _get_dm_url_json(self, url, params=None, action='get', service_type='rtyserver', raise_on_error=True):
        resp = self._get_dm_url_raw(url, params=params, action=action, service_type=service_type, raise_on_error=raise_on_error)
        try:
            return resp.json()
        except simplejson.JSONDecodeError as e:
            self.LOGGER.fatal('DM response "%s" can\'t be decoded as JSON, response status code was: %s', e.doc, resp.status_code)
            raise DeployManagerApiError(e)

    def _get_dm_url_text(self, url, params=None, action='get', service_type='rtyserver', raise_on_error=True):
        return self._get_dm_url_raw(url, params=params, action=action, service_type=service_type, raise_on_error=raise_on_error).text

    def _get_dm_api_url_raw(self, url, params=None, service_type=None, raise_on_error=True):
        """
        :param url: part of dm url after /api
        :param service_type: rtyserver, searchproxy, indexerproxy
        :type url: AnyStr
        :type params: Mapping[str, Any]
        :rtype: requests.Response
        """
        dm_base_url = urljoin(self.base_url, 'api/')
        if service_type:
            stripped_url = url.rstrip('/')
            url = '{}/:{}'.format(stripped_url, service_type)
        request_url = urljoin(dm_base_url, url)
        self.LOGGER.debug('URL: %s, PARAMS: %s', request_url, params)
        dm_response = self.session.get(request_url, params=params)
        if raise_on_error:
            dm_response.raise_for_status()
        return dm_response

    def _get_dm_api_url_json(self, url, params=None, service_type=None, raise_on_error=True):
        resp = self._get_dm_api_url_raw(url, params=params, service_type=service_type, raise_on_error=raise_on_error)
        try:
            return resp.json()
        except simplejson.JSONDecodeError as e:
            self.LOGGER.fatal('DM response "%s" can\'t be decoded as JSON, response status code was: %s', e.doc, resp.status_code)
            raise DeployManagerApiError(e)

    def _get_dm_api_url_text(self, url, params=None, service_type=None, raise_on_error=True):
        return self._get_dm_api_url_raw(url, params=params, service_type=service_type, raise_on_error=raise_on_error).text

    def get_config_content(self, config_name, service_name, service_ctype, service_type='rtyserver'):
        params = {
            'ctype': service_ctype,
            'service': service_name,
            'filename': config_name
        }
        return self._get_dm_url_raw('get_conf', params=params, service_type=service_type).content

    def set_config_content(self, config_name, service_name, content):
        params = {
            'hex': 'yes',
            'filename': config_name,
            'service': service_name
        }

        url = urljoin(self.base_url, 'set_conf')
        encoded_data = binascii.hexlify(content)

        response = self.session.post(url, params=params, data=encoded_data)

        try:  # TODO(vbushev): requests handling should be moved to some common function
            return response.json()
        except simplejson.JSONDecodeError as e:
            self.LOGGER.fatal('DM response "%s" can\'t be decoded as JSON, response status code was: %s',
                              e.doc, response.status_code)
            raise DeployManagerApiError(e)

    def get_configs(self, service_name, service_ctype, service_type='rtyserver'):
        params = {
            'ctype': service_ctype,
            'service': service_name
        }
        response_data = self._get_dm_url_json('list_conf', params=params, service_type=service_type)
        return response_data.get('files', [])
