# -*- coding: utf-8 -*-

import requests
import logging
import yaml

from saas.library.python.token_store import PersistentTokenStore
from saas.library.python.token_store.errors import PersistentTokenStoreError


def read_configuration(config_file):
    """
    Read configuration file in yaml format
    """
    try:
        with open(config_file) as cf:
            config = yaml.safe_load(''.join(cf.readlines()))
        return config
    except EnvironmentError:
        logging.critical('Cannot open/parse config file: %s', config_file)
        return {}


class StartrekAPI(object):
    """
    Startrek API wrapper for SaaS issues.
    Wiki: https://wiki.yandex-team.ru/tracker/api/
    """
    def __init__(self, queue='', config='conf/ssm.conf'):
        self.__STARTREK_API = 'https://st-api.yandex-team.ru/v2/issues'

        # Auth settings
        self.__AUTH = {}
        # Issue settings
        self.__ISSUE_FOLLOWERS = []
        self.__ISSUE_ASSIGNEE = 'saas-robot'
        self.__ISSUE_QUEUE = ''
        self.__ISSUE_TAGS = []
        try:
            self.__OAUTH_TOKEN = PersistentTokenStore.get_token_from_store_env_or_file('startrek')
        except PersistentTokenStoreError:
            logging.warning('Attention! ENV variable with auth token was not found!')
            self.__OAUTH_TOKEN = ''

        # Issues settings (owners, followers, tags etc.)
        self.__CONFIGURATION = read_configuration(config).get('startrek_settings')
        logging.debug(self.__CONFIGURATION)
        if type(self.__CONFIGURATION) is dict:
            self.__ISSUE_QUEUE = self.__CONFIGURATION.get('queue')
            self.__ISSUE_ASSIGNEE = self.__CONFIGURATION.get('assignee')
            self.__ISSUE_FOLLOWERS = self.__CONFIGURATION.get('followers')
            self.__ISSUE_TAGS = self.__CONFIGURATION.get('tags')
        if queue:
            self.__ISSUE_QUEUE = queue

        self.__HEADERS = {
            'Content-Type': 'application/json',
            'Authorization': 'OAuth %s' % self.__OAUTH_TOKEN
        }

        # Connection settings.
        self.__connection = requests.session()
        self.__connection.headers = self.__HEADERS

    def _create_issue(self, subject, body, assignee='', followers='', tags=''):
        """
        Create Startrek issue.
        :param subject: type str
        :param body: type str or dict
        :param assignee: type str
        :param followers: type str or list
        :param tags: type str or list
        :return: type requests.model.Response
        """
        if not assignee:
            assignee = self.__ISSUE_ASSIGNEE
        if not followers:
            followers = self.__ISSUE_FOLLOWERS
        if not tags:
            tags = self.__ISSUE_TAGS

        request_data = {'queue': self.__ISSUE_QUEUE,
                        'assignee': assignee,
                        'summary': subject,
                        'description': body,
                        'followers': followers,
                        'tags': tags
                        }
        logging.debug(request_data)
        return self.__connection.post(url=self.__STARTREK_API, json=request_data)

    def _modify_issue(self, issue, subject='', body='', tags='', created_by='', assignee='', followers='', comment=''):
        """
        Modify issue attrs
        :param issue: type str
        :param subject: type str
        :param body: type str or dict
        :param tags: type str or list
        :param created_by: type dict
        :param assignee: type str
        :param followers: type str or list or dict
        :param comment: type str
        :return: type requests.model.Response
        """
        request_data = {}
        if subject:
            request_data['summary'] = subject
        if body:
            request_data['description'] = body
        if tags:
            request_data['tags'] = tags
        if created_by:
            request_data['createdBy'] = created_by
        if assignee:
            request_data['assignee'] = assignee
        if followers:
            request_data['followers'] = followers
        if comment:
            request_data['comment'] = comment
        return self.__connection.patch(self.__STARTREK_API + '/' + issue, json=request_data)

    def _make_link(self, issue, link_issue, relationship='relates'):
        """
        Make remote links fot issues: https://wiki.yandex-team.ru/tracker/api/issues/links/create/
        :param issue: type str
        :param link_issue: type str
        :param relationship: type str
        :return: type requests.model.Response
        """
        relationships = ['relates', 'is dependent by', 'depends on', 'is subtask for', 'is parent task for',
                         'duplicates', 'is duplicated by', 'is epic of', 'has epic', 'original', 'clone']
        if relationship not in relationships:
            logging.error('Relationship %s is not supported', relationship)
            return
        request_data = {
            'relationship': relationship,
            'issue': link_issue,
        }
        return self.__connection.post(self.__STARTREK_API + '/' + issue + '/links', json=request_data)

    def _make_transition(self, issue, action, data=None):
        """
        Universal transition func: open, resolve, reopen issue etc.
        :param issue: type str
        :param action: type str
        :param data: type dict
        :return: type requests.model.Response or None
        """
        available_actions = self._get_transitions(issue)
        if action not in available_actions:
            logging.error('[%s] Issue was not supported action %s now', issue, action)
        else:
            logging.info('[%s] Change issue workflow status to %s', issue, action)
        if data:
            resp = self.__connection.post(self.__STARTREK_API + '/' + issue + '/transitions/' + action + '/_execute', json=data)
        else:
            resp = self.__connection.post(self.__STARTREK_API + '/' + issue + '/transitions/' + action + '/_execute')
        return resp

    def _resolve_issue(self, issue):
        """
        Resolve issue.
        :param issue: type str
        :return: type requests.model.Response
        """
        request_data = {
            'resolution': 'fixed'
        }
        return self._make_transition(issue, 'resolve', data=request_data)

    def _block_issue(self, issue):
        """
        Block issue.
        :param issue: type str
        :return: type requests.model.Response
        """
        return self._make_transition(issue, 'blocked')

    def _close_issue(self, issue):
        """
        Resolve issue.
        :param issue: type str
        :return: type requests.model.Response
        """
        request_data = {
            'resolution': 'fixed'
        }
        return self._make_transition(issue, 'close', data=request_data)

    def _reopen_issue(self, issue):
        """
        Reopen issue
        :param issue: type str
        :return: type requests.model.Response
        """
        return self._make_transition(issue, 'reopen')

    def _start_progress_issue(self, issue):
        """
        Reopen issue
        :param issue: type str
        :return: type requests.model.Response
        """
        return self._make_transition(issue, 'start_progress')

    def _add_comment(self, issue, text, summonees=None):
        """
        Adds comment. Also can summon users.
        :param issue: type str
        :param text: type str
        :param summonees: type str or list
        :return: type requests.model.Response
        """
        request_data = {
            'text': text,
            'summonees': summonees
        }
        return self.__connection.post(self.__STARTREK_API + '/' + issue + '/comments', json=request_data)

    def _get_comments(self, issue):
        """
        Get issue comments
        :param issue: type str

        :return: type requests.model.Response
        """
        return self.__connection.get(self.__STARTREK_API + '/' + issue + '/comments')

    def _get_issue_info(self, issue):
        """
        Get information about issue
        :param issue: type str
        :return: type requests.model.Response
        """
        return self.__connection.get(self.__STARTREK_API + '/' + issue).json()

    def _get_transitions(self, issue):
        """
        Get available transitions for issue.
        :param issue: type str
        :return: type list
        """
        transitions = []
        resp = self.__connection.get(self.__STARTREK_API + '/' + issue + '/transitions')
        if resp.status_code == 200:
            logging.info('[%s] Getting available transitions', issue)
            for transition in resp.json():
                transitions.append(transition['id'])
        else:
            logging.warning('[%s] Getting available transitions FAILED', issue)
        return transitions


