from ssl import SSLError
from typing import Annotated, Any, TypedDict, Unpack

import httpx

from .common import ResultResponse
from .logger import logger

SSL_CONTEXT = httpx.create_ssl_context()


class HttpKwargs(TypedDict, total=False):
  headers: dict[str, str] | None
  params: dict | None
  json: dict | list | None
  data: dict | None
  timeout: float | None
  collector_token: str | None
  collector_user: str | None


class ClientKwargs(TypedDict, total=False):
  base_url: str
  mdt_proxy: str
  proxy: str
  http2: bool
  collector_user: str | None
  collector_token: str | None
  json: dict | None
  headers: dict | None
  params: dict | None
  timeout: float | None
  transport: httpx.AsyncBaseTransport | None
  verify: bool


class AsyncClient(httpx.AsyncClient):
  mdt_proxy: str | None = None
  transport_error_status_code = 599

  def __init__(
    self,
    *args,
    mdt_proxy: str | None = None,
    transport: httpx.AsyncBaseTransport | None = None,
    proxy: str | None = None,
    collector_token: str | None = None,
    collector_user: str | None = None,
    **kwargs,
  ):
    headers = kwargs.pop('headers', None) or {}
    kwargs['headers'] = headers
    if collector_token is not None:
      headers['Authorization'] = collector_token
    if collector_user is not None:
      headers['Collector-User'] = collector_user

    if transport is None and proxy is not None and mdt_proxy is not None:
      logger.warning('proxy and mdt_proxy are both set, using mdt_proxy')
    if transport is None:
      proxy = mdt_proxy or proxy
      if proxy is not None:
        kwargs['proxy'] = proxy
      if mdt_proxy is not None:
        kwargs.setdefault('verify', False)
    verify = kwargs.get('verify', True)
    if verify is True:
      kwargs['verify'] = SSL_CONTEXT

    super().__init__(*args, transport=transport, **kwargs)

  async def handle_response(self, func, *args, **kwargs) -> ResultResponse[Any]:
    try:
      res = await func(*args, **kwargs)
    except httpx.WriteError:
      res = self._transport_error_response(
        req=kwargs.get('request') or args[0],
        detail='upstream write error',
      )
    except httpx.ReadTimeout:
      res = self._transport_error_response(
        req=kwargs.get('request') or args[0],
        detail='upstream read timeout',
      )
    except httpx.ReadError:
      res = self._transport_error_response(
        req=kwargs.get('request') or args[0],
        detail='upstream read error',
      )
    except httpx.ConnectTimeout:
      res = self._transport_error_response(
        req=kwargs.get('request') or args[0],
        detail='upstream connect timeout',
      )
    except httpx.ConnectError:
      res = self._transport_error_response(
        req=kwargs.get('request') or args[0],
        detail='upstream connect error',
      )
    except httpx.RemoteProtocolError:
      res = self._transport_error_response(
        req=kwargs.get('request') or args[0],
        detail='upstream remote protocol error',
      )
    except httpx.ProxyError:
      res = self._transport_error_response(
        req=kwargs.get('request') or args[0],
        detail='upstream proxy error',
      )
    except SSLError:
      res = self._transport_error_response(
        req=kwargs.get('request') or args[0],
        detail='upstream ssl error',
      )
    return ResultResponse(res)

  def _transport_error_response(
    self,
    req: httpx.Request,
    detail: str,
  ) -> httpx.Response:
    """Convert transport failures into a synthetic response wrapper payload."""
    logger.warning('%s: %s %s', detail, req.method, req.url)
    return httpx.Response(
      status_code=self.transport_error_status_code,
      request=req,
      json={'detail': detail},
    )

  def build_request(
    self,
    method,
    url,
    *,
    content=None,
    data=None,
    files=None,
    json=None,
    params=None,
    headers=None,
    cookies=None,
    timeout=None,
    extensions=None,
    collector_token: str | None = None,
    collector_user: Annotated[str | None, 'userid:appid'] = None,
    **kwargs,
  ):
    headers = headers or {}
    if collector_token is not None:
      headers['Authorization'] = collector_token
    if collector_user is not None:
      headers['Collector-User'] = collector_user
    if timeout is not None:
      kwargs['timeout'] = timeout
    return super().build_request(
      method,
      url,
      content=content,
      data=data,
      files=files,
      json=json,
      params=params,
      headers=headers,
      cookies=cookies,
      extensions=extensions,
      **kwargs,
    )

  async def post(
    self,
    url: str,
    collector_token: str | None = None,
    collector_user: str | None = None,
    **kwargs: Unpack[HttpKwargs],
  ) -> ResultResponse[Any]:
    return await self.send(
      self.build_request(
        'POST',
        url,
        collector_token=collector_token,
        collector_user=collector_user,
        **kwargs,
      )
    )

  async def get(
    self,
    url: str,
    collector_token: str | None = None,
    collector_user: str | None = None,
    **kwargs: Unpack[HttpKwargs],
  ) -> ResultResponse[Any]:
    return await self.send(
      self.build_request(
        'GET',
        url,
        collector_token=collector_token,
        collector_user=collector_user,
        **kwargs,
      )
    )

  async def put(
    self,
    url: str,
    collector_token: str | None = None,
    collector_user: str | None = None,
    **kwargs: Unpack[HttpKwargs],
  ) -> ResultResponse[Any]:
    return await self.send(
      self.build_request(
        'PUT',
        url,
        collector_token=collector_token,
        collector_user=collector_user,
        **kwargs,
      )
    )

  async def patch(
    self,
    url: str,
    collector_token: str | None = None,
    collector_user: str | None = None,
    **kwargs: Unpack[HttpKwargs],
  ) -> ResultResponse[Any]:
    return await self.send(
      self.build_request(
        'PATCH',
        url,
        collector_token=collector_token,
        collector_user=collector_user,
        **kwargs,
      )
    )

  async def delete(
    self,
    url: str,
    collector_token: str | None = None,
    collector_user: str | None = None,
    **kwargs: Unpack[HttpKwargs],
  ) -> ResultResponse[Any]:
    return await self.send(
      self.build_request(
        'DELETE',
        url,
        collector_token=collector_token,
        collector_user=collector_user,
        **kwargs,
      )
    )

  async def send(self, *args, **kwargs) -> ResultResponse[Any]:
    return await self.handle_response(super().send, *args, **kwargs)

  async def post_or_stream(
    self,
    url,
    stream: bool = False,
    **kwargs: Unpack[ClientKwargs],
  ) -> ResultResponse[Any]:
    req = self.build_request('POST', url, **kwargs)
    return await self.send(req, stream=stream)
