#!/usr/bin/env python3

# pylint:disable=line-too-long
"""
The tool to check the availability or syntax of domains, IPv4 or URL.

::

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

This submodule will provide the auto-continue logic.

Author:
    Nissar Chababy, @funilrys, contactTATAfunilrysTODTODcom

Special thanks:
    https://pyfunceble.readthedocs.io/en/master/special-thanks.html

Contributors:
    http://pyfunceble.readthedocs.io/en/master/special-thanks.html

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

Project documentation:
    https://pyfunceble.readthedocs.io/en/master/

Project homepage:
    https://funilrys.github.io/PyFunceble/

License:
::

    MIT License

    Copyright (c) 2017-2019 Nissar Chababy

    Permission is hereby granted, free of charge, to any person obtaining a copy
    of this software and associated documentation files (the "Software"), to deal
    in the Software without restriction, including without limitation the rights
    to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    copies of the Software, and to permit persons to whom the Software is
    furnished to do so, subject to the following conditions:

    The above copyright notice and this permission notice shall be included in all
    copies or substantial portions of the Software.

    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
    SOFTWARE.
"""
# pylint: enable=line-too-long
# pylint: disable=bad-continuation
import PyFunceble
from PyFunceble.helpers import Dict, File

class AutoContinue:
    """
    Provide the auto-continue subsystem.
    """

    def __init__(self):
        if (
            PyFunceble.CONFIGURATION["auto_continue"]
            and not PyFunceble.CONFIGURATION["no_files"]
        ):
            # * The auto_continue subsystem is activated.
            # and
            # * We are authorized to generate files.

            # We set the log file location.
            self.autocontinue_log_file = (
                PyFunceble.OUTPUT_DIRECTORY
                + PyFunceble.OUTPUTS["parent_directory"]
                + PyFunceble.OUTPUTS["logs"]["filenames"]["auto_continue"]
            )

            if PyFunceble.path.isfile(self.autocontinue_log_file):
                # The log file already exist.

                # We get its content and save it inside backup_content.
                self.backup_content = Dict().from_json(
                    File(self.autocontinue_log_file).read()
                )
            else:
                # The log file does not exist.

                # We initiate the backup content.
                self.backup_content = {}
                # And we save our empty backup_content to the log file.
                File(self.autocontinue_log_file).write(str(self.backup_content))

    def backup(self):
        """
        Backup the current execution state.
        """

        if PyFunceble.CONFIGURATION["auto_continue"]:
            # The auto_continue subsystem is activated.

            # We initiate the location where we are going to save the data to backup.
            data_to_backup = {}
            # We get the current counter states.
            configuration_counter = PyFunceble.INTERN["counter"]["number"]

            # We initiate the data we have to backup.
            data_to_backup[PyFunceble.INTERN["file_to_test"]] = {
                # We backup the number of tested.
                "tested": configuration_counter["tested"],
                # We backup the number of up.
                "up": configuration_counter["up"],
                # We backup the number of down.
                "down": configuration_counter["down"],
                # We backup the number of invalid.
                "invalid": configuration_counter["invalid"],
            }

            # We initiate the final data we have to save.
            # We initiate this variable instead of updating backup_content because
            # we do not want to touch the backup_content.
            to_save = {}

            # We add the backup_content into to_save.
            to_save.update(self.backup_content)
            # And we overwrite with the newly data to backup.
            to_save.update(data_to_backup)

            # Finaly, we save our informations into the log file.
            Dict(to_save).to_json(self.autocontinue_log_file)

    def restore(self):
        """
        Restore data from the given path.
        """

        if PyFunceble.CONFIGURATION["auto_continue"] and self.backup_content:
            # The auto_continue subsystem is activated and the backup_content
            # is not empty.

            # We get the file we have to restore.
            file_to_restore = PyFunceble.INTERN["file_to_test"]

            if file_to_restore in self.backup_content:
                # The file we are working with is already into the backup content.

                # We initiate the different status to set.
                to_initiate = ["up", "down", "invalid", "tested"]

                # Because at some time it was not the current status, we have to map
                # the new with the old. This way, if someone is running the latest
                # version but with old data, we still continue like nothing happend.
                alternatives = {
                    "up": "number_of_up",
                    "down": "number_of_down",
                    "invalid": "number_of_invalid",
                    "tested": "number_of_tested",
                }

                for string in to_initiate:
                    # We loop over the status we have to initiate.

                    try:
                        # We try to update the counters by using the currently read status.
                        PyFunceble.INTERN["counter"]["number"].update(
                            {string: self.backup_content[file_to_restore][string]}
                        )
                    except KeyError:
                        # But if the status is not present, we try with the older index
                        # we mapped previously.
                        PyFunceble.INTERN["counter"]["number"].update(
                            {
                                string: self.backup_content[file_to_restore][
                                    alternatives[string]
                                ]
                            }
                        )


#!/usr/bin/env python3

# pylint:disable=line-too-long
"""
The tool to check the availability or syntax of domains, IPv4 or URL.

::

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

This submodule will provide adblock decoding interface.

Author:
    Nissar Chababy, @funilrys, contactTATAfunilrysTODTODcom

Special thanks:
    https://pyfunceble.readthedocs.io/en/master/special-thanks.html

Contributors:
    http://pyfunceble.readthedocs.io/en/master/special-thanks.html

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

Project documentation:
    https://pyfunceble.readthedocs.io/en/master/

Project homepage:
    https://funilrys.github.io/PyFunceble/

License:
::

    MIT License

    Copyright (c) 2017-2019 Nissar Chababy

    Permission is hereby granted, free of charge, to any person obtaining a copy
    of this software and associated documentation files (the "Software"), to deal
    in the Software without restriction, including without limitation the rights
    to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    copies of the Software, and to permit persons to whom the Software is
    furnished to do so, subject to the following conditions:

    The above copyright notice and this permission notice shall be included in all
    copies or substantial portions of the Software.

    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
    SOFTWARE.
"""
# pylint: enable=line-too-long
# pylint: disable=bad-continuation

from PyFunceble.check import Check
from PyFunceble.helpers import List, Regex

