#!/usr/bin/python
# -*- coding: utf-8 -*-

# Copyright (c) 2017 Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)

DOCUMENTATION = r"""
---

version_added: 1.0.0
module: cloudfront_distribution

short_description: Create, update and delete AWS CloudFront distributions

description:
  - Allows for easy creation, updating and deletion of CloudFront distributions.

author:
  - Willem van Ketwich (@wilvk)
  - Will Thames (@willthames)

options:

    state:
      description:
        - The desired state of the distribution.
        - I(state=present) creates a new distribution or updates an existing distribution.
        - I(state=absent) deletes an existing distribution.
      choices: ['present', 'absent']
      default: 'present'
      type: str

    distribution_id:
      description:
        - The ID of the CloudFront distribution.
        - This parameter can be exchanged with I(alias) or I(caller_reference) and is used in conjunction with I(e_tag).
      type: str

    e_tag:
      description:
        - A unique identifier of a modified or existing distribution. Used in conjunction with I(distribution_id).
        - Is determined automatically if not specified.
      type: str

    caller_reference:
      description:
        - A unique identifier for creating and updating CloudFront distributions.
        - Each caller reference must be unique across all distributions. e.g. a caller reference used in a web
          distribution cannot be reused in a streaming distribution. This parameter can be used instead of I(distribution_id)
          to reference an existing distribution. If not specified, this defaults to a datetime stamp of the format
          C(YYYY-MM-DDTHH:MM:SS.ffffff).
      type: str

    alias:
      description:
        - The name of an alias (CNAME) that is used in a distribution. This is used to effectively reference a distribution by its alias as an alias can only
          be used by one distribution per AWS account. This variable avoids having to provide the I(distribution_id) as well as
          the I(e_tag), or I(caller_reference) of an existing distribution.
      type: str

    aliases:
      description:
        - A list of domain name aliases (CNAMEs) as strings to be used for the distribution.
        - Each alias must be unique across all distribution for the AWS account.
      type: list
      elements: str
      default: []

    purge_aliases:
      description:
        - Specifies whether existing aliases will be removed before adding new aliases.
        - When I(purge_aliases=true), existing aliases are removed and I(aliases) are added.
      default: false
      type: bool

    default_root_object:
      description:
        - A config element that specifies the path to request when the user requests the origin.
        - e.g. if specified as 'index.html', this maps to www.example.com/index.html when www.example.com is called by the user.
        - This prevents the entire distribution origin from being exposed at the root.
      type: str

    default_origin_domain_name:
      description:
        - The domain name to use for an origin if no I(origins) have been specified.
        - Should only be used on a first run of generating a distribution and not on
          subsequent runs.
        - Should not be used in conjunction with I(distribution_id), I(caller_reference) or I(alias).
      type: str

    default_origin_path:
      description:
        - The default origin path to specify for an origin if no I(origins) have been specified. Defaults to empty if not specified.
      type: str

    origins:
      type: list
      elements: dict
      description:
        - A config element that is a list of complex origin objects to be specified for the distribution. Used for creating and updating distributions.
      suboptions:
        id:
          description: A unique identifier for the origin or origin group. I(id) must be unique within the distribution.
          type: str
        domain_name:
          description:
            - The domain name which CloudFront will query as the origin.
            - For more information see the CloudFront documentation
              at U(https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/distribution-web-values-specify.html#DownloadDistValuesDomainName)
          type: str
        origin_path:
          description: Tells CloudFront to request your content from a directory in your Amazon S3 bucket or your custom origin.
          type: str
        origin_shield:
          description: Specify origin shield options for the origin.
          type: dict
          suboptions:
            enabled:
              description: Indicate whether you want the origin to have Origin Shield enabled or not.
              type: bool
            origin_shield_region:
              description: Specify which AWS region will be used for Origin Shield. Required if Origin Shield is enabled.
              type: str
          version_added: 6.0.0
        custom_headers:
          description:
            - Custom headers you wish to add to the request before passing it to the origin.
            - For more information see the CloudFront documentation
              at U(https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/forward-custom-headers.html).
          type: list
          elements: dict
          suboptions:
            header_name:
              description: The name of a header that you want CloudFront to forward to your origin.
              type: str
            header_value:
              description: The value for the header that you specified in the I(header_name) field.
              type: str
        s3_origin_access_identity_enabled:
          description:
            - Use an origin access identity to configure the origin so that viewers can only access objects in an Amazon S3 bucket through CloudFront.
            - Will automatically create an Identity for you if no I(s3_origin_config) is specified.
            - See also U(https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/PrivateContent.html).
          type: bool
        s3_origin_config:
          description: Specify origin access identity for S3 origins.
          type: dict
          suboptions:
            origin_access_identity:
              description: Existing origin access identity in the format C(origin-access-identity/cloudfront/OID_ID).
              type: str
        custom_origin_config:
          description: Connection information about the origin.
          type: dict
          suboptions:
            http_port:
              description: The HTTP port the custom origin listens on.
              type: int
            https_port:
              description: The HTTPS port the custom origin listens on.
              type: int
            origin_protocol_policy:
              description: The origin protocol policy to apply to your origin.
              type: str
            origin_ssl_protocols:
              description: A list of SSL/TLS protocols that you want CloudFront to use when communicating to the origin over HTTPS.
              type: list
              elements: str
            origin_read_timeout:
              description: A timeout (in seconds) when reading from your origin.
              type: int
            origin_keepalive_timeout:
              description: A keep-alive timeout (in seconds).
              type: int
        connection_attempts:
          description: The number of times that CloudFront attempts to connect to the origin.
                       The minimum number is C(1), the maximum is C(3).
          type: int
          default: 3
          version_added: 6.0.0
        connection_timeout:
          description: The number of seconds that CloudFront waits when trying to establish a connection to the origin.
                       The minimum timeout is C(1) second, the maximum is C(10) seconds.
          type: int
          default: 10
          version_added: 6.0.0
    purge_origins:
      description: Whether to remove any origins that aren't listed in I(origins).
      default: false
      type: bool

    default_cache_behavior:
      type: dict
      description:
        - A dict specifying the default cache behavior of the distribution.
        - If not specified, the I(target_origin_id) is defined as the I(target_origin_id) of the first valid
          cache_behavior in I(cache_behaviors) with defaults.
      suboptions:
        target_origin_id:
          description:
            - The ID of the origin that you want CloudFront to route requests to
              by default.
          type: str
        response_headers_policy_id:
          description:
            - The ID of the header policy that CloudFront adds to responses that it sends to viewers.
          type: str
        cache_policy_id:
          version_added: 7.1.0
          description:
            - The ID of the cache policy for CloudFront to use for the default cache behavior.
            - A behavior should use either a C(cache_policy_id) or a C(forwarded_values) option.
            - For more information see the CloudFront documentation
              at U(https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/controlling-the-cache-key.html)
          type: str
        origin_request_policy_id:
          version_added: 7.1.0
          description:
            - The ID of the origin request policy for CloudFront to use for the default cache behavior.
            - For more information see the CloudFront documentation
              at U(https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/controlling-origin-requests.html)
          type: str
        forwarded_values:
          description:
            - A dict that specifies how CloudFront handles query strings and cookies.
            - A behavior should use either a C(cache_policy_id) or a C(forwarded_values) option.
          type: dict
          suboptions:
            query_string:
              description:
                - Indicates whether you want CloudFront to forward query strings
                  to the origin that is associated with this cache behavior.
              type: bool
            cookies:
              description: A dict that specifies whether you want CloudFront to forward cookies to the origin and, if so, which ones.
              type: dict
              suboptions:
                forward:
                  description:
                    - Specifies which cookies to forward to the origin for this cache behavior.
                    - Valid values are C(all), C(none), or C(whitelist).
                  type: str
                whitelisted_names:
                  type: list
                  elements: str
                  description: A list of cookies to forward to the origin for this cache behavior.
            headers:
              description:
              - A list of headers to forward to the origin for this cache behavior.
              - To forward all headers use a list containing a single element '*' (C(['*']))
              type: list
              elements: str
            query_string_cache_keys:
              description:
                - A list that contains the query string parameters you want CloudFront to use as a basis for caching for a cache behavior.
              type: list
              elements: str
        trusted_signers:
          description:
            - A dict that specifies the AWS accounts that you want to allow to create signed URLs for private content.
          type: dict
          suboptions:
            enabled:
              description: Whether you want to require viewers to use signed URLs to access the files specified by I(target_origin_id)
              type: bool
            items:
              description: A list of trusted signers for this cache behavior.
              elements: str
              type: list
        viewer_protocol_policy:
          description:
            - The protocol that viewers can use to access the files in the origin specified by I(target_origin_id).
            - Valid values are C(allow-all), C(redirect-to-https) and C(https-only).
          type: str
        default_ttl:
          description: The default amount of time that you want objects to stay in CloudFront caches.
          type: int
        max_ttl:
          description: The maximum amount of time that you want objects to stay in CloudFront caches.
          type: int
        min_ttl:
          description: The minimum amount of time that you want objects to stay in CloudFront caches.
          type: int
        allowed_methods:
          description: A dict that controls which HTTP methods CloudFront processes and forwards.
          type: dict
          suboptions:
            items:
              description: A list of HTTP methods that you want CloudFront to process and forward.
              type: list
              elements: str
            cached_methods:
              description:
                - A list of HTTP methods that you want CloudFront to apply caching to.
                - This can either be C([GET,HEAD]), or C([GET,HEAD,OPTIONS]).
              type: list
              elements: str
        smooth_streaming:
          description:
            - Whether you want to distribute media files in the Microsoft Smooth Streaming format.
          type: bool
        compress:
          description:
            - Whether you want CloudFront to automatically compress files.
          type: bool
        lambda_function_associations:
          description:
            - A list of Lambda function associations to use for this cache behavior.
          type: list
          elements: dict
          suboptions:
            lambda_function_arn:
              description: The ARN of the Lambda function.
              type: str
            event_type:
              description:
                - Specifies the event type that triggers a Lambda function invocation.
                - This can be C(viewer-request), C(origin-request), C(origin-response) or C(viewer-response).
              type: str
        field_level_encryption_id:
          description:
            - The field-level encryption configuration that you want CloudFront to use for encrypting specific fields of data.
          type: str

    cache_behaviors:
      type: list
      elements: dict
      description:
        - A list of dictionaries describing the cache behaviors for the distribution.
        - The order of the list is preserved across runs unless I(purge_cache_behaviors) is enabled.
      suboptions:
        path_pattern:
          description:
            - The pattern that specifies which requests to apply the behavior to.
          type: str
        target_origin_id:
          description:
            - The ID of the origin that you want CloudFront to route requests to
              by default.
          type: str
        response_headers_policy_id:
          description:
            - The ID of the header policy that CloudFront adds to responses that it sends to viewers.
          type: str
        cache_policy_id:
          version_added: 7.1.0
          description:
            - The ID of the cache policy for CloudFront to use for the cache behavior.
            - A behavior should use either a C(cache_policy_id) or a C(forwarded_values) option.
            - For more information see the CloudFront documentation
              at U(https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/controlling-the-cache-key.html)
          type: str
        origin_request_policy_id:
          version_added: 7.1.0
          description:
            - The ID of the origin request policy for CloudFront to use for the cache behavior.
            - For more information see the CloudFront documentation
              at U(https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/controlling-origin-requests.html)
          type: str
        forwarded_values:
          description:
            - A dict that specifies how CloudFront handles query strings and cookies.
            - A behavior should use either a C(cache_policy_id) or a C(forwarded_values) option.
          type: dict
          suboptions:
            query_string:
              description:
                - Indicates whether you want CloudFront to forward query strings
                  to the origin that is associated with this cache behavior.
              type: bool
            cookies:
              description: A dict that specifies whether you want CloudFront to forward cookies to the origin and, if so, which ones.
              type: dict
              suboptions:
                forward:
                  description:
                    - Specifies which cookies to forward to the origin for this cache behavior.
                    - Valid values are C(all), C(none), or C(whitelist).
                  type: str
                whitelisted_names:
                  type: list
                  elements: str
                  description: A list of cookies to forward to the origin for this cache behavior.
            headers:
              description:
              - A list of headers to forward to the origin for this cache behavior.
              - To forward all headers use a list containing a single element '*' (C(['*']))
              type: list
              elements: str
            query_string_cache_keys:
              description:
                - A list that contains the query string parameters you want CloudFront to use as a basis for caching for a cache behavior.
              type: list
              elements: str
        trusted_signers:
          description:
            - A dict that specifies the AWS accounts that you want to allow to create signed URLs for private content.
          type: dict
          suboptions:
            enabled:
              description: Whether you want to require viewers to use signed URLs to access the files specified by I(path_pattern) and I(target_origin_id)
              type: bool
            items:
              description: A list of trusted signers for this cache behavior.
              elements: str
              type: list
        viewer_protocol_policy:
          description:
            - The protocol that viewers can use to access the files in the origin specified by I(target_origin_id) when a request matches I(path_pattern).
            - Valid values are C(allow-all), C(redirect-to-https) and C(https-only).
          type: str
        default_ttl:
          description: The default amount of time that you want objects to stay in CloudFront caches.
          type: int
        max_ttl:
          description: The maximum amount of time that you want objects to stay in CloudFront caches.
          type: int
        min_ttl:
          description: The minimum amount of time that you want objects to stay in CloudFront caches.
          type: int
        allowed_methods:
          description: A dict that controls which HTTP methods CloudFront processes and forwards.
          type: dict
          suboptions:
            items:
              description: A list of HTTP methods that you want CloudFront to process and forward.
              type: list
              elements: str
            cached_methods:
              description:
                - A list of HTTP methods that you want CloudFront to apply caching to.
                - This can either be C([GET,HEAD]), or C([GET,HEAD,OPTIONS]).
              type: list
              elements: str
        smooth_streaming:
          description:
            - Whether you want to distribute media files in the Microsoft Smooth Streaming format.
          type: bool
        compress:
          description:
            - Whether you want CloudFront to automatically compress files.
          type: bool
        lambda_function_associations:
          description:
            - A list of Lambda function associations to use for this cache behavior.
          type: list
          elements: dict
          suboptions:
            lambda_function_arn:
              description: The ARN of the Lambda function.
              type: str
            event_type:
              description:
                - Specifies the event type that triggers a Lambda function invocation.
                - This can be C(viewer-request), C(origin-request), C(origin-response) or C(viewer-response).
              type: str
        field_level_encryption_id:
          description:
            - The field-level encryption configuration that you want CloudFront to use for encrypting specific fields of data.
          type: str


    purge_cache_behaviors:
      description:
        - Whether to remove any cache behaviors that aren't listed in I(cache_behaviors).
        - This switch also allows the reordering of I(cache_behaviors).
      default: false
      type: bool

    custom_error_responses:
      type: list
      elements: dict
      description:
        - A config element that is a I(list[]) of complex custom error responses to be specified for the distribution.
        - This attribute configures custom http error messages returned to the user.
      suboptions:
        error_code:
          type: int
          description: The error code the custom error page is for.
        error_caching_min_ttl:
          type: int
          description: The length of time (in seconds) that CloudFront will cache status codes for.
        response_code:
          type: int
          description:
            - The HTTP status code that CloudFront should return to a user when the origin returns the HTTP status code specified by I(error_code).
        response_page_path:
          type: str
          description:
            - The path to the custom error page that you want CloudFront to return to a viewer when your origin returns
              the HTTP status code specified by I(error_code).

    purge_custom_error_responses:
      description: Whether to remove any custom error responses that aren't listed in I(custom_error_responses).
      default: false
      type: bool

    comment:
      description:
        - A comment that describes the CloudFront distribution.
        - If not specified, it defaults to a generic message that it has been created with Ansible, and a datetime stamp.
      type: str

    logging:
      description:
        - A config element that is a complex object that defines logging for the distribution.
      suboptions:
        enabled:
          description: When I(enabled=true) CloudFront will log access to an S3 bucket.
          type: bool
        include_cookies:
          description: When I(include_cookies=true) CloudFront will include cookies in the logs.
          type: bool
        bucket:
          description: The S3 bucket to store the log in.
          type: str
        prefix:
          description: A prefix to include in the S3 object names.
          type: str
      type: dict

    price_class:
      description:
        - A string that specifies the pricing class of the distribution. As per
          U(https://aws.amazon.com/cloudfront/pricing/)
        - I(price_class=PriceClass_100) consists of the areas United States, Canada and Europe.
        - I(price_class=PriceClass_200) consists of the areas United States, Canada, Europe, Japan, India,
          Hong Kong, Philippines, S. Korea, Singapore & Taiwan.
        - I(price_class=PriceClass_All) consists of the areas United States, Canada, Europe, Japan, India,
          South America, Australia, Hong Kong, Philippines, S. Korea, Singapore & Taiwan.
        - AWS defaults this to C(PriceClass_All).
        - Valid values are C(PriceClass_100), C(PriceClass_200) and C(PriceClass_All)
      type: str

    enabled:
      description:
        - A boolean value that specifies whether the distribution is enabled or disabled.
        - Defaults to C(false).
      type: bool

    viewer_certificate:
      type: dict
      description:
        - A dict that specifies the encryption details of the distribution.
      suboptions:
        cloudfront_default_certificate:
          type: bool
          description:
            - If you're using the CloudFront domain name for your distribution, such as C(123456789abcde.cloudfront.net)
              you should set I(cloudfront_default_certificate=true).
            - If I(cloudfront_default_certificate=true) do not set I(ssl_support_method).
        iam_certificate_id:
          type: str
          description:
            - The ID of a certificate stored in IAM to use for HTTPS connections.
            - If I(iam_certificate_id) is set then you must also specify I(ssl_support_method).
        acm_certificate_arn:
          type: str
          description:
            - The ID of a certificate stored in ACM to use for HTTPS connections.
            - If I(acm_certificate_id) is set then you must also specify I(ssl_support_method).
        ssl_support_method:
          type: str
          description:
            - How CloudFront should serve SSL certificates.
            - Valid values are C(sni-only) for SNI, and C(vip) if CloudFront is configured to use a dedicated IP for your content.
        minimum_protocol_version:
          type: str
          description:
            - The security policy that you want CloudFront to use for HTTPS connections.
            - See U(https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/secure-connections-supported-viewer-protocols-ciphers.html)
              for supported security policies.

    restrictions:
      type: dict
      description:
        - A config element that is a complex object that describes how a distribution should restrict it's content.
      suboptions:
        geo_restriction:
          description: Apply a restriction based on the location of the requester.
          type: dict
          suboptions:
            restriction_type:
              type: str
              description:
              - The method that you want to use to restrict distribution of your content by country.
              - Valid values are C(none), C(whitelist), C(blacklist).
            items:
              description:
              - A list of ISO 3166-1 two letter (Alpha 2) country codes that the
                restriction should apply to.
              - 'See the ISO website for a full list of codes U(https://www.iso.org/obp/ui/#search/code/).'
              type: list
              elements: str

    web_acl_id:
      description:
        - The ID of a Web Application Firewall (WAF) Access Control List (ACL).
      type: str

    http_version:
      description:
        - The version of the http protocol to use for the distribution.
        - AWS defaults this to C(http2).
        - Valid values are C(http1.1), C(http2), C(http3) and C(http2and3).
      type: str

    ipv6_enabled:
      description:
        - Determines whether IPv6 support is enabled or not.
        - Defaults to C(false).
      type: bool

    wait:
      description:
        - Specifies whether the module waits until the distribution has completed processing the creation or update.
      type: bool
      default: false

    wait_timeout:
      description:
        - Specifies the duration in seconds to wait for a timeout of a cloudfront create or update.
      default: 1800
      type: int

extends_documentation_fragment:
  - amazon.aws.common.modules
  - amazon.aws.region.modules
  - amazon.aws.tags
  - amazon.aws.boto3
"""

