# --------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------

# pylint: disable=line-too-long

import json
import time
import re
import copy

from knack.log import get_logger
from knack.util import CLIError

from ._utils import resolve_connection_string, user_confirmation
from ._azconfig.azconfig_client import AzconfigClient
from ._azconfig.constants import StatusCodes
from ._azconfig.exceptions import HTTPException
from ._azconfig.models import (KeyValue,
                               ModifyKeyValueOptions,
                               QueryKeyValueCollectionOptions,
                               QueryKeyValueOptions)
from ._featuremodels import (map_keyvalue_to_featureflag,
                             map_keyvalue_to_featureflagvalue,
                             FeatureFilter)


logger = get_logger(__name__)
FEATURE_FLAG_PREFIX = ".appconfig.featureflag/"
FEATURE_FLAG_CONTENT_TYPE = "application/vnd.microsoft.appconfig.ff+json;charset=utf-8"

# Feature commands #


def set_feature(cmd,
                feature,
                name=None,
                label=None,
                description=None,
                yes=False,
                connection_string=None):
    key = FEATURE_FLAG_PREFIX + feature

    # when creating a new Feature flag, these defaults will be used
    tags = {}
    default_conditions = {'client_filters': []}

    default_value = {
        "id": feature,
        "description": description,
        "enabled": False,
        "conditions": default_conditions
    }

    connection_string = resolve_connection_string(cmd, name, connection_string)
    azconfig_client = AzconfigClient(connection_string)
    retry_times = 3
    retry_interval = 1

    label = label if label and label != ModifyKeyValueOptions.empty_label else None
    query_options = QueryKeyValueOptions(label=label)

    for i in range(0, retry_times):
        try:
            retrieved_kv = azconfig_client.get_keyvalue(key, query_options)
        except HTTPException as exception:
            raise CLIError(str(exception))

        try:
            # if kv exists and only content-type is wrong, we can force correct it by updating the kv
            if retrieved_kv is None:
                set_kv = KeyValue(
                    key,
                    json.dumps(default_value, ensure_ascii=False),
                    label,
                    tags,
                    FEATURE_FLAG_CONTENT_TYPE)
            else:
                if retrieved_kv.content_type != FEATURE_FLAG_CONTENT_TYPE:
                    logger.warning(
                        "This feature contains invalid content-type. The feature flag will be overwritten.")
                # we make sure that value retrieved is a valid json and only has the fields supported by backend.
                # if it's invalid, we catch appropriate exception that contains
                # detailed message
                feature_flag_value = map_keyvalue_to_featureflagvalue(
                    retrieved_kv)

                # User can only update description if the key already exists
                feature_flag_value.description = description
                set_kv = KeyValue(
                    key=key,
                    label=label,
                    value=json.dumps(
                        feature_flag_value,
                        default=lambda o: o.__dict__,
                        ensure_ascii=False),
                    content_type=FEATURE_FLAG_CONTENT_TYPE,
                    tags=retrieved_kv.tags if retrieved_kv.tags else tags)
                set_kv.etag = retrieved_kv.etag
                set_kv.last_modified = retrieved_kv.last_modified

            # Convert KeyValue object to required FeatureFlag format for
            # display
            feature_flag = map_keyvalue_to_featureflag(
                set_kv, show_conditions=True)
            entry = json.dumps(feature_flag, default=lambda o: o.__dict__, indent=2, sort_keys=True, ensure_ascii=False)

        except Exception as exception:
            # Exceptions for ValueError and AttributeError already have customized message
            # No need to catch specific exception here and customize
            raise CLIError(str(exception))

        confirmation_message = "Are you sure you want to set the feature flag: \n" + entry + "\n"
        user_confirmation(confirmation_message, yes)

        try:
            updated_key_value = azconfig_client.add_keyvalue(
                set_kv,
                ModifyKeyValueOptions()) if set_kv.etag is None else azconfig_client.update_keyvalue(
                    set_kv,
                    ModifyKeyValueOptions())
            return map_keyvalue_to_featureflag(
                keyvalue=updated_key_value, show_conditions=True)
        except HTTPException as exception:
            if exception.status == StatusCodes.PRECONDITION_FAILED:
                logger.debug(
                    'Retrying setting %s times with exception: concurrent setting operations',
                    i + 1)
                time.sleep(retry_interval)
            else:
                raise CLIError(str(exception))
        except Exception as exception:
            raise CLIError(str(exception))
    raise CLIError(
        "Failed to set the feature flag '{}' due to a conflicting operation.".format(feature))


