import json
import re
import time
import pkg_resources
import random
import string

from datetime import datetime

from boto3 import Session

from moto.config.exceptions import (
    InvalidResourceTypeException,
    InvalidDeliveryFrequency,
    InvalidConfigurationRecorderNameException,
    NameTooLongException,
    MaxNumberOfConfigurationRecordersExceededException,
    InvalidRecordingGroupException,
    NoSuchConfigurationRecorderException,
    NoAvailableConfigurationRecorderException,
    InvalidDeliveryChannelNameException,
    NoSuchBucketException,
    InvalidS3KeyPrefixException,
    InvalidSNSTopicARNException,
    MaxNumberOfDeliveryChannelsExceededException,
    NoAvailableDeliveryChannelException,
    NoSuchDeliveryChannelException,
    LastDeliveryChannelDeleteFailedException,
    TagKeyTooBig,
    TooManyTags,
    TagValueTooBig,
    TooManyAccountSources,
    InvalidParameterValueException,
    InvalidNextTokenException,
    NoSuchConfigurationAggregatorException,
    InvalidTagCharacters,
    DuplicateTags,
    InvalidLimit,
    InvalidResourceParameters,
    TooManyResourceIds,
    ResourceNotDiscoveredException,
    TooManyResourceKeys,
    InvalidResultTokenException,
    ValidationException,
    NoSuchOrganizationConformancePackException,
)

from moto.core import BaseBackend, BaseModel
from moto.s3.config import s3_account_public_access_block_query, s3_config_query
from moto.core import ACCOUNT_ID as DEFAULT_ACCOUNT_ID

from moto.iam.config import role_config_query, policy_config_query

POP_STRINGS = [
    "capitalizeStart",
    "CapitalizeStart",
    "capitalizeArn",
    "CapitalizeArn",
    "capitalizeARN",
    "CapitalizeARN",
]
DEFAULT_PAGE_SIZE = 100

# Map the Config resource type to a backend:
RESOURCE_MAP = {
    "AWS::S3::Bucket": s3_config_query,
    "AWS::S3::AccountPublicAccessBlock": s3_account_public_access_block_query,
    "AWS::IAM::Role": role_config_query,
    "AWS::IAM::Policy": policy_config_query,
}


def datetime2int(date):
    return int(time.mktime(date.timetuple()))


def snake_to_camels(original, cap_start, cap_arn):
    parts = original.split("_")

    camel_cased = parts[0].lower() + "".join(p.title() for p in parts[1:])

    if cap_arn:
        camel_cased = camel_cased.replace(
            "Arn", "ARN"
        )  # Some config services use 'ARN' instead of 'Arn'

    if cap_start:
        camel_cased = camel_cased[0].upper() + camel_cased[1::]

    return camel_cased


def random_string():
    """Returns a random set of 8 lowercase letters for the Config Aggregator ARN"""
    chars = []
    for x in range(0, 8):
        chars.append(random.choice(string.ascii_lowercase))

    return "".join(chars)


def validate_tag_key(tag_key, exception_param="tags.X.member.key"):
    """Validates the tag key.

    :param tag_key: The tag key to check against.
    :param exception_param: The exception parameter to send over to help format the message. This is to reflect
                            the difference between the tag and untag APIs.
    :return:
    """
    # Validate that the key length is correct:
    if len(tag_key) > 128:
        raise TagKeyTooBig(tag_key, param=exception_param)

    # Validate that the tag key fits the proper Regex:
    # [\w\s_.:/=+\-@]+ SHOULD be the same as the Java regex on the AWS documentation: [\p{L}\p{Z}\p{N}_.:/=+\-@]+
    match = re.findall(r"[\w\s_.:/=+\-@]+", tag_key)
    # Kudos if you can come up with a better way of doing a global search :)
    if not len(match) or len(match[0]) < len(tag_key):
        raise InvalidTagCharacters(tag_key, param=exception_param)


def check_tag_duplicate(all_tags, tag_key):
    """Validates that a tag key is not a duplicate

    :param all_tags: Dict to check if there is a duplicate tag.
    :param tag_key: The tag key to check against.
    :return:
    """
    if all_tags.get(tag_key):
        raise DuplicateTags()


def validate_tags(tags):
    proper_tags = {}

    if len(tags) > 50:
        raise TooManyTags(tags)

    for tag in tags:
        # Validate the Key:
        validate_tag_key(tag["Key"])
        check_tag_duplicate(proper_tags, tag["Key"])

        # Validate the Value:
        if len(tag["Value"]) > 256:
            raise TagValueTooBig(tag["Value"])

        proper_tags[tag["Key"]] = tag["Value"]

    return proper_tags