EXAMPLES = r"""
- name: create a basic distribution with defaults and tags
  community.aws.cloudfront_distribution:
    state: present
    default_origin_domain_name: www.my-cloudfront-origin.com
    tags:
      Name: example distribution
      Project: example project
      Priority: '1'

- name: update a distribution comment by distribution_id
  community.aws.cloudfront_distribution:
    state: present
    distribution_id: E1RP5A2MJ8073O
    comment: modified by ansible cloudfront.py

- name: update a distribution comment by caller_reference
  community.aws.cloudfront_distribution:
    state: present
    caller_reference: my cloudfront distribution 001
    comment: modified by ansible cloudfront.py

- name: update a distribution's aliases and comment using the distribution_id as a reference
  community.aws.cloudfront_distribution:
    state: present
    distribution_id: E1RP5A2MJ8073O
    comment: modified by cloudfront.py again
    aliases:
      - 'www.my-distribution-source.com'
      - 'zzz.aaa.io'

- name: update a distribution's aliases and comment using an alias as a reference
  community.aws.cloudfront_distribution:
    state: present
    caller_reference: my test distribution
    comment: modified by cloudfront.py again
    aliases:
      - www.my-distribution-source.com
      - zzz.aaa.io

- name: update a distribution's comment and aliases and tags and remove existing tags
  community.aws.cloudfront_distribution:
    state: present
    distribution_id: E15BU8SDCGSG57
    comment: modified by cloudfront.py again
    aliases:
      - tested.com
    tags:
      Project: distribution 1.2
    purge_tags: true

- name: create a distribution with an origin, logging and default cache behavior
  community.aws.cloudfront_distribution:
    state: present
    caller_reference: unique test distribution ID
    origins:
      - id: 'my test origin-000111'
        domain_name: www.example.com
        origin_path: /production
        custom_headers:
          - header_name: MyCustomHeaderName
            header_value: MyCustomHeaderValue
    default_cache_behavior:
      target_origin_id: 'my test origin-000111'
      forwarded_values:
        query_string: true
        cookies:
          forward: all
        headers:
          - '*'
      viewer_protocol_policy: allow-all
      smooth_streaming: true
      compress: true
      allowed_methods:
        items:
          - GET
          - HEAD
        cached_methods:
          - GET
          - HEAD
    logging:
      enabled: true
      include_cookies: false
      bucket: mylogbucket.s3.amazonaws.com
      prefix: myprefix/
    enabled: false
    comment: this is a CloudFront distribution with logging

- name: delete a distribution
  community.aws.cloudfront_distribution:
    state: absent
    caller_reference: replaceable distribution
"""

