"""
Generated by Sideko (sideko.dev)
"""

from json import JSONDecodeError
from typing import Any, List, TypeVar, Dict, Optional, Type, Union, cast

import httpx
from pydantic import BaseModel

from .api_error import ApiError
from .auth import AuthProvider
from .request import RequestConfig, RequestOptions, default_request_options, QueryParams
from .response import from_encodable
from .utils import is_binary_content_type, get_content_type
from .binary_response import BinaryResponse

NoneType = type(None)
T = TypeVar(
    "T",
    bound=Union[object, None, str, "BaseModel", List[Any], Dict[str, Any], Any],
)


class BaseClient:
    def __init__(
        self,
        *,
        base_url: str,
    ):
        self._base_url = base_url
        self._auths: Dict[str, AuthProvider] = {}

    def register_auth(self, auth_id: str, provider: AuthProvider):
        self._auths[auth_id] = provider

    def default_headers(self) -> Dict[str, str]:
        headers: Dict[str, str] = {
            "x-sideko-sdk-language": "Python",
        }
        return headers

    def get_base_url(self) -> str:
        return self._base_url

    def build_url(self, path: str) -> str:
        base = self._base_url
        if base.endswith("/"):
            base = base[:-1]
        if path.startswith("/"):
            path = path[1:]

        return f"{base}/{path}"

    def _apply_auth(
        self, *, cfg: RequestConfig, auth_names: List[str]
    ) -> RequestConfig:
        for auth_name in auth_names:
            auth_provider = self._auths.get(auth_name)
            if auth_provider is not None:
                cfg = auth_provider.add_to_request(cfg)

        return cfg

    def _apply_headers(
        self,
        *,
        cfg: RequestConfig,
        opts: RequestOptions,
        content_type: Optional[str] = None,
        explicit_headers: Optional[Dict[str, str]] = None,
    ) -> RequestConfig:
        headers = cfg.get("headers", {})
        headers.update(self.default_headers())

        if content_type is not None:
            headers["content-type"] = content_type

        if explicit_headers is not None:
            headers.update(explicit_headers)

        additional_headers = opts.get("additional_headers", None)
        if additional_headers is not None:
            headers.update(additional_headers)

        if len(headers) > 0:
            cfg["headers"] = headers

        return cfg

    def _apply_query_params(
        self,
        *,
        cfg: RequestConfig,
        opts: RequestOptions,
        query_params: Optional[QueryParams] = None,
    ) -> RequestConfig:
        params = cfg.get("params", {})

        if query_params is not None:
            params.update(query_params)

        additional_params = opts.get("additional_params", None)
        if additional_params is not None:
            params.update(additional_params)

        if len(params) > 0:
            cfg["params"] = params

        return cfg

    def _apply_timeout(
        self,
        *,
        cfg: RequestConfig,
        opts: RequestOptions,
    ) -> RequestConfig:
        timeout = opts.get("timeout", None)

        # TODO: timeout from base class
        if timeout is not None:
            cfg["timeout"] = timeout

        return cfg

    def _apply_body(
        self,
        *,
        cfg: RequestConfig,
        data: Optional[httpx._types.RequestData] = None,
        files: Optional[httpx._types.RequestFiles] = None,
        json: Optional[Any] = None,
        content: Optional[httpx._types.RequestContent] = None,
    ) -> RequestConfig:
        if data is not None:
            cfg["data"] = data

        if files is not None:
            cfg["files"] = files

        if json is not None:
            cfg["json"] = json

        if content is not None:
            cfg["content"] = content

        return cfg

    def build_request(
        self,
        *,
        method: str,
        path: str,
        auth_names: Optional[List[str]] = None,
        query_params: Optional[QueryParams] = None,
        headers: Optional[Dict[str, str]] = None,
        data: Optional[httpx._types.RequestData] = None,
        files: Optional[httpx._types.RequestFiles] = None,
        json: Optional[Any] = None,
        content_type: Optional[str] = None,
        content: Optional[httpx._types.RequestContent] = None,
        request_options: Optional[RequestOptions] = None,
    ) -> RequestConfig:
        opts = request_options or default_request_options()
        req_cfg: RequestConfig = {"method": method, "url": self.build_url(path)}
        req_cfg = self._apply_auth(cfg=req_cfg, auth_names=auth_names or [])
        req_cfg = self._apply_headers(
            cfg=req_cfg, opts=opts, content_type=content_type, explicit_headers=headers
        )
        req_cfg = self._apply_query_params(
            cfg=req_cfg, opts=opts, query_params=query_params
        )
        req_cfg = self._apply_body(
            cfg=req_cfg, data=data, files=files, json=json, content=content
        )
        req_cfg = self._apply_timeout(cfg=req_cfg, opts=opts)

        return req_cfg

    def process_response(self, *, response=httpx.Response, cast_to: Type[T]) -> T:
        if 200 <= response.status_code < 300:
            content_type = get_content_type(response.headers)
            if response.status_code == 204 or cast_to == NoneType:
                return cast(T, None)
            elif cast_to == BinaryResponse:
                return cast(
                    T,
                    BinaryResponse(content=response.content, headers=response.headers),
                )
            else:
                if "json" in content_type or "form" in content_type:
                    return from_encodable(data=response.json(), load_with=cast_to)
                elif is_binary_content_type(content_type):
                    return cast(
                        T,
                        BinaryResponse(
                            content=response.content, headers=response.headers
                        ),
                    )
                else:
                    return from_encodable(data=response.content, load_with=cast_to)
        else:
            try:
                response_json = response.json()
            except JSONDecodeError:
                raise ApiError(status_code=response.status_code, body=response.text)
            raise ApiError(status_code=response.status_code, body=response_json)


