#!/usr/bin/env python
# -*- coding: utf-8 -*-

import requests
import logging
import json
import jinja2
import os
import time
import datetime
import socket
import binascii

import saas.tools.ssm.modules.dm_tvm as dm_tvm

from retrying import retry
from six.moves.urllib.parse import urljoin
from saas.tools.devops.lib23.saas_service import SaasService
from saas.library.python.token_store import PersistentTokenStore


_retryable_mark = '_saas_retryable_request'


def retryable(func):
    setattr(func, _retryable_mark, 1)
    return func


def request(func):
    """
    Handle requests information.
    """
    def decorator(*args, **kwargs):
        resp = func(*args, **kwargs)
        message = 'Status code: %d | URL: %s ' % (resp.status_code, resp.url)
        if resp.ok:
            logging.debug('Success request | %s', message)
        else:
            logging.debug('Failed request | %s | Reason: %s | Response data: %s', message, resp.reason, resp.text)
        return resp

    if getattr(func, _retryable_mark, None):
        decorator = retry(**{
            'stop_max_attempt_number': 5,
            'wait_random_min': 240,
            'wait_random_max': 980,
            'wrap_exception': False,
        })(decorator)

    return decorator


def update_history(func):
    """
    Check return func status and write his to history
    :param func:
    :return:
    """
    def decorator(*args, **kwargs):
        fail_result = u'[x]'
        success_result = u'[v]'
        action = func.__name__
        action_result = func(*args, **kwargs)
        action_args = args[1] if len(args[1:]) > 0 else ''
        action_kwargs = str(kwargs) if len(kwargs) > 0 else ''
        if action_result:
            args[0].actions_history.append(
                u'%-4s %-24s %s %s' % (success_result, action, action_args, action_kwargs))
        else:
            args[0].actions_history.append(
                u'%-4s %-24s %s %s' % (fail_result, action, action_args, action_kwargs))
        return action_result
    return decorator


def check_status(func):
    """
    Handle for checking dm tasks and waiting his for finish
    :param func:
    :return:
    """
    def decorator(*args, **kwargs):
        resp = func(*args, **kwargs)
        timeout = 600  # seconds
        time_step = 15
        if resp.ok:
            task_id = resp.text.strip()
            task_type = task_id.split(';')[0].split('=')[1]

            service_name = args[0]._DeployManager__service_name
            logging.info('[%s] Waiting for finish task %s', service_name, task_id)

            while timeout >= time_step:
                deploy_info = args[0]._get_deploy_info(task_id).json()
                if deploy_info['is_finished']:
                    logging.debug('[%s][%s][%s] Finished task: %s', service_name, task_type, deploy_info['result_code'], task_id)
                    break
                else:
                    logging.debug('[%s][%s][%s] Running task: %s', service_name, task_type, deploy_info['result_code'], task_id)
                time.sleep(time_step)
            else:
                raise RuntimeError('Task {} is FAILED by timeout. Please check.'.format(task_id))
        return resp
    return decorator


def get_slots(slots_data):
    """
    Parse data with specific json format and return list of host:port
    :param slots_data: type json
    :return: type list
    """
    slots = []
    for dc in slots_data:
        for host in slots_data[dc]:
            for instance in slots_data[dc][host]:
                slots.append('%s:%d' % (slots_data[dc][host][instance]['host'],
                                        slots_data[dc][host][instance]['port']
                                        )
                             )
    return slots


