# Copyright 2014-2017 Hewlett Packard Enterprise Development LP
# Copyright 2018 OP5 AB
#
# 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 re

import falcon
from monasca_common.simport import simport
from oslo_config import cfg
from oslo_log import log
import six

from monasca_api.api import alarms_api_v2
from monasca_api.common.repositories import exceptions
from monasca_api.v2.common.exceptions import HTTPUnprocessableEntityError
from monasca_api.v2.common.schemas import alarm_update_schema as schema_alarm
from monasca_api.v2.common import validation
from monasca_api.v2.reference import alarming
from monasca_api.v2.reference import helpers
from monasca_api.v2.reference import resource

LOG = log.getLogger(__name__)


class Alarms(alarms_api_v2.AlarmsV2API,
             alarming.Alarming):
    def __init__(self):
        try:
            super(Alarms, self).__init__()
            self._region = cfg.CONF.region
            self._alarms_repo = simport.load(
                cfg.CONF.repositories.alarms_driver)()

        except Exception as ex:
            LOG.exception(ex)
            raise exceptions.RepositoryException(ex)

    @resource.resource_try_catch_block
    def on_put(self, req, res, alarm_id):

        helpers.validate_authorization(req, ['api:alarms:put'])

        alarm = helpers.from_json(req)
        schema_alarm.validate(alarm)

        # Validator makes state optional, so check it here
        if 'state' not in alarm or not alarm['state']:
            raise HTTPUnprocessableEntityError('Unprocessable Entity',
                                               "Field 'state' is required")
        if 'lifecycle_state' not in alarm or not alarm['lifecycle_state']:
            raise HTTPUnprocessableEntityError('Unprocessable Entity',
                                               "Field 'lifecycle_state' is required")
        if 'link' not in alarm or not alarm['link']:
            raise HTTPUnprocessableEntityError('Unprocessable Entity',
                                               "Field 'link' is required")

        self._alarm_update(req.project_id, alarm_id, alarm['state'],
                           alarm['lifecycle_state'], alarm['link'])

        result = self._alarm_show(req.uri, req.project_id, alarm_id)

        res.body = helpers.to_json(result)
        res.status = falcon.HTTP_200

    @resource.resource_try_catch_block
    def on_patch(self, req, res, alarm_id):

        helpers.validate_authorization(req, ['api:alarms:patch'])

        alarm = helpers.from_json(req)
        schema_alarm.validate(alarm)

        old_alarm = self._alarms_repo.get_alarm(req.project_id, alarm_id)[0]

        # if a field is not present or is None, replace it with the old value
        if 'state' not in alarm or not alarm['state']:
            alarm['state'] = old_alarm['state']
        if 'lifecycle_state' not in alarm or alarm['lifecycle_state'] is None:
            alarm['lifecycle_state'] = old_alarm['lifecycle_state']
        if 'link' not in alarm or alarm['link'] is None:
            alarm['link'] = old_alarm['link']

        self._alarm_patch(req.project_id, alarm_id, alarm['state'],
                          alarm['lifecycle_state'], alarm['link'])

        result = self._alarm_show(req.uri, req.project_id, alarm_id)

        res.body = helpers.to_json(result)
        res.status = falcon.HTTP_200

    @resource.resource_try_catch_block
    def on_delete(self, req, res, alarm_id):

        helpers.validate_authorization(req, ['api:alarms:delete'])

        self._alarm_delete(req.project_id, alarm_id)

        res.status = falcon.HTTP_204

    @resource.resource_try_catch_block
    def on_get(self, req, res, alarm_id=None):
        helpers.validate_authorization(req, ['api:alarms:get'])

        if alarm_id is None:
            query_parms = falcon.uri.parse_query_string(req.query_string)
            if 'state' in query_parms:
                validation.validate_alarm_state(query_parms['state'])
                query_parms['state'] = query_parms['state'].upper()

            if 'severity' in query_parms:
                validation.validate_severity_query(query_parms['severity'])
                query_parms['severity'] = query_parms['severity'].upper()

            if 'sort_by' in query_parms:
                if isinstance(query_parms['sort_by'], six.string_types):
                    query_parms['sort_by'] = query_parms['sort_by'].split(',')

                allowed_sort_by = {
                    'alarm_id', 'alarm_definition_id', 'alarm_definition_name',
                    'state', 'severity', 'lifecycle_state', 'link',
                    'state_updated_timestamp', 'updated_timestamp', 'created_timestamp'}
                validation.validate_sort_by(query_parms['sort_by'], allowed_sort_by)

            query_parms['metric_dimensions'] = helpers.get_query_dimensions(
                req, 'metric_dimensions')
            helpers.validate_query_dimensions(query_parms['metric_dimensions'])

            offset = helpers.get_query_param(req, 'offset')
            if offset is not None and not isinstance(offset, int):
                try:
                    offset = int(offset)
                except Exception as ex:
                    LOG.exception(ex)
                    raise HTTPUnprocessableEntityError(
                        "Unprocessable Entity",
                        "Offset value {} must be an integer".format(offset))

            result = self._alarm_list(req.uri, req.project_id,
                                      query_parms, offset,
                                      req.limit)

            res.body = helpers.to_json(result)
            res.status = falcon.HTTP_200

        else:
            result = self._alarm_show(req.uri, req.project_id, alarm_id)

            res.body = helpers.to_json(result)
            res.status = falcon.HTTP_200

    def _alarm_update(self, tenant_id, alarm_id, new_state, lifecycle_state,
                      link):

        alarm_metric_rows = self._alarms_repo.get_alarm_metrics(alarm_id)
        sub_alarm_rows = self._alarms_repo.get_sub_alarms(tenant_id, alarm_id)

        old_alarm, time_ms = self._alarms_repo.update_alarm(tenant_id, alarm_id,
                                                            new_state,
                                                            lifecycle_state, link)
        old_state = old_alarm['state']
        # alarm_definition_id is the same for all rows.
        alarm_definition_id = sub_alarm_rows[0]['alarm_definition_id']

        state_info = {u'alarmState': new_state, u'oldAlarmState': old_state}

        self._send_alarm_event(u'alarm-updated', tenant_id,
                               alarm_definition_id, alarm_metric_rows,
                               sub_alarm_rows, link, lifecycle_state, state_info)

        if old_state != new_state:
            try:
                alarm_definition_row = self._alarms_repo.get_alarm_definition(
                    tenant_id, alarm_id)
            except exceptions.DoesNotExistException:
                # Alarm definition does not exist. May have been deleted
                # in another transaction. In that case, all associated
                # alarms were also deleted, so don't send transition events.
                pass
            else:
                self._send_alarm_transitioned_event(tenant_id, alarm_id,
                                                    alarm_definition_row,
                                                    alarm_metric_rows,
                                                    old_state, new_state,
                                                    link, lifecycle_state,
                                                    time_ms)

    def _alarm_patch(self, tenant_id, alarm_id, new_state, lifecycle_state,
                     link):

        alarm_metric_rows = self._alarms_repo.get_alarm_metrics(alarm_id)
        sub_alarm_rows = self._alarms_repo.get_sub_alarms(tenant_id, alarm_id)

        old_alarm, time_ms = self._alarms_repo.update_alarm(tenant_id, alarm_id,
                                                            new_state,
                                                            lifecycle_state, link)

        # alarm_definition_id is the same for all rows.
        alarm_definition_id = sub_alarm_rows[0]['alarm_definition_id']

        state_info = {u'alarmState': new_state, u'oldAlarmState': old_alarm['state']}

        self._send_alarm_event(u'alarm-updated', tenant_id,
                               alarm_definition_id, alarm_metric_rows,
                               sub_alarm_rows, link, lifecycle_state, state_info)

        if old_alarm['state'] != new_state:
            try:
                alarm_definition_row = self._alarms_repo.get_alarm_definition(
                    tenant_id, alarm_id)
            except exceptions.DoesNotExistException:
                # Alarm definition does not exist. May have been deleted
                # in another transaction. In that case, all associated
                # alarms were also deleted, so don't send transition events.
                pass
            else:
                self._send_alarm_transitioned_event(tenant_id, alarm_id,
                                                    alarm_definition_row,
                                                    alarm_metric_rows,
                                                    old_alarm['state'], new_state,
                                                    link, lifecycle_state,
                                                    time_ms)

    def _alarm_delete(self, tenant_id, id):

        alarm_metric_rows = self._alarms_repo.get_alarm_metrics(id)
        sub_alarm_rows = self._alarms_repo.get_sub_alarms(tenant_id, id)

        self._alarms_repo.delete_alarm(tenant_id, id)

        # alarm_definition_id is the same for all rows.
        alarm_definition_id = sub_alarm_rows[0]['alarm_definition_id']

        self._send_alarm_event(u'alarm-deleted', tenant_id,
                               alarm_definition_id, alarm_metric_rows,
                               sub_alarm_rows, None, None)

    def _alarm_show(self, req_uri, tenant_id, alarm_id):

        alarm_rows = self._alarms_repo.get_alarm(tenant_id, alarm_id)

        req_uri_no_id = req_uri.replace('/' + alarm_id, "")
        first_row = True
        for alarm_row in alarm_rows:
            if first_row:
                ad = {u'id': alarm_row['alarm_definition_id'],
                      u'name': alarm_row['alarm_definition_name'],
                      u'severity': alarm_row['severity'], }
                helpers.add_links_to_resource(ad,
                                              re.sub('alarms',
                                                     'alarm-definitions',
                                                     req_uri_no_id))

                metrics = []
                alarm = {u'id': alarm_row['alarm_id'], u'metrics': metrics,
                         u'state': alarm_row['state'],
                         u'lifecycle_state': alarm_row['lifecycle_state'],
                         u'link': alarm_row['link'],
                         u'state_updated_timestamp':
                             alarm_row['state_updated_timestamp'].isoformat() +
                             'Z',
                         u'updated_timestamp':
                             alarm_row['updated_timestamp'].isoformat() + 'Z',
                         u'created_timestamp':
                             alarm_row['created_timestamp'].isoformat() + 'Z',
                         u'alarm_definition': ad}
                helpers.add_links_to_resource(alarm, req_uri_no_id)

                first_row = False

            dimensions = {}
            metric = {u'name': alarm_row['metric_name'],
                      u'dimensions': dimensions}

            if alarm_row['metric_dimensions']:
                for dimension in alarm_row['metric_dimensions'].split(','):
                    parsed_dimension = dimension.split('=')
                    dimensions[parsed_dimension[0]] = parsed_dimension[1]

            metrics.append(metric)

        return alarm

    def _alarm_list(self, req_uri, tenant_id, query_parms, offset, limit):

        alarm_rows = self._alarms_repo.get_alarms(tenant_id, query_parms,
                                                  offset, limit)

        result = []
        if not alarm_rows:
            return helpers.paginate_alarming(result, req_uri, limit)

        # Forward declaration
        alarm = {}
        prev_alarm_id = None
        for alarm_row in alarm_rows:
            if prev_alarm_id != alarm_row['alarm_id']:
                if prev_alarm_id is not None:
                    result.append(alarm)

                ad = {u'id': alarm_row['alarm_definition_id'],
                      u'name': alarm_row['alarm_definition_name'],
                      u'severity': alarm_row['severity'], }
                helpers.add_links_to_resource(ad,
                                              re.sub('alarms',
                                                     'alarm-definitions',
                                                     req_uri))

                metrics = []
                alarm = {u'id': alarm_row['alarm_id'], u'metrics': metrics,
                         u'state': alarm_row['state'],
                         u'lifecycle_state': alarm_row['lifecycle_state'],
                         u'link': alarm_row['link'],
                         u'state_updated_timestamp':
                             alarm_row['state_updated_timestamp'].isoformat() +
                             'Z',
                         u'updated_timestamp':
                             alarm_row['updated_timestamp'].isoformat() + 'Z',
                         u'created_timestamp':
                             alarm_row['created_timestamp'].isoformat() + 'Z',
                         u'alarm_definition': ad}
                helpers.add_links_to_resource(alarm, req_uri)

                prev_alarm_id = alarm_row['alarm_id']

            dimensions = {}
            metric = {u'name': alarm_row['metric_name'],
                      u'dimensions': dimensions}

            if alarm_row['metric_dimensions']:
                for dimension in alarm_row['metric_dimensions'].split(','):
                    parsed_dimension = dimension.split('=')
                    dimensions[parsed_dimension[0]] = parsed_dimension[1]

            metrics.append(metric)

        result.append(alarm)

        return helpers.paginate_alarming(result, req_uri, limit)


