import os
import logging
import gzip
import base64
import functools
import tempfile
import ssl
import json
import http.client
import urllib.parse
import urllib.request
from tealeaf.headers import Headers

logger = logging.getLogger(__name__)

default_headers = {
    "User-Agent": "python-tealeaf",
    "Connection": "keep-alive",
    "Accept": "*/*",
    "Accept-Encoding": "gzip, deflate, br"
}

def urljoin(*parts: str) -> str:
    return "/".join([str(s).strip("/") for s in parts if s])


def format_cls(instance, **kws):
    return (
        f'{instance.__class__.__name__}({", ".join(f"{key}={repr(value)}" for key, value in kws.items())})'
    )


class ApiError(Exception):
    pass


class Request:
    def __init__(
        self,
        url: str = "",
        method: str = "get",
        headers: dict = None,
        json: dict = None,
        data: bytes = None,
        ssl_context: ssl.SSLContext = None,
    ):
        self.url = url
        self.method = method

        # use default headers and update with user provided
        headers = headers or dict()
        self.headers = Headers(**default_headers)
        self.headers.update(headers)
        
        self.json = json
        self._data = data
        self.ssl_context = ssl_context

    def __new__(cls, *args, **kws):
        _json = kws.get("json")
        _data = kws.get("data")
        if _json and _data:
            raise ValueError('Must specify at most one of ["json", "data"] arguments')
        elif _json:
            return super(Request, JsonRequest).__new__(JsonRequest)
        else:
            return super().__new__(cls)

    @property
    def data(self):
        return self._data

    def get_request_kws(self):
        """
        See source: https://github.com/python/cpython/tree/main/Lib/urllib
        """
        return dict(
            url=self.url, method=self.method, headers=self.headers, data=self.data
        )

    def create_request(self):
        return urllib.request.Request(**self.get_request_kws())

    def execute(self):
        # create a urllib.request.Request object
        request = self.create_request()

        # kws to pass to urllib.request.urlopen
        urlopen_kws = (
            dict() if self.ssl_context is None else dict(ssl_context=self.ssl_context)
        )

        # response data used to construct a response object
        response_kws = dict()

        try:
            with urllib.request.urlopen(request, **urlopen_kws) as response:
                response_kws["content"] = response.read()
                response_kws["response"] = response
        except urllib.error.HTTPError as e:
            response_kws["content"] = e.read()
            response_kws["response"] = e
        response_kws["request"] = request
        return Response(**response_kws)


class JsonRequest(Request):
    def __init__(self, *args, **kws):
        super().__init__(*args, **kws)
        self.headers["Content-Type"] = "application/json"

    def get_request_kws(self):
        if self.json is not None:
            return {
                **super().get_request_kws(),
                **dict(data=json.dumps(self.json).encode("utf-8")),
            }
        else:
            return super().get_request_kws()

    @property
    def data(self):
        return json.dumps(self.json).encode("utf-8")


class ApiCredential:
    def preprocess_request(self, request: Request) -> Request:
        headers = getattr(self, "headers", {})
        request.headers.update(headers)
        return request


class HeaderSecrets(ApiCredential):
    def __init__(self, **headers):
        self.__headers = headers

    @property
    def headers(self):
        return self.__headers


class BearerToken(ApiCredential):
    def __init__(self, token: str):
        self.__token = token

    @property
    def headers(self):
        return {"Authorization": f"Bearer {self.__token}"}


class BasicAuth(ApiCredential):
    def __init__(self, username: str, password: str):
        username = username
        password = password
        encoded = base64.b64encode(
            f"{username}:{password}".encode("utf-8")
        ).decode("utf-8")
        self.__authorization = f"Basic {encoded}"

    @property
    def headers(self):
        return {"Authorization": self.__authorization}


class JsonBodyCredentials(ApiCredential):
    def __init__(self, fields: dict, **kws):
        self.__fields = fields

    def preprocess_request(self, request: Request) -> JsonRequest:
        if request.json is None:
            return JsonRequest(**request.get_request_kws(), json=self.__fields)
        else:
            request.json.update(self.__fields)
            return request