def delete_feature(cmd,
                   feature,
                   name=None,
                   label=None,
                   yes=False,
                   connection_string=None):
    connection_string = resolve_connection_string(cmd, name, connection_string)
    azconfig_client = AzconfigClient(connection_string)

    delete_one_version_message = "Are you sure you want to delete the feature '{}'".format(
        feature)
    confirmation_message = delete_one_version_message
    user_confirmation(confirmation_message, yes)

    try:
        retrieved_keyvalues = __list_all_keyvalues(azconfig_client,
                                                   feature=feature,
                                                   label=QueryKeyValueCollectionOptions.empty_label if label is None else label)
    except HTTPException as exception:
        raise CLIError('Delete operation failed. ' + str(exception))

    deleted_kv = []
    not_deleted_kv = []
    http_exception = None
    for entry in retrieved_keyvalues:
        try:
            deleted_kv.append(
                azconfig_client.delete_keyvalue(
                    entry, ModifyKeyValueOptions()))
        except HTTPException as exception:
            not_deleted_kv.append(entry)
            http_exception = exception
        except Exception as exception:
            raise CLIError(str(exception))

    if not_deleted_kv:
        if deleted_kv:
            # Log partial success - display feature flags that failed to be
            # deleted
            logger.error(
                'Delete operation partially succeeded. Unable to delete the following keys: \n')
            not_deleted_ff = []
            for failed_kv in not_deleted_kv:
                failed_ff = map_keyvalue_to_featureflag(
                    failed_kv, show_conditions=False)
                not_deleted_ff.append(failed_ff)
                logger.error(
                    json.dumps(
                        failed_ff,
                        default=lambda o: o.__dict__,
                        indent=2,
                        sort_keys=True,
                        ensure_ascii=False))
        else:
            raise CLIError('Delete operation failed.' + str(http_exception))

    # Convert result list of KeyValue to ist of FeatureFlag
    deleted_ff = []
    for success_kv in deleted_kv:
        success_ff = map_keyvalue_to_featureflag(
            success_kv, show_conditions=False)
        deleted_ff.append(success_ff)

    return deleted_ff


def show_feature(cmd,
                 feature,
                 name=None,
                 label=None,
                 fields=None,
                 connection_string=None):
    key = FEATURE_FLAG_PREFIX + feature
    connection_string = resolve_connection_string(cmd, name, connection_string)
    azconfig_client = AzconfigClient(connection_string)

    try:
        query_options = QueryKeyValueOptions(label=label)
        retrieved_kv = azconfig_client.get_keyvalue(key, query_options)

        if retrieved_kv is None or retrieved_kv.content_type != FEATURE_FLAG_CONTENT_TYPE:
            raise CLIError(
                "The feature flag '{}' does not exist.".format(feature))

        feature_flag = map_keyvalue_to_featureflag(
            keyvalue=retrieved_kv, show_conditions=True)

        # If user has specified fields, we still get all the fields and then
        # filter what we need from the response.
        if fields:
            partial_featureflag = {}
            for field in fields:
                # feature_flag is guaranteed to have all the fields because
                # we validate this in map_keyvalue_to_featureflag()
                # So this line will never throw AttributeError
                partial_featureflag[field.name.lower()] = getattr(
                    feature_flag, field.name.lower())
            return partial_featureflag
        return feature_flag

    except Exception as exception:
        raise CLIError(str(exception))