class SaaSStartrekWorkflow(StartrekAPI):
    """
    Create and manage Startrek issues for SaaS service creation
    """
    def __init__(self, queue='SAASSUP'):
        super(SaaSStartrekWorkflow, self).__init__(queue=queue)
        self.issue = ''

    @staticmethod
    def _prepare_issue_body(service_name, service_ctype, service_type, instances_count=None, comment=None, other_data=None):
        issue_body = []
        issue_body_final = ''
        issue_body.append(('Имя сервиса', service_name))
        issue_body.append(('Тип сервиса', service_type))
        issue_body.append(('Cluster type (ctype) сервиса', service_ctype))
        if instances_count:
            issue_body.append(('Количество шардов', instances_count))
        if other_data:
            if 'service_tvm' in other_data.keys():
                issue_body.append(('Пользовательский tvm_id', other_data['service_tvm']))
            if 'service_saas_tvm' in other_data.keys():
                issue_body.append(('Разрешать команде SaaS ходить в сервис со своим tvm_id', other_data['service_saas_tvm']))
            if 'abc_user_service' in other_data.keys():
                issue_body.append(('ABC сервиса', other_data['abc_user_service']))
            if 'abc_quota_service' in other_data.keys():
                issue_body.append(('ABC сервиса - поставщика ресурсов', other_data['abc_quota_service']))
            if 'req_memory' in other_data.keys():
                issue_body.append(('Память на шард (Gb)', other_data['req_memory']))
            if 'req_cpu' in other_data.keys():
                issue_body.append(('Ядер CPU на шард', other_data['req_cpu']))
            if 'replicas_per_dc' in other_data.keys():
                issue_body.append(('Количество реплик на ДЦ', other_data['replicas_per_dc']))
            if 'allocation_type' in other_data.keys():
                issue_body.append(('Метод аллокации бекендов', other_data['allocation_type']))
            if 'delivery_type' in other_data.keys():
                issue_body.append(('Тип доставки данных', other_data['delivery_type']))
                if 'yt_cluster' in other_data.keys():
                    issue_body.append(('Кластер YT', other_data['yt_cluster']))
                if 'yt_ferryman_format' in other_data.keys():
                    issue_body.append(('Формат таблиц Ferryman', other_data['yt_ferryman_format']))
                if 'logbroker_tvm_id' in other_data.keys():
                    issue_body.append(('Пользовательский tvm_id', other_data['logbroker_tvm_id']))
                if 'logbroker_traffic_volume' in other_data.keys():
                    issue_body.append(('Трафик на индексацию (Мбайт/с)', other_data['logbroker_traffic_volume']))
            if 'search_rps' in other_data.keys():
                issue_body.append(('Поисковый RPS', other_data['search_rps']))
            if 'maxdocs' in other_data.keys():
                issue_body.append(('Количество документов (млн)', other_data['maxdocs']))
            if 'total_index_size_bytes' in other_data.keys():
                issue_body.append(('Объем данных (Gb)', other_data['total_index_size_bytes']))
            if 'search_q_99_ms' in other_data.keys():
                issue_body.append(('99 квантиль ответа (ms)', other_data['search_q_99_ms']))
            if 'search_q_999_ms' in other_data.keys():
                issue_body.append(('999 квантиль ответа (ms)', other_data['search_q_999_ms']))
            if 'owners' in other_data.keys():
                issue_body.append(('Список ответственных', ', '.join(other_data['owners'])))
        if comment:
            issue_body.append(('Комментарий', comment))
        for item in issue_body:
            issue_body_final += """
**%s**
%%%%
%s
%%%%

""" % (item[0], item[1])
        return issue_body_final

    def _prepare_namespace_body(self, namespace_name, ferryman_name, ferryman_ctype,
                                owners_list=None, namespace_size=None, namespace_doccount=None, comment=None):
        issue_body = []
        issue_body_final = ''
        issue_body.append(('Имя неймспейса', namespace_name))
        issue_body.append(('Комунальный сервис', ferryman_name))
        issue_body.append(('Cluster type (ctype) сервиса', ferryman_ctype))
        if owners_list:
            issue_body.append(('Список ответственных', ', '.join(owners_list)))
        if namespace_size:
            issue_body.append(('Размер неймспейса (Gb)', namespace_size))
        if namespace_doccount:
            issue_body.append(('Количество документов (Млн)', namespace_doccount))
        if comment:
            issue_body.append(('Комментарий', comment))

        for item in issue_body:
            issue_body_final += """
**%s**
%%%%
%s
%%%%

""" % (item[0], item[1])
        return issue_body_final

    def _collect_comments(self, issue_num):
        comments = []
        resp = self._get_comments(issue_num)
        if resp.status_code == 200:
            for comment in resp.json():
                comments.append((comment['createdBy']['id'], comment['text']))
        else:
            logging.warning('[%s] Getting comments list FAILED')
        return comments

    def _new_request(self, issue_subject, issue_body, tags='', followers='', assignee=''):
        """
        Create new issue backbone.
        :param issue_subject: type str
        :param issue_body: type str
        :param tags: type str or list
        :param followers: type str or list
        """
        logging.info('issue_tags: %s, issue_followers: %s', ','.join(tags), ','.join(followers))
        issue_resp = self._create_issue(issue_subject, issue_body, tags=tags, followers=followers, assignee=assignee)
        if issue_resp.status_code == 201:
            logging.info('New issue %s was created', issue_resp.json()['key'])
            self.issue = issue_resp.json()['key']
            return self.issue
        else:
            logging.error('Creating new issue FAILED')
            logging.error(issue_resp.json())

    def check_for_approve(self, issue_num, approve_word, approvers_list):
        """
        Check for approve by specified paramaters "approve_word" and "approvers_list".
        :param issue_num: type str
        :param approve_word: type str
        :param approvers_list: type list
        :return: type boolean
        """
        result = False
        comments = self._collect_comments(issue_num)
        for comment in comments:
            if approve_word in comment[1].lower() and comment[0] in approvers_list:
                result = True
        return result

    def new_service_request(self, service_name, service_ctype, service_type, instances_count, comment,
                            other_data=None, tags='new_service', followers='', assignee=''):
        """
        Create new issue with new service request. Request body depends of the specified paramaters.
        :param service_name: type str
        :param service_ctype: type str
        :param service_type: type str
        :param instances_count: type int
        :param comment: type str
        :param other_data: type dict
        :param tags: type str or list
        :param followers: type str or list
        :return: type str
        """
        issue_subj = '[%s] Новый сервис %s (%s) ' % (service_ctype, service_name, service_type)
        issue_body = self._prepare_issue_body(service_name, service_ctype, service_type, instances_count, comment,
                                              other_data=other_data)
        issue_body_patch = """
%%(wacko wrapper=text align=center)**Внимание! Если вам потребовалось изменить что-либо в параметрах заявки, просто напишите об этом в тикете. Не нужно править тело задачи.**%%
        """
        return self._new_request(issue_subj, issue_body_patch + issue_body, tags=tags, followers=followers, assignee=assignee)

    def new_namespace_request(self, namespace_name, ferryman_name, ferryman_ctype, comment, owners_list=None, namespace_size=None,
                              namespace_doccount=None, tags='new_namespace', followers='', assignee=''):
        """
        Create new issue with new namespace request. Request body depends of the specified paramaters.
        :param namespace_name: type str
        :param ferryman_name: type str
        :param ferryman_ctype: type str
        :param comment: type str
        :param owners_list: type str or list
        :param namespace_size: type int
        :param namespace_doccount: type int
        :param tags: type str or list
        :param followers: type str or list
        :return: type str
        """
        issue_subj = '[%s] Новый неймспейс %s' % (ferryman_name, namespace_name)
        issue_body = self._prepare_namespace_body(namespace_name, ferryman_name, ferryman_ctype,
                                                  owners_list=owners_list,
                                                  namespace_doccount=namespace_doccount,
                                                  namespace_size=namespace_size,
                                                  comment=comment)
        issue_body_patch = """
%%(wacko wrapper=text align=center)**Внимание! Если вам потребовалось изменить что-либо в параметрах заявки, просто напишите об этом в тикете. Не нужно править тело задачи.**%%
        """
        return self._new_request(issue_subj, issue_body_patch + issue_body, tags=tags, followers=followers, assignee=assignee)

    def new_perftest_request(self, service_name, service_ctype, service_type, tags='performance_test', followers=''):
        """
        Create a new issue with performance test request.
        :param service_name: type str
        :param service_ctype: type str
        :param service_type: type str
        :param tags: type str or list
        :param followers: type str or list
        :return:
        """
        issue_subj = '[%s] Произвести обстрел сервиса %s (%s)' % (service_ctype, service_name, service_type)
        issue_body = self._prepare_issue_body(service_name, service_ctype, service_type)
        issue_body_patch = """
Перед введением в эксплуатацию сервиса, необходимо провести нагрузочное тестирование.
https://wiki.yandex-team.ru/jandekspoisk/saas/Saas-FAQ/#strelbyvsaasservisy
        """
        issue_body = issue_body_patch + issue_body
        return self._new_request(issue_subj, issue_body, tags=tags, followers=followers)

    def new_security_request(self, service_name, service_ctype, service_type, tags='security_check', followers=''):
        """
        Create a new issue with tvm security check
        :param service_name: type str
        :param service_ctype: type str
        :param service_type: type str
        :param tags: type str or list
        :param followers: type str or list
        :return:
        """
        issue_subj = '[%s] TVM авторизация для сервиса %s (%s)' % (service_ctype, service_name, service_type)
        issue_body = self._prepare_issue_body(service_name, service_ctype, service_type)
        issue_body_patch = """
Для введения в эксплуатацию сервиса необходимо настроить TVM авторизацию или обосновать причину отказа от нее.

"""
        issue_body = issue_body_patch + issue_body
        return self._new_request(issue_subj, issue_body, tags=tags, followers=followers)

    def new_service_production_request(self, service_name, service_ctype, service_type, tags='', followers=''):
        """
        Create a new issue with tvm security check
        :param service_name: type str
        :param service_ctype: type str
        :param service_type: type str
        :param tags: type str or list
        :param followers: type str or list
        :return:
        """
        issue_subj = '[%s] Вывод сервиса %s в production (%s)' % (service_ctype, service_name, service_type)
        issue_body = self._prepare_issue_body(service_name, service_ctype, service_type)
        issue_body_patch = """
Для введения в эксплуатацию сервиса необходимо выполнить все связанные с данным тикетом задачи.

"""
        issue_body = issue_body_patch + issue_body
        return self._new_request(issue_subj, issue_body, tags=tags, followers=followers)

    def create_issue(self, **kwargs):
        resp = self._create_issue(**kwargs)
        if resp.status_code == 201:
            logging.info('[%s] New issue created', resp.__dict__)
        else:
            logging.error('[%s] creating issue FAILED', resp.__dict__)
        return resp

    def add_comment(self, issue_num, comment, summonees=None):
        resp = self._add_comment(issue_num, comment, summonees=summonees)
        if resp.status_code == 201:
            logging.info('[%s] New comment added', issue_num)
        else:
            logging.error('[%s] Adding comment FAILED', issue_num)
        return resp

    def add_to_followers(self, issue_num, followers, add=True):
        if add:
            resp = self._modify_issue(issue_num, followers={'add': followers})
        else:
            resp = self._modify_issue(issue_num, followers=followers)
        if resp.status_code == 200:
            logging.info('[%s] Followers were changed', issue_num)
        else:
            logging.error('[%s] Changing followers FAILED', issue_num)
            logging.error(resp.json())
        return resp

    def block_issue(self, issue_num):
        resp = self._block_issue(issue_num)
        if resp.status_code == 200:
            logging.info('[%s] Issue blocked', issue_num)
        else:
            logging.error('[%s] Blocking issue FAILED', issue_num)
        return resp

    def close_issue(self, issue_num):
        resp = self._close_issue(issue_num)
        if resp.status_code == 200:
            logging.info('[%s] Issue closed', issue_num)
        else:
            logging.error('[%s] Closing issue FAILED', issue_num)
            logging.error(resp.json())

        return resp

    def change_assignee(self, issue_num, assignee):
        resp = self._modify_issue(issue_num, assignee=assignee)
        if resp.status_code == 200:
            logging.info('[%s] Assignee changed to %s', issue_num, assignee)
        else:
            logging.error('[%s] Changing assignee FAILED', issue_num)
            logging.error(resp.json())
        return resp

    def change_tags(self, issue_num, tags):
        resp = self._modify_issue(issue_num, tags=tags)
        if resp.status_code == 200:
            logging.info('[%s] Tags changed', issue_num)
        else:
            logging.error('[%s] Changing tags FAILED', issue_num)
            logging.error(resp.json())
        return resp

    def resolve_issue(self, issue_num):
        resp = self._resolve_issue(issue_num)
        if resp.status_code == 200:
            logging.info('[%s] Issue resolved', issue_num)
        else:
            logging.error('[%s] Resolving issue FAILED', issue_num)
        return resp

    def reopen_issue(self, issue_num):
        resp = self._reopen_issue(issue_num)
        if resp.status_code == 200:
            logging.info('[%s] Issue re-opened', issue_num)
        else:
            logging.error('[%s] Try to reopen issue FAILED', issue_num)
        return resp

    def set_issue_author(self, issue_num, author):
        resp = self._modify_issue(issue_num, created_by=author)
        if resp.status_code == 200:
            logging.info('[%s] Issue author changed', issue_num)
        else:
            logging.error('[%s] Try to change author FAILED', issue_num)
        return resp

    def start_progress_issue(self, issue_num):
        resp = self._start_progress_issue(issue_num)
        if resp.status_code == 200:
            logging.info('[%s] Issue start progress', issue_num)
        else:
            logging.error('[%s] Try to start progress FAILED', issue_num)
        return resp

    def make_depends_link(self, issue_num, issue_link_num):
        resp = self._make_link(issue_num, issue_link_num, relationship='depends on')
        if resp and resp.status_code == 200:
            logging.info('[%s] Issue now depends on %s', issue_num, issue_link_num)
        else:
            logging.error('[%s] Try to make issue depends on %s was FAILED', issue_num, issue_link_num)

    def make_dependent_link(self, issue_num, issue_link_num):
        resp = self._make_link(issue_num, issue_link_num, relationship='is dependent by')
        if resp and resp.status_code == 200:
            logging.info('[%s] Issue now depends on %s', issue_num, issue_link_num)
        else:
            logging.error('[%s] Try to make issue dependent by %s was FAILED', issue_num, issue_link_num)
