# -*- coding: utf-8 -*-
'''
Connection module for Telemetry

.. versionadded:: 2016.3.0

https://github.com/mongolab/mongolab-telemetry-api-docs/blob/master/alerts.md

:configuration: This module accepts explicit telemetry credentials or
    can also read api key credentials from a pillar. More Information available
    here__.

.. __: https://github.com/mongolab/mongolab-telemetry-api-docs/blob/master/alerts.md

In the minion's config file:

.. code-block:: yaml

   telemetry.telemetry_api_keys:
     - abc123  # Key 1
     - efg321  # Backup Key 1
   telemetry_api_base_url: https://telemetry-api.mongolab.com/v0

:depends: requests

'''
# Import Python libs
from __future__ import absolute_import, unicode_literals, print_function
import logging

# Import Salt libs
import salt.utils.json
import salt.utils.stringutils

# Import 3rd-party libs
from salt.ext import six

log = logging.getLogger(__name__)

# Import third party libs
try:
    import requests
    HAS_REQUESTS = True
except ImportError:
    HAS_REQUESTS = False

__virtualname__ = 'telemetry'


def __virtual__():
    # Only load if imports exist.
    if not HAS_REQUESTS:
        return False
    return __virtualname__


def _get_telemetry_base(profile):
    config = __salt__['config.option'](profile)
    return config.get('telemetry_api_base_url')


def _auth(api_key=None, profile='telemetry'):
    # return telemetry api key in the header
    if api_key is None and profile is None:
        raise Exception("Missing api_key and profile")
    if profile:
        if isinstance(profile, six.string_types):
            _profile = __salt__['config.option'](profile)
        elif isinstance(profile, dict):
            _profile = profile

        if _profile:
            api_key = _profile.get('telemetry_api_keys')[0]
        else:
            raise Exception("Missing api_key")

    return {'Telemetry-API-Key': api_key, 'content-type': 'application/json'}


def _update_cache(deployment_id, metric_name, alert):
    key = "telemetry.{0}.alerts".format(deployment_id)

    if key in __context__:
        alerts = __context__[key]
        alerts[metric_name] = alert
        __context__[key] = alerts

    return __context__.get(key, [])


def _retrieve_channel_id(email, profile='telemetry'):
    '''
    Given an email address, checks the local
    cache if corresponding email address: channel_id
    mapping exists

    email
        Email escalation policy
    profile
        A dict of telemetry config information.
    '''
    key = "telemetry.channels"
    auth = _auth(profile=profile)

    if key not in __context__:
        get_url = _get_telemetry_base(profile) + "/notification-channels?_type=EmailNotificationChannel"
        response = requests.get(get_url, headers=auth)

        if response.status_code == 200:
            cache_result = {}
            for index, alert in enumerate(response.json()):
                cache_result[alert.get('email')] = alert.get('_id', 'false')

            __context__[key] = cache_result

    return __context__[key].get(email, False)


def get_alert_config(deployment_id, metric_name=None, api_key=None, profile="telemetry"):
    '''
    Get all alert definitions associated with a given deployment or if metric_name
    is specified, obtain the specific alert config

    Returns dictionary or list of dictionaries.

    CLI Example:

        salt myminion telemetry.get_alert_config rs-ds033197 currentConnections profile=telemetry
        salt myminion telemetry.get_alert_config rs-ds033197 profile=telemetry
    '''

    auth = _auth(profile=profile)
    alert = False

    key = "telemetry.{0}.alerts".format(deployment_id)

    if key not in __context__:
        try:
            get_url = _get_telemetry_base(profile) + "/alerts?deployment={0}".format(deployment_id)
            response = requests.get(get_url, headers=auth)
        except requests.exceptions.RequestException as e:
            log.error(six.text_type(e))
            return False

        http_result = {}
        if response.status_code == 200:
            for alert in response.json():
                http_result[alert.get('condition', {}).get('metric')] = alert
                __context__[key] = http_result

    if not __context__.get(key):
        return []

    alerts = __context__[key].values()

    if metric_name:
        return __context__[key].get(metric_name)

    return [alert['_id'] for alert in alerts if '_id' in alert]


def get_notification_channel_id(notify_channel, profile="telemetry"):
    '''
    Given an email address, creates a notification-channels
    if one is not found and also returns the corresponding
    notification channel id.

    notify_channel
        Email escalation policy
    profile
        A dict of telemetry config information.

    CLI Example:

        salt myminion telemetry.get_notification_channel_id userx@company.com profile=telemetry
    '''

    # This helper is used to procure the channel ids
    # used to notify when the alarm threshold is violated
    auth = _auth(profile=profile)

    notification_channel_id = _retrieve_channel_id(notify_channel)

    if not notification_channel_id:
        log.info("%s channel does not exist, creating.", notify_channel)

        # create the notification channel and cache the id
        post_url = _get_telemetry_base(profile) + "/notification-channels"
        data = {
            "_type": "EmailNotificationChannel",
            "name":  notify_channel[:notify_channel.find('@')] + 'EscalationPolicy',
            "email": notify_channel
        }
        response = requests.post(post_url, data=salt.utils.json.dumps(data), headers=auth)
        if response.status_code == 200:
            log.info("Successfully created EscalationPolicy %s with EmailNotificationChannel %s",
                     data.get('name'), notify_channel)
            notification_channel_id = response.json().get('_id')
            __context__["telemetry.channels"][notify_channel] = notification_channel_id
        else:
            raise Exception("Failed to created notification channel {0}".format(notify_channel))

    return notification_channel_id


