"""Module provider for Easyname DNS"""

import json
import logging
from argparse import ArgumentParser
from typing import List

from bs4 import BeautifulSoup, Tag
from requests import Response, Session

from lexicon.interfaces import Provider as BaseProvider

LOGGER = logging.getLogger(__name__)


class Provider(BaseProvider):
    """
    easyname provider
    """

    URLS = {
        "login": "https://my.easyname.com/en/login",
        "login_endpoint": "https://my.easyname.com/en/authentication-api/login",
        "domain_list": "https://my.easyname.com/domains/",
        "dashboard": "https://my.easyname.com/en/dashboard",
        "overview": "https://my.easyname.com/hosting/view-user.php",
        "dns": "https://my.easyname.com/en/domain/dns/index/domain/{}/",
        "dns_create_entry": "https://my.easyname.com/en/domain/dns/create/domain/{}",
        "dns_update_entry": "https://my.easyname.com/en/domain/dns/edit/domain/{}/id/{}",
        "dns_delete_entry": "https://my.easyname.com/en/domain/dns/delete/domain/{}/id/{}",
        "dns_delete_entry_confirm": "https://my.easyname.com/en/domain/dns/delete/domain/{}/id/{}/confirm/1",
    }

    @staticmethod
    def get_nameservers() -> List[str]:
        return ["easyname.eu"]

    @staticmethod
    def configure_parser(parser: ArgumentParser) -> None:
        parser.description = """A provider for Easyname DNS."""
        parser.add_argument(
            "--auth-username", help="Specify username used to authenticate"
        )
        parser.add_argument(
            "--auth-password",
            help="Specify password used to authenticate",
        )

    def __init__(self, config):
        super(Provider, self).__init__(config)
        self.session = Session()
        self.domain_id = None
        self._records = None

    def _get_csrf_token(self):
        """Return the CSRF Token of easyname login form."""
        home_response = self.session.get(self.URLS["login"])
        self._log("Home", home_response)
        assert home_response.status_code == 200, "Could not load Easyname login page."

        csrf_token_field = home_response.cookies["CSRF-TOKEN"]

        assert csrf_token_field is not None, "Could not find login token."
        return csrf_token_field

    def _login(self, csrf_token):
        """Attempt to login session on easyname."""
        payload = {
            "emailAddress": self._get_provider_option("auth_username"),
            "password": self._get_provider_option("auth_password"),
        }
        headers = self.session.headers
        headers["X-CSRF-TOKEN"] = csrf_token

        login_response = self.session.post(
            self.URLS["login_endpoint"],
            data=json.dumps(payload),
            headers=headers,
            allow_redirects=True,
        )
        self._log("Login", login_response)
        json_response = json.loads(login_response.content)
        assert (
            login_response.status_code == 200
        ), "Could not login due to a network error."

        assert json_response["redirectUrl"] == self.URLS["dashboard"], (
            "Easyname login failed, bad EASYNAME_USER or EASYNAME_PASS.%s"
            % login_response.url
        )

    def authenticate(self):
        csrf_token = self._get_csrf_token()
        self._login(csrf_token)

        domain_text_element = self._get_domain_text_of_authoritative_zone()
        self.domain_id = self._get_domain_id(domain_text_element)
        LOGGER.debug("Easyname domain ID: %s", self.domain_id)

        return True

    def cleanup(self) -> None:
        pass

    def create_record(self, rtype, name, content):
        return self._create_record_internal(rtype=rtype, name=name, content=content)

    def _create_record_internal(self, rtype, name, content, identifier=None):
        """
        Create a new DNS entry in the domain zone if it does not already exist.

        Args:
          rtype (str): The DNS type (e.g. A, TXT, MX, etc) of the new entry.
          name (str): The name of the new DNS entry, e.g the domain for which a
                      MX entry shall be valid.
          content (str): The content of the new DNS entry, e.g. the mail server
                         hostname for a MX entry.
          [identifier] (str): The easyname id of a DNS entry. Use to overwrite an
                    existing entry.

        Returns:
          bool: True if the record was created successfully, False otherwise.
        """
        name = self._relative_name(name) if name is not None else name
        LOGGER.debug("Creating record with name %s", name)
        if self._is_duplicate_record(rtype, name, content):
            return True

        data = self._get_post_data_to_create_dns_entry(rtype, name, content, identifier)
        if not identifier:
            LOGGER.debug("Create DNS data: %s", data)
            create_response = self.session.post(
                self.URLS["dns_create_entry"].format(self.domain_id), data=data
            )
            self._log("Create DNS entry", create_response)
        else:
            LOGGER.debug("Update DNS data: %s", data)
            update_response = self.session.post(
                self.URLS["dns_update_entry"].format(self.domain_id, identifier),
                data=data,
            )
            self._log("Update DNS entry", update_response)

        self._invalidate_records_cache()

        # Pull a list of records and check for ours
        was_success = len(self.list_records(rtype, name, content)) > 0
        if identifier != "" and was_success:
            msg = "Successfully updated record {}".format(name)
        elif was_success:
            msg = "Successfully added record {}".format(name)
        else:
            msg = "Failed to add/update record {}".format(name)
        LOGGER.info(msg, name)
        return was_success

    def delete_record(self, identifier=None, rtype=None, name=None, content=None):
        """
        Delete one or more DNS entries in the domain zone that match the given
        criteria.

        Args:
          [identifier] (str): An ID to match against DNS entry easyname IDs.
          [rtype] (str): A DNS rtype (e.g. A, TXT, MX, etc) to match against DNS
                      entry types.
          [name] (str): A name to match against DNS entry names.
          [content] (str): A content to match against a DNS entry contents.

        Returns:
          bool: True if the record(s) were deleted successfully, False
                otherwise.
        """
        record_ids = self._get_matching_dns_entry_ids(identifier, rtype, name, content)
        LOGGER.debug("Record IDs to delete: %s", record_ids)

        success = True
        for rec_id in record_ids:
            delete_response = self.session.get(
                self.URLS["dns_delete_entry"].format(self.domain_id, rec_id)
            )
            delete_response_confirm = self.session.post(
                self.URLS["dns_delete_entry_confirm"].format(self.domain_id, rec_id)
            )
            self._invalidate_records_cache()
            self._log("Delete DNS entry {rec_id}", delete_response)
            # success = success and delete_response_confirm.url == success_url
            success = "feedback-message--success" in delete_response_confirm.text

        return success

    def update_record(self, identifier, rtype=None, name=None, content=None):
        """
        Update a DNS entry identified by identifier or name in the domain zone.
        Any non given argument will leave the current value of the DNS entry.

        Args:
          identifier (str): The easyname id of the DNS entry to update.
          [rtype] (str): The DNS rtype (e.g. A, TXT, MX, etc) of the new entry.
          [name] (str): The name of the new DNS entry, e.g the domain for which
                        a MX entry shall be valid.
          [content] (str): The content of the new DNS entry, e.g. the mail
                           server hostname for a MX entry.

        Returns:
          bool: True if the record was updated successfully, False otherwise.

        Raises:
          AssertionError: When a request returns unexpected or unknown data.
        """
        if identifier is not None:
            identifier = int(identifier)
            records = self._list_records_internal(identifier=identifier)
        else:
            records = self._list_records_internal(name=name, rtype=rtype)
        LOGGER.debug("Records to update (%d): %s", len(records), records)
        assert records, "No record found to update"
        success = True

        for record in records:
            name = name if name is not None else record["name"]
            rtype = rtype if rtype is not None else record["type"]
            content = content if content is not None else record["content"]
            success = success and self._create_record_internal(
                rtype, name, content, record["id"]
            )
        return success

    def list_records(self, rtype=None, name=None, content=None):
        return self._list_records_internal(rtype=rtype, name=name, content=content)

    def _list_records_internal(
        self, rtype=None, name=None, content=None, identifier=None
    ):
        """
        Filter and list DNS entries of domain zone on Easyname.
        Easyname shows each entry in a HTML table row and each attribute on a
        table column.

        Args:
          [rtype] (str): Filter by DNS rtype (e.g. A, TXT, MX, etc)
          [name] (str): Filter by the name of the DNS entry, e.g the domain for
                      which a MX entry shall be valid.
          [content] (str): Filter by the content of the DNS entry, e.g. the
                           mail server hostname for a MX entry.
          [identifier] (str): Filter by the easyname id of the DNS entry.

        Returns:
          list: A list of DNS entries. A DNS entry is an object with DNS
                attribute names as keys (e.g. name, content, priority, etc)
                and additionally an id.

        Raises:
          AssertionError: When a request returns unexpected or unknown data.
        """
        name = self._full_name(name) if name is not None else name
        if self._records is None:
            records = []
            # skip the first record which contains the table header
            rows = self._get_dns_entry_trs()[1:]

            for row in rows:
                self._log("DNS list entry", row)
                try:
                    rec = {}
                    columns = row.find_all("td")
                    remove_span_from_name = columns[0].text.replace(
                        '<span style="color: var(--color-grey-500);">', ""
                    )
                    columns[0].replace_with(remove_span_from_name)
                    rec["name"] = (remove_span_from_name or "").strip()
                    rec["type"] = (columns[1].contents[1].text or "").strip()
                    rec["content"] = (columns[2].contents[1].string or "").strip()
                    rec["priority"] = (columns[3].contents[1].string or "").strip()
                    rec["ttl"] = (columns[4].contents[1].string or "").strip()
                    rec["id"] = ""
                    for a in columns[5].find_all(
                        "a", class_="button button--transparent"
                    ):
                        rec["id"] = int(a["href"].rsplit("/", 1)[-1])
                    if rec["priority"]:
                        rec["priority"] = int(rec["priority"])
                    if rec["ttl"]:
                        rec["ttl"] = int(rec["ttl"])
                except Exception as error:
                    errmsg = "Cannot parse DNS entry ({}).".format(error)
                    LOGGER.warning(errmsg)
                    raise AssertionError(errmsg)
                records.append(rec)
            self._records = records
        records = self._filter_records(self._records, rtype, name, content, identifier)
        LOGGER.debug("Final records (%d): %s", len(records), records)
        return records

    def _request(self, action="GET", url="/", data=None, query_params=None):
        pass

    def _invalidate_records_cache(self):
        """
        Invalidate DNS entries cache such that list_records will do a new
        request to retrieve DNS entries.
        """
        self._records = None

    def _get_post_data_to_create_dns_entry(self, rtype, name, content, identifier=None):
        """
        Build and return the post date that is needed to create a DNS entry.
        """
        is_update = identifier is not None
        record = None
        if is_update:
            records = self._list_records_internal(identifier=identifier)
            assert len(records) == 1, "ID is not unique or does not exist"
            record = records[0]
            LOGGER.debug("Create post data to update record: %s", record)

        data = {
            "id": str(identifier) if is_update else "",
            "name": name,
            "type": rtype,
            "content": content,
            "priority": str(record["priority"]) if is_update else "10",
            "ttl": str(record["ttl"]) if is_update else "360",
        }
        ttl = self._get_lexicon_option("ttl")
        if ttl and ttl > 360:
            data["ttl"] = str(ttl)

        prio = self._get_lexicon_option("priority")
        if prio and prio > 0:
            data["priority"] = str(prio)

        return data

    def _is_duplicate_record(self, rtype, name, content):
        """Check if DNS entry already exists."""
        records = self.list_records(rtype, name, content)
        is_duplicate = len(records) >= 1
        if is_duplicate:
            LOGGER.info("Duplicate record %s %s %s, NOOP", rtype, name, content)
        return is_duplicate

    def _get_matching_dns_entry_ids(
        self, identifier=None, rtype=None, name=None, content=None
    ):
        """Return a list of DNS entries that match the given criteria."""
        record_ids = []
        if not identifier:
            records = self.list_records(rtype, name, content)
            record_ids = [record["id"] for record in records]
        else:
            record_ids.append(identifier)
        return record_ids

    def _get_dns_entry_trs(self):
        """
        Return the TR elements holding the DNS entries.
        """
        dns_list_response = self.session.get(self.URLS["dns"].format(self.domain_id))
        self._log("DNS list", dns_list_response)
        assert dns_list_response.status_code == 200, "Could not load DNS entries."

        html = BeautifulSoup(dns_list_response.content, "html.parser")
        self._log("DNS list", html)
        dns_table = html.find("table")
        assert dns_table is not None, "Could not find DNS entry table"

        def _is_zone_tr(elm):
            return elm.name.lower() == "tr" and (elm.has_attr("class"))

        rows = dns_table.find_all(_is_zone_tr)
        assert rows is not None and rows, "Could not find any DNS entries"
        return rows

    def _filter_records(
        self, records, rtype=None, name=None, content=None, identifier=None
    ):
        """
        Filter dns entries based on type, name or content.
        """
        if not records:
            return []
        if identifier is not None:
            LOGGER.debug("Filtering %d records by id: %s", len(records), identifier)
            records = [record for record in records if record["id"] == identifier]
        if rtype is not None:
            LOGGER.debug("Filtering %d records by type: %s", len(records), rtype)
            records = [record for record in records if record["type"] == rtype]
        if name is not None:
            LOGGER.debug("Filtering %d records by name: %s", len(records), name)
            if name.endswith("."):
                name = name[:-1]
            if rtype.lower() != "txt":
                name = name.split(".")[0]
                records = [
                    record for record in records if name == record["name"].split(".")[0]
                ]
            else:
                records = [record for record in records if name == record["name"]]
        if content is not None:
            LOGGER.debug(
                "Filtering %d records by content: %s", len(records), content.lower()
            )
            records = [
                record
                for record in records
                if record["content"].lower() == content.lower()
            ]
        return records

    def _get_domain_text_of_authoritative_zone(self):
        """Get the authoritative name zone."""
        # We are logged in, so get the domain list
        zones_response = self.session.get(self.URLS["domain_list"])
        self._log("Zone", zones_response)
        assert (
            zones_response.status_code == 200
        ), "Could not retrieve domain list due to a network error."

        html = BeautifulSoup(zones_response.content, "html.parser")
        self._log("Zone", html)
        domain_table = html.find("table")
        assert domain_table is not None, "Could not find domain table"

        # (Sub)domains can either be managed in their own zones or by the
        # zones of their parent (sub)domains. Iterate over all subdomains
        # (starting with the deepest one) and see if there is an own zone
        # for it.
        domain = self.domain or ""
        domain_text = None
        subdomains = domain.split(".")
        while True:
            domain = ".".join(subdomains)
            LOGGER.debug("Check if %s has own zone", domain)
            domain_text = domain_table.find(string=domain)
            if domain_text is not None or len(subdomains) < 3:
                break
            subdomains.pop(0)

        # Update domain to equal the zone's domain. This is important if we are
        # handling a subdomain that has no zone of itself. If we do not do
        # this, self._relative_name will strip also a part of the subdomain
        # away.
        self.domain = domain
        assert domain_text is not None, "The domain does not exist on Easyname."
        return domain_text

    def _get_domain_id(self, domain_text_element):
        """Return the easyname id of the domain."""
        try:
            # Hierarchy: TR > TD > SPAN > Domain Text
            tr_anchor = domain_text_element.parent.parent.parent
            td_anchor = tr_anchor.find("td", {"class": "entity__field taright"})
            link = td_anchor.find("a")["href"]
            domain_id = link.rsplit("/", 1)[-1]
            return domain_id
        except Exception as error:
            errmsg = (
                "Cannot get the domain id even though the domain seems "
                "to exist (%s).",
                error,
            )
            LOGGER.warning(errmsg)
            raise AssertionError(errmsg)

    def _log(self, name, element):
        """
        Log Response and Tag elements. Do nothing if elements is none of them.
        """
        if isinstance(element, Response):
            LOGGER.debug(
                "%s response: URL=%s Code=%s", name, element.url, element.status_code
            )
        elif isinstance(element, (BeautifulSoup, Tag)):
            LOGGER.debug("%s HTML:\n%s", name, element)