def list_feature(cmd,
                 feature=None,
                 name=None,
                 label=None,
                 fields=None,
                 connection_string=None,
                 top=None,
                 all_=False):
    connection_string = resolve_connection_string(cmd, name, connection_string)
    azconfig_client = AzconfigClient(connection_string)

    try:
        feature = '*' if feature is None else feature
        retrieved_keyvalues = __list_all_keyvalues(azconfig_client,
                                                   feature=feature,
                                                   label=label)
        retrieved_featureflags = []
        for kv in retrieved_keyvalues:
            retrieved_featureflags.append(
                map_keyvalue_to_featureflag(
                    keyvalue=kv, show_conditions=True))
        filtered_featureflags = []
        count = 0

        if all_:
            top = len(retrieved_featureflags)
        elif top is None:
            top = 100

        for featureflag in retrieved_featureflags:
            if fields:
                partial_featureflags = {}
                for field in fields:
                    # featureflag is guaranteed to have all the fields because
                    # we validate this in map_keyvalue_to_featureflag()
                    # So this line will never throw AttributeError
                    partial_featureflags[field.name.lower()] = getattr(
                        featureflag, field.name.lower())
                filtered_featureflags.append(partial_featureflags)
            else:
                filtered_featureflags.append(featureflag)
            count += 1
            if count >= top:
                break
        return filtered_featureflags

    except Exception as exception:
        raise CLIError(str(exception))


def lock_feature(cmd,
                 feature,
                 name=None,
                 label=None,
                 connection_string=None,
                 yes=False):
    key = FEATURE_FLAG_PREFIX + feature
    connection_string = resolve_connection_string(cmd, name, connection_string)
    azconfig_client = AzconfigClient(connection_string)

    retry_times = 3
    retry_interval = 1
    for i in range(0, retry_times):
        try:
            retrieved_kv = azconfig_client.get_keyvalue(key, QueryKeyValueOptions(label))
        except HTTPException as exception:
            raise CLIError(str(exception))

        if retrieved_kv is None or retrieved_kv.content_type != FEATURE_FLAG_CONTENT_TYPE:
            raise CLIError(
                "The feature '{}' you are trying to lock does not exist.".format(feature))

        confirmation_message = "Are you sure you want to lock the feature '{}'".format(feature)
        user_confirmation(confirmation_message, yes)

        try:
            updated_key_value = azconfig_client.lock_keyvalue(
                retrieved_kv, ModifyKeyValueOptions())
            return map_keyvalue_to_featureflag(
                updated_key_value, show_conditions=False)

        except HTTPException as exception:
            if exception.status == StatusCodes.PRECONDITION_FAILED:
                logger.debug(
                    'Retrying locking %s times with exception: concurrent setting operations',
                    i + 1)
                time.sleep(retry_interval)
            else:
                raise CLIError(str(exception))
        except Exception as exception:
            raise CLIError(str(exception))
    raise CLIError(
        "Failed to lock the feature '{}' due to a conflicting operation.".format(feature))


def unlock_feature(cmd,
                   feature,
                   name=None,
                   label=None,
                   connection_string=None,
                   yes=False):
    key = FEATURE_FLAG_PREFIX + feature
    connection_string = resolve_connection_string(cmd, name, connection_string)
    azconfig_client = AzconfigClient(connection_string)

    retry_times = 3
    retry_interval = 1
    for i in range(0, retry_times):
        try:
            retrieved_kv = azconfig_client.get_keyvalue(key, QueryKeyValueOptions(label))
        except HTTPException as exception:
            raise CLIError(str(exception))

        if retrieved_kv is None or retrieved_kv.content_type != FEATURE_FLAG_CONTENT_TYPE:
            raise CLIError(
                "The feature '{}' you are trying to unlock does not exist.".format(feature))

        confirmation_message = "Are you sure you want to unlock the feature '{}'".format(feature)
        user_confirmation(confirmation_message, yes)

        try:
            updated_key_value = azconfig_client.unlock_keyvalue(
                retrieved_kv, ModifyKeyValueOptions())
            return map_keyvalue_to_featureflag(
                updated_key_value, show_conditions=False)

        except HTTPException as exception:
            if exception.status == StatusCodes.PRECONDITION_FAILED:
                logger.debug(
                    'Retrying unlocking %s times with exception: concurrent setting operations',
                    i + 1)
                time.sleep(retry_interval)
            else:
                raise CLIError(str(exception))
        except Exception as exception:
            raise CLIError(str(exception))
    raise CLIError(
        "Failed to unlock the feature '{}' due to a conflicting operation.".format(feature))


