"""Module provider for Aliyun"""

import base64
import datetime
import hmac
import logging
import sys
import uuid
from argparse import ArgumentParser
from hashlib import sha1
from typing import List
from urllib.parse import quote_plus, urlencode

import requests

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

LOGGER = logging.getLogger(__name__)

ALIYUN_DNS_API_ENDPOINT = "https://alidns.aliyuncs.com"


class Provider(BaseProvider):
    """Provider class for Aliyun"""

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

    @staticmethod
    def configure_parser(parser: ArgumentParser) -> None:
        parser.description = """
            Aliyun Provider requires an access key id and access secret with full rights on dns.
            Better to use RAM on Aliyun cloud to create a specified user for the dns operation.
            The referrence for Aliyun DNS production:
            https://help.aliyun.com/product/29697.html"""
        parser.add_argument(
            "--auth-key-id", help="specify access key id for authentication"
        )
        parser.add_argument(
            "--auth-secret", help="specify access secret for authentication"
        )

    def authenticate(self):
        response = self._request_aliyun("DescribeDomainInfo")

        if "DomainId" not in response:
            raise AuthenticationError(
                f"failed to fetch basic domain info for {self.domain}"
            )

        self.domain_id = response["DomainId"]

        return self

    def cleanup(self) -> None:
        pass

    def create_record(self, rtype, name, content):
        if not self.list_records(rtype, name, content):
            query_params = {
                "Value": content,
                "Type": rtype,
                "RR": self._relative_name(name),
                "TTL": self._get_lexicon_option("ttl"),
            }
            self._request_aliyun("AddDomainRecord", query_params=query_params)

        return True

    # List all records. Return an empty list if no records found
    # type, name and content are used to filter records.
    # If possible filter during the query, otherwise filter after response is received.
    def list_records(self, rtype=None, name=None, content=None):
        query_params = {}

        if rtype:
            query_params["TypeKeyWord"] = rtype

        if name:
            query_params["RRKeyWord"] = self._relative_name(name)

        if content:
            query_params["ValueKeyWord"] = content

        response = self._request_aliyun(
            "DescribeDomainRecords", query_params=query_params
        )

        resource_list = response["DomainRecords"]["Record"]

        processed_records = []
        for resource in resource_list:
            processed_records.append(
                {
                    "id": resource["RecordId"],
                    "type": resource["Type"],
                    "name": self._full_name(resource["RR"]),
                    "ttl": resource["TTL"],
                    "content": resource["Value"],
                }
            )
        LOGGER.debug("list_records: %s", processed_records)
        return processed_records

    # Create or update a record.
    def update_record(self, identifier, rtype=None, name=None, content=None):
        resources = self.list_records(rtype, name, None)

        for record in resources:
            if (
                rtype == record["type"]
                and (self._relative_name(name) == self._relative_name(record["name"]))
                and (content == record["content"])
            ):
                return True

        if not identifier:
            record = resources[0] if resources else None
            identifier = record["id"] if record else None

        if not identifier:
            raise ValueError(f"updating {identifier} identifier not exists")

        if len(resources) > 1:
            LOGGER.warning(
                """There's more than one records match the given critiaria,
             only the first one would be updated"""
            )

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

        query_params = {"RecordId": identifier}

        if rtype:
            query_params["Type"] = rtype

        if name:
            query_params["RR"] = self._relative_name(name)

        if content:
            query_params["Value"] = content

        query_params["TTL"] = self._get_lexicon_option("ttl")
        self._request_aliyun("UpdateDomainRecord", query_params=query_params)

        return True

    # Delete an existing record.
    # If record does not exist, do nothing.
    def delete_record(self, identifier=None, rtype=None, name=None, content=None):
        delete_resource_id = []
        if not identifier:
            resources = self.list_records(rtype, name, content)
            delete_resource_id = [resource["id"] for resource in resources]
        else:
            delete_resource_id.append(identifier)

        LOGGER.debug("delete_records: %s", delete_resource_id)

        for resource_id in delete_resource_id:
            self._request_aliyun(
                "DeleteDomainRecord", query_params={"RecordId": resource_id}
            )

        return True

    def _request(self, action="GET", url="/", data=None, query_params=None):
        response = requests.request("GET", ALIYUN_DNS_API_ENDPOINT, params=query_params)
        response.raise_for_status()

        try:
            return response.json()
        except ValueError as invalid_json_ve:
            LOGGER.error(
                "aliyun dns api responsed with invalid json content, %s", response.text
            )
            raise invalid_json_ve

    def _request_aliyun(self, action, query_params=None):
        if query_params is None:
            query_params = {}

        query_params.update(self._build_default_query_params(action))
        query_params.update(self._build_signature_parameters())

        query_params.update(
            {"Signature": self._calculate_signature("GET", query_params)}
        )

        return self._request(url=ALIYUN_DNS_API_ENDPOINT, query_params=query_params)

    def _calculate_signature(self, http_method, query_params):
        access_secret = self._get_provider_option("auth_secret")

        if not access_secret:
            raise ValueError(
                "auth-secret (access secret) is not specified, did you forget that?"
            )

        sign_secret = access_secret + "&"

        query_list = list(query_params.items())
        query_list.sort(key=lambda t: t[0])

        canonicalized_query_string = urlencode(query_list)

        string_to_sign = "&".join(
            [
                http_method,
                quote_plus("/"),
                quote_plus(canonicalized_query_string),
            ]
        )

        if sys.version_info.major > 2:
            sign_secret_bytes = bytes(sign_secret, "utf-8")
            string_to_sign_bytes = bytes(string_to_sign, "utf-8")
            sign = hmac.new(sign_secret_bytes, string_to_sign_bytes, sha1)
            signature = base64.b64encode(sign.digest()).decode()
        else:
            sign = hmac.new(sign_secret, string_to_sign, sha1)
            signature = sign.digest().encode("base64").rstrip("\n")

        return signature

    def _build_signature_parameters(self):
        access_key_id = self._get_provider_option("auth_key_id")

        if not access_key_id:
            raise ValueError(
                "auth-key-id (access key id) is not specified, did you forget that?"
            )

        signature_nonce = str(uuid.uuid4())

        return {
            "SignatureMethod": "HMAC-SHA1",
            "SignatureVersion": "1.0",
            "SignatureNonce": signature_nonce,
            "Timestamp": datetime.datetime.now(datetime.timezone.utc)
            .replace(microsecond=0)
            .isoformat()
            + "Z",
            "AccessKeyId": access_key_id,
        }

    def _build_default_query_params(self, action):
        return {
            "Action": action,
            "DomainName": self.domain,
            "Format": "json",
            "Version": "2015-01-09",
        }
