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

# Copyright (c) 2016 Michael Gruener <michael.gruener@chaosmoon.net>
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later

from __future__ import absolute_import, division, print_function
__metaclass__ = type

DOCUMENTATION = r'''
---
module: cloudflare_dns
author:
- Michael Gruener (@mgruener)
short_description: Manage Cloudflare DNS records
description:
   - "Manages dns records via the Cloudflare API, see the docs: U(https://api.cloudflare.com/)."
extends_documentation_fragment:
   - community.general.attributes
attributes:
  check_mode:
    support: full
  diff_mode:
    support: none
options:
  api_token:
    description:
    - API token.
    - Required for api token authentication.
    - "You can obtain your API token from the bottom of the Cloudflare 'My Account' page, found here: U(https://dash.cloudflare.com/)."
    - Can be specified in E(CLOUDFLARE_TOKEN) environment variable since community.general 2.0.0.
    type: str
    required: false
    version_added: '0.2.0'
  account_api_key:
    description:
    - Account API key.
    - Required for api keys authentication.
    - "You can obtain your API key from the bottom of the Cloudflare 'My Account' page, found here: U(https://dash.cloudflare.com/)."
    type: str
    required: false
    aliases: [ account_api_token ]
  account_email:
    description:
    - Account email. Required for API keys authentication.
    type: str
    required: false
  algorithm:
    description:
    - Algorithm number.
    - Required for O(type=DS) and O(type=SSHFP) when O(state=present).
    type: int
  cert_usage:
    description:
    - Certificate usage number.
    - Required for O(type=TLSA) when O(state=present).
    type: int
    choices: [ 0, 1, 2, 3 ]
  flag:
    description:
    - Issuer Critical Flag.
    - Required for O(type=CAA) when O(state=present).
    type: int
    choices: [ 0, 1 ]
    version_added: 8.0.0
  tag:
    description:
    - CAA issue restriction.
    - Required for O(type=CAA) when O(state=present).
    type: str
    choices: [ issue, issuewild, iodef ]
    version_added: 8.0.0
  hash_type:
    description:
    - Hash type number.
    - Required for O(type=DS), O(type=SSHFP) and O(type=TLSA) when O(state=present).
    type: int
    choices: [ 1, 2 ]
  key_tag:
    description:
    - DNSSEC key tag.
    - Needed for O(type=DS) when O(state=present).
    type: int
  port:
    description:
    - Service port.
    - Required for O(type=SRV) and O(type=TLSA).
    type: int
  priority:
    description:
    - Record priority.
    - Required for O(type=MX) and O(type=SRV)
    default: 1
    type: int
  proto:
    description:
    - Service protocol. Required for O(type=SRV) and O(type=TLSA).
    - Common values are TCP and UDP.
    type: str
  proxied:
    description:
    - Proxy through Cloudflare network or just use DNS.
    type: bool
    default: false
  record:
    description:
    - Record to add.
    - Required if O(state=present).
    - Default is V(@) (that is, the zone name).
    type: str
    default: '@'
    aliases: [ name ]
  selector:
    description:
    - Selector number.
    - Required for O(type=TLSA) when O(state=present).
    choices: [ 0, 1 ]
    type: int
  service:
    description:
    - Record service.
    - Required for O(type=SRV).
    type: str
  solo:
    description:
    - Whether the record should be the only one for that record type and record name.
    - Only use with O(state=present).
    - This will delete all other records with the same record name and type.
    type: bool
  state:
    description:
    - Whether the record(s) should exist or not.
    type: str
    choices: [ absent, present ]
    default: present
  timeout:
    description:
    - Timeout for Cloudflare API calls.
    type: int
    default: 30
  ttl:
    description:
    - The TTL to give the new record.
    - Must be between 120 and 2,147,483,647 seconds, or 1 for automatic.
    type: int
    default: 1
  type:
    description:
      - The type of DNS record to create. Required if O(state=present).
      - Support for V(SPF) has been removed from community.general 9.0.0 since that record type is no longer supported by CloudFlare.
    type: str
    choices: [ A, AAAA, CNAME, DS, MX, NS, SRV, SSHFP, TLSA, CAA, TXT ]
  value:
    description:
    - The record value.
    - Required for O(state=present).
    type: str
    aliases: [ content ]
  weight:
    description:
    - Service weight.
    - Required for O(type=SRV).
    type: int
    default: 1
  zone:
    description:
    - The name of the Zone to work with (e.g. "example.com").
    - The Zone must already exist.
    type: str
    required: true
    aliases: [ domain ]
'''

