#
#    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 argparse

from cliff import command
from cliff import lister
from cliff import show
from oslo_serialization import jsonutils
from oslo_utils import strutils
from oslo_utils import uuidutils

from aodhclient import exceptions
from aodhclient.i18n import _
from aodhclient import utils

ALARM_TYPES = ['prometheus', 'event', 'composite', 'threshold',
               'gnocchi_resources_threshold',
               'gnocchi_aggregation_by_metrics_threshold',
               'gnocchi_aggregation_by_resources_threshold',
               'loadbalancer_member_health']
ALARM_STATES = ['ok', 'alarm', 'insufficient data']
ALARM_SEVERITY = ['low', 'moderate', 'critical']
ALARM_OPERATORS = ['lt', 'le', 'eq', 'ne', 'ge', 'gt']
ALARM_OP_MAP = dict(zip(ALARM_OPERATORS, ('<', '<=', '=', '!=', '>=', '>')))
STATISTICS = ['max', 'min', 'avg', 'sum', 'count']
ALARM_LIST_COLS = ['alarm_id', 'type', 'name', 'state', 'severity', 'enabled']


class CliAlarmList(lister.Lister):
    """List alarms"""

    @staticmethod
    def split_filter_param(param):
        key, eq_op, value = param.partition('=')
        if not eq_op:
            msg = 'Malformed parameter(%s). Use the key=value format.' % param
            raise ValueError(msg)
        return key, value

    def get_parser(self, prog_name):
        parser = super().get_parser(prog_name)
        exclusive_group = parser.add_mutually_exclusive_group()
        exclusive_group.add_argument(
            "--query",
            help="List alarms using rich queries API, "
                 "e.g. project_id!=my-id user_id=foo or user_id=bar."
        )
        exclusive_group.add_argument(
            '--filter',
            dest='filter',
            metavar='<KEY1=VALUE1;KEY2=VALUE2...>',
            type=self.split_filter_param,
            action='append',
            help='Filter parameters to apply on returned alarms. '
                 'To list alarms in all projects, use all_projects=true '
                 'filter (admin only).'
        )
        parser.add_argument("--limit", type=int, metavar="<LIMIT>",
                            help="Number of resources to return "
                                 "(Default is server default)")
        parser.add_argument("--marker", metavar="<MARKER>",
                            help="Last item of the previous listing. "
                                 "Return the next results after this value,"
                                 "the supported marker is alarm_id.")
        parser.add_argument("--sort", action="append",
                            metavar="<SORT_KEY:SORT_DIR>",
                            help="Sort of resource attribute, "
                                 "e.g. name:asc")
        return parser

    def take_action(self, parsed_args):
        if parsed_args.query:
            if any([parsed_args.limit, parsed_args.sort, parsed_args.marker]):
                raise exceptions.CommandError(
                    "Query and pagination options are mutually "
                    "exclusive.")
            query = jsonutils.dumps(
                utils.search_query_builder(parsed_args.query))
            alarms = utils.get_client(self).alarm.query(query=query)
        else:
            filters = dict(parsed_args.filter) if parsed_args.filter else None
            alarms = utils.get_client(self).alarm.list(
                filters=filters, sorts=parsed_args.sort,
                limit=parsed_args.limit, marker=parsed_args.marker)
        return utils.list2cols(ALARM_LIST_COLS, alarms)


def _format_alarm(alarm):
    if alarm.get('composite_rule'):
        composite_rule = jsonutils.dumps(alarm['composite_rule'], indent=2)
        alarm['composite_rule'] = composite_rule
        return alarm
    for alarm_type in ALARM_TYPES:
        if alarm.get('%s_rule' % alarm_type):
            alarm.update(alarm.pop('%s_rule' % alarm_type))
    if alarm["time_constraints"]:
        alarm["time_constraints"] = jsonutils.dumps(alarm["time_constraints"],
                                                    sort_keys=True,
                                                    indent=2)
    # only works for threshold and event alarm
    if isinstance(alarm.get('query'), list):
        query_rows = []
        for q in alarm['query']:
            op = ALARM_OP_MAP.get(q['op'], q['op'])
            query_rows.append('{} {} {}'.format(q['field'], op, q['value']))
        alarm['query'] = ' AND\n'.join(query_rows)
    return alarm


