# (C) Copyright 2016-2017 Hewlett Packard Enterprise Development LP
# Copyright 2017 Fujitsu LIMITED
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import os
import urllib

from debtcollector import removals
import jinja2
from oslo_config import cfg
import requests
import simplejson as json

from monasca_notification.plugins import abstract_notifier

CONF = cfg.CONF


class SlackNotifier(abstract_notifier.AbstractNotifier):
    """This module is a notification plugin to integrate with Slack.

    This plugin supports 2 types of APIs below.

    1st: Slack API
        notification.address = https://slack.com/api/chat.postMessage?token={token}&channel=#foobar

        You need to specify your token and channel name in the address.
        Regarding {token}, login to your slack account via browser and check the following page.
            https://api.slack.com/docs/oauth-test-tokens

    2nd: Incoming webhook
        notification.address = https://hooks.slack.com/services/foo/bar/buz

        You need to get the Incoming webhook URL.
        Login to your slack account via browser and check the following page.
            https://my.slack.com/services/new/incoming-webhook/
        Slack document about incoming webhook:
            https://api.slack.com/incoming-webhooks
    """

    type = 'slack'

    MAX_CACHE_SIZE = 100
    RESPONSE_OK = 'ok'

    _raw_data_url_caches = []

    def __init__(self, log):
        super(SlackNotifier, self).__init__()
        self._log = log

    @removals.remove(
        message='Configuration of notifier is available through oslo.cfg',
        version='1.9.0',
        removal_version='3.0.0'
    )
    def config(self, config_dict):
        pass

    @property
    def statsd_name(self):
        return 'sent_slack_count'

    def _build_slack_message(self, notification):
        """Builds slack message body
        """
        body = {'alarm_id': notification.alarm_id,
                'alarm_definition_id': notification.raw_alarm['alarmDefinitionId'],
                'alarm_name': notification.alarm_name,
                'alarm_description': notification.raw_alarm['alarmDescription'],
                'alarm_timestamp': notification.alarm_timestamp,
                'state': notification.state,
                'old_state': notification.raw_alarm['oldState'],
                'message': notification.message,
                'tenant_id': notification.tenant_id,
                'metrics': notification.metrics}

        slack_request = {}
        if CONF.slack_notifier.message_template:
            slack_request['text'] = self._render_message_template(body)
        else:
            slack_request['text'] = json.dumps(body, indent=3)

        return slack_request

    def _render_message_template(self, params):
        path, name = os.path.split(CONF.slack_notifier.message_template)
        loader = jinja2.FileSystemLoader(path)
        env = jinja2.Environment(loader=loader, autoescape=True)
        return env.get_template(name).render(params)

    def _check_response(self, result):
        if 'application/json' in result.headers.get('Content-Type'):
            response = result.json()
            if response.get(self.RESPONSE_OK):
                return True
            else:
                self._log.error('Received an error message when trying to send to slack. error={}'
                                .format(response.get('error')))
                return False
        elif self.RESPONSE_OK == result.text:
            return True
        else:
            self._log.error('Received an error message when trying to send to slack. error={}'
                            .format(result.text))
            return False

    def _send_message(self, request_options):
        try:
            url = request_options.get('url')
            result = requests.post(**request_options)
            if result.status_code not in range(200, 300):
                self._log.error('Received an HTTP code {} when trying to post on URL {}.'
                                .format(result.status_code, url))
                return False

            # Slack returns 200 ok even if the token is invalid. Response has valid error message
            if self._check_response(result):
                self._log.info('Notification successfully posted.')
                return True

            self._log.error('Failed to send to slack on URL {}.'.format(url))
            return False
        except Exception as err:
            self._log.exception('Error trying to send to slack on URL {}. Detail: {}'
                                .format(url, err))
            return False

    def send_notification(self, notification):
        """Send the notification via slack
            Posts on the given url
        """

        slack_message = self._build_slack_message(notification)

        address = notification.address
        #  '#' is reserved character and replace it with ascii equivalent
        #  Slack room has '#' as first character
        address = address.replace('#', '%23')

        parsed_url = urllib.parse.urlsplit(address)
        query_params = urllib.parse.parse_qs(parsed_url.query)
        # URL without query params
        url = urllib.parse.urljoin(address, urllib.parse.urlparse(address).path)

        # Default option is to do cert verification
        # If ca_certs is specified, do cert validation and ignore insecure flag
        verify = CONF.slack_notifier.ca_certs or not CONF.slack_notifier.insecure

        proxy = CONF.slack_notifier.proxy
        proxy_dict = None
        if proxy is not None:
            proxy_dict = {'https': proxy}

        data_format_list = ['json', 'data']
        if url in SlackNotifier._raw_data_url_caches:
            data_format_list = ['data']

        for data_format in data_format_list:
            self._log.info('Trying to send message to {} as {}'
                           .format(url, data_format))
            request_options = {
                'url': url,
                'verify': verify,
                'params': query_params,
                'proxies': proxy_dict,
                'timeout': CONF.slack_notifier.timeout,
                data_format: slack_message
            }
            if self._send_message(request_options):
                if (data_format == 'data' and
                        url not in SlackNotifier._raw_data_url_caches and
                        len(SlackNotifier._raw_data_url_caches) < self.MAX_CACHE_SIZE):
                    # NOTE:
                    #    There are a few URLs which can accept only raw data, so
                    #    only the URLs with raw data are kept in the cache. When
                    #    too many URLs exists, it can be considered malicious
                    #    user registers them.
                    #    In this case, older ones should be safer than newer
                    #    ones. When exceeding the cache size, do not replace the
                    #    the old cache with the newer one.
                    SlackNotifier._raw_data_url_caches.append(url)
                return True

            self._log.info('Failed to send message to {} as {}'
                           .format(url, data_format))
        return False


slack_notifier_group = cfg.OptGroup(name='%s_notifier' % SlackNotifier.type)
slack_notifier_opts = [
    cfg.IntOpt(name='timeout', default=5, min=1),
    cfg.BoolOpt(name='insecure', default=True),
    cfg.StrOpt(name='ca_certs', default=None),
    cfg.StrOpt(name='proxy', default=None),
    cfg.StrOpt(name='message_template', default=None)
]


def register_opts(conf):
    conf.register_group(slack_notifier_group)
    conf.register_opts(slack_notifier_opts, group=slack_notifier_group)


def list_opts():
    return {
        slack_notifier_group: slack_notifier_opts
    }