def enable_feature(cmd,
                   feature,
                   name=None,
                   label=None,
                   connection_string=None,
                   yes=False):
    key = FEATURE_FLAG_PREFIX + feature
    connection_string = resolve_connection_string(cmd, name, connection_string)
    azconfig_client = AzconfigClient(connection_string)

    retry_times = 3
    retry_interval = 1
    for i in range(0, retry_times):
        try:
            query_options = QueryKeyValueOptions(label=label)
            retrieved_kv = azconfig_client.get_keyvalue(key, query_options)

            if retrieved_kv is None or retrieved_kv.content_type != FEATURE_FLAG_CONTENT_TYPE:
                raise CLIError(
                    "The feature flag {} does not exist.".format(feature))

            # we make sure that value retrieved is a valid json and only has the fields supported by backend.
            # if it's invalid, we catch appropriate exception that contains
            # detailed message
            feature_flag_value = map_keyvalue_to_featureflagvalue(retrieved_kv)

            feature_flag_value.enabled = True
            confirmation_message = "Are you sure you want to enable this feature '{}'?".format(
                feature)
            user_confirmation(confirmation_message, yes)

            updated_key_value = __update_existing_key_value(azconfig_client=azconfig_client,
                                                            retrieved_kv=retrieved_kv,
                                                            updated_value=json.dumps(feature_flag_value,
                                                                                     default=lambda o: o.__dict__,
                                                                                     ensure_ascii=False))

            return map_keyvalue_to_featureflag(
                keyvalue=updated_key_value, show_conditions=False)

        except HTTPException as exception:
            if exception.status == StatusCodes.PRECONDITION_FAILED:
                logger.debug(
                    'Retrying enabling %s times with exception: concurrent setting operations',
                    i + 1)
                time.sleep(retry_interval)
            else:
                raise CLIError(str(exception))

        except Exception as exception:
            raise CLIError(str(exception))
    raise CLIError(
        "Failed to enable the feature flag '{}' due to a conflicting operation.".format(feature))


def disable_feature(cmd,
                    feature,
                    name=None,
                    label=None,
                    connection_string=None,
                    yes=False):
    key = FEATURE_FLAG_PREFIX + feature
    connection_string = resolve_connection_string(cmd, name, connection_string)
    azconfig_client = AzconfigClient(connection_string)

    retry_times = 3
    retry_interval = 1
    for i in range(0, retry_times):
        try:
            query_options = QueryKeyValueOptions(label=label)
            retrieved_kv = azconfig_client.get_keyvalue(key, query_options)

            if retrieved_kv is None or retrieved_kv.content_type != FEATURE_FLAG_CONTENT_TYPE:
                raise CLIError(
                    "The feature flag {} does not exist.".format(feature))

            # we make sure that value retrieved is a valid json and only has the fields supported by backend.
            # if it's invalid, we catch appropriate exception that contains
            # detailed message
            feature_flag_value = map_keyvalue_to_featureflagvalue(retrieved_kv)

            feature_flag_value.enabled = False
            confirmation_message = "Are you sure you want to disable this feature '{}'?".format(
                feature)
            user_confirmation(confirmation_message, yes)

            updated_key_value = __update_existing_key_value(azconfig_client=azconfig_client,
                                                            retrieved_kv=retrieved_kv,
                                                            updated_value=json.dumps(feature_flag_value,
                                                                                     default=lambda o: o.__dict__,
                                                                                     ensure_ascii=False))

            return map_keyvalue_to_featureflag(
                keyvalue=updated_key_value, show_conditions=False)

        except HTTPException as exception:
            if exception.status == StatusCodes.PRECONDITION_FAILED:
                logger.debug(
                    'Retrying disabling %s times with exception: concurrent setting operations',
                    i + 1)
                time.sleep(retry_interval)
            else:
                raise CLIError(str(exception))

        except Exception as exception:
            raise CLIError(str(exception))
    raise CLIError(
        "Failed to disable the feature flag '{}' due to a conflicting operation.".format(feature))