class SyncBaseClient(BaseClient):
    def __init__(
        self,
        *,
        base_url: str,
        httpx_client: httpx.Client,
    ):
        super().__init__(base_url=base_url)
        self.httpx_client = httpx_client

    def request(
        self,
        *,
        method: str,
        path: str,
        cast_to: Type[T],
        auth_names: Optional[List[str]] = None,
        query_params: Optional[QueryParams] = None,
        headers: Optional[Dict[str, str]] = None,
        data: Optional[httpx._types.RequestData] = None,
        files: Optional[httpx._types.RequestFiles] = None,
        json: Optional[Any] = None,
        content_type: Optional[str] = None,
        content: Optional[httpx._types.RequestContent] = None,
        request_options: Optional[RequestOptions] = None,
    ) -> T:
        req_cfg = self.build_request(
            method=method,
            path=path,
            auth_names=auth_names,
            query_params=query_params,
            headers=headers,
            data=data,
            files=files,
            json=json,
            content_type=content_type,
            content=content,
            request_options=request_options,
        )

        response = self.httpx_client.request(**req_cfg)

        return self.process_response(response=response, cast_to=cast_to)


class AsyncBaseClient(BaseClient):
    def __init__(
        self,
        *,
        base_url: str,
        httpx_client: httpx.AsyncClient,
    ):
        super().__init__(base_url=base_url)
        self.httpx_client = httpx_client

    async def request(
        self,
        method: str,
        path: str,
        cast_to: Type[T],
        auth_names: Optional[List[str]] = None,
        query_params: Optional[QueryParams] = None,
        headers: Optional[Dict[str, str]] = None,
        data: Optional[httpx._types.RequestData] = None,
        files: Optional[httpx._types.RequestFiles] = None,
        json: Optional[Any] = None,
        content_type: Optional[str] = None,
        content: Optional[httpx._types.RequestContent] = None,
        request_options: Optional[RequestOptions] = None,
    ) -> T:
        req_cfg = self.build_request(
            method=method,
            path=path,
            auth_names=auth_names,
            query_params=query_params,
            headers=headers,
            data=data,
            files=files,
            json=json,
            content_type=content_type,
            content=content,
            request_options=request_options,
        )

        response = await self.httpx_client.request(**req_cfg)

        return self.process_response(response=response, cast_to=cast_to)