EXAMPLES = r'''
- name: Create a test.example.net A record to point to 127.0.0.1
  community.general.cloudflare_dns:
    zone: example.net
    record: test
    type: A
    value: 127.0.0.1
    account_email: test@example.com
    account_api_key: dummyapitoken
  register: record

- name: Create a record using api token
  community.general.cloudflare_dns:
    zone: example.net
    record: test
    type: A
    value: 127.0.0.1
    api_token: dummyapitoken

- name: Create a example.net CNAME record to example.com
  community.general.cloudflare_dns:
    zone: example.net
    type: CNAME
    value: example.com
    account_email: test@example.com
    account_api_key: dummyapitoken
    state: present

- name: Change its TTL
  community.general.cloudflare_dns:
    zone: example.net
    type: CNAME
    value: example.com
    ttl: 600
    account_email: test@example.com
    account_api_key: dummyapitoken
    state: present

- name: Delete the record
  community.general.cloudflare_dns:
    zone: example.net
    type: CNAME
    value: example.com
    account_email: test@example.com
    account_api_key: dummyapitoken
    state: absent

- name: Create a example.net CNAME record to example.com and proxy through Cloudflare's network
  community.general.cloudflare_dns:
    zone: example.net
    type: CNAME
    value: example.com
    proxied: true
    account_email: test@example.com
    account_api_key: dummyapitoken
    state: present

# This deletes all other TXT records named "test.example.net"
- name: Create TXT record "test.example.net" with value "unique value"
  community.general.cloudflare_dns:
    domain: example.net
    record: test
    type: TXT
    value: unique value
    solo: true
    account_email: test@example.com
    account_api_key: dummyapitoken
    state: present

- name: Create an SRV record _foo._tcp.example.net
  community.general.cloudflare_dns:
    domain: example.net
    service: foo
    proto: tcp
    port: 3500
    priority: 10
    weight: 20
    type: SRV
    value: fooserver.example.net

- name: Create a SSHFP record login.example.com
  community.general.cloudflare_dns:
    zone: example.com
    record: login
    type: SSHFP
    algorithm: 4
    hash_type: 2
    value: 9dc1d6742696d2f51ca1f1a78b3d16a840f7d111eb9454239e70db31363f33e1

- name: Create a TLSA record _25._tcp.mail.example.com
  community.general.cloudflare_dns:
    zone: example.com
    record: mail
    port: 25
    proto: tcp
    type: TLSA
    cert_usage: 3
    selector: 1
    hash_type: 1
    value: 6b76d034492b493e15a7376fccd08e63befdad0edab8e442562f532338364bf3

- name: Create a CAA record subdomain.example.com
  community.general.cloudflare_dns:
    zone: example.com
    record: subdomain
    type: CAA
    flag: 0
    tag: issue
    value: ca.example.com

- name: Create a DS record for subdomain.example.com
  community.general.cloudflare_dns:
    zone: example.com
    record: subdomain
    type: DS
    key_tag: 5464
    algorithm: 8
    hash_type: 2
    value: B4EB5AC4467D2DFB3BAF9FB9961DC1B6FED54A58CDFAA3E465081EC86F89BFAB
'''

