#!/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 database 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 PyFunceble
from PyFunceble.helpers import Dict, File, List

class Inactive:
    """
    Logic behind the generation and the usage of a database system.
    The main idea behind this is to provide an inactive-db.json and test all
    inactive domain which are into to it regularly
    """

    def __init__(self):

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

            # We set the equivalent of one day in seconds.
            self.one_day_in_seconds = 1 * 24 * 3600

            # We convert the number of days between the database retest
            # to seconds.
            self.days_in_seconds = (
                PyFunceble.CONFIGURATION["days_between_db_retest"] * 24 * 3600
            )

            # We set the path to the inactive database file.
            self.inactive_db_path = (
                PyFunceble.CURRENT_DIRECTORY
                + PyFunceble.OUTPUTS["default_files"]["inactive_db"]
            )

            if "inactive_db" not in PyFunceble.INTERN:
                # The database is empty or equal to None.

                # We initiate it with an empty dictionnary.
                PyFunceble.INTERN["inactive_db"] = {}

            if (
                "flatten_inactive_db" not in PyFunceble.INTERN
                or not PyFunceble.INTERN["flatten_inactive_db"]
            ):
                # The flatten version of the database does not exist or is not set.

                # We create it.
                PyFunceble.INTERN["flatten_inactive_db"] = self.content()

    def _reformat_historical_formating_error(self):  # pragma: no cover
        """
        Format the old format so it can be merged into the newer format.
        """

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

            # We construct the possible path to an older version of the database.
            historical_formating_error = (
                PyFunceble.CURRENT_DIRECTORY + "inactive-db.json"
            )

            if PyFunceble.path.isfile(historical_formating_error):
                # The histortical file already exists.

                # We get its content.
                data = Dict().from_json(File(historical_formating_error).read())

                # We initiate a variable which will save the data that is going
                # to be merged.
                data_to_parse = {}

                # We get the database keybase.
                top_keys = data.keys()

                for top_key in top_keys:
                    # We loop through the list of upper keys.

                    # We get the lowest keys.
                    low_keys = data[top_key].keys()

                    # We initiate the data to parse.
                    data_to_parse[top_key] = {}

                    for low_key in low_keys:
                        # We loop through the list of lower keys.

                        if low_key.isdigit():
                            # The current low key is a digit.

                            # We parse its content (from the old) into the new format.
                            # In between, we remove 30 days from the low_key so that
                            # it become in the past. This way they will be retested
                            # automatically.
                            data_to_parse[top_key][
                                int(low_key) - (self.one_day_in_seconds * 30)
                            ] = data[top_key][low_key]
                        else:
                            # The current low key is not a digit.

                            # We parse its content (from the old) into the new format.
                            # In between, we remove 30 days from the current time so that
                            # it become in the past. This way they will be retested
                            # automatically.
                            data_to_parse[top_key][
                                int(PyFunceble.time()) - (self.one_day_in_seconds * 30)
                            ] = data[top_key][low_key]

                if "inactive_db" in PyFunceble.INTERN:
                    # The current (new) database is not empty.

                    # We update add the content of the old into the current database.
                    PyFunceble.INTERN["inactive_db"].update(data_to_parse)
                else:
                    # The current (new) database is empty.

                    # We replace the content with the data_to_parse as it is complient
                    # with the new format.
                    PyFunceble.INTERN["inactive_db"] = data_to_parse

                # We delete the old database file.
                File(historical_formating_error).delete()

    def _merge(self):
        """
        Merge the real database with the older one which
        has already been set into :code:`PyFunceble.INTERN["inactive_db"]`
        """

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

            # We get the content of the database.
            database_content = Dict().from_json(File(self.inactive_db_path).read())

            # We get the database top keys.
            database_top_keys = database_content.keys()

            for database_top_key in database_top_keys:
                # We loop through the list of database top keys.

                if database_top_key not in PyFunceble.INTERN["inactive_db"]:
                    # The currently read top key is not already into the database.

                    # We initiate the currently read key with the same key from
                    # our database file.
                    PyFunceble.INTERN["inactive_db"][
                        database_top_key
                    ] = database_content[database_top_key]
                else:
                    # The currently read top key is already into the database.

                    # We get the list of lower indexes.
                    database_low_keys = database_content[database_top_key].keys()

                    for database_low_key in database_low_keys:
                        # We loop through the lower keys.

                        if (
                            database_low_key
                            not in PyFunceble.INTERN["inactive_db"][database_top_key]
                        ):  # pragma: no cover
                            # The lower key is not already into the database.

                            # We initiate the currently read low and top key with the
                            # same combinaison from our database file.
                            PyFunceble.INTERN["inactive_db"][database_top_key][
                                database_low_key
                            ] = database_content[database_top_key][database_low_key]
                        else:
                            # The lower key is not already into the database.

                            # We exted the currently read low and top key combinaison
                            # with the same combinaison from our database file.
                            PyFunceble.INTERN["inactive_db"][database_top_key][
                                database_low_key
                            ].extend(
                                database_content[database_top_key][database_low_key]
                            )

                            # And we format the list of element to ensure that there is no
                            # duplicate into the database content.
                            PyFunceble.INTERN["inactive_db"][database_top_key][
                                database_low_key
                            ] = List(
                                PyFunceble.INTERN["inactive_db"][database_top_key][
                                    database_low_key
                                ]
                            ).format()

    def _retrieve(self):
        """
        Return the current content of the inactive-db.json file.
        """

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

            # We get, format and initiate the historical database file.
            self._reformat_historical_formating_error()

            if PyFunceble.path.isfile(self.inactive_db_path):
                # The database file exist.

                # We merge our current database into already initiated one.
                self._merge()

    def _backup(self):
        """
        Save the current database into the inactive-db.json file.
        """

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

            # We save the current database state into the database file.
            Dict(PyFunceble.INTERN["inactive_db"]).to_json(self.inactive_db_path)

    def _add_to_test(self, to_add):
        """
        Add an element or a list of element into
        :code:`PyFunceble.INTERN['inactive_db'][PyFunceble.INTERN["file_to_test"]]['to_test']`.

        :param to_add: The domain, IP or URL to add.
        :type to_add: str|list
        """

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

            if not isinstance(to_add, list):
                # The element to add is not a list.

                # We set it into a list.
                to_add = [to_add]

            if PyFunceble.INTERN["file_to_test"] in PyFunceble.INTERN["inactive_db"]:
                # The file we are testing is into the database.

                if (
                    "to_test"
                    in PyFunceble.INTERN["inactive_db"][
                        PyFunceble.INTERN["file_to_test"]
                    ]
                ):
                    # The `to_test` index is into the database related to the file
                    # we are testing.

                    # We extend the `to_test` element with the list we have to restest.
                    PyFunceble.INTERN["inactive_db"][PyFunceble.INTERN["file_to_test"]][
                        "to_test"
                    ].extend(to_add)
                else:
                    # The `to_test` index is not into the database related to the file
                    # we are testing.

                    # We initiate the `to_test` element with the list we have to retest.
                    PyFunceble.INTERN["inactive_db"][PyFunceble.INTERN["file_to_test"]][
                        "to_test"
                    ] = to_add
            else:
                # The file we are testing is not into the database.

                # We add the file and its to_test information into the database.
                PyFunceble.INTERN["inactive_db"].update(
                    {PyFunceble.INTERN["file_to_test"]: {"to_test": to_add}}
                )

            # We format the list to test in order to avoid duplicate.
            PyFunceble.INTERN["inactive_db"][PyFunceble.INTERN["file_to_test"]][
                "to_test"
            ] = List(
                PyFunceble.INTERN["inactive_db"][PyFunceble.INTERN["file_to_test"]][
                    "to_test"
                ]
            ).format()

            # And we finally backup the database.
            self._backup()

    def to_test(self):
        """
        Get the list to test for the next session.
        """

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

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

            # We initiate a variable which is going to save the key to remove
            # once they are merged into the `to_test` index.
            to_delete = []

            # We retrieve the database informations.
            self._retrieve()

            if PyFunceble.INTERN["file_to_test"] in PyFunceble.INTERN["inactive_db"]:
                # The file we are testing is into the database.

                for data in PyFunceble.INTERN["inactive_db"][
                    PyFunceble.INTERN["file_to_test"]
                ]:
                    # We loop through the database content related to the file we
                    # are testing.

                    if data != "to_test":
                        # The currently read index is not `to_test`.

                        if int(PyFunceble.time()) > int(data) + self.days_in_seconds:
                            # The currently read index is older than the excepted time
                            # for retesting.

                            # We extend our result variable with the content from the
                            # currently read index.
                            result.extend(
                                PyFunceble.INTERN["inactive_db"][
                                    PyFunceble.INTERN["file_to_test"]
                                ][data]
                            )

                            # And we append the currently read index into the list of
                            # index to delete.
                            to_delete.append(data)

                # We remove all indexes which are present into the list of index to delete.
                Dict(
                    PyFunceble.INTERN["inactive_db"][PyFunceble.INTERN["file_to_test"]]
                ).remove_key(to_delete)

                # And we append our list of element to retest into the `to_test` index.s
                self._add_to_test(result)
            else:
                # The file we are testing is not into the database.

                # We add the file we are testing into the database.
                PyFunceble.INTERN["inactive_db"].update(
                    {PyFunceble.INTERN["file_to_test"]: {}}
                )

            # And we finally backup the database.
            self._backup()

    def _timestamp(self):
        """
        Get the timestamp where we are going to save our current list.

        :return: The timestamp to append with the currently tested element.
        :rtype: int|str
        """

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

            if (
                "inactive_db" in PyFunceble.INTERN
                and PyFunceble.INTERN["file_to_test"]
                in PyFunceble.INTERN["inactive_db"]
                and PyFunceble.INTERN["inactive_db"][PyFunceble.INTERN["file_to_test"]]
            ):
                # The file we are testing is into the database and its content
                # is not empty.

                # We get the indexes of the current file (in the dabase).
                database_keys = [
                    x
                    for x in PyFunceble.INTERN["inactive_db"][
                        PyFunceble.INTERN["file_to_test"]
                    ].keys()
                    if x.isdigit()
                ]

                if database_keys:
                    # The list of keys is not empty.

                    # We get the most recent date.
                    recent_date = max(database_keys)
                else:  # pragma: no cover
                    # The list of keys is empty.

                    # We return the current time.
                    return int(PyFunceble.time())

                if int(PyFunceble.time()) > int(recent_date) + self.one_day_in_seconds:
                    # The most recent time was in more than one day.

                    # We return the current time.
                    return int(PyFunceble.time())

                # The most recent time was in less than one day.

                if int(PyFunceble.time()) < int(recent_date) + self.days_in_seconds:
                    # The most recent time was in less than the expected number of day for
                    # retesting.

                    # We return the most recent data.
                    return int(recent_date)

        # The database subsystem is not activated.

        # We return the current time.
        return int(PyFunceble.time())

    def add(self):
        """
        Save the current :code.`PyFunceble.CONFIGURATION['to_test']`
        into the current timestamp.
        """

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

            # We get the timestamp to use as index.
            timestamp = str(self._timestamp())

            if (
                "inactive_db" in PyFunceble.INTERN
                and PyFunceble.INTERN["file_to_test"]
                in PyFunceble.INTERN["inactive_db"]
            ):
                # * The file path is not into the database.

                if (
                    timestamp
                    in PyFunceble.INTERN["inactive_db"][
                        PyFunceble.INTERN["file_to_test"]
                    ]
                ):
                    # The timetamp is already into the database related to the file we
                    # are testing.

                    if (
                        PyFunceble.INTERN["to_test"]
                        not in PyFunceble.INTERN["inactive_db"][
                            PyFunceble.INTERN["file_to_test"]
                        ][timestamp]
                    ):
                        # The currently tested element is not into the database related
                        # to the file we are testing.

                        # We append the currently tested element into the database.
                        PyFunceble.INTERN["inactive_db"][
                            PyFunceble.INTERN["file_to_test"]
                        ][timestamp].append(PyFunceble.INTERN["to_test"])
                else:
                    # The timetamp is not into the database related to the file we
                    # are testing.

                    # We append the index and the database element into the databse
                    # related to the file we are testing.
                    PyFunceble.INTERN["inactive_db"][
                        PyFunceble.INTERN["file_to_test"]
                    ].update({timestamp: [PyFunceble.INTERN["to_test"]]})

                if (
                    "to_test"
                    in PyFunceble.INTERN["inactive_db"][
                        PyFunceble.INTERN["file_to_test"]
                    ]
                    and PyFunceble.INTERN["to_test"]
                    in PyFunceble.INTERN["inactive_db"][
                        PyFunceble.INTERN["file_to_test"]
                    ]["to_test"]
                ):
                    # * The `to_test` index is into the database related to the file we
                    #   are testing.
                    # and
                    # * The element we are testing is into the `to_test` index related to
                    #   the file we are testing.

                    # We remove the element from the list of element to test.
                    PyFunceble.INTERN["inactive_db"][PyFunceble.INTERN["file_to_test"]][
                        "to_test"
                    ].remove(PyFunceble.INTERN["to_test"])
            else:
                # The file path is not into the database.

                # We initiate the file path and its content into the database.
                PyFunceble.INTERN["inactive_db"] = {
                    PyFunceble.INTERN["file_to_test"]: {
                        timestamp: [PyFunceble.INTERN["to_test"]]
                    }
                }

            # And we save the data into the database.
            self._backup()

    def remove(self):
        """
        Remove all occurence of :code:`PyFunceble.CONFIGURATION['to_test']`
        from the database.
        """

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

            if PyFunceble.INTERN["file_to_test"] in PyFunceble.INTERN["inactive_db"]:
                #  The file path is into the database.

                for data in PyFunceble.INTERN["inactive_db"][
                    PyFunceble.INTERN["file_to_test"]
                ]:
                    # We loop through the index of the file database.

                    if (
                        PyFunceble.INTERN["to_test"]
                        in PyFunceble.INTERN["inactive_db"][
                            PyFunceble.INTERN["file_to_test"]
                        ][data]
                    ):
                        # The currently tested element into the currently read index.

                        # We remove the currently tested element from the read index.
                        PyFunceble.INTERN["inactive_db"][
                            PyFunceble.INTERN["file_to_test"]
                        ][data].remove(PyFunceble.INTERN["to_test"])

            # And we save the data into the database.
            self._backup()

    @classmethod
    def content(cls):
        """
        Get the content of the database.

        :return: The content of the database.
        :rtype: list
        """

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

        if (
            PyFunceble.CONFIGURATION["inactive_database"]
            and PyFunceble.INTERN["inactive_db"]
        ):
            # * The database subsystem is activated.
            # and
            # * The database is not empty.

            for key in PyFunceble.INTERN["inactive_db"][
                PyFunceble.INTERN["file_to_test"]
            ]:
                # We loop through the index of the current file database.

                if key == "to_test":
                    # The current key is `to_test`.

                    # We continue to the next element.
                    continue

                # We extend the result with the content of the currently read index.
                result.extend(
                    PyFunceble.INTERN["inactive_db"][PyFunceble.INTERN["file_to_test"]][
                        key
                    ]
                )

        # We return the content of the database.
        return result

    @classmethod
    def is_present(cls):
        """
        Check if the currently tested element is into the database.
        """

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

            if PyFunceble.INTERN["to_test"] in PyFunceble.INTERN[
                "flatten_inactive_db"
            ] or (
                PyFunceble.INTERN["file_to_test"] in PyFunceble.INTERN["inactive_db"]
                and PyFunceble.INTERN["inactive_db"][PyFunceble.INTERN["file_to_test"]]
                and "to_test"
                in PyFunceble.INTERN["inactive_db"][PyFunceble.INTERN["file_to_test"]]
                and PyFunceble.INTERN["to_test"]
                in PyFunceble.INTERN["inactive_db"][PyFunceble.INTERN["file_to_test"]][
                    "to_test"
                ]
            ):
                return True

        return False

