"""Module provider for DirectAdmin hosts"""

import logging
import warnings
from argparse import ArgumentParser
from typing import List

import requests
from requests.auth import HTTPBasicAuth

from lexicon.exceptions import AuthenticationError
from lexicon.interfaces import Provider as BaseProvider

LOGGER = logging.getLogger(__name__)


# See https://www.directadmin.com/features.php?id=504 for the specification of
# the URIs for the different operations
class Provider(BaseProvider):
    """Provider class for DirectAdmin"""

    @staticmethod
    def get_nameservers() -> List[str]:
        # DirectAdmin is not tied to a specific domain, so there is nothing to specify here
        return []

    @staticmethod
    def configure_parser(parser: ArgumentParser) -> None:
        parser.add_argument(
            "--auth-password",
            help="specify password for authentication (or login key for two-factor authentication)",
        )
        parser.add_argument(
            "--auth-username", help="specify username for authentication"
        )
        parser.add_argument("--endpoint", help="specify the DirectAdmin endpoint")

    def __init__(self, config):
        super(Provider, self).__init__(config)

        self.endpoint = self._get_provider_option("endpoint")
        if self.endpoint is None:
            raise Exception("Specify endpoint of DirectAdmin")

    def authenticate(self):
        try:
            response = self._get("/CMD_API_SHOW_DOMAINS")
        except requests.exceptions.HTTPError as err:
            # A 401 error will be returned in case of incorrect or missing
            # credentials
            raise err

        # The response is a URL encoded array containing all available domains
        domains = [domain.split("=").pop() for domain in response.split("&")]
        try:
            self.domain_id = domains.index(self.domain)
        except ValueError:
            raise AuthenticationError(f"Domain {self.domain} not found")

    def cleanup(self) -> None:
        pass

    def create_record(self, rtype, name, content):
        # Refuse to create duplicate records
        existing_records = self.list_records(rtype, name, content)
        if existing_records:
            return True

        query_params = {
            "action": "add",
            "json": "yes",
            "name": f"{self._full_name(name)}.",
            "type": rtype,
            "value": content,
        }

        if self._get_lexicon_option("ttl"):
            query_params["ttl"] = self._get_lexicon_option("ttl")

        try:
            response = self._get("/CMD_API_DNS_CONTROL", query_params)
        except requests.exceptions.HTTPError:
            response = {"success": "Create Failed"}

        LOGGER.debug("create_record: %s", response)

        return response["success"].lower().find("added") >= 0

    def list_records(self, rtype=None, name=None, content=None):
        response = {"records": []}
        try:
            response = self._get("/CMD_API_DNS_CONTROL", {"json": "yes"})
        except requests.exceptions.HTTPError as err:
            warnings.warn(err.response.text)
            raise

        records = [
            self._parse_response_record(record) for record in response["records"]
        ]
        if rtype:
            records = [record for record in records if record["type"] == rtype]
        if name:
            cmp_name = self._full_name(name.lower())
            records = [record for record in records if record["name"] == cmp_name]
        if content:
            records = [record for record in records if record["content"] == content]

        LOGGER.debug("list_records: %s", records)

        return records

    def _parse_response_record(self, response_record):
        # Most fields in a record response match, except for content which is
        # value
        return {
            "content": response_record["value"],
            "id": response_record["combined"],
            "name": self._full_name(response_record["name"]),
            # 4 hours appears to be the default shown by DirectAdmin if no TTL
            # is set
            "ttl": response_record.get("ttl", 14400),
            "type": response_record["type"],
        }

    def update_record(self, identifier, rtype=None, name=None, content=None):
        # Editing a record is a combination of removing the old record and
        # adding a new one, but specifying 'edit' as the action while passing
        # all parameters necessary for both deletion and creation

        if not identifier:
            # The identifier of the original record to edit is necessary to be
            # able to create the appropriate delete payload
            original_records = self.list_records(rtype, name)
            if len(original_records) > 1:
                warnings.warn("Found multiple records to edited. Cannot continue...")
                return False

            if not original_records:
                warnings.warn("Found no records to edit. Cannot continue...")
                return False

            identifier = original_records[0]["id"]

        delete_key = self._determine_delete_key(identifier, rtype)

        query_params = {
            "action": "edit",
            "json": "yes",
            delete_key: identifier,
            "name": f"{self._full_name(name)}.",
            "type": rtype,
            "value": content,
        }

        try:
            response = self._get("/CMD_API_DNS_CONTROL", query_params)
        except requests.exceptions.HTTPError:
            response = {"success": "Update Failed"}

        LOGGER.debug("update_record: %s", response)

        return response["success"].lower().find("added") > 0

    def delete_record(self, identifier=None, rtype=None, name=None, content=None):
        if not identifier:
            identifiers = [
                record["id"] for record in self.list_records(rtype, name, content)
            ]
        else:
            identifiers = [identifier]

        response = {"success": "noop"}
        for identifier_to_delete in identifiers:
            delete_key = self._determine_delete_key(identifier_to_delete, rtype)
            query_params = {
                "action": "select",
                "json": "yes",
                delete_key: identifier_to_delete,
            }

            try:
                response = self._get("/CMD_API_DNS_CONTROL", query_params)
            except requests.exceptions.HTTPError:
                response = {"success": "Delete Failed"}

            LOGGER.debug("delete_record: %s", response)

        return response["success"].lower().find("deleted") > 0

    def _determine_delete_key(self, identifier, rtype):
        # The indicator for the record that needs to be removed is determined
        # by the type of the record and its index within all records of that
        # type. There's an additional check on the name and value which still
        # need to match for the removal to actually occur
        if not rtype:
            # An rtype is necessary to create the delete_key. However, it may
            # not be specified in which case an effort needs to be made to
            # figure it out automatically. The necessary data can be recreated
            # from the identifier
            identifier_parts = identifier.split("&")
            name = identifier_parts[0].split("=").pop()
            content = identifier_parts[1].split("=").pop()

            records = self.list_records(None, name, content)
            rtype = records[0]["type"]

        existing_records = self.list_records(rtype)
        existing_record_index = 0
        for index, record in enumerate(existing_records):
            if record["id"] == identifier:
                existing_record_index = index

        return f"{rtype}recs{existing_record_index}".lower()

    def _request(self, action="GET", url="/", data=None, query_params=None):
        if data is None:
            data = {}
        if query_params is None:
            query_params = {}

        query_params["domain"] = self.domain

        response = requests.request(
            action,
            self.endpoint + url,
            auth=HTTPBasicAuth(
                self._get_provider_option("auth_username"),
                self._get_provider_option("auth_password"),
            ),
            params=query_params,
        )

        # If the request fails for any reason, throw an error
        response.raise_for_status()

        if "json" in response.headers["Content-Type"].lower():
            return response.json()

        return response.text
