#
#    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
from urllib import parse as urllib_parse

import pyparsing as pp

uninary_operators = ("not", )
binary_operator = (">=", "<=", "!=", ">", "<", "=", "==", "eq", "ne",
                   "lt", "gt", "ge", "le")
multiple_operators = ("and", "or")

operator = pp.Regex("|".join(binary_operator))
null = pp.Regex("None|none|null").setParseAction(pp.replaceWith(None))
boolean = "False|True|false|true"
boolean = pp.Regex(boolean).setParseAction(lambda t: t[0].lower() == "true")
hex_string = lambda n: pp.Word(pp.hexnums, exact=n)  # noqa: E731
uuid = pp.Combine(hex_string(8) + ("-" + hex_string(4)) * 3 +
                  "-" + hex_string(12))
number = r"[+-]?\d+(:?\.\d*)?(:?[eE][+-]?\d+)?"
number = pp.Regex(number).setParseAction(lambda t: float(t[0]))
identifier = pp.Word(pp.alphas, pp.alphanums + "_")
quoted_string = pp.QuotedString('"') | pp.QuotedString("'")
comparison_term = pp.Forward()
in_list = pp.Group(pp.Suppress('[') +
                   pp.Optional(pp.delimitedList(comparison_term)) +
                   pp.Suppress(']'))("list")
comparison_term << (null | boolean | uuid | identifier | number |
                    quoted_string)
condition = pp.Group(comparison_term + operator + comparison_term)

expr = pp.infixNotation(condition, [
    ("not", 1, pp.opAssoc.RIGHT, ),
    ("and", 2, pp.opAssoc.LEFT, ),
    ("or", 2, pp.opAssoc.LEFT, ),
])

OP_LOOKUP = {'!=': 'ne',
             '>=': 'ge',
             '<=': 'le',
             '>': 'gt',
             '<': 'lt',
             '=': 'eq'}

OP_LOOKUP_KEYS = '|'.join(sorted(OP_LOOKUP.keys(), key=len, reverse=True))
OP_SPLIT_RE = re.compile(r'(%s)' % OP_LOOKUP_KEYS)


def _parsed_query2dict(parsed_query):
    result = None
    while parsed_query:
        part = parsed_query.pop()
        if part in binary_operator:
            result = {part: {parsed_query.pop(): result}}

        elif part in multiple_operators:
            if result.get(part):
                result[part].append(
                    _parsed_query2dict(parsed_query.pop()))
            else:
                result = {part: [result]}

        elif part in uninary_operators:
            result = {part: result}
        elif isinstance(part, pp.ParseResults):
            kind = part.getName()
            if kind == "list":
                res = part.asList()
            else:
                res = _parsed_query2dict(part)
            if result is None:
                result = res
            elif isinstance(result, dict):
                list(result.values())[0].append(res)
        else:
            result = part
    return result


def search_query_builder(query):
    parsed_query = expr.parseString(query)[0]
    return _parsed_query2dict(parsed_query)


def list2cols(cols, objs):
    return cols, [tuple([o[k] for k in cols])
                  for o in objs]


def format_string_list(objs, field):
    objs[field] = ", ".join(objs[field])


def format_dict_list(objs, field):
    objs[field] = "\n".join(
        "- " + ", ".join("{}: {}".format(k, v)
                         for k, v in elem.items())
        for elem in objs[field])


def format_move_dict_to_root(obj, field):
    for attr in obj[field]:
        obj["{}/{}".format(field, attr)] = obj[field][attr]
    del obj[field]


def format_archive_policy(ap):
    format_dict_list(ap, "definition")
    format_string_list(ap, "aggregation_methods")


def dict_from_parsed_args(parsed_args, attrs):
    d = {}
    for attr in attrs:
        if attr == "metric":
            if parsed_args.metrics:
                value = parsed_args.metrics[0]
            else:
                value = None
        else:
            value = getattr(parsed_args, attr)
        if value is not None:
            if value == [""]:
                # NOTE(jake): As options like --alarm-actions is an array,
                # their value can be array with empty string if user set option
                # to ''. In this case we set it to None here so that a None
                # value gets sent to API.
                d[attr] = None
            else:
                d[attr] = value
    return d


def dict_to_querystring(objs):
    return "&".join(["{}={}".format(k, v)
                     for k, v in objs.items()
                     if v is not None])


def cli_to_array(cli_query):
    """Convert CLI list of queries to the Python API format.

    This will convert the following:
        "this<=34;that=string::foo"
    to
        "[{field=this,op=le,value=34,type=''},
          {field=that,op=eq,value=foo,type=string}]"

    """

    opts = []
    queries = cli_query.split(';')
    for q in queries:
        try:
            field, q_operator, type_value = OP_SPLIT_RE.split(q, maxsplit=1)
        except ValueError:
            raise ValueError('Invalid or missing operator in query %(q)s,'
                             'the supported operators are: %(k)s' %
                             {'q': q, 'k': OP_LOOKUP.keys()})
        if not field:
            raise ValueError('Missing field in query %s' % q)
        if not type_value:
            raise ValueError('Missing value in query %s' % q)
        opt = dict(field=field, op=OP_LOOKUP[q_operator])

        if '::' not in type_value:
            opt['type'], opt['value'] = '', type_value
        else:
            opt['type'], _, opt['value'] = type_value.partition('::')

        if opt['type'] and opt['type'] not in (
                'string', 'integer', 'float', 'datetime', 'boolean'):
            err = ('Invalid value type %(type)s, the type of value'
                   'should be one of: integer, string, float, datetime,'
                   ' boolean.' % opt)
            raise ValueError(err)
        opts.append(opt)
    return opts


def get_pagination_options(limit=None, marker=None, sorts=None):
    options = []
    if limit:
        options.append("limit=%d" % limit)
    if marker:
        options.append("marker=%s" % urllib_parse.quote(marker))
    for sort in sorts or []:
        options.append("sort=%s" % urllib_parse.quote(sort))
    return "&".join(options)


def get_client(obj):
    if hasattr(obj.app, 'client_manager'):
        # NOTE(liusheng): cliff objects loaded by OSC
        return obj.app.client_manager.alarming
    else:
        # TODO(liusheng): Remove this when OSC is able
        # to install the aodh client binary itself
        return obj.app.client