class Whois:
    """
    Logic behind the whois database. Indeed, the idea is to implement #2.

    :param expiration_date: The extracted expiration date.
    :type expiration_date: str
    """

    def __init__(self, expiration_date=None):
        if self._authorization():
            # We are authorized to run this submodule.

            # We get the extracted expiration date.
            self.expiration_date = expiration_date

            if self.expiration_date:
                # We get the epoch of the expiration date.
                self.epoch = int(
                    PyFunceble.mktime(
                        PyFunceble.strptime(self.expiration_date, "%d-%b-%Y")
                    )
                )

            if (
                "file_to_test" in PyFunceble.INTERN
                and PyFunceble.INTERN["file_to_test"]
            ):
                # The file path was given previously.
                PyFunceble.INTERN["file_to_test"] = PyFunceble.INTERN["file_to_test"]
            else:
                # The file path was not given previously.

                # We set a dummy index.
                PyFunceble.INTERN["file_to_test"] = "single_testing"

            # We set the path to the whois database file.
            self.whois_db_path = (
                PyFunceble.CURRENT_DIRECTORY
                + PyFunceble.OUTPUTS["default_files"]["whois_db"]
            )

            if "to_test" in PyFunceble.INTERN and PyFunceble.INTERN["to_test"]:
                # We are testing something.

                # We set a variable which will save the actual element we are working with.
                PyFunceble.INTERN["to_test"] = PyFunceble.INTERN["to_test"]

            # We try to retrieve the information from the database file.
            self._retrieve()

    @classmethod
    def _authorization(cls):
        """
        Check if we are authorized to work with our database.
        """

        if (
            not PyFunceble.CONFIGURATION["no_whois"]
            and PyFunceble.CONFIGURATION["whois_database"]
        ):
            # * The usage of whois lookup is activated.
            # and
            # * The usage of the whois database is activated.

            # We return True, we are authorized to work.
            return True

        # * The usage of whois lookup is not activated.
        # or
        # * The usage of the whois database is not activated.

        # We return False, we are not authorized to work.
        return False

    def _retrieve(self):
        """
        Retrieve the data from the database.
        """

        if self._authorization() and "whois_db" not in PyFunceble.INTERN:
            # The usage of the whois database is activated.

            if PyFunceble.path.isfile(self.whois_db_path):
                # The database file exist.

                # We merge our current database into already initiated one.
                PyFunceble.INTERN["whois_db"] = Dict().from_json(
                    File(self.whois_db_path).read()
                )
            else:
                # The database file does not exist.

                # We initiate an empty database.
                PyFunceble.INTERN["whois_db"] = {}

    def _backup(self):
        """
        Backup the database into its file.
        """

        if self._authorization():
            # We are authorized to work.

            # We backup the current state of the datbase.
            Dict(PyFunceble.INTERN["whois_db"]).to_json(self.whois_db_path)

    def is_in_database(self):
        """
        Check if the element is into the database.
        """

        if (
            self._authorization()
            and PyFunceble.INTERN["file_to_test"] in PyFunceble.INTERN["whois_db"]
            and PyFunceble.INTERN["to_test"]
            in PyFunceble.INTERN["whois_db"][PyFunceble.INTERN["file_to_test"]]
        ):
            # * We are authorized to work.
            # and
            # * The given file path exist in the database.
            # and
            # * The element we are testing is in the database related to the
            # given file path.

            # We return True, the element we are testing is into the database.
            return True

        # * We are not authorized to work.
        # or
        # * The given file path does not exist in the database.
        # or
        # * The element we are testing is not in the database related to the
        # given file path.

        # We return False,the element we are testing is not into the database.
        return False

    def is_time_older(self):
        """
        Check if the current time is older than the one in the database.
        """

        if (
            self._authorization()
            and self.is_in_database()
            and int(
                PyFunceble.INTERN["whois_db"][PyFunceble.INTERN["file_to_test"]][
                    PyFunceble.INTERN["to_test"]
                ]["epoch"]
            )
            < int(PyFunceble.time())
        ):
            # * We are authorized to work.
            # and
            # * The element we are testing is in the database.
            # and
            # * The epoch of the expiration date is less than our current epoch.

            # The expiration date is in the past, we return True.
            return True

        # The expiration date is in the future, we return False.
        return False

    def get_expiration_date(self):
        """
        Get the expiration date from the database.

        :return: The expiration date from the database.
        :rtype: str|None
        """

        if self._authorization() and self.is_in_database() and not self.is_time_older():
            # * We are authorized to work.
            # and
            # * The element we are testing is in the database.
            # and
            # * The expiration date is in the future.

            # We get the expiration date from the database.
            result = PyFunceble.INTERN["whois_db"][PyFunceble.INTERN["file_to_test"]][
                PyFunceble.INTERN["to_test"]
            ]["expiration_date"]

            if result:
                # The expiration date from the database is not empty nor
                # equal to None.

                # We return it.
                return result

        # We return None, there is no data to work with.
        return None

    def add(self):
        """
        Add the currently tested element into the database.
        """

        if self._authorization():
            # We are authorized to work.

            if self.epoch < int(PyFunceble.time()):
                state = "past"
            else:
                state = "future"

            if self.is_in_database():
                # The element we are working with is in the database.

                if (
                    str(self.epoch)
                    != PyFunceble.INTERN["whois_db"][PyFunceble.INTERN["file_to_test"]][
                        PyFunceble.INTERN["to_test"]
                    ]["epoch"]
                ):
                    # The given epoch is diffent from the one saved.

                    # We update it.
                    PyFunceble.INTERN["whois_db"][PyFunceble.INTERN["file_to_test"]][
                        PyFunceble.INTERN["to_test"]
                    ].update(
                        {
                            "epoch": str(self.epoch),
                            "state": state,
                            "expiration_date": self.expiration_date,
                        }
                    )

                elif self.is_time_older():
                    # The expiration date from the database is in the past.

                    if (
                        PyFunceble.INTERN["whois_db"][
                            PyFunceble.INTERN["file_to_test"]
                        ][PyFunceble.INTERN["to_test"]]["state"]
                        != "past"
                    ):  # pragma: no cover
                        # The state of the element in the datbase is not
                        # equal to `past`.

                        # We update it to `past`.
                        PyFunceble.INTERN["whois_db"][
                            PyFunceble.INTERN["file_to_test"]
                        ][PyFunceble.INTERN["to_test"]].update({"state": "past"})
                elif (
                    PyFunceble.INTERN["whois_db"][PyFunceble.INTERN["file_to_test"]][
                        PyFunceble.INTERN["to_test"]
                    ]["state"]
                    != "future"
                ):
                    # * The expiration date from the database is in the future.
                    # and
                    # * The state of the element in the database is not
                    # equal to `future`.

                    # We update it to `future`.
                    PyFunceble.INTERN["whois_db"][PyFunceble.INTERN["file_to_test"]][
                        PyFunceble.INTERN["to_test"]
                    ].update({"state": "future"})
            else:
                # The element we are working with is not in the database.

                if (
                    not PyFunceble.INTERN["file_to_test"]
                    in PyFunceble.INTERN["whois_db"]
                ):
                    # The file path is not in the database.

                    # We initiate it.
                    PyFunceble.INTERN["whois_db"][
                        PyFunceble.INTERN["file_to_test"]
                    ] = {}

                # We create the first dataset.
                PyFunceble.INTERN["whois_db"][PyFunceble.INTERN["file_to_test"]].update(
                    {
                        PyFunceble.INTERN["to_test"]: {
                            "epoch": str(self.epoch),
                            "state": state,
                            "expiration_date": self.expiration_date,
                        }
                    }
                )

            # We do a safety backup of our database.
            self._backup()