def _find_alarm_by_name(client, name):
    # then try to get entity as name
    query = jsonutils.dumps({"=": {"name": name}})
    alarms = client.alarm.query(query)
    if len(alarms) > 1:
        msg = (_("Multiple alarms matches found for '%s', "
                 "use an ID to be more specific.") % name)
        raise exceptions.NoUniqueMatch(msg)
    elif not alarms:
        msg = (_("Alarm %s not found") % name)
        raise exceptions.NotFound(msg)
    else:
        return alarms[0]


def _find_alarm_id_by_name(client, name):
    alarm = _find_alarm_by_name(client, name)
    return alarm['alarm_id']


def _check_name_and_id_coexist(parsed_args, action):
    if parsed_args.id and parsed_args.name:
        raise exceptions.CommandError(
            "You should provide only one of "
            "alarm ID and alarm name(--name) "
            "to %s an alarm." % action)


def _check_name_and_id_exist(parsed_args, action):
    if not parsed_args.id and not parsed_args.name:
        msg = (_("You need to specify one of "
                 "alarm ID and alarm name(--name) "
                 "to %s an alarm.") % action)
        raise exceptions.CommandError(msg)


def _check_name_and_id(parsed_args, action):
    _check_name_and_id_coexist(parsed_args, action)
    _check_name_and_id_exist(parsed_args, action)


def _add_name_to_parser(parser, required=False):
    parser.add_argument('--name', metavar='<NAME>',
                        required=required,
                        help='Name of the alarm')
    return parser


def _add_id_to_parser(parser):
    parser.add_argument("id", nargs='?',
                        metavar='<ALARM ID or NAME>',
                        help="ID or name of an alarm.")
    return parser


class CliAlarmShow(show.ShowOne):
    """Show an alarm"""

    def get_parser(self, prog_name):
        return _add_name_to_parser(
            _add_id_to_parser(
                super().get_parser(prog_name)))

    def take_action(self, parsed_args):
        _check_name_and_id(parsed_args, 'query')
        c = utils.get_client(self)
        if parsed_args.name:
            alarm = _find_alarm_by_name(c, parsed_args.name)
        else:
            if uuidutils.is_uuid_like(parsed_args.id):
                try:
                    alarm = c.alarm.get(alarm_id=parsed_args.id)
                except exceptions.NotFound:
                    # Maybe it's a name
                    alarm = _find_alarm_by_name(c, parsed_args.id)
            else:
                alarm = _find_alarm_by_name(c, parsed_args.id)

        return self.dict2columns(_format_alarm(alarm))