class AlarmsCount(alarms_api_v2.AlarmsCountV2API, alarming.Alarming):

    def __init__(self):
        try:
            super(AlarmsCount, self).__init__()
            self._region = cfg.CONF.region
            self._alarms_repo = simport.load(
                cfg.CONF.repositories.alarms_driver)()

        except Exception as ex:
            LOG.exception(ex)
            raise exceptions.RepositoryException(ex)

    @resource.resource_try_catch_block
    def on_get(self, req, res):
        helpers.validate_authorization(req, ['api:alarms:count'])
        query_parms = falcon.uri.parse_query_string(req.query_string)

        if 'state' in query_parms:
            validation.validate_alarm_state(query_parms['state'])
            query_parms['state'] = query_parms['state'].upper()

        if 'severity' in query_parms:
            validation.validate_severity_query(query_parms['severity'])
            query_parms['severity'] = query_parms['severity'].upper()

        if 'group_by' in query_parms:
            if not isinstance(query_parms['group_by'], list):
                query_parms['group_by'] = query_parms['group_by'].split(',')
            self._validate_group_by(query_parms['group_by'])

        query_parms['metric_dimensions'] = helpers.get_query_dimensions(req, 'metric_dimensions')
        helpers.validate_query_dimensions(query_parms['metric_dimensions'])

        offset = helpers.get_query_param(req, 'offset')

        if offset is not None:
            try:
                offset = int(offset)
            except Exception:
                raise HTTPUnprocessableEntityError(
                    "Unprocessable Entity",
                    "Offset must be a valid integer, was {}".format(offset))

        result = self._alarms_count(req.uri, req.project_id, query_parms, offset, req.limit)

        res.body = helpers.to_json(result)
        res.status = falcon.HTTP_200

    def _alarms_count(self, req_uri, tenant_id, query_parms, offset, limit):

        count_data = self._alarms_repo.get_alarms_count(tenant_id, query_parms, offset, limit)
        group_by = query_parms['group_by'] if 'group_by' in query_parms else []

        # result = count_data
        result = {
            'links': [
                {
                    'rel': 'self',
                    'href': req_uri
                }
            ],
            'columns': ['count']
        }

        if len(count_data) == 0 or count_data[0]['count'] == 0:
            count = [0]
            if 'group_by' in query_parms:
                for field in query_parms['group_by']:
                    result['columns'].append(field)
                    count.append(None)
            result['counts'] = [count]
            return result

        if len(count_data) > limit:
            result['links'].append({
                'rel': 'next',
                'href': helpers.create_alarms_count_next_link(req_uri, offset, limit)})
            count_data = count_data[:limit]

        result['columns'].extend(group_by)

        result['counts'] = []
        for row in count_data:
            count_result = [row['count']]
            for field in group_by:
                count_result.append(row[field])
            result['counts'].append(count_result)

        return result

    def _validate_group_by(self, group_by):
        allowed_values = {'alarm_definition_id', 'name', 'state', 'severity',
                          'link', 'lifecycle_state', 'metric_name',
                          'dimension_name', 'dimension_value'}
        if not set(group_by).issubset(allowed_values):
            raise HTTPUnprocessableEntityError(
                "Unprocessable Entity",
                "One or more group-by values from {} are not in {}"
                .format(group_by, allowed_values))


