"""
The tool to check the availability or syntax of domain, IP or URL.

::


    ██████╗ ██╗   ██╗███████╗██╗   ██╗███╗   ██╗ ██████╗███████╗██████╗ ██╗     ███████╗
    ██╔══██╗╚██╗ ██╔╝██╔════╝██║   ██║████╗  ██║██╔════╝██╔════╝██╔══██╗██║     ██╔════╝
    ██████╔╝ ╚████╔╝ █████╗  ██║   ██║██╔██╗ ██║██║     █████╗  ██████╔╝██║     █████╗
    ██╔═══╝   ╚██╔╝  ██╔══╝  ██║   ██║██║╚██╗██║██║     ██╔══╝  ██╔══██╗██║     ██╔══╝
    ██║        ██║   ██║     ╚██████╔╝██║ ╚████║╚██████╗███████╗██████╔╝███████╗███████╗
    ╚═╝        ╚═╝   ╚═╝      ╚═════╝ ╚═╝  ╚═══╝ ╚═════╝╚══════╝╚═════╝ ╚══════╝╚══════╝

Provides a way to get or guess the nameserver to use.

Author:
    Nissar Chababy, @funilrys, contactTATAfunilrysTODTODcom

Special thanks:
    https://pyfunceble.github.io/#/special-thanks

Contributors:
    https://pyfunceble.github.io/#/contributors

Project link:
    https://github.com/funilrys/PyFunceble

Project documentation:
    https://docs.pyfunceble.com

Project homepage:
    https://pyfunceble.github.io/

License:
::


    Copyright 2017, 2018, 2019, 2020, 2022, 2023, 2024, 2025 Nissar Chababy

    Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.
    You may obtain a copy of the License at

        https://www.apache.org/licenses/LICENSE-2.0

    Unless required by applicable law or agreed to in writing, software
    distributed under the License is distributed on an "AS IS" BASIS,
    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    See the License for the specific language governing permissions and
    limitations under the License.
"""

import ipaddress
from typing import List, Optional, Tuple

import dns.exception
import dns.resolver

import PyFunceble.facility
import PyFunceble.storage
from PyFunceble.checker.syntax.domain import DomainSyntaxChecker
from PyFunceble.checker.syntax.url import URLSyntaxChecker
from PyFunceble.converter.url2netloc import Url2Netloc


