# -*- coding: utf-8 -*-

# Copyright (c) 2023, Felix Fontein <felix@fontein.de>
# 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 = '''
name: lookup_as_dict
author: Felix Fontein (@felixfontein)
short_description: Look up DNS records as dictionaries
version_added: 2.6.0
requirements:
    - dnspython >= 1.15.0 (maybe older versions also work)
    - ipaddress (on Python 2.7 when using O(server))
description:
    - Look up DNS records and return them as interpreted dictionaries.
options:
    _terms:
        description:
            - Domain name(s) to query.
        type: list
        elements: str
        required: true
    type:
        description:
            - The record type to retrieve.
        type: str
        default: A
        choices:
            - A
            - ALL
            - AAAA
            - CAA
            - CNAME
            - DNAME
            - DNSKEY
            - DS
            - HINFO
            - LOC
            - MX
            - NAPTR
            - NS
            - NSEC
            - NSEC3
            - NSEC3PARAM
            - PTR
            - RP
            - RRSIG
            - SOA
            - SPF
            - SRV
            - SSHFP
            - TLSA
            - TXT
    query_retry:
        description:
            - Number of retries for DNS query timeouts.
        type: int
        default: 3
    query_timeout:
        description:
            - Timeout per DNS query in seconds.
        type: float
        default: 10
    server:
        description:
            - The DNS server(s) to use to look up the result.
            - By default, the system's standard resolver is used.
        type: list
        elements: str
    servfail_retries:
        description:
            - How often to retry on SERVFAIL errors.
        type: int
        default: 0
    nxdomain_handling:
        description:
            - How to handle NXDOMAIN errors. These appear if an unknown domain name is queried.
            - V(empty) (default) returns an empty result for that domain name.
              This means that for the corresponding domain name, nothing is added to RV(_result).
            - V(fail) makes the lookup fail.
        type: str
        choices:
            - empty
            - fail
        default: empty