# Feature Flter commands #


def add_filter(cmd,
               feature,
               filter_name,
               name=None,
               label=None,
               filter_parameters=None,
               yes=False,
               index=None,
               connection_string=None):
    key = FEATURE_FLAG_PREFIX + feature
    connection_string = resolve_connection_string(cmd, name, connection_string)
    azconfig_client = AzconfigClient(connection_string)

    if index is None:
        index = float("-inf")

    # Construct feature filter to be added
    if filter_parameters is None:
        filter_parameters = {}
    new_filter = FeatureFilter(filter_name, filter_parameters)

    retry_times = 3
    retry_interval = 1
    for i in range(0, retry_times):
        try:
            query_options = QueryKeyValueOptions(label=label)
            retrieved_kv = azconfig_client.get_keyvalue(key, query_options)

            if retrieved_kv is None or retrieved_kv.content_type != FEATURE_FLAG_CONTENT_TYPE:
                raise CLIError(
                    "The feature flag {} does not exist.".format(feature))

            # we make sure that value retrieved is a valid json and only has the fields supported by backend.
            # if it's invalid, we catch appropriate exception that contains
            # detailed message
            feature_flag_value = map_keyvalue_to_featureflagvalue(retrieved_kv)
            feature_filters = feature_flag_value.conditions['client_filters']

            entry = json.dumps(new_filter.__dict__, indent=2, ensure_ascii=False)
            confirmation_message = "Are you sure you want to add this filter?\n" + entry
            user_confirmation(confirmation_message, yes)

            # If user has specified index, we insert at that index
            if 0 <= index <= len(feature_filters):
                logger.debug("Adding new filter at index '%s'.\n", index)
                feature_filters.insert(index, new_filter)
            else:
                if index != float("-inf"):
                    logger.debug(
                        "Ignoring the provided index '%s' because it is out of range or invalid.\n", index)
                logger.debug("Adding new filter to the end of list.\n")
                feature_filters.append(new_filter)

            __update_existing_key_value(azconfig_client=azconfig_client,
                                        retrieved_kv=retrieved_kv,
                                        updated_value=json.dumps(feature_flag_value,
                                                                 default=lambda o: o.__dict__,
                                                                 ensure_ascii=False))

            return new_filter

        except HTTPException as exception:
            if exception.status == StatusCodes.PRECONDITION_FAILED:
                logger.debug(
                    'Retrying adding filter %s times with exception: concurrent setting operations',
                    i + 1)
                time.sleep(retry_interval)
            else:
                raise CLIError(str(exception))

        except Exception as exception:
            raise CLIError(str(exception))
    raise CLIError(
        "Failed to add filter for the feature flag '{}' due to a conflicting operation.".format(feature))