RETURN = r'''
record:
    description: A dictionary containing the record data.
    returned: success, except on record deletion
    type: complex
    contains:
        content:
            description: The record content (details depend on record type).
            returned: success
            type: str
            sample: 192.0.2.91
        created_on:
            description: The record creation date.
            returned: success
            type: str
            sample: "2016-03-25T19:09:42.516553Z"
        data:
            description: Additional record data.
            returned: success, if type is SRV, DS, SSHFP TLSA or CAA
            type: dict
            sample: {
                name: "jabber",
                port: 8080,
                priority: 10,
                proto: "_tcp",
                service: "_xmpp",
                target: "jabberhost.sample.com",
                weight: 5,
            }
        id:
            description: The record ID.
            returned: success
            type: str
            sample: f9efb0549e96abcb750de63b38c9576e
        locked:
            description: No documentation available.
            returned: success
            type: bool
            sample: false
        meta:
            description: No documentation available.
            returned: success
            type: dict
            sample: { auto_added: false }
        modified_on:
            description: Record modification date.
            returned: success
            type: str
            sample: "2016-03-25T19:09:42.516553Z"
        name:
            description: The record name as FQDN (including _service and _proto for SRV).
            returned: success
            type: str
            sample: www.sample.com
        priority:
            description: Priority of the MX record.
            returned: success, if type is MX
            type: int
            sample: 10
        proxiable:
            description: Whether this record can be proxied through Cloudflare.
            returned: success
            type: bool
            sample: false
        proxied:
            description: Whether the record is proxied through Cloudflare.
            returned: success
            type: bool
            sample: false
        ttl:
            description: The time-to-live for the record.
            returned: success
            type: int
            sample: 300
        type:
            description: The record type.
            returned: success
            type: str
            sample: A
        zone_id:
            description: The ID of the zone containing the record.
            returned: success
            type: str
            sample: abcede0bf9f0066f94029d2e6b73856a
        zone_name:
            description: The name of the zone containing the record.
            returned: success
            type: str
            sample: sample.com
'''

import json

from ansible.module_utils.basic import AnsibleModule, env_fallback
from ansible.module_utils.six.moves.urllib.parse import urlencode
from ansible.module_utils.common.text.converters import to_native, to_text
from ansible.module_utils.urls import fetch_url


def lowercase_string(param):
    if not isinstance(param, str):
        return param
    return param.lower()