class Nameservers:
    """
    Provides an interface to get the right nameserver to communicate with.
    """

    DEFAULT_NAMESERVERS: List[str] = [
        "9.9.9.10",
        "149.112.112.10",
        "2620:fe::10",
        "2620:fe::fe:10",
    ]

    nameservers: Optional[List[str]] = None
    nameserver_ports: Optional[dict] = None

    protocol: Optional[str] = None

    domain_syntax_checker: Optional[DomainSyntaxChecker] = None
    url_syntax_checker: Optional[URLSyntaxChecker] = None
    url2netloc: Optional[Url2Netloc] = None

    def __init__(
        self, nameserver: Optional[List[str]] = None, protocol: str = "TCP"
    ) -> None:
        self.protocol = protocol

        self.domain_syntax_checker = DomainSyntaxChecker()
        self.url_syntax_checker = URLSyntaxChecker()
        self.url2netloc = Url2Netloc()

        if nameserver is not None:
            self.set_nameservers(nameserver)

    @staticmethod
    def split_nameserver_from_port(
        nameserver: str,
        *,
        default_port: int = 53,
    ) -> Tuple[str, int]:
        """
        Splits the nameserver from its port.re

        :param nameserver:
            The nameserver to work with.
        :param default_port:
            The default port to apply, if none is found.
        """

        if ":" in nameserver:
            if nameserver.count(":") > 1:
                if "]" not in nameserver:
                    return nameserver, default_port

                nameserver, port = nameserver.split("]:", 1)

                if port.isdigit():
                    return nameserver[1:], int(port)
                return nameserver[1:], default_port

            splitted = nameserver.rsplit(":")

            if splitted[-1].isdigit():
                return ":".join(splitted[:-1]), int(splitted[-1])

            return ":".join(splitted[:-1]), default_port
        return nameserver, default_port

    @classmethod
    def get_ip_from_nameserver(cls, nameserver: str) -> List[str]:
        """
        Given a nameserver, this method resolve it in order to get the
        IP to contact.

        :param nameserver:
            The name to resolve.
        """

        PyFunceble.facility.Logger.info(
            "Started to get ip from nameserver (%r)", nameserver
        )

        result = []

        try:
            _ = ipaddress.ip_address(nameserver)
            result.append(nameserver)
        except ValueError:
            resolver = dns.resolver.get_default_resolver()

            for record_type in ["A", "AAAA"]:
                try:
                    result.extend(
                        [x.address for x in resolver.resolve(nameserver, record_type)]
                    )
                except dns.exception.DNSException:
                    pass

        PyFunceble.facility.Logger.debug(
            "IP from nameserver (%r):\n%r", nameserver, result
        )

        PyFunceble.facility.Logger.info(
            "Finished to get ip from nameserver (%r)", nameserver
        )

        return result

    def set_nameservers(self, value: List[str]) -> "Nameservers":
        """
        Sets the nameserver to use.

        Side Effect:
            Also updates the :code:`nameserver_ports` variable.

        :raise TypeError:
            When the given :code:`value` is not a :py:class:`list`.
        :raise ValueError:
            When the given :code:`value` is emtpy.
        """

        if not isinstance(value, list):
            raise TypeError(f"<value> should be {list}, {type(value)} given.")

        if not value:
            raise ValueError("<value> should not be empty.")

        self.nameserver_ports = {}
        self.nameservers = []

        for nameserver in value:
            if isinstance(self.protocol, str) and self.protocol.lower() == "https":
                if not nameserver.startswith("https://"):
                    netloc = self.url2netloc.set_data_to_convert(
                        nameserver
                    ).get_converted()

                    if "/" in nameserver:
                        path = nameserver[nameserver.find("/") :]
                    else:
                        path = ""

                    self.nameservers.append(
                        "https://"
                        # pylint: disable=line-too-long
                        f"{netloc}{path}"
                    )
                else:
                    self.nameservers.append(nameserver)

                # 443 is because it's more likely to be for DOH.
                self.nameserver_ports.update({self.nameservers[-1]: 443})
                continue

            server, port = self.split_nameserver_from_port(nameserver)

            for dns_ip in self.get_ip_from_nameserver(server):
                self.nameservers.append(dns_ip)
                self.nameserver_ports.update({dns_ip: port})

        return self

    def get_nameservers(self) -> Optional[List[str]]:
        """
        Provides the currently set nameservers.
        """

        return self.nameservers

    def get_nameserver_ports(self) -> Optional[dict]:
        """
        Provides the currently set nameserver_ports.
        """

        return self.nameserver_ports

    def guess_and_set_nameservers(self) -> "Nameservers":
        """
        Try to guess and set the nameserver to use.
        """

        if PyFunceble.facility.ConfigLoader.is_already_loaded():
            if PyFunceble.storage.CONFIGURATION.dns.server:
                if isinstance(PyFunceble.storage.CONFIGURATION.dns.server, list):
                    self.set_nameservers(PyFunceble.storage.CONFIGURATION.dns.server)
                else:
                    self.set_nameservers([PyFunceble.storage.CONFIGURATION.dns.server])
            else:  # pragma: no cover
                try:
                    ## Well, I don't like playing with the default resolver.
                    self.set_nameservers(
                        dns.resolver.get_default_resolver().nameservers
                    )
                except dns.resolver.NoResolverConfiguration:
                    self.set_nameservers(self.DEFAULT_NAMESERVERS)
        else:  # pragma: no cover
            try:
                ## Well, I don't like playing with the default resolver.
                self.set_nameservers(dns.resolver.get_default_resolver().nameservers)
            except dns.resolver.NoResolverConfiguration:
                self.set_nameservers(self.DEFAULT_NAMESERVERS)

        return self

    def guess_all_settings(
        self,
    ) -> "Nameservers":  # pragma: no cover ## Method themselves are more important
        """
        Try to guess all settings.
        """

        to_ignore = ["guess_all_settings"]

        for method in dir(self):
            if method in to_ignore or not method.startswith("guess_"):
                continue

            getattr(self, method)()

        return self