class CliAlarmCreate(show.ShowOne):
    """Create an alarm"""

    create = True

    def get_parser(self, prog_name):
        parser = _add_name_to_parser(
            super().get_parser(prog_name),
            required=self.create)

        parser.add_argument('-t', '--type', metavar='<TYPE>',
                            required=self.create,
                            choices=ALARM_TYPES,
                            help='Type of alarm, should be one of: '
                                 '%s.' % ', '.join(ALARM_TYPES))
        parser.add_argument('--project-id', metavar='<PROJECT_ID>',
                            help='Project to associate with alarm '
                                 '(configurable by admin users only)')
        parser.add_argument('--user-id', metavar='<USER_ID>',
                            help='User to associate with alarm '
                            '(configurable by admin users only)')
        parser.add_argument('--description', metavar='<DESCRIPTION>',
                            help='Free text description of the alarm')
        parser.add_argument('--state', metavar='<STATE>',
                            choices=ALARM_STATES,
                            help='State of the alarm, one of: '
                            + str(ALARM_STATES))
        parser.add_argument('--severity', metavar='<SEVERITY>',
                            choices=ALARM_SEVERITY,
                            help='Severity of the alarm, one of: '
                            + str(ALARM_SEVERITY))
        parser.add_argument('--enabled', type=strutils.bool_from_string,
                            metavar='{True|False}',
                            help=('True if alarm evaluation is enabled'))
        parser.add_argument('--alarm-action', dest='alarm_actions',
                            metavar='<Webhook URL>', action='append',
                            help=('URL to invoke when state transitions to '
                                  'alarm. May be used multiple times'))
        parser.add_argument('--ok-action', dest='ok_actions',
                            metavar='<Webhook URL>', action='append',
                            help=('URL to invoke when state transitions to '
                                  'OK. May be used multiple times'))
        parser.add_argument('--insufficient-data-action',
                            dest='insufficient_data_actions',
                            metavar='<Webhook URL>', action='append',
                            help=('URL to invoke when state transitions to '
                                  'insufficient data. May be used multiple '
                                  'times'))
        parser.add_argument(
            '--time-constraint', dest='time_constraints',
            metavar='<Time Constraint>', action='append',
            type=self.validate_time_constraint,
            help=('Only evaluate the alarm if the time at evaluation '
                  'is within this time constraint. Start point(s) of '
                  'the constraint are specified with a cron expression'
                  ', whereas its duration is given in seconds. '
                  'Can be specified multiple times for multiple '
                  'time constraints, format is: '
                  'name=<CONSTRAINT_NAME>;start=<CRON>;'
                  'duration=<SECONDS>;[description=<DESCRIPTION>;'
                  '[timezone=<IANA Timezone>]]'))
        parser.add_argument('--repeat-actions', dest='repeat_actions',
                            metavar='{True|False}',
                            type=strutils.bool_from_string,
                            help=('True if actions should be repeatedly '
                                  'notified while alarm remains in target '
                                  'state'))

        common_group = parser.add_argument_group('common alarm rules')
        common_group.add_argument(
            '--query', metavar='<QUERY>', dest='query',
            help="For alarms of type threshold or event: "
                 "key[op]data_type::value; list. data_type is optional, "
                 "but if supplied must be string, integer, float, or boolean. "
                 'For alarms of '
                 'type gnocchi_aggregation_by_resources_threshold: '
                 'need to specify a complex query json string, like:'
                 ' {"and": [{"=": {"ended_at": null}}, ...]}. '
                 'For alarms of type prometheus this should be valid '
                 'PromQL query.')
        common_group.add_argument(
            '--comparison-operator', metavar='<OPERATOR>',
            dest='comparison_operator', choices=ALARM_OPERATORS,
            help='Operator to compare with, one of: ' + str(ALARM_OPERATORS))
        common_group.add_argument(
            '--evaluation-periods', type=int, metavar='<EVAL_PERIODS>',
            dest='evaluation_periods',
            help='Number of periods to evaluate over')
        common_group.add_argument(
            '--threshold', type=float, metavar='<THRESHOLD>',
            dest='threshold', help='Threshold to evaluate against.')

        # For event type alarm
        event_group = parser.add_argument_group('event alarm')
        event_group.add_argument(
            '--event-type', metavar='<EVENT_TYPE>',
            dest='event_type', help='Event type to evaluate against')

        # For Ceilometer threshold type alarm
        threshold_group = parser.add_argument_group('threshold alarm')
        threshold_group.add_argument(
            '-m', '--meter-name', metavar='<METER NAME>',
            dest='meter_name', help='Meter to evaluate against')
        threshold_group.add_argument(
            '--period', type=int, metavar='<PERIOD>', dest='period',
            help='Length of each period (seconds) to evaluate over.')
        threshold_group.add_argument(
            '--statistic', metavar='<STATISTIC>', dest='statistic',
            choices=STATISTICS,
            help='Statistic to evaluate, one of: ' + str(STATISTICS))

        # For common Gnocchi threshold type alarm
        gnocchi_common_group = parser.add_argument_group(
            'common gnocchi alarm rules')
        gnocchi_common_group.add_argument(
            '--granularity', metavar='<GRANULARITY>',
            dest='granularity',
            help='The time range in seconds over which to query.')
        gnocchi_common_group.add_argument(
            '--aggregation-method', metavar='<AGGR_METHOD>',
            dest='aggregation_method',
            help='The aggregation_method to compare to the threshold.')
        gnocchi_common_group.add_argument(
            '--metric', '--metrics', metavar='<METRIC>', action='append',
            dest='metrics', help='The metric id or name '
            'depending of the alarm type')

        gnocchi_resource_threshold_group = parser.add_argument_group(
            'gnocchi resource threshold alarm')
        gnocchi_resource_threshold_group.add_argument(
            '--resource-type', metavar='<RESOURCE_TYPE>',
            dest='resource_type', help='The type of resource.')
        gnocchi_resource_threshold_group.add_argument(
            '--resource-id', metavar='<RESOURCE_ID>',
            dest='resource_id', help='The id of a resource.')

        # For composite type alarm
        composite_group = parser.add_argument_group('composite alarm')
        composite_group.add_argument(
            '--composite-rule', metavar='<COMPOSITE_RULE>',
            dest='composite_rule',
            type=jsonutils.loads,
            help='Composite threshold rule with JSON format, the form can '
                 'be a nested dict which combine threshold/gnocchi rules by '
                 '"and", "or". For example, the form is like: '
                 '{"or":[RULE1, RULE2, {"and": [RULE3, RULE4]}]}, The '
                 'RULEx can be basic threshold rules but must include a '
                 '"type" field, like this: {"threshold": 0.8,'
                 '"meter_name":"cpu_util","type":"threshold"}'
        )

        loadbalancer_member_health_group = parser.add_argument_group(
            'loadbalancer member health alarm')
        loadbalancer_member_health_group.add_argument(
            '--stack-id', metavar='<STACK_NAME_OR_ID>',
            dest="stack_id",
            type=str, help=(
                "Name or ID of the root / top level Heat stack containing the "
                "loadbalancer pool and members. An update will be triggered "
                "on the root Stack if an unhealthy member is detected in the "
                "loadbalancer pool.")
        )
        loadbalancer_member_health_group.add_argument(
            '--pool-id', metavar='<LOADBALANCER_POOL_NAME_OR_ID>',
            dest="pool_id",
            type=str, help=(
                "Name or ID of the loadbalancer pool for which the health of "
                "each member will be evaluated.")
        )
        loadbalancer_member_health_group.add_argument(
            '--autoscaling-group-id', metavar='<AUTOSCALING_GROUP_NAME_OR_ID>',
            dest="autoscaling_group_id",
            type=str, help=(
                "ID of the Heat autoscaling group that contains the "
                "loadbalancer members. Unhealthy members will be marked "
                "as such before an update is triggered on the root stack."))
        self.parser = parser
        return parser

    def validate_time_constraint(self, values_to_convert):
        """Converts 'a=1;b=2' to {a:1,b:2}."""

        try:
            return dict((item.strip(" \"'")
                         for item in kv.split("=", 1))
                        for kv in values_to_convert.split(";"))
        except ValueError:
            msg = ('must be a list of '
                   'key1=value1;key2=value2;... not %s'
                   % values_to_convert)
            raise argparse.ArgumentTypeError(msg)

    def _validate_args(self, parsed_args):
        if (parsed_args.type == 'threshold' and
                not (parsed_args.meter_name and parsed_args.threshold)):
            self.parser.error('Threshold alarm requires -m/--meter-name and '
                              '--threshold parameters. Meter name can be '
                              'found in Ceilometer')
        elif (parsed_args.type == 'gnocchi_resources_threshold' and
                not (parsed_args.metrics and parsed_args.threshold is not None
                     and parsed_args.resource_id and parsed_args.resource_type
                     and parsed_args.aggregation_method)):
            self.parser.error('gnocchi_resources_threshold requires --metric, '
                              '--threshold, --resource-id, --resource-type '
                              'and --aggregation-method')
        elif (parsed_args.type == 'gnocchi_aggregation_by_metrics_threshold'
              and not (parsed_args.metrics
                       and parsed_args.threshold is not None
                       and parsed_args.aggregation_method)):
            self.parser.error('gnocchi_aggregation_by_metrics_threshold '
                              'requires --metric, --threshold and '
                              '--aggregation-method')
        elif (parsed_args.type == 'gnocchi_aggregation_by_resources_threshold'
              and not (parsed_args.metrics
                       and parsed_args.threshold is not None
                       and parsed_args.query and parsed_args.resource_type and
                       parsed_args.aggregation_method)):
            self.parser.error('gnocchi_aggregation_by_resources_threshold '
                              'requires --metric, --threshold, '
                              '--aggregation-method, --query and '
                              '--resource-type')
        elif (parsed_args.type == 'composite' and
              not parsed_args.composite_rule):
            self.parser.error('Composite alarm requires'
                              ' --composite-rule parameter')
        elif (parsed_args.type == 'loadbalancer_member_health') and \
                (parsed_args.stack_id is None or
                 parsed_args.pool_id is None or
                 parsed_args.autoscaling_group_id is None):
            self.parser.error('Loadbalancer member health alarm requires'
                              '--stack-id, --pool-id and'
                              '--autoscaling-group-id')
        elif (parsed_args.type == 'prometheus' and
                not (parsed_args.query and parsed_args.threshold)):
            self.parser.error('Prometheus alarm requires --query and '
                              '--threshold parameters.')

    def _alarm_from_args(self, parsed_args):
        alarm = utils.dict_from_parsed_args(
            parsed_args, ['name', 'type', 'project_id', 'user_id',
                          'description', 'state', 'severity', 'enabled',
                          'alarm_actions', 'ok_actions',
                          'insufficient_data_actions',
                          'time_constraints', 'repeat_actions'])
        if parsed_args.type in ('threshold', 'event') and parsed_args.query:
            parsed_args.query = utils.cli_to_array(parsed_args.query)
        alarm['threshold_rule'] = utils.dict_from_parsed_args(
            parsed_args, ['meter_name', 'period', 'evaluation_periods',
                          'statistic', 'comparison_operator', 'threshold',
                          'query'])
        alarm['event_rule'] = utils.dict_from_parsed_args(
            parsed_args, ['event_type', 'query'])
        alarm['prometheus_rule'] = (
            utils.dict_from_parsed_args(parsed_args,
                                        ['comparison_operator', 'threshold',
                                         'query']))
        alarm['gnocchi_resources_threshold_rule'] = (
            utils.dict_from_parsed_args(parsed_args,
                                        ['granularity', 'comparison_operator',
                                         'threshold', 'aggregation_method',
                                         'evaluation_periods', 'metric',
                                         'resource_id', 'resource_type']))
        alarm['gnocchi_aggregation_by_metrics_threshold_rule'] = (
            utils.dict_from_parsed_args(parsed_args,
                                        ['granularity', 'comparison_operator',
                                         'threshold', 'aggregation_method',
                                         'evaluation_periods', 'metrics']))
        alarm['gnocchi_aggregation_by_resources_threshold_rule'] = (
            utils.dict_from_parsed_args(parsed_args,
                                        ['granularity', 'comparison_operator',
                                         'threshold', 'aggregation_method',
                                         'evaluation_periods', 'metric',
                                         'query', 'resource_type']))

        alarm['loadbalancer_member_health_rule'] = (
            utils.dict_from_parsed_args(parsed_args, [
                "stack_id", "pool_id", "autoscaling_group_id"]))

        alarm['composite_rule'] = parsed_args.composite_rule
        if self.create:
            alarm['type'] = parsed_args.type
            self._validate_args(parsed_args)
        return alarm

    def take_action(self, parsed_args):
        alarm = utils.get_client(self).alarm.create(
            alarm=self._alarm_from_args(parsed_args))
        return self.dict2columns(_format_alarm(alarm))