class CloudflareAPI(object):

    cf_api_endpoint = 'https://api.cloudflare.com/client/v4'
    changed = False

    def __init__(self, module):
        self.module = module
        self.api_token = module.params['api_token']
        self.account_api_key = module.params['account_api_key']
        self.account_email = module.params['account_email']
        self.algorithm = module.params['algorithm']
        self.cert_usage = module.params['cert_usage']
        self.hash_type = module.params['hash_type']
        self.flag = module.params['flag']
        self.tag = module.params['tag']
        self.key_tag = module.params['key_tag']
        self.port = module.params['port']
        self.priority = module.params['priority']
        self.proto = lowercase_string(module.params['proto'])
        self.proxied = module.params['proxied']
        self.selector = module.params['selector']
        self.record = lowercase_string(module.params['record'])
        self.service = lowercase_string(module.params['service'])
        self.is_solo = module.params['solo']
        self.state = module.params['state']
        self.timeout = module.params['timeout']
        self.ttl = module.params['ttl']
        self.type = module.params['type']
        self.value = module.params['value']
        self.weight = module.params['weight']
        self.zone = lowercase_string(module.params['zone'])

        if self.record == '@':
            self.record = self.zone

        if (self.type in ['CNAME', 'NS', 'MX', 'SRV']) and (self.value is not None):
            self.value = self.value.rstrip('.').lower()

        if (self.type == 'AAAA') and (self.value is not None):
            self.value = self.value.lower()

        if (self.type == 'SRV'):
            if (self.proto is not None) and (not self.proto.startswith('_')):
                self.proto = '_' + self.proto
            if (self.service is not None) and (not self.service.startswith('_')):
                self.service = '_' + self.service

        if (self.type == 'TLSA'):
            if (self.proto is not None) and (not self.proto.startswith('_')):
                self.proto = '_' + self.proto
            if (self.port is not None):
                self.port = '_' + str(self.port)

        if not self.record.endswith(self.zone):
            self.record = self.record + '.' + self.zone

        if (self.type == 'DS'):
            if self.record == self.zone:
                self.module.fail_json(msg="DS records only apply to subdomains.")

    def _cf_simple_api_call(self, api_call, method='GET', payload=None):
        if self.api_token:
            headers = {
                'Authorization': 'Bearer ' + self.api_token,
                'Content-Type': 'application/json',
            }
        else:
            headers = {
                'X-Auth-Email': self.account_email,
                'X-Auth-Key': self.account_api_key,
                'Content-Type': 'application/json',
            }
        data = None
        if payload:
            try:
                data = json.dumps(payload)
            except Exception as e:
                self.module.fail_json(msg="Failed to encode payload as JSON: %s " % to_native(e))

        resp, info = fetch_url(self.module,
                               self.cf_api_endpoint + api_call,
                               headers=headers,
                               data=data,
                               method=method,
                               timeout=self.timeout)

        if info['status'] not in [200, 304, 400, 401, 403, 429, 405, 415]:
            self.module.fail_json(msg="Failed API call {0}; got unexpected HTTP code {1}: {2}".format(api_call, info['status'], info.get('msg')))

        error_msg = ''
        if info['status'] == 401:
            # Unauthorized
            error_msg = "API user does not have permission; Status: {0}; Method: {1}: Call: {2}".format(info['status'], method, api_call)
        elif info['status'] == 403:
            # Forbidden
            error_msg = "API request not authenticated; Status: {0}; Method: {1}: Call: {2}".format(info['status'], method, api_call)
        elif info['status'] == 429:
            # Too many requests
            error_msg = "API client is rate limited; Status: {0}; Method: {1}: Call: {2}".format(info['status'], method, api_call)
        elif info['status'] == 405:
            # Method not allowed
            error_msg = "API incorrect HTTP method provided; Status: {0}; Method: {1}: Call: {2}".format(info['status'], method, api_call)
        elif info['status'] == 415:
            # Unsupported Media Type
            error_msg = "API request is not valid JSON; Status: {0}; Method: {1}: Call: {2}".format(info['status'], method, api_call)
        elif info['status'] == 400:
            # Bad Request
            error_msg = "API bad request; Status: {0}; Method: {1}: Call: {2}".format(info['status'], method, api_call)

        result = None
        try:
            content = resp.read()
        except AttributeError:
            if info['body']:
                content = info['body']
            else:
                error_msg += "; The API response was empty"

        if content:
            try:
                result = json.loads(to_text(content, errors='surrogate_or_strict'))
            except (getattr(json, 'JSONDecodeError', ValueError)) as e:
                error_msg += "; Failed to parse API response with error {0}: {1}".format(to_native(e), content)

        # Without a valid/parsed JSON response no more error processing can be done
        if result is None:
            self.module.fail_json(msg=error_msg)

        if 'success' not in result:
            error_msg += "; Unexpected error details: {0}".format(result.get('error'))
            self.module.fail_json(msg=error_msg)

        if not result['success']:
            error_msg += "; Error details: "
            for error in result['errors']:
                error_msg += "code: {0}, error: {1}; ".format(error['code'], error['message'])
                if 'error_chain' in error:
                    for chain_error in error['error_chain']:
                        error_msg += "code: {0}, error: {1}; ".format(chain_error['code'], chain_error['message'])
            self.module.fail_json(msg=error_msg)

        return result, info['status']

    def _cf_api_call(self, api_call, method='GET', payload=None):
        result, status = self._cf_simple_api_call(api_call, method, payload)

        data = result['result']

        if 'result_info' in result:
            pagination = result['result_info']
            if pagination['total_pages'] > 1:
                next_page = int(pagination['page']) + 1
                parameters = ['page={0}'.format(next_page)]
                # strip "page" parameter from call parameters (if there are any)
                if '?' in api_call:
                    raw_api_call, query = api_call.split('?', 1)
                    parameters += [param for param in query.split('&') if not param.startswith('page')]
                else:
                    raw_api_call = api_call
                while next_page <= pagination['total_pages']:
                    raw_api_call += '?' + '&'.join(parameters)
                    result, status = self._cf_simple_api_call(raw_api_call, method, payload)
                    data += result['result']
                    next_page += 1

        return data, status

    def _get_zone_id(self, zone=None):
        if not zone:
            zone = self.zone

        zones = self.get_zones(zone)
        if len(zones) > 1:
            self.module.fail_json(msg="More than one zone matches {0}".format(zone))

        if len(zones) < 1:
            self.module.fail_json(msg="No zone found with name {0}".format(zone))

        return zones[0]['id']

    def get_zones(self, name=None):
        if not name:
            name = self.zone
        param = ''
        if name:
            param = '?' + urlencode({'name': name})
        zones, status = self._cf_api_call('/zones' + param)
        return zones

    def get_dns_records(self, zone_name=None, type=None, record=None, value=''):
        if not zone_name:
            zone_name = self.zone
        if not type:
            type = self.type
        if not record:
            record = self.record
        # necessary because None as value means to override user
        # set module value
        if (not value) and (value is not None):
            value = self.value

        zone_id = self._get_zone_id()
        api_call = '/zones/{0}/dns_records'.format(zone_id)
        query = {}
        if type:
            query['type'] = type
        if record:
            query['name'] = record
        if value:
            query['content'] = value
        if query:
            api_call += '?' + urlencode(query)

        records, status = self._cf_api_call(api_call)
        return records

    def delete_dns_records(self, **kwargs):
        params = {}
        for param in ['port', 'proto', 'service', 'solo', 'type', 'record', 'value', 'weight', 'zone',
                      'algorithm', 'cert_usage', 'hash_type', 'selector', 'key_tag', 'flag', 'tag']:
            if param in kwargs:
                params[param] = kwargs[param]
            else:
                params[param] = getattr(self, param)

        records = []
        content = params['value']
        search_record = params['record']
        if params['type'] == 'SRV':
            if not (params['value'] is None or params['value'] == ''):
                content = str(params['weight']) + '\t' + str(params['port']) + '\t' + params['value']
            search_record = params['service'] + '.' + params['proto'] + '.' + params['record']
        elif params['type'] == 'DS':
            if not (params['value'] is None or params['value'] == ''):
                content = str(params['key_tag']) + '\t' + str(params['algorithm']) + '\t' + str(params['hash_type']) + '\t' + params['value']
        elif params['type'] == 'SSHFP':
            if not (params['value'] is None or params['value'] == ''):
                content = str(params['algorithm']) + ' ' + str(params['hash_type']) + ' ' + params['value'].upper()
        elif params['type'] == 'TLSA':
            if not (params['value'] is None or params['value'] == ''):
                content = str(params['cert_usage']) + '\t' + str(params['selector']) + '\t' + str(params['hash_type']) + '\t' + params['value']
            search_record = params['port'] + '.' + params['proto'] + '.' + params['record']
        if params['solo']:
            search_value = None
        else:
            search_value = content

        records = self.get_dns_records(params['zone'], params['type'], search_record, search_value)

        for rr in records:
            if params['solo']:
                if not ((rr['type'] == params['type']) and (rr['name'] == search_record) and (rr['content'] == content)):
                    self.changed = True
                    if not self.module.check_mode:
                        result, info = self._cf_api_call('/zones/{0}/dns_records/{1}'.format(rr['zone_id'], rr['id']), 'DELETE')
            else:
                self.changed = True
                if not self.module.check_mode:
                    result, info = self._cf_api_call('/zones/{0}/dns_records/{1}'.format(rr['zone_id'], rr['id']), 'DELETE')
        return self.changed

    def ensure_dns_record(self, **kwargs):
        params = {}
        for param in ['port', 'priority', 'proto', 'proxied', 'service', 'ttl', 'type', 'record', 'value', 'weight', 'zone',
                      'algorithm', 'cert_usage', 'hash_type', 'selector', 'key_tag', 'flag', 'tag']:
            if param in kwargs:
                params[param] = kwargs[param]
            else:
                params[param] = getattr(self, param)

        search_value = params['value']
        search_record = params['record']
        new_record = None
        if (params['type'] is None) or (params['record'] is None):
            self.module.fail_json(msg="You must provide a type and a record to create a new record")

        if (params['type'] in ['A', 'AAAA', 'CNAME', 'TXT', 'MX', 'NS']):
            if not params['value']:
                self.module.fail_json(msg="You must provide a non-empty value to create this record type")

            # there can only be one CNAME per record
            # ignoring the value when searching for existing
            # CNAME records allows us to update the value if it
            # changes
            if params['type'] == 'CNAME':
                search_value = None

            new_record = {
                "type": params['type'],
                "name": params['record'],
                "content": params['value'],
                "ttl": params['ttl']
            }

        if (params['type'] in ['A', 'AAAA', 'CNAME']):
            new_record["proxied"] = params["proxied"]

        if params['type'] == 'MX':
            for attr in [params['priority'], params['value']]:
                if (attr is None) or (attr == ''):
                    self.module.fail_json(msg="You must provide priority and a value to create this record type")
            new_record = {
                "type": params['type'],
                "name": params['record'],
                "content": params['value'],
                "priority": params['priority'],
                "ttl": params['ttl']
            }

        if params['type'] == 'SRV':
            for attr in [params['port'], params['priority'], params['proto'], params['service'], params['weight'], params['value']]:
                if (attr is None) or (attr == ''):
                    self.module.fail_json(msg="You must provide port, priority, proto, service, weight and a value to create this record type")
            srv_data = {
                "target": params['value'],
                "port": params['port'],
                "weight": params['weight'],
                "priority": params['priority'],
                "name": params['record'],
                "proto": params['proto'],
                "service": params['service']
            }

            new_record = {"type": params['type'], "ttl": params['ttl'], 'data': srv_data}
            search_value = str(params['weight']) + '\t' + str(params['port']) + '\t' + params['value']
            search_record = params['service'] + '.' + params['proto'] + '.' + params['record']

        if params['type'] == 'DS':
            for attr in [params['key_tag'], params['algorithm'], params['hash_type'], params['value']]:
                if (attr is None) or (attr == ''):
                    self.module.fail_json(msg="You must provide key_tag, algorithm, hash_type and a value to create this record type")
            ds_data = {
                "key_tag": params['key_tag'],
                "algorithm": params['algorithm'],
                "digest_type": params['hash_type'],
                "digest": params['value'],
            }
            new_record = {
                "type": params['type'],
                "name": params['record'],
                'data': ds_data,
                "ttl": params['ttl'],
            }
            search_value = str(params['key_tag']) + '\t' + str(params['algorithm']) + '\t' + str(params['hash_type']) + '\t' + params['value']

        if params['type'] == 'SSHFP':
            for attr in [params['algorithm'], params['hash_type'], params['value']]:
                if (attr is None) or (attr == ''):
                    self.module.fail_json(msg="You must provide algorithm, hash_type and a value to create this record type")
            sshfp_data = {
                "fingerprint": params['value'].upper(),
                "type": params['hash_type'],
                "algorithm": params['algorithm'],
            }
            new_record = {
                "type": params['type'],
                "name": params['record'],
                'data': sshfp_data,
                "ttl": params['ttl'],
            }
            search_value = str(params['algorithm']) + ' ' + str(params['hash_type']) + ' ' + params['value']

        if params['type'] == 'TLSA':
            for attr in [params['port'], params['proto'], params['cert_usage'], params['selector'], params['hash_type'], params['value']]:
                if (attr is None) or (attr == ''):
                    self.module.fail_json(msg="You must provide port, proto, cert_usage, selector, hash_type and a value to create this record type")
            search_record = params['port'] + '.' + params['proto'] + '.' + params['record']
            tlsa_data = {
                "usage": params['cert_usage'],
                "selector": params['selector'],
                "matching_type": params['hash_type'],
                "certificate": params['value'],
            }
            new_record = {
                "type": params['type'],
                "name": search_record,
                'data': tlsa_data,
                "ttl": params['ttl'],
            }
            search_value = str(params['cert_usage']) + '\t' + str(params['selector']) + '\t' + str(params['hash_type']) + '\t' + params['value']

        if params['type'] == 'CAA':
            for attr in [params['flag'], params['tag'], params['value']]:
                if (attr is None) or (attr == ''):
                    self.module.fail_json(msg="You must provide flag, tag and a value to create this record type")
            caa_data = {
                "flags": params['flag'],
                "tag": params['tag'],
                "value": params['value'],
            }
            new_record = {
                "type": params['type'],
                "name": params['record'],
                'data': caa_data,
                "ttl": params['ttl'],
            }
            search_value = None

        zone_id = self._get_zone_id(params['zone'])
        records = self.get_dns_records(params['zone'], params['type'], search_record, search_value)
        # in theory this should be impossible as cloudflare does not allow
        # the creation of duplicate records but lets cover it anyways
        if len(records) > 1:
            # As Cloudflare API cannot filter record containing quotes
            # CAA records must be compared locally
            if params['type'] == 'CAA':
                for rr in records:
                    if rr['data']['flags'] == caa_data['flags'] and rr['data']['tag'] == caa_data['tag'] and rr['data']['value'] == caa_data['value']:
                        return rr, self.changed
            else:
                self.module.fail_json(msg="More than one record already exists for the given attributes. That should be impossible, please open an issue!")
        # record already exists, check if it must be updated
        if len(records) == 1:
            cur_record = records[0]
            do_update = False
            if (params['ttl'] is not None) and (cur_record['ttl'] != params['ttl']):
                do_update = True
            if (params['priority'] is not None) and ('priority' in cur_record) and (cur_record['priority'] != params['priority']):
                do_update = True
            if ('proxied' in new_record) and ('proxied' in cur_record) and (cur_record['proxied'] != params['proxied']):
                do_update = True
            if ('data' in new_record) and ('data' in cur_record):
                if (cur_record['data'] != new_record['data']):
                    do_update = True
            if (params['type'] == 'CNAME') and (cur_record['content'] != new_record['content']):
                do_update = True
            if do_update:
                if self.module.check_mode:
                    result = new_record
                else:
                    result, info = self._cf_api_call('/zones/{0}/dns_records/{1}'.format(zone_id, records[0]['id']), 'PUT', new_record)
                self.changed = True
                return result, self.changed
            else:
                return records, self.changed
        if self.module.check_mode:
            result = new_record
        else:
            result, info = self._cf_api_call('/zones/{0}/dns_records'.format(zone_id), 'POST', new_record)
        self.changed = True
        return result, self.changed


