Ensure the given CloudFront distribution exists in the described state.

    The implementation of this function, and all those following, is orthagonal
    to that of :py:mod:`boto_cloudfront.present
    <salt.states.boto_cloudfront.present>`. Resources created with
    :py:mod:`boto_cloudfront.present <salt.states.boto_cloudfront.present>`
    will not be correctly managed by this function, as a different method is
    used to store Salt's state signifier. This function and those following are
    a suite, designed to work together. As an extra bonus, they correctly
    process updates of the managed resources, so it is recommended to use them
    in preference to :py:mod:`boto_cloudfront.present
    <salt.states.boto_cloudfront.present>` above.

    Note that the semantics of DistributionConfig (below) are rather arcane,
    and vary wildly depending on whether the distribution already exists or not
    (e.g. is being initially created, or being updated in place). Many more
    details can be found here__.

    .. __: https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/distribution-overview-required-fields.html

    name (string)
        Name of the state definition.

    Name (string)
        Name of the resource (for purposes of Salt's idempotency). If not
        provided, the value of ``name`` will be used.

    DistributionConfig (dict)
        Configuration for the distribution.

        Notes:

        - The CallerReference field should NOT be provided - it will be
          autopopulated by Salt.

        - A large number of sub- (and sub-sub-) fields require a ``Quantity``
          element, which simply COUNTS the number of items in the ``Items``
          element. This is bluntly stupid, so as a convenience, Salt will
          traverse the provided configuration, and add (or fix) a ``Quantity``
          element for any ``Items`` elements of list-type it encounters. This
          adds a bit of sanity to an otherwise error-prone situation. Note
          that for this to work, zero-length lists must be inlined as ``[]``.

        - Due to the unavailibity of a better way to store stateful idempotency
          information about Distributions, the Comment sub-element (as the only
          user-settable attribute without weird self-blocking semantics, and
          which is available from the core ``get_distribution()`` API call) is
          utilized to store the Salt state signifier, which is used to
          determine resource existence and state. That said, to enable **some**
          usability of this field, only the value up to the first colon
          character is taken as the signifier, with everything afterward
          free-form, and ignored (but preserved) by Salt.

    Tags (dict)
        Tags to associate with the distribution.

    region (string)
        Region to connect to.

    key (string)
        Secret key to use.

    keyid (string)
        Access key to use.

    profile (dict or string)
        Dict, or pillar key pointing to a dict, containing AWS region/key/keyid.

    Example:

    .. code-block:: yaml

          plt-dev-spaapi-cf-dist-cf_dist-present:
            boto_cloudfront.distribution_present:
            - Name: plt-dev-spaapi-cf-dist
            - DistributionConfig:
                Comment: SPA
                Logging:
                  Enabled: false
                  Prefix: ''
                  Bucket: ''
                  IncludeCookies: false
                WebACLId: ''
                Origins:
                  Items:
                  - S3OriginConfig:
                      OriginAccessIdentity: the-SPA-OAI
                    OriginPath: ''
                    CustomHeaders:
                      Items: []
                    Id: S3-hs-backend-srpms
                    DomainName: hs-backend-srpms.s3.amazonaws.com
                PriceClass: PriceClass_All
                DefaultRootObject: ''
                Enabled: true
                DefaultCacheBehavior:
                  ViewerProtocolPolicy: allow-all
                  TrustedSigners:
                    Items: []
                    Enabled: false
                  SmoothStreaming: false
                  TargetOriginId: S3-hs-backend-srpms
                  FieldLevelEncryptionId: ''
                  ForwardedValues:
                    Headers:
                      Items: []
                    Cookies:
                      Forward: none
                    QueryStringCacheKeys:
                      Items: []
                    QueryString: false
                  MaxTTL: 31536000
                  LambdaFunctionAssociations:
                    Items: []
                  DefaultTTL: 86400
                  AllowedMethods:
                    CachedMethods:
                      Items:
                      - HEAD
                      - GET
                    Items:
                    - HEAD
                    - GET
                  MinTTL: 0
                  Compress: false
                IsIPV6Enabled: true
                ViewerCertificate:
                  CloudFrontDefaultCertificate: true
                  MinimumProtocolVersion: TLSv1
                  CertificateSource: cloudfront
                Aliases:
                  Items:
                  - bubba-hotep.bodhi-dev.io
                HttpVersion: http2
            - Tags:
                Owner: dev_engrs

def distribution_present(name, region=None, key=None, keyid=None, profile=None, **kwargs):
    '''
    Ensure the given CloudFront distribution exists in the described state.

    The implementation of this function, and all those following, is orthagonal
    to that of :py:mod:`boto_cloudfront.present
    <salt.states.boto_cloudfront.present>`. Resources created with
    :py:mod:`boto_cloudfront.present <salt.states.boto_cloudfront.present>`
    will not be correctly managed by this function, as a different method is
    used to store Salt's state signifier. This function and those following are
    a suite, designed to work together. As an extra bonus, they correctly
    process updates of the managed resources, so it is recommended to use them
    in preference to :py:mod:`boto_cloudfront.present
    <salt.states.boto_cloudfront.present>` above.

    Note that the semantics of DistributionConfig (below) are rather arcane,
    and vary wildly depending on whether the distribution already exists or not
    (e.g. is being initially created, or being updated in place). Many more
    details can be found here__.

    .. __: https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/distribution-overview-required-fields.html

    name (string)
        Name of the state definition.

    Name (string)
        Name of the resource (for purposes of Salt's idempotency). If not
        provided, the value of ``name`` will be used.

    DistributionConfig (dict)
        Configuration for the distribution.

        Notes:

        - The CallerReference field should NOT be provided - it will be
          autopopulated by Salt.

        - A large number of sub- (and sub-sub-) fields require a ``Quantity``
          element, which simply COUNTS the number of items in the ``Items``
          element. This is bluntly stupid, so as a convenience, Salt will
          traverse the provided configuration, and add (or fix) a ``Quantity``
          element for any ``Items`` elements of list-type it encounters. This
          adds a bit of sanity to an otherwise error-prone situation. Note
          that for this to work, zero-length lists must be inlined as ``[]``.

        - Due to the unavailibity of a better way to store stateful idempotency
          information about Distributions, the Comment sub-element (as the only
          user-settable attribute without weird self-blocking semantics, and
          which is available from the core ``get_distribution()`` API call) is
          utilized to store the Salt state signifier, which is used to
          determine resource existence and state. That said, to enable **some**
          usability of this field, only the value up to the first colon
          character is taken as the signifier, with everything afterward
          free-form, and ignored (but preserved) by Salt.

    Tags (dict)
        Tags to associate with the distribution.

    region (string)
        Region to connect to.

    key (string)
        Secret key to use.

    keyid (string)
        Access key to use.

    profile (dict or string)
        Dict, or pillar key pointing to a dict, containing AWS region/key/keyid.

    Example:

    .. code-block:: yaml

          plt-dev-spaapi-cf-dist-cf_dist-present:
            boto_cloudfront.distribution_present:
            - Name: plt-dev-spaapi-cf-dist
            - DistributionConfig:
                Comment: SPA
                Logging:
                  Enabled: false
                  Prefix: ''
                  Bucket: ''
                  IncludeCookies: false
                WebACLId: ''
                Origins:
                  Items:
                  - S3OriginConfig:
                      OriginAccessIdentity: the-SPA-OAI
                    OriginPath: ''
                    CustomHeaders:
                      Items: []
                    Id: S3-hs-backend-srpms
                    DomainName: hs-backend-srpms.s3.amazonaws.com
                PriceClass: PriceClass_All
                DefaultRootObject: ''
                Enabled: true
                DefaultCacheBehavior:
                  ViewerProtocolPolicy: allow-all
                  TrustedSigners:
                    Items: []
                    Enabled: false
                  SmoothStreaming: false
                  TargetOriginId: S3-hs-backend-srpms
                  FieldLevelEncryptionId: ''
                  ForwardedValues:
                    Headers:
                      Items: []
                    Cookies:
                      Forward: none
                    QueryStringCacheKeys:
                      Items: []
                    QueryString: false
                  MaxTTL: 31536000
                  LambdaFunctionAssociations:
                    Items: []
                  DefaultTTL: 86400
                  AllowedMethods:
                    CachedMethods:
                      Items:
                      - HEAD
                      - GET
                    Items:
                    - HEAD
                    - GET
                  MinTTL: 0
                  Compress: false
                IsIPV6Enabled: true
                ViewerCertificate:
                  CloudFrontDefaultCertificate: true
                  MinimumProtocolVersion: TLSv1
                  CertificateSource: cloudfront
                Aliases:
                  Items:
                  - bubba-hotep.bodhi-dev.io
                HttpVersion: http2
            - Tags:
                Owner: dev_engrs

    '''
    ret = {'name': name, 'result': True, 'comment': '', 'changes': {}}
    kwargs = {k: v for k, v in kwargs.items() if not k.startswith('_')}
    authargs = {'region': region, 'key': key, 'keyid': keyid, 'profile': profile}
    Name = kwargs.pop('Name', name)
    Tags = kwargs.pop('Tags', None)
    DistributionConfig = kwargs.get('DistributionConfig', {})

    ## Sub-element munging on config data should go in here, before we proceed:
    #  For instance, origin access identities must be of the form
    #  `origin-access-identity/cloudfront/ID-of-origin-access-identity`, but we can't really
    #  know that ID apriori, so any OAI state names inside the config data must be resolved
    #  and converted into that format before submission. Be aware that the `state names` of
    #  salt managed OAIs are stored in their Comment fields for lack of any better place...
    for item in range(len(DistributionConfig.get('Origins', {}).get('Items', []))):
        oai = DistributionConfig['Origins']['Items'][item].get('S3OriginConfig',
                                                   {}).get('OriginAccessIdentity', '')
        if oai and not oai.startswith('origin-access-identity/cloudfront/'):
            res = __salt__['boto_cloudfront.get_cloud_front_origin_access_identities_by_comment'](
                    Comment=oai, region=region, key=key, keyid=keyid, profile=profile)
            if res is None:  # An error occurred, bubble it up...
                log.warning('Error encountered while trying to determine the Resource ID of'
                            ' CloudFront origin access identity `%s`.  Passing as-is.', oai)
            elif not res:
                log.warning('Failed to determine the Resource ID of CloudFront origin access'
                            ' identity `%s`.  Passing as-is.', oai)
            elif len(res) > 1:
                log.warning('Failed to find unique Resource ID for CloudFront origin access'
                            ' identity `%s`.  Passing as-is.', oai)
            else:
                # One unique OAI resource found -- deref and replace it...
                new = 'origin-access-identity/cloudfront/{}'.format(res[0]['Id'])
                DistributionConfig['Origins']['Items'][item]['S3OriginConfig']['OriginAccessIdentity'] = new
    # Munge Name into the Comment field...
    DistributionConfig['Comment'] = '{}:{}'.format(Name, DistributionConfig['Comment']) \
            if DistributionConfig.get('Comment') else Name

    # Fix up any missing (or wrong) Quantity sub-elements...
    DistributionConfig = _fix_quantities(DistributionConfig)
    kwargs['DistributionConfig'] = DistributionConfig

    # Current state of the thing?
    res = __salt__['boto_cloudfront.get_distributions_by_comment'](Comment=Name, region=region,
            key=key, keyid=keyid, profile=profile)
    if res is None:
        msg = 'Error determining current state of distribution `{}`.'.format(Name)
        log.error(msg)
        ret['comment'] = msg
        ret['result'] = False
        return ret
    if len(res) > 1:
        msg = 'Multiple CloudFront distibutions matched `{}`.'.format(Name)
        log.error(msg)
        ret['comment'] = msg
        ret['result'] = False
        return ret

    # Luckily, the `DistributionConfig` structure returned by `get_distribution()` (as a sub-
    # element of `Distribution`) is identical to that returned by `get_distribution_config(),
    # and as a bonus, the ETag's are ALSO compatible...
    # Since "updates" are actually "replace everything from scratch" events, this implies that
    # it's enough to simply determine SOME update is necessary to trigger one, rather than
    # exhaustively calculating all changes needed - this makes life MUCH EASIER :)
    # Thus our workflow here is:
    # - check if the distribution exists
    # - if it doesn't, create it fresh with the requested DistributionConfig, and Tag it if needed
    # - if it does, grab its ETag, and TWO copies of the current DistributionConfig
    # - merge the requested DistributionConfig on top of one of them
    # - compare the copy we just merged against the one we didn't
    # - if they differ, send the merged copy, along with the ETag we got, back as an update
    # - lastly, verify and set/unset any Tags which may need changing...
    exists = bool(res)
    if not exists:
        if 'CallerReference' not in kwargs['DistributionConfig']:
            kwargs['DistributionConfig']['CallerReference'] = str(uuid.uuid4())
        if __opts__['test']:
            ret['result'] = None
            ret['comment'] = 'CloudFront distribution `{}` would be created.'.format(Name)
            new = {'DistributionConfig': kwargs['DistributionConfig']}
            new.update({'Tags': Tags}) if Tags else None
            ret['pchanges'] = {'old': None, 'new': new}
            return ret
        kwargs.update(authargs)
        comments = []
        res = __salt__['boto_cloudfront.create_distribution_v2'](**kwargs)
        if res is None:
            ret['result'] = False
            msg = 'Error occurred while creating distribution `{}`.'.format(Name)
            log.error(msg)
            ret['comment'] = msg
            return ret
        new = {'DistributionConfig': res['Distribution']['DistributionConfig']}
        comments += ['Created distribution `{}`.'.format(Name)]
        newARN = res.get('Distribution', {}).get('ARN')
        tagged = __salt__['boto_cloudfront.tag_resource'](Tags=Tags, **authargs)
        if tagged is False:
            ret['result'] = False
            msg = 'Error occurred while tagging distribution `{}`.'.format(Name)
            log.error(msg)
            comments += [msg]
            ret['comment'] = '  '.join(comments)
            return ret
        comments += ['Tagged distribution `{}`.'.format(Name)]
        new['Tags'] = Tags
        ret['comment'] = '  '.join(comments)
        ret['changes'] = {'old': None, 'new': new}
        return ret
    else:
        currentId = res[0]['Id']
        current = __salt__['boto_cloudfront.get_distribution_v2'](Id=currentId, **authargs)
        # Insanely unlikely given that we JUST got back this Id from the previous search, but....
        if not current:
            msg = 'Failed to lookup CloudFront distribution with Id `{}`.'.format(currentId)
            log.error(msg)
            ret['comment'] = msg
            ret['result'] = False
            return ret
        currentDC = current['Distribution']['DistributionConfig']
        currentARN = current['Distribution']['ARN']
        currentETag = current['ETag']
        currentTags = __salt__['boto_cloudfront.list_tags_for_resource'](Resource=currentARN,
                                                                         **authargs)
        copyOne = copy.deepcopy(currentDC)
        copyTwo = copy.deepcopy(currentDC)
        copyTwo.update(kwargs['DistributionConfig'])
        correct = __utils__['boto3.json_objs_equal'](copyOne, copyTwo)
        tags_correct = (currentTags == Tags)
        comments = []
        old = {}
        new = {}

        if correct and tags_correct:
            ret['comment'] = 'CloudFront distribution `{}` is in the correct state.'.format(Name)
            return ret
        if __opts__['test']:
            ret['result'] = None
            if not correct:
                comments += ['CloudFront distribution `{}` config would be updated.'.format(Name)]
                old['DistributionConfig'] = copyOne
                new['DistributionConfig'] = copyTwo
            if not tags_correct:
                comments += ['CloudFront distribution `{}` Tags would be updated.'.format(Name)]
                old['Tags'] = currentTags
                new['Tags'] = Tags
            ret['comment'] = '  '.join(comments)
            ret['pchanges'] = {'old': old, 'new': new}
            return ret
        if not correct:
            kwargs = {'DistributionConfig': copyTwo, 'Id': currentId, 'IfMatch': currentETag}
            kwargs.update(authargs)
            log.debug('Calling `boto_cloudfront.update_distribution_v2()` with **kwargs =='
                      ' %s', kwargs)
            res = __salt__['boto_cloudfront.update_distribution_v2'](**kwargs)
            if res is None:
                ret['result'] = False
                msg = 'Error occurred while updating distribution `{}`.'.format(Name)
                log.error(msg)
                ret['comment'] = msg
                return ret
            old['DistributionConfig'] = copyOne
            new['DistributionConfig'] = res['Distribution']['DistributionConfig']
            comments += ['CloudFront distribution `{}` config updated.'.format(Name)]
        if not tags_correct:
            tagged = __salt__['boto_cloudfront.enforce_tags'](Resource=currentARN, Tags=Tags,
                    **authargs)
            if tagged is False:
                ret['result'] = False
                msg = 'Error occurred while updating Tags on distribution `{}`.'.format(Name)
                log.error(msg)
                comments += [msg]
                ret['comment'] = '  '.join(comments)
                return ret
            comments += ['CloudFront distribution `{}` Tags updated.'.format(Name)]
            old['Tags'] = currentTags
            new['Tags'] = Tags
        ret['comment'] = '  '.join(comments)
        ret['changes'] = {'old': old, 'new': new}
        return ret