# coding: utf-8

"""
Some helpers for common operations with nanny api. Requires nanny oauth token in OAUTH_NANNY environment variable
"""

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

import logging
import copy
from six.moves import queue
from typing import Set  # noqa

import saas.library.python.gencfg as gencfg

from saas.tools.devops.lib23.endpointset import Endpointset
from saas.tools.devops.lib23.service_token import ServiceTokenStore

import infra.nanny.nanny_services_rest.nanny_services_rest.client as nanny_client
import infra.nanny.nanny_services_rest.nanny_services_rest.errors as nanny_errors
from infra.nanny.nanny_services_rest.nanny_services_rest.client import ServiceRepoClient

import nanny_rpc_client
from nanny_rpc_client import RequestsRpcClient
from nanny_repo import repo_api_stub, repo_api_pb2

from infra.nanny.yp_lite_api.proto import pod_sets_api_pb2
from infra.nanny.yp_lite_api.py_stubs import pod_sets_api_stub
from infra.nanny.yp_lite_api.proto import endpoint_sets_api_pb2
from infra.nanny.yp_lite_api.py_stubs import endpoint_sets_api_stub

from google.protobuf import json_format

from nanny_tickets import tickets_api_stub
from nanny_tickets import tickets_api_pb2

from library.python.vault_client.instances import Production as VaultClient


class NannyApiError(Exception):
    pass


class InvalidPropertyForAllocationType(NannyApiError):
    pass