class ClientSslCertificate(ApiCredential):
    def __init__(self, certificate: bytes = b"", key: bytes = b""):
        self.__certificate = certificate
        self.__key = key
        self.__context = None

    def preprocess_request(self, request: Request) -> Request:
        request.ssl_context = self.get_context()
        return request

    def get_context(self):
        """
        Generates a `ssl.SSLContext` object that can be passed to `urllib.request.urlopen`
        """
        if self.__context is None:
            # create ssl context
            self.__context = ssl.SSLContext()

            # get temporary cert file
            cert = tempfile.NamedTemporaryFile(mode="wb", delete=False)
            cert.write(self.__certificate)
            cert.close()

            # get temporary key file
            key = tempfile.NamedTemporaryFile(mode="wb", delete=False)
            key.write(self.__key)
            key.close()

            # create ssl context
            self.__context.load_cert_chain(certfile=cert.name, keyfile=key.name)

            # remove files
            os.remove(key.name)
            os.remove(cert.name)

        return self.__context


class CredentialChain(ApiCredential):
    def __init__(self, *credentials: ApiCredential):
        self.__credentials = credentials

    def preprocess_request(self, request: Request) -> Request:
        for credential in self.__credentials:
            request = credential.preprocess_request(request)
        return request


class Response:
    def __init__(
        self,
        content: bytes,
        response: http.client.HTTPResponse,
        request: urllib.request.Request = None,
    ):
        self.content = content
        self.response = response
        self.request = request

        self._data = None
        self.content_encoding = self.response.headers.get("Content-Encoding")
        if self.content_encoding == "gzip":
            self.content = gzip.decompress(self.content)

    def __new__(cls, *args, **kws):
        code = getattr(kws.get("response"), "code", -1)
        if code == -1:
            return super(Response, UnknownResponse).__new__(UnknownResponse)
        elif 200 <= code < 300:
            return super().__new__(cls)
        else:
            return super(Response, ErrorResponse).__new__(ErrorResponse)

    def __repr__(self):
        return self.__str__()

    def __str__(self):
        return format_cls(self, code=self.code)

    def __getattr__(self, attr):
        if hasattr(self.response, attr):
            return getattr(self.response, attr)
        raise AttributeError(attr)

    @property
    def ok(self):
        return 200 <= self.code < 300

    def json(self, *args, **kws):
        return json.loads(self.content.decode(kws.get("encoding", "utf-8")))

    def raise_(self):
        pass

    def astype(self, cls: type):
        return cls(**self.json())


class ErrorResponse(Response):
    def __str__(self):
        return format_cls(
            self, code=self.code, reason=self.reason, content=self.content
        )

    def raise_(self):
        raise ApiError(self.__str__())


class UnknownResponse(Response):
    pass


class Api:
    supported_methods = ["get", "head", "put", "patch", "post", "delete", "options"]

    def __init__(
        self,
        url: str,
        credentials: ApiCredential = None,
        headers: dict = None,
        raise_for_status: bool = False
    ):
        self.url = url
        self.credentials = credentials or ApiCredential()
        self.headers = headers or dict()
        self.raise_for_status = raise_for_status

    def __str__(self):
        return f'{self.__class__.__name__}("{self.url}")'

    def __repr__(self):
        return self.__str__()

    @property
    def url(self):
        return self._url

    @url.setter
    def url(self, url: str):
        """
        Parse url and validate scheme on attribute set
        """
        self._url = url
        self._parsed_url = urllib.parse.urlparse(self._url)
        if not self._parsed_url.scheme:
            self.url = f'https://{self._url.strip("/")}'

    def request(
        self,
        url: str = "",
        json: dict = None,
        method: str = "get",
        headers: dict = None,
        data: bytes = None,
    ):

        headers = headers or dict()
        url = urljoin(self.url, url) if url else self.url
        logger.info(f"{method} {url}")
        request = Request(
            url=url, 
            method=method, 
            headers={**self.headers, **headers}, 
            json=json, 
            data=data
        )
        request = self.credentials.preprocess_request(request)
        response = request.execute()
        logger.info(f"{method} {url} responded with {response.code}")
        if self.raise_for_status:
            response.raise_()
        return response

    def __getattr__(self, attr: str):
        if attr in self.supported_methods:
            return functools.partial(self.request, method=attr.upper())
        raise AttributeError(attr)
