from functools import wraps
from json import JSONDecodeError
from typing import (
  Any,
  Awaitable,
  Callable,
  Generic,
  ParamSpec,
  Self,
  Type,
  TypeVar,
  cast,
  overload,
)

import httpx
from pydantic import BaseModel, ValidationError

from .exception import RCError
from .logger import logger

P = ParamSpec('P')
T = TypeVar('T', bound=BaseModel)
M = TypeVar('M', bound=BaseModel)


class ResultResponse(Generic[T]):
  res: httpx.Response
  model: Type[T] | None = None
  rc_response: bool = False

  def __init__(
    self,
    res: httpx.Response,
    model: Type[T] | None = None,
    rc_response: bool = False,
  ):
    self.model = model
    self.res = res
    self.rc_response = rc_response

  def __getattr__(self, name: str) -> Any:
    """Delegate unknown attributes to the wrapped `httpx.Response`."""
    return getattr(self.res, name)

  @classmethod
  def from_dict(
    cls,
    json: Any,
    model: Type[T] | None = None,
    method: str = 'GET',
    url: str = 'http://example.com',
    status_code: int = 200,
  ) -> Self:
    """Build a response wrapper from an in-memory JSON payload."""
    return cls(
      res=httpx.Response(
        status_code=status_code,
        json=json,
        request=httpx.Request(method=method, url=url),
      ),
      model=model,
    )

  @property
  def content(self) -> bytes:
    return self.res.content

  @property
  def request(self) -> httpx.Request:
    return self.res.request

  @property
  def status_code(self) -> int:
    return self.res.status_code

  @overload
  def with_config(
    self,
    model: Type[M],
    rc_response: bool | None = None,
  ) -> 'ResultResponse[M]': ...

  @overload
  def with_config(
    self,
    model: None = None,
    rc_response: bool | None = None,
  ) -> 'ResultResponse[T]': ...

  def with_config(
    self,
    model: Type[T] | None = None,
    rc_response: bool | None = None,
  ) -> 'ResultResponse[T]':
    """Return a new wrapper over the same response with updated parsing config."""
    return cast(
      ResultResponse[T],
      type(self)(
        self.res,
        model=self.model if model is None else model,
        rc_response=self.rc_response if rc_response is None else rc_response,
      ),
    )

  def _check_rc(self, result: Any, raise_if_rc: bool = False):
    if not self.rc_response:
      return result
    if not isinstance(result, dict):
      return result
    rc: int | None = result.get('rc')
    if rc != 0:
      if raise_if_rc:
        raise RCError(result.get('message'), rc)
      logger.error(
        '%s rc error %s: %s',
        type(self.model),
        rc,
        result.get('message', 'no message'),
      )
    return result.get('result')

  def raise_for_status(self) -> Self:
    """Raise on non-success HTTP status and return `self` for chaining."""
    self.res.raise_for_status()
    return self

  def json_or_raise(self) -> Any:
    """Return JSON payload or raise on HTTP, decode, or RC errors."""
    self.raise_for_status()
    try:
      result = self.res.json()
    except JSONDecodeError:
      logger.warning('json decode error %s', self.res.text[:100000])
      raise
    return self._check_rc(result, raise_if_rc=True)

  def content_or_raise(self) -> bytes:
    """Return response bytes or raise on non-success HTTP status."""
    self.raise_for_status()
    return self.res.content

  def text_or_raise(self) -> str:
    """Return response text or raise on non-success HTTP status."""
    self.raise_for_status()
    return self.res.text

  def rc_result_or_none(self) -> dict | list | None:
    """Return the unwrapped rc payload, or None when rc/status is invalid."""
    if self.res.status_code != 200:
      logger.warning(
        '%s status code %d',
        self.res.request.url.path,
        self.res.status_code,
      )
      return None
    try:
      result = self.res.json()
    except JSONDecodeError:
      logger.warning('json decode error %s', self.res.text)
      return None
    if not isinstance(result, dict):
      return result
    if result.get('rc') != 0:
      logger.error(
        '%s rc error %s: %s',
        self.res.request.url.path,
        result.get('rc'),
        result.get('message', 'no message'),
      )
      return None
    return result.get('result')

  def json_or_none(self) -> Any:
    try:
      return self.json_or_raise()
    except (httpx.HTTPStatusError, JSONDecodeError, RCError):
      logger.warning(
        '%s status code %d',
        self.res.request.url.path,
        self.res.status_code,
      )
      return None

  def content_or_none(self) -> bytes | None:
    try:
      return self.content_or_raise()
    except httpx.HTTPStatusError:
      logger.warning(
        '%s status code %d\n%s',
        self.res.request.url.path,
        self.res.status_code,
        self.res.text,
      )
      return None

  def text_or_none(self) -> str | None:
    try:
      return self.text_or_raise()
    except httpx.HTTPStatusError:
      logger.warning(
        '%s status code %d\n%s',
        self.res.request.url.path,
        self.res.status_code,
        self.res.text,
      )
      return None

  def rc_result_or_raise(self) -> dict | list:
    """Return the unwrapped rc payload or raise when `rc != 0`."""
    result = self.raise_for_status().res.json()
    if not isinstance(result, dict):
      return result
    rc = result.get('rc')
    if rc != 0:
      raise RCError(result.get('message'), rc)
    return result.get('result')

  def pydantic_or_none(self) -> T | None:
    if self.model is None:
      raise ValueError('model is not set')
    try:
      payload = self.json_or_raise()
    except (httpx.HTTPStatusError, JSONDecodeError, RCError):
      logger.warning(
        '%s status code %d',
        self.res.request.url.path,
        self.res.status_code,
      )
      return None
    try:
      return self.model(**payload)
    except ValidationError as err:
      logger.error('%s error %r\n%s', self.model.__name__, payload, str(err))
      return None

  def pydantic_list_or_none(self) -> list[T] | None:
    if self.model is None:
      raise ValueError('model is not set')
    try:
      payload = self.json_or_raise()
    except (httpx.HTTPStatusError, JSONDecodeError, RCError):
      logger.warning(
        '%s status code %d',
        self.res.request.url.path,
        self.res.status_code,
      )
      return None
    try:
      return [self.model(**item) for item in payload]
    except ValidationError as err:
      logger.error('%s error %r\n%s', self.model.__name__, payload, str(err))
      return None

  def pydantic_or_raise(self) -> T:
    if self.model is None:
      raise ValueError('model is not set')
    payload = self.json_or_raise()
    return self.model(**payload)

  def pydantic_list_or_raise(self) -> list[T]:
    """Return a parsed list of pydantic models or raise on failure."""
    if self.model is None:
      raise ValueError('model is not set')
    payload = self.json_or_raise()
    return [self.model(**item) for item in payload]

  async def aclose(self):
    await self.res.aclose()