class AdBlock:  # pylint: disable=too-few-public-methods
    """
    Provide the adblock decoding logic.

    :param list_from_file: The file in list format.
    :type list_from_file: list
    """

    def __init__(self, list_from_file, aggressive=False):
        self.to_format = self._remove_ignored(list_from_file)
        self.aggressive = aggressive

        # We set the options separator.
        self.options_separator = "$"

        # We set the separator of options
        self.option_separator = ","

        # We create an instance of the checker.
        self.checker = Check()

    def _remove_ignored(self, list_from_file):
        """
        Removed the ignored element from the given list.

        :param list_from_file:
            The list which represent the file we are decoding.
        :type list_from_list: list

        :return: The filtered list.
        :rtype: list
        """

        return [x for x in list_from_file if not self._is_to_ignore(x)]

    @classmethod
    def _is_to_ignore(cls, line):
        """
        Check if we have to ignore the given line.

        :param line: The line from the file.
        :type line: str
        """

        # We set the list of regex to match to be
        # considered as ignored.
        to_ignore = [r"(^!|^@@|^\/|^\[|^\.|^-|^_|^\?|^&)"]  # , r"(\$|,)(image)"]

        for element in to_ignore:
            # We loop through the list of regex.

            if Regex(line, element, return_data=False).match():
                # The currently read line match the currently read
                # regex.

                # We return true, it has to be ignored.
                return True

        # Wer return False, it does not has to be ignored.
        return False

    def _handle_options(self, options):
        """
        Handle the data from the options.

        :param options: The list of options from the rule.
        :type options: list

        :return: The list of domains to return globally.
        :rtype: list
        """

        # We initiate a variable which will save our result
        result = []

        # We initiate the regex which will be used to extract the domain listed
        # under the option domain=
        regex_domain_option = r"domain=(.*)"

        for option in options:
            # We loop through the list of option.
            try:
                # We try to extract the list of domains from the currently read
                # option.
                domains = Regex(
                    option, regex_domain_option, return_data=True, rematch=True, group=0
                ).match()[-1]

                if domains:
                    # We could extract something.

                    if self.aggressive:  # pragma: no cover
                        result.extend(
                            [
                                x
                                for x in domains.split("|")
                                if x and not x.startswith("~")
                            ]
                        )
                    else:
                        # We return True.
                        return True
            except TypeError:
                pass

        # We return the result.
        return result

    def _extract_base(self, element):
        """
        Extract the base of the given element.

        .. example:
            given "hello/world?world=beautiful" return "hello"

        :param element: The element we are working with.
        :type element: str|list
        """

        if isinstance(element, list):
            # The given element is a list.

            # We get the base of each element of the list.
            return [self._extract_base(x) for x in element]

        # We get the base if it is an URL.
        base = self.checker.is_url_valid(url=element, return_base=True)

        if base:
            # It is an URL.

            # We return the extracted base.
            return base

        if "/" in element:
            # / is in the given element.

            # We return the first element before the
            # first /
            return element.split("/")[0]

        # / is not in the given element.

        # We return the given element.
        return element

    def decode(self):
        """
        Decode/extract the domains to test from the adblock formated file.

        :return: The list of domains to test.
        :rtype: list
        """

        # We initiate a variable which will save what we are going to return.
        result = []

        # We initiate the first regex we are going to use to get
        # the element to format.
        regex = r"^(?:.*\|\|)([^\/\$\^]{1,}).*$"

        # We initiate the third regex we are going to use to get
        # the element to format.
        regex_v3 = (
            r"(?:#+(?:[a-z]+?)?\[[a-z]+(?:\^|\*)\=(?:\'|\"))(.*\..*)(?:(?:\'|\")\])"
        )

        # We initiate the fourth regex we are going to use to get
        # the element to format.
        regex_v4 = r"^\|(.*\..*)\|$"

        for line in self.to_format:
            # We loop through the different line.

            rematch = rematch_v3 = rematch_v4 = None

            # We extract the different group from our first regex.
            rematch = Regex(
                line, regex, return_data=True, rematch=True, group=0
            ).match()

            # We extract the different group from our fourth regex.
            #
            # Note: We execute the following in second because it is more
            # specific that others.
            rematch_v4 = Regex(
                line, regex_v4, return_data=True, rematch=True, group=0
            ).match()

            # We extract the different group from our third regex.
            rematch_v3 = Regex(
                line, regex_v3, return_data=True, rematch=True, group=0
            ).match()

            if rematch:
                # The first extraction was successfull.

                if self.options_separator in line:
                    options = line.split(self.options_separator)[-1].split(
                        self.option_separator
                    )

                    if (
                        not options[-1]
                        or "third-party" in options
                        or "script" in options
                        or "popup" in options
                        or "xmlhttprequest" in options
                    ):
                        # We extend the result with the extracted elements.
                        result.extend(self._extract_base(rematch))

                    extra = self._handle_options(options)

                    if extra and isinstance(extra, list):  # pragma: no cover
                        extra.extend(self._extract_base(rematch))
                        result.extend(self._extract_base(extra))
                    elif extra:
                        result.extend(self._extract_base(rematch))

                else:
                    # We extend the result with the extracted elements.
                    result.extend(self._extract_base(rematch))

            if rematch_v4:
                # The fourth extraction was successfull.

                # We extend the formatted elements from the extracted elements.
                result.extend(List(self._format_decoded(rematch_v4)).format())

            if rematch_v3:
                # The second extraction was successfull.

                # We extend the formatted elements from the extracted elements.
                result.extend(List(self._format_decoded(rematch_v3)).format())

        # We return the result.
        return List(result).format()

    def _format_decoded(self, to_format, result=None):  # pragma: no cover
        """
        Format the exctracted adblock line before passing it to the system.

        :param to_format: The extracted line from the file.
        :type to_format: str

        :param result: A list of the result of this method.
        :type result: list

        :return: The list of domains or IP to test.
        :rtype: list
        """

        if not result:
            # The result is not given.

            # We set the result as an empty list.
            result = []

        for data in List(to_format).format():
            # We loop through the different lines to format.

            if data:
                # The currently read line is not empty.

                if "^" in data:
                    # There is an accent in the currently read line.

                    # We recall this method but with the current result state
                    # and splited data.
                    return self._format_decoded(data.split("^"), result)

                if "#" in data:
                    # There is a dash in the currently read line.

                    # We recall this method but with the current result state
                    # and splited data.
                    return self._format_decoded(data.split("#"), result)

                if "," in data:
                    # There is a comma in the currently read line.

                    # We recall this method but with the current result state
                    # and splited data.
                    return self._format_decoded(data.split(","), result)

                if "!" in data:
                    # There is an exclamation mark in the currently read line.

                    # We recall this method but with the current result state
                    # and splited data.
                    return self._format_decoded(data.split("!"), result)

                if "|" in data:
                    # There is a vertival bar in the currently read line.

                    # We recall this method but with the current result state
                    # and splited data.
                    return self._format_decoded(data.split("|"), result)

                if data:
                    # The currently read line is not empty.

                    data = self._extract_base(data)

                    if data and (
                        self.checker.is_domain_valid(data)
                        or self.checker.is_ip_valid(data)
                    ):
                        # The extraced base is not empty.
                        # and
                        # * The currently read line is a valid domain.
                        # or
                        # * The currently read line is a valid IP.

                        # We append the currently read line to the result.
                        result.append(data)
                    elif data:
                        # * The currently read line is not a valid domain.
                        # or
                        # * The currently read line is not a valid IP.

                        # We try to get the url base.
                        url_base = self.checker.is_url_valid(data, return_base=True)

                        if url_base:
                            # The url_base is not empty or equal to False or None.

                            # We append the url base to the result.
                            result.append(url_base)

        # We return the result element.
        return result


#!/usr/bin/env python3