RETURN = r"""
active_trusted_signers:
  description: Key pair IDs that CloudFront is aware of for each trusted signer.
  returned: always
  type: complex
  contains:
    enabled:
      description: Whether trusted signers are in use.
      returned: always
      type: bool
      sample: false
    quantity:
      description: Number of trusted signers.
      returned: always
      type: int
      sample: 1
    items:
      description:
        - A list of trusted signers.
        - This return value has been deprecated and will be removed in a release after 2026-12-16.
          Use RV(active_trusted_signers.elements) instead.
      returned: when there are trusted signers
      type: list
      sample:
      - key_pair_id
    elements:
      description: A list of trusted signers.
      returned: when there are trusted signers
      type: list
      version_added: 10.1.0
      sample:
      - key_pair_id
active_trusted_key_groups:
  description:
    - This field contains a list of key groups and the public keys in each key group that CloudFront
      can use to verify the signatures of signed URLs or signed cookies.
  returned: always
  type: complex
  contains:
    enabled:
      description:
        - This field is true if any of the key groups have public keys that CloudFront can use to verify
          the signatures of signed URLs and signed cookies. If not, this field is false.
      returned: always
      type: bool
      sample: false
    quantity:
      description: Number of key groups.
      returned: always
      type: int
      sample: 1
    items:
      description:
        - A list of key groups.
        - This return value has been deprecated and will be removed in a release after 2026-12-16.
          Use RV(active_trusted_key_groups.elements) instead.
      returned: when there are trusted signers
      type: list
    elements:
      description: A list of key groups.
      returned: when there are trusted signers
      type: list
      version_added: 10.1.0
aliases:
  description: Aliases that refer to the distribution.
  returned: always
  type: complex
  contains:
    items:
      description:
        - List of aliases.
        - This return value has been deprecated and will be removed in a release after 2026-12-16.
          Use RV(aliases.elements) instead.
      returned: always
      type: list
      sample:
      - test.example.com
    elements:
      description: List of aliases.
      returned: always
      type: list
      version_added: 10.1.0
      sample:
      - test.example.com
    quantity:
      description: Number of aliases.
      returned: always
      type: int
      sample: 1
arn:
  description: Amazon Resource Name of the distribution.
  returned: always
  type: str
  sample: arn:aws:cloudfront::123456789012:distribution/E1234ABCDEFGHI
cache_behaviors:
  description: CloudFront cache behaviors.
  returned: always
  type: complex
  contains:
    items:
      description:
        - List of cache behaviors.
        - This return value has been deprecated and will be removed in a release after 2026-12-16.
          Use RV(cache_behaviors.elements) instead.
      returned: always
      type: complex
      contains:
        allowed_methods:
          description: Methods allowed by the cache behavior.
          returned: always
          type: complex
          contains:
            cached_methods:
              description: Methods cached by the cache behavior.
              returned: always
              type: complex
              contains:
                items:
                  description: List of cached methods.
                  returned: always
                  type: list
                  sample:
                  - HEAD
                  - GET
                quantity:
                  description: Count of cached methods.
                  returned: always
                  type: int
                  sample: 2
            items:
              description:
                - List of methods allowed by the cache behavior.
                - This return value has been deprecated and will be removed in a release after 2026-12-16.
                  Use RV(cache_behaviors.elements.allowed_methods.elements) instead.
              returned: always
              type: list
              sample:
              - HEAD
              - GET
            quantity:
              description: Count of methods allowed by the cache behavior.
              returned: always
              type: int
              sample: 2
        compress:
          description: Whether compression is turned on for the cache behavior.
          returned: always
          type: bool
          sample: false
        default_ttl:
          description: Default Time to Live of the cache behavior.
          returned: always
          type: int
          sample: 86400
        forwarded_values:
          description: Values forwarded to the origin for this cache behavior.
          returned: always
          type: complex
          contains:
            cookies:
              description: Cookies to forward to the origin.
              returned: always
              type: complex
              contains:
                forward:
                  description: Which cookies to forward to the origin for this cache behavior.
                  returned: always
                  type: str
                  sample: none
                whitelisted_names:
                  description: The names of the cookies to forward to the origin for this cache behavior.
                  returned: when I(forward=whitelist)
                  type: complex
                  contains:
                    quantity:
                      description: Count of cookies to forward.
                      returned: always
                      type: int
                      sample: 1
                    items:
                      description:
                        - List of cookies to forward.
                        - This return value has been deprecated and will be removed in a release after 2026-12-16.
                          Use RV(cache_behaviors.elements.forwarded_values.cookies.whitelisted_names.elements) instead.
                      returned: when list is not empty
                      type: list
                      sample: my_cookie
            headers:
              description: Which headers are used to vary on cache retrievals.
              returned: always
              type: complex
              contains:
                quantity:
                  description: Count of headers to vary on.
                  returned: always
                  type: int
                  sample: 1
                items:
                  description:
                    - List of headers to vary on.
                    - This return value has been deprecated and will be removed in a release after 2026-12-16.
                      Use RV(cache_behaviors.elements.forwarded_values.headers.elements) instead.
                  returned: when list is not empty
                  type: list
                  sample:
                  - Host
            query_string:
              description: Whether the query string is used in cache lookups.
              returned: always
              type: bool
              sample: false
            query_string_cache_keys:
              description: Which query string keys to use in cache lookups.
              returned: always
              type: complex
              contains:
                quantity:
                  description: Count of query string cache keys to use in cache lookups.
                  returned: always
                  type: int
                  sample: 1
                items:
                  description:
                    - List of query string cache keys to use in cache lookups.
                    - This return value has been deprecated and will be removed in a release after 2026-12-16.
                      Use RV(cache_behaviors.elements.forwarded_values.query_string_cache_keys.elements) instead.
                  returned: when list is not empty
                  type: list
                  sample:
        lambda_function_associations:
          description: Lambda function associations for a cache behavior.
          returned: always
          type: complex
          contains:
            quantity:
              description: Count of lambda function associations.
              returned: always
              type: int
              sample: 1
            items:
              description:
                - List of lambda function associations.
                - This return value has been deprecated and will be removed in a release after 2026-12-16.
                  Use RV(cache_behaviors.elements.lambda_function_associations.elements) instead.
              returned: when list is not empty
              type: list
              sample:
              - lambda_function_arn: arn:aws:lambda:123456789012:us-east-1/lambda/lambda-function
                event_type: viewer-response
        max_ttl:
          description: Maximum Time to Live.
          returned: always
          type: int
          sample: 31536000
        min_ttl:
          description: Minimum Time to Live.
          returned: always
          type: int
          sample: 0
        path_pattern:
          description: Path pattern that determines this cache behavior.
          returned: always
          type: str
          sample: /path/to/files/*
        smooth_streaming:
          description: Whether smooth streaming is enabled.
          returned: always
          type: bool
          sample: false
        target_origin_id:
          description: ID of origin reference by this cache behavior.
          returned: always
          type: str
          sample: origin_abcd
        trusted_signers:
          description: Trusted signers.
          returned: always
          type: complex
          contains:
            enabled:
              description: Whether trusted signers are enabled for this cache behavior.
              returned: always
              type: bool
              sample: false
            quantity:
              description: Count of trusted signers.
              returned: always
              type: int
              sample: 1
        viewer_protocol_policy:
          description: Policy of how to handle http/https.
          returned: always
          type: str
          sample: redirect-to-https
    elements:
      description: List of cache behaviors.
      returned: always
      type: complex
      version_added: 10.1.0
      contains:
        allowed_methods:
          description: Methods allowed by the cache behavior.
          returned: always
          type: complex
          contains:
            cached_methods:
              description: Methods cached by the cache behavior.
              returned: always
              type: complex
              contains:
                elements:
                  description: List of cached methods.
                  returned: always
                  type: list
                  sample:
                  - HEAD
                  - GET
                quantity:
                  description: Count of cached methods.
                  returned: always
                  type: int
                  sample: 2
            elements:
              description: List of methods allowed by the cache behavior.
              returned: always
              type: list
              sample:
              - HEAD
              - GET
            quantity:
              description: Count of methods allowed by the cache behavior.
              returned: always
              type: int
              sample: 2
        compress:
          description: Whether compression is turned on for the cache behavior.
          returned: always
          type: bool
          sample: false
        default_ttl:
          description: Default Time to Live of the cache behavior.
          returned: always
          type: int
          sample: 86400
        forwarded_values:
          description: Values forwarded to the origin for this cache behavior.
          returned: always
          type: complex
          contains:
            cookies:
              description: Cookies to forward to the origin.
              returned: always
              type: complex
              contains:
                forward:
                  description: Which cookies to forward to the origin for this cache behavior.
                  returned: always
                  type: str
                  sample: none
                whitelisted_names:
                  description: The names of the cookies to forward to the origin for this cache behavior.
                  returned: when I(forward=whitelist)
                  type: complex
                  contains:
                    quantity:
                      description: Count of cookies to forward.
                      returned: always
                      type: int
                      sample: 1
                    elements:
                      description: List of cookies to forward.
                      returned: when list is not empty
                      type: list
                      sample: my_cookie
            headers:
              description: Which headers are used to vary on cache retrievals.
              returned: always
              type: complex
              contains:
                quantity:
                  description: Count of headers to vary on.
                  returned: always
                  type: int
                  sample: 1
                elements:
                  description: List of headers to vary on.
                  returned: when list is not empty
                  type: list
                  sample:
                  - Host
            query_string:
              description: Whether the query string is used in cache lookups.
              returned: always
              type: bool
              sample: false
            query_string_cache_keys:
              description: Which query string keys to use in cache lookups.
              returned: always
              type: complex
              contains:
                quantity:
                  description: Count of query string cache keys to use in cache lookups.
                  returned: always
                  type: int
                  sample: 1
                elements:
                  description: List of query string cache keys to use in cache lookups.
                  returned: when list is not empty
                  type: list
                  sample:
        lambda_function_associations:
          description: Lambda function associations for a cache behavior.
          returned: always
          type: complex
          contains:
            quantity:
              description: Count of lambda function associations.
              returned: always
              type: int
              sample: 1
            elements:
              description: List of lambda function associations.
              returned: when list is not empty
              type: list
              sample:
              - lambda_function_arn: arn:aws:lambda:123456789012:us-east-1/lambda/lambda-function
                event_type: viewer-response
        max_ttl:
          description: Maximum Time to Live.
          returned: always
          type: int
          sample: 31536000
        min_ttl:
          description: Minimum Time to Live.
          returned: always
          type: int
          sample: 0
        path_pattern:
          description: Path pattern that determines this cache behavior.
          returned: always
          type: str
          sample: /path/to/files/*
        smooth_streaming:
          description: Whether smooth streaming is enabled.
          returned: always
          type: bool
          sample: false
        target_origin_id:
          description: ID of origin reference by this cache behavior.
          returned: always
          type: str
          sample: origin_abcd
        trusted_signers:
          description: Trusted signers.
          returned: always
          type: complex
          contains:
            enabled:
              description: Whether trusted signers are enabled for this cache behavior.
              returned: always
              type: bool
              sample: false
            quantity:
              description: Count of trusted signers.
              returned: always
              type: int
              sample: 1
        viewer_protocol_policy:
          description: Policy of how to handle http/https.
          returned: always
          type: str
          sample: redirect-to-https
    quantity:
      description: Count of cache behaviors.
      returned: always
      type: int
      sample: 1

caller_reference:
  description: Idempotency reference given when creating CloudFront distribution.
  returned: always
  type: str
  sample: '1484796016700'
comment:
  description: Any comments you want to include about the distribution.
  returned: always
  type: str
  sample: 'my first CloudFront distribution'
custom_error_responses:
  description: Custom error responses to use for error handling.
  returned: always
  type: complex
  contains:
    items:
      description:
        - List of custom error responses.
        - This return value has been deprecated and will be removed in a release after 2026-12-16.
          Use RV(custom_error_responses.elements) instead.
      returned: always
      type: complex
      contains:
        error_caching_min_ttl:
          description: Minimum time to cache this error response.
          returned: always
          type: int
          sample: 300
        error_code:
          description: Origin response code that triggers this error response.
          returned: always
          type: int
          sample: 500
        response_code:
          description: Response code to return to the requester.
          returned: always
          type: str
          sample: '500'
        response_page_path:
          description: Path that contains the error page to display.
          returned: always
          type: str
          sample: /errors/5xx.html
    elements:
      description: List of custom error responses.
      returned: always
      type: complex
      version_added: 10.1.0
      contains:
        error_caching_min_ttl:
          description: Minimum time to cache this error response.
          returned: always
          type: int
          sample: 300
        error_code:
          description: Origin response code that triggers this error response.
          returned: always
          type: int
          sample: 500
        response_code:
          description: Response code to return to the requester.
          returned: always
          type: str
          sample: '500'
        response_page_path:
          description: Path that contains the error page to display.
          returned: always
          type: str
          sample: /errors/5xx.html
    quantity:
      description: Count of custom error response items
      returned: always
      type: int
      sample: 1
default_cache_behavior:
  description: Default cache behavior.
  returned: always
  type: complex
  contains:
    allowed_methods:
      description: Methods allowed by the cache behavior.
      returned: always
      type: complex
      contains:
        cached_methods:
          description: Methods cached by the cache behavior.
          returned: always
          type: complex
          contains:
            items:
              description:
                - List of cached methods.
                - This return value has been deprecated and will be removed in a release after 2026-12-16.
                  Use RV(default_cache_behavior.allowed_methods.cached_methods.elements) instead.
              returned: always
              type: list
              sample:
              - HEAD
              - GET
            elements:
              description: List of cached methods.
              returned: always
              type: list
              version_added: 10.1.0
              sample:
              - HEAD
              - GET
            quantity:
              description: Count of cached methods.
              returned: always
              type: int
              sample: 2
        items:
          description:
            - List of methods allowed by the cache behavior.
            - This return value has been deprecated and will be removed in a release after 2026-12-16.
              Use RV(default_cache_behavior.allowed_methods.elements) instead.
          returned: always
          type: list
          sample:
          - HEAD
          - GET
        elements:
          description: List of methods allowed by the cache behavior.
          returned: always
          type: list
          version_added: 10.1.0
          sample:
          - HEAD
          - GET
        quantity:
          description: Count of methods allowed by the cache behavior.
          returned: always
          type: int
          sample: 2
    compress:
      description: Whether compression is turned on for the cache behavior.
      returned: always
      type: bool
      sample: false
    default_ttl:
      description: Default Time to Live of the cache behavior.
      returned: always
      type: int
      sample: 86400
    forwarded_values:
      description: Values forwarded to the origin for this cache behavior.
      returned: always
      type: complex
      contains:
        cookies:
          description: Cookies to forward to the origin.
          returned: always
          type: complex
          contains:
            forward:
              description: Which cookies to forward to the origin for this cache behavior.
              returned: always
              type: str
              sample: none
            whitelisted_names:
              description: The names of the cookies to forward to the origin for this cache behavior.
              returned: when I(forward=whitelist)
              type: complex
              contains:
                quantity:
                  description: Count of cookies to forward.
                  returned: always
                  type: int
                  sample: 1
                items:
                  description: List of cookies to forward.
                  returned: when list is not empty
                  type: list
                  sample: my_cookie
                elements:
                  description:
                    - List of cookies to forward.
                    - This return value has been deprecated and will be removed in a release after 2026-12-16.
                      Use RV(default_cache_behavior.forwarded_values.cookies.whitelisted_names.elements) instead.
                  returned: when list is not empty
                  version_added: 10.1.0
                  type: list
                  sample: my_cookie
        headers:
          description: Which headers are used to vary on cache retrievals.
          returned: always
          type: complex
          contains:
            quantity:
              description: Count of headers to vary on.
              returned: always
              type: int
              sample: 1
            items:
              description:
                - List of headers to vary on.
                - This return value has been deprecated and will be removed in a release after 2026-12-16.
                  Use RV(default_cache_behavior.forwarded_values.headers.elements) instead.
              returned: when list is not empty
              type: list
              sample:
              - Host
            elements:
              description: List of headers to vary on.
              returned: when list is not empty
              type: list
              version_added: 10.1.0
              sample:
              - Host
        query_string:
          description: Whether the query string is used in cache lookups.
          returned: always
          type: bool
          sample: false
        query_string_cache_keys:
          description: Which query string keys to use in cache lookups.
          returned: always
          type: complex
          contains:
            quantity:
              description: Count of query string cache keys to use in cache lookups.
              returned: always
              type: int
              sample: 1
            items:
              description:
                - List of query string cache keys to use in cache lookups.
                - This return value has been deprecated and will be removed in a release after 2026-12-16.
                  Use RV(default_cache_behavior.forwarded_values.query_string_cache_keys.elements) instead.
              returned: when list is not empty
              type: list
            elements:
              description: List of query string cache keys to use in cache lookups.
              returned: when list is not empty
              type: list
              version_added: 10.1.0
    lambda_function_associations:
      description: Lambda function associations for a cache behavior.
      returned: always
      type: complex
      contains:
        quantity:
          description: Count of lambda function associations.
          returned: always
          type: int
          sample: 1
        items:
          description:
            - List of lambda function associations.
            - This return value has been deprecated and will be removed in a release after 2026-12-16.
              Use RV(default_cache_behavior.lambda_function_associations.elements) instead.
          returned: when list is not empty
          type: list
          sample:
          - lambda_function_arn: arn:aws:lambda:123456789012:us-east-1/lambda/lambda-function
            event_type: viewer-response
        elements:
          description: List of lambda function associations.
          returned: when list is not empty
          type: list
          version_added: 10.1.0
          sample:
          - lambda_function_arn: arn:aws:lambda:123456789012:us-east-1/lambda/lambda-function
            event_type: viewer-response
    max_ttl:
      description: Maximum Time to Live.
      returned: always
      type: int
      sample: 31536000
    min_ttl:
      description: Minimum Time to Live.
      returned: always
      type: int
      sample: 0
    path_pattern:
      description: Path pattern that determines this cache behavior.
      returned: always
      type: str
      sample: /path/to/files/*
    smooth_streaming:
      description: Whether smooth streaming is enabled.
      returned: always
      type: bool
      sample: false
    target_origin_id:
      description: ID of origin reference by this cache behavior.
      returned: always
      type: str
      sample: origin_abcd
    trusted_signers:
      description: Trusted signers.
      returned: always
      type: complex
      contains:
        enabled:
          description: Whether trusted signers are enabled for this cache behavior.
          returned: always
          type: bool
          sample: false
        quantity:
          description: Count of trusted signers.
          returned: always
          type: int
          sample: 1
    viewer_protocol_policy:
      description: Policy of how to handle http/https.
      returned: always
      type: str
      sample: redirect-to-https
default_root_object:
  description: The object that you want CloudFront to request from your origin (for example, index.html)
    when a viewer requests the root URL for your distribution.
  returned: always
  type: str
  sample: ''
diff:
  description: Difference between previous configuration and new configuration.
  returned: always
  type: dict
  sample: {}
domain_name:
  description: Domain name of CloudFront distribution.
  returned: always
  type: str
  sample: d1vz8pzgurxosf.cloudfront.net
enabled:
  description: Whether the CloudFront distribution is enabled or not.
  returned: always
  type: bool
  sample: true
http_version:
  description: Version of HTTP supported by the distribution.
  returned: always
  type: str
  sample: http2
id:
  description: CloudFront distribution ID.
  returned: always
  type: str
  sample: E123456ABCDEFG
in_progress_invalidation_batches:
  description: The number of invalidation batches currently in progress.
  returned: always
  type: int
  sample: 0
is_ipv6_enabled:
  description: Whether IPv6 is enabled.
  returned: always
  type: bool
  sample: true
last_modified_time:
  description: Date and time distribution was last modified.
  returned: always
  type: str
  sample: '2017-10-13T01:51:12.656000+00:00'
logging:
  description: Logging information.
  returned: always
  type: complex
  contains:
    bucket:
      description: S3 bucket logging destination.
      returned: always
      type: str
      sample: logs-example-com.s3.amazonaws.com
    enabled:
      description: Whether logging is enabled.
      returned: always
      type: bool
      sample: true
    include_cookies:
      description: Whether to log cookies.
      returned: always
      type: bool
      sample: false
    prefix:
      description: Prefix added to logging object names.
      returned: always
      type: str
      sample: cloudfront/test
origins:
  description: Origins in the CloudFront distribution.
  returned: always
  type: complex
  contains:
    items:
      description:
        - List of origins.
        - This return value has been deprecated and will be removed in a release after 2026-12-16.
          Use RV(origins.elements) instead.
      returned: always
      type: complex
      contains:
        custom_headers:
          description: Custom headers passed to the origin.
          returned: always
          type: complex
          contains:
            quantity:
              description: Count of headers.
              returned: always
              type: int
              sample: 1
        custom_origin_config:
          description: Configuration of the origin.
          returned: always
          type: complex
          contains:
            http_port:
              description: Port on which HTTP is listening.
              returned: always
              type: int
              sample: 80
            https_port:
              description: Port on which HTTPS is listening.
              returned: always
              type: int
              sample: 443
            origin_keepalive_timeout:
              description: Keep-alive timeout.
              returned: always
              type: int
              sample: 5
            origin_protocol_policy:
              description: Policy of which protocols are supported.
              returned: always
              type: str
              sample: https-only
            origin_read_timeout:
              description: Timeout for reads to the origin.
              returned: always
              type: int
              sample: 30
            origin_ssl_protocols:
              description: SSL protocols allowed by the origin.
              returned: always
              type: complex
              contains:
                items:
                  description:
                    - List of SSL protocols.
                    - This return value has been deprecated and will be removed in a release after 2026-12-16.
                      Use RV(origins.elements.custom_origin_config.origin_ssl_protocols.elements) instead.
                  returned: always
                  type: list
                  sample:
                  - TLSv1
                  - TLSv1.1
                  - TLSv1.2
                quantity:
                  description: Count of SSL protocols.
                  returned: always
                  type: int
                  sample: 3
        domain_name:
          description: Domain name of the origin.
          returned: always
          type: str
          sample: test-origin.example.com
        id:
          description: ID of the origin.
          returned: always
          type: str
          sample: test-origin.example.com
        origin_path:
          description: Subdirectory to prefix the request from the S3 or HTTP origin.
          returned: always
          type: str
          sample: ''
        connection_attempts:
          description: The number of times that CloudFront attempts to connect to the origin.
          returned: always
          type: int
          sample: 3
        connection_timeout:
          description: The number of seconds that CloudFront waits when trying to establish a connection to the origin.
          returned: always
          type: int
          sample: 10
        origin_shield:
          description: Configuration of the origin Origin Shield.
          returned: always
          type: complex
          contains:
            enabled:
              description: Whether Origin Shield is enabled or not.
              returned: always
              type: bool
              sample: false
            origin_shield_region:
              description: Which region is used by Origin Shield.
              returned: when enabled is true
              type: str
              sample: us-east-1
          version_added: 6.0.0
        s3_origin_config:
          description: Origin access identity configuration for S3 Origin.
          returned: when s3_origin_access_identity_enabled is true
          type: dict
          contains:
            origin_access_identity:
              type: str
              description: The origin access id as a path.
              sample: origin-access-identity/cloudfront/EXAMPLEID
    elements:
      description: List of origins.
      returned: always
      version_added: 10.1.0
      type: complex
      contains:
        custom_headers:
          description: Custom headers passed to the origin.
          returned: always
          type: complex
          contains:
            quantity:
              description: Count of headers.
              returned: always
              type: int
              sample: 1
        custom_origin_config:
          description: Configuration of the origin.
          returned: always
          type: complex
          contains:
            http_port:
              description: Port on which HTTP is listening.
              returned: always
              type: int
              sample: 80
            https_port:
              description: Port on which HTTPS is listening.
              returned: always
              type: int
              sample: 443
            origin_keepalive_timeout:
              description: Keep-alive timeout.
              returned: always
              type: int
              sample: 5
            origin_protocol_policy:
              description: Policy of which protocols are supported.
              returned: always
              type: str
              sample: https-only
            origin_read_timeout:
              description: Timeout for reads to the origin.
              returned: always
              type: int
              sample: 30
            origin_ssl_protocols:
              description: SSL protocols allowed by the origin.
              returned: always
              type: complex
              contains:
                elements:
                  description: List of SSL protocols.
                  returned: always
                  type: list
                  sample:
                  - TLSv1
                  - TLSv1.1
                  - TLSv1.2
                quantity:
                  description: Count of SSL protocols.
                  returned: always
                  type: int
                  sample: 3
        domain_name:
          description: Domain name of the origin.
          returned: always
          type: str
          sample: test-origin.example.com
        id:
          description: ID of the origin.
          returned: always
          type: str
          sample: test-origin.example.com
        origin_path:
          description: Subdirectory to prefix the request from the S3 or HTTP origin.
          returned: always
          type: str
          sample: ''
        connection_attempts:
          description: The number of times that CloudFront attempts to connect to the origin.
          returned: always
          type: int
          sample: 3
        connection_timeout:
          description: The number of seconds that CloudFront waits when trying to establish a connection to the origin.
          returned: always
          type: int
          sample: 10
        origin_shield:
          description: Configuration of the origin Origin Shield.
          returned: always
          type: complex
          contains:
            enabled:
              description: Whether Origin Shield is enabled or not.
              returned: always
              type: bool
              sample: false
            origin_shield_region:
              description: Which region is used by Origin Shield.
              returned: when enabled is true
              type: str
              sample: us-east-1
          version_added: 6.0.0
        s3_origin_config:
          description: Origin access identity configuration for S3 Origin.
          returned: when s3_origin_access_identity_enabled is true
          type: dict
          contains:
            origin_access_identity:
              type: str
              description: The origin access id as a path.
              sample: origin-access-identity/cloudfront/EXAMPLEID
    quantity:
      description: Count of origins.
      returned: always
      type: int
      sample: 1
price_class:
  description: Price class of CloudFront distribution.
  returned: always
  type: str
  sample: PriceClass_All
restrictions:
  description: Restrictions in use by CloudFront.
  returned: always
  type: complex
  contains:
    geo_restriction:
      description: Controls the countries in which your content is distributed.
      returned: always
      type: complex
      contains:
        quantity:
          description: Count of restrictions.
          returned: always
          type: int
          sample: 1
        items:
          description:
            - List of country codes allowed or disallowed.
            - This return value has been deprecated and will be removed in a release after 2026-12-16.
              Use RV(restrictions.geo_restriction.elements) instead.
          returned: always
          type: list
          sample: xy
        elements:
          description: List of country codes allowed or disallowed.
          returned: always
          type: list
          version_added: 10.1.0
          sample: xy
        restriction_type:
          description: Type of restriction.
          returned: always
          type: str
          sample: blacklist
status:
  description: Status of the CloudFront distribution.
  returned: always
  type: str
  sample: InProgress
tags:
  description: Distribution tags.
  returned: always
  type: dict
  sample:
    Hello: World
viewer_certificate:
  description: Certificate used by CloudFront distribution.
  returned: always
  type: complex
  contains:
    acm_certificate_arn:
      description: ARN of ACM certificate.
      returned: when certificate comes from ACM
      type: str
      sample: arn:aws:acm:us-east-1:123456789012:certificate/abcd1234-1234-1234-abcd-123456abcdef
    certificate:
      description: Reference to certificate.
      returned: always
      type: str
      sample: arn:aws:acm:us-east-1:123456789012:certificate/abcd1234-1234-1234-abcd-123456abcdef
    certificate_source:
      description: Where certificate comes from.
      returned: always
      type: str
      sample: acm
    minimum_protocol_version:
      description: Minimum SSL/TLS protocol supported by this distribution.
      returned: always
      type: str
      sample: TLSv1
    ssl_support_method:
      description: Support for pre-SNI browsers or not.
      returned: always
      type: str
      sample: sni-only
web_acl_id:
  description: ID of Web Access Control List (from WAF service).
  returned: always
  type: str
  sample: abcd1234-1234-abcd-abcd-abcd12345678
"""