class NannyService(object):
    LOGGER = logging.getLogger(__name__)
    NANNY = None
    NANNY_RPC = None
    _PODSET_API = None
    _ENDPOINTSET_API = None

    @classmethod
    def initialize_nanny_client(cls):
        if cls.NANNY is None:
            cls.LOGGER.debug('Initializing nanny service repo client')
            cls.NANNY = nanny_client.ServiceRepoClient('https://nanny.yandex-team.ru', token=ServiceTokenStore.get_token('nanny'))

    @classmethod
    def initialize_nanny_rpc_client(cls):
        if cls.NANNY_RPC is None:
            cls.LOGGER.debug('Initializing nanny rpc service repo client')
            cls.NANNY_RPC = nanny_rpc_client.RetryingRpcClient(rpc_url='https://nanny.yandex-team.ru/api/repo/',
                                                               oauth_token=ServiceTokenStore.get_token('nanny'))
            cls.repo_stub = repo_api_stub.RepoServiceStub(cls.NANNY_RPC)

    @classmethod
    def initialize_podset_api(cls):
        if cls._PODSET_API is None:
            cls.LOGGER.debug('Initializing YP.lite api client')
            cls._PODSET_API = nanny_rpc_client.RetryingRpcClient(
                rpc_url='https://yp-lite-ui.nanny.yandex-team.ru/api/yplite/pod-sets/',
                oauth_token=ServiceTokenStore.get_token('nanny'),
                request_timeout=60
            )
            cls.podset_api = pod_sets_api_stub.YpLiteUIPodSetsServiceStub(cls._PODSET_API)

    @classmethod
    def initialize_endpointset_api(cls):
        if cls._ENDPOINTSET_API is None:
            cls.LOGGER.debug('Initializing endpointset api')
            cls._ENDPOINTSET_API = nanny_rpc_client.RetryingRpcClient(
                rpc_url='https://yp-lite-ui.nanny.yandex-team.ru/api/yplite/endpoint-sets/',
                oauth_token=ServiceTokenStore.get_token('nanny')
            )
            cls.endpointset_api = endpoint_sets_api_stub.YpLiteUIEndpointSetsServiceStub(cls._ENDPOINTSET_API)

    def __init__(self, name, runtime_attrs=None, info_attrs=None, auth_attrs=None, history_runtime_attrs=None):
        self.initialize_nanny_client()
        self.name = name
        self._runtime_attrs = runtime_attrs
        self._info_attrs = info_attrs
        self._auth_attrs = auth_attrs
        self._history_runtime_attrs = history_runtime_attrs
        if self.is_yp_lite():
            self.initialize_nanny_rpc_client()
            self.initialize_podset_api()
            self.initialize_endpointset_api()
            # self.yp_lite = self._PODSET_API
            # self.endpointset_api = self._ENDPOINTSET_API

    def __eq__(self, other):
        if isinstance(other, NannyService):
            return self.name == other.name
        else:
            return NotImplemented

    def __ne__(self, other):
        return not self.__eq__(other)

    def __hash__(self):
        return hash(self.name)

    def __repr__(self):
        return 'NannyService({})'.format(self.name)

    @property
    def gencfg_groups_names(self):
        return self.get_gencfg_groups(names_only=True)

    def get_gencfg_groups(self, no_raise=False, names_only=False):
        # type: (bool, bool) -> (Set[gencfg.GencfgGroup], Set[str])
        """
        :param no_raise: Return empty set instead of rising exception if service has non-gencfg allocation type
        :param names_only: Return only names of Gencfg groups, not GencfgGroup objects
        :return: Set of Gencfg groups
        """
        runtime_instances = self.runtime_attrs['instances']
        instance_source = runtime_instances['chosen_type']
        if instance_source == 'EXTENDED_GENCFG_GROUPS':
            groups_dict = runtime_instances['extended_gencfg_groups']['groups']
            if names_only:
                result = [g['name'] for g in groups_dict]
            else:
                result = [gencfg.GencfgGroup(g['name'], g['release'], validate=False) for g in groups_dict]
            return set(result)
        else:
            error_msg = 'Service {} dont use gencfg groups. Actual instance source: {}'.format(self.name, instance_source)
            if no_raise:
                self.LOGGER.error(error_msg)
                return set()
            else:
                raise NannyApiError(error_msg)

    def has_groups_with_io_limits(self):
        if not self.is_gencfg_allocated():
            return False
        else:
            return any([g.io_limits for g in self.get_gencfg_groups()])

    def has_groups_without_io_limits(self):
        if not self.is_gencfg_allocated():
            return False
        else:
            return any([not g.io_limits for g in self.get_gencfg_groups()])

    @property
    def porto_properties_on_slot(self):
        slot_porto_properties = 'NONE'
        try:
            slot_porto_properties = self.runtime_attrs['instances']['extended_gencfg_groups']['containers_settings']['slot_porto_properties']
        except KeyError:
            logging.error('slot_porto_properties not found for service %s', self.name)
        return slot_porto_properties == 'ALL_EXCEPT_GUARANTEES'

    @porto_properties_on_slot.setter
    def porto_properties_on_slot(self, value):
        """
        :type value: bool
        """
        if not self.is_gencfg_allocated():
            raise InvalidPropertyForAllocationType(
                'Setting porto_properties_on_slot only available for Gencfg allocated services. {} service has {} allocation'.format(
                    self.name, self.allocation_type
                )
            )
        runtime_attrs = self.runtime_attrs

        if value is True or value == 'ALL_EXCEPT_GUARANTEES':
            runtime_attrs['instances']['extended_gencfg_groups']['containers_settings']['slot_porto_properties'] = 'ALL_EXCEPT_GUARANTEES'
            comment = 'Set porto properties on slot'
        else:
            runtime_attrs['instances']['extended_gencfg_groups']['containers_settings']['slot_porto_properties'] = 'NONE'
            comment = 'Set porto properties on instance (not slot)'
        self.update_runtime_attrs(runtime_attrs, comment)

    @property
    def runtime_attrs(self):
        try:
            if self._runtime_attrs is None:
                self._runtime_attrs = self.NANNY.get_runtime_attrs(self.name)
            return copy.deepcopy(self._runtime_attrs['content'])
        except nanny_errors.NotFoundError as e:
            self.LOGGER.critical('Service %s not found in Nanny service repo', self.name)
            raise NannyApiError(e)

    @property
    def history_runtime_attrs(self):
        if self._history_runtime_attrs is None:
            self._history_runtime_attrs = self.NANNY.get_history_runtime_attrs(self.name)
        return copy.deepcopy(self._history_runtime_attrs['result'])

    @property
    def info_attrs(self):
        if self._info_attrs is None:
            self._info_attrs = self.NANNY.get_info_attrs(self.name)
        return copy.deepcopy(self._info_attrs['content'])

    @property
    def auth_attrs(self):
        auth_attrs = self.NANNY.get_auth_attrs(self.name)
        if self._auth_attrs is not None and self._auth_attrs != auth_attrs:
            logging.warn('Looks like auth attrs changed externaly')
        logging.debug('Got auth attrs for service %s : %s', self.name, auth_attrs)
        self._auth_attrs = auth_attrs
        return copy.deepcopy(auth_attrs['content'])

    def _create_request_data(self, enable_replication=True, wait_duration=3600, snapshot_priority='NORMAL',
                             max_unavailable=1, toleration_duration=43200, replication_method='MOVE', pod_group_id_path='/labels/shard', max_unavailable_in_group=1):
        request_data = {
            'meta': {
                'id': self.name,
            },
            'spec': {
                'involuntary': {
                    'disruption_timeout_seconds': wait_duration,
                },
                'voluntary': {
                    'disruption_toleration_seconds': toleration_duration,
                },
                'rate_limit': {
                    'delay_seconds': toleration_duration
                },
                'max_tolerable_downtime_seconds': wait_duration,
                'involuntary_replication_choice': 'ENABLED' if enable_replication else 'DISABLED',
                'max_unavailable': max_unavailable,
                'snapshot_priority': snapshot_priority,
                'replication_method': replication_method,
                'pod_group_id_path': pod_group_id_path,
                'max_unavailable_in_group': max_unavailable_in_group
            }
        }
        return request_data

    def create_policy(self, enable_replication=True, wait_duration=3600, snapshot_priority='NORMAL',
                      max_unavailable=1, toleration_duration=43200, replication_method='MOVE', pod_group_id_path='/labels/shard', max_unavailable_in_group=1):
        if not self.is_yp_lite():
            raise Exception('Can\'t create policy for none yp-lite service')

        if self.is_policy_exists():
            logging.debug('Couldn\'t create policy for %s nanny service : policy already exists', self.name)
            return None

        request_data = self._create_request_data(enable_replication=enable_replication, wait_duration=wait_duration, snapshot_priority=snapshot_priority,
                                                 max_unavailable=max_unavailable, toleration_duration=toleration_duration, replication_method=replication_method,
                                                 pod_group_id_path=pod_group_id_path, max_unavailable_in_group=max_unavailable_in_group)

        replication_policy_request = repo_api_pb2.CreateReplicationPolicyRequest()
        json_format.ParseDict(request_data, replication_policy_request)

        return self.repo_stub.create_replication_policy(replication_policy_request)

    def update_policy(self, enable_replication=True, wait_duration=3600, snapshot_priority='NORMAL',
                      max_unavailable=1, toleration_duration=43200, replication_method='MOVE', pod_group_id_path='/labels/shard', max_unavailable_in_group=1):
        if not self.is_yp_lite():
            raise Exception('Can\'t update policy for none yp-lite service')

        version_request = repo_api_pb2.GetReplicationPolicyRequest(policy_id=self.name)
        version = self.repo_stub.get_replication_policy(version_request).policy.meta.version

        request_data = self._create_request_data(enable_replication=enable_replication, wait_duration=wait_duration, snapshot_priority=snapshot_priority,
                                                 max_unavailable=max_unavailable, toleration_duration=toleration_duration, replication_method=replication_method,
                                                 pod_group_id_path=pod_group_id_path, max_unavailable_in_group=max_unavailable_in_group)

        replication_policy_request = repo_api_pb2.CreateReplicationPolicyRequest()
        json_format.ParseDict(request_data, replication_policy_request)
        replication_policy_request.meta.version = version

        return self.repo_stub.update_replication_policy(replication_policy_request)

    def get_policy(self):
        if not self.is_yp_lite():
            raise Exception('Can\'t get policy for none yp-lite service')

        replication_policy_request = repo_api_pb2.GetReplicationPolicyRequest()
        replication_policy_request.policy_id = self.name
        return self.repo_stub.get_replication_policy(replication_policy_request)

    def remove_policy(self):
        if not self.is_yp_lite():
            raise Exception('Can\'t remove policy for none yp-lite service')

        replication_policy_request = repo_api_pb2.RemoveReplicationPolicyRequest()
        replication_policy_request.policy_id = self.name
        return self.repo_stub.remove_replication_policy(replication_policy_request)

    def is_policy_exists(self):
        try:
            self.get_policy()
            return True
        except Exception:
            return False

    def update_info_attrs(self, new_info_attrs, comment):
        put_content = {
            'comment': comment,
            'snapshot_id': self._info_attrs['_id'],
            'content': new_info_attrs
        }
        self._info_attrs = self.NANNY.put_info_attrs(self.name, put_content)
        return self.info_attrs

    def update_runtime_attrs(self, new_runtime_attrs, comment):
        put_content = {
            'comment': comment,
            'snapshot_id': self._runtime_attrs['_id'],
            'content': new_runtime_attrs
        }
        self._runtime_attrs = self.NANNY.put_runtime_attrs(self.name, put_content)
        return self.runtime_attrs

    def update_auth_attrs(self, new_auth_attrs, comment):
        put_content = {
            'comment': comment,
            'snapshot_id': self._auth_attrs['_id'],
            'content': new_auth_attrs
        }
        self._auth_attrs = self.NANNY.put_auth_attrs(self.name, put_content)
        return self.auth_attrs

    def patch_service_info(self, patch_function, commit_comment=None):
        """
        Retrieves service_info, applies patch_function to it and saves back.
        patch_function gets something like result of json.load() and must return similar shing
        """
        new_info_attrs = patch_function(copy.deepcopy(self.info_attrs))
        if not commit_comment:
            # FIXME: TBD: Make a good autogenerated diff
            commit_comment = "Patching info_attrs with saas automatization scripts"

        result = self.update_info_attrs(new_info_attrs, commit_comment)
        logging.info('Patched general info for service %s', self.name)
        logging.debug('Patched content for service %s : %s', self.name, commit_comment)

        return result

    def patch_runtime_attributes(self, patch_function, commit_comment=None):
        """
        Retrieves runtime attributes, applies patch_function to it and saves back.
        :param patch_function:
        :param commit_comment:
        :return:
        """
        new_runtime_attrs = patch_function(self.runtime_attrs)
        if not commit_comment:
            # FIXME: TBD: Make a good autogenerated diff
            commit_comment = "Patching runtime_attrs with saas automatization scripts"

        result = self.update_runtime_attrs(new_runtime_attrs, commit_comment)
        logging.info('Patched %s runtime attributes', self.name)
        return result

    def patch_auth_attributes(self, patch_function, commit_comment=None):
        """
        Retrieves auth attributes, applies patch_function to it and saves back.
        :param patch_function:
        :param commit_comment:
        :return:
        """
        new_auth_attrs = patch_function(self.auth_attrs)
        if not commit_comment:
            # FIXME: TBD: Make a good autogenerated diff
            commit_comment = "Patching auth_attrs with saas automatization scripts"

        result = self.update_auth_attrs(new_auth_attrs, commit_comment)
        logging.info('Patched %s auth attributes', self.name)
        return result

    @property
    def allocation_type(self):
        return self.runtime_attrs['instances']['chosen_type']

    def is_yp_lite(self):
        return self.allocation_type == 'YP_POD_IDS'

    def is_gencfg_allocated(self):
        return self.allocation_type == 'EXTENDED_GENCFG_GROUPS'

    def add_ticket_integration_rule(self, sandbox_task_type, sandbox_resource_type, filter_expression, desc,
                                    queue_id='SAAS', ticket_priority='NORMAL'):
        new_info_attrs = copy.deepcopy(self.info_attrs)
        release_rule = {
            'ticket_priority': ticket_priority,
            'desc': desc,
            'queue_id': queue_id,
            'approve_policy': {'type': 'NONE'},
            'filter_params': {
                'expression': filter_expression
            },
            'sandbox_task_type': sandbox_task_type,
            'sandbox_resource_type': sandbox_resource_type,
            'responsibles': []
        }
        rules = new_info_attrs['tickets_integration']['service_release_rules']
        found = False
        for i in range(0, len(rules)):
            if rules[i]['sandbox_task_type'] == sandbox_task_type:
                rules[i] = release_rule
                found = True
                break
        if not found:
            new_info_attrs['tickets_integration']['service_release_rules'].append(release_rule)
        self.update_info_attrs(new_info_attrs, 'Add {} ticket integration'.format(desc))

    def add_common_global_recipe(self):
        def _add_common_global_recipe(j):
            recipes = j['recipes']['content']
            result = copy.deepcopy(j)
            for recipe in recipes:
                if recipe['id'] == 'common':
                    common_global_recipe = copy.deepcopy(recipe)
                    common_global_recipe['id'] = 'common_global'
                    for entry in common_global_recipe['context']:
                        if entry['key'] == 'operating_degrade_level':
                            entry['value'] = '1'
                        if entry['key'] == 'stop_degrade_level':
                            entry['value'] = '0.9'
                        if entry['key'] == 'manual_confirm':
                            entry['value'] = 'False'

                # We already have global activation recipe, doing nothing
                if recipe['id'] == 'common_global':
                    return result

            result['recipes']['content'].append(common_global_recipe)
            return result

        return self.patch_service_info(_add_common_global_recipe, commit_comment='Add recipe with DL=1')

    def add_common_noconfirm_recipe(self, geo=None):
        def _add_common_noconfirm_recipe(j):
            recipes = j['recipes']['content']
            result = copy.deepcopy(j)
            for recipe in recipes:
                if recipe['id'] == 'common':
                    common_noconfirm_recipe = copy.deepcopy(recipe)
                    common_noconfirm_recipe['id'] = 'common_noconfirm'
                    geo_is_set = False
                    for entry in common_noconfirm_recipe['context']:
                        if entry['key'] == 'manual_confirm':
                            entry['value'] = 'False'
                        if entry['key'] == 'geo' and geo is not None:
                            geo_is_set = True
                            entry['value'] = geo
                    if geo is not None and not geo_is_set:
                        common_noconfirm_recipe['context'].append({'key': 'geo', 'value': geo})

                # We already have nonconfirm activation recipe, doing nothing
                if recipe['id'] == 'common_noconfirm':
                    return result

            result['recipes']['content'].append(common_noconfirm_recipe)
            return result
        return self.patch_service_info(_add_common_noconfirm_recipe, commit_comment='Add recipe with no manual confirm')

    def _add_resource(self, local_path, rtype='sandbox_files', **kwargs):
        new_runtime_attrs = copy.deepcopy(self.runtime_attrs)
        resource = {'local_path': local_path}
        resource.update(kwargs)
        new_runtime_attrs['resources'][rtype].append(resource)
        self.update_runtime_attrs(new_runtime_attrs, comment='Add {} file'.format(local_path))

    def remove_resource(self, local_path):
        new_runtime_attrs = copy.deepcopy(self.runtime_attrs)
        for rt in {'url_files', 'sandbox_files', 'static_files', 'template_set_files'}:
            new_runtime_attrs['resources'][rt] = [r for r in self.runtime_attrs['resources'][rt] if r['local_path'] != local_path]

        return self.update_runtime_attrs(new_runtime_attrs, 'remove file {}'.format(local_path))

    def add_sandbox_file(self, local_path, sandbox_resource_info):
        return self._add_resource(local_path=local_path, rtype='sandbox_files', **sandbox_resource_info)

    def add_or_replace_sandbox_file(self, local_path, sandbox_resource_info):
        new_runtime_attrs = copy.deepcopy(self.runtime_attrs)
        for rt in {'url_files', 'sandbox_files', 'static_files', 'template_set_files'}:
            new_runtime_attrs['resources'][rt] = [r for r in self.runtime_attrs['resources'][rt] if r['local_path'] != local_path]

        resource = {'local_path': local_path}
        resource.update(**sandbox_resource_info)
        new_runtime_attrs['resources']['sandbox_files'].append(resource)
        return self.update_runtime_attrs(new_runtime_attrs, 'Set {} file'.format(local_path))

    def configure_stalled_deploy_monitoring(self, max_deploy_duration, calendar_id):
        def _configure_stalled_deploy_monitoring(j):
            result = copy.deepcopy(j)
            result['monitoring_settings']['deploy_monitoring'] = {
                'is_enabled': True,
                'content': {
                    'deploy_timeout': max_deploy_duration,
                    'alert_methods': ['sms'],
                    'responsible': {
                        'calendar_id': calendar_id
                    }
                }
            }
            return result
        return self.patch_service_info(_configure_stalled_deploy_monitoring, commit_comment='Configure stalled deploy monitoring with timeout = {}'.format(max_deploy_duration))

    def set_active_snapshot_state(self, snapshot_id, recipe='common', prepare_recipe=None, comment="Activating snapshot"):
        logging.debug('Activating snapshot %s for service %s', snapshot_id, self.name)
        post_data = {
            "type": "SET_TARGET_STATE",
            "content": {
                "is_enabled": True,
                "snapshot_id": str(snapshot_id),
                "comment": comment,
                "recipe": recipe,
            }

        }
        if prepare_recipe:
            post_data['content']['prepare_recipie'] = prepare_recipe

        event = self.NANNY.create_event(self.name, post_data)
        logging.debug("Created event %s", event)
        return event

    def activate_last_snapshot(self, recipe='common', prepare_recipe=None, comment="Activating snapshot"):
        current_snapshot_id = self._runtime_attrs['_id']
        self.set_active_snapshot_state(
            current_snapshot_id,
            recipe=recipe,
            prepare_recipe=prepare_recipe,
            comment=comment
        )

    @property
    def yp_pods(self):
        pods_by_cluster = {}
        for pod in self.runtime_attrs['instances']['yp_pod_ids']['pods']:
            cluster = pod['cluster']
            if cluster not in pods_by_cluster:
                pods_by_cluster[cluster] = []
            pods_by_cluster[cluster].append(pod['pod_id'])
        return pods_by_cluster

    @property
    def yp_clusters(self):
        return self.yp_pods.keys()

    def trim_yp_pods(self):
        assert self.is_yp_lite(), "Can trim YP pods only in YP.lite-allocated services"

        target_pods = self.yp_pods
        for cluster in target_pods.keys():
            req = pod_sets_api_pb2.ListPodsRequest()
            req.cluster = cluster
            req.service_id = self.name
            all_pods = self.podset_api.list_pods(req)
            for pod in all_pods.pods:
                if pod.meta.id not in target_pods[cluster]:
                    del_req = pod_sets_api_pb2.RemovePodRequest()
                    del_req.pod_id = pod.meta.id
                    del_req.cluster = cluster
                    # del_req.version = str(pod.meta.creation_time)
                    try:
                        self.podset_api.remove_pod(del_req)
                        logging.info("Removed pod %s from service %s in cluster %s", pod.meta.id, self.name, cluster)
                    except Exception as e:
                        logging.info("Could not delete pod %s from service %s in cluster %s", pod.meta.id, self.name, cluster)
                        logging.debug(e.message)

    def list_endpointsets(self):
        assert self.is_yp_lite(), "Can trim YP pods only in YP.lite-allocated services"

        result = {}

        clusters = self.yp_clusters
        for cluster in clusters:
            result[cluster] = []
            req = endpoint_sets_api_pb2.ListEndpointSetsRequest()
            req.cluster = cluster
            req.service_id = self.name

            for es in self.endpointset_api.list_endpoint_sets(req).endpoint_sets:
                result[cluster].append(es.meta.id)

        return result

    def create_endpointset(self, endpointset_name, cluster, protocol='tcp', port=80, description=None, pod_filter=''):
        assert self.is_yp_lite(), "Can create endpointsets only in YP.lite-allocated services"  # TODO: get rid of asserts

        cluster = cluster.upper()
        if not description:
            description = "Endpointset {endpointset_name} in cluster {cluster} on port {protocol}:{port} with filter '{pod_filter}' generated by some saas scripts".format(
                endpointset_name=endpointset_name,
                cluster=cluster,
                protocol=protocol,
                port=port,
                pod_filter=pod_filter
            )

        req = endpoint_sets_api_pb2.CreateEndpointSetRequest()
        req.meta.id = endpointset_name
        req.meta.service_id = self.name
        req.cluster = cluster
        req.spec.protocol = protocol
        req.spec.port = port
        req.spec.pod_filter = pod_filter
        req.spec.description = description

        res = self.endpointset_api.create_endpoint_set(req)

        return Endpointset.from_proto_msg(cluster, res.endpoint_set)

    def copy(self, new_service, nanny_category, description=None):

        if description is None:
            description = 'Copy service {}'.format(self.name)

        logging.info('Copying service {} to {}'.format(self.name, new_service))

        COPY_INSTANCES = False
        COPY_AUTH_ATTRS = True
        COPY_SECRETS = True

        copy_url = 'v2/services/{}/copies/'.format(self.name)

        req_data = {
            'id': new_service,
            'copy_auth_attrs': COPY_AUTH_ATTRS,
            'copy_instances': COPY_INSTANCES,
            'copy_secrets': COPY_SECRETS,
            'category': nanny_category,
            'desc': description,
        }

        result = self.NANNY._request_with_retries('POST', copy_url, data=req_data)

        logging.info('Copied service %s successfuly. You can find result at https://nanny.yandex-team.ru/ui/#/services/catalog/%s/', self.name, new_service)

        return NannyService(new_service)

    def _add_env_variable(self, section, variable, valuefrom):
        def _add_env_variable(runtime_attrs):
            for container in runtime_attrs['instance_spec']['containers']:
                if container['name'] == section:
                    env_is_set = False
                    for e in container['env']:
                        if e['name'] == variable:
                            e['valueFrom'] = valuefrom
                            env_is_set = True
                    if not env_is_set:
                        container['env'].append({
                            'name': variable,
                            'valueFrom': valuefrom
                })
            return runtime_attrs
        self.patch_runtime_attributes(_add_env_variable, commit_comment='Set variable {} in section {} to {}'.format(variable, section, valuefrom))

    def add_env_variable(self, section, variable, value):
        valuefrom = {'literalEnv': {'value': value}, 'type': 'LITERAL_ENV'}
        return self._add_env_variable(section, variable, valuefrom)

    def add_yav_secret(self, section, variable, secret_uuid, secret_version=None, field='', comment='Grant secret to nanny with some SaaS automatization scripts', yav_token=None):

        INSTANCECTL_TVM_ID = 2002924
        if not yav_token:
            yav_token=ServiceTokenStore.get_token('yav')
        yav = VaultClient(decode_files=True, authorization='OAuth {}'.format(yav_token))

        if not secret_version:
            secret = yav.get_secret(secret_uuid=secret_uuid)
            secret_version = [v for v in sorted(secret['secret_versions'], key=lambda x: x['created_at'], reverse=True) if not field or field in v['keys']][0]['version']

        delegation_token, _tid = yav.create_token(secret_uuid, tvm_client_id=INSTANCECTL_TVM_ID, signature=self.name, comment=comment)
        valuefrom = {
            "type": "VAULT_SECRET_ENV",
            "vaultSecretEnv": {
                "field": field,
                "vaultSecret": {
                    "secretId": secret_uuid,
                    "secretVer": secret_version,
                    "delegationToken": delegation_token,
                    "secretName": ''
                }
            }
        }
        return self._add_env_variable(section, variable, valuefrom)

    def set_infra_notifications(self, service_id, service_name, environment_id, environment_name):
        def _set_infra_notifications(info_attrs):
            if 'infra_notifications' not in info_attrs:
                info_attrs['infra_notifications'] = {}
            info_attrs['infra_notifications']['service_id'] = str(service_id)
            info_attrs['infra_notifications']['service_name'] = service_name
            info_attrs['infra_notifications']['environment_id'] = str(environment_id)
            info_attrs['infra_notifications']['environment_name'] = environment_name

            return info_attrs

        self.patch_service_info(_set_infra_notifications)