class ConfigEmptyDictable(BaseModel):
    """Base class to make serialization easy. This assumes that the sub-class will NOT return 'None's in the JSON."""

    def __init__(self, capitalize_start=False, capitalize_arn=True):
        """Assists with the serialization of the config object
        :param capitalize_start: For some Config services, the first letter is lowercase -- for others it's capital
        :param capitalize_arn: For some Config services, the API expects 'ARN' and for others, it expects 'Arn'
        """
        self.capitalize_start = capitalize_start
        self.capitalize_arn = capitalize_arn

    def to_dict(self):
        data = {}
        for item, value in self.__dict__.items():
            # ignore private attributes
            if not item.startswith("_") and value is not None:
                if isinstance(value, ConfigEmptyDictable):
                    data[
                        snake_to_camels(
                            item, self.capitalize_start, self.capitalize_arn
                        )
                    ] = value.to_dict()
                else:
                    data[
                        snake_to_camels(
                            item, self.capitalize_start, self.capitalize_arn
                        )
                    ] = value

        # Cleanse the extra properties:
        for prop in POP_STRINGS:
            data.pop(prop, None)

        return data


class ConfigRecorderStatus(ConfigEmptyDictable):
    def __init__(self, name):
        super(ConfigRecorderStatus, self).__init__()

        self.name = name
        self.recording = False
        self.last_start_time = None
        self.last_stop_time = None
        self.last_status = None
        self.last_error_code = None
        self.last_error_message = None
        self.last_status_change_time = None

    def start(self):
        self.recording = True
        self.last_status = "PENDING"
        self.last_start_time = datetime2int(datetime.utcnow())
        self.last_status_change_time = datetime2int(datetime.utcnow())

    def stop(self):
        self.recording = False
        self.last_stop_time = datetime2int(datetime.utcnow())
        self.last_status_change_time = datetime2int(datetime.utcnow())


class ConfigDeliverySnapshotProperties(ConfigEmptyDictable):
    def __init__(self, delivery_frequency):
        super(ConfigDeliverySnapshotProperties, self).__init__()

        self.delivery_frequency = delivery_frequency


class ConfigDeliveryChannel(ConfigEmptyDictable):
    def __init__(
        self, name, s3_bucket_name, prefix=None, sns_arn=None, snapshot_properties=None
    ):
        super(ConfigDeliveryChannel, self).__init__()

        self.name = name
        self.s3_bucket_name = s3_bucket_name
        self.s3_key_prefix = prefix
        self.sns_topic_arn = sns_arn
        self.config_snapshot_delivery_properties = snapshot_properties


class RecordingGroup(ConfigEmptyDictable):
    def __init__(
        self,
        all_supported=True,
        include_global_resource_types=False,
        resource_types=None,
    ):
        super(RecordingGroup, self).__init__()

        self.all_supported = all_supported
        self.include_global_resource_types = include_global_resource_types
        self.resource_types = resource_types


class ConfigRecorder(ConfigEmptyDictable):
    def __init__(self, role_arn, recording_group, name="default", status=None):
        super(ConfigRecorder, self).__init__()

        self.name = name
        self.role_arn = role_arn
        self.recording_group = recording_group

        if not status:
            self.status = ConfigRecorderStatus(name)
        else:
            self.status = status


class AccountAggregatorSource(ConfigEmptyDictable):
    def __init__(self, account_ids, aws_regions=None, all_aws_regions=None):
        super(AccountAggregatorSource, self).__init__(capitalize_start=True)

        # Can't have both the regions and all_regions flag present -- also can't have them both missing:
        if aws_regions and all_aws_regions:
            raise InvalidParameterValueException(
                "Your configuration aggregator contains a list of regions and also specifies "
                "the use of all regions. You must choose one of these options."
            )

        if not (aws_regions or all_aws_regions):
            raise InvalidParameterValueException(
                "Your request does not specify any regions. Select AWS Config-supported "
                "regions and try again."
            )

        self.account_ids = account_ids
        self.aws_regions = aws_regions

        if not all_aws_regions:
            all_aws_regions = False

        self.all_aws_regions = all_aws_regions


class OrganizationAggregationSource(ConfigEmptyDictable):
    def __init__(self, role_arn, aws_regions=None, all_aws_regions=None):
        super(OrganizationAggregationSource, self).__init__(
            capitalize_start=True, capitalize_arn=False
        )

        # Can't have both the regions and all_regions flag present -- also can't have them both missing:
        if aws_regions and all_aws_regions:
            raise InvalidParameterValueException(
                "Your configuration aggregator contains a list of regions and also specifies "
                "the use of all regions. You must choose one of these options."
            )

        if not (aws_regions or all_aws_regions):
            raise InvalidParameterValueException(
                "Your request does not specify any regions. Select AWS Config-supported "
                "regions and try again."
            )

        self.role_arn = role_arn
        self.aws_regions = aws_regions

        if not all_aws_regions:
            all_aws_regions = False

        self.all_aws_regions = all_aws_regions