#!/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 create the autosave 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

from sys import stdout as sys_stdout

import PyFunceble
from PyFunceble.helpers import Command
from PyFunceble.percentage import Percentage

class AutoSave:  # pragma: no cover  pylint: disable=too-few-public-methods
    """
    Provide the autosave logic.

    :param is_last_domain:
        Tell this subsystem if we are at the very end of the file testing.
    :type is_last_domain: bool

    :param is_bypass:
        Tell this subsystem if we are in bypassing mode.
    :type is_bypass: bool
    """

    def __init__(self, is_last_domain=False, is_bypass=False):
        if PyFunceble.CONFIGURATION["travis"]:
            self.last = is_last_domain
            self.bypass = is_bypass
            self._travis()

    @classmethod
    def travis_permissions(cls):
        """
        Set permissions in order to avoid issues before commiting.
        """

        if PyFunceble.CONFIGURATION["travis"]:
            try:
                build_dir = PyFunceble.environ["TRAVIS_BUILD_DIR"]
                commands = [
                    "sudo chown -R travis:travis %s" % (build_dir),
                    "sudo chgrp -R travis %s" % (build_dir),
                    "sudo chmod -R g+rwX %s" % (build_dir),
                    "sudo chmod 777 -Rf %s.git"
                    % (build_dir + PyFunceble.directory_separator),
                    r"sudo find %s -type d -exec chmod g+x '{}' \;" % (build_dir),
                ]

                for command in commands:
                    Command(command).execute()

                if Command("git config core.sharedRepository").execute() == "":
                    Command("git config core.sharedRepository group").execute()
            except KeyError:
                pass

    def _travis(self):
        """
        Logic behind autosave under Travis CI.
        """

        if PyFunceble.CONFIGURATION["travis"]:
            try:
                _ = PyFunceble.environ["TRAVIS_BUILD_DIR"]
                time_autorisation = False

                try:
                    time_autorisation = int(PyFunceble.time()) >= int(
                        PyFunceble.INTERN["start"]
                    ) + (int(PyFunceble.CONFIGURATION["travis_autosave_minutes"]) * 60)
                except KeyError:
                    if self.last and not self.bypass:
                        raise Exception(
                            "Please review the way `ExecutionTime()` is called."
                        )

                if self.last or time_autorisation or self.bypass:
                    Percentage().log()
                    self.travis_permissions()

                    command = 'git add --all && git commit -a -m "%s"'

                    if self.last or self.bypass:
                        if PyFunceble.CONFIGURATION["command_before_end"]:
                            for line in Command(
                                PyFunceble.CONFIGURATION["command_before_end"]
                            ).run():
                                sys_stdout.write("{}\n".format(line))

                            self.travis_permissions()

                        message = (
                            PyFunceble.CONFIGURATION["travis_autosave_final_commit"]
                            + " [ci skip]"
                        )

                        Command(command % message).execute()
                    else:
                        if PyFunceble.CONFIGURATION["command"]:
                            for line in Command(
                                PyFunceble.CONFIGURATION["command"]
                            ).run():
                                sys_stdout.write("{}\n".format(line))

                            self.travis_permissions()

                        Command(
                            command % PyFunceble.CONFIGURATION["travis_autosave_commit"]
                        ).execute()

                    print(
                        Command(
                            "git push origin %s"
                            % PyFunceble.CONFIGURATION["travis_branch"]
                        ).execute()
                    )
                    exit(0)
            except KeyError:
                pass