class AlarmsStateHistory(alarms_api_v2.AlarmsStateHistoryV2API,
                         alarming.Alarming):
    def __init__(self):
        try:
            super(AlarmsStateHistory, self).__init__()
            self._region = cfg.CONF.region
            self._alarms_repo = simport.load(
                cfg.CONF.repositories.alarms_driver)()
            self._metrics_repo = simport.load(
                cfg.CONF.repositories.metrics_driver)()

        except Exception as ex:
            LOG.exception(ex)
            raise exceptions.RepositoryException(ex)

    @resource.resource_try_catch_block
    def on_get(self, req, res, alarm_id=None):
        helpers.validate_authorization(req, ['api:alarms:state_history'])
        offset = helpers.get_query_param(req, 'offset')

        if alarm_id is None:
            start_timestamp = helpers.get_query_starttime_timestamp(req, False)
            end_timestamp = helpers.get_query_endtime_timestamp(req, False)

            dimensions = helpers.get_query_dimensions(req)
            helpers.validate_query_dimensions(dimensions)

            result = self._alarm_history_list(req.project_id, start_timestamp,
                                              end_timestamp, dimensions,
                                              req.uri, offset, req.limit)

        else:
            result = self._alarm_history(req.project_id, alarm_id,
                                         req.uri, offset,
                                         req.limit)

        res.body = helpers.to_json(result)
        res.status = falcon.HTTP_200

    def _alarm_history_list(self, tenant_id, start_timestamp,
                            end_timestamp, dimensions, req_uri, offset,
                            limit):

        # get_alarms expects 'metric_dimensions' for dimensions key.
        new_query_parms = {'metric_dimensions': dimensions}

        alarm_rows = self._alarms_repo.get_alarms(tenant_id, new_query_parms,
                                                  None, None)
        alarm_id_list = [alarm_row['alarm_id'] for alarm_row in alarm_rows]

        result = self._metrics_repo.alarm_history(tenant_id, alarm_id_list,
                                                  offset, limit,
                                                  start_timestamp,
                                                  end_timestamp)

        return helpers.paginate(result, req_uri, limit)

    def _alarm_history(self, tenant_id, alarm_id, req_uri, offset, limit):

        result = self._metrics_repo.alarm_history(tenant_id, [alarm_id], offset,
                                                  limit)

        return helpers.paginate(result, req_uri, limit)