import datetime
import re
from collections import OrderedDict

try:
    import botocore
except ImportError:
    pass  # caught by AnsibleAWSModule

from ansible.module_utils._text import to_native
from ansible.module_utils._text import to_text
from ansible.module_utils.common.dict_transformations import camel_dict_to_snake_dict
from ansible.module_utils.common.dict_transformations import recursive_diff
from ansible.module_utils.common.dict_transformations import snake_dict_to_camel_dict

from ansible_collections.amazon.aws.plugins.module_utils.cloudfront_facts import CloudFrontFactsServiceManager
from ansible_collections.amazon.aws.plugins.module_utils.retries import AWSRetry
from ansible_collections.amazon.aws.plugins.module_utils.tagging import ansible_dict_to_boto3_tag_list
from ansible_collections.amazon.aws.plugins.module_utils.tagging import boto3_tag_list_to_ansible_dict
from ansible_collections.amazon.aws.plugins.module_utils.tagging import compare_aws_tags

from ansible_collections.community.aws.plugins.module_utils.modules import AnsibleCommunityAWSModule as AnsibleAWSModule


def change_dict_key_name(dictionary, old_key, new_key):
    if old_key in dictionary:
        dictionary[new_key] = dictionary.get(old_key)
        dictionary.pop(old_key, None)
    return dictionary