#!/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 lookup 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

import PyFunceble
from PyFunceble.check import Check

class Lookup:
    """
    Can be used to NSLOOKUP or WHOIS lookup.
    """

    @classmethod
    def nslookup(cls):
        """
        Implementation of UNIX nslookup.
        """

        try:
            # We try to get the addresse information of the given domain or IP.

            if "current_test_data" in PyFunceble.INTERN:  # pragma: no cover
                # The end-user want more information whith his test.

                if not Check().is_ip_valid():
                    # The element we are testing is not an IP.

                    # We request the address informations.
                    request = PyFunceble.socket.getaddrinfo(
                        PyFunceble.INTERN["to_test"],
                        80,
                        0,
                        0,
                        PyFunceble.socket.IPPROTO_TCP,
                    )

                    for sequence in request:
                        # We loop through the sequence returned by the request.

                        # We append the NS informations into the nslookup index.
                        PyFunceble.INTERN["current_test_data"]["nslookup"].append(
                            sequence[-1][0]
                        )
                else:
                    # The element we are testing is an IP.
                    request = PyFunceble.socket.gethostbyaddr(
                        PyFunceble.INTERN["to_test"]
                    )

                    # We append the NS informations into the nslookup index.
                    PyFunceble.INTERN["current_test_data"]["nslookup"][
                        "hostname"
                    ] = request[0]
                    PyFunceble.INTERN["current_test_data"]["nslookup"][
                        "aliases"
                    ] = request[1]
                    PyFunceble.INTERN["current_test_data"]["nslookup"]["ips"] = request[
                        2
                    ]
            else:

                if not Check().is_ip_valid():
                    # The element we are testing is not an IP.
                    PyFunceble.socket.getaddrinfo(
                        PyFunceble.INTERN["to_test"],
                        80,
                        0,
                        0,
                        PyFunceble.socket.IPPROTO_TCP,
                    )
                else:
                    # The element we are testing is an IP.
                    PyFunceble.socket.gethostbyaddr(PyFunceble.INTERN["to_test"])

            # It was done successfuly, we return True.
            # Note: we don't need to read the addresses so we consider as successful
            # as long as there is no error.
            return True

        except (OSError, PyFunceble.socket.herror, PyFunceble.socket.gaierror):
            # One of the listed exception is matched.

            # It was done unsuccesfuly, we return False.
            return False

    @classmethod
    def whois(cls, whois_server, domain=None, timeout=None):  # pragma: no cover
        """
        Implementation of UNIX whois.

        :param whois_server: The WHOIS server to use to get the record.
        :type whois_server: str

        :param domain: The domain to get the whois record from.
        :type domain: str

        :param timeout: The timeout to apply to the request.
        :type timeout: int

        :return: The whois record from the given whois server, if exist.
        :rtype: str|None
        """

        if domain is None:
            # The domain is not given (localy).

            # We consider the domain as the domain or IP we are currently testing.
            domain = PyFunceble.INTERN["to_test"]

        if timeout is None:
            # The time is not given (localy).

            # We consider the timeout from the configuration as the timeout to use.
            timeout = PyFunceble.CONFIGURATION["seconds_before_http_timeout"]

        if whois_server:
            # A whois server is given.

            # We initiate a PyFunceble.socket.
            req = PyFunceble.socket.socket(
                PyFunceble.socket.AF_INET, PyFunceble.socket.SOCK_STREAM
            )

            if timeout % 3 == 0:
                # The timeout is modulo 3.

                # We report the timeout to our initiated PyFunceble.socket.
                req.settimeout(timeout)
            else:
                # The timeout is not modulo 3.

                # We report 3 seconds as the timeout to our initiated PyFunceble.socket.
                req.settimeout(3)

            try:
                # We try to connect to the whois server at the port 43.
                req.connect((whois_server, 43))
            except PyFunceble.socket.error:
                # We got an error.

                # We return None.
                return None

            # We send end encode the domain we want the data from.
            req.send((domain + "\r\n").encode())

            # We initiate a bytes variable which will save the response
            # from the server.
            response = b""

            while True:
                # We loop infinitly.
                try:
                    # We try to receive the data in a buffer of 4096 bytes.
                    data = req.recv(4096)
                except (PyFunceble.socket.timeout, ConnectionResetError):
                    # We got an error.

                    # We close the connection.
                    req.close()

                    # And we return None.
                    return None

                # Everything goes right.

                # We append data to the response we got.
                response += data

                if not data:
                    # The data is empty.

                    # We break the loop.
                    break

            # We close the connection.
            req.close()

            try:

                # We finally decode and return the response we got from the
                # server.
                return response.decode()

            except UnicodeDecodeError:
                # We got an encoding error.

                # We decode the response.
                # Note: Because we don't want to deal with other issue, we
                # decided to use `replace` in order to automatically replace
                # all non utf-8 encoded characters.
                return response.decode("utf-8", "replace")

        # The whois server is not given.

        # We return None.
        return None


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