# pylint:disable=line-too-long
"""
The tool to check the availability or syntax of domains, IPv4 or URL.

::

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

This submodule will provide HTTP Code extraction logic and interface.

Author:
    Nissar Chababy, @funilrys, contactTATAfunilrysTODTODcom

Special thanks:
    https://pyfunceble.readthedocs.io/en/master/special-thanks.html

Contributors:
    http://pyfunceble.readthedocs.io/en/master/special-thanks.html

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

Project documentation:
    https://pyfunceble.readthedocs.io/en/master/

Project homepage:
    https://funilrys.github.io/PyFunceble/

License:
::

    MIT License

    Copyright (c) 2017-2019 Nissar Chababy

    Permission is hereby granted, free of charge, to any person obtaining a copy
    of this software and associated documentation files (the "Software"), to deal
    in the Software without restriction, including without limitation the rights
    to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    copies of the Software, and to permit persons to whom the Software is
    furnished to do so, subject to the following conditions:

    The above copyright notice and this permission notice shall be included in all
    copies or substantial portions of the Software.

    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
    SOFTWARE.
"""
# pylint: enable=line-too-long
# pylint: disable=bad-continuation

import urllib3.exceptions as urllib3_exceptions
from urllib3 import disable_warnings

import PyFunceble

class HTTPCode:  # pylint: disable=too-few-public-methods
    """
    Get and return the HTTP code status of a given domain.
    """

    def __init__(self):  # pragma: no cover
        if PyFunceble.INTERN["to_test_type"] == "url":
            # We should work with full URL which actualy means that we have to get the
            # http status code from the URL we are currently testing.

            # We disable the urllib warning.
            disable_warnings(urllib3_exceptions.InsecureRequestWarning)

            # We initiate the element we have to get.
            self.to_get = PyFunceble.INTERN["to_test"]
        elif PyFunceble.INTERN["to_test_type"] == "domain":
            # We are working with domain.

            # We construct the element we have to get.
            # Note: As we may work with IP, we explicitly set the port we are
            # working with.
            self.to_get = "http://%s:80" % PyFunceble.INTERN["to_test"]
        else:
            raise Exception("Unknow type of test.")

        if PyFunceble.CONFIGURATION["user_agent"]:
            # The user-agent is given.

            # We append the user agent to the header we are going to parse with
            # the request.
            self.headers = {"User-Agent": PyFunceble.CONFIGURATION["user_agent"]}
        else:
            # The user-agent is not given or is empty.

            # We return an empty header.
            self.headers = {}

    def _access(self):  # pragma: no cover
        """
        Get the HTTP code status.

        :return: The matched HTTP status code.
        :rtype: int|None
        """

        try:
            # We try to get the HTTP status code.

            if PyFunceble.INTERN["to_test_type"] == "url":
                # We are globally testing a URL.

                # We get the head of the URL.
                req = PyFunceble.requests.head(
                    self.to_get,
                    timeout=PyFunceble.CONFIGURATION["seconds_before_http_timeout"],
                    headers=self.headers,
                    verify=PyFunceble.CONFIGURATION["verify_ssl_certificate"],
                )
            else:
                # We are not globally testing a URL.

                # We get the head of the constructed URL.
                req = PyFunceble.requests.head(
                    self.to_get,
                    timeout=PyFunceble.CONFIGURATION["seconds_before_http_timeout"],
                    headers=self.headers,
                )

            # And we try to get the status code.
            return req.status_code

        except (
            PyFunceble.requests.exceptions.InvalidURL,
            PyFunceble.socket.timeout,
            PyFunceble.requests.exceptions.Timeout,
            PyFunceble.requests.ConnectionError,
            urllib3_exceptions.InvalidHeader,
            UnicodeDecodeError,  # The probability that this happend in production is minimal.
        ):
            # If one of the listed exception is matched, that means that something
            # went wrong and we were unable to extract the status code.

            # We return None.
            return None

    def get(self):
        """
        Return the HTTP code status.

        :return: The matched and formatted status code.
        :rtype: str|int|None
        """
        if PyFunceble.HTTP_CODE["active"]:
            # The http status code extraction is activated.

            # We get the http status code.
            http_code = self._access()

            # We initiate a variable which will save the list of allowed
            # http status code.
            list_of_valid_http_code = []

            for codes in [
                PyFunceble.HTTP_CODE["list"]["up"],
                PyFunceble.HTTP_CODE["list"]["potentially_down"],
                PyFunceble.HTTP_CODE["list"]["potentially_up"],
            ]:
                # We loop throught the list of http status code.

                # We extend the list of valid with the currently read
                # codes.
                list_of_valid_http_code.extend(codes)

            if http_code not in list_of_valid_http_code or http_code is None:
                # * The extracted http code is not in the list of valid http code.
                # or
                # * The extracted http code is equal to `None`.

                # We return 3 star in order to mention that we were not eable to extract
                # the http status code.
                return "*" * 3

            # * The extracted http code is in the list of valid http code.
            # or
            # * The extracted http code is not equal to `None`.

            # We return the extracted http status code.
            return http_code

        # The http status code extraction is activated.

        # We return None.
        return None


#!/usr/bin/env python3

# pylint:disable=line-too-long, too-many-lines
"""
The tool to check the availability or syntax of domains, IPv4 or URL.

::

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

This submodule is the main entry of PyFunceble.

Author:
    Nissar Chababy, @funilrys, contactTATAfunilrysTODTODcom

Special thanks:
    https://pyfunceble.readthedocs.io/en/master/special-thanks.html

Contributors:
    http://pyfunceble.readthedocs.io/en/master/special-thanks.html

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

Project documentation:
    https://pyfunceble.readthedocs.io/en/master/

Project homepage:
    https://funilrys.github.io/PyFunceble/

License:
::

    MIT License

    Copyright (c) 2017-2019 Nissar Chababy

    Permission is hereby granted, free of charge, to any person obtaining a copy
    of this software and associated documentation files (the "Software"), to deal
    in the Software without restriction, including without limitation the rights
    to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    copies of the Software, and to permit persons to whom the Software is
    furnished to do so, subject to the following conditions:

    The above copyright notice and this permission notice shall be included in all
    copies or substantial portions of the Software.

    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
    SOFTWARE.
"""
# pylint: enable=line-too-long
# pylint: disable=invalid-name,cyclic-import, bad-continuation
import argparse
import socket
from collections import OrderedDict
from inspect import getsourcefile
from os import environ, getcwd, mkdir, path, rename
from os import sep as directory_separator
from os import walk
from platform import system
from random import choice
from shutil import copy, rmtree
from time import mktime, strftime, strptime, time

import requests
from colorama import Back, Fore, Style
from colorama import init as initiate

from PyFunceble.check import Check
from PyFunceble.clean import Clean
from PyFunceble.config import Load, Merge, Version
from PyFunceble.core import Core
from PyFunceble.directory_structure import DirectoryStructure
from PyFunceble.iana import IANA
from PyFunceble.production import Production
from PyFunceble.publicsuffix import PublicSuffix

