text: #!/usr/bin/python
  # Copyright (c) 2017 Will Thames
  # Copyright (c) 2015 Mike Mochan
  # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
  
  from __future__ import (absolute_import, division, print_function)
  __metaclass__ = type
  
  
  DOCUMENTATION = r'''
  module: aws_waf_rule
  short_description: Create and delete WAF Rules
  version_added: 1.0.0
  description:
    - Read the AWS documentation for WAF
      U(https://aws.amazon.com/documentation/waf/).
  
  author:
    - Mike Mochan (@mmochan)
    - Will Thames (@willthames)
  extends_documentation_fragment:
  - amazon.aws.aws
  - amazon.aws.ec2
  
  options:
      name:
          description: Name of the Web Application Firewall rule.
          required: yes
          type: str
      metric_name:
          description:
          - A friendly name or description for the metrics for the rule.
          - The name can contain only alphanumeric characters (A-Z, a-z, 0-9); the name can't contain whitespace.
          - You can't change I(metric_name) after you create the rule.
          - Defaults to the same as I(name) with disallowed characters removed.
          type: str
      state:
          description: Whether the rule should be present or absent.
          choices:
          - present
          - absent
          default: present
          type: str
      conditions:
          description: >
            List of conditions used in the rule. M(community.aws.aws_waf_condition) can be used to
            create new conditions.
          type: list
          elements: dict
          suboptions:
              type:
                  required: true
                  type: str
                  choices: ['byte','geo','ip','size','sql','xss']
                  description: The type of rule to match.
              negated:
                  required: true
                  type: bool
                  description: Whether the condition should be negated.
              condition:
                  required: true
                  type: str
                  description: The name of the condition.  The condition must already exist.
      purge_conditions:
          description:
            - Whether or not to remove conditions that are not passed when updating `conditions`.
          default: false
          type: bool
      waf_regional:
          description: Whether to use waf-regional module.
          default: false
          required: false
          type: bool
  '''
  
  EXAMPLES = r'''
    - name: create WAF rule
      community.aws.aws_waf_rule:
        name: my_waf_rule
        conditions:
          - name: my_regex_condition
            type: regex
            negated: no
          - name: my_geo_condition
            type: geo
            negated: no
          - name: my_byte_condition
            type: byte
            negated: yes
  
    - name: remove WAF rule
      community.aws.aws_waf_rule:
        name: "my_waf_rule"
        state: absent
  '''
  
  RETURN = r'''
  rule:
    description: WAF rule contents
    returned: always
    type: complex
    contains:
      metric_name:
        description: Metric name for the rule.
        returned: always
        type: str
        sample: ansibletest1234rule
      name:
        description: Friendly name for the rule.
        returned: always
        type: str
        sample: ansible-test-1234_rule
      predicates:
        description: List of conditions used in the rule.
        returned: always
        type: complex
        contains:
          data_id:
            description: ID of the condition.
            returned: always
            type: str
            sample: 8251acdb-526c-42a8-92bc-d3d13e584166
          negated:
            description: Whether the sense of the condition is negated.
            returned: always
            type: bool
            sample: false
          type:
            description: type of the condition.
            returned: always
            type: str
            sample: ByteMatch
      rule_id:
        description: ID of the WAF rule.
        returned: always
        type: str
        sample: 15de0cbc-9204-4e1f-90e6-69b2f415c261
  '''
  
  import re
  
  try:
      import botocore
  except ImportError:
      pass  # handled by AnsibleAWSModule
  
  from ansible_collections.amazon.aws.plugins.module_utils.core import AnsibleAWSModule
  from ansible_collections.amazon.aws.plugins.module_utils.ec2 import camel_dict_to_snake_dict
  from ansible_collections.amazon.aws.plugins.module_utils.waf import (
      MATCH_LOOKUP,
      list_regional_rules_with_backoff,
      list_rules_with_backoff,
      run_func_with_change_token_backoff,
      get_web_acl_with_backoff,
      list_web_acls_with_backoff,
      list_regional_web_acls_with_backoff,
  )
  
  
  def get_rule_by_name(client, module, name):
      rules = [d['RuleId'] for d in list_rules(client, module) if d['Name'] == name]
      if rules:
          return rules[0]
  
  
  def get_rule(client, module, rule_id):
      try:
          return client.get_rule(RuleId=rule_id)['Rule']
      except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
          module.fail_json_aws(e, msg='Could not get WAF rule')
  
  
  def list_rules(client, module):
      if client.__class__.__name__ == 'WAF':
          try:
              return list_rules_with_backoff(client)
          except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
              module.fail_json_aws(e, msg='Could not list WAF rules')
      elif client.__class__.__name__ == 'WAFRegional':
          try:
              return list_regional_rules_with_backoff(client)
          except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
              module.fail_json_aws(e, msg='Could not list WAF Regional rules')
  
  
  def list_regional_rules(client, module):
      try:
          return list_regional_rules_with_backoff(client)
      except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
          module.fail_json_aws(e, msg='Could not list WAF rules')
  
  
  def find_and_update_rule(client, module, rule_id):
      rule = get_rule(client, module, rule_id)
      rule_id = rule['RuleId']
  
      existing_conditions = dict((condition_type, dict()) for condition_type in MATCH_LOOKUP)
      desired_conditions = dict((condition_type, dict()) for condition_type in MATCH_LOOKUP)
      all_conditions = dict()
  
      for condition_type in MATCH_LOOKUP:
          method = 'list_' + MATCH_LOOKUP[condition_type]['method'] + 's'
          all_conditions[condition_type] = dict()
          try:
              paginator = client.get_paginator(method)
              func = paginator.paginate().build_full_result
          except (KeyError, botocore.exceptions.OperationNotPageableError):
              # list_geo_match_sets and list_regex_match_sets do not have a paginator
              # and throw different exceptions
              func = getattr(client, method)
          try:
              pred_results = func()[MATCH_LOOKUP[condition_type]['conditionset'] + 's']
          except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
              module.fail_json_aws(e, msg='Could not list %s conditions' % condition_type)
          for pred in pred_results:
              pred['DataId'] = pred[MATCH_LOOKUP[condition_type]['conditionset'] + 'Id']
              all_conditions[condition_type][pred['Name']] = camel_dict_to_snake_dict(pred)
              all_conditions[condition_type][pred['DataId']] = camel_dict_to_snake_dict(pred)
  
      for condition in module.params['conditions']:
          desired_conditions[condition['type']][condition['name']] = condition
  
      reverse_condition_types = dict((v['type'], k) for (k, v) in MATCH_LOOKUP.items())
      for condition in rule['Predicates']:
          existing_conditions[reverse_condition_types[condition['Type']]][condition['DataId']] = camel_dict_to_snake_dict(condition)
  
      insertions = list()
      deletions = list()
  
      for condition_type in desired_conditions:
          for (condition_name, condition) in desired_conditions[condition_type].items():
              if condition_name not in all_conditions[condition_type]:
                  module.fail_json(msg="Condition %s of type %s does not exist" % (condition_name, condition_type))
              condition['data_id'] = all_conditions[condition_type][condition_name]['data_id']
              if condition['data_id'] not in existing_conditions[condition_type]:
                  insertions.append(format_for_insertion(condition))
  
      if module.params['purge_conditions']:
          for condition_type in existing_conditions:
              deletions.extend([format_for_deletion(condition) for condition in existing_conditions[condition_type].values()
                                if not all_conditions[condition_type][condition['data_id']]['name'] in desired_conditions[condition_type]])
  
      changed = bool(insertions or deletions)
      update = {
          'RuleId': rule_id,
          'Updates': insertions + deletions
      }
      if changed:
          try:
              run_func_with_change_token_backoff(client, module, update, client.update_rule, wait=True)
          except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
              module.fail_json_aws(e, msg='Could not update rule conditions')
  
      return changed, get_rule(client, module, rule_id)
  
  
  def format_for_insertion(condition):
      return dict(Action='INSERT',
                  Predicate=dict(Negated=condition['negated'],
                                 Type=MATCH_LOOKUP[condition['type']]['type'],
                                 DataId=condition['data_id']))
  
  
  def format_for_deletion(condition):
      return dict(Action='DELETE',
                  Predicate=dict(Negated=condition['negated'],
                                 Type=condition['type'],
                                 DataId=condition['data_id']))
  
  
  def remove_rule_conditions(client, module, rule_id):
      conditions = get_rule(client, module, rule_id)['Predicates']
      updates = [format_for_deletion(camel_dict_to_snake_dict(condition)) for condition in conditions]
      try:
          run_func_with_change_token_backoff(client, module, {'RuleId': rule_id, 'Updates': updates}, client.update_rule)
      except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
          module.fail_json_aws(e, msg='Could not remove rule conditions')
  
  
  def ensure_rule_present(client, module):
      name = module.params['name']
      rule_id = get_rule_by_name(client, module, name)
      params = dict()
      if rule_id:
          return find_and_update_rule(client, module, rule_id)
      else:
          params['Name'] = module.params['name']
          metric_name = module.params['metric_name']
          if not metric_name:
              metric_name = re.sub(r'[^a-zA-Z0-9]', '', module.params['name'])
          params['MetricName'] = metric_name
          try:
              new_rule = run_func_with_change_token_backoff(client, module, params, client.create_rule)['Rule']
          except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
              module.fail_json_aws(e, msg='Could not create rule')
          return find_and_update_rule(client, module, new_rule['RuleId'])
  
  
  def find_rule_in_web_acls(client, module, rule_id):
      web_acls_in_use = []
      try:
          if client.__class__.__name__ == 'WAF':
              all_web_acls = list_web_acls_with_backoff(client)
          elif client.__class__.__name__ == 'WAFRegional':
              all_web_acls = list_regional_web_acls_with_backoff(client)
      except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
          module.fail_json_aws(e, msg='Could not list Web ACLs')
      for web_acl in all_web_acls:
          try:
              web_acl_details = get_web_acl_with_backoff(client, web_acl['WebACLId'])
          except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
              module.fail_json_aws(e, msg='Could not get Web ACL details')
          if rule_id in [rule['RuleId'] for rule in web_acl_details['Rules']]:
              web_acls_in_use.append(web_acl_details['Name'])
      return web_acls_in_use
  
  
  def ensure_rule_absent(client, module):
      rule_id = get_rule_by_name(client, module, module.params['name'])
      in_use_web_acls = find_rule_in_web_acls(client, module, rule_id)
      if in_use_web_acls:
          web_acl_names = ', '.join(in_use_web_acls)
          module.fail_json(msg="Rule %s is in use by Web ACL(s) %s" %
                           (module.params['name'], web_acl_names))
      if rule_id:
          remove_rule_conditions(client, module, rule_id)
          try:
              return True, run_func_with_change_token_backoff(client, module, {'RuleId': rule_id}, client.delete_rule, wait=True)
          except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
              module.fail_json_aws(e, msg='Could not delete rule')
      return False, {}
  
  
  def main():
      argument_spec = dict(
          name=dict(required=True),
          metric_name=dict(),
          state=dict(default='present', choices=['present', 'absent']),
          conditions=dict(type='list', elements='dict'),
          purge_conditions=dict(type='bool', default=False),
          waf_regional=dict(type='bool', default=False),
      )
      module = AnsibleAWSModule(argument_spec=argument_spec)
      state = module.params.get('state')
  
      resource = 'waf' if not module.params['waf_regional'] else 'waf-regional'
      client = module.client(resource)
      if state == 'present':
          (changed, results) = ensure_rule_present(client, module)
      else:
          (changed, results) = ensure_rule_absent(client, module)
  
      module.exit_json(changed=changed, rule=camel_dict_to_snake_dict(results))
  
  
  if __name__ == '__main__':
      main()
  