def delete_filter(cmd,
                  feature,
                  filter_name=None,
                  name=None,
                  label=None,
                  index=None,
                  yes=False,
                  connection_string=None,
                  all_=False):
    key = FEATURE_FLAG_PREFIX + feature
    azconfig_client = AzconfigClient(resolve_connection_string(cmd, name, connection_string))

    if index is None:
        index = float("-inf")

    if all_:
        return __clear_filter(azconfig_client, feature, label, yes)

    if filter_name is None:
        raise CLIError("Cannot delete filters because filter name is missing. To delete all filters, run the command again with --all option.")

    retry_times = 3
    retry_interval = 1
    for i in range(0, retry_times):
        try:
            retrieved_kv = azconfig_client.get_keyvalue(
                key, QueryKeyValueOptions(label=label))

            if retrieved_kv is None or retrieved_kv.content_type != FEATURE_FLAG_CONTENT_TYPE:
                raise CLIError(
                    "The feature flag {} does not exist.".format(feature))

            # we make sure that value retrieved is a valid json and only has the fields supported by backend.
            # if it's invalid, we catch appropriate exception that contains
            # detailed message
            feature_flag_value = map_keyvalue_to_featureflagvalue(retrieved_kv)
            feature_filters = feature_flag_value.conditions['client_filters']

            display_filter = {}
            match_index = []

            # get all filters where name matches filter_name provided by user
            for idx, feature_filter in enumerate(feature_filters):
                if feature_filter.name == filter_name:
                    if idx == index:
                        # name and index both match this filter - delete it.
                        # create a deep copy of the filter to display to the
                        # user after deletion
                        display_filter = copy.deepcopy(feature_filters[index])

                        confirmation_message = "Are you sure you want to delete this filter?\n" + \
                            json.dumps(display_filter.__dict__, indent=2, ensure_ascii=False)
                        user_confirmation(confirmation_message, yes)

                        del feature_filters[index]
                        break

                    match_index.append(idx)

            if not display_filter:
                # this means we have not deleted the filter yet
                if len(match_index) == 1:
                    if index != float("-inf"):
                        logger.warning("Found filter '%s' at index '%s'. Invalidating provided index '%s'", filter_name, match_index[0], index)

                    display_filter = copy.deepcopy(
                        feature_filters[match_index[0]])

                    confirmation_message = "Are you sure you want to delete this filter?\n" + \
                        json.dumps(display_filter.__dict__, indent=2, ensure_ascii=False)
                    user_confirmation(confirmation_message, yes)

                    del feature_filters[match_index[0]]

                elif len(match_index) > 1:
                    error_msg = "Feature '{0}' contains multiple instances of filter '{1}'. ".format(feature, filter_name) +\
                                "For resolving this conflict run the command again with the filter name and correct zero-based index of the filter you want to delete.\n"
                    raise CLIError(str(error_msg))

                else:
                    raise CLIError(
                        "No filter named '{0}' was found for feature '{1}'".format(filter_name, feature))

            __update_existing_key_value(azconfig_client=azconfig_client,
                                        retrieved_kv=retrieved_kv,
                                        updated_value=json.dumps(feature_flag_value,
                                                                 default=lambda o: o.__dict__,
                                                                 ensure_ascii=False))

            return display_filter

        except HTTPException as exception:
            if exception.status == StatusCodes.PRECONDITION_FAILED:
                logger.debug(
                    'Retrying deleting filter %s times with exception: concurrent setting operations',
                    i + 1)
                time.sleep(retry_interval)
            else:
                raise CLIError(str(exception))

        except Exception as exception:
            raise CLIError(str(exception))
    raise CLIError(
        "Failed to delete filter '{}' for the feature flag '{}' due to a conflicting operation.".format(
            filter_name,
            feature))


def show_filter(cmd,
                feature,
                filter_name,
                index=None,
                name=None,
                label=None,
                connection_string=None):
    key = FEATURE_FLAG_PREFIX + feature
    connection_string = resolve_connection_string(cmd, name, connection_string)
    azconfig_client = AzconfigClient(connection_string)

    if index is None:
        index = float("-inf")

    try:
        query_options = QueryKeyValueOptions(label=label)
        retrieved_kv = azconfig_client.get_keyvalue(key, query_options)

        if retrieved_kv is None or retrieved_kv.content_type != FEATURE_FLAG_CONTENT_TYPE:
            raise CLIError(
                "The feature flag {} does not exist.".format(feature))

        # we make sure that value retrieved is a valid json and only has the fields supported by backend.
        # if it's invalid, we catch appropriate exception that contains
        # detailed message
        feature_flag_value = map_keyvalue_to_featureflagvalue(retrieved_kv)
        feature_filters = feature_flag_value.conditions['client_filters']

        display_filters = []

        # If user has specified index, we use it as secondary check to display
        # a unique filter
        if 0 <= index < len(feature_filters):
            if feature_filters[index].name == filter_name:
                return feature_filters[index]
        if index != float("-inf"):
            logger.warning(
                "Could not find filter with the index provided. Ignoring index and trying to find the filter by name.")

        # get all filters where name matches filter_name provided by user
        display_filters = [
            featurefilter for featurefilter in feature_filters if featurefilter.name == filter_name]

        if not display_filters:
            raise CLIError(
                "No filter named '{0}' was found for feature '{1}'".format(filter_name, feature))
        return display_filters

    except Exception as exception:
        raise CLIError(str(exception))