def merge_validation_into_config(config, validated_node, node_name):
    if validated_node is not None:
        if isinstance(validated_node, dict):
            config_node = config.get(node_name)
            if config_node is not None:
                config_node_items = list(config_node.items())
            else:
                config_node_items = []
            config[node_name] = dict(config_node_items + list(validated_node.items()))
        if isinstance(validated_node, list):
            config[node_name] = list(set(config.get(node_name) + validated_node))
    return config


def ansible_list_to_cloudfront_list(list_items=None, include_quantity=True):
    if list_items is None:
        list_items = []
    if not isinstance(list_items, list):
        raise ValueError(f"Expected a list, got a {type(list_items).__name__} with value {str(list_items)}")
    result = {}
    if include_quantity:
        result["quantity"] = len(list_items)
    if len(list_items) > 0:
        result["items"] = list_items
    return result


def create_distribution(client, module, config, tags):
    try:
        if not tags:
            return client.create_distribution(aws_retry=True, DistributionConfig=config)["Distribution"]
        else:
            distribution_config_with_tags = {"DistributionConfig": config, "Tags": {"Items": tags}}
            return client.create_distribution_with_tags(
                aws_retry=True, DistributionConfigWithTags=distribution_config_with_tags
            )["Distribution"]
    except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
        module.fail_json_aws(e, msg="Error creating distribution")


def delete_distribution(client, module, distribution):
    try:
        return client.delete_distribution(
            aws_retry=True, Id=distribution["Distribution"]["Id"], IfMatch=distribution["ETag"]
        )
    except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
        module.fail_json_aws(e, msg=f"Error deleting distribution {to_native(distribution['Distribution'])}")


def update_distribution(client, module, config, distribution_id, e_tag):
    try:
        return client.update_distribution(aws_retry=True, DistributionConfig=config, Id=distribution_id, IfMatch=e_tag)[
            "Distribution"
        ]
    except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
        module.fail_json_aws(e, msg=f"Error updating distribution to {to_native(config)}")


def tag_resource(client, module, arn, tags):
    try:
        return client.tag_resource(aws_retry=True, Resource=arn, Tags=dict(Items=tags))
    except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
        module.fail_json_aws(e, msg="Error tagging resource")


def untag_resource(client, module, arn, tag_keys):
    try:
        return client.untag_resource(aws_retry=True, Resource=arn, TagKeys=dict(Items=tag_keys))
    except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
        module.fail_json_aws(e, msg="Error untagging resource")


def list_tags_for_resource(client, module, arn):
    try:
        response = client.list_tags_for_resource(aws_retry=True, Resource=arn)
        return boto3_tag_list_to_ansible_dict(response.get("Tags").get("Items"))
    except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
        module.fail_json_aws(e, msg="Error listing tags for resource")


def update_tags(client, module, existing_tags, valid_tags, purge_tags, arn):
    if valid_tags is None:
        return False
    changed = False
    to_add, to_remove = compare_aws_tags(existing_tags, valid_tags, purge_tags)
    if to_remove:
        untag_resource(client, module, arn, to_remove)
        changed = True
    if to_add:
        tag_resource(client, module, arn, ansible_dict_to_boto3_tag_list(to_add))
        changed = True
    return changed