# We set our project name.
NAME = "PyFunceble"
# We set out project version.
VERSION = "1.17.1. (Blue Bontebok: Caterpillar)"

# We set the list of windows "platforms"
WINDOWS_PLATFORMS = ["windows", "cygwin", "cygwin_nt-10.0"]

if "PYFUNCEBLE_OUTPUT_DIR" in environ:  # pragma: no cover
    # We handle the case that the `PYFUNCEBLE_OUTPUT_DIR` environnement variable is set.
    CURRENT_DIRECTORY = environ["PYFUNCEBLE_OUTPUT_DIR"]
elif Version(True).is_cloned():  # pragma: no cover
    # We handle the case that we are in a cloned.
    CURRENT_DIRECTORY = getcwd() + directory_separator
elif "TRAVIS_BUILD_DIR" in environ:  # pragma: no cover
    # We handle the case that we are under Travis CI.
    CURRENT_DIRECTORY = getcwd() + directory_separator
else:  # pragma: no cover
    # We handle all other case and distributions specific cases.

    if system().lower() == "linux" or system().lower() == "darwin":
        # We are under a Linux distribution.

        # We set the default configuration location path.
        config_dir_path = (
            path.expanduser("~" + directory_separator + ".config") + directory_separator
        )

        if path.isdir(config_dir_path):
            # Everything went right:
            #   * `~/.config` exists.
            # We set our configuration location path as the directory we are working with.
            CURRENT_DIRECTORY = config_dir_path
        elif path.isdir(path.expanduser("~")):
            # Something went wrong:
            #   * `~/.config` does not exists.
            #   * `~` exists.
            # We set `~/` as the directory we are working with.
            #
            # Note: The `.` at the end is because we want to hide the directory we are
            # going to create.
            CURRENT_DIRECTORY = (
                path.expanduser("~") + directory_separator + "."
            )  # pylint: disable=line-too-long
        else:
            # Everything went wrong:
            #   * `~/.config` does not exists.
            #   * `~` soes not exists.
            # We set the current directory as the directory we are working with.
            CURRENT_DIRECTORY = getcwd() + directory_separator
    elif system().lower() in WINDOWS_PLATFORMS:
        # We are under Windows or CygWin.

        if "APPDATA" in environ:
            # Everything went right:
            #   * `APPDATA` is into the environnement variables.
            # We set it as the directory we are working with.
            CURRENT_DIRECTORY = environ["APPDATA"]
        else:
            # Everything went wrong:
            #   * `APPDATA` is not into the environnement variables.
            # We set the current directory as the directory we are working with.
            CURRENT_DIRECTORY = getcwd() + directory_separator

    if not CURRENT_DIRECTORY.endswith(directory_separator):
        # If the directory we are working with does not ends with the directory
        # separator, we append it to the end.
        CURRENT_DIRECTORY += directory_separator

    # We append the name of the project to the directory we are working with.
    CURRENT_DIRECTORY += NAME + directory_separator

    if not path.isdir(CURRENT_DIRECTORY):
        # If the directory does not exist we create it.
        mkdir(CURRENT_DIRECTORY)

if not CURRENT_DIRECTORY.endswith(directory_separator):  # pragma: no cover
    # Again for safety, if the directory we are working with does not ends with
    # the directory separator, we append it to the end.
    CURRENT_DIRECTORY += directory_separator

# We set the location of the `output` directory which should always be in the current
# directory.
OUTPUT_DIRECTORY = getcwd() + directory_separator

# We set the filename of the default configuration file.
DEFAULT_CONFIGURATION_FILENAME = ".PyFunceble_production.yaml"
# We set the filename of the configuration file we are actually using.
CONFIGURATION_FILENAME = ".PyFunceble.yaml"

# We set the current time (return the current time) in a specific format.
CURRENT_TIME = strftime("%a %d %b %H:%m:%S %Z %Y")

# We initiate the location where we are going to save our whole configuration content.
CONFIGURATION = {}
# We initiate the location where we are going to get all statuses.
STATUS = {}
# We initiate the location where we are going to get all outputs.
OUTPUTS = {}
# We initiate the location where we are going to get the map of the classification
# of each status codes for the analytic part.
HTTP_CODE = {}
# We initiate the location where we are going to get all links.
LINKS = {}
# We initiate a location which will have all internal data.
INTERN = {
    "counter": {
        "number": {"down": 0, "invalid": 0, "tested": 0, "up": 0},
        "percentage": {"down": 0, "invalid": 0, "up": 0},
    }
}

# We initiate the CLI logo of PyFunceble.
ASCII_PYFUNCEBLE = """
██████╗ ██╗   ██╗███████╗██╗   ██╗███╗   ██╗ ██████╗███████╗██████╗ ██╗     ███████╗
██╔══██╗╚██╗ ██╔╝██╔════╝██║   ██║████╗  ██║██╔════╝██╔════╝██╔══██╗██║     ██╔════╝
██████╔╝ ╚████╔╝ █████╗  ██║   ██║██╔██╗ ██║██║     █████╗  ██████╔╝██║     █████╗
██╔═══╝   ╚██╔╝  ██╔══╝  ██║   ██║██║╚██╗██║██║     ██╔══╝  ██╔══██╗██║     ██╔══╝
██║        ██║   ██║     ╚██████╔╝██║ ╚████║╚██████╗███████╗██████╔╝███████╗███████╗
╚═╝        ╚═╝   ╚═╝      ╚═════╝ ╚═╝  ╚═══╝ ╚═════╝╚══════╝╚═════╝ ╚══════╝╚══════╝
"""

def test(domain, complete=False, config=None):  # pragma: no cover
    """
    Test the availability of the given domain or IP.

    :param domain: The domain or IP to test.
    :type domain: str

    :param complete:
        Activate the return of a dict with some significant data from
        the test.
    :type complete: bool

    :param config:
        A dict with the configuration index (from .PyFunceble.yaml) to update.
    :type config: dict

    :return: The status or the informations of the domain.
    :rtype: str|dict

    .. warning::
        If an empty or a non-string :code:`domain` is given, we return :code:`None`.

    .. warning::
        If :code:`config` is given, the given :code:`dict` overwrite
        the last value of the given indexes.

    .. note::
        If :code:`complete` is set to :code:`True`, we return the following indexes.

        ::

            {
                "_status_source": None,
                "_status": None,
                "domain_syntax_validation": None,
                "expiration_date": None,
                "http_status_code": None,
                "ip4_syntax_validation": None,
                "nslookup": [],
                "status_source": None,
                "status": None,
                "tested": None,
                "url_syntax_validation": None,
                "whois_record": None,
                "whois_server": None,
            }
    """

    if domain and isinstance(domain, str):
        # * The given domain is not empty nor None.
        # and
        # * The given domain is a string.

        # We silently load the configuration.
        load_config(True)

        if config and isinstance(config, dict):
            # The given configuration is not None or empty.
            # and
            # It is a dict.

            # We update the configuration index.
            CONFIGURATION.update(config)

        # And we return the status of the given domain.
        return Core(domain_or_ip_to_test=domain, modulo_test=True).test(complete)

    # We return None, there is nothing to test.
    return None

