#!/usr/bin/env python
# -*- coding: utf-8 -*-
import jinja2
import requests
import os
import sys
import logging
import argparse

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

from saas.tools.ssm.SaaSServiceManager import SaaSServiceManager
from saas.tools.ssm.modules.nanny_api import SaaSNannyService
from saas.tools.ssm.modules.nanny_yp_api import SaaSNannyYpWorkflow, NannyReplicationPolicy
from saas.tools.ssm.modules.misc import load_and_write_resources, get_ferryman_name
from saas.tools.ssm.modules.sandbox_ferryman import FerrymanConfigRequest
from saas.library.python.nanny_rest.resource import SandboxFile
from saas.library.python.awacs.namespace import NamespaceManager


class FerrymanSolomon:
    """
    Class for adding ferryman services to Solomon.
    """
    def __init__(self, service_name, service_cluster='prestable'):
        # API settings
        self._solomon_project_api = 'http://solomon.yandex.net/api/v2/projects/saas_ferryman'

        # Basic service settings
        self._service_name = service_name
        self._service_name_id = get_ferryman_name(self._service_name)
        self._service_cluster = 'saas_ferryman_' + service_cluster
        self._service_ctype = service_cluster if service_cluster not in ['stable', 'middle_kv'] else 'saas_' + service_cluster
        self._service_shard_id = self._service_cluster + '_' + service_name

        # Auth settings
        self._OAUTH_TOKEN = ServiceTokenStore.get_token('solomon')

        # Connection settings.
        self._HEADERS = {
            'Content-Type': 'application/json',
            'Accept': 'application/json',
            'Authorization': 'OAuth %s' % self._OAUTH_TOKEN
        }
        self._connection = requests.session()
        self._connection.headers = self._HEADERS

    def add_service_to_project(self):
        request_url = self._solomon_project_api + '/services'
        request_data = {
            'id': self._service_name_id,
            'type': 'JSON_GENERIC',
            'name': self._service_name,
            'interval': 15
        }
        resp = self._connection.post(request_url, json=request_data)
        if resp.status_code != 200:
            logging.error(resp.text)
        return resp

    def add_service_to_shard(self):
        request_url = self._solomon_project_api + '/shards'
        request_data = {
            'id': self._service_shard_id,
            'projectId': 'saas_ferryman',
            'clusterId': self._service_cluster,
            'serviceId': self._service_name_id
        }
        resp = self._connection.post(request_url, json=request_data)
        if resp.status_code != 200:
            logging.error(resp.text)
        return resp

    def add_update_time_alert(self):
        request_url = self._solomon_project_api + '/alerts'
        request_data = {
            'id': 'saas_ferryman-' + self._service_name + '-update_time',
            'projectId': 'saas_ferryman',
            'name': self._service_name + ' - update time',
            'version': 1,
            'notificationChannels': [
                'saas-alerts-email'
            ],
            'type': {
                'expression': {
                    'program': "let items = drop_below({\n  cluster='%s',\n  service='%s',\n  sensor='FullStateAge'\n}, 1);\n"
                               "\n// traffic lights\nlet isRed = count(items) == 0;\n"
                               "let isYellow = count(items) > 0 && count(tail(items, 12h)) == 0;\n"
                               "let trafficColor = isRed ? 'red' : (isYellow ? 'yellow' : 'green');\n"
                               "let trafficValue = last(items);" % (self._service_ctype, self._service_name),
                    'checkExpression': 'isRed'
                }
            },
            'annotations': {
                'trafficLight.color': '{{expression.trafficColor}}',
                'trafficLight.value': '{{expression.trafficValue}}',
                'details': '%s data haven\'t been updated for more than 24 hours. '
                           'Please check if the process is alive:\n'
                           'https://nanny.yandex-team.ru/ui/#/services/catalog/saas_fm_%s/' % (self._service_name, self._service_name),
                'service':  self._service_ctype,
                'tags': 'saas_ferryman_' + self._service_ctype,
                'host': self._service_name
            },
            'periodMillis': 86400000,
            'delaySeconds': 0
        }
        resp = self._connection.post(request_url, json=request_data)
        if resp.status_code != 200:
            logging.error(resp.text)
        return resp