::

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

This submodule will provide the interface for URL testing.

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

import PyFunceble
from PyFunceble.check import Check
from PyFunceble.http_code import HTTPCode
from PyFunceble.status import URLStatus

class URL:  # pylint: disable=too-few-public-methods
    """
    Manage everything around the URL testing.
    """

    @classmethod
    def get(cls):  # pragma: no cover
        """
        Execute the logic behind the URL handling.

        :return: The status of the URL.
        :rtype: str
        """

        if Check().is_url_valid() or PyFunceble.CONFIGURATION["local"]:
            # * The url is valid.
            # or
            # * We are testing in/for a local or private network.

            if "current_test_data" in PyFunceble.INTERN:
                PyFunceble.INTERN["current_test_data"]["url_syntax_validation"] = True

            # We initiate the HTTP status code.
            PyFunceble.INTERN.update({"http_code": HTTPCode().get()})

            # We initiate the list of active status code.
            active_list = []
            active_list.extend(PyFunceble.HTTP_CODE["list"]["potentially_up"])
            active_list.extend(PyFunceble.HTTP_CODE["list"]["up"])

            # We initiate the list of inactive status code.
            inactive_list = []
            inactive_list.extend(PyFunceble.HTTP_CODE["list"]["potentially_down"])
            inactive_list.append("*" * 3)

            if PyFunceble.INTERN["http_code"] in active_list:
                # The extracted HTTP status code is in the list of active list.

                # We handle and return the up status.
                return URLStatus(PyFunceble.STATUS["official"]["up"]).handle()

            if PyFunceble.INTERN["http_code"] in inactive_list:
                # The extracted HTTP status code is in the list of inactive list.

                # We handle and return the down status.
                return URLStatus(PyFunceble.STATUS["official"]["down"]).handle()

        # The extracted HTTP status code is not in the list of active nor invalid list.

        if "current_test_data" in PyFunceble.INTERN:
            # The end-user want more information whith his test.

            # We update the url_syntax_validation index.
            PyFunceble.INTERN["current_test_data"]["url_syntax_validation"] = False

        # We handle and return the invalid down status.
        return URLStatus(PyFunceble.STATUS["official"]["invalid"]).handle()