'''

EXAMPLES = """
- name: Look up A (IPv4) records for example.org as a list of dictionaries
  ansible.builtin.debug:
    msg: "{{ lookup('community.dns.lookup_as_dict', 'example.org.') }}"

- name: Look up AAAA (IPv6) records for example.org as a list of IPv6 addresses
  ansible.builtin.debug:
    msg: "{{ lookup('community.dns.lookup_as_dict', 'example.org.', type='AAAA' ) | map(attribute='address') }}"

- name: Look up TXT records for ansible.com as a list of strings
  ansible.builtin.debug:
    msg: "{{ lookup('community.dns.lookup_as_dict', 'ansible.com.', type='TXT' ) | map(attribute='value') }}"
"""

RETURN = """
_result:
    description:
        - The records of type O(type) for all queried DNS names.
        - If multiple DNS names are queried in O(_terms), the resulting lists have been concatenated.
        - Depending on O(type), different fields are returned.
        - For O(type=TXT) and O(type=SPF), also the concatenated value is returned as RV(_result[].value).
    type: list
    elements: dict
    sample:
        - address: 127.0.0.1
    contains:
        address:
            description:
                - A IPv4 respectively IPv6 address.
            type: str
            returned: if O(type=A) or O(type=AAAA)
        algorithm:
            description:
                - The algorithm ID.
            type: int
            returned: if O(type=DNSKEY) or O(type=DS) or O(type=NSEC3) or O(type=NSEC3PARAM) or O(type=RRSIG) or O(type=SSHFP)
        altitude:
            description:
                - The altitute.
            type: float
            returned: if O(type=LOC)
        cert:
            description:
                - The certificate.
            type: str
            returned: if O(type=TLSA)
        cpu:
            description:
                - The CPU.
            type: str
            returned: if O(type=HINFO)
        digest:
            description:
                - The digest.
            type: str
            returned: if O(type=DS)
        digest_type:
            description:
                - The digest's type.
            type: int
            returned: if O(type=DS)
        exchange:
            description:
                - The exchange server.
            type: str
            returned: if O(type=MX)
        expiration:
            description:
                - The expiration Unix timestamp.
            type: int
            returned: if O(type=RRSIG)
        expire:
            description:
                - Number of seconds after which secondary name servers should stop answering request
                  for this zone if the main name server does not respond.
            type: int
            returned: if O(type=SOA)
        fingerprint:
            description:
                - The fingerprint.
            type: str
            returned: if O(type=SSHFP)
        flags:
            description:
                - Flags.
                - This is actually of type C(string) for O(type=NAPTR).
            type: int
            returned: if O(type=CAA) or O(type=DNSKEY) or O(type=NAPTR) or O(type=NSEC3) or O(type=NSEC3PARAM)
        fp_type:
            description:
                - The fingerprint's type.
            type: int
            returned: if O(type=SSHFP)
        horizontal_precision:
            description:
                - The horizontal precision of the location.
            type: float
            returned: if O(type=LOC)
        inception:
            description:
                - The inception Unix timestamp.
            type: int
            returned: if O(type=RRSIG)
        iterations:
            description:
                - The number of iterations.
            type: int
            returned: if O(type=NSEC3) or O(type=NSEC3PARAM)
        key:
            description:
                - The key.
            type: str
            returned: if O(type=DNSKEY)
        key_tag:
            description:
                - The key's tag.
            type: int
            returned: if O(type=DS) or O(type=RRSIG)
        labels:
            description:
                - The labels.
            type: int
            returned: if O(type=RRSIG)
        latitude:
            description:
                - The location's latitude.
            type: list
            elements: int
            returned: if O(type=LOC)
        longitude:
            description:
                - The location's longitude.
            type: list
            elements: int
            returned: if O(type=LOC)
        mbox:
            description:
                - The mbox.
            type: str
            returned: if O(type=RP)
        minimum:
            description:
                - Used to calculate the TTL for purposes of negative caching.
            type: int
            returned: if O(type=SOA)
        mname:
            description:
                - Primary main name server for this zone.
            type: str
            returned: if O(type=SOA)
        mtype:
            description:
                - The mtype.
            type: int
            returned: if O(type=TLSA)
        next:
            description:
                - The next value.
            type: str
            returned: if O(type=NSEC) or O(type=NSEC3)
        order:
            description:
                - The order value.
            type: int
            returned: if O(type=NAPTR)
        original_ttl:
            description:
                - The original TTL.
            type: int
            returned: if O(type=RRSIG)
        os:
            description:
                - The operating system.
            type: str
            returned: if O(type=HINFO)
        port:
            description:
                - The port.
            type: int
            returned: if O(type=SRV)
        preference:
            description:
                - The preference value for this record.
            type: int
            returned: if O(type=MX) or O(type=NAPTR)
        priority:
            description:
                - The priority value for this record.
            type: int
            returned: if O(type=SRV)
        protocol:
            description:
                - The protocol.
            type: int
            returned: if O(type=DNSKEY)
        refresh:
            description:
                - Number of seconds after which secondary name servers should query the main
                  name server for the SOA record to detect zone changes.
            type: int
            returned: if O(type=SOA)
        regexp:
            description:
                - A regular expression.
            type: str
            returned: if O(type=NAPTR)
        replacement:
            description:
                - The replacement.
            type: str
            returned: if O(type=NAPTR)
        retry:
            description:
                - Number of seconds after which secondary name servers should retry to request
                  the serial number from the main name server if the main name server does not respond.
            type: int
            returned: if O(type=SOA)
        rname:
            description:
                - E-mail address of the administrator responsible for this zone.
            type: str
            returned: if O(type=SOA)
        salt:
            description:
                - The salt.
            type: str
            returned: if O(type=NSEC3) or O(type=NSEC3PARAM)
        selector:
            description:
                - The selector.
            type: int
            returned: if O(type=TLSA)
        serial:
            description:
                - Serial number for this zone.
            type: int
            returned: if O(type=SOA)
        service:
            description:
                - The service.
            type: str
            returned: if O(type=NAPTR)
        signature:
            description:
                - The signature.
            type: str
            returned: if O(type=RRSIG)
        signer:
            description:
                - The signer.
            type: str
            returned: if O(type=RRSIG)
        size:
            description:
                - The size of the location.
            type: float
            returned: if O(type=LOC)
        strings:
            description:
                - List of strings for this record.
                - See RV(_result[].value) for the concatenated result.
            type: list
            elements: str
            returned: if O(type=SPF) or O(type=TXT)
        tag:
            description:
                - The tag.
            type: str
            returned: if O(type=CAA)
        target:
            description:
                - The target.
            type: str
            returned: if O(type=CNAME) or O(type=DNAME) or O(type=NS) or O(type=PTR) or O(type=SRV)
        txt:
            description:
                - The TXT value.
            type: str
            returned: if O(type=RP)
        type_covered:
            description:
                - The type covered.
            type: str
            returned: if O(type=RRSIG)
        usage:
            description:
                - The usage flag.
            type: int
            returned: if O(type=TLSA)
        value:
            description:
                - The value.
                - For O(type=SPF) or O(type=TXT), this is the concatenation of RV(_result[].strings).
            type: str
            returned: if O(type=CAA) or O(type=SPF) or O(type=TXT)
        vertical_precision:
            description:
                - The vertical precision of the location.
            type: float
            returned: if O(type=LOC)
        weight:
            description:
                - The service's weight.
            type: int
            returned: if O(type=SRV)
        windows:
            description:
                - The windows.
            type: str
            returned: if O(type=NSEC) or O(type=NSEC3)
"""

from ansible.errors import AnsibleLookupError
from ansible.plugins.lookup import LookupBase

from ansible_collections.community.dns.plugins.module_utils.ips import (
    is_ip_address,
)

from ansible_collections.community.dns.plugins.module_utils.dnspython_records import (
    NAME_TO_RDTYPE,
    convert_rdata_to_dict,
)

from ansible_collections.community.dns.plugins.module_utils.resolver import (
    SimpleResolver,
)

from ansible_collections.community.dns.plugins.plugin_utils.ips import (
    assert_requirements_present as assert_requirements_present_ipaddress,
)

from ansible_collections.community.dns.plugins.plugin_utils.resolver import (
    assert_requirements_present as assert_requirements_present_dnspython,
    guarded_run,
)

try:
    import dns.resolver
except ImportError:
    # handled by assert_requirements_present_dnspython
    pass


class LookupModule(LookupBase):
    @staticmethod
    def _resolve(resolver, name, rdtype, server_addresses, nxdomain_handling):
        def callback():
            try:
                rrset = resolver.resolve(
                    name,
                    rdtype=rdtype,
                    server_addresses=server_addresses,
                    nxdomain_is_empty=nxdomain_handling == 'empty',
                )
                if not rrset:
                    return []
                return [convert_rdata_to_dict(data) for data in rrset]
            except dns.resolver.NXDOMAIN:
                raise AnsibleLookupError('Got NXDOMAIN when querying {name}'.format(name=name))

        return guarded_run(
            callback,
            error_class=AnsibleLookupError,
            server=name,
        )

    def run(self, terms, variables=None, **kwargs):
        assert_requirements_present_dnspython('community.dns.lookup', 'lookup_as_dict')

        self.set_options(var_options=variables, direct=kwargs)

        resolver = SimpleResolver(
            timeout=self.get_option('query_timeout'),
            timeout_retries=self.get_option('query_retry'),
            servfail_retries=self.get_option('servfail_retries'),
        )

        rdtype = NAME_TO_RDTYPE[self.get_option('type')]

        nxdomain_handling = self.get_option('nxdomain_handling')

        server_addresses = None
        if self.get_option('server'):
            server_addresses = []
            assert_requirements_present_ipaddress('community.dns.lookup', 'lookup_as_dict')
            for server in self.get_option('server'):
                if is_ip_address(server):
                    server_addresses.append(server)
                    continue
                else:
                    server_addresses.extend(guarded_run(
                        lambda: resolver.resolve_addresses(server),
                        error_class=AnsibleLookupError,
                        server=server,
                    ))

        result = []
        for name in terms:
            result.extend(self._resolve(resolver, name, rdtype, server_addresses, nxdomain_handling))
        return result