class FerrymanNanny(SaaSNannyService):
    """
    Class for SaaS Ferryman nanny services
    """
    def __init__(self, service_name, yt_cluster, row_processor, service_ctype, input_size,
                 service_type='ferryman', ferryman_type='ytpull', debug=False, old_names_format=False, specific_nanny_name=None):
        # Compare ferryman service name
        self._service_name_basic = self._get_service_name_basic(service_name, old_names_format)
        if specific_nanny_name:
            self._service_name = specific_nanny_name
        else:
            self._service_name = get_ferryman_name(self._service_name_basic, service_ctype=service_ctype, old_names_format=old_names_format)
        super(FerrymanNanny, self).__init__(self._service_name, config='conf/ferryman.conf', service_type='ferryman',
                                            service_ctype=service_ctype, debug=debug)
        # Basic parameters
        self._abc_group = 2115  # Saas Ferryman abc group
        self._service_ctype = service_ctype
        self._service_balancer_name = self._get_service_balancer_name()
        self._service_type = service_type
        self._templates_dir = 'tmpl/ferryman'
        self._ferryman_type = ferryman_type
        self._yp_network_macros = '_SAAS_FERRYMAN_NETS_'

        self._config_name = 'config.pb.txt'
        # Config file variables
        self._var_yt_cluster = yt_cluster
        self._var_row_processor = row_processor
        if ferryman_type == 'logbroker_backup':
            self._var_allowed_input_size = 100000000
        else:
            self._var_allowed_input_size = input_size
        if ferryman_type in ['ytpull', 'logbroker_backup']:
            self._var_max_states_to_keep = 20
        else:
            self._var_max_states_to_keep = 3

        self._var_fullstate_cycle = 1

        self._var_suffix = self._service_ctype.replace('_', '-')

        if ferryman_type == 'logbroker_backup':
            self._var_namespace_validator = 'KeyPrefixZero'
        else:
            if 'kv' in self._service_type:
                self._var_namespace_validator = 'KeyPrefixAll'
            else:
                self._var_namespace_validator = 'KeyPrefixNonZero'

        if self._service_ctype == 'stable':
            self._var_solomon_ctype = 'saas_stable'
        elif self._service_ctype == 'stable_middle_kv':
            self._var_solomon_ctype = 'saas_middle_kv'
        else:
            self._var_solomon_ctype = self._service_ctype

        self._template_variables = {
            'ctype': self._service_ctype,
            'service': self._service_name_basic,
            'suffix': self._var_suffix,
            'solomon_ctype': self._var_solomon_ctype,
            'yt_cluster': self._var_yt_cluster,
            'max_states_to_keep': self._var_max_states_to_keep,
            'namespace_validator': self._var_namespace_validator,
            'fullstate_cycle': self._var_fullstate_cycle,
            'row_processor': self._var_row_processor,
            'allowed_input_size': self._var_allowed_input_size,
            'ferryman_type': self._ferryman_type
        }

        # Balancer parameters
        self.endpointset_for_balancer = "common_{}_endp".format(self._service_name)

    @staticmethod
    def _get_service_name_basic(service, old_names_format=False):
        if old_names_format:
            return service.replace('-', '_')
        return service

    def _get_service_balancer_name(self):
        prefix = self._service_name_basic
        if 'prestable' in self._service_ctype:
            prefix += '_p'
        if 'hamster' in self._service_ctype:
            prefix += '_h'
        return '%s.ferryman.n.yandex-team.ru' % prefix.replace('_', '-')#'%s.f.saas.yandex.net' % prefix.replace('_', '-')

    def _load_templates(self):
        """
        Load  templates from filesystem
        :return: type dict
        """
        loader = jinja2.FileSystemLoader(self._templates_dir)
        templates = jinja2.Environment(loader=loader)
        return templates

    def add_service_to_solomon(self):
        solomon_client = FerrymanSolomon(self._service_name_basic, service_cluster=self._service_ctype)
        solomon_client.add_service_to_project()
        solomon_client.add_service_to_shard()
        if 'prestable' not in self._service_ctype:
            solomon_client.add_update_time_alert()

    def add_resources_from_templates(self):
        templates = self._load_templates()
        for template_name in templates.list_templates():
            if template_name != self._config_name:
                template_data = templates.get_template(template_name).render(self._template_variables)
                self.add_resource_from_string(template_name, template_data)

    @property
    def dc_from_yt_cluster(self):
        return "sas" if self._var_yt_cluster == 'hahn' else "vla"

    def render_config(self):
        templates = self._load_templates()
        if self._config_name in templates.list_templates():
            return templates.get_template(self._config_name).render(self._template_variables)
        else:
            raise RuntimeError('{} was not found in templates'.format(self._config_name))

    def allocate_yp_pods(self):
        yp_client = SaaSNannyYpWorkflow(self._service_name, self._abc_group, flexible_quota=True, token=self._OAUTH_TOKEN,
                                        log_volume_size=2048)
        if self._var_yt_cluster == 'hahn':
            location = 'SAS'
        else:
            location = 'VLA'
        # Add volume gear for ferryman with standalone_indexer
        yp_client.add_persistent_volume('/gear', 'hdd', 3000)
        yp_pods = yp_client.allocate_pods(500, 500, 1024, self._yp_network_macros,
                                          instances_count=2, locations=[location], workdir_capacity=1024,
                                          rack_max_pods=1, snapshot_count=5, node_segment_id='default')
        return yp_pods

    def activate_configuration(self):
        self.configure_yp_instances(tags={'metaprj': 'dynamic',
                                          'itype': 'saasferryman',
                                          'ctype': 'prod' if 'stable' in self._service_ctype else 'prestable'})
        self.snapshot_activate(recipe='default', activation_wait=True)

    def prepare_environment(self):
        if 'prestable' not in self._service_ctype:
            if self._ferryman_type == 'logbroker_backup':
                self.add_env_variable('ferryman_server',  'YT_POOL', 'saas-lb-backup')
                self.add_env_variable('ferryman_server',  'FERRYMAN_COOKER_YT_POOL', 'saas-lb-backup')
            else:
                self.add_env_variable('ferryman_server',  'YT_POOL', 'saas-ferryman')
                self.add_env_variable('ferryman_server',  'FERRYMAN_COOKER_YT_POOL', 'saas-cook')

    def add_ferryman_config(self):
        task_id, resource_id = FerrymanConfigRequest.add_ferryman_config(
            self._service_name_basic, self._service_ctype, self._var_yt_cluster, self._var_allowed_input_size,
            self._ferryman_type, self._var_row_processor, self._service_type
        )

        new_resource = SandboxFile(
            self._config_name,
            'BUILD_FERRYMAN_CONFIGS',
            str(task_id),
            'FERRYMAN_CONFIG',
            str(resource_id)
        )
        comment = 'Added ferryman config {config_name} '.format(config_name=self._config_name)

        with self._get_nanny_rest_service().runtime_attrs_transaction(comment) as nanny_service:
            nanny_service.set_resource(new_resource)

    def _get_ti_filter_expression(self, rule_release_type, rule_task_type):
        filter_expression = super(FerrymanNanny, self)._get_ti_filter_expression(rule_release_type, rule_task_type)
        if rule_task_type == 'BUILD_FERRYMAN_CONFIGS':
            filter_expression += ' and sandbox_release.title == "Ferryman config for {ctype}/{service}"'.format(
                ctype=self._service_ctype, service=self._service_name_basic
            )
        return filter_expression

    def set_up_balancer(self):
        ns_manager = NamespaceManager()
        if not ns_manager.backend_manager.get_backend("ferryman", self._service_name):
            ns_manager.create_ferryman_backend(ferryman_nanny=self._service_name,
                                               endpoint_set_id=self.endpointset_for_balancer,
                                               ferryman_dc="sas" if self._var_yt_cluster == 'hahn' else "vla")
        else:
            logging.warning("Backend {} is already exist in ferryman namespace".format(self._service_name))
        if not ns_manager.upstream_manager.get_upstream("ferryman", self._service_name):
            ns_manager.create_ferryman_upstream(ferryman_nanny=self._service_name,
                                                ferryman_dc="sas" if self._var_yt_cluster == 'hahn' else "vla")
        else:
            logging.warning("Upstream {} is already exist in ferryman namespace".format(self._service_name))

    def create_service(self):
        """
        Create new service with configuration based on service attrs.
        """
        service_data = {
            'id': self._service_name,
            'info_attrs': {
                'category': self._service_path,
                'desc': 'New ferryman service named {name}'.format(name=self._service_name)
            }
        }
        logging.info('Creating service %s', self._service_name)
        self._create_service(service_data)
        # Auth settings
        logging.info('Updating auth settings')
        self.update_auth_settings()
        # ABC group
        logging.info('Updating abc owner group')
        self.update_abc_group(self._abc_group)
        # Resources
        logging.info('Updating service resources')
        self.update_service_resources()
        # Static resources
        logging.info('Updating resources from templates')
        self.add_resources_from_templates()
        # Ferryman configuration
        logging.info('Render and add ferryman config to svn and nanny service')
        self.add_ferryman_config()
        # Keychains and secrets
        logging.info('Configuting secrets')
        self.configure_secrets()
        self.configure_secret_volumes()
        NannyService(self._service_name).add_yav_secret('ferryman_server', 'SOLOMON_TOKEN', 'sec-01d74p02xrkrwa8m8nc26f22ta', secret_version=None, field='solomon_token')
        # Environment
        logging.info('Preparing environment')
        self.prepare_environment()
        # Isolation
        logging.info('Preparing isolation')
        self.prepare_isolation(skynet_enable=True, ssh_enable=True, logrotate_enable=True, coredump_enable=False)
        # Tickets integration
        logging.info('Configuring tickets integration')
        self.update_tickets_integration()
        # Allocate pods
        logging.info('Allocating YP pods')
        self.allocate_yp_pods()
        # Add service to solomon
        logging.info('Adding service to Solomon')
        self.add_service_to_solomon()
        # Attach yp pods to service
        logging.info('Activate configuration')
        self.activate_configuration()

        # Create replication policy
        logging.info('Configuring replication policy')
        replication_client = NannyReplicationPolicy(self._service_name, token=self._OAUTH_TOKEN)
        replication_client.update_policy(enable_replication=True, wait_duration=3600,
                                         snapshot_priority='NORMAL', max_unavailable=1, replication_method='REPLACE')

        # Create service_balancer
        logging.info('Creating service balancer: %s ', self._service_balancer_name)
        self.manage_balancer_autoupdate(True)
        self.create_service_balancer(self._service_balancer_name)

        # It's creator of separated balancer ferryman upstream and backend, please, apply this if SAAS-6295 is closed
        ''''# Create endpoint set
        logging.info('Creating nanny service endpoint set')
        if not Endpointset.get_endpointset(self.endpointset_for_balancer, self.dc_from_yt_cluster.upper()):
            NannyService(self._service_name).create_endpointset(
                endpointset_name=self.endpointset_for_balancer, cluster=self.dc_from_yt_cluster,
                pod_filter="", protocol='tcp', port=80, description='All instances')

        # Set up service_balancer
        logging.info('Creating service backend')
        self.set_up_balancer()'''

        logging.info('DONE')
        return self._service_balancer_name

    def remove_ferryman(self):
        """
        Remove Ferryman service with next steps:
        1) Remove all snapshots
        2) Remove yp pors
        3) Remove nanny service
        :return:
        """
        ns_manager = NamespaceManager()
        if ns_manager.backend_manager.get_backend("ferryman", self._service_name):
            ns_manager.backend_manager.remove_backend("ferryman", self._service_name)
        if ns_manager.upstream_manager.get_upstream("ferryman", self._service_name):
            ns_manager.upstream_manager.remove_upstream("ferryman", self._service_name)
        ns_manager.wait_for_active_state("ferryman")
        Endpointset.remove_endpointset(self.endpointset_for_balancer, self.dc_from_yt_cluster.upper())
        self.remove_service()
        logging.info('Ferryman is removed')