"""
The tool to check the availability or syntax of domains, IPv4 or URL.

::

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

PyFunceble is the little sister of Funceble (https://github.com/funilrys/funceble)
which was archived on 13th March 2018.

Its main objective is to provide the availability of domains, IPs and since recently
URL by generating an accurate result based on results from WHOIS, NSLOOKUP and
HTTP status codes.

PyFunceble is currently running actively and daily with the help of Travis CI under
60+ repositories. It is used to clean or test the availability of data which are
present in hosts files, list of IP, list of domains, block lists or even AdBlock
filter lists.

PyFunceble provides some useful features for continuous testing.

As an example, its auto-continue system coupled with its auto-save system allows
it to run nice and smoothly under Travis CI without even reaching Travis CI time
restriction. In the other side, its internal inactive database system
let :code:`INACTIVE` and :code:`INVALID` caught domains, IPs or URLs being
automatically retested over time on next run.

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.
"""

from re import compile as comp
from unittest import TestLoader

from setuptools import setup

def _test_suite():
    """
    This function will discover and run all the tests.
    """

    test_loader = TestLoader()
    test_suite = test_loader.discover("tests", pattern="test_*.py")
    return test_suite

def _get_requirements():
    """
    This function extract all requirements from requirements.txt.
    """

    with open("requirements.txt") as file:
        requirements = file.read().splitlines()

    return requirements

def _get_version():
    """
    This function will extract the version from PyFunceble/__init__.py
    """

    to_match = comp(r'VERSION\s=\s"(.*)"\n')
    extracted = to_match.findall(
        open("PyFunceble/__init__.py", encoding="utf-8").read()
    )[0]

    return ".".join([x for x in extracted.split(".") if x.isdigit()])

def _get_long_description():  # pragma: no cover
    """
    This function return the long description.
    """

    return open("README.rst", encoding="utf-8").read()

if __name__ == "__main__":
    setup(
        name="PyFunceble",
        version=_get_version(),
        install_requires=_get_requirements(),
        description="The tool to check the availability or syntax of domains, IPv4 or URL.",
        long_description=_get_long_description(),
        author="funilrys",
        author_email="contact@funilrys.com",
        license="https://git.io/vh1mP",
        url="https://github.com/funilrys/PyFunceble",
        platforms=["any"],
        packages=["PyFunceble"],
        keywords=[
            "Python",
            "domain",
            "IP",
            "availability",
            "syntax",
            "syntax-checker",
            "PyFunceble",
            "WHOIS",
            "nslookup",
        ],
        classifiers=[
            "Environment :: Console",
            "Topic :: Internet",
            "Development Status :: 5 - Production/Stable",
            "Intended Audience :: Developers",
            "Programming Language :: Python",
            "Programming Language :: Python :: 3",
            "License :: OSI Approved :: MIT License",
        ],
        test_suite="setup._test_suite",
        entry_points={
            "console_scripts": [
                "PyFunceble=PyFunceble:_command_line",
                "pyfunceble=PyFunceble:_command_line",
            ]
        },
    )


#!/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 referer extraction 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 PyFunceble
from PyFunceble.logs import Logs