def list_filter(cmd,
                feature,
                name=None,
                label=None,
                connection_string=None,
                top=None,
                all_=False):
    key = FEATURE_FLAG_PREFIX + feature
    connection_string = resolve_connection_string(cmd, name, connection_string)
    azconfig_client = AzconfigClient(connection_string)

    try:
        query_options = QueryKeyValueOptions(label=label)
        retrieved_kv = azconfig_client.get_keyvalue(key, query_options)

        if retrieved_kv is None or retrieved_kv.content_type != FEATURE_FLAG_CONTENT_TYPE:
            raise CLIError(
                "The feature flag {} does not exist.".format(feature))

        # we make sure that value retrieved is a valid json and only has the fields supported by backend.
        # if it's invalid, we catch appropriate exception that contains
        # detailed message
        feature_flag_value = map_keyvalue_to_featureflagvalue(retrieved_kv)
        feature_filters = feature_flag_value.conditions['client_filters']

        if all_:
            top = len(feature_filters)
        elif top is None:
            top = 100

        return feature_filters[:top]

    except Exception as exception:
        raise CLIError(str(exception))


# Helper functions #


def __clear_filter(azconfig_client,
                   feature,
                   label=None,
                   yes=False):
    key = FEATURE_FLAG_PREFIX + feature

    retry_times = 3
    retry_interval = 1
    for i in range(0, retry_times):
        try:
            query_options = QueryKeyValueOptions(label=label)
            retrieved_kv = azconfig_client.get_keyvalue(key, query_options)

            if retrieved_kv is None or retrieved_kv.content_type != FEATURE_FLAG_CONTENT_TYPE:
                raise CLIError(
                    "The feature flag {} does not exist.".format(feature))

            # we make sure that value retrieved is a valid json and only has the fields supported by backend.
            # if it's invalid, we catch appropriate exception that contains
            # detailed message
            feature_flag_value = map_keyvalue_to_featureflagvalue(retrieved_kv)

            # These fields will never be missing because we validate that
            # in map_keyvalue_to_featureflagvalue
            feature_filters = feature_flag_value.conditions['client_filters']

            # create a deep copy of the filters to display to the user
            # after deletion
            display_filters = []
            if feature_filters:
                confirmation_message = "Are you sure you want to delete all filters for feature '{0}'?\n".format(feature)
                user_confirmation(confirmation_message, yes)

                display_filters = copy.deepcopy(feature_filters)
                # clearing feature_filters list for python 2.7 compatibility
                del feature_filters[:]

                __update_existing_key_value(azconfig_client=azconfig_client,
                                            retrieved_kv=retrieved_kv,
                                            updated_value=json.dumps(feature_flag_value,
                                                                     default=lambda o: o.__dict__,
                                                                     ensure_ascii=False))

            return display_filters

        except HTTPException as exception:
            if exception.status == StatusCodes.PRECONDITION_FAILED:
                logger.debug(
                    'Retrying deleting filters %s times with exception: concurrent setting operations',
                    i + 1)
                time.sleep(retry_interval)
            else:
                raise CLIError(str(exception))

        except Exception as exception:
            raise CLIError(str(exception))
    raise CLIError(
        "Failed to delete filters for the feature flag '{}' due to a conflicting operation.".format(feature))