def prepare_logging(options):
    """
    Prepare logging settings
    :param options: argparse.Namespace
    """
    basedir = os.getcwd()
    logdir = '%s/logs' % basedir
    if not os.path.exists(logdir):
        os.makedirs(logdir)

    if options.debug:
        level = logging.DEBUG
    else:
        level = logging.INFO

    logging.basicConfig(
        level=level,
        format='%(asctime)s | %(filename)18s#%(lineno)s | %(levelname)-5s | %(message)s',
        datefmt='%H:%M:%S',
        filename='logs/ferryman.log')

    console = logging.StreamHandler()
    console.addFilter(logging.Filter(name='root'))
    console.setFormatter(logging.Formatter('[%(module)18s][%(levelname)-5s] %(message)-20s '))
    console.setLevel(level)
    logging.getLogger('').addHandler(console)


def parse_args(*args):
    options = argparse.ArgumentParser(prog='ssm_ferryman',
                                      formatter_class=argparse.RawTextHelpFormatter,
                                      description="""
DESCRIPTION
Tool for SaaS Ferryman creation.
Ferryman documentation: https://wiki.yandex-team.ru/jandekspoisk/SaaS/Ferryman/
DevOps documentation: https://wiki.yandex-team.ru/jandekspoisk/saas/ferryman/devopsing/

Requires tokens in environment variables:
 - NANNY_OAUTH_TOKEN (https://nanny.yandex-team.ru/ui/#/oauth/)
 - SOLOMON_OAUTH (https://solomon.yandex-team.ru/api/internal/auth)
 - OAUTH_SANDBOX (https://sandbox.yandex-team.ru/oauth/token)

Example:
ssm_ferryman --ctype prestable --yt-cluster arnold --type ytpull --row-processor json --input-size 1000000 --service_type search test_service

""")

    options.add_argument('service_name', type=str,
                         help='Specifies service name for ferryman creation')
    options.add_argument('--ctype', dest='service_ctype', required=True, choices=SaaSServiceManager.ctype_tags.keys(),
                         help='Sets ctype of service')
    options.add_argument('--yt-cluster', dest='yt_cluster', default='arnold', choices=['hahn', 'arnold'],
                         help='Specifies Yt cluster')
    options.add_argument('--row-processor', dest='row_processor', required='--remove' not in sys.argv,
                         help='Specifies row_processor')
    options.add_argument('--input-size', dest='input_size', type=int, required='--remove' not in sys.argv,
                         help='Specifies allowed input size')
    options.add_argument('--type', dest='ferryman_type', required='--remove' not in sys.argv, choices=['ytpull', 'snapshot',
                                                                                                       'delta', 'logbroker_backup'],
                         help='Specifies ferryman type. Possible values: ytpull, snapshot, logbroker_backup')
    options.add_argument('--service_type', dest='service_type', choices=['kv', 'search', 'dj', 'knn'], required='--remove' not in sys.argv,
                         help='Set one of saas service types: kv, search, dj, knn')
    options.add_argument('--debug', dest='debug', action='store_true', default=False,
                         help='Enable debug mode with additional information')
    options.add_argument('--old-names-format', dest='old_names_format', action='store_true', default=False,
                         help='Use old format of names in ferryman service: use SERVICE_NAME with replaced "-" -> "_" '
                              'and nanny service name saas_ferryman_SERVICE_NAME')
    options.add_argument('--remove', dest='remove_ferryman', action='store_true', default=False,
                         help='Remove ferryman nanny service and backends')
    options.add_argument('--specific-nanny-name', dest='specific_nanny_name',
                         help='use this variable if nanny name of ferryman is very specific')

    if args:
        opts = options.parse_args(args)
    else:
        opts = options.parse_args()
    return opts


def main(*args):
    options = parse_args(*args)
    # Logging
    prepare_logging(options)

    # Load resources (required with arcadia building)
    load_and_write_resources('/tmpl/ferryman')
    load_and_write_resources('/conf')

    ferryman_client = FerrymanNanny(options.service_name, options.yt_cluster,
                                    options.row_processor, options.service_ctype,
                                    options.input_size,
                                    specific_nanny_name=options.specific_nanny_name,
                                    service_type=options.service_type,
                                    ferryman_type=options.ferryman_type,
                                    old_names_format=options.old_names_format)
    # Cases
    if options.remove_ferryman:
        ferryman_client.remove_ferryman()
    else:
        ferryman_client.create_service()


if __name__ == '__main__':
    main()