class NannyServiceIterator(object):
    def __init__(self, category='/', filter_=None):
        """
        :param category: Nanny service category (path). For example /saas/cloud/prod/
        :param filter_: Lambda (NannyService) -> bool
        """
        self._nanny = ServiceRepoClient('https://nanny.yandex-team.ru', token=ServiceTokenStore.get_token('nanny'))
        self._category = category
        self._filter = filter_
        self._page_size = 100
        self._skip = 0
        self._buffer = queue.Queue(maxsize=self._page_size * 2)

    def __iter__(self):
        return self

    def __next__(self):
        """
        :rtype: NannyService
        """
        while self._buffer.empty():
            params = dict(limit=self._page_size, skip=self._skip, category=self._category)
            services = [
                NannyService(name=s['_id'], runtime_attrs=s['runtime_attrs'], info_attrs=s['info_attrs'])
                for s in
                self._nanny._request_with_retries(method='GET', url='/v2/services/', params=params)['result']
            ]
            self._skip += len(services)
            if not services:
                raise StopIteration
            if self._filter:
                services = filter(self._filter, services)
            for service_ in services:
                self._buffer.put_nowait(service_)
        return self._buffer.get_nowait()

    def next(self):
        return self.__next__()


class NannyReleaseRunner(object):
    def __init__(self, nanny_release_id=None, sandbox_task_id=None, release_type=None, nanny_oauth_token=None):
        if not nanny_oauth_token:
            nanny_oauth_token = ServiceTokenStore.get_token('nanny')

        client = RequestsRpcClient('http://nanny.yandex-team.ru/api/tickets', request_timeout=3, oauth_token=nanny_oauth_token)
        self.tickets_stub = tickets_api_stub.TicketServiceStub(client)

        req = tickets_api_pb2.FindReleasesRequest()
        if sandbox_task_id:
            req.query.sandbox_task_id = sandbox_task_id
        if release_type:
            req.query.sandbox_release_type.extend([release_type])
        if nanny_release_id:
            req.query.id_in.values.extend([nanny_release_id])

        resp = self.tickets_stub.find_releases(req)
        assert len(resp.value) > 0, "Didn't match any release"
        assert len(resp.value) == 1, "Could not determine release request"
        self.nanny_release_id = resp.value[0].id

        self.tickets = self.obtain_release_tickets()

    def activate_ticket(self, ticket):

        req = tickets_api_pb2.CreateTicketEventRequest()
        req.ticket_id = ticket.id
        req.spec.type = 5  # ACTIVATE_SERVICE_SNAPSHOT
        req.spec.activate_snapshot.activate_recipe = 'common_noconfirm'

        nanny_service_id = ticket.spec.service_deployment.service_id
        ns = NannyService(nanny_service_id)
        ns.add_common_noconfirm_recipe()

        resp = self.tickets_stub.create_ticket_event(req)
        return resp

    def commit_ticket(self, ticket):
        req = tickets_api_pb2.CreateTicketEventRequest()
        req.ticket_id = ticket.id
        req.spec.type = 4
        req.spec.commit_release.scheduling_settings.priority = 2

        resp = self.tickets_stub.create_ticket_event(req)
        return resp

    def obtain_release_tickets(self):
        req = tickets_api_pb2.FindTicketsRequest()
        req.query.release_id = self.nanny_release_id
        return self.tickets_stub.find_tickets(req).value


def get_gencfg_groups(service_name):
    """
    Get service gencfg groups with releases
    :param service_name: Nanny service name
    :return: list of dicts with groups name in 'name' key and group release in 'release' key
    :rtype: List[Dict]
    """
    ns = NannyService(service_name)
    return ns.get_gencfg_groups()


def get_gencfg_groups_names(service_name):
    """
    Get unique names of service gencfg groups
    :param service_name: Nanny service name
    :rtype: List[str]
    """
    return list(set([g['name'] for g in get_gencfg_groups(service_name)]))