def main():
    module = AnsibleModule(
        argument_spec=dict(
            api_token=dict(
                type="str",
                required=False,
                no_log=True,
                fallback=(env_fallback, ["CLOUDFLARE_TOKEN"]),
            ),
            account_api_key=dict(type='str', required=False, no_log=True, aliases=['account_api_token']),
            account_email=dict(type='str', required=False),
            algorithm=dict(type='int'),
            cert_usage=dict(type='int', choices=[0, 1, 2, 3]),
            hash_type=dict(type='int', choices=[1, 2]),
            key_tag=dict(type='int', no_log=False),
            port=dict(type='int'),
            flag=dict(type='int', choices=[0, 1]),
            tag=dict(type='str', choices=['issue', 'issuewild', 'iodef']),
            priority=dict(type='int', default=1),
            proto=dict(type='str'),
            proxied=dict(type='bool', default=False),
            record=dict(type='str', default='@', aliases=['name']),
            selector=dict(type='int', choices=[0, 1]),
            service=dict(type='str'),
            solo=dict(type='bool'),
            state=dict(type='str', default='present', choices=['absent', 'present']),
            timeout=dict(type='int', default=30),
            ttl=dict(type='int', default=1),
            type=dict(type='str', choices=['A', 'AAAA', 'CNAME', 'DS', 'MX', 'NS', 'SRV', 'SSHFP', 'TLSA', 'CAA', 'TXT']),
            value=dict(type='str', aliases=['content']),
            weight=dict(type='int', default=1),
            zone=dict(type='str', required=True, aliases=['domain']),
        ),
        supports_check_mode=True,
        required_if=[
            ('state', 'present', ['record', 'type', 'value']),
            ('state', 'absent', ['record']),
            ('type', 'SRV', ['proto', 'service']),
            ('type', 'TLSA', ['proto', 'port']),
            ('type', 'CAA', ['flag', 'tag']),
        ],
    )

    if not module.params['api_token'] and not (module.params['account_api_key'] and module.params['account_email']):
        module.fail_json(msg="Either api_token or account_api_key and account_email params are required.")
    if module.params['type'] == 'SRV':
        if not ((module.params['weight'] is not None and module.params['port'] is not None
                 and not (module.params['value'] is None or module.params['value'] == ''))
                or (module.params['weight'] is None and module.params['port'] is None
                    and (module.params['value'] is None or module.params['value'] == ''))):
            module.fail_json(msg="For SRV records the params weight, port and value all need to be defined, or not at all.")

    if module.params['type'] == 'SSHFP':
        if not ((module.params['algorithm'] is not None and module.params['hash_type'] is not None
                 and not (module.params['value'] is None or module.params['value'] == ''))
                or (module.params['algorithm'] is None and module.params['hash_type'] is None
                    and (module.params['value'] is None or module.params['value'] == ''))):
            module.fail_json(msg="For SSHFP records the params algorithm, hash_type and value all need to be defined, or not at all.")

    if module.params['type'] == 'TLSA':
        if not ((module.params['cert_usage'] is not None and module.params['selector'] is not None and module.params['hash_type'] is not None
                 and not (module.params['value'] is None or module.params['value'] == ''))
                or (module.params['cert_usage'] is None and module.params['selector'] is None and module.params['hash_type'] is None
                    and (module.params['value'] is None or module.params['value'] == ''))):
            module.fail_json(msg="For TLSA records the params cert_usage, selector, hash_type and value all need to be defined, or not at all.")

    if module.params['type'] == 'CAA':
        if not ((module.params['flag'] is not None and module.params['tag'] is not None
                 and not (module.params['value'] is None or module.params['value'] == ''))
                or (module.params['flag'] is None and module.params['tag'] is None
                    and (module.params['value'] is None or module.params['value'] == ''))):
            module.fail_json(msg="For CAA records the params flag, tag and value all need to be defined, or not at all.")

    if module.params['type'] == 'DS':
        if not ((module.params['key_tag'] is not None and module.params['algorithm'] is not None and module.params['hash_type'] is not None
                 and not (module.params['value'] is None or module.params['value'] == ''))
                or (module.params['key_tag'] is None and module.params['algorithm'] is None and module.params['hash_type'] is None
                    and (module.params['value'] is None or module.params['value'] == ''))):
            module.fail_json(msg="For DS records the params key_tag, algorithm, hash_type and value all need to be defined, or not at all.")

    changed = False
    cf_api = CloudflareAPI(module)

    # sanity checks
    if cf_api.is_solo and cf_api.state == 'absent':
        module.fail_json(msg="solo=true can only be used with state=present")

    # perform add, delete or update (only the TTL can be updated) of one or
    # more records
    if cf_api.state == 'present':
        # delete all records matching record name + type
        if cf_api.is_solo:
            changed = cf_api.delete_dns_records(solo=cf_api.is_solo)
        result, changed = cf_api.ensure_dns_record()
        if isinstance(result, list):
            module.exit_json(changed=changed, result={'record': result[0]})

        module.exit_json(changed=changed, result={'record': result})
    else:
        # force solo to False, just to be sure
        changed = cf_api.delete_dns_records(solo=False)
        module.exit_json(changed=changed)


if __name__ == '__main__':
    main()