class DeployManagerAPI(object):
    """
    Main class for DeployManager api requests.
    Supports ctypes: prestable, stable
    """
    __DM_HOST_PROXY = 'http://saas-dm-proxy.n.yandex-team.ru/'
    __DM_HOST_STABLE = 'http://saas-dm.yandex.net'
    __DM_HOST_PRESTABLE = 'http://saas-dm-test.yandex.net'
    __DEFAULT_TYPE = 'rtyserver'
    __DM_VIEWER_URL = 'http://saas-dm.n.yandex-team.ru/DM/index.php'

    __DM_INDEXERPROXY_TESTING = 'http://saas-indexerproxy-testing.yandex.net'

    __DM_SEARCHPROXY_STABLE = 'http://saas-searchproxy.yandex.net:17000'
    __DM_SEARCHPROXY_STABLE_KV = 'http://saas-searchproxy-kv.yandex.net:17000'
    __DM_SEARCHPROXY_STABLE_MIDDLE_KV = 'http://saas-searchproxy-middle-kv.yandex.net:17000'
    __DM_SEARCHPROXY_PRESTABLE = 'http://saas-searchproxy-prestable.yandex.net:17000'
    __DM_SEARCHPROXY_TESTING = 'https://saas-searchproxy-testing.yandex.net'

    def __init__(self, service_name, ctype, stype="", use_proxy=False):
        # Basic variables
        self.__service_name = service_name
        self.__service_ctype = ctype

        # Service settings
        if use_proxy:
            self._DM_HOST = self.__DM_HOST_PROXY

        if self.__service_ctype in ['prestable', 'testing']:
            if not use_proxy:
                self._DM_HOST = self.__DM_HOST_PRESTABLE
            if self.__service_ctype in 'prestable':
                self.__DM_SEARCHPROXY = self.__DM_SEARCHPROXY_PRESTABLE
                self.__DM_INDEXERPROXY = ''
            else:
                self.__DM_INDEXERPROXY = self.__DM_INDEXERPROXY_TESTING
                self.__DM_SEARCHPROXY = self.__DM_SEARCHPROXY_TESTING
        else:
            if not use_proxy:
                self._DM_HOST = self.__DM_HOST_STABLE
            if 'stable_middle_kv' in self.__service_ctype:
                self.__DM_SEARCHPROXY = self.__DM_SEARCHPROXY_STABLE_MIDDLE_KV
            elif 'stable_kv' in self.__service_ctype:
                self.__DM_SEARCHPROXY = self.__DM_SEARCHPROXY_STABLE_KV
            elif 'stable' in self.__service_ctype:
                self.__DM_SEARCHPROXY = self.__DM_SEARCHPROXY_STABLE
            else:
                self.__DM_SEARCHPROXY = ''
            self.__DM_INDEXERPROXY = ''

        if not stype:
            self.__type = self.__DEFAULT_TYPE
        else:
            self.__type = stype

        # Service connection settings
        self._connection = requests.session()

        self.__basic_request_uri = {
            'service': self.__service_name,
            'ctype': self.__service_ctype,
            'service_type': self.__type
        }
        self.actions_history = []
        self.__keys = ''

    def __str__(self):
        if self.__keys:
            keys = '[KEYS]:\n%s' % '\n'.join('%s: %s' % (key, value) for key, value in
                                             self.__keys.items())
        else:
            keys = ''
        return('[SERVICE]: %s\n\n[CTYPE]: %s\n\n[ACTIONS]: \n%s\n\n%s\n\n[PROXY]:\n%s\n%s'
               % (self.__service_name, self.__service_ctype,
                  '\n'.join(self.actions_history), str(keys),
                  self.__DM_INDEXERPROXY, self.__DM_SEARCHPROXY))

    def get_tags_info(self):
        resp = self._update_tags_info(action='get')
        return resp.json() if resp.ok else {}

    def get_slots_nanny_services(self):
        nanny_services = set()
        slots_info = self._get_slots_and_stats(slot_filter='result.slot.properties.NANNY_SERVICE_ID').json()['slots']
        for slot in slots_info:
            nanny_service = slot.get('result.slot.properties.NANNY_SERVICE_ID', None)
            if nanny_service:
                nanny_services.add(nanny_service)
        return nanny_services

    # Request methods
    @request
    def _add_service(self):
        """
        Adding a new service to Deploy Manager
        :return: type requests.model.Response
        """
        request_query_params = self.__basic_request_uri.copy()
        request_query_params['action'] = 'add_service'
        request_url = urljoin(self._DM_HOST, '/modify_searchmap')
        return self._connection.get(request_url, params=request_query_params)

    @request
    def _add_service_to_frontend(self):
        """
        Adding a new service to Deploy Manager frontend
        :return: type requests.model.Response
        """
        request_query_params = self.__basic_request_uri.copy()
        request_query_params['path'] = '/configs/{}'.format(self.__service_name)
        return self._connection.get(self.__DM_VIEWER_URL, params=request_query_params)

    @check_status
    @request
    def _add_replica(self, slots_count, replicas_count, slots_alloc):
        """
        Adding new replicas to Deploy Manager service
        :param slots_count: type int
        :param replicas_count: type int
        :param slots_alloc: type str or list
        :return: type requests.model.Response
        """
        request_query_params = self.__basic_request_uri.copy()
        request_query_params.update({
            'slots_count': str(slots_count),
            'replicas_count': str(replicas_count),
            'slots_allocator': json.dumps({'custom_slots': slots_alloc}),
            'deploy_proxy': 0,
            'slots': str([])
        })
        request_url = urljoin(self._DM_HOST, '/add_replica')
        return self._connection.get(request_url, params=request_query_params)

    @request
    def _copy_service(self, src_service):
        """
        Copy service configuration from src_service
        :param src_service: type str
        :return: type requests.model.Response
        """
        request_query_params = self.__basic_request_uri.copy()
        request_query_params.pop('service')
        request_query_params['from'] = src_service
        request_query_params['to'] = self.__service_name
        request_url = urljoin(self._DM_HOST, '/copy_service')
        return self._connection.get(request_url, params=request_query_params)

    @check_status
    @request
    def _deploy_proxy(self, proxy_type='searchproxy', from_service=False, only_save=True, degrade_level=0.15):
        """
        Deploy searchproxy|indexerproxy function.
        :param proxy_type: type str
        :param only_save: type boolean
        :param degrade_level: type int
        :return: type requests.model.Response
        """
        request_query_params = self.__basic_request_uri.copy()
        if proxy_type not in ['searchproxy', 'indexerproxy']:
            logging.error('Available values of parameter proxy_type is "searchproxy" or "indexerproxy"')
            return
        if only_save:
            request_query_params['slots'] = str([])
        if from_service:
            request_query_params['version'] = 'CURRENT'
            request_query_params['force_services'] = self.__service_name
        request_query_params['service'] = proxy_type
        request_query_params['service_type'] = proxy_type
        request_query_params['may_be_dead_procentage'] = degrade_level
        request_url = urljoin(self._DM_HOST, '/deploy')
        return self._connection.get(request_url, params=request_query_params)

    @request
    def _remove_service(self):
        """
        Remove service from Deploy Manager
        :return: type requests.model.Response
        """
        request_query_params = self.__basic_request_uri.copy()
        request_query_params['action'] = 'remove_service'
        request_url = urljoin(self._DM_HOST, '/modify_searchmap')
        return self._connection.get(request_url, params=request_query_params)

    @request
    def _update_tags_info(self, action='set', nanny='', slots='', use_containers=False):
        """
        Add tags information for Deploy Manager service. Required to add replicas in the future.
        :param action: type str
        :param nanny: type str
        :param slots: type str or list
        :param use_containers: type boolean
        :return: type requests.model.Response
        """
        request_query_params = self.__basic_request_uri.copy()
        request_query_params['action'] = action
        if nanny:
            request_query_params['nanny_services'] = nanny
        if use_containers:
            request_query_params['use_container_names'] = use_containers
        if slots:
            request_query_params['slots'] = slots
        request_query_params['exclude'] = ''
        request_url = urljoin(self._DM_HOST, '/modify_tags_info')
        return self._connection.get(request_url, params=request_query_params)

    @request
    def _chance_service_info(self, shard_by='', per_dc_search=False):
        """
        Manage shard_by type and. Supportable values 'url_hash' and 'keyprefix'.
        :param shard_by: type str
        :param per_dc_search: type boolean
        :return: type requests.model.Response
        """
        service_info_atts = {}
        if shard_by:
            service_info_atts['shard_by'] = shard_by
        if per_dc_search:
            service_info_atts['per_dc_search'] = per_dc_search

        request_query_params = self.__basic_request_uri.copy()
        request_query_params['action'] = 'edit_service'
        request_query_params['service_info'] = json.dumps(service_info_atts)
        request_url = urljoin(self._DM_HOST, '/modify_searchmap')
        return self._connection.get(request_url, params=request_query_params)

    # @request
    # def _change_per_dc_search(self, enabled=False):
    #     """
    #     Manage per_dc_search option.
    #     :param enabled: type boolean
    #     :return: type requests.model.Response
    #     """
    #     request_query_params = self.__basic_request_uri.copy()
    #     request_query_params['action'] = 'edit_service'
    #     request_query_params['service_info'] = json.dumps({'per_dc_search': enabled})
    #     request_url = urljoin(self._DM_HOST, '/modify_searchmap')
    #     return self._connection.get(request_url, params=request_query_params)

    @request
    @retryable
    def _get_conf(self, config_file):
        """
        Get config file by name. Use direct method for config file.
        :return: type requests.model.Response
        """
        request_url = urljoin(self._DM_HOST, '/get_conf?filename=%s' % config_file)
        return self._connection.get(request_url)

    @request
    @retryable
    def _get_deploy_info(self, task_id):
        """
        Get information about task progress
        :param task_id: type long string like type=ADD_REPLICA_TASK;srv=service_name;time=timestamp;hash=hashsum;tsdiscr=10
        :return: type requests.model.Response
        """
        request_query_params = self.__basic_request_uri.copy()
        request_query_params['id'] = task_id
        request_url = urljoin(self._DM_HOST, '/deploy_info')
        return self._connection.get(request_url, params=request_query_params)

    @request
    @retryable
    def _get_list_conf(self, service_name='', service_type='', slot=''):
        """
        Get list of configuration files for service
        :param slot: type srt
        :return: type requests.model.Response
        """
        request_query_params = self.__basic_request_uri.copy()
        if service_name:
            request_query_params['service'] = service_name
        if service_type:
            request_query_params['service_type'] = service_type
        request_query_params['slot'] = slot
        request_url = urljoin(self._DM_HOST, '/list_conf')
        return self._connection.get(request_url, params=request_query_params)

    @request
    @retryable
    def _get_files(self):
        """
        Get files using by service
        :return: type requests.model.Response
        """
        request_query_params = self.__basic_request_uri.copy()
        request_url = urljoin(self._DM_HOST, '/get_files')
        return self._connection.get(request_url, params=request_query_params)

    @request
    @retryable
    def _get_tag_slots(self, tag):
        """
        Get slots by tag from Deploy Manager
        :param tag: type str
        :return: type requests.model.Response
        """
        request_url = urljoin(self._DM_HOST, '/slots_by_tag')
        return self._connection.get(request_url, params={'tag': tag})

    @request
    @retryable
    def _get_services(self, ctype='', service_type=''):
        """
        Get information about all SaaS services
        :param ctype: type str in any of SaaS ctypes
        :param service_type: type str in ['rtyserver', 'searchproxy', 'indexerproxy']
        :return: type requests.model.Response
        """
        request_query_params = self.__basic_request_uri.copy()
        if ctype:
            request_query_params['ctype'] = ctype
        if service_type:
            request_query_params['service_type'] = service_type

        request_url = urljoin(self._DM_HOST, '/get_services')
        return self._connection.get(request_url, params=request_query_params)

    @request
    @retryable
    def _get_unused_slots(self, check_pool=False):
        """
        Get slots by interval from Deploy Manager
        :param check_pool: type boolean
        :return: type requests.model.Response
        """
        request_query_params = self.__basic_request_uri.copy()
        if check_pool:
            request_query_params['service'] = 'unused'
        request_url = urljoin(self._DM_HOST, '/free_slots')
        return self._connection.get(request_url, params=request_query_params)

    @request
    @retryable
    def _get_used_slots(self):
        """
        Get information about used service slots from Deploy Manager
        :return: type requests.model.Response
        """
        request_url = urljoin(self._DM_HOST, '/api/slots_by_interval/')
        return self._connection.get(request_url, params=self.__basic_request_uri.copy())

    @request
    @retryable
    def _get_service_keys(self):
        """
        Get keys for service
        :return: type requests.model.Response
        """
        request_url = urljoin(self._DM_HOST, '/secret_key')
        return self._connection.get(request_url, params={'service': self.__service_name})

    @request
    @retryable
    def _get_searchproxy_instances(self):
        """
        Get list of searchproxy instances for service by ctype
        :return: type requests.model.Response
        """
        request_query_params = self.__basic_request_uri.copy()
        request_query_params['service_type'] = 'searchproxy'
        request_query_params['service'] = 'searchproxy'
        request_url = urljoin(self._DM_HOST, '/api/slots_by_interval')
        return self._connection.get(request_url, params=request_query_params)

    @request
    @retryable
    def _get_sla_info(self):
        """
        Get SLA information for service
        :return: type requests.model.Response
        """
        request_query_params = self.__basic_request_uri.copy()
        request_query_params.pop('service_type')
        request_query_params['action'] = 'get'
        request_url = urljoin(self._DM_HOST, '/process_sla_description')
        return self._connection.get(request_url, params=request_query_params)

    @request
    def _set_configuration(self, filename, data):
        """
        Add/Edit configuration files
        :param filename: type str
        :param data: type str
        :return: type requests.model.Response
        """
        request_query_params = {
            'hex': 'yes',
            'service': '',
            'root': '/',
            'filename': 'configs/{}/{}'.format(self.__service_name, filename)
        }
        encoded_data = binascii.hexlify(data)
        request_url = urljoin(self._DM_HOST, '/set_conf')
        return self._connection.post(request_url, data=encoded_data, params=request_query_params)

    @request
    def _set_sla_description(self, data):
        """
        Set service owner, responsible, ticket etc.
        Available keys for data:
        owners, responsibles, ticket,
        maxdocs, search_rps, search_rps_planned, index_rps, total_index_size_bytes,
        search_q_99_ms, search_q_999_ms,
        unanswers_5min_perc_warn, unanswers_5min_perc_crit,
        disaster_alerts, ferrymans
        :param data: type str
        :return: type requests.model.Response
        """
        request_query_params = self.__basic_request_uri.copy()
        request_query_params.pop('service_type')
        request_query_params['action'] = 'set'
        request_url = urljoin(self._DM_HOST, '/process_sla_description')
        return self._connection.post(request_url, json=data, params=request_query_params)

    @check_status
    @request
    def _deploy_rtyserver(self):
        """
        Deploy rtyserver configuration for all slots in service
        :return: type requests.model.Response
        """
        request_url = urljoin(self._DM_HOST, '/deploy')
        return self._connection.get(request_url, params=self.__basic_request_uri.copy())

    @request
    def _restart_all_slots(self):
        """
        Restart all slots in service
        :return: type requests.model.Response
        """
        request_query_params = self.__basic_request_uri.copy()
        request_query_params['command'] = 'restart'
        request_url = urljoin(self._DM_HOST, '/broadcast')
        return self._connection.get(request_url, params=request_query_params)

    @request
    def _get_slots_and_stats(self, slot_filter=None):
        request_url = urljoin(self._DM_HOST, '/api/slots_and_stat/:{}'.format(self.__type))
        request_query_params = self.__basic_request_uri.copy()
        if slot_filter:
            request_query_params['filter'] = slot_filter
        return self._connection.get(request_url, params=request_query_params)

    # Manage methods

    def _collect_tag_slots(self, tag):
        """
        Collect slots host:port by tag
        :param tag: type str
        :return: type list
        """
        resp = self._get_tag_slots(tag).json()
        return get_slots(resp)

    def _collect_unused_slots(self, check_pool=False):
        """
        Collect unused slots host:port
        :return: type list
        """
        resp = self._get_unused_slots(check_pool=check_pool).json().get(self.__service_ctype)
        return get_slots(resp)

    @update_history
    def _collect_service_keys(self):
        resp = self._get_service_keys()
        if resp.status_code == 200:
            logging.info('[%s] Getting service keys operation success', self.__service_name)
            # add service keys to class field
            self.__keys = resp.json()[self.__service_ctype][self.__service_name]
            self.__keys = {key: value for key, value in self.__keys.items() if 'json_' in key}
            return self.__keys
        else:
            logging.warning('[%s] Getting service keys operation failed', self.__service_name)
            return None

    @update_history
    def _new_service(self):
        """
        Creating a new service
        """
        result = False
        resp = self._add_service()
        if resp.status_code == 200:
            try:
                front_resp = self._add_service_to_frontend()
                if front_resp.status_code != 200:
                    logging.warning('[%s] Cannot place service on deploy manager frontend', self.__service_name)
                else:
                    result = True
            except Exception as e:
                logging.warning('[%s] Cannot place service on deploy manager frontend: %s', self.__service_name, e)
        else:
            logging.warning('[%s] Cannot create new service on deploy manager', self.__service_name)
        return result

    @update_history
    def _tags_info_management(self, action='set', nanny='', slots='', use_containers=False):
        """
        Adding slots or tags for service
        :param action: type str
        :param nanny: type str
        :param slots: type str or list
        :param use_containers: type boolean
        :return: type boolean
        """
        result = False
        if action not in ["set", "invalidate"]:
            logging.error('[%s] Unknown action. Supportable values: set, invalidate', self.__service_name)
        else:
            resp = self._update_tags_info(action=action, nanny=nanny, slots=slots, use_containers=use_containers)
            if resp.status_code == 200:
                logging.info('[%s] Success tags update', self.__service_name)
                result = True
        return result

    @update_history
    def _shard_by_management(self, shard_by):
        """
        Manage shard_by parameter
        :param shard_by: type str, available values ['url_hash', 'keyprefix']
        :return: type boolean
        """
        result = False
        if shard_by in ['url_hash', 'keyprefix']:
            resp = self._change_shard_by(shard_by)
            if resp.status_code != 200:
                logging.error('[%s] Failed to switch shard_by parameter', self.__service_name)
            else:
                result = True
        else:
            logging.error('[%s] Unknown shard_by parameter: %s', self.__service_name, shard_by)
        return result

    @update_history
    def _add_config_file(self, filename):
        """
        Add new configuration file. File can me rewrited. Use with careful.
        :param filename: type str
        :return: type boolean
        """
        resp = self._set_configuration(filename, 'new_file')
        if resp.status_code == 200:
            logging.info('[%s] Added new configuration file %s', self.__service_name, filename)
            return True
        else:
            logging.warning('[%s] Failed to add configuration file %s', self.__service_name, filename)
            return False

    @update_history
    def _update_config_file(self, filename, data):
        """
        Rewrite configuration file by specified data
        :param filename: type str
        :param data: type str
        :return: type boolean
        """
        resp = self._set_configuration(filename, data)
        if resp.status_code == 200:
            logging.info('[%s] Edit configuration file %s', self.__service_name, filename)
            return True
        else:
            logging.warning('[%s] Failed to edit configuration file %s', self.__service_name, filename)
            return False

    @update_history
    def _get_replicas_data(self):
        """
        Get sorted collect sorted by DC replicas slots. Also used "replic_id" parameter
        for prevent intersection of replicas
        :return: type dict or type None
        """
        replicas_data = {}
        resp = self._get_used_slots()
        if resp.status_code != 200:
            logging.error('[%s] Failed to get slots used by service', self.__service_name)
            return None

        for shard in resp.json():
            for slot in shard['slots']:
                if not replicas_data.get(slot['$datacenter$']):
                    replicas_data[slot['$datacenter$']] = {}
                if not replicas_data[slot['$datacenter$']].get(slot['replic_id']):
                    replicas_data[slot['$datacenter$']][slot['replic_id']] = []
                replicas_data[slot['$datacenter$']][slot['replic_id']].append(slot['slot'])

        return replicas_data