class CloudFrontValidationManager(object):
    """
    Manages CloudFront validations
    """

    def __init__(self, module):
        self.__cloudfront_facts_mgr = CloudFrontFactsServiceManager(module)
        self.module = module
        self.__default_distribution_enabled = True
        self.__default_http_port = 80
        self.__default_https_port = 443
        self.__default_ipv6_enabled = False
        self.__default_origin_ssl_protocols = [
            "TLSv1",
            "TLSv1.1",
            "TLSv1.2",
        ]
        self.__default_custom_origin_protocol_policy = "match-viewer"
        self.__default_custom_origin_read_timeout = 30
        self.__default_custom_origin_keepalive_timeout = 5
        self.__default_datetime_string = datetime.datetime.now().strftime("%Y-%m-%dT%H:%M:%S.%f")
        self.__default_cache_behavior_min_ttl = 0
        self.__default_cache_behavior_max_ttl = 31536000
        self.__default_cache_behavior_default_ttl = 86400
        self.__default_cache_behavior_compress = False
        self.__default_cache_behavior_viewer_protocol_policy = "allow-all"
        self.__default_cache_behavior_smooth_streaming = False
        self.__default_cache_behavior_forwarded_values_forward_cookies = "none"
        self.__default_cache_behavior_forwarded_values_query_string = True
        self.__default_trusted_signers_enabled = False
        self.__valid_price_classes = set(
            [
                "PriceClass_100",
                "PriceClass_200",
                "PriceClass_All",
            ]
        )
        self.__valid_origin_protocol_policies = set(
            [
                "http-only",
                "match-viewer",
                "https-only",
            ]
        )
        self.__valid_origin_ssl_protocols = set(
            [
                "SSLv3",
                "TLSv1",
                "TLSv1.1",
                "TLSv1.2",
            ]
        )
        self.__valid_cookie_forwarding = set(
            [
                "none",
                "whitelist",
                "all",
            ]
        )
        self.__valid_viewer_protocol_policies = set(
            [
                "allow-all",
                "https-only",
                "redirect-to-https",
            ]
        )
        self.__valid_methods = set(
            [
                "GET",
                "HEAD",
                "POST",
                "PUT",
                "PATCH",
                "OPTIONS",
                "DELETE",
            ]
        )
        self.__valid_methods_cached_methods = [
            set(
                [
                    "GET",
                    "HEAD",
                ]
            ),
            set(
                [
                    "GET",
                    "HEAD",
                    "OPTIONS",
                ]
            ),
        ]
        self.__valid_methods_allowed_methods = [
            self.__valid_methods_cached_methods[0],
            self.__valid_methods_cached_methods[1],
            self.__valid_methods,
        ]
        self.__valid_lambda_function_association_event_types = set(
            [
                "viewer-request",
                "viewer-response",
                "origin-request",
                "origin-response",
            ]
        )
        self.__valid_viewer_certificate_ssl_support_methods = set(
            [
                "sni-only",
                "vip",
            ]
        )
        self.__valid_viewer_certificate_minimum_protocol_versions = set(
            [
                "SSLv3",
                "TLSv1",
                "TLSv1_2016",
                "TLSv1.1_2016",
                "TLSv1.2_2018",
                "TLSv1.2_2019",
                "TLSv1.2_2021",
            ]
        )
        self.__valid_viewer_certificate_certificate_sources = set(
            [
                "cloudfront",
                "iam",
                "acm",
            ]
        )
        self.__valid_http_versions = set(
            [
                "http1.1",
                "http2",
                "http3",
                "http2and3",
            ]
        )
        self.__s3_bucket_domain_regex = re.compile(r"\.s3(?:\.[^.]+)?\.amazonaws\.com$")

    def add_missing_key(self, dict_object, key_to_set, value_to_set):
        if key_to_set not in dict_object and value_to_set is not None:
            dict_object[key_to_set] = value_to_set
        return dict_object

    def add_key_else_change_dict_key(self, dict_object, old_key, new_key, value_to_set):
        if old_key not in dict_object and value_to_set is not None:
            dict_object[new_key] = value_to_set
        else:
            dict_object = change_dict_key_name(dict_object, old_key, new_key)
        return dict_object

    def add_key_else_validate(
        self, dict_object, key_name, attribute_name, value_to_set, valid_values, to_aws_list=False
    ):
        if key_name in dict_object:
            self.validate_attribute_with_allowed_values(value_to_set, attribute_name, valid_values)
        else:
            if to_aws_list:
                dict_object[key_name] = ansible_list_to_cloudfront_list(value_to_set)
            elif value_to_set is not None:
                dict_object[key_name] = value_to_set
        return dict_object

    def validate_logging(self, logging):
        try:
            if logging is None:
                return None
            valid_logging = {}
            if logging and not set(["enabled", "include_cookies", "bucket", "prefix"]).issubset(logging):
                self.module.fail_json(
                    msg="The logging parameters enabled, include_cookies, bucket and prefix must be specified."
                )
            valid_logging["include_cookies"] = logging.get("include_cookies")
            valid_logging["enabled"] = logging.get("enabled")
            valid_logging["bucket"] = logging.get("bucket")
            valid_logging["prefix"] = logging.get("prefix")
            return valid_logging
        except Exception as e:
            self.module.fail_json_aws(e, msg="Error validating distribution logging")

    def validate_is_list(self, list_to_validate, list_name):
        if not isinstance(list_to_validate, list):
            self.module.fail_json(msg=f"{list_name} is of type {type(list_to_validate).__name__}. Must be a list.")

    def validate_required_key(self, key_name, full_key_name, dict_object):
        if key_name not in dict_object:
            self.module.fail_json(msg=f"{full_key_name} must be specified.")

    def validate_origins(
        self,
        client,
        config,
        origins,
        default_origin_domain_name,
        default_origin_path,
        create,
        purge_origins=False,
    ):
        try:
            if origins is None:
                if default_origin_domain_name is None and not create:
                    if purge_origins:
                        return None
                    else:
                        return ansible_list_to_cloudfront_list(config)
                if default_origin_domain_name is not None:
                    origins = [{"domain_name": default_origin_domain_name, "origin_path": default_origin_path or ""}]
                else:
                    origins = []
            self.validate_is_list(origins, "origins")
            if not origins and default_origin_domain_name is None and create:
                self.module.fail_json(
                    msg="Both origins[] and default_origin_domain_name have not been specified. Please specify at least one."
                )
            all_origins = OrderedDict()
            new_domains = list()
            for origin in config:
                all_origins[origin.get("domain_name")] = origin
            for origin in origins:
                origin = self.validate_origin(
                    client, all_origins.get(origin.get("domain_name"), {}), origin, default_origin_path
                )
                all_origins[origin["domain_name"]] = origin
                new_domains.append(origin["domain_name"])
            if purge_origins:
                for domain in list(all_origins.keys()):
                    if domain not in new_domains:
                        del all_origins[domain]
            return ansible_list_to_cloudfront_list(list(all_origins.values()))
        except Exception as e:
            self.module.fail_json_aws(e, msg="Error validating distribution origins")

    def validate_s3_origin_configuration(self, client, existing_config, origin):
        if origin.get("s3_origin_config", {}).get("origin_access_identity"):
            return origin["s3_origin_config"]["origin_access_identity"]

        if existing_config.get("s3_origin_config", {}).get("origin_access_identity"):
            return existing_config["s3_origin_config"]["origin_access_identity"]

        try:
            comment = f"access-identity-by-ansible-{origin.get('domain_name')}-{self.__default_datetime_string}"
            caller_reference = f"{origin.get('domain_name')}-{self.__default_datetime_string}"
            cfoai_config = dict(
                CloudFrontOriginAccessIdentityConfig=dict(CallerReference=caller_reference, Comment=comment)
            )
            oai = client.create_cloud_front_origin_access_identity(**cfoai_config)["CloudFrontOriginAccessIdentity"][
                "Id"
            ]
        except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e:
            self.module.fail_json_aws(e, msg=f"Couldn't create Origin Access Identity for id {origin['id']}")
        return f"origin-access-identity/cloudfront/{oai}"

    def validate_origin(self, client, existing_config, origin, default_origin_path):
        try:
            origin = self.add_missing_key(
                origin, "origin_path", existing_config.get("origin_path", default_origin_path or "")
            )
            self.validate_required_key("origin_path", "origins[].origin_path", origin)
            origin = self.add_missing_key(origin, "id", existing_config.get("id", self.__default_datetime_string))
            if "custom_headers" in origin and len(origin.get("custom_headers")) > 0:
                for custom_header in origin.get("custom_headers"):
                    if "header_name" not in custom_header or "header_value" not in custom_header:
                        self.module.fail_json(
                            msg="Both origins[].custom_headers.header_name and origins[].custom_headers.header_value must be specified."
                        )
                origin["custom_headers"] = ansible_list_to_cloudfront_list(origin.get("custom_headers"))
            else:
                origin["custom_headers"] = ansible_list_to_cloudfront_list()
            if "origin_shield" in origin:
                origin_shield = origin.get("origin_shield")
                if origin_shield.get("enabled"):
                    origin_shield_region = origin_shield.get("origin_shield_region")
                    if origin_shield_region is None:
                        self.module.fail_json(
                            msg="origins[].origin_shield.origin_shield_region must be specified"
                            " when origins[].origin_shield.enabled is true."
                        )
                    else:
                        origin_shield_region = origin_shield_region.lower()
            if self.__s3_bucket_domain_regex.search(origin.get("domain_name").lower()):
                if origin.get("s3_origin_access_identity_enabled") is not None:
                    if origin["s3_origin_access_identity_enabled"]:
                        s3_origin_config = self.validate_s3_origin_configuration(client, existing_config, origin)
                    else:
                        s3_origin_config = None

                    del origin["s3_origin_access_identity_enabled"]

                    if s3_origin_config:
                        oai = s3_origin_config
                    else:
                        oai = ""

                    origin["s3_origin_config"] = dict(origin_access_identity=oai)

                if "custom_origin_config" in origin:
                    self.module.fail_json(
                        msg="s3 origin domains and custom_origin_config are mutually exclusive",
                    )
            else:
                origin = self.add_missing_key(
                    origin, "custom_origin_config", existing_config.get("custom_origin_config", {})
                )
                custom_origin_config = origin.get("custom_origin_config")
                custom_origin_config = self.add_key_else_validate(
                    custom_origin_config,
                    "origin_protocol_policy",
                    "origins[].custom_origin_config.origin_protocol_policy",
                    self.__default_custom_origin_protocol_policy,
                    self.__valid_origin_protocol_policies,
                )
                custom_origin_config = self.add_missing_key(
                    custom_origin_config, "origin_read_timeout", self.__default_custom_origin_read_timeout
                )
                custom_origin_config = self.add_missing_key(
                    custom_origin_config, "origin_keepalive_timeout", self.__default_custom_origin_keepalive_timeout
                )
                custom_origin_config = self.add_key_else_change_dict_key(
                    custom_origin_config, "http_port", "h_t_t_p_port", self.__default_http_port
                )
                custom_origin_config = self.add_key_else_change_dict_key(
                    custom_origin_config, "https_port", "h_t_t_p_s_port", self.__default_https_port
                )
                if custom_origin_config.get("origin_ssl_protocols", {}).get("items"):
                    custom_origin_config["origin_ssl_protocols"] = custom_origin_config["origin_ssl_protocols"]["items"]
                if custom_origin_config.get("origin_ssl_protocols"):
                    self.validate_attribute_list_with_allowed_list(
                        custom_origin_config["origin_ssl_protocols"],
                        "origins[].origin_ssl_protocols",
                        self.__valid_origin_ssl_protocols,
                    )
                else:
                    custom_origin_config["origin_ssl_protocols"] = self.__default_origin_ssl_protocols
                custom_origin_config["origin_ssl_protocols"] = ansible_list_to_cloudfront_list(
                    custom_origin_config["origin_ssl_protocols"]
                )
            return origin
        except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
            self.module.fail_json_aws(e, msg="Error validating distribution origin")

    def validate_cache_behaviors(self, config, cache_behaviors, valid_origins, purge_cache_behaviors=False):
        try:
            if cache_behaviors is None and valid_origins is not None and purge_cache_behaviors is False:
                return ansible_list_to_cloudfront_list(config)
            all_cache_behaviors = OrderedDict()
            # cache behaviors are order dependent so we don't preserve the existing ordering when purge_cache_behaviors
            # is true (if purge_cache_behaviors is not true, we can't really know the full new order)
            if not purge_cache_behaviors:
                for behavior in config:
                    all_cache_behaviors[behavior["path_pattern"]] = behavior
            for cache_behavior in cache_behaviors:
                valid_cache_behavior = self.validate_cache_behavior(
                    all_cache_behaviors.get(cache_behavior.get("path_pattern"), {}), cache_behavior, valid_origins
                )
                all_cache_behaviors[cache_behavior["path_pattern"]] = valid_cache_behavior
            if purge_cache_behaviors:
                for target_origin_id in set(all_cache_behaviors.keys()) - set(
                    [cb["path_pattern"] for cb in cache_behaviors]
                ):
                    del all_cache_behaviors[target_origin_id]
            return ansible_list_to_cloudfront_list(list(all_cache_behaviors.values()))
        except Exception as e:
            self.module.fail_json_aws(e, msg="Error validating distribution cache behaviors")

    def validate_cache_behavior(self, config, cache_behavior, valid_origins, is_default_cache=False):
        if is_default_cache and cache_behavior is None:
            cache_behavior = {}
        if cache_behavior is None and valid_origins is not None:
            return config
        cache_behavior = self.validate_cache_behavior_first_level_keys(
            config, cache_behavior, valid_origins, is_default_cache
        )
        if cache_behavior.get("cache_policy_id") is None:
            cache_behavior = self.validate_forwarded_values(
                config, cache_behavior.get("forwarded_values"), cache_behavior
            )
        cache_behavior = self.validate_allowed_methods(config, cache_behavior.get("allowed_methods"), cache_behavior)
        cache_behavior = self.validate_lambda_function_associations(
            config, cache_behavior.get("lambda_function_associations"), cache_behavior
        )
        cache_behavior = self.validate_trusted_signers(config, cache_behavior.get("trusted_signers"), cache_behavior)
        cache_behavior = self.validate_field_level_encryption_id(
            config, cache_behavior.get("field_level_encryption_id"), cache_behavior
        )
        return cache_behavior

    def validate_cache_behavior_first_level_keys(self, config, cache_behavior, valid_origins, is_default_cache):
        if cache_behavior.get("cache_policy_id") is not None and cache_behavior.get("forwarded_values") is not None:
            if is_default_cache:
                cache_behavior_name = "Default cache behavior"
            else:
                cache_behavior_name = f"Cache behavior for path {cache_behavior['path_pattern']}"
            self.module.fail_json(
                msg=f"{cache_behavior_name} cannot have both a cache_policy_id and a forwarded_values option."
            )
        try:
            if cache_behavior.get("cache_policy_id") is None:
                cache_behavior = self.add_key_else_change_dict_key(
                    cache_behavior,
                    "min_ttl",
                    "min_t_t_l",
                    config.get("min_t_t_l", self.__default_cache_behavior_min_ttl),
                )
                cache_behavior = self.add_key_else_change_dict_key(
                    cache_behavior,
                    "max_ttl",
                    "max_t_t_l",
                    config.get("max_t_t_l", self.__default_cache_behavior_max_ttl),
                )
                cache_behavior = self.add_key_else_change_dict_key(
                    cache_behavior,
                    "default_ttl",
                    "default_t_t_l",
                    config.get("default_t_t_l", self.__default_cache_behavior_default_ttl),
                )
            cache_behavior = self.add_missing_key(
                cache_behavior, "compress", config.get("compress", self.__default_cache_behavior_compress)
            )
            target_origin_id = cache_behavior.get("target_origin_id", config.get("target_origin_id"))
            if not target_origin_id:
                target_origin_id = self.get_first_origin_id_for_default_cache_behavior(valid_origins)
            if target_origin_id not in [origin["id"] for origin in valid_origins.get("items", [])]:
                if is_default_cache:
                    cache_behavior_name = "Default cache behavior"
                else:
                    cache_behavior_name = f"Cache behavior for path {cache_behavior['path_pattern']}"
                self.module.fail_json(
                    msg=f"{cache_behavior_name} has target_origin_id pointing to an origin that does not exist."
                )
            cache_behavior["target_origin_id"] = target_origin_id
            cache_behavior = self.add_key_else_validate(
                cache_behavior,
                "viewer_protocol_policy",
                "cache_behavior.viewer_protocol_policy",
                config.get("viewer_protocol_policy", self.__default_cache_behavior_viewer_protocol_policy),
                self.__valid_viewer_protocol_policies,
            )
            cache_behavior = self.add_missing_key(
                cache_behavior,
                "smooth_streaming",
                config.get("smooth_streaming", self.__default_cache_behavior_smooth_streaming),
            )
            return cache_behavior
        except Exception as e:
            self.module.fail_json_aws(e, msg="Error validating distribution cache behavior first level keys")

    def validate_forwarded_values(self, config, forwarded_values, cache_behavior):
        try:
            if not forwarded_values:
                forwarded_values = dict()
            existing_config = config.get("forwarded_values", {})
            headers = forwarded_values.get("headers", existing_config.get("headers", {}).get("items"))
            if headers:
                headers.sort()
            forwarded_values["headers"] = ansible_list_to_cloudfront_list(headers)
            if "cookies" not in forwarded_values:
                forward = existing_config.get("cookies", {}).get(
                    "forward", self.__default_cache_behavior_forwarded_values_forward_cookies
                )
                forwarded_values["cookies"] = {"forward": forward}
            else:
                existing_whitelist = existing_config.get("cookies", {}).get("whitelisted_names", {}).get("items")
                whitelist = forwarded_values.get("cookies").get("whitelisted_names", existing_whitelist)
                if whitelist:
                    self.validate_is_list(whitelist, "forwarded_values.whitelisted_names")
                    forwarded_values["cookies"]["whitelisted_names"] = ansible_list_to_cloudfront_list(whitelist)
                cookie_forwarding = forwarded_values.get("cookies").get(
                    "forward", existing_config.get("cookies", {}).get("forward")
                )
                self.validate_attribute_with_allowed_values(
                    cookie_forwarding, "cache_behavior.forwarded_values.cookies.forward", self.__valid_cookie_forwarding
                )
                forwarded_values["cookies"]["forward"] = cookie_forwarding
            query_string_cache_keys = forwarded_values.get(
                "query_string_cache_keys", existing_config.get("query_string_cache_keys", {}).get("items", [])
            )
            self.validate_is_list(query_string_cache_keys, "forwarded_values.query_string_cache_keys")
            forwarded_values["query_string_cache_keys"] = ansible_list_to_cloudfront_list(query_string_cache_keys)
            forwarded_values = self.add_missing_key(
                forwarded_values,
                "query_string",
                existing_config.get("query_string", self.__default_cache_behavior_forwarded_values_query_string),
            )
            cache_behavior["forwarded_values"] = forwarded_values
            return cache_behavior
        except Exception as e:
            self.module.fail_json_aws(e, msg="Error validating forwarded values")

    def validate_lambda_function_associations(self, config, lambda_function_associations, cache_behavior):
        try:
            if lambda_function_associations is not None:
                self.validate_is_list(lambda_function_associations, "lambda_function_associations")
                for association in lambda_function_associations:
                    association = change_dict_key_name(association, "lambda_function_arn", "lambda_function_a_r_n")
                    self.validate_attribute_with_allowed_values(
                        association.get("event_type"),
                        "cache_behaviors[].lambda_function_associations.event_type",
                        self.__valid_lambda_function_association_event_types,
                    )
                cache_behavior["lambda_function_associations"] = ansible_list_to_cloudfront_list(
                    lambda_function_associations
                )
            else:
                if "lambda_function_associations" in config:
                    cache_behavior["lambda_function_associations"] = config.get("lambda_function_associations")
                else:
                    cache_behavior["lambda_function_associations"] = ansible_list_to_cloudfront_list([])
            return cache_behavior
        except Exception as e:
            self.module.fail_json_aws(e, msg="Error validating lambda function associations")

    def validate_field_level_encryption_id(self, config, field_level_encryption_id, cache_behavior):
        if field_level_encryption_id is not None:
            cache_behavior["field_level_encryption_id"] = field_level_encryption_id
        elif "field_level_encryption_id" in config:
            cache_behavior["field_level_encryption_id"] = config.get("field_level_encryption_id")
        else:
            cache_behavior["field_level_encryption_id"] = ""
        return cache_behavior

    def validate_allowed_methods(self, config, allowed_methods, cache_behavior):
        try:
            if allowed_methods is not None:
                self.validate_required_key("items", "cache_behavior.allowed_methods.items[]", allowed_methods)
                temp_allowed_items = allowed_methods.get("items")
                self.validate_is_list(temp_allowed_items, "cache_behavior.allowed_methods.items")
                self.validate_attribute_list_with_allowed_list(
                    temp_allowed_items, "cache_behavior.allowed_methods.items[]", self.__valid_methods_allowed_methods
                )
                cached_items = allowed_methods.get("cached_methods")
                if "cached_methods" in allowed_methods:
                    self.validate_is_list(cached_items, "cache_behavior.allowed_methods.cached_methods")
                    self.validate_attribute_list_with_allowed_list(
                        cached_items,
                        "cache_behavior.allowed_items.cached_methods[]",
                        self.__valid_methods_cached_methods,
                    )
                # we don't care if the order of how cloudfront stores the methods differs - preserving existing
                # order reduces likelihood of making unnecessary changes
                if "allowed_methods" in config and set(config["allowed_methods"]["items"]) == set(temp_allowed_items):
                    cache_behavior["allowed_methods"] = config["allowed_methods"]
                else:
                    cache_behavior["allowed_methods"] = ansible_list_to_cloudfront_list(temp_allowed_items)

                if cached_items and set(cached_items) == set(
                    config.get("allowed_methods", {}).get("cached_methods", {}).get("items", [])
                ):
                    cache_behavior["allowed_methods"]["cached_methods"] = config["allowed_methods"]["cached_methods"]
                else:
                    cache_behavior["allowed_methods"]["cached_methods"] = ansible_list_to_cloudfront_list(cached_items)
            else:
                if "allowed_methods" in config:
                    cache_behavior["allowed_methods"] = config.get("allowed_methods")
            return cache_behavior
        except Exception as e:
            self.module.fail_json_aws(e, msg="Error validating allowed methods")

    def validate_trusted_signers(self, config, trusted_signers, cache_behavior):
        try:
            if trusted_signers is None:
                trusted_signers = {}
            if "items" in trusted_signers:
                valid_trusted_signers = ansible_list_to_cloudfront_list(trusted_signers.get("items"))
            else:
                valid_trusted_signers = dict(quantity=config.get("quantity", 0))
                if "items" in config:
                    valid_trusted_signers = dict(items=config["items"])
            valid_trusted_signers["enabled"] = trusted_signers.get(
                "enabled", config.get("enabled", self.__default_trusted_signers_enabled)
            )
            cache_behavior["trusted_signers"] = valid_trusted_signers
            return cache_behavior
        except Exception as e:
            self.module.fail_json_aws(e, msg="Error validating trusted signers")

    def validate_viewer_certificate(self, viewer_certificate):
        try:
            if viewer_certificate is None:
                return None
            if (
                viewer_certificate.get("cloudfront_default_certificate")
                and viewer_certificate.get("ssl_support_method") is not None
            ):
                self.module.fail_json(
                    msg="viewer_certificate.ssl_support_method should not be specified with viewer_certificate_cloudfront_default"
                    + "_certificate set to true."
                )
            self.validate_attribute_with_allowed_values(
                viewer_certificate.get("ssl_support_method"),
                "viewer_certificate.ssl_support_method",
                self.__valid_viewer_certificate_ssl_support_methods,
            )
            self.validate_attribute_with_allowed_values(
                viewer_certificate.get("minimum_protocol_version"),
                "viewer_certificate.minimum_protocol_version",
                self.__valid_viewer_certificate_minimum_protocol_versions,
            )
            self.validate_attribute_with_allowed_values(
                viewer_certificate.get("certificate_source"),
                "viewer_certificate.certificate_source",
                self.__valid_viewer_certificate_certificate_sources,
            )
            viewer_certificate = change_dict_key_name(
                viewer_certificate, "cloudfront_default_certificate", "cloud_front_default_certificate"
            )
            viewer_certificate = change_dict_key_name(viewer_certificate, "ssl_support_method", "s_s_l_support_method")
            viewer_certificate = change_dict_key_name(viewer_certificate, "iam_certificate_id", "i_a_m_certificate_id")
            viewer_certificate = change_dict_key_name(
                viewer_certificate, "acm_certificate_arn", "a_c_m_certificate_arn"
            )
            return viewer_certificate
        except Exception as e:
            self.module.fail_json_aws(e, msg="Error validating viewer certificate")

    def validate_custom_error_responses(self, config, custom_error_responses, purge_custom_error_responses):
        try:
            if custom_error_responses is None and not purge_custom_error_responses:
                return ansible_list_to_cloudfront_list(config)
            self.validate_is_list(custom_error_responses, "custom_error_responses")
            result = list()
            existing_responses = dict((response["error_code"], response) for response in custom_error_responses)
            for custom_error_response in custom_error_responses:
                self.validate_required_key("error_code", "custom_error_responses[].error_code", custom_error_response)
                custom_error_response = change_dict_key_name(
                    custom_error_response, "error_caching_min_ttl", "error_caching_min_t_t_l"
                )
                if "response_code" in custom_error_response:
                    custom_error_response["response_code"] = str(custom_error_response["response_code"])
                if custom_error_response["error_code"] in existing_responses:
                    del existing_responses[custom_error_response["error_code"]]
                result.append(custom_error_response)
            if not purge_custom_error_responses:
                result.extend(existing_responses.values())

            return ansible_list_to_cloudfront_list(result)
        except Exception as e:
            self.module.fail_json_aws(e, msg="Error validating custom error responses")

    def validate_restrictions(self, config, restrictions, purge_restrictions=False):
        try:
            if restrictions is None:
                if purge_restrictions:
                    return None
                else:
                    return config
            self.validate_required_key("geo_restriction", "restrictions.geo_restriction", restrictions)
            geo_restriction = restrictions.get("geo_restriction")
            self.validate_required_key(
                "restriction_type", "restrictions.geo_restriction.restriction_type", geo_restriction
            )
            existing_restrictions = (
                config.get("geo_restriction", {}).get(geo_restriction["restriction_type"], {}).get("items", [])
            )
            geo_restriction_items = geo_restriction.get("items")
            if not purge_restrictions:
                geo_restriction_items.extend(
                    [rest for rest in existing_restrictions if rest not in geo_restriction_items]
                )
            valid_restrictions = ansible_list_to_cloudfront_list(geo_restriction_items)
            valid_restrictions["restriction_type"] = geo_restriction.get("restriction_type")
            return {"geo_restriction": valid_restrictions}
        except Exception as e:
            self.module.fail_json_aws(e, msg="Error validating restrictions")

    def validate_distribution_config_parameters(
        self, config, default_root_object, ipv6_enabled, http_version, web_acl_id
    ):
        try:
            config["default_root_object"] = default_root_object or config.get("default_root_object", "")
            config["is_i_p_v6_enabled"] = (
                ipv6_enabled
                if ipv6_enabled is not None
                else config.get("is_i_p_v6_enabled", self.__default_ipv6_enabled)
            )
            if http_version is not None or config.get("http_version"):
                self.validate_attribute_with_allowed_values(http_version, "http_version", self.__valid_http_versions)
                config["http_version"] = http_version or config.get("http_version")
            if web_acl_id or config.get("web_a_c_l_id"):
                config["web_a_c_l_id"] = web_acl_id or config.get("web_a_c_l_id")
            return config
        except Exception as e:
            self.module.fail_json_aws(e, msg="Error validating distribution config parameters")

    def validate_common_distribution_parameters(
        self, config, enabled, aliases, logging, price_class, purge_aliases=False
    ):
        try:
            if config is None:
                config = {}
            if aliases is not None:
                if not purge_aliases:
                    aliases.extend(
                        [alias for alias in config.get("aliases", {}).get("items", []) if alias not in aliases]
                    )
                config["aliases"] = ansible_list_to_cloudfront_list(aliases)
            if logging is not None:
                config["logging"] = self.validate_logging(logging)
            config["enabled"] = (
                enabled if enabled is not None else config.get("enabled", self.__default_distribution_enabled)
            )
            if price_class is not None:
                self.validate_attribute_with_allowed_values(price_class, "price_class", self.__valid_price_classes)
                config["price_class"] = price_class
            return config
        except Exception as e:
            self.module.fail_json_aws(e, msg="Error validating common distribution parameters")

    def validate_comment(self, config, comment):
        config["comment"] = comment or config.get(
            "comment", "Distribution created by Ansible with datetime stamp " + self.__default_datetime_string
        )
        return config

    def validate_caller_reference(self, caller_reference):
        return caller_reference or self.__default_datetime_string

    def get_first_origin_id_for_default_cache_behavior(self, valid_origins):
        try:
            if valid_origins is not None:
                valid_origins_list = valid_origins.get("items")
                if (
                    valid_origins_list is not None
                    and isinstance(valid_origins_list, list)
                    and len(valid_origins_list) > 0
                ):
                    return str(valid_origins_list[0].get("id"))
            self.module.fail_json(
                msg="There are no valid origins from which to specify a target_origin_id for the default_cache_behavior configuration."
            )
        except Exception as e:
            self.module.fail_json_aws(e, msg="Error getting first origin_id for default cache behavior")

    def validate_attribute_list_with_allowed_list(self, attribute_list, attribute_list_name, allowed_list):
        try:
            self.validate_is_list(attribute_list, attribute_list_name)
            if (
                isinstance(allowed_list, list)
                and set(attribute_list) not in allowed_list
                or isinstance(allowed_list, set)
                and not set(allowed_list).issuperset(attribute_list)
            ):
                attribute_list = " ".join(str(a) for a in allowed_list)
                self.module.fail_json(msg=f"The attribute list {attribute_list_name} must be one of [{attribute_list}]")
        except Exception as e:
            self.module.fail_json_aws(e, msg="Error validating attribute list with allowed value list")

    def validate_attribute_with_allowed_values(self, attribute, attribute_name, allowed_list):
        if attribute is not None and attribute not in allowed_list:
            attribute_list = " ".join(str(a) for a in allowed_list)
            self.module.fail_json(msg=f"The attribute {attribute_name} must be one of [{attribute_list}]")

    def validate_distribution_from_caller_reference(self, caller_reference):
        try:
            distributions = self.__cloudfront_facts_mgr.list_distributions(keyed=False)
            distribution_name = "Distribution"
            distribution_config_name = "DistributionConfig"
            distribution_ids = [dist.get("Id") for dist in distributions]
            for distribution_id in distribution_ids:
                distribution = self.__cloudfront_facts_mgr.get_distribution(id=distribution_id)
                if distribution is not None:
                    distribution_config = distribution[distribution_name].get(distribution_config_name)
                    if (
                        distribution_config is not None
                        and distribution_config.get("CallerReference") == caller_reference
                    ):
                        distribution[distribution_name][distribution_config_name] = distribution_config
                        return distribution

        except Exception as e:
            self.module.fail_json_aws(e, msg="Error validating distribution from caller reference")

    def validate_distribution_from_aliases_caller_reference(self, distribution_id, aliases, caller_reference):
        try:
            if caller_reference is not None:
                return self.validate_distribution_from_caller_reference(caller_reference)
            else:
                if aliases and distribution_id is None:
                    distribution_id = self.validate_distribution_id_from_alias(aliases)
                if distribution_id:
                    return self.__cloudfront_facts_mgr.get_distribution(id=distribution_id)
            return None
        except Exception as e:
            self.module.fail_json_aws(
                e, msg="Error validating distribution_id from alias, aliases and caller reference"
            )

    def validate_distribution_id_from_alias(self, aliases):
        distributions = self.__cloudfront_facts_mgr.list_distributions(keyed=False)
        if distributions:
            for distribution in distributions:
                distribution_aliases = distribution.get("Aliases", {}).get("Items", [])
                if set(aliases) & set(distribution_aliases):
                    return distribution["Id"]
        return None

    def wait_until_processed(self, client, wait_timeout, distribution_id, caller_reference):
        if distribution_id is None:
            distribution = self.validate_distribution_from_caller_reference(caller_reference=caller_reference)
            distribution_id = distribution["Distribution"]["Id"]

        try:
            waiter = client.get_waiter("distribution_deployed")
            attempts = 1 + int(wait_timeout / 60)
            waiter.wait(Id=distribution_id, WaiterConfig={"MaxAttempts": attempts})
        except botocore.exceptions.WaiterError as e:
            self.module.fail_json_aws(
                e,
                msg=f"Timeout waiting for CloudFront action. Waited for {to_text(wait_timeout)} seconds before timeout.",
            )

        except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
            self.module.fail_json_aws(e, msg=f"Error getting distribution {distribution_id}")


