import os
import platform
from typing import Dict, List, MutableMapping, Optional, Tuple
from urllib.parse import urlparse, urlunparse

from twilio import __version__
from twilio.http import HttpClient
from twilio.http.http_client import TwilioHttpClient
from twilio.http.response import Response
from twilio.credential.credential_provider import CredentialProvider


class ClientBase(object):
    """A client for accessing the Twilio API."""

    def __init__(
        self,
        username: Optional[str] = None,
        password: Optional[str] = None,
        account_sid: Optional[str] = None,
        region: Optional[str] = None,
        http_client: Optional[HttpClient] = None,
        environment: Optional[MutableMapping[str, str]] = None,
        edge: Optional[str] = None,
        user_agent_extensions: Optional[List[str]] = None,
        credential_provider: Optional[CredentialProvider] = None,
    ):
        """
        Initializes the Twilio Client

        :param username: Username to authenticate with
        :param password: Password to authenticate with
        :param account_sid: Account SID, defaults to Username
        :param region: Twilio Region to make requests to, defaults to 'us1' if an edge is provided
        :param http_client: HttpClient, defaults to TwilioHttpClient
        :param environment: Environment to look for auth details, defaults to os.environ
        :param edge: Twilio Edge to make requests to, defaults to None.
        :param user_agent_extensions: Additions to the user agent string
        :param credential_provider: credential provider for authentication method that needs to be used
        """

        environment = environment or os.environ

        self.username = username or environment.get("TWILIO_ACCOUNT_SID")
        """ :type : str """
        self.password = password or environment.get("TWILIO_AUTH_TOKEN")
        """ :type : str """
        self.edge = edge or environment.get("TWILIO_EDGE")
        """ :type : str """
        self.region = region or environment.get("TWILIO_REGION")
        """ :type : str """
        self.user_agent_extensions = user_agent_extensions or []
        """ :type : list[str] """
        self.credential_provider = credential_provider or None
        """ :type : CredentialProvider """

        self.account_sid = account_sid or self.username
        """ :type : str """
        self.auth = (self.username, self.password)
        """ :type : tuple(str, str) """
        self.http_client: HttpClient = http_client or TwilioHttpClient()
        """ :type : HttpClient """

    def request(
        self,
        method: str,
        uri: str,
        params: Optional[Dict[str, object]] = None,
        data: Optional[Dict[str, object]] = None,
        headers: Optional[Dict[str, str]] = None,
        auth: Optional[Tuple[str, str]] = None,
        timeout: Optional[float] = None,
        allow_redirects: bool = False,
    ) -> Response:
        """
        Makes a request to the Twilio API using the configured http client
        Authentication information is automatically added if none is provided

        :param method: HTTP Method
        :param uri: Fully qualified url
        :param params: Query string parameters
        :param data: POST body data
        :param headers: HTTP Headers
        :param auth: Authentication
        :param timeout: Timeout in seconds
        :param allow_redirects: Should the client follow redirects

        :returns: Response from the Twilio API
        """
        headers = self.get_headers(method, headers)

        if self.credential_provider:

            auth_strategy = self.credential_provider.to_auth_strategy()
            headers["Authorization"] = auth_strategy.get_auth_string()
        elif self.username is not None and self.password is not None:
            auth = self.get_auth(auth)
        else:
            auth = None

        if method == "DELETE":
            del headers["Accept"]

        uri = self.get_hostname(uri)
        filtered_data = self.copy_non_none_values(data)
        return self.http_client.request(
            method,
            uri,
            params=params,
            data=filtered_data,
            headers=headers,
            auth=auth,
            timeout=timeout,
            allow_redirects=allow_redirects,
        )

    async def request_async(
        self,
        method: str,
        uri: str,
        params: Optional[Dict[str, object]] = None,
        data: Optional[Dict[str, object]] = None,
        headers: Optional[Dict[str, str]] = None,
        auth: Optional[Tuple[str, str]] = None,
        timeout: Optional[float] = None,
        allow_redirects: bool = False,
    ) -> Response:
        """
        Asynchronously makes a request to the Twilio API  using the configured http client
        The configured http client must be an asynchronous http client
        Authentication information is automatically added if none is provided

        :param method: HTTP Method
        :param uri: Fully qualified url
        :param params: Query string parameters
        :param data: POST body data
        :param headers: HTTP Headers
        :param auth: Authentication
        :param timeout: Timeout in seconds
        :param allow_redirects: Should the client follow redirects

        :returns: Response from the Twilio API
        """
        if not self.http_client.is_async:
            raise RuntimeError(
                "http_client must be asynchronous to support async API requests"
            )

        headers = self.get_headers(method, headers)
        if method == "DELETE":
            del headers["Accept"]

        if self.credential_provider:
            auth_strategy = self.credential_provider.to_auth_strategy()
            headers["Authorization"] = auth_strategy.get_auth_string()
        elif self.username is not None and self.password is not None:
            auth = self.get_auth(auth)
        else:
            auth = None

        uri = self.get_hostname(uri)
        filtered_data = self.copy_non_none_values(data)
        return await self.http_client.request(
            method,
            uri,
            params=params,
            data=filtered_data,
            headers=headers,
            auth=auth,
            timeout=timeout,
            allow_redirects=allow_redirects,
        )

    def copy_non_none_values(self, data):
        if isinstance(data, dict):
            return {
                k: self.copy_non_none_values(v)
                for k, v in data.items()
                if v is not None
            }
        elif isinstance(data, list):
            return [
                self.copy_non_none_values(item) for item in data if item is not None
            ]
        return data

    def get_auth(self, auth: Optional[Tuple[str, str]]) -> Tuple[str, str]:
        """
        Get the request authentication object
        :param auth: Authentication (username, password)
        :returns: The authentication object
        """
        return auth or self.auth

    def get_headers(
        self, method: str, headers: Optional[Dict[str, str]]
    ) -> Dict[str, str]:
        """
        Get the request headers including user-agent, extensions, encoding, content-type, MIME type
        :param method: HTTP method
        :param headers: HTTP headers
        :returns: HTTP headers
        """
        headers = headers or {}

        # Set User-Agent
        pkg_version = __version__
        os_name = platform.system()
        os_arch = platform.machine()
        python_version = platform.python_version()
        headers["User-Agent"] = "twilio-python/{} ({} {}) Python/{}".format(
            pkg_version,
            os_name,
            os_arch,
            python_version,
        )
        # Extensions
        for extension in self.user_agent_extensions:
            headers["User-Agent"] += " {}".format(extension)
        headers["X-Twilio-Client"] = "python-{}".format(__version__)

        # Types, encodings, etc.
        headers["Accept-Charset"] = "utf-8"
        if (method == "POST" or method == "PUT") and ("Content-Type" not in headers):
            headers["Content-Type"] = "application/x-www-form-urlencoded"
        if "Accept" not in headers:
            headers["Accept"] = "application/json"

        return headers

    def get_hostname(self, uri: str) -> str:
        """
        Determines the proper hostname given edge and region preferences
        via client configuration or uri.

        :param uri: Fully qualified url

        :returns: The final uri used to make the request
        """
        if not self.edge and not self.region:
            return uri

        parsed_url = urlparse(uri)
        pieces = parsed_url.netloc.split(".")
        prefix = pieces[0]
        suffix = ".".join(pieces[-2:])
        region = None
        edge = None
        if len(pieces) == 4:
            # product.region.twilio.com
            region = pieces[1]
        elif len(pieces) == 5:
            # product.edge.region.twilio.com
            edge = pieces[1]
            region = pieces[2]

        edge = self.edge or edge
        region = self.region or region or (edge and "us1")

        parsed_url = parsed_url._replace(
            netloc=".".join([part for part in [prefix, edge, region, suffix] if part])
        )
        return str(urlunparse(parsed_url))

    def __repr__(self) -> str:
        """
        Provide a friendly representation

        :returns: Machine friendly representation
        """
        return "<Twilio {}>".format(self.account_sid)