def syntax_check(domain):  # pragma: no cover
    """
    Check the syntax of the given domain.

    :param domain: The domain to check the syntax for.
    :type domain: str

    :return: The syntax validity.
    :rtype: bool

    .. warning::
        If an empty or a non-string :code:`domain` is given, we return :code:`None`.
    """

    if domain and isinstance(domain, str):
        # * The given domain is not empty nor None.
        # and
        # * The given domain is a string.

        # We silently load the configuration.
        load_config(True)

        return Check(domain).is_domain_valid()

    # We return None, there is nothing to check.
    return None

def is_subdomain(domain):  # pragma: no cover
    """
    Check if the given domain is a subdomain.

    :param domain: The domain we are checking.
    :type domain: str

    :return: The subdomain state.
    :rtype: bool

    .. warning::
        If an empty or a non-string :code:`domain` is given, we return :code:`None`.
    """

    if domain and isinstance(domain, str):
        # * The given domain is not empty nor None.
        # and
        # * The given domain is a string.

        # We silently load the configuration.
        load_config(True)

        return Check(domain).is_subdomain()

    # We return None, there is nothing to check.
    return None

def ipv4_syntax_check(ip):  # pragma: no cover
    """
    Check the syntax of the given IPv4.

    :param ip: The IPv4 to check the syntax for.
    :type ip: str

    :return: The syntax validity.
    :rtype: bool

    .. warning::
        If an empty or a non-string :code:`ip` is given, we return :code:`None`.
    """

    if ip and isinstance(ip, str):
        # The given IP is not empty nor None.
        # and
        # * The given IP is a string.

        # We silently load the configuration.
        load_config(True)

        return Check(ip).is_ip_valid()

    # We return None, there is nothing to check.
    return None

def is_ipv4_range(ip):  # pragma: no cover
    """
    Check if the given IP is an IP range.

    :param ip: The IP we are checking.
    :type ip: str

    :return: The IPv4 range state.
    :rtype: bool

    .. warning::
        If an empty or a non-string :code:`ip` is given, we return :code:`None`.
    """

    if ip and isinstance(ip, str):
        # The given IP is not empty nor None.
        # and
        # * The given IP is a string.

        # We silently load the configuration.
        load_config(True)

        return Check(ip).is_ip_range()

    # We return None, there is nothing to check.
    return None

def url_syntax_check(url):  # pragma: no cover
    """
    Check the syntax of the given URL.

    :param url: The URL to check the syntax for.
    :type url: str

    :return: The syntax validity.
    :rtype: bool

    .. warning::
        If an empty or a non-string :code:`url` is given, we return :code:`None`.
    """

    if url and isinstance(url, str):
        # The given URL is not empty nor None.
        # and
        # * The given URL is a string.

        # We silently load the configuration.
        load_config(True)

        return Check(url).is_url_valid()

    # We return None, there is nothing to check.
    return None

def url_test(url, complete=False, config=None):  # pragma: no covere
    """
    Test the availability of the given URL.

    :param url: The URL to test.
    :type url: str

    :param complete:
        Activate the return of a dict with some significant data from
        the test.
    :type complete: bool

    :param config:
        A dict with the configuration index (from .PyFunceble.yaml) to update.
    :type config: dict

    :return: The status or the informations of the URL.
    :rtype: str|dict

    .. warning::
        If an empty or a non-string :code:`url` is given, we return :code:`None`.

    .. warning::
        If :code:`config` is given, the given :code:`dict` overwrite
        the last value of the given indexes.

    """

    if url and isinstance(url, str):
        # The given URL is not empty nor None.
        # and
        # * The given URL is a string.

        # We silently load the configuration.
        load_config(True)

        if config and isinstance(config, dict):
            # The given configuration is not None or empty.
            # and
            # It is a dict.

            # We update the configuration index.
            CONFIGURATION.update(config)

        # And we return the status of the given URL.
        return Core(url_to_test=url, modulo_test=True).test(complete)

    # We return None, there is nothing to test.
    return None

def load_config(under_test=False, custom=None):  # pragma: no cover
    """
    Load the configuration.

    :param under_test:
        Tell us if we only have to load the configuration file (True)
        or load the configuration file and initate the output directory
        if it does not exist (False).
    :type under_test: bool

    :param custom:
        A dict with the configuration index (from .PyFunceble.yaml) to update.
    :type custom: dict

    .. warning::
        If :code:`custom` is given, the given :code:`dict` overwrite
        the last value of the given configuration indexes.
    """

    if "config_loaded" not in INTERN:
        # The configuration was not already loaded.

        # We load and download the different configuration file if they are non
        # existant.
        Load(CURRENT_DIRECTORY)

        if not under_test:
            # If we are not under test which means that we want to save informations,
            # we initiate the directory structure.
            DirectoryStructure()

        # We save that the configuration was loaded.
        INTERN.update({"config_loaded": True})

        if custom and isinstance(custom, dict):
            # The given configuration is not None or empty.
            # and
            # It is a dict.

            # We update the configuration index.
            CONFIGURATION.update(custom)

def stay_safe():  # pragma: no cover
    """
    Print a friendly message.
    """

    random = int(choice(str(int(time()))))

    if not CONFIGURATION["quiet"] and random % 3 == 0:
        print("\n" + Fore.GREEN + Style.BRIGHT + "Thanks for using PyFunceble!")
        print(
            Fore.YELLOW
            + Style.BRIGHT
            + "Share your experience on "
            + Fore.CYAN
            + "Twitter"
            + Fore.YELLOW
            + " with "
            + Fore.CYAN
            + "#PyFunceble"
            + Fore.YELLOW
            + "!"
        )
        print(
            Fore.GREEN
            + Style.BRIGHT
            + "Have a feedback, an issue or an improvement idea ?"
        )
        print(
            Fore.YELLOW
            + Style.BRIGHT
            + "Let us know on "
            + Fore.CYAN
            + "GitHub"
            + Fore.YELLOW
            + "!"
        )