def duplicate_keys_for_deprecation(result):
    """
    Rename all dict 'items' keys to 'elements'
    """
    for t in (
        "active_trusted_signers.elements",
        "active_trusted_key_groups.elements",
        "distribution_config.aliases.elements",
        "distribution_config.cache_behaviors.elements",
        "distribution_config.custom_error_responses.elements",
        "distribution_config.default_cache_behavior.allowed_methods.cached_methods.elements",
        "distribution_config.default_cache_behavior.allowed_methods.elements",
        "distribution_config.default_cache_behavior.forwarded_values.cookies.whitelisted_names.elements",
        "distribution_config.default_cache_behavior.forwarded_values.headers.elements",
        "distribution_config.default_cache_behavior.forwarded_values.query_string_cache_keys.elements",
        "distribution_config.default_cache_behavior.lambda_function_associations.elements",
        "distribution_config.origins.elements",
        "distribution_config.restrictions.geo_restriction.elements",
    ):
        keys = t.split(".")
        copy_result = result
        while True:
            try:
                if len(keys) == 1:
                    copy_result["elements"] = copy_result["items"]
                    break
                copy_result = copy_result[keys.pop(0)]
            except KeyError:
                # The result does contain the key, skipping it
                break

    # Origins
    for i in result.get("distribution_config", {}).get("origins", {}).get("elements", []):
        try:
            items = i["custom_origin_config"]["origin_ssl_protocols"]["items"]
            i["elements"] = items
            del i["custom_origin_config"]["origin_ssl_protocols"]["items"]
        except KeyError:
            pass

    # Cache behaviors
    for i in result.get("distribution_config", {}).get("cache_behaviors", {}).get("elements", []):
        try:
            i["allowed_methods"]["elements"] = i["allowed_methods"]["items"]
            del i["allowed_methods"]["items"]
        except KeyError:
            pass

        try:
            i["lambda_function_associations"]["elements"] = i["lambda_function_associations"]["items"]
            del i["lambda_function_associations"]["items"]
        except KeyError:
            pass
        try:
            i["forwarded_values"]["cookies"]["whitelisted_names"]["elements"] = i["forwarded_values"]["cookies"][
                "whitelisted_names"
            ]["items"]
            del i["forwarded_values"]["cookies"]["whitelisted_names"]["items"]
        except KeyError:
            pass
        try:
            i["forwarded_values"]["headers"]["elements"] = i["forwarded_values"]["headers"]["items"]
            del i["forwarded_values"]["headers"]["items"]
        except KeyError:
            pass
        try:
            i["forwarded_values"]["query_string_cache_keys"]["elements"] = i["forwarded_values"][
                "query_string_cache_keys"
            ]["items"]
            del i["forwarded_values"]["query_string_cache_keys"]["items"]
        except KeyError:
            pass