class CliAlarmUpdate(CliAlarmCreate):
    """Update an alarm"""

    create = False

    def get_parser(self, prog_name):
        return _add_id_to_parser(
            super().get_parser(prog_name))

    def take_action(self, parsed_args):
        attributes = self._alarm_from_args(parsed_args)
        _check_name_and_id_exist(parsed_args, 'update')
        c = utils.get_client(self)

        if uuidutils.is_uuid_like(parsed_args.id):
            try:
                alarm = c.alarm.update(alarm_id=parsed_args.id,
                                       alarm_update=attributes)
            except exceptions.NotFound:
                # Maybe it was not an ID but a name, damn
                _id = _find_alarm_id_by_name(c, parsed_args.id)
            else:
                return self.dict2columns(_format_alarm(alarm))
        elif parsed_args.id:
            _id = _find_alarm_id_by_name(c, parsed_args.id)
        else:
            _id = _find_alarm_id_by_name(c, parsed_args.name)

        alarm = c.alarm.update(alarm_id=_id, alarm_update=attributes)
        return self.dict2columns(_format_alarm(alarm))


class CliAlarmDelete(command.Command):
    """Delete an alarm"""

    def get_parser(self, prog_name):
        return _add_name_to_parser(
            _add_id_to_parser(
                super().get_parser(prog_name)))

    def take_action(self, parsed_args):
        _check_name_and_id(parsed_args, 'delete')
        c = utils.get_client(self)

        if parsed_args.name:
            _id = _find_alarm_id_by_name(c, parsed_args.name)
        elif uuidutils.is_uuid_like(parsed_args.id):
            try:
                return c.alarm.delete(parsed_args.id)
            except exceptions.NotFound:
                # Maybe it was not an ID after all
                _id = _find_alarm_id_by_name(c, parsed_args.id)
        else:
            _id = _find_alarm_id_by_name(c, parsed_args.id)

        c.alarm.delete(_id)


