import vonage

from .account import Account
from .application import ApplicationV2, Application
from .errors import *
from .messages import Messages
from .number_insight import NumberInsight
from .number_management import Numbers
from .redact import Redact
from .short_codes import ShortCodes
from .sms import Sms
from .ussd import Ussd
from .voice import Voice
from .verify import Verify

import logging
from platform import python_version

import base64
import hashlib
import hmac
import jwt
import os
import time
import re
from uuid import uuid4

from requests import Response
from requests.adapters import HTTPAdapter
from requests.sessions import Session

string_types = (str, bytes)

try:
    from json import JSONDecodeError
except ImportError:
    JSONDecodeError = ValueError

logger = logging.getLogger("vonage")


class Client:
    """
    Create a Client object to start making calls to Vonage/Nexmo APIs.

    The credentials you provide when instantiating a Client determine which
    methods can be called. Consult the `Vonage API docs <https://developer.vonage.com/concepts/guides/authentication/>`
    for details of the authentication used by the APIs you wish to use, and instantiate your
    client with the appropriate credentials.

    :param str key: Your Vonage API key
    :param str secret: Your Vonage API secret.
    :param str signature_secret: Your Vonage API signature secret.
        You may need to have this enabled by Vonage support. It is only used for SMS authentication.
    :param str signature_method:
        The encryption method used for signature encryption. This must match the method
        configured in the Vonage Dashboard. We recommend `sha256` or `sha512`.
        This should be one of `md5`, `sha1`, `sha256`, or `sha512` if using HMAC digests.
        If you want to use a simple MD5 hash, leave this as `None`.
    :param str application_id: Your application ID if calling methods which use JWT authentication.
    :param str private_key: Your private key, for calling methods which use JWT authentication.
        This should either be a str containing the key in its PEM form, or a path to a private key file.
    :param str app_name: This optional value is added to the user-agent header
        provided by this library and can be used to track your app statistics.
    :param str app_version: This optional value is added to the user-agent header
        provided by this library and can be used to track your app statistics.
    :param timeout: (optional) How many seconds to wait for the server to send data
        before giving up, as a float, or a (connect timeout, read
        timeout) tuple. If set this timeout is used for every call to the Vonage enpoints
    :type timeout: float or tuple
    """

    def __init__(
        self,
        key=None,
        secret=None,
        signature_secret=None,
        signature_method=None,
        application_id=None,
        private_key=None,
        app_name=None,
        app_version=None,
        timeout=None,
        pool_connections=10,
        pool_maxsize=10,
        max_retries=3,
    ):
        self.api_key = key or os.environ.get("VONAGE_API_KEY", None)
        self.api_secret = secret or os.environ.get("VONAGE_API_SECRET", None)

        self.signature_secret = signature_secret or os.environ.get("VONAGE_SIGNATURE_SECRET", None)
        self.signature_method = signature_method or os.environ.get("VONAGE_SIGNATURE_METHOD", None)

        if self.signature_method in {"md5", "sha1", "sha256", "sha512"}:
            self.signature_method = getattr(hashlib, signature_method)

        self._jwt_auth_params = {}

        if private_key is not None and application_id is not None:
            self._application_id = application_id
            self._private_key = private_key

            if isinstance(self._private_key, string_types) and re.search("[.][a-zA-Z0-9_]+$", self._private_key):
                with open(self._private_key, "rb") as key_file:
                    self._private_key = key_file.read()

        self._host = "rest.nexmo.com"
        self._api_host = "api.nexmo.com"

        user_agent = f"vonage-python/{vonage.__version__} python/{python_version()}"

        if app_name and app_version:
            user_agent += f" {app_name}/{app_version}"

        self.headers = {"User-Agent": user_agent, "Accept": "application/json"}

        self.account = Account(self)
        self.application = Application(self)
        self.messages = Messages(self)
        self.number_insight = NumberInsight(self)
        self.numbers = Numbers(self)
        self.short_codes = ShortCodes(self)
        self.sms = Sms(self)
        self.ussd = Ussd(self)
        self.verify = Verify(self)
        self.voice = Voice(self)

        self.timeout = timeout
        self.session = Session()
        self.adapter = HTTPAdapter(
            pool_connections=pool_connections, pool_maxsize=pool_maxsize, max_retries=max_retries
        )
        self.session.mount("https://", self.adapter)

    # Get and Set _host attribute
    def host(self, value=None):
        if value is None:
            return self._host
        else:
            self._host = value

    # Gets And Set _api_host attribute
    def api_host(self, value=None):
        if value is None:
            return self._api_host
        else:
            self._api_host = value

    def auth(self, params=None, **kwargs):
        self._jwt_auth_params = params or kwargs

    def check_signature(self, params):
        params = dict(params)
        signature = params.pop("sig", "").lower()
        return hmac.compare_digest(signature, self.signature(params))

    def signature(self, params):
        if self.signature_method:
            hasher = hmac.new(self.signature_secret.encode(), digestmod=self.signature_method)
        else:
            hasher = hashlib.md5()

        # Add timestamp if not already present
        if not params.get("timestamp"):
            params["timestamp"] = int(time.time())

        for key in sorted(params):
            value = params[key]

            if isinstance(value, str):
                value = value.replace("&", "_").replace("=", "_")

            hasher.update(f"&{key}={value}".encode("utf-8"))

        if self.signature_method is None:
            hasher.update(self.signature_secret.encode())

        return hasher.hexdigest()

    def get(self, host, request_uri, params=None, auth_type=None):
        uri = f"https://{host}{request_uri}"
        self._request_headers = self.headers

        if auth_type == 'jwt':
            self._request_headers = self._add_jwt_to_request_headers()
        elif auth_type == 'params':
            params = dict(params or {}, api_key=self.api_key, api_secret=self.api_secret)
        elif auth_type == 'header':
            hash = base64.b64encode(f"{self.api_key}:{self.api_secret}".encode("utf-8")).decode("ascii")
            self._request_headers = dict(self.headers or {}, Authorization=f"Basic {hash}")
        else:
            raise InvalidAuthenticationTypeError(
                f'Invalid authentication type. Must be one of "jwt", "header" or "params".'
            )

        logger.debug(f"GET to {repr(uri)} with params {repr(params)}, headers {repr(self._request_headers)}")
        return self.parse(
            host, self.session.get(uri, params=params, headers=self._request_headers, timeout=self.timeout)
        )

    def post(self, host, request_uri, params, auth_type=None, body_is_json=True, supports_signature_auth=False):
        """
        Low-level method to make a post request to an API server.
        This method automatically adds authentication, picking the first applicable authentication method from the following:
        - If the supports_signature_auth param is True, and the client was instantiated with a signature_secret,
            then signature authentication will be used.
        :param bool supports_signature_auth: Preferentially use signature authentication if a signature_secret was provided
            when initializing this client.
        """
        uri = f"https://{host}{request_uri}"
        self._request_headers = self.headers

        if supports_signature_auth and self.signature_secret:
            params["api_key"] = self.api_key
            params["sig"] = self.signature(params)
        elif auth_type == 'jwt':
            self._request_headers = self._add_jwt_to_request_headers()
        elif auth_type == 'params':
            params = dict(params, api_key=self.api_key, api_secret=self.api_secret)
        elif auth_type == 'header':
            hash = base64.b64encode(f"{self.api_key}:{self.api_secret}".encode("utf-8")).decode("ascii")
            self._request_headers = dict(self.headers or {}, Authorization=f"Basic {hash}")
        else:
            raise InvalidAuthenticationTypeError(
                f'Invalid authentication type. Must be one of "jwt", "header" or "params".'
            )

        logger.debug(f"POST to {repr(uri)} with params {repr(params)}, headers {repr(self._request_headers)}")
        if body_is_json:
            return self.parse(
                host, self.session.post(uri, json=params, headers=self._request_headers, timeout=self.timeout)
            )
        else:
            return self.parse(
                host, self.session.post(uri, data=params, headers=self._request_headers, timeout=self.timeout)
            )

    def put(self, host, request_uri, params, auth_type=None):
        uri = f"https://{host}{request_uri}"
        self._request_headers = self.headers

        if auth_type == 'jwt':
            self._request_headers = self._add_jwt_to_request_headers()
        elif auth_type == 'header':
            hash = base64.b64encode(f"{self.api_key}:{self.api_secret}".encode("utf-8")).decode("ascii")
            self._request_headers = dict(self._request_headers or {}, Authorization=f"Basic {hash}")
        else:
            raise InvalidAuthenticationTypeError(
                f'Invalid authentication type. Must be one of "jwt", "header" or "params".'
            )

        logger.debug(f"PUT to {repr(uri)} with params {repr(params)}, headers {repr(self._request_headers)}")
        # All APIs that currently use put methods require a json-formatted body so don't need to check this
        return self.parse(host, self.session.put(uri, json=params, headers=self._request_headers, timeout=self.timeout))

    def delete(self, host, request_uri, auth_type=None):
        uri = f"https://{host}{request_uri}"
        self._request_headers = self.headers

        if auth_type == 'jwt':
            self._request_headers = self._add_jwt_to_request_headers()
        elif auth_type == 'header':
            hash = base64.b64encode(f"{self.api_key}:{self.api_secret}".encode("utf-8")).decode("ascii")
            self._request_headers = dict(self._request_headers or {}, Authorization=f"Basic {hash}")
        else:
            raise InvalidAuthenticationTypeError(
                f'Invalid authentication type. Must be one of "jwt", "header" or "params".'
            )

        logger.debug(f"DELETE to {repr(uri)} with headers {repr(self._request_headers)}")
        return self.parse(host, self.session.delete(uri, headers=self._request_headers, timeout=self.timeout))

    def parse(self, host, response: Response):
        logger.debug(f"Response headers {repr(response.headers)}")
        if response.status_code == 401:
            raise AuthenticationError("Authentication failed. Check you're using a valid authentication method.")
        elif response.status_code == 204:
            return None
        elif 200 <= response.status_code < 300:
            # Strip off any encoding from the content-type header:
            content_mime = response.headers.get("content-type").split(";", 1)[0]
            if content_mime == "application/json":
                return response.json()
            else:
                return response.content
        elif 400 <= response.status_code < 500:
            logger.warning(f"Client error: {response.status_code} {repr(response.content)}")
            message = f"{response.status_code} response from {host}"

            # Test for standard error format:
            try:
                error_data = response.json()
                if "type" in error_data and "title" in error_data and "detail" in error_data:
                    title = error_data["title"]
                    detail = error_data["detail"]
                    type = error_data["type"]
                    message = f"{title}: {detail} ({type})"

            except JSONDecodeError:
                pass
            raise ClientError(message)
        elif 500 <= response.status_code < 600:
            logger.warning(f"Server error: {response.status_code} {repr(response.content)}")
            message = f"{response.status_code} response from {host}"
            raise ServerError(message)

    def _add_jwt_to_request_headers(self):
        return dict(self.headers, Authorization=b"Bearer " + self._generate_application_jwt())

    def _generate_application_jwt(self):
        iat = int(time.time())

        payload = dict(self._jwt_auth_params)
        payload.setdefault("application_id", self._application_id)
        payload.setdefault("iat", iat)
        payload.setdefault("exp", iat + 60)
        payload.setdefault("jti", str(uuid4()))

        token = jwt.encode(payload, self._private_key, algorithm="RS256")

        # If token is string transform it to byte type
        if type(token) is str:
            token = bytes(token, 'utf-8')

        return token