def __update_existing_key_value(azconfig_client,
                                retrieved_kv,
                                updated_value):
    '''
        To update the value of a pre-existing KeyValue

        Args:
            azconfig_client - AppConfig client making calls to the service
            retrieved_kv - Pre-existing KeyValue object
            updated_value - Value string to be updated

        Return:
            KeyValue object
    '''
    set_kv = KeyValue(key=retrieved_kv.key,
                      value=updated_value,
                      label=retrieved_kv.label,
                      tags=retrieved_kv.tags,
                      content_type=FEATURE_FLAG_CONTENT_TYPE)
    set_kv.etag = retrieved_kv.etag
    set_kv.last_modified = retrieved_kv.last_modified

    try:
        return azconfig_client.update_keyvalue(set_kv, ModifyKeyValueOptions())

    except Exception as exception:
        raise CLIError(str(exception))


def __list_all_keyvalues(azconfig_client,
                         feature,
                         label=None):
    '''
        To get all keys by name or pattern

        Args:
            azconfig_client - AppConfig client making calls to the service
            feature - Feature name or pattern
            label - Feature label or pattern

        Return:
            List of KeyValue objects
    '''

    # We dont support listing comma separated keys and ned to fail with appropriate error
    # (?<!\\)    Matches if the preceding character is not a backslash
    # (?:\\\\)*  Matches any number of occurrences of two backslashes
    # ,          Matches a comma
    unescaped_comma_regex = re.compile(r'(?<!\\)(?:\\\\)*,')
    if unescaped_comma_regex.search(feature):
        raise CLIError("Comma separated feature names are not supported. Please provide escaped string if your feature name contains comma. \nSee \"az appconfig feature list -h\" for correct usage.")

    # Filtering keys on these patterns needs to happen on client side after getting all keys that match user specified pattern
    # If user provides *abc or *abc* or * -> get all keys that match this
    # pattern, then filter based on whether they are feature flags or not
    all_keys_pattern = "*"
    if feature.startswith("*") and feature != all_keys_pattern:
        key = feature
    else:
        key = FEATURE_FLAG_PREFIX + feature

    # If user has specified fields, we still get all the fields and then
    # filter what we need from the response.
    query_option = QueryKeyValueCollectionOptions(
        key_filter=key,
        label_filter=QueryKeyValueCollectionOptions.empty_label if label is not None and not label else label,
        fields=None)
    try:
        retrieved_kv = azconfig_client.get_keyvalues(query_option)
        if key != feature:
            valid_features = []
            for kv in retrieved_kv:
                if kv.content_type == FEATURE_FLAG_CONTENT_TYPE:
                    valid_features.append(kv)
            return valid_features

        return __custom_key_filtering(retrieved_kv=retrieved_kv, user_key_filter=feature)

    except Exception as exception:
        raise CLIError(str(exception))


def __custom_key_filtering(retrieved_kv, user_key_filter):
    '''
        To get all keys after Client side Filtering based on user specified pattern

        Args:
            retrieved_kv - List of KeyValue objects
            user_key_filter - key pattern to be matched

        Return:
            List of KeyValue objects
    '''

    filtered_kv = []
    try:
        user_key_pattern_regex = re.compile(r"." + user_key_filter)
        for kv in retrieved_kv:
            internal_key = kv.key
            internal_content_type = kv.content_type
            # filter only feature flags
            if internal_key.startswith(
                    FEATURE_FLAG_PREFIX) and internal_content_type == FEATURE_FLAG_CONTENT_TYPE:
                feature_name = internal_key[len(FEATURE_FLAG_PREFIX):]
                # search for user pattern in actual feature name
                if user_key_pattern_regex.search(feature_name):
                    filtered_kv.append(kv)
        return filtered_kv

    except re.error as exception:
        error_msg = "Regular expression error in parsing '{0}'. ".format(user_key_filter) +\
            "Please provide escaped string if your feature name contains special characters. \nSee \"az appconfig feature list -h\" for correct usage.\n"
        raise re.error(error_msg + "Error: " + str(exception))

    except AttributeError as exception:
        raise AttributeError(
            "Could not find 'content_type' attribute in the retrieved key-value data.\n" +
            str(exception))

    return filtered_kv