def _command_line():  # pragma: no cover pylint: disable=too-many-branches,too-many-statements
    """
    Provide the command line interface.
    """

    if __name__ == "PyFunceble":
        # We initiate the end of the coloration at the end of each line.
        initiate(autoreset=True)

        # We load the configuration and the directory structure.
        load_config(True)
        try:
            # The following handle the command line argument.

            try:
                PARSER = argparse.ArgumentParser(
                    epilog="Crafted with %s by %s"
                    % (
                        Fore.RED + "♥" + Fore.RESET,
                        Style.BRIGHT
                        + Fore.CYAN
                        + "Nissar Chababy (Funilrys) "
                        + Style.RESET_ALL
                        + "with the help of "
                        + Style.BRIGHT
                        + Fore.GREEN
                        + "https://pyfunceble.rtfd.io/en/master/contributors.html "
                        + Style.RESET_ALL
                        + "&& "
                        + Style.BRIGHT
                        + Fore.GREEN
                        + "https://pyfunceble.rtfd.io/en/master/special-thanks.html",
                    ),
                    add_help=False,
                )

                CURRENT_VALUE_FORMAT = (
                    Fore.YELLOW + Style.BRIGHT + "Configured value: " + Fore.BLUE
                )

                PARSER.add_argument(
                    "-ad",
                    "--adblock",
                    action="store_true",
                    help="Switch the decoding of the adblock format. %s"
                    % (
                        CURRENT_VALUE_FORMAT
                        + repr(CONFIGURATION["adblock"])
                        + Style.RESET_ALL
                    ),
                )

                PARSER.add_argument(
                    "-a",
                    "--all",
                    action="store_false",
                    help="Output all available information on the screen. %s"
                    % (
                        CURRENT_VALUE_FORMAT
                        + repr(CONFIGURATION["less"])
                        + Style.RESET_ALL
                    ),
                )

                PARSER.add_argument(
                    "" "-c",
                    "--auto-continue",
                    "--continue",
                    action="store_true",
                    help="Switch the value of the auto continue mode. %s"
                    % (
                        CURRENT_VALUE_FORMAT
                        + repr(CONFIGURATION["auto_continue"])
                        + Style.RESET_ALL
                    ),
                )

                PARSER.add_argument(
                    "--autosave-minutes",
                    type=int,
                    help="Update the minimum of minutes before we start "
                    "committing to upstream under Travis CI. %s"
                    % (
                        CURRENT_VALUE_FORMAT
                        + repr(CONFIGURATION["travis_autosave_minutes"])
                        + Style.RESET_ALL
                    ),
                )

                PARSER.add_argument(
                    "--clean", action="store_true", help="Clean all files under output."
                )

                PARSER.add_argument(
                    "--clean-all",
                    action="store_true",
                    help="Clean all files under output and all file generated by PyFunceble.",
                )

                PARSER.add_argument(
                    "--cmd",
                    type=str,
                    help="Pass a command to run before each commit "
                    "(except the final one) under the Travis mode. %s"
                    % (
                        CURRENT_VALUE_FORMAT
                        + repr(CONFIGURATION["command_before_end"])
                        + Style.RESET_ALL
                    ),
                )

                PARSER.add_argument(
                    "--cmd-before-end",
                    type=str,
                    help="Pass a command to run before the results "
                    "(final) commit under the Travis mode. %s"
                    % (
                        CURRENT_VALUE_FORMAT
                        + repr(CONFIGURATION["command_before_end"])
                        + Style.RESET_ALL
                    ),
                )

                PARSER.add_argument(
                    "--commit-autosave-message",
                    type=str,
                    help="Replace the default autosave commit message. %s"
                    % (
                        CURRENT_VALUE_FORMAT
                        + repr(CONFIGURATION["travis_autosave_commit"])
                        + Style.RESET_ALL
                    ),
                )

                PARSER.add_argument(
                    "--commit-results-message",
                    type=str,
                    help="Replace the default results (final) commit message. %s"
                    % (
                        CURRENT_VALUE_FORMAT
                        + repr(CONFIGURATION["travis_autosave_final_commit"])
                        + Style.RESET_ALL
                    ),
                )

                PARSER.add_argument(
                    "-d", "--domain", type=str, help="Set and test the given domain."
                )

                PARSER.add_argument(
                    "-db",
                    "--database",
                    action="store_true",
                    help="Switch the value of the usage of a database to store "
                    "inactive domains of the currently tested list. %s"
                    % (
                        CURRENT_VALUE_FORMAT
                        + repr(CONFIGURATION["inactive_database"])
                        + Style.RESET_ALL
                    ),
                )

                PARSER.add_argument(
                    "-dbr",
                    "--days-between-db-retest",
                    type=int,
                    help="Set the numbers of days between each retest of domains present "
                    "into inactive-db.json. %s"
                    % (
                        CURRENT_VALUE_FORMAT
                        + repr(CONFIGURATION["days_between_db_retest"])
                        + Style.RESET_ALL
                    ),
                )

                PARSER.add_argument(
                    "--debug",
                    action="store_true",
                    help="Switch the value of the debug mode. %s"
                    % (
                        CURRENT_VALUE_FORMAT
                        + repr(CONFIGURATION["debug"])
                        + Style.RESET_ALL
                    ),
                )

                PARSER.add_argument(
                    "--directory-structure",
                    action="store_true",
                    help="Generate the directory and files that are needed and which does "
                    "not exist in the current directory.",
                )

                PARSER.add_argument(
                    "-ex",
                    "--execution",
                    action="store_true",
                    help="Switch the default value of the execution time showing. %s"
                    % (
                        CURRENT_VALUE_FORMAT
                        + repr(CONFIGURATION["show_execution_time"])
                        + Style.RESET_ALL
                    ),
                )

                PARSER.add_argument(
                    "-f",
                    "--file",
                    type=str,
                    help="Read the given file and test all domains inside it. "
                    "If a URL is given we download and test the content of the given URL.",  # pylint: disable=line-too-long
                )

                PARSER.add_argument(
                    "--filter", type=str, help="Domain to filter (regex)."
                )

                PARSER.add_argument(
                    "--help",
                    action="help",
                    default=argparse.SUPPRESS,
                    help="Show this help message and exit.",
                )

                PARSER.add_argument(
                    "--hierarchical",
                    action="store_true",
                    help="Switch the value of the hierarchical sorting of the tested file. %s"
                    % (
                        CURRENT_VALUE_FORMAT
                        + repr(CONFIGURATION["hierarchical_sorting"])
                        + Style.RESET_ALL
                    ),
                )

                PARSER.add_argument(
                    "-h",
                    "--host",
                    action="store_true",
                    help="Switch the value of the generation of hosts file. %s"
                    % (
                        CURRENT_VALUE_FORMAT
                        + repr(CONFIGURATION["generate_hosts"])
                        + Style.RESET_ALL
                    ),
                )

                PARSER.add_argument(
                    "--http",
                    action="store_true",
                    help="Switch the value of the usage of HTTP code. %s"
                    % (
                        CURRENT_VALUE_FORMAT
                        + repr(HTTP_CODE["active"])
                        + Style.RESET_ALL
                    ),
                )

                PARSER.add_argument(
                    "--iana",
                    action="store_true",
                    help="Update/Generate `iana-domains-db.json`.",
                )

                PARSER.add_argument(
                    "--idna",
                    action="store_true",
                    help="Switch the value of the IDNA conversion. %s"
                    % (
                        CURRENT_VALUE_FORMAT
                        + repr(CONFIGURATION["idna_conversion"])
                        + Style.RESET_ALL
                    ),
                )

                PARSER.add_argument(
                    "-ip",
                    type=str,
                    help="Change the IP to print in the hosts files with the given one. %s"
                    % (
                        CURRENT_VALUE_FORMAT
                        + repr(CONFIGURATION["custom_ip"])
                        + Style.RESET_ALL
                    ),
                )

                PARSER.add_argument(
                    "--json",
                    action="store_true",
                    help="Switch the value of the generation "
                    "of the JSON formatted list of domains. %s"
                    % (
                        CURRENT_VALUE_FORMAT
                        + repr(CONFIGURATION["generate_json"])
                        + Style.RESET_ALL
                    ),
                )

                PARSER.add_argument(
                    "--less",
                    action="store_true",
                    help="Output less informations on screen. %s"
                    % (
                        CURRENT_VALUE_FORMAT
                        + repr(Core.switch("less"))
                        + Style.RESET_ALL
                    ),
                )

                PARSER.add_argument(
                    "--local",
                    action="store_true",
                    help="Switch the value of the local network testing. %s"
                    % (
                        CURRENT_VALUE_FORMAT
                        + repr(Core.switch("local"))
                        + Style.RESET_ALL
                    ),
                )

                PARSER.add_argument(
                    "--link", type=str, help="Download and test the given file."
                )

                PARSER.add_argument(
                    "-m",
                    "--mining",
                    action="store_true",
                    help="Switch the value of the mining subsystem usage. %s"
                    % (
                        CURRENT_VALUE_FORMAT
                        + repr(CONFIGURATION["mining"])
                        + Style.RESET_ALL
                    ),
                )

                PARSER.add_argument(
                    "-n",
                    "--no-files",
                    action="store_true",
                    help="Switch the value of the production of output files. %s"
                    % (
                        CURRENT_VALUE_FORMAT
                        + repr(CONFIGURATION["no_files"])
                        + Style.RESET_ALL
                    ),
                )

                PARSER.add_argument(
                    "-nl",
                    "--no-logs",
                    action="store_true",
                    help="Switch the value of the production of logs files "
                    "in the case we encounter some errors. %s"
                    % (
                        CURRENT_VALUE_FORMAT
                        + repr(not CONFIGURATION["logs"])
                        + Style.RESET_ALL
                    ),
                )

                PARSER.add_argument(
                    "-ns",
                    "--no-special",
                    action="store_true",
                    help="Switch the value of the usage of the SPECIAL rules. %s"
                    % (
                        CURRENT_VALUE_FORMAT
                        + repr(CONFIGURATION["no_special"])
                        + Style.RESET_ALL
                    ),
                )

                PARSER.add_argument(
                    "-nu",
                    "--no-unified",
                    action="store_true",
                    help="Switch the value of the production unified logs "
                    "under the output directory. %s"
                    % (
                        CURRENT_VALUE_FORMAT
                        + repr(CONFIGURATION["unified"])
                        + Style.RESET_ALL
                    ),
                )

                PARSER.add_argument(
                    "-nw",
                    "--no-whois",
                    action="store_true",
                    help="Switch the value the usage of whois to test domain's status. %s"
                    % (
                        CURRENT_VALUE_FORMAT
                        + repr(CONFIGURATION["no_whois"])
                        + Style.RESET_ALL
                    ),
                )

                PARSER.add_argument(
                    "-p",
                    "--percentage",
                    action="store_true",
                    help="Switch the value of the percentage output mode. %s"
                    % (
                        CURRENT_VALUE_FORMAT
                        + repr(CONFIGURATION["show_percentage"])
                        + Style.RESET_ALL
                    ),
                )

                PARSER.add_argument(
                    "--plain",
                    action="store_true",
                    help="Switch the value of the generation "
                    "of the plain list of domains. %s"
                    % (
                        CURRENT_VALUE_FORMAT
                        + repr(CONFIGURATION["plain_list_domain"])
                        + Style.RESET_ALL
                    ),
                )

                PARSER.add_argument(
                    "--production",
                    action="store_true",
                    help="Prepare the repository for production.",
                )

                PARSER.add_argument(
                    "-psl",
                    "--public-suffix",
                    action="store_true",
                    help="Update/Generate `public-suffix.json`.",
                )

                PARSER.add_argument(
                    "-q",
                    "--quiet",
                    action="store_true",
                    help="Run the script in quiet mode. %s"
                    % (
                        CURRENT_VALUE_FORMAT
                        + repr(CONFIGURATION["quiet"])
                        + Style.RESET_ALL
                    ),
                )

                PARSER.add_argument(
                    "--share-logs",
                    action="store_true",
                    help="Switch the value of the sharing of logs. %s"
                    % (
                        CURRENT_VALUE_FORMAT
                        + repr(CONFIGURATION["share_logs"])
                        + Style.RESET_ALL
                    ),
                )

                PARSER.add_argument(
                    "-s",
                    "--simple",
                    action="store_true",
                    help="Switch the value of the simple output mode. %s"
                    % (
                        CURRENT_VALUE_FORMAT
                        + repr(CONFIGURATION["simple"])
                        + Style.RESET_ALL
                    ),
                )

                PARSER.add_argument(
                    "--split",
                    action="store_true",
                    help="Switch the value of the split of the generated output files. %s"
                    % (
                        CURRENT_VALUE_FORMAT
                        + repr(CONFIGURATION["inactive_database"])
                        + Style.RESET_ALL
                    ),
                )

                PARSER.add_argument(
                    "--syntax",
                    action="store_true",
                    help="Switch the value of the syntax test mode. %s"
                    % (
                        CURRENT_VALUE_FORMAT
                        + repr(CONFIGURATION["syntax"])
                        + Style.RESET_ALL
                    ),
                )

                PARSER.add_argument(
                    "-t",
                    "--timeout",
                    type=int,
                    default=3,
                    help="Switch the value of the timeout. %s"
                    % (
                        CURRENT_VALUE_FORMAT
                        + repr(CONFIGURATION["seconds_before_http_timeout"])
                        + Style.RESET_ALL
                    ),
                )

                PARSER.add_argument(
                    "--travis",
                    action="store_true",
                    help="Switch the value of the Travis mode. %s"
                    % (
                        CURRENT_VALUE_FORMAT
                        + repr(CONFIGURATION["travis"])
                        + Style.RESET_ALL
                    ),
                )

                PARSER.add_argument(
                    "--travis-branch",
                    type=str,
                    default="master",
                    help="Switch the branch name where we are going to push. %s"
                    % (
                        CURRENT_VALUE_FORMAT
                        + repr(CONFIGURATION["travis_branch"])
                        + Style.RESET_ALL
                    ),
                )

                PARSER.add_argument(
                    "-u", "--url", type=str, help="Analyze the given URL."
                )

                PARSER.add_argument(
                    "-uf",
                    "--url-file",
                    type=str,
                    help="Read and test the list of URL of the given file. "
                    "If a URL is given we download and test the content of the given URL.",  # pylint: disable=line-too-long
                )

                PARSER.add_argument(
                    "-ua",
                    "--user-agent",
                    type=str,
                    help="Set the user-agent to use and set every time we "
                    "interact with everything which is not our logs sharing system.",  # pylint: disable=line-too-long
                )

                PARSER.add_argument(
                    "-v",
                    "--version",
                    help="Show the version of PyFunceble and exit.",
                    action="version",
                    version="%(prog)s " + VERSION,
                )

                PARSER.add_argument(
                    "-vsc",
                    "--verify-ssl-certificate",
                    action="store_true",
                    help="Switch the value of the verification of the "
                    "SSL/TLS certificate when testing for URL. %s"
                    % (
                        CURRENT_VALUE_FORMAT
                        + repr(CONFIGURATION["verify_ssl_certificate"])
                        + Style.RESET_ALL
                    ),
                )

                PARSER.add_argument(
                    "-wdb",
                    "--whois-database",
                    action="store_true",
                    help="Switch the value of the usage of a database to store "
                    "whois data in order to avoid whois servers rate limit. %s"
                    % (
                        CURRENT_VALUE_FORMAT
                        + repr(CONFIGURATION["whois_database"])
                        + Style.RESET_ALL
                    ),
                )

                ARGS = PARSER.parse_args()

                if ARGS.less:
                    CONFIGURATION.update({"less": ARGS.less})
                elif not ARGS.all:
                    CONFIGURATION.update({"less": ARGS.all})

                if ARGS.adblock:
                    CONFIGURATION.update({"adblock": Core.switch("adblock")})

                if ARGS.auto_continue:
                    CONFIGURATION.update(
                        {"auto_continue": Core.switch("auto_continue")}
                    )

                if ARGS.autosave_minutes:
                    CONFIGURATION.update(
                        {"travis_autosave_minutes": ARGS.autosave_minutes}
                    )

                if ARGS.clean:
                    Clean(None)

                if ARGS.clean_all:
                    Clean(None, ARGS.clean_all)

                if ARGS.cmd:
                    CONFIGURATION.update({"command": ARGS.cmd})

                if ARGS.cmd_before_end:
                    CONFIGURATION.update({"command_before_end": ARGS.cmd_before_end})

                if ARGS.commit_autosave_message:
                    CONFIGURATION.update(
                        {"travis_autosave_commit": ARGS.commit_autosave_message}
                    )

                if ARGS.commit_results_message:
                    CONFIGURATION.update(
                        {"travis_autosave_final_commit": ARGS.commit_results_message}
                    )

                if ARGS.database:
                    CONFIGURATION.update(
                        {"inactive_database": Core.switch("inactive_database")}
                    )

                if ARGS.days_between_db_retest:
                    CONFIGURATION.update(
                        {"days_between_db_retest": ARGS.days_between_db_retest}
                    )

                if ARGS.debug:
                    CONFIGURATION.update({"debug": Core.switch("debug")})

                if ARGS.directory_structure:
                    DirectoryStructure()

                if ARGS.execution:
                    CONFIGURATION.update(
                        {"show_execution_time": Core.switch("show_execution_time")}
                    )

                if ARGS.filter:
                    CONFIGURATION.update({"filter": ARGS.filter})

                if ARGS.hierarchical:
                    CONFIGURATION.update(
                        {"hierarchical_sorting": Core.switch("hierarchical_sorting")}
                    )

                if ARGS.host:
                    CONFIGURATION.update(
                        {"generate_hosts": Core.switch("generate_hosts")}
                    )

                if ARGS.http:
                    HTTP_CODE.update({"active": Core.switch(HTTP_CODE["active"], True)})

                if ARGS.iana:
                    IANA().update()

                if ARGS.idna:
                    CONFIGURATION.update(
                        {"idna_conversion": Core.switch("idna_conversion")}
                    )

                if ARGS.ip:
                    CONFIGURATION.update({"custom_ip": ARGS.ip})

                if ARGS.json:
                    CONFIGURATION.update(
                        {"generate_json": Core.switch("generate_json")}
                    )

                if ARGS.local:
                    CONFIGURATION.update({"local": Core.switch("local")})

                if ARGS.mining:
                    CONFIGURATION.update({"mining": Core.switch("mining")})

                if ARGS.no_files:
                    CONFIGURATION.update({"no_files": Core.switch("no_files")})

                if ARGS.no_logs:
                    CONFIGURATION.update({"logs": Core.switch("logs")})

                if ARGS.no_special:
                    CONFIGURATION.update({"no_special": Core.switch("no_special")})

                if ARGS.no_unified:
                    CONFIGURATION.update({"unified": Core.switch("unified")})

                if ARGS.no_whois:
                    CONFIGURATION.update({"no_whois": Core.switch("no_whois")})

                if ARGS.percentage:
                    CONFIGURATION.update(
                        {"show_percentage": Core.switch("show_percentage")}
                    )

                if ARGS.plain:
                    CONFIGURATION.update(
                        {"plain_list_domain": Core.switch("plain_list_domain")}
                    )

                if ARGS.production:
                    Production()

                if ARGS.public_suffix:
                    PublicSuffix().update()

                if ARGS.quiet:
                    CONFIGURATION.update({"quiet": Core.switch("quiet")})

                if ARGS.share_logs:
                    CONFIGURATION.update({"share_logs": Core.switch("share_logs")})

                if ARGS.simple:
                    CONFIGURATION.update(
                        {"simple": Core.switch("simple"), "quiet": Core.switch("quiet")}
                    )

                if ARGS.split:
                    CONFIGURATION.update({"split": Core.switch("split")})

                if ARGS.syntax:
                    CONFIGURATION.update({"syntax": Core.switch("syntax")})

                if ARGS.timeout and ARGS.timeout % 3 == 0:
                    CONFIGURATION.update({"seconds_before_http_timeout": ARGS.timeout})

                if ARGS.travis:
                    CONFIGURATION.update({"travis": Core.switch("travis")})

                if ARGS.travis_branch:
                    CONFIGURATION.update({"travis_branch": ARGS.travis_branch})

                if ARGS.user_agent:
                    CONFIGURATION.update({"user_agent": ARGS.user_agent})

                if ARGS.verify_ssl_certificate:
                    CONFIGURATION.update(
                        {"verify_ssl_certificate": ARGS.verify_ssl_certificate}
                    )

                if ARGS.whois_database:
                    CONFIGURATION.update(
                        {"whois_database": Core.switch("whois_database")}
                    )

                if not CONFIGURATION["quiet"]:
                    Core.colorify_logo(home=True)

                # We compare the versions (upstream and local) and in between.
                Version().compare()

                # We call our Core which will handle all case depending of the configuration or
                # the used command line arguments.
                Core(
                    domain_or_ip_to_test=ARGS.domain,
                    file_path=ARGS.file,
                    url_to_test=ARGS.url,
                    url_file=ARGS.url_file,
                    link_to_test=ARGS.link,
                )
            except KeyError as e:
                if not Version(True).is_cloned():
                    # We are not into the cloned version.

                    # We merge the local with the upstream configuration.
                    Merge(CURRENT_DIRECTORY)
                else:
                    # We are in the cloned version.

                    # We raise the exception.
                    #
                    # Note: The purpose of this is to avoid having
                    # to search for a mistake while developing.
                    raise e
        except KeyboardInterrupt:
            stay_safe()