from typing_extensions import TypeVar, Any, TypedDict, Literal, NotRequired
import os
from dataclasses import dataclass
import httpx
import pydantic

from .util import path_join
from .http import HttpMixin
from .exc import ApiError

T = TypeVar('T')

@dataclass(kw_only=True)
class Endpoint(HttpMixin):
  validate: bool = True

  def should_validate(self, validate_param: bool | None = None) -> bool:
    return self.validate if validate_param is None else validate_param

  def raise_error(self, response: httpx.Response):
    try:
      payload: Any = response.json()
    except Exception:
      payload = response.text
    raise ApiError(response.status_code, payload)

ALCHEMY_DATA_API_URL = 'https://api.g.alchemy.com'

@dataclass
class PortfolioEndpoint(Endpoint):
  @classmethod
  def new(cls, *, api_key: str | None = None, validate: bool = True):
    if api_key is None:
      api_key = os.environ['ALCHEMY_API_KEY']
    url = path_join(ALCHEMY_DATA_API_URL, f'data/v1/{api_key}')
    return cls(base_url=url, validate=validate)

@dataclass
class PricesEndpoint(Endpoint):
  @classmethod
  def new(cls, *, api_key: str | None = None, validate: bool = True):
    if api_key is None:
      api_key = os.environ['ALCHEMY_API_KEY']
    url = path_join(ALCHEMY_DATA_API_URL, f'prices/v1/{api_key}')
    return cls(base_url=url, validate=validate)

ETHEREUM_ALCHEMY_URL = 'https://eth-mainnet.g.alchemy.com/v2'
BNB_ALCHEMY_URL = 'https://bnb-mainnet.g.alchemy.com/v2'
POLYGON_ALCHEMY_URL = 'https://polygon-mainnet.g.alchemy.com/v2'
BASE_ALCHEMY_URL = 'https://base-mainnet.g.alchemy.com/v2'
AVAX_ALCHEMY_URL = 'https://avax-mainnet.g.alchemy.com/v2'
OPTIMISM_ALCHEMY_URL = 'https://opt-mainnet.g.alchemy.com/v2'
ARBITRUM_ALCHEMY_URL = 'https://arb-mainnet.g.alchemy.com/v2'
GNOSIS_ALCHEMY_URL = 'https://gnosis-mainnet.g.alchemy.com/v2'
CELO_ALCHEMY_URL = 'https://celo-mainnet.g.alchemy.com/v2'

class Response(TypedDict):
  jsonrpc: Literal['2.0']
  id: int | str | None
  result: NotRequired[Any]
  error: NotRequired[Any]

rpc_response_adapter = pydantic.TypeAdapter(Response)

@dataclass
class RpcEndpoint(Endpoint):
  @classmethod
  def new(cls, node_url: str, *, api_key: str | None = None, validate: bool = True):
    if api_key is None:
      api_key = os.environ['ALCHEMY_API_KEY']
    url = path_join(node_url, api_key)
    return cls(base_url=url, validate=validate)

  @classmethod
  def ethereum(cls, *, api_key: str | None = None, validate: bool = True):
    return cls.new(ETHEREUM_ALCHEMY_URL, api_key=api_key, validate=validate)

  @classmethod
  def bnb(cls, *, api_key: str | None = None, validate: bool = True):
    return cls.new(BNB_ALCHEMY_URL, api_key=api_key, validate=validate)

  @classmethod
  def polygon(cls, *, api_key: str | None = None, validate: bool = True):
    return cls.new(POLYGON_ALCHEMY_URL, api_key=api_key, validate=validate)

  @classmethod
  def base(cls, *, api_key: str | None = None, validate: bool = True):
    return cls.new(BASE_ALCHEMY_URL, api_key=api_key, validate=validate)

  @classmethod
  def avalanche(cls, *, api_key: str | None = None, validate: bool = True):
    return cls.new(AVAX_ALCHEMY_URL, api_key=api_key, validate=validate)

  @classmethod
  def optimism(cls, *, api_key: str | None = None, validate: bool = True):
    return cls.new(OPTIMISM_ALCHEMY_URL, api_key=api_key, validate=validate)

  @classmethod
  def arbitrum(cls, *, api_key: str | None = None, validate: bool = True):
    return cls.new(ARBITRUM_ALCHEMY_URL, api_key=api_key, validate=validate)

  @classmethod
  def gnosis(cls, *, api_key: str | None = None, validate: bool = True):
    return cls.new(GNOSIS_ALCHEMY_URL, api_key=api_key, validate=validate)

  @classmethod
  def celo(cls, *, api_key: str | None = None, validate: bool = True):
    return cls.new(CELO_ALCHEMY_URL, api_key=api_key, validate=validate)

  async def rpc_request(self, method: str, params: Any, *, id: int = 1, validate: bool | None = None) -> Any:
    r = await self.http.request('POST', self.base_url, json={
      'jsonrpc': '2.0',
      'id': id,
      'method': method,
      'params': [params],
    })
    if r.status_code != 200:
      self.raise_error(r)
    obj = rpc_response_adapter.validate_json(r.text) if self.should_validate(validate) else r.json()
    if 'error' in obj:
      raise ApiError(r.status_code, obj['error'])
    elif not 'result' in obj:
      raise ApiError(r.status_code, obj)
    return obj['result']

@dataclass
class Router(Endpoint):
  def __post_init__(self):
    for field, cls in self.__annotations__.items():
      if issubclass(cls, Endpoint) or issubclass(cls, Router):
        setattr(self, field, cls(base_url=self.base_url, http=self.http, validate=self.validate))