class ConfigAggregator(ConfigEmptyDictable):
    def __init__(self, name, region, account_sources=None, org_source=None, tags=None):
        super(ConfigAggregator, self).__init__(
            capitalize_start=True, capitalize_arn=False
        )

        self.configuration_aggregator_name = name
        self.configuration_aggregator_arn = "arn:aws:config:{region}:{id}:config-aggregator/config-aggregator-{random}".format(
            region=region, id=DEFAULT_ACCOUNT_ID, random=random_string()
        )
        self.account_aggregation_sources = account_sources
        self.organization_aggregation_source = org_source
        self.creation_time = datetime2int(datetime.utcnow())
        self.last_updated_time = datetime2int(datetime.utcnow())

        # Tags are listed in the list_tags_for_resource API call ... not implementing yet -- please feel free to!
        self.tags = tags or {}

    # Override the to_dict so that we can format the tags properly...
    def to_dict(self):
        result = super(ConfigAggregator, self).to_dict()

        # Override the account aggregation sources if present:
        if self.account_aggregation_sources:
            result["AccountAggregationSources"] = [
                a.to_dict() for a in self.account_aggregation_sources
            ]

        # Tags are listed in the list_tags_for_resource API call ... not implementing yet -- please feel free to!
        # if self.tags:
        #     result['Tags'] = [{'Key': key, 'Value': value} for key, value in self.tags.items()]

        return result


class ConfigAggregationAuthorization(ConfigEmptyDictable):
    def __init__(
        self, current_region, authorized_account_id, authorized_aws_region, tags=None
    ):
        super(ConfigAggregationAuthorization, self).__init__(
            capitalize_start=True, capitalize_arn=False
        )

        self.aggregation_authorization_arn = (
            "arn:aws:config:{region}:{id}:aggregation-authorization/"
            "{auth_account}/{auth_region}".format(
                region=current_region,
                id=DEFAULT_ACCOUNT_ID,
                auth_account=authorized_account_id,
                auth_region=authorized_aws_region,
            )
        )
        self.authorized_account_id = authorized_account_id
        self.authorized_aws_region = authorized_aws_region
        self.creation_time = datetime2int(datetime.utcnow())

        # Tags are listed in the list_tags_for_resource API call ... not implementing yet -- please feel free to!
        self.tags = tags or {}


class OrganizationConformancePack(ConfigEmptyDictable):
    def __init__(
        self,
        region,
        name,
        delivery_s3_bucket,
        delivery_s3_key_prefix=None,
        input_parameters=None,
        excluded_accounts=None,
    ):
        super(OrganizationConformancePack, self).__init__(
            capitalize_start=True, capitalize_arn=False
        )

        self._status = "CREATE_SUCCESSFUL"
        self._unique_pack_name = "{0}-{1}".format(name, random_string())

        self.conformance_pack_input_parameters = input_parameters or []
        self.delivery_s3_bucket = delivery_s3_bucket
        self.delivery_s3_key_prefix = delivery_s3_key_prefix
        self.excluded_accounts = excluded_accounts or []
        self.last_update_time = datetime2int(datetime.utcnow())
        self.organization_conformance_pack_arn = "arn:aws:config:{0}:{1}:organization-conformance-pack/{2}".format(
            region, DEFAULT_ACCOUNT_ID, self._unique_pack_name
        )
        self.organization_conformance_pack_name = name

    def update(
        self,
        delivery_s3_bucket,
        delivery_s3_key_prefix,
        input_parameters,
        excluded_accounts,
    ):
        self._status = "UPDATE_SUCCESSFUL"

        self.conformance_pack_input_parameters = input_parameters
        self.delivery_s3_bucket = delivery_s3_bucket
        self.delivery_s3_key_prefix = delivery_s3_key_prefix
        self.excluded_accounts = excluded_accounts
        self.last_update_time = datetime2int(datetime.utcnow())