def main():
    argument_spec = dict(
        state=dict(choices=["present", "absent"], default="present"),
        caller_reference=dict(),
        comment=dict(),
        distribution_id=dict(),
        e_tag=dict(),
        tags=dict(type="dict", aliases=["resource_tags"]),
        purge_tags=dict(type="bool", default=True),
        alias=dict(),
        aliases=dict(type="list", default=[], elements="str"),
        purge_aliases=dict(type="bool", default=False),
        default_root_object=dict(),
        origins=dict(type="list", elements="dict"),
        purge_origins=dict(type="bool", default=False),
        default_cache_behavior=dict(type="dict"),
        cache_behaviors=dict(type="list", elements="dict"),
        purge_cache_behaviors=dict(type="bool", default=False),
        custom_error_responses=dict(type="list", elements="dict"),
        purge_custom_error_responses=dict(type="bool", default=False),
        logging=dict(type="dict"),
        price_class=dict(),
        enabled=dict(type="bool"),
        viewer_certificate=dict(type="dict"),
        restrictions=dict(type="dict"),
        web_acl_id=dict(),
        http_version=dict(),
        ipv6_enabled=dict(type="bool"),
        default_origin_domain_name=dict(),
        default_origin_path=dict(),
        wait=dict(default=False, type="bool"),
        wait_timeout=dict(default=1800, type="int"),
    )

    result = {}
    changed = True

    module = AnsibleAWSModule(
        argument_spec=argument_spec,
        supports_check_mode=False,
        mutually_exclusive=[
            ["distribution_id", "alias"],
            ["default_origin_domain_name", "distribution_id"],
            ["default_origin_domain_name", "alias"],
        ],
    )

    client = module.client("cloudfront", retry_decorator=AWSRetry.jittered_backoff())

    validation_mgr = CloudFrontValidationManager(module)

    state = module.params.get("state")
    caller_reference = module.params.get("caller_reference")
    comment = module.params.get("comment")
    e_tag = module.params.get("e_tag")
    tags = module.params.get("tags")
    purge_tags = module.params.get("purge_tags")
    distribution_id = module.params.get("distribution_id")
    alias = module.params.get("alias")
    aliases = module.params.get("aliases")
    purge_aliases = module.params.get("purge_aliases")
    default_root_object = module.params.get("default_root_object")
    origins = module.params.get("origins")
    purge_origins = module.params.get("purge_origins")
    default_cache_behavior = module.params.get("default_cache_behavior")
    cache_behaviors = module.params.get("cache_behaviors")
    purge_cache_behaviors = module.params.get("purge_cache_behaviors")
    custom_error_responses = module.params.get("custom_error_responses")
    purge_custom_error_responses = module.params.get("purge_custom_error_responses")
    logging = module.params.get("logging")
    price_class = module.params.get("price_class")
    enabled = module.params.get("enabled")
    viewer_certificate = module.params.get("viewer_certificate")
    restrictions = module.params.get("restrictions")
    purge_restrictions = module.params.get("purge_restrictions")
    web_acl_id = module.params.get("web_acl_id")
    http_version = module.params.get("http_version")
    ipv6_enabled = module.params.get("ipv6_enabled")
    default_origin_domain_name = module.params.get("default_origin_domain_name")
    default_origin_path = module.params.get("default_origin_path")
    wait = module.params.get("wait")
    wait_timeout = module.params.get("wait_timeout")

    if alias and alias not in aliases:
        aliases.append(alias)

    distribution = validation_mgr.validate_distribution_from_aliases_caller_reference(
        distribution_id, aliases, caller_reference
    )

    update = state == "present" and distribution
    create = state == "present" and not distribution
    delete = state == "absent" and distribution

    if not (update or create or delete):
        module.exit_json(changed=False)

    config = {}
    if update or delete:
        config = distribution["Distribution"]["DistributionConfig"]
        e_tag = distribution["ETag"]
        distribution_id = distribution["Distribution"]["Id"]

    if update:
        config = camel_dict_to_snake_dict(config, reversible=True)

    if create or update:
        config = validation_mgr.validate_common_distribution_parameters(
            config, enabled, aliases, logging, price_class, purge_aliases
        )
        config = validation_mgr.validate_distribution_config_parameters(
            config, default_root_object, ipv6_enabled, http_version, web_acl_id
        )
        config["origins"] = validation_mgr.validate_origins(
            client,
            config.get("origins", {}).get("items", []),
            origins,
            default_origin_domain_name,
            default_origin_path,
            create,
            purge_origins,
        )
        config["cache_behaviors"] = validation_mgr.validate_cache_behaviors(
            config.get("cache_behaviors", {}).get("items", []),
            cache_behaviors,
            config["origins"],
            purge_cache_behaviors,
        )
        config["default_cache_behavior"] = validation_mgr.validate_cache_behavior(
            config.get("default_cache_behavior", {}), default_cache_behavior, config["origins"], True
        )
        config["custom_error_responses"] = validation_mgr.validate_custom_error_responses(
            config.get("custom_error_responses", {}).get("items", []),
            custom_error_responses,
            purge_custom_error_responses,
        )
        valid_restrictions = validation_mgr.validate_restrictions(
            config.get("restrictions", {}), restrictions, purge_restrictions
        )
        if valid_restrictions:
            config["restrictions"] = valid_restrictions
        valid_viewer_certificate = validation_mgr.validate_viewer_certificate(viewer_certificate)
        config = merge_validation_into_config(config, valid_viewer_certificate, "viewer_certificate")
        config = validation_mgr.validate_comment(config, comment)
        config = snake_dict_to_camel_dict(config, capitalize_first=True)

    if create:
        config["CallerReference"] = validation_mgr.validate_caller_reference(caller_reference)
        result = create_distribution(client, module, config, ansible_dict_to_boto3_tag_list(tags or {}))
        result = camel_dict_to_snake_dict(result)
        duplicate_keys_for_deprecation(result)
        result["tags"] = list_tags_for_resource(client, module, result["arn"])

    if delete:
        if config["Enabled"]:
            config["Enabled"] = False
            result = update_distribution(client, module, config, distribution_id, e_tag)
            validation_mgr.wait_until_processed(client, wait_timeout, distribution_id, config.get("CallerReference"))
        distribution = validation_mgr.validate_distribution_from_aliases_caller_reference(
            distribution_id, aliases, caller_reference
        )
        # e_tag = distribution['ETag']
        result = delete_distribution(client, module, distribution)
        result.pop("ResponseMetadata", None)

    if update:
        changed = config != distribution["Distribution"]["DistributionConfig"]
        if changed:
            result = update_distribution(client, module, config, distribution_id, e_tag)
        else:
            result = distribution["Distribution"]
        existing_tags = list_tags_for_resource(client, module, result["ARN"])
        distribution["Distribution"]["DistributionConfig"]["tags"] = existing_tags
        changed |= update_tags(client, module, existing_tags, tags, purge_tags, result["ARN"])
        result = camel_dict_to_snake_dict(result)
        duplicate_keys_for_deprecation(result)
        result["distribution_config"]["tags"] = config["tags"] = list_tags_for_resource(client, module, result["arn"])
        result["diff"] = dict()
        diff = recursive_diff(distribution["Distribution"]["DistributionConfig"], config)
        if diff:
            result["diff"]["before"] = diff[0]
            result["diff"]["after"] = diff[1]

    if wait and (create or update):
        validation_mgr.wait_until_processed(client, wait_timeout, distribution_id, config.get("CallerReference"))

    if "distribution_config" in result:
        result.update(result["distribution_config"])
        del result["distribution_config"]

    module.exit_json(changed=changed, **result)


if __name__ == "__main__":
    main()
