"""
Compendium of generic DNS utilities.

.. note::

    Some functions in the ``dnsutil`` execution module depend on ``dig``.
"""

import logging
import socket
import time

import salt.utils.files
import salt.utils.path
import salt.utils.stringutils

log = logging.getLogger(__name__)


def __virtual__():
    """
    Generic, should work on any platform (including Windows). Functionality
    which requires dependencies outside of Python do not belong in this module.
    """
    return True


def parse_hosts(hostsfile="/etc/hosts", hosts=None):
    """
    Parse /etc/hosts file.

    CLI Example:

    .. code-block:: bash

        salt '*' dnsutil.parse_hosts
    """
    if not hosts:
        try:
            with salt.utils.files.fopen(hostsfile, "r") as fp_:
                hosts = salt.utils.stringutils.to_unicode(fp_.read())
        except Exception:  # pylint: disable=broad-except
            return "Error: hosts data was not found"

    hostsdict = {}
    for line in hosts.splitlines():
        if not line:
            continue
        if line.startswith("#"):
            continue
        comps = line.split()
        ip = comps[0]
        aliases = comps[1:]
        hostsdict.setdefault(ip, []).extend(aliases)

    return hostsdict


def hosts_append(hostsfile="/etc/hosts", ip_addr=None, entries=None):
    """
    Append a single line to the /etc/hosts file.

    CLI Example:

    .. code-block:: bash

        salt '*' dnsutil.hosts_append /etc/hosts 127.0.0.1 ad1.yuk.co,ad2.yuk.co
    """
    host_list = entries.split(",")
    hosts = parse_hosts(hostsfile=hostsfile)
    if ip_addr in hosts:
        for host in host_list:
            if host in hosts[ip_addr]:
                host_list.remove(host)

    if not host_list:
        return "No additional hosts were added to {}".format(hostsfile)

    append_line = "\n{} {}".format(ip_addr, " ".join(host_list))
    with salt.utils.files.fopen(hostsfile, "a") as fp_:
        fp_.write(salt.utils.stringutils.to_str(append_line))

    return "The following line was added to {}:{}".format(hostsfile, append_line)


def hosts_remove(hostsfile="/etc/hosts", entries=None):
    """
    Remove a host from the /etc/hosts file. If doing so will leave a line
    containing only an IP address, then the line will be deleted. This function
    will leave comments and blank lines intact.

    CLI Examples:

    .. code-block:: bash

        salt '*' dnsutil.hosts_remove /etc/hosts ad1.yuk.co
        salt '*' dnsutil.hosts_remove /etc/hosts ad2.yuk.co,ad1.yuk.co
    """
    with salt.utils.files.fopen(hostsfile, "r") as fp_:
        hosts = salt.utils.stringutils.to_unicode(fp_.read())

    host_list = entries.split(",")
    with salt.utils.files.fopen(hostsfile, "w") as out_file:
        for line in hosts.splitlines():
            if not line or line.strip().startswith("#"):
                out_file.write(salt.utils.stringutils.to_str("{}\n".format(line)))
                continue
            comps = line.split()
            for host in host_list:
                if host in comps[1:]:
                    comps.remove(host)
            if len(comps) > 1:
                out_file.write(salt.utils.stringutils.to_str(" ".join(comps)))
                out_file.write(salt.utils.stringutils.to_str("\n"))


def parse_zone(zonefile=None, zone=None):
    """
    Parses a zone file. Can be passed raw zone data on the API level.

    CLI Example:

    .. code-block:: bash

        salt ns1 dnsutil.parse_zone /var/lib/named/example.com.zone
    """
    if zonefile:
        try:
            with salt.utils.files.fopen(zonefile, "r") as fp_:
                zone = salt.utils.stringutils.to_unicode(fp_.read())
        except Exception:  # pylint: disable=broad-except
            pass

    if not zone:
        return "Error: Zone data was not found"

    zonedict = {}
    mode = "single"
    for line in zone.splitlines():
        comps = line.split(";")
        line = comps[0].strip()
        if not line:
            continue
        comps = line.split()
        if line.startswith("$"):
            zonedict[comps[0].replace("$", "")] = comps[1]
            continue
        if "(" in line and ")" not in line:
            mode = "multi"
            multi = ""
        if mode == "multi":
            multi += " {}".format(line)
            if ")" in line:
                mode = "single"
                line = multi.replace("(", "").replace(")", "")
            else:
                continue
        if "ORIGIN" in zonedict:
            comps = line.replace("@", zonedict["ORIGIN"]).split()
        else:
            comps = line.split()
        if "SOA" in line:
            if comps[1] != "IN":
                comps.pop(1)
            zonedict["ORIGIN"] = comps[0]
            zonedict["NETWORK"] = comps[1]
            zonedict["SOURCE"] = comps[3]
            zonedict["CONTACT"] = comps[4].replace(".", "@", 1)
            zonedict["SERIAL"] = comps[5]
            zonedict["REFRESH"] = _to_seconds(comps[6])
            zonedict["RETRY"] = _to_seconds(comps[7])
            zonedict["EXPIRE"] = _to_seconds(comps[8])
            zonedict["MINTTL"] = _to_seconds(comps[9])
            continue
        if comps[0] == "IN":
            comps.insert(0, zonedict["ORIGIN"])
        if not comps[0].endswith(".") and "NS" not in line:
            comps[0] = "{}.{}".format(comps[0], zonedict["ORIGIN"])
        if comps[2] == "NS":
            zonedict.setdefault("NS", []).append(comps[3])
        elif comps[2] == "MX":
            if "MX" not in zonedict:
                zonedict.setdefault("MX", []).append(
                    {"priority": comps[3], "host": comps[4]}
                )
        elif comps[3] in ("A", "AAAA"):
            zonedict.setdefault(comps[3], {})[comps[0]] = {
                "TARGET": comps[4],
                "TTL": comps[1],
            }
        else:
            zonedict.setdefault(comps[2], {})[comps[0]] = comps[3]
    return zonedict