class Referer:  # pragma: no cover pylint: disable=too-few-public-methods
    """
    Get the WHOIS server (referer) of the current domain extension according to
    the IANA database.
    """

    def __init__(self):
        # Note: A URL testing or an IP testing does not come around
        # here. So there is no need to be scared by the following.

        try:
            # We get the extension of the currently tested element.
            # We basically get everything after the last point.
            self.domain_extension = PyFunceble.INTERN["to_test"][
                PyFunceble.INTERN["to_test"].rindex(".") + 1 :
            ]

            if not self.domain_extension and PyFunceble.INTERN["to_test"].endswith("."):
                self.domain_extension = [
                    x for x in PyFunceble.INTERN["to_test"].split(".") if x
                ][-1]
        except ValueError:
            # There was not point, so no extension to work with.
            self.domain_extension = None

        # We create a list of ignored extension.
        # Note: We need the following because those extension does
        # not have a centralized whois server (yet).
        self.ignored_extension = [
            "ad",
            "al",
            "an",
            "ao",
            "aq",
            "arpa",
            "az",
            "ba",
            "bb",
            "bd",
            "bf",
            "bh",
            "bl",
            "boots",
            "bq",
            "bs",
            "bt",
            "bv",
            "cg",
            "chloe",
            "ck",
            "cu",
            "cv",
            "cw",
            "cy",
            "dj",
            "doosan",
            "eg",
            "eh",
            "er",
            "et",
            "fk",
            "flsmidth",
            "fm",
            "gb",
            "gm",
            "gn",
            "goodhands",
            "gp",
            "gr",
            "gt",
            "gu",
            "gw",
            "htc",
            "iinet",
            "jm",
            "jo",
            "kh",
            "km",
            "kp",
            "lb",
            "lr",
            "mc",
            "meo",
            "mf",
            "mh",
            "mil",
            "mm",
            "mt",
            "mv",
            "mw",
            "ne",
            "ni",
            "np",
            "nr",
            "pa",
            "pamperedchef",
            "panerai",
            "pg",
            "ph",
            "pk",
            "pn",
            "py",
            "sd",
            "sj",
            "spiegel",
            "sr",
            "ss",
            "sv",
            "sz",
            "telecity",
            "tj",
            "tp",
            "tt",
            "um",
            "va",
            "vi",
            "vista",
            "vn",
            "xn--0zwm56d",
            "xn--11b5bs3a9aj6g",
            "xn--54b7fta0cc",
            "xn--80akhbyknj4f",
            "xn--9t4b11yi5a",
            "xn--deba0ad",
            "xn--g6w251d",
            "xn--hgbk6aj7f53bba",
            "xn--hlcj6aya9esc7a",
            "xn--hlcj6aya9esc7a",
            "xn--jxalpdlp",
            "xn--kgbechtv",
            "xn--l1acc",
            "xn--mgbai9azgqp6j",
            "xn--mgbayh7gpa",
            "xn--mgbc0a9azcg",
            "xn--mgbpl2fh",
            "xn--pgbs0dh",
            "xn--qxam",
            "xn--zckzah",
            "xperia",
            "ye",
            "zw",
        ]

    def get(self):
        """
        Return the referer aka the WHOIS server of the current domain extension.
        """

        if not PyFunceble.CONFIGURATION["local"]:
            # We are not running a test in a local network.

            if self.domain_extension not in self.ignored_extension:
                # The extension of the domain we are testing is not into
                # the list of ignored extensions.

                # We set the referer to None as we do not have any.
                referer = None

                if self.domain_extension in PyFunceble.INTERN["iana_db"]:
                    # The domain extension is in the iana database.

                    if not PyFunceble.CONFIGURATION["no_whois"]:
                        # We are authorized to use WHOIS for the test result.

                        # We get the referer from the database.
                        referer = PyFunceble.INTERN["iana_db"][self.domain_extension]

                        if not referer:
                            # The referer is not filled.

                            # We log the case of the current extension.
                            Logs().referer_not_found(self.domain_extension)

                            # And we handle and return None status.
                            return None

                        # The referer is into the database.

                        # We return the extracted referer.
                        return referer

                    # We are not authorized to use WHOIS for the test result.

                    # We return None.
                    return None

                # The domain extension is not in the iana database.

                # We return False, it is an invalid domain.
                return False

            # The extension of the domain we are testing is not into
            # the list of ignored extensions.

            # We return None, the domain does not have a whois server.
            return None

        # We are running a test in a local network.

        # We return None.
        return None


import inspect
import logging
import os.path

from sea import exceptions, utils
from sea.config import Config, ConfigAttribute
from sea.datatypes import ImmutableDict, ConstantsObject

class BaseApp:
    """The BaseApp object implements grpc application

    :param root_path: the root path
    :param env: the env
    """
    config_class = Config
    debug = ConfigAttribute('DEBUG')
    testing = ConfigAttribute('TESTING')
    tz = ConfigAttribute('TIMEZONE')
    default_config = ImmutableDict({
        'DEBUG': False,
        'TESTING': False,
        'TIMEZONE': 'UTC',
        'GRPC_WORKERS': 3,
        'GRPC_HOST': '[::]',
        'GRPC_PORT': 6000,
        'GRPC_LOG_LEVEL': 'WARNING',
        'GRPC_LOG_HANDLER': logging.StreamHandler(),
        'GRPC_LOG_FORMAT': '[%(asctime)s %(levelname)s in %(module)s] %(message)s',  # NOQA
        'GRPC_GRACE': 5,
        'PROMETHEUS_SCRAPE': False,
        'PROMETHEUS_PORT': 9091,
        'MIDDLEWARES': [
            'sea.middleware.RpcErrorMiddleware'
        ]
    })

    def __init__(self, root_path, env):
        if not os.path.isabs(root_path):
            root_path = os.path.abspath(root_path)
        self.root_path = root_path
        self.name = os.path.basename(root_path)
        self.env = env
        self.config = self.config_class(root_path, self.default_config)
        self._servicers = {}
        self._extensions = {}
        self._middlewares = []

    @utils.cached_property
    def logger(self):
        logger = logging.getLogger('sea.app')
        if self.debug and logger.level == logging.NOTSET:
            logger.setLevel(logging.DEBUG)
        if not utils.logger_has_level_handler(logger):
            h = logging.StreamHandler()
            h.setFormatter(logging.Formatter('%(message)s'))
            logger.addHandler(h)
        return logger

    @utils.cached_property
    def servicers(self):
        rv = ConstantsObject(self._servicers)
        del self._servicers
        return rv

    @utils.cached_property
    def extensions(self):
        rv = ConstantsObject(self._extensions)
        del self._extensions
        return rv

    @utils.cached_property
    def middlewares(self):
        rv = tuple(self._middlewares)
        del self._middlewares
        return rv

    def _register_servicer(self, servicer):
        """register serviser

        :param servicer: servicer
        """
        name = servicer.__name__
        if name in self._servicers:
            raise exceptions.ConfigException(
                'servicer duplicated: {}'.format(name))
        add_func = self._get_servicer_add_func(servicer)
        self._servicers[name] = (add_func, servicer)

    def _get_servicer_add_func(self, servicer):
        for b in servicer.__bases__:
            if b.__name__.endswith('Servicer'):
                m = inspect.getmodule(b)
                return getattr(m, 'add_{}_to_server'.format(b.__name__))

    def _register_extension(self, name, ext):
        """register extension

        :param name: extension name
        :param ext: extension object
        """
        ext.init_app(self)
        if name in self._extensions:
            raise exceptions.ConfigException(
                'extension duplicated: {}'.format(name))
        self._extensions[name] = ext

    def load_middlewares(self):
        mids = ['sea.middleware.GuardMiddleware'] + \
            self.config.get('MIDDLEWARES')
        for mn in mids:
            m = utils.import_string(mn)
            self._middlewares.insert(0, m)
        return self.middlewares

    def load_extensions_in_module(self, module):
        def is_ext(ins):
            return not inspect.isclass(ins) and hasattr(ins, 'init_app')
        for n, ext in inspect.getmembers(module, is_ext):
            self._register_extension(n, ext)
        return self.extensions

    def load_servicers_in_module(self, module):
        for _, _servicer in inspect.getmembers(module, inspect.isclass):
            if _servicer.__name__.endswith('Servicer'):
                self._register_servicer(_servicer)
        return self.servicers

    def ready(self):
        pass


import copy