@overload
def patch_response_class(
  model: Type[T],
  rc_response: bool = False,
) -> Callable[
  [Callable[P, Awaitable[httpx.Response | ResultResponse[Any]]]],
  Callable[P, Awaitable[ResultResponse[T]]],
]: ...


@overload
def patch_response_class(
  model: None = None,
  rc_response: bool = False,
) -> Callable[
  [Callable[P, Awaitable[httpx.Response | ResultResponse[Any]]]],
  Callable[P, Awaitable[ResultResponse[Any]]],
]: ...


def patch_response_class(
  model: Type[T] | None = None,
  rc_response: bool = False,
) -> Callable[
  [Callable[P, Awaitable[httpx.Response | ResultResponse[Any]]]],
  Callable[P, Awaitable[ResultResponse[T] | ResultResponse[Any]]],
]:
  def decorator_wrapper(
    func: Callable[P, Awaitable[httpx.Response | ResultResponse[Any]]],
  ) -> Callable[P, Awaitable[ResultResponse[T] | ResultResponse[Any]]]:
    @wraps(func)
    async def wrapper(
      *args: P.args, **kwargs: P.kwargs
    ) -> ResultResponse[T] | ResultResponse[Any]:
      res = await func(*args, **kwargs)
      if isinstance(res, ResultResponse):
        return res.with_config(model=model, rc_response=rc_response)
      return ResultResponse(res, model=model, rc_response=rc_response)

    return wrapper

  return decorator_wrapper