class CliAlarmStateGet(show.ShowOne):
    """Get state of an alarm"""

    def get_parser(self, prog_name):
        return _add_name_to_parser(
            _add_id_to_parser(
                super().get_parser(prog_name)))

    def take_action(self, parsed_args):
        _check_name_and_id(parsed_args, 'get state of')
        c = utils.get_client(self)

        if parsed_args.name:
            _id = _find_alarm_id_by_name(c, parsed_args.name)
        elif uuidutils.is_uuid_like(parsed_args.id):
            try:
                state = c.alarm.get_state(parsed_args.id)
            except exceptions.NotFound:
                # Maybe it was not an ID after all
                _id = _find_alarm_id_by_name(c, parsed_args.id)
            else:
                return self.dict2columns({'state': state})
        else:
            _id = _find_alarm_id_by_name(c, parsed_args.id)

        state = c.alarm.get_state(_id)
        return self.dict2columns({'state': state})


class CliAlarmStateSet(show.ShowOne):
    """Set state of an alarm"""

    def get_parser(self, prog_name):
        parser = _add_name_to_parser(
            _add_id_to_parser(
                super().get_parser(prog_name)))
        parser.add_argument('--state', metavar='<STATE>',
                            required=True,
                            choices=ALARM_STATES,
                            help='State of the alarm, one of: '
                            + str(ALARM_STATES))
        return parser

    def take_action(self, parsed_args):
        _check_name_and_id(parsed_args, 'set state of')
        c = utils.get_client(self)

        if parsed_args.name:
            _id = _find_alarm_id_by_name(c, parsed_args.name)
        elif uuidutils.is_uuid_like(parsed_args.id):
            try:
                state = c.alarm.set_state(parsed_args.id, parsed_args.state)
            except exceptions.NotFound:
                # Maybe it was not an ID after all
                _id = _find_alarm_id_by_name(c, parsed_args.id)
            else:
                return self.dict2columns({'state': state})
        else:
            _id = _find_alarm_id_by_name(c, parsed_args.id)

        state = c.alarm.set_state(_id, parsed_args.state)
        return self.dict2columns({'state': state})