def _default_cls_attr(name, type_, cls_value):
    # Proxy uses properties to forward the standard
    # class attributes __module__, __name__ and __doc__ to the real
    # object, but these needs to be a string when accessed from
    # the Proxy class directly.  This is a hack to make that work.
    # -- See Issue #1087.

    def __new__(cls, getter):
        instance = type_.__new__(cls, cls_value)
        instance.__getter = getter
        return instance

    def __get__(self, obj, cls=None):
        return self.__getter(obj) if obj is not None else self

    return type(name, (type_,), {
        '__new__': __new__, '__get__': __get__,
    })

class Proxy(object):
    """Proxy to another object."""

    # Code stolen from werkzeug.local.Proxy.
    __slots__ = ('__local', '__args', '__kwargs', '__dict__')

    def __init__(self, local, args=None, kwargs=None):
        object.__setattr__(self, '_Proxy__local', local)
        object.__setattr__(self, '_Proxy__args', args or ())
        object.__setattr__(self, '_Proxy__kwargs', kwargs or {})

    @_default_cls_attr('name', str, __name__)
    def __name__(self):
        return self._get_current_object().__name__

    @_default_cls_attr('qualname', str, __name__)
    def __qualname__(self):
        return self._get_current_object().__qualname__

    @_default_cls_attr('module', str, __name__)
    def __module__(self):
        return self._get_current_object().__module__

    @_default_cls_attr('doc', str, __doc__)
    def __doc__(self):
        return self._get_current_object().__doc__

    def _get_class(self):
        return self._get_current_object().__class__

    @property
    def __class__(self):
        return self._get_class()

    def _get_current_object(self):
        """Get current object.
        This is useful if you want the real
        object behind the proxy at a time for performance reasons or because
        you want to pass the object into a different context.
        """
        loc = object.__getattribute__(self, '_Proxy__local')
        if not hasattr(loc, '__release_local__'):
            return loc(*self.__args, **self.__kwargs)
        try:  # pragma: no cover
            # not sure what this is about
            return getattr(loc, self.__name__)
        except AttributeError:  # pragma: no cover
            raise RuntimeError('no object bound to {0.__name__}'.format(self))

    @property
    def __dict__(self):
        try:
            return self._get_current_object().__dict__
        except RuntimeError:  # pragma: no cover
            raise AttributeError('__dict__')

    def __repr__(self):
        try:
            obj = self._get_current_object()
        except RuntimeError:  # pragma: no cover
            return '<{0} unbound>'.format(self.__class__.__name__)
        return repr(obj)

    def __bool__(self):
        try:
            return bool(self._get_current_object())
        except RuntimeError:  # pragma: no cover
            return False

    def __dir__(self):
        try:
            return dir(self._get_current_object())
        except RuntimeError:  # pragma: no cover
            return []

    def __getattr__(self, name):
        if name == '__members__':
            return dir(self._get_current_object())
        return getattr(self._get_current_object(), name)

    def __setitem__(self, key, value):
        self._get_current_object()[key] = value

    def __delitem__(self, key):
        del self._get_current_object()[key]

    def __setattr__(self, name, value):
        setattr(self._get_current_object(), name, value)

    def __delattr__(self, name):
        delattr(self._get_current_object(), name)

    def __str__(self):
        return str(self._get_current_object())

    def __lt__(self, other):
        return self._get_current_object() < other

    def __le__(self, other):
        return self._get_current_object() <= other

    def __eq__(self, other):
        return self._get_current_object() == other

    def __ne__(self, other):
        return self._get_current_object() != other

    def __gt__(self, other):
        return self._get_current_object() > other

    def __ge__(self, other):
        return self._get_current_object() >= other

    def __hash__(self):
        return hash(self._get_current_object())

    def __call__(self, *a, **kw):
        return self._get_current_object()(*a, **kw)

    def __len__(self):
        return len(self._get_current_object())

    def __getitem__(self, i):
        return self._get_current_object()[i]

    def __iter__(self):
        return iter(self._get_current_object())

    def __contains__(self, i):
        return i in self._get_current_object()

    def __add__(self, other):
        return self._get_current_object() + other

    def __radd__(self, other):
        return other + self._get_current_object()

    def __sub__(self, other):
        return self._get_current_object() - other

    def __rsub__(self, other):
        return other - self._get_current_object()

    def __mul__(self, other):
        return self._get_current_object() * other

    def __rmul__(self, other):
        return other * self._get_current_object()

    def __floordiv__(self, other):
        return self._get_current_object() // other

    def __mod__(self, other):
        return self._get_current_object() % other

    def __rmod__(self, other):
        return other % self._get_current_object()

    def __divmod__(self, other):
        return self._get_current_object().__divmod__(other)

    def __rdivmod__(self, other):
        return self._get_current_object().__rdivmod__(other)

    def __pow__(self, other):
        return self._get_current_object() ** other

    def __lshift__(self, other):
        return self._get_current_object() << other

    def __rshift__(self, other):
        return self._get_current_object() >> other

    def __and__(self, other):
        return self._get_current_object() & other

    def __xor__(self, other):
        return self._get_current_object() ^ other

    def __or__(self, other):
        return self._get_current_object() | other

    def __div__(self, other):
        return self._get_current_object().__div__(other)

    def __rdiv__(self, other):
        return other / self._get_current_object()

    def __rfloordiv__(self, other):
        return other // self._get_current_object()

    def __truediv__(self, other):
        return self._get_current_object().__truediv__(other)

    def __neg__(self):
        return -(self._get_current_object())

    def __pos__(self):
        return +(self._get_current_object())

    def __abs__(self):
        return abs(self._get_current_object())

    def __invert__(self):
        return ~(self._get_current_object())

    def __complex__(self):
        return complex(self._get_current_object())

    def __int__(self):
        return int(self._get_current_object())

    def __float__(self):
        return float(self._get_current_object())

    def __oct__(self):
        return oct(self._get_current_object())

    def __hex__(self):
        return hex(self._get_current_object())

    def __index__(self):
        return self._get_current_object().__index__()

    def __coerce__(self, other):
        return self._get_current_object().__coerce__(other)  # pragma: no cover

    def __enter__(self):
        return self._get_current_object().__enter__()  # pragma: no cover

    def __exit__(self, *a, **kw):
        return self._get_current_object().__exit__(*a, **kw)  # pragma: no cover # noqa

    def __reduce__(self):
        return self._get_current_object().__reduce__()  # pragma: no cover

    def __copy__(self):
        return copy.copy(self._get_current_object())

    def __deepcopy__(self, memo):
        return copy.deepcopy(self._get_current_object(), memo)

    __rtruediv__ = __rdiv__