def get_alarms(deployment_id, profile="telemetry"):
    '''
    get all the alarms set up against the current deployment

    Returns dictionary of alarm information

    CLI Example:

        salt myminion telemetry.get_alarms rs-ds033197 profile=telemetry

    '''
    auth = _auth(profile=profile)

    try:
        response = requests.get(_get_telemetry_base(profile) + "/alerts?deployment={0}".format(deployment_id), headers=auth)
    except requests.exceptions.RequestException as e:
        log.error(six.text_type(e))
        return False

    if response.status_code == 200:
        alarms = response.json()

        if len(alarms) > 0:
            return alarms

        return 'No alarms defined for deployment: {0}'.format(deployment_id)
    else:
        # Non 200 response, sent back the error response'
        return {'err_code': response.status_code, 'err_msg': salt.utils.json.loads(response.text).get('err', '')}


def create_alarm(deployment_id, metric_name, data, api_key=None, profile="telemetry"):
    '''
    create an telemetry alarms.

    data is a dict of alert configuration data.

    Returns (bool success, str message) tuple.

    CLI Example:

        salt myminion telemetry.create_alarm rs-ds033197 {} profile=telemetry

    '''

    auth = _auth(api_key, profile)
    request_uri = _get_telemetry_base(profile) + "/alerts"

    key = "telemetry.{0}.alerts".format(deployment_id)

    # set the notification channels if not already set
    post_body = {
        "deployment": deployment_id,
        "filter": data.get('filter'),
        "notificationChannel": get_notification_channel_id(data.get('escalate_to')).split(),
        "condition": {
            "metric": metric_name,
            "max": data.get('max'),
            "min": data.get('min')
        }
    }

    try:
        response = requests.post(request_uri, data=salt.utils.json.dumps(post_body), headers=auth)
    except requests.exceptions.RequestException as e:
        # TODO: May be we should retry?
        log.error(six.text_type(e))

    if response.status_code >= 200 and response.status_code < 300:
        # update cache
        log.info('Created alarm on metric: %s in deployment: %s', metric_name, deployment_id)
        log.debug('Updating cache for metric %s in deployment %s: %s',
                  metric_name, deployment_id, response.json())
        _update_cache(deployment_id, metric_name, response.json())
    else:
        log.error(
            'Failed to create alarm on metric: %s in '
            'deployment %s: payload: %s',
            metric_name, deployment_id, salt.utils.json.dumps(post_body)
        )

    return response.status_code >= 200 and response.status_code < 300, response.json()


def update_alarm(deployment_id, metric_name, data, api_key=None, profile="telemetry"):
    '''
    update an telemetry alarms. data is a dict of alert configuration data.

    Returns (bool success, str message) tuple.

    CLI Example:

        salt myminion telemetry.update_alarm rs-ds033197 {} profile=telemetry

    '''
    auth = _auth(api_key, profile)
    alert = get_alert_config(deployment_id, metric_name, api_key, profile)

    if not alert:
        return False, "No entity found matching deployment {0} and alarms {1}".format(deployment_id, metric_name)

    request_uri = _get_telemetry_base(profile) + '/alerts/' + alert['_id']

    # set the notification channels if not already set
    post_body = {
        "deployment": deployment_id,
        "filter": data.get('filter'),
        "notificationChannel": get_notification_channel_id(data.get('escalate_to')).split(),
        "condition": {
            "metric": metric_name,
            "max": data.get('max'),
            "min": data.get('min')
        }
    }

    try:
        response = requests.put(request_uri, data=salt.utils.json.dumps(post_body), headers=auth)
    except requests.exceptions.RequestException as e:
        log.error('Update failed: %s', e)
        return False, six.text_type(e)

    if response.status_code >= 200 and response.status_code < 300:
        # Also update cache
        log.debug('Updating cache for metric %s in deployment %s: %s',
                  metric_name, deployment_id, response.json())
        _update_cache(deployment_id, metric_name, response.json())
        log.info('Updated alarm on metric: %s in deployment: %s', metric_name, deployment_id)
        return True, response.json()

    err_msg = six.text_type(  # future lint: disable=blacklisted-function
        'Failed to create alarm on metric: {0} in deployment: {1} '
        'payload: {2}').format(
            salt.utils.stringutils.to_unicode(metric_name),
            salt.utils.stringutils.to_unicode(deployment_id),
            salt.utils.json.dumps(post_body)
        )
    log.error(err_msg)
    return False, err_msg


def delete_alarms(deployment_id, alert_id=None, metric_name=None, api_key=None, profile='telemetry'):
    '''delete an alert specified by alert_id or if not specified blows away all the alerts
         in the current deployment.

    Returns (bool success, str message) tuple.

    CLI Example:

        salt myminion telemetry.delete_alarms rs-ds033197 profile=telemetry

    '''
    auth = _auth(profile=profile)

    if alert_id is None:
        # Delete all the alarms associated with this deployment
        alert_ids = get_alert_config(deployment_id, api_key=api_key, profile=profile)
    else:
        alert_ids = [alert_id]

    if len(alert_ids) == 0:
        return False, "failed to find alert associated with deployment: {0}".format(deployment_id)

    failed_to_delete = []
    for id in alert_ids:
        delete_url = _get_telemetry_base(profile) + "/alerts/{0}".format(id)

        try:
            response = requests.delete(delete_url, headers=auth)
            if metric_name:
                log.debug("updating cache and delete %s key from %s",
                          metric_name, deployment_id)
                _update_cache(deployment_id, metric_name, None)

        except requests.exceptions.RequestException as e:
            log.error('Delete failed: %s', e)

        if response.status_code != 200:
            failed_to_delete.append(id)

    if len(failed_to_delete) > 0:
        return False, "Failed to delete {0} alarms in deployment: {1}" .format(', '.join(failed_to_delete), deployment_id)

    return True, "Successfully deleted {0} alerts in deployment: {1}".format(', '.join(alert_ids), deployment_id)