def _to_seconds(timestr):
    """
    Converts a time value to seconds.

    As per RFC1035 (page 45), max time is 1 week, so anything longer (or
    unreadable) will be set to one week (604800 seconds).
    """
    timestr = timestr.upper()
    if "H" in timestr:
        seconds = int(timestr.replace("H", "")) * 3600
    elif "D" in timestr:
        seconds = int(timestr.replace("D", "")) * 86400
    elif "W" in timestr:
        seconds = 604800
    else:
        try:
            seconds = int(timestr)
        except ValueError:
            seconds = 604800
    if seconds > 604800:
        seconds = 604800
    return seconds


def _has_dig():
    """
    The dig-specific functions have been moved into their own module, but
    because they are also DNS utilities, a compatibility layer exists. This
    function helps add that layer.
    """
    return salt.utils.path.which("dig") is not None


def check_ip(ip_addr):
    """
    Check that string ip_addr is a valid IP

    CLI Example:

    .. code-block:: bash

        salt ns1 dnsutil.check_ip 127.0.0.1
    """
    if _has_dig():
        return __salt__["dig.check_ip"](ip_addr)

    return "This function requires dig, which is not currently available"


def A(host, nameserver=None):
    """
    Return the A record(s) for ``host``.

    Always returns a list.

    CLI Example:

    .. code-block:: bash

        salt ns1 dnsutil.A www.google.com
    """
    if _has_dig():
        return __salt__["dig.A"](host, nameserver)
    elif nameserver is None:
        # fall back to the socket interface, if we don't care who resolves
        try:
            addresses = [
                sock[4][0]
                for sock in socket.getaddrinfo(
                    host, None, socket.AF_INET, 0, socket.SOCK_RAW
                )
            ]
            return addresses
        except socket.gaierror:
            return "Unable to resolve {}".format(host)

    return "This function requires dig, which is not currently available"


def AAAA(host, nameserver=None):
    """
    Return the AAAA record(s) for ``host``.

    Always returns a list.

    .. versionadded:: 2014.7.5

    CLI Example:

    .. code-block:: bash

        salt ns1 dnsutil.AAAA www.google.com
    """
    if _has_dig():
        return __salt__["dig.AAAA"](host, nameserver)
    elif nameserver is None:
        # fall back to the socket interface, if we don't care who resolves
        try:
            addresses = [
                sock[4][0]
                for sock in socket.getaddrinfo(
                    host, None, socket.AF_INET6, 0, socket.SOCK_RAW
                )
            ]
            return addresses
        except socket.gaierror:
            return "Unable to resolve {}".format(host)

    return "This function requires dig, which is not currently available"


def NS(domain, resolve=True, nameserver=None):
    """
    Return a list of IPs of the nameservers for ``domain``

    If 'resolve' is False, don't resolve names.

    CLI Example:

    .. code-block:: bash

        salt ns1 dnsutil.NS google.com

    """
    if _has_dig():
        return __salt__["dig.NS"](domain, resolve, nameserver)

    return "This function requires dig, which is not currently available"


def SPF(domain, record="SPF", nameserver=None):
    """
    Return the allowed IPv4 ranges in the SPF record for ``domain``.

    If record is ``SPF`` and the SPF record is empty, the TXT record will be
    searched automatically. If you know the domain uses TXT and not SPF,
    specifying that will save a lookup.

    CLI Example:

    .. code-block:: bash

        salt ns1 dnsutil.SPF google.com
    """
    if _has_dig():
        return __salt__["dig.SPF"](domain, record, nameserver)

    return "This function requires dig, which is not currently available"


def MX(domain, resolve=False, nameserver=None):
    """
    Return a list of lists for the MX of ``domain``.

    If the 'resolve' argument is True, resolve IPs for the servers.

    It's limited to one IP, because although in practice it's very rarely a
    round robin, it is an acceptable configuration and pulling just one IP lets
    the data be similar to the non-resolved version. If you think an MX has
    multiple IPs, don't use the resolver here, resolve them in a separate step.

    CLI Example:

    .. code-block:: bash

        salt ns1 dnsutil.MX google.com
    """
    if _has_dig():
        return __salt__["dig.MX"](domain, resolve, nameserver)

    return "This function requires dig, which is not currently available"


def serial(zone="", update=False):
    """
    Return, store and update a dns serial for your zone files.

    zone: a keyword for a specific zone

    update: store an updated version of the serial in a grain

    If ``update`` is False, the function will retrieve an existing serial or
    return the current date if no serial is stored. Nothing will be stored

    If ``update`` is True, the function will set the serial to the current date
    if none exist or if the existing serial is for a previous date. If a serial
    for greater than the current date is already stored, the function will
    increment it.

    This module stores the serial in a grain, you can explicitly set the
    stored value as a grain named ``dnsserial_<zone_name>``.

    CLI Example:

    .. code-block:: bash

        salt ns1 dnsutil.serial example.com
    """
    grains = {}
    key = "dnsserial"
    if zone:
        key += "_{}".format(zone)
    stored = __salt__["grains.get"](key=key)
    present = time.strftime("%Y%m%d01")
    if not update:
        return stored or present
    if stored and stored >= present:
        current = str(int(stored) + 1)
    else:
        current = present
    __salt__["grains.setval"](key=key, val=current)
    return current