class DeployManager(DeployManagerAPI):
    """
    Create and manage DeployManager services.
    """
    __SEARCH_SERVICE_TYPE = 'search'
    __MIDDLE_KV_SERVICE_TYPE = 'stable_middle_kv'
    __KV_SERVICE_TYPE = 'kv'
    __DEFAULT_SERVICE_TYPE = ''

    # __CONFIG_TEMPLATES_DIR = '%s/tmpl' % os.path.abspath(__file__).split('modules')[0]
    __CONFIG_TEMPLATES_DIR = 'tmpl/dm'
    __DELIVERY_TEMPLATES_PREFIX = 'delivery/'

    def __init__(self, service_name, ctype, stype='', service_type='', use_proxy=False):
        super(DeployManager, self).__init__(service_name, ctype, stype=stype, use_proxy=use_proxy)
        self.__service_name = service_name
        self.__ctype = ctype

        # Service type settings
        if service_type and service_type in [self.__SEARCH_SERVICE_TYPE, self.__KV_SERVICE_TYPE,
                                             self.__MIDDLE_KV_SERVICE_TYPE]:
            self.__service_type = service_type
        else:
            self.__service_type = self.__DEFAULT_SERVICE_TYPE

        # Template variables
        self.__template_variables = {
            'service_name': self.__service_name,
            'service_type': self.__service_type,
            'connect_timeout': '20',
        }

    @update_history
    def _prepare_stable_service(self, slots_count, replicas_per_dc, nanny_service):
        """
        Preparing stable environment for service
        :param slots_count: type int or str
        :param nanny_service: type str
        """
        logging.info('[%s] Preparing stable environment', self.__service_name)
        self._tags_info_management(nanny=nanny_service, use_containers=True)
        # slots_alloc = self._get_tag_slots(nanny_service).json()
        unused_slots = self._collect_unused_slots()
        #  was "3 * replicas_per_dc" but MAN
        resp = self._add_replica(int(slots_count/replicas_per_dc), 2 * replicas_per_dc, unused_slots)
        if resp.status_code == 200 and self.check_service_with_slots():
            return True
        else:
            return False

    @update_history
    def _prepare_prestable_service(self, slots_count, replicas_count=1):
        """
        Preparing prestable environment for service
        :param slots_count: type int or str
        :param replicas_count: type int
        """
        logging.info('[%s] Preparing prestable environment', self.__service_name)
        unused_slots = self._collect_unused_slots(check_pool=True)
        if len(unused_slots) >= slots_count:
            slots = unused_slots[0:slots_count]
            self._tags_info_management(nanny='saas_yp_cloud_prestable', slots='', use_containers=True)
            logging.info('[%s] Trying to add replica with slots: %s', self.__service_name, slots)
            resp = self._add_replica(slots_count/replicas_count, replicas_count, slots)
            if resp.status_code == 200 and self.check_service_with_slots():
                return True
            else:
                return False
        else:
            logging.error('[%s] Not enouch free slots count %s', self.__service_name, slots_count)
            return False

    @update_history
    def _prepare_testing_service(self, slots_count, replicas_count=1):
        """
        Preparing prestable environment for service
        :param slots_count: type int or str
        :param replicas_count: type int
        """
        logging.info('[%s] Preparing testing environment', self.__service_name)
        unused_slots = self._collect_unused_slots(check_pool=True)
        if len(unused_slots) >= slots_count:
            slots = unused_slots[0:slots_count]
            self._tags_info_management(nanny='saas_yp_cloud_base_testing', slots='', use_containers=True)
            logging.info('[%s] Trying to add replica with slots: %s', self.__service_name, slots)
            resp = self._add_replica(slots_count/replicas_count, replicas_count, slots)
            if resp.status_code == 200 and self.check_service_with_slots():
                return True
            else:
                return False
        else:
            logging.error('[%s] Not enouch free slots count %s', self.__service_name, slots_count)
            return False

    @update_history
    def _load_templates(self):
        """
        Load  templates from filesystem
        :return: type dict
        """
        # Prepare config folder
        if not os.path.exists(self.__CONFIG_TEMPLATES_DIR):
            os.makedirs(self.__CONFIG_TEMPLATES_DIR)

        # Load templates
        loader = jinja2.FileSystemLoader(self.__CONFIG_TEMPLATES_DIR)
        templates = jinja2.Environment(loader=loader)
        return templates

    @update_history
    def _load_configs_from_templates(self):
        """
        Load configuration templates from filesystem and make configuration files
        :return: type dict
        """
        logging.info('[%s] Getting config files from templates', self.__service_name)
        templates = self._load_templates()

        configs = {}
        for template_name in templates.list_templates():
            if self.__service_type in template_name and not template_name.startswith(self.__DELIVERY_TEMPLATES_PREFIX):
                template = templates.get_template(template_name)
                config_data = template.render(self.__template_variables)
                if 'searchproxy' in template_name or 'indexerproxy' in template_name:
                    config_name = '%s-%s.conf' % (template_name.split('/')[1].split('.')[0],
                                                  self.__service_name)
                elif 'ctdiff' in template_name:
                    config_name = '%s-%s-%s' % (template_name.split('/')[1], self.__ctype, self.__service_name)
                else:
                    config_name = '%s-%s' % (template_name.split('/')[1],
                                             self.__service_name)
                configs[config_name] = config_data
        return configs

    def get_allow_empty_timestamp(self):
        plus_2_weeks = datetime.datetime.utcnow() + datetime.timedelta(weeks=2)
        return plus_2_weeks.strftime('%Y-%m-%dT%H:%M:%S')  # don't care about time zones

    @update_history
    def _prepare_docfetcher(self, yt_path, yt_cluster, delivery_type, yt_token=None):
        """
        Get configuration templates, make patch for rtyserver.diff config and upload
        """
        logging.info('[%s] Patching rtyserver.diff for docfetcher %s for cluster %s', self.__service_name, delivery_type, yt_cluster)
        rtyserver_data = {}
        delivery_patch_data = ''

        if not yt_token:
            yt_token = PersistentTokenStore.get_token_from_store_env_or_file('yt')

        variables = {
            'service_name': self.__service_name,
            'service_type': self.__service_type,
            'yt_cluster': yt_cluster,
            'yt_path':  yt_path,
            'yt_token': yt_token,
            'snapshot_allow_empty': self.get_allow_empty_timestamp()
        }
        templates = self._load_templates()
        for template_name in templates.list_templates():
            template = templates.get_template(template_name)
            if self.__service_type in template_name and 'rtyserver' in template_name:
                rtyserver_data = {'name': '%s-%s' % (template_name.split('/')[1],
                                                     self.__service_name),
                                  'data': template.render(variables)}
            if template_name.startswith(self.__DELIVERY_TEMPLATES_PREFIX) and delivery_type in template_name:
                delivery_patch_data = template.render(variables)
        if rtyserver_data and delivery_patch_data:
            rtyserver_data['data'] = rtyserver_data['data'].split('\n}')[0] + ',\n\n' + delivery_patch_data + '\n}'
            if self._update_config_file(rtyserver_data['name'], rtyserver_data['data']):
                return True
        return False

    @update_history
    def _prepare_environment(self):
        """
        Get basic configuration files for search or kv service type and load it to Deploy Manager
        :return:
        """
        logging.info('[%s] Prepare %s environment with basic configs', self.__service_name, self.__service_type)
        result = False
        configs = self._load_configs_from_templates()
        for config_name in configs:
            if self._add_config_file(config_name):
                if self._update_config_file(config_name, configs[config_name]):
                    result = True
        return result

    @update_history
    def _prepare_tvm_configuration(self, tvm_ids):
        """
        Get searchproxy config.
        Get existing tvm_data.
        Patch or generate tvm_data.
        Patch searchproxy config with new tvm_data and upload.
        :param tvm_ids: type str or int
        :return:
        """
        logging.info('[%s] Preparing tvm configuration', self.__service_name)
        searchproxy_conf = self._get_conf('{}/searchproxy-{}.conf'.format(self.__service_name, self.__service_name)).text
        tvm_data, sp_data = dm_tvm.extract_tvm_data(searchproxy_conf)
        if type(tvm_ids) is list:
            for tvm_id in tvm_ids:
                tvm_data = dm_tvm.prepare_tvm_data(self.__ctype, str(tvm_id), tvm_data=tvm_data)
        else:
            tvm_data = dm_tvm.prepare_tvm_data(self.__ctype, str(tvm_ids), tvm_data=tvm_data)
        searchproxy_conf = dm_tvm.patch_searchproxy_config(sp_data, tvm_data)
        logging.debug('New searchproxy config\n' + searchproxy_conf)
        if self._update_config_file('searchproxy-{}.conf'.format(self.__service_name), searchproxy_conf):
            return True
        return False

    @update_history
    def _set_sla_info(self, owners, resps='', ticket='', search_rps='', index_size='',
                      search_99='', search_999='', max_docs='', other_data=''):
        """
        Collect information about service owners/responsibles
        :param owners: type list
        :param resps: type list
        :param ticket:  type list
        :param search_rps: type str
        :param index_size: type str
        :param search_99: type str
        :param search_999: type str
        :param max_docs: type str
        :param other_data: type dict
        :return:
        """
        logging.info('[%s] Set service owners/responsibles', self.__service_name)
        data = {
            'owners': owners,
            'responsibles': owners
        }
        if resps:
            data['responsibles'] = resps
        if ticket:
            data['ticket'] = ticket
        if search_rps:
            data['search_rps'] = search_rps
            data['search_rps_planned'] = search_rps
        if search_99:
            data['search_q_99_ms'] = search_99
        if search_999:
            data['search_q_999_ms'] = search_999
        if max_docs:
            data['maxdocs'] = max_docs
        if index_size:
            data['total_index_size_bytes'] = index_size
        if other_data:
            data.update(other_data)
        resp = self._set_sla_description(data)
        if resp.status_code == 200:
            return True
        else:
            return False

    def check_service_with_slots(self):
        """
        Check for existing slots in service before deploying proxies.
        """
        resp = self._get_slots_and_stats()
        if resp.status_code == 200:
            slots_data = resp.json().get('slots')
            if len(slots_data) > 0:
                return True
        return False

    def check_service_exists(self, strong_ctype=False):
        """
        Check service exists in all ctypes. If sets option "strong_ctype" check only target ctype
        If request fails return true for prevent probable configuration override
        :return: type boolean
        """
        service_exists = False
        resp = self._get_service_keys()
        if resp.status_code == 200:
            service_keys = resp.json()
            if strong_ctype:
                if len(service_keys.get(self.__ctype)) > 0:
                    return True
                else:
                    return False
            for _ctype in service_keys:
                if len(service_keys[_ctype]) > 0:
                    logging.info('[%s] Found already existing service in ctype %s', self.__service_name, _ctype)
                    service_exists = True
            return service_exists
        else:
            logging.warning('[%s] Cannot check for service exists. Please check DM', self.__service_name)
            return True

    def create_service(self, slots_count, slots_data, sla_info='', delivery_info='', src_service='',
                       shard_by='url_hash', tvm_ids='', replicas_per_dc=1, prepare_env=False, deploy_proxies=False):
        """
        Create and customize a new service by specified parameters.
        :param slots_count: type int or str
        :param slots_data: type str or list
        :param sla_info: type dict
        :param delivery_info: type dict
        :param src_service: type str
        :param shard_by: type str
        :param replicas_per_dc: type int
        :param prepare_env: type boolean
        :param deploy_proxies: type boolean
        :return: type dict
        """
        logging.info('Creating service %s type: %s ctype: %s with %d slots count', self.__service_name, self.__service_type,
                     self.__ctype, slots_count)
        # Check service exists in any ctype
        service_exists = self.check_service_exists()

        # Copy and create service if does not found
        if not self.check_service_exists(strong_ctype=True):
            self._new_service()
        if src_service:
            logging.info('[%s] Copy from service %s', self.__service_name, src_service)
            self._copy_service(src_service)

        # Manage per_dc_search SAAS-5347 / SAAS-5679
        if replicas_per_dc >= 3:
            logging.info('[%s] Enable per dc search', self.__service_name)
            per_dc_search = True
            self.__template_variables['connect_timeout'] = 7
        else:
            per_dc_search = False

        # Prepare search or kv environment. If service does not exists in any ctype the
        # environment will be prepared.
        if not src_service:
            if prepare_env and self.__service_type or not service_exists:
                self._prepare_environment()
                # Prepare ytpull if configured
                if delivery_info and len(delivery_info) > 0:
                    logging.info('docfetcher will be configured')
                    self._prepare_docfetcher(delivery_info.get('yt_path'), delivery_info.get('yt_cluster'), delivery_info.get('delivery_type'), delivery_info.get('yt_token'))

        # Manage shard_by parameter
        if shard_by and shard_by in ['url_hash', 'keyprefix']:
            change_shard_by = True
        else:
            change_shard_by = False

        if per_dc_search or change_shard_by:
            self._chance_service_info(shard_by, per_dc_search)

        # Prepare stable or prestable slots/replicas
        if self.__ctype == 'testing':
            prepare_result = self._prepare_testing_service(slots_count, replicas_per_dc)
        else:
            prepare_result = self._prepare_stable_service(slots_count, replicas_per_dc, slots_data)

        if not prepare_result:
            raise RuntimeError('[%s] Failed to prepare service replicas. '
                               'Please check instances and rule instances//replicas_count = 0')

        # Prepare tvm configuration
        if tvm_ids:
            self._prepare_tvm_configuration(tvm_ids)

        # Deploy rtyserver
        logging.info('[%s] Deploying rtyserver', self.__service_name)
        self._deploy_rtyserver()

        # Create endpointsets
        saas_service = SaasService(self.__ctype, self.__service_name)
        saas_service.update_pod_labels()
        saas_service.create_endpointsets(do_update_dm=True)

        # Deploy proxy
        logging.info('[%s] Deploying searchproxy & indexerproxy', self.__service_name)
        if deploy_proxies:
            # Only save deploy to avoid DM bug
            self._deploy_proxy(proxy_type='searchproxy', only_save=True)
            self._deploy_proxy(proxy_type='indexerproxy', only_save=True)
            self._deploy_proxy(proxy_type='indexerproxy', from_service=True, only_save=True)
            self._deploy_proxy(proxy_type='searchproxy', only_save=False)
            self._deploy_proxy(proxy_type='indexerproxy', only_save=False)
        else:
            self._deploy_proxy(proxy_type='searchproxy', only_save=True)
            self._deploy_proxy(proxy_type='searchproxy', from_service=True, only_save=True)
            self._deploy_proxy(proxy_type='indexerproxy', only_save=True)

        self._set_sla_info(sla_info['owners'], other_data=sla_info)
        self._collect_service_keys()
        self.actions_history.append('Worker hostname: ' + socket.gethostname())

    def get_services(self, ctype=''):
        """
        Get information about services of specified ctype and service_type
        :param ctype: type str in any of SaaS ctypes
        :return: type list
        """
        services_list = self._get_services().json()
        ctype = ctype if ctype else self.__ctype
        return services_list[ctype]['rtyserver'].keys()