class ConfigBackend(BaseBackend):
    def __init__(self):
        self.recorders = {}
        self.delivery_channels = {}
        self.config_aggregators = {}
        self.aggregation_authorizations = {}
        self.organization_conformance_packs = {}

    @staticmethod
    def _validate_resource_types(resource_list):
        # Load the service file:
        resource_package = "botocore"
        resource_path = "/".join(("data", "config", "2014-11-12", "service-2.json"))
        config_schema = json.loads(
            pkg_resources.resource_string(resource_package, resource_path)
        )

        # Verify that each entry exists in the supported list:
        bad_list = []
        for resource in resource_list:
            # For PY2:
            r_str = str(resource)

            if r_str not in config_schema["shapes"]["ResourceType"]["enum"]:
                bad_list.append(r_str)

        if bad_list:
            raise InvalidResourceTypeException(
                bad_list, config_schema["shapes"]["ResourceType"]["enum"]
            )

    @staticmethod
    def _validate_delivery_snapshot_properties(properties):
        # Load the service file:
        resource_package = "botocore"
        resource_path = "/".join(("data", "config", "2014-11-12", "service-2.json"))
        conifg_schema = json.loads(
            pkg_resources.resource_string(resource_package, resource_path)
        )

        # Verify that the deliveryFrequency is set to an acceptable value:
        if (
            properties.get("deliveryFrequency", None)
            not in conifg_schema["shapes"]["MaximumExecutionFrequency"]["enum"]
        ):
            raise InvalidDeliveryFrequency(
                properties.get("deliveryFrequency", None),
                conifg_schema["shapes"]["MaximumExecutionFrequency"]["enum"],
            )

    def put_configuration_aggregator(self, config_aggregator, region):
        # Validate the name:
        if len(config_aggregator["ConfigurationAggregatorName"]) > 256:
            raise NameTooLongException(
                config_aggregator["ConfigurationAggregatorName"],
                "configurationAggregatorName",
            )

        account_sources = None
        org_source = None

        # Tag validation:
        tags = validate_tags(config_aggregator.get("Tags", []))

        # Exception if both AccountAggregationSources and OrganizationAggregationSource are supplied:
        if config_aggregator.get("AccountAggregationSources") and config_aggregator.get(
            "OrganizationAggregationSource"
        ):
            raise InvalidParameterValueException(
                "The configuration aggregator cannot be created because your request contains both the"
                " AccountAggregationSource and the OrganizationAggregationSource. Include only "
                "one aggregation source and try again."
            )

        # If neither are supplied:
        if not config_aggregator.get(
            "AccountAggregationSources"
        ) and not config_aggregator.get("OrganizationAggregationSource"):
            raise InvalidParameterValueException(
                "The configuration aggregator cannot be created because your request is missing either "
                "the AccountAggregationSource or the OrganizationAggregationSource. Include the "
                "appropriate aggregation source and try again."
            )

        if config_aggregator.get("AccountAggregationSources"):
            # Currently, only 1 account aggregation source can be set:
            if len(config_aggregator["AccountAggregationSources"]) > 1:
                raise TooManyAccountSources(
                    len(config_aggregator["AccountAggregationSources"])
                )

            account_sources = []
            for a in config_aggregator["AccountAggregationSources"]:
                account_sources.append(
                    AccountAggregatorSource(
                        a["AccountIds"],
                        aws_regions=a.get("AwsRegions"),
                        all_aws_regions=a.get("AllAwsRegions"),
                    )
                )

        else:
            org_source = OrganizationAggregationSource(
                config_aggregator["OrganizationAggregationSource"]["RoleArn"],
                aws_regions=config_aggregator["OrganizationAggregationSource"].get(
                    "AwsRegions"
                ),
                all_aws_regions=config_aggregator["OrganizationAggregationSource"].get(
                    "AllAwsRegions"
                ),
            )

        # Grab the existing one if it exists and update it:
        if not self.config_aggregators.get(
            config_aggregator["ConfigurationAggregatorName"]
        ):
            aggregator = ConfigAggregator(
                config_aggregator["ConfigurationAggregatorName"],
                region,
                account_sources=account_sources,
                org_source=org_source,
                tags=tags,
            )
            self.config_aggregators[
                config_aggregator["ConfigurationAggregatorName"]
            ] = aggregator

        else:
            aggregator = self.config_aggregators[
                config_aggregator["ConfigurationAggregatorName"]
            ]
            aggregator.tags = tags
            aggregator.account_aggregation_sources = account_sources
            aggregator.organization_aggregation_source = org_source
            aggregator.last_updated_time = datetime2int(datetime.utcnow())

        return aggregator.to_dict()

    def describe_configuration_aggregators(self, names, token, limit):
        limit = DEFAULT_PAGE_SIZE if not limit or limit < 0 else limit
        agg_list = []
        result = {"ConfigurationAggregators": []}

        if names:
            for name in names:
                if not self.config_aggregators.get(name):
                    raise NoSuchConfigurationAggregatorException(number=len(names))

                agg_list.append(name)

        else:
            agg_list = list(self.config_aggregators.keys())

        # Empty?
        if not agg_list:
            return result

        # Sort by name:
        sorted_aggregators = sorted(agg_list)

        # Get the start:
        if not token:
            start = 0
        else:
            # Tokens for this moto feature are just the next names of the items in the list:
            if not self.config_aggregators.get(token):
                raise InvalidNextTokenException()

            start = sorted_aggregators.index(token)

        # Get the list of items to collect:
        agg_list = sorted_aggregators[start : (start + limit)]
        result["ConfigurationAggregators"] = [
            self.config_aggregators[agg].to_dict() for agg in agg_list
        ]

        if len(sorted_aggregators) > (start + limit):
            result["NextToken"] = sorted_aggregators[start + limit]

        return result

    def delete_configuration_aggregator(self, config_aggregator):
        if not self.config_aggregators.get(config_aggregator):
            raise NoSuchConfigurationAggregatorException()

        del self.config_aggregators[config_aggregator]

    def put_aggregation_authorization(
        self, current_region, authorized_account, authorized_region, tags
    ):
        # Tag validation:
        tags = validate_tags(tags or [])

        # Does this already exist?
        key = "{}/{}".format(authorized_account, authorized_region)
        agg_auth = self.aggregation_authorizations.get(key)
        if not agg_auth:
            agg_auth = ConfigAggregationAuthorization(
                current_region, authorized_account, authorized_region, tags=tags
            )
            self.aggregation_authorizations[
                "{}/{}".format(authorized_account, authorized_region)
            ] = agg_auth
        else:
            # Only update the tags:
            agg_auth.tags = tags

        return agg_auth.to_dict()

    def describe_aggregation_authorizations(self, token, limit):
        limit = DEFAULT_PAGE_SIZE if not limit or limit < 0 else limit
        result = {"AggregationAuthorizations": []}

        if not self.aggregation_authorizations:
            return result

        # Sort by name:
        sorted_authorizations = sorted(self.aggregation_authorizations.keys())

        # Get the start:
        if not token:
            start = 0
        else:
            # Tokens for this moto feature are just the next names of the items in the list:
            if not self.aggregation_authorizations.get(token):
                raise InvalidNextTokenException()

            start = sorted_authorizations.index(token)

        # Get the list of items to collect:
        auth_list = sorted_authorizations[start : (start + limit)]
        result["AggregationAuthorizations"] = [
            self.aggregation_authorizations[auth].to_dict() for auth in auth_list
        ]

        if len(sorted_authorizations) > (start + limit):
            result["NextToken"] = sorted_authorizations[start + limit]

        return result

    def delete_aggregation_authorization(self, authorized_account, authorized_region):
        # This will always return a 200 -- regardless if there is or isn't an existing
        # aggregation authorization.
        key = "{}/{}".format(authorized_account, authorized_region)
        self.aggregation_authorizations.pop(key, None)

    def put_configuration_recorder(self, config_recorder):
        # Validate the name:
        if not config_recorder.get("name"):
            raise InvalidConfigurationRecorderNameException(config_recorder.get("name"))
        if len(config_recorder.get("name")) > 256:
            raise NameTooLongException(
                config_recorder.get("name"), "configurationRecorder.name"
            )

        # We're going to assume that the passed in Role ARN is correct.

        # Config currently only allows 1 configuration recorder for an account:
        if len(self.recorders) == 1 and not self.recorders.get(config_recorder["name"]):
            raise MaxNumberOfConfigurationRecordersExceededException(
                config_recorder["name"]
            )

        # Is this updating an existing one?
        recorder_status = None
        if self.recorders.get(config_recorder["name"]):
            recorder_status = self.recorders[config_recorder["name"]].status

        # Validate the Recording Group:
        if config_recorder.get("recordingGroup") is None:
            recording_group = RecordingGroup()
        else:
            rg = config_recorder["recordingGroup"]

            # If an empty dict is passed in, then bad:
            if not rg:
                raise InvalidRecordingGroupException()

            # Can't have both the resource types specified and the other flags as True.
            if rg.get("resourceTypes") and (
                rg.get("allSupported", False)
                or rg.get("includeGlobalResourceTypes", False)
            ):
                raise InvalidRecordingGroupException()

            # Must supply resourceTypes if 'allSupported' is not supplied:
            if not rg.get("allSupported") and not rg.get("resourceTypes"):
                raise InvalidRecordingGroupException()

            # Validate that the list provided is correct:
            self._validate_resource_types(rg.get("resourceTypes", []))

            recording_group = RecordingGroup(
                all_supported=rg.get("allSupported", True),
                include_global_resource_types=rg.get(
                    "includeGlobalResourceTypes", False
                ),
                resource_types=rg.get("resourceTypes", []),
            )

        self.recorders[config_recorder["name"]] = ConfigRecorder(
            config_recorder["roleARN"],
            recording_group,
            name=config_recorder["name"],
            status=recorder_status,
        )

    def describe_configuration_recorders(self, recorder_names):
        recorders = []

        if recorder_names:
            for rn in recorder_names:
                if not self.recorders.get(rn):
                    raise NoSuchConfigurationRecorderException(rn)

                # Format the recorder:
                recorders.append(self.recorders[rn].to_dict())

        else:
            for recorder in self.recorders.values():
                recorders.append(recorder.to_dict())

        return recorders

    def describe_configuration_recorder_status(self, recorder_names):
        recorders = []

        if recorder_names:
            for rn in recorder_names:
                if not self.recorders.get(rn):
                    raise NoSuchConfigurationRecorderException(rn)

                # Format the recorder:
                recorders.append(self.recorders[rn].status.to_dict())

        else:
            for recorder in self.recorders.values():
                recorders.append(recorder.status.to_dict())

        return recorders

    def put_delivery_channel(self, delivery_channel):
        # Must have a configuration recorder:
        if not self.recorders:
            raise NoAvailableConfigurationRecorderException()

        # Validate the name:
        if not delivery_channel.get("name"):
            raise InvalidDeliveryChannelNameException(delivery_channel.get("name"))
        if len(delivery_channel.get("name")) > 256:
            raise NameTooLongException(
                delivery_channel.get("name"), "deliveryChannel.name"
            )

        # We are going to assume that the bucket exists -- but will verify if the bucket provided is blank:
        if not delivery_channel.get("s3BucketName"):
            raise NoSuchBucketException()

        # We are going to assume that the bucket has the correct policy attached to it. We are only going to verify
        # if the prefix provided is not an empty string:
        if delivery_channel.get("s3KeyPrefix", None) == "":
            raise InvalidS3KeyPrefixException()

        # Ditto for SNS -- Only going to assume that the ARN provided is not an empty string:
        if delivery_channel.get("snsTopicARN", None) == "":
            raise InvalidSNSTopicARNException()

        # Config currently only allows 1 delivery channel for an account:
        if len(self.delivery_channels) == 1 and not self.delivery_channels.get(
            delivery_channel["name"]
        ):
            raise MaxNumberOfDeliveryChannelsExceededException(delivery_channel["name"])

        if not delivery_channel.get("configSnapshotDeliveryProperties"):
            dp = None

        else:
            # Validate the config snapshot delivery properties:
            self._validate_delivery_snapshot_properties(
                delivery_channel["configSnapshotDeliveryProperties"]
            )

            dp = ConfigDeliverySnapshotProperties(
                delivery_channel["configSnapshotDeliveryProperties"][
                    "deliveryFrequency"
                ]
            )

        self.delivery_channels[delivery_channel["name"]] = ConfigDeliveryChannel(
            delivery_channel["name"],
            delivery_channel["s3BucketName"],
            prefix=delivery_channel.get("s3KeyPrefix", None),
            sns_arn=delivery_channel.get("snsTopicARN", None),
            snapshot_properties=dp,
        )

    def describe_delivery_channels(self, channel_names):
        channels = []

        if channel_names:
            for cn in channel_names:
                if not self.delivery_channels.get(cn):
                    raise NoSuchDeliveryChannelException(cn)

                # Format the delivery channel:
                channels.append(self.delivery_channels[cn].to_dict())

        else:
            for channel in self.delivery_channels.values():
                channels.append(channel.to_dict())

        return channels

    def start_configuration_recorder(self, recorder_name):
        if not self.recorders.get(recorder_name):
            raise NoSuchConfigurationRecorderException(recorder_name)

        # Must have a delivery channel available as well:
        if not self.delivery_channels:
            raise NoAvailableDeliveryChannelException()

        # Start recording:
        self.recorders[recorder_name].status.start()

    def stop_configuration_recorder(self, recorder_name):
        if not self.recorders.get(recorder_name):
            raise NoSuchConfigurationRecorderException(recorder_name)

        # Stop recording:
        self.recorders[recorder_name].status.stop()

    def delete_configuration_recorder(self, recorder_name):
        if not self.recorders.get(recorder_name):
            raise NoSuchConfigurationRecorderException(recorder_name)

        del self.recorders[recorder_name]

    def delete_delivery_channel(self, channel_name):
        if not self.delivery_channels.get(channel_name):
            raise NoSuchDeliveryChannelException(channel_name)

        # Check if a channel is recording -- if so, bad -- (there can only be 1 recorder):
        for recorder in self.recorders.values():
            if recorder.status.recording:
                raise LastDeliveryChannelDeleteFailedException(channel_name)

        del self.delivery_channels[channel_name]

    def list_discovered_resources(
        self,
        resource_type,
        backend_region,
        resource_ids,
        resource_name,
        limit,
        next_token,
    ):
        """This will query against the mocked AWS Config (non-aggregated) listing function that must exist for the resource backend.

        :param resource_type:
        :param backend_region:
        :param ids:
        :param name:
        :param limit:
        :param next_token:
        :return:
        """
        identifiers = []
        new_token = None

        limit = limit or DEFAULT_PAGE_SIZE
        if limit > DEFAULT_PAGE_SIZE:
            raise InvalidLimit(limit)

        if resource_ids and resource_name:
            raise InvalidResourceParameters()

        # Only 20 maximum Resource IDs:
        if resource_ids and len(resource_ids) > 20:
            raise TooManyResourceIds()

        # If the resource type exists and the backend region is implemented in moto, then
        # call upon the resource type's Config Query class to retrieve the list of resources that match the criteria:
        if RESOURCE_MAP.get(resource_type, {}):
            # Is this a global resource type? -- if so, re-write the region to 'global':
            backend_query_region = (
                backend_region  # Always provide the backend this request arrived from.
            )
            if RESOURCE_MAP[resource_type].backends.get("global"):
                backend_region = "global"

            # For non-aggregated queries, the we only care about the backend_region. Need to verify that moto has implemented
            # the region for the given backend:
            if RESOURCE_MAP[resource_type].backends.get(backend_region):
                # Fetch the resources for the backend's region:
                identifiers, new_token = RESOURCE_MAP[
                    resource_type
                ].list_config_service_resources(
                    resource_ids,
                    resource_name,
                    limit,
                    next_token,
                    backend_region=backend_query_region,
                )

        resource_identifiers = []
        for identifier in identifiers:
            item = {"resourceType": identifier["type"], "resourceId": identifier["id"]}

            # Some resource types lack names:
            if identifier.get("name"):
                item["resourceName"] = identifier["name"]

            resource_identifiers.append(item)

        result = {"resourceIdentifiers": resource_identifiers}

        if new_token:
            result["nextToken"] = new_token

        return result

    def list_aggregate_discovered_resources(
        self, aggregator_name, resource_type, filters, limit, next_token
    ):
        """This will query against the mocked AWS Config listing function that must exist for the resource backend.

            As far a moto goes -- the only real difference between this function and the `list_discovered_resources` function is that
            this will require a Config Aggregator be set up a priori and can search based on resource regions.

        :param aggregator_name:
        :param resource_type:
        :param filters:
        :param limit:
        :param next_token:
        :return:
        """
        if not self.config_aggregators.get(aggregator_name):
            raise NoSuchConfigurationAggregatorException()

        identifiers = []
        new_token = None
        filters = filters or {}

        limit = limit or DEFAULT_PAGE_SIZE
        if limit > DEFAULT_PAGE_SIZE:
            raise InvalidLimit(limit)

        # If the resource type exists and the backend region is implemented in moto, then
        # call upon the resource type's Config Query class to retrieve the list of resources that match the criteria:
        if RESOURCE_MAP.get(resource_type, {}):
            # We only care about a filter's Region, Resource Name, and Resource ID:
            resource_region = filters.get("Region")
            resource_id = [filters["ResourceId"]] if filters.get("ResourceId") else None
            resource_name = filters.get("ResourceName")

            identifiers, new_token = RESOURCE_MAP[
                resource_type
            ].list_config_service_resources(
                resource_id,
                resource_name,
                limit,
                next_token,
                resource_region=resource_region,
                aggregator=self.config_aggregators.get(aggregator_name).__dict__,
            )

        resource_identifiers = []
        for identifier in identifiers:
            item = {
                "SourceAccountId": DEFAULT_ACCOUNT_ID,
                "SourceRegion": identifier["region"],
                "ResourceType": identifier["type"],
                "ResourceId": identifier["id"],
            }
            if identifier.get("name"):
                item["ResourceName"] = identifier["name"]

            resource_identifiers.append(item)

        result = {"ResourceIdentifiers": resource_identifiers}

        if new_token:
            result["NextToken"] = new_token

        return result

    def get_resource_config_history(self, resource_type, id, backend_region):
        """Returns the configuration of an item in the AWS Config format of the resource for the current regional backend.

        NOTE: This is --NOT-- returning history as it is not supported in moto at this time. (PR's welcome!)
              As such, the later_time, earlier_time, limit, and next_token are ignored as this will only
              return 1 item. (If no items, it raises an exception)
        """
        # If the type isn't implemented then we won't find the item:
        if resource_type not in RESOURCE_MAP:
            raise ResourceNotDiscoveredException(resource_type, id)

        # Is the resource type global?
        backend_query_region = (
            backend_region  # Always provide the backend this request arrived from.
        )
        if RESOURCE_MAP[resource_type].backends.get("global"):
            backend_region = "global"

        # If the backend region isn't implemented then we won't find the item:
        if not RESOURCE_MAP[resource_type].backends.get(backend_region):
            raise ResourceNotDiscoveredException(resource_type, id)

        # Get the item:
        item = RESOURCE_MAP[resource_type].get_config_resource(
            id, backend_region=backend_query_region
        )
        if not item:
            raise ResourceNotDiscoveredException(resource_type, id)

        item["accountId"] = DEFAULT_ACCOUNT_ID

        return {"configurationItems": [item]}

    def batch_get_resource_config(self, resource_keys, backend_region):
        """Returns the configuration of an item in the AWS Config format of the resource for the current regional backend.

        :param resource_keys:
        :param backend_region:
        """
        # Can't have more than 100 items
        if len(resource_keys) > 100:
            raise TooManyResourceKeys(
                ["com.amazonaws.starling.dove.ResourceKey@12345"] * len(resource_keys)
            )

        results = []
        for resource in resource_keys:
            # Does the resource type exist?
            if not RESOURCE_MAP.get(resource["resourceType"]):
                # Not found so skip.
                continue

            # Is the resource type global?
            config_backend_region = backend_region
            backend_query_region = (
                backend_region  # Always provide the backend this request arrived from.
            )
            if RESOURCE_MAP[resource["resourceType"]].backends.get("global"):
                config_backend_region = "global"

            # If the backend region isn't implemented then we won't find the item:
            if not RESOURCE_MAP[resource["resourceType"]].backends.get(
                config_backend_region
            ):
                continue

            # Get the item:
            item = RESOURCE_MAP[resource["resourceType"]].get_config_resource(
                resource["resourceId"], backend_region=backend_query_region
            )
            if not item:
                continue

            item["accountId"] = DEFAULT_ACCOUNT_ID

            results.append(item)

        return {
            "baseConfigurationItems": results,
            "unprocessedResourceKeys": [],
        }  # At this time, moto is not adding unprocessed items.

    def batch_get_aggregate_resource_config(
        self, aggregator_name, resource_identifiers
    ):
        """Returns the configuration of an item in the AWS Config format of the resource for the current regional backend.

        As far a moto goes -- the only real difference between this function and the `batch_get_resource_config` function is that
        this will require a Config Aggregator be set up a priori and can search based on resource regions.

        Note: moto will IGNORE the resource account ID in the search query.
        """
        if not self.config_aggregators.get(aggregator_name):
            raise NoSuchConfigurationAggregatorException()

        # Can't have more than 100 items
        if len(resource_identifiers) > 100:
            raise TooManyResourceKeys(
                ["com.amazonaws.starling.dove.AggregateResourceIdentifier@12345"]
                * len(resource_identifiers)
            )

        found = []
        not_found = []
        for identifier in resource_identifiers:
            resource_type = identifier["ResourceType"]
            resource_region = identifier["SourceRegion"]
            resource_id = identifier["ResourceId"]
            resource_name = identifier.get("ResourceName", None)

            # Does the resource type exist?
            if not RESOURCE_MAP.get(resource_type):
                not_found.append(identifier)
                continue

            # Get the item:
            item = RESOURCE_MAP[resource_type].get_config_resource(
                resource_id,
                resource_name=resource_name,
                resource_region=resource_region,
            )
            if not item:
                not_found.append(identifier)
                continue

            item["accountId"] = DEFAULT_ACCOUNT_ID

            # The 'tags' field is not included in aggregate results for some reason...
            item.pop("tags", None)

            found.append(item)

        return {
            "BaseConfigurationItems": found,
            "UnprocessedResourceIdentifiers": not_found,
        }

    def put_evaluations(self, evaluations=None, result_token=None, test_mode=False):
        if not evaluations:
            raise InvalidParameterValueException(
                "The Evaluations object in your request cannot be null."
                "Add the required parameters and try again."
            )

        if not result_token:
            raise InvalidResultTokenException()

        # Moto only supports PutEvaluations with test mode currently (missing rule and token support)
        if not test_mode:
            raise NotImplementedError(
                "PutEvaluations without TestMode is not yet implemented"
            )

        return {
            "FailedEvaluations": [],
        }  # At this time, moto is not adding failed evaluations.

    def put_organization_conformance_pack(
        self,
        region,
        name,
        template_s3_uri,
        template_body,
        delivery_s3_bucket,
        delivery_s3_key_prefix,
        input_parameters,
        excluded_accounts,
    ):
        # a real validation of the content of the template is missing at the moment
        if not template_s3_uri and not template_body:
            raise ValidationException("Template body is invalid")

        if not re.match(r"s3://.*", template_s3_uri):
            raise ValidationException(
                "1 validation error detected: "
                "Value '{}' at 'templateS3Uri' failed to satisfy constraint: "
                "Member must satisfy regular expression pattern: "
                "s3://.*".format(template_s3_uri)
            )

        pack = self.organization_conformance_packs.get(name)

        if pack:
            pack.update(
                delivery_s3_bucket=delivery_s3_bucket,
                delivery_s3_key_prefix=delivery_s3_key_prefix,
                input_parameters=input_parameters,
                excluded_accounts=excluded_accounts,
            )
        else:
            pack = OrganizationConformancePack(
                region=region,
                name=name,
                delivery_s3_bucket=delivery_s3_bucket,
                delivery_s3_key_prefix=delivery_s3_key_prefix,
                input_parameters=input_parameters,
                excluded_accounts=excluded_accounts,
            )

        self.organization_conformance_packs[name] = pack

        return {
            "OrganizationConformancePackArn": pack.organization_conformance_pack_arn
        }

    def describe_organization_conformance_packs(self, names):
        packs = []

        for name in names:
            pack = self.organization_conformance_packs.get(name)

            if not pack:
                raise NoSuchOrganizationConformancePackException(
                    "One or more organization conformance packs with specified names are not present. "
                    "Ensure your names are correct and try your request again later."
                )

            packs.append(pack.to_dict())

        return {"OrganizationConformancePacks": packs}

    def describe_organization_conformance_pack_statuses(self, names):
        packs = []
        statuses = []

        if names:
            for name in names:
                pack = self.organization_conformance_packs.get(name)

                if not pack:
                    raise NoSuchOrganizationConformancePackException(
                        "One or more organization conformance packs with specified names are not present. "
                        "Ensure your names are correct and try your request again later."
                    )

                packs.append(pack)
        else:
            packs = list(self.organization_conformance_packs.values())

        for pack in packs:
            statuses.append(
                {
                    "OrganizationConformancePackName": pack.organization_conformance_pack_name,
                    "Status": pack._status,
                    "LastUpdateTime": pack.last_update_time,
                }
            )

        return {"OrganizationConformancePackStatuses": statuses}

    def get_organization_conformance_pack_detailed_status(self, name):
        pack = self.organization_conformance_packs.get(name)

        if not pack:
            raise NoSuchOrganizationConformancePackException(
                "One or more organization conformance packs with specified names are not present. "
                "Ensure your names are correct and try your request again later."
            )

        # actually here would be a list of all accounts in the organization
        statuses = [
            {
                "AccountId": DEFAULT_ACCOUNT_ID,
                "ConformancePackName": "OrgConformsPack-{0}".format(
                    pack._unique_pack_name
                ),
                "Status": pack._status,
                "LastUpdateTime": datetime2int(datetime.utcnow()),
            }
        ]

        return {"OrganizationConformancePackDetailedStatuses": statuses}

    def delete_organization_conformance_pack(self, name):
        pack = self.organization_conformance_packs.get(name)

        if not pack:
            raise NoSuchOrganizationConformancePackException(
                "Could not find an OrganizationConformancePack for given request with resourceName {}".format(
                    name
                )
            )

        self.organization_conformance_packs.pop(name)


config_backends = {}
for region in Session().get_available_regions("config"):
    config_backends[region] = ConfigBackend()
for region in Session().get_available_regions("config", partition_name="aws-us-gov"):
    config_backends[region] = ConfigBackend()
for region in Session().get_available_regions("config", partition_name="aws-cn"):
    config_backends[region] = ConfigBackend()
