"""Sync and async clients for the Krystal Cloud API."""

from __future__ import annotations

from typing import Any, Optional, Sequence

import httpx

from .exceptions import AuthError, InsufficientCreditsError, KrystalAPIError

BASE_URL = "https://cloud-api.krystal.app"


def _build_params(mapping: dict[str, Any]) -> dict[str, Any]:
    """Drop None values and flatten lists into repeated keys."""
    params: list[tuple[str, str]] = []
    for k, v in mapping.items():
        if v is None:
            continue
        if isinstance(v, (list, tuple)):
            for item in v:
                params.append((k, str(item)))
        elif isinstance(v, bool):
            params.append((k, str(v).lower()))
        else:
            params.append((k, str(v)))
    return params  # type: ignore[return-value]


def _check(resp: httpx.Response) -> Any:
    if resp.status_code == 401:
        raise AuthError("Authentication failed", 401, resp.json() if resp.content else None)
    if resp.status_code == 402:
        raise InsufficientCreditsError("Insufficient credits", 402, resp.json() if resp.content else None)
    if resp.status_code >= 400:
        try:
            body = resp.json()
        except Exception:
            body = None
        raise KrystalAPIError(f"API error {resp.status_code}", resp.status_code, body)
    return resp.json()


class KrystalCloud:
    """Synchronous client for the Krystal Cloud API.

    Usage::

        from krystal_cloud import KrystalCloud

        client = KrystalCloud(api_key="your-key")
        chains = client.get_chains()
    """

    def __init__(self, api_key: str, *, base_url: str = BASE_URL, timeout: float = 30.0) -> None:
        self._client = httpx.Client(
            base_url=base_url,
            headers={"KC-APIKey": api_key},
            timeout=timeout,
        )

    def close(self) -> None:
        self._client.close()

    def __enter__(self) -> KrystalCloud:
        return self

    def __exit__(self, *args: Any) -> None:
        self.close()

    def _get(self, path: str, params: Any = None) -> Any:
        return _check(self._client.get(path, params=params))

    # -- Balances --

    def get_balances(
        self,
        wallet: str,
        *,
        chain_ids: Optional[Sequence[int]] = None,
        token_address: Optional[str] = None,
        include_dust_token: Optional[bool] = None,
    ) -> Any:
        """Get wallet token balances."""
        return self._get(f"/v1/balances/{wallet}", _build_params({
            "chainIds[]": chain_ids,
            "tokenAddress": token_address,
            "includeDustToken": include_dust_token,
        }))

    # -- Chains --

    def get_chains(self) -> Any:
        """Get all supported chains."""
        return self._get("/v1/chains")

    def get_chain(self, chain_id: int) -> Any:
        """Get chain stats by ID."""
        return self._get(f"/v1/chains/{chain_id}")

    # -- Pools --

    def get_pools(
        self,
        *,
        chain_id: Optional[int] = None,
        factory_address: Optional[str] = None,
        protocol: Optional[str] = None,
        token: Optional[str] = None,
        sort_by: Optional[str] = None,
        min_tvl: Optional[float] = None,
        min_volume_24h: Optional[float] = None,
        limit: Optional[int] = None,
        offset: Optional[int] = None,
        with_incentives: Optional[bool] = None,
        include_token_price: Optional[bool] = None,
    ) -> Any:
        """Get pools with optional filters."""
        return self._get("/v1/pools", _build_params({
            "chainId": chain_id,
            "factoryAddress": factory_address,
            "protocol": protocol,
            "token": token,
            "sortBy": sort_by,
            "minTvl": min_tvl,
            "minVolume24h": min_volume_24h,
            "limit": limit,
            "offset": offset,
            "withIncentives": with_incentives,
            "includeTokenPrice": include_token_price,
        }))

    def get_pool(
        self,
        chain_id: int,
        pool_address: str,
        *,
        factory_address: Optional[str] = None,
        with_incentives: Optional[bool] = None,
    ) -> Any:
        """Get pool detail."""
        return self._get(f"/v1/pools/{chain_id}/{pool_address}", _build_params({
            "factoryAddress": factory_address,
            "withIncentives": with_incentives,
        }))

    def get_pool_historical(
        self,
        chain_id: int,
        pool_address: str,
        *,
        start_time: Optional[int] = None,
        end_time: Optional[int] = None,
    ) -> Any:
        """Get pool historical data."""
        return self._get(f"/v1/pools/{chain_id}/{pool_address}/historical", _build_params({
            "startTime": start_time,
            "endTime": end_time,
        }))

    def get_pool_ticks(
        self,
        chain_id: int,
        pool_address: str,
        *,
        factory_address: Optional[str] = None,
    ) -> Any:
        """Get pool ticks."""
        return self._get(f"/v1/pools/{chain_id}/{pool_address}/ticks", _build_params({
            "factoryAddress": factory_address,
        }))

    def get_pool_transactions(
        self,
        chain_id: int,
        pool_address: str,
        *,
        factory_address: Optional[str] = None,
        start_time: Optional[int] = None,
        end_time: Optional[int] = None,
        limit: Optional[int] = None,
        offset: Optional[int] = None,
    ) -> Any:
        """Get pool transactions."""
        return self._get(f"/v1/pools/{chain_id}/{pool_address}/transactions", _build_params({
            "factoryAddress": factory_address,
            "startTime": start_time,
            "endTime": end_time,
            "limit": limit,
            "offset": offset,
        }))

    # -- Positions --

    def get_positions(
        self,
        wallet: str,
        *,
        position_status: Optional[str] = None,
        protocols: Optional[Sequence[str]] = None,
        chain_ids: Optional[Sequence[int]] = None,
        include_closed_position: Optional[bool] = None,
        include_spam_position: Optional[bool] = None,
        offset: Optional[int] = None,
        limit: Optional[int] = None,
        order_by: Optional[str] = None,
        order_desc: Optional[bool] = None,
    ) -> Any:
        """Get user positions."""
        return self._get("/v1/positions", _build_params({
            "wallet": wallet,
            "positionStatus": position_status,
            "protocols[]": protocols,
            "chainIds[]": chain_ids,
            "includeClosedPosition": include_closed_position,
            "includeSpamPosition": include_spam_position,
            "offset": offset,
            "limit": limit,
            "orderBy": order_by,
            "orderDesc": order_desc,
        }))

    def get_position(self, chain_id: int, position_id: str, *, wallet: Optional[str] = None) -> Any:
        """Get position detail."""
        return self._get(f"/v1/positions/{chain_id}/{position_id}", _build_params({"wallet": wallet}))

    def get_position_performance(
        self,
        chain_id: int,
        position_id: str,
        *,
        wallet: Optional[str] = None,
        timeframe: Optional[str] = None,
    ) -> Any:
        """Get position historical performance."""
        return self._get(f"/v1/positions/{chain_id}/{position_id}/historicalPerformance", _build_params({
            "wallet": wallet,
            "timeframe": timeframe,
        }))

    def get_position_transactions(
        self,
        chain_id: int,
        position_id: str,
        *,
        wallet: Optional[str] = None,
        start_timestamp: Optional[int] = None,
        end_timestamp: Optional[int] = None,
    ) -> Any:
        """Get position transactions."""
        return self._get(f"/v1/positions/{chain_id}/{position_id}/transactions", _build_params({
            "wallet": wallet,
            "startTimestamp": start_timestamp,
            "endTimestamp": end_timestamp,
        }))

    # -- Protocols --

    def get_protocols(self) -> Any:
        """Get supported protocols."""
        return self._get("/v1/protocols")

    # -- Strategies --

    def get_strategies(
        self,
        wallet: str,
        *,
        status: Optional[str] = None,
        page: Optional[int] = None,
        per_page: Optional[int] = None,
    ) -> Any:
        """Get strategies by wallet."""
        return self._get("/v1/strategies", _build_params({
            "wallet": wallet,
            "status": status,
            "page": page,
            "perPage": per_page,
        }))

    def get_strategy_positions(
        self,
        strategy_id: str,
        *,
        page: Optional[int] = None,
        per_page: Optional[int] = None,
    ) -> Any:
        """Get positions by strategy."""
        return self._get(f"/v1/strategies/{strategy_id}/positions", _build_params({
            "page": page,
            "perPage": per_page,
        }))


class AsyncKrystalCloud:
    """Async client for the Krystal Cloud API.

    Usage::

        from krystal_cloud import AsyncKrystalCloud

        async with AsyncKrystalCloud(api_key="your-key") as client:
            chains = await client.get_chains()
    """

    def __init__(self, api_key: str, *, base_url: str = BASE_URL, timeout: float = 30.0) -> None:
        self._client = httpx.AsyncClient(
            base_url=base_url,
            headers={"KC-APIKey": api_key},
            timeout=timeout,
        )

    async def close(self) -> None:
        await self._client.aclose()

    async def __aenter__(self) -> AsyncKrystalCloud:
        return self

    async def __aexit__(self, *args: Any) -> None:
        await self.close()

    async def _get(self, path: str, params: Any = None) -> Any:
        return _check(await self._client.get(path, params=params))

    # -- Balances --

    async def get_balances(self, wallet: str, *, chain_ids: Optional[Sequence[int]] = None, token_address: Optional[str] = None, include_dust_token: Optional[bool] = None) -> Any:
        """Get wallet token balances."""
        return await self._get(f"/v1/balances/{wallet}", _build_params({"chainIds[]": chain_ids, "tokenAddress": token_address, "includeDustToken": include_dust_token}))

    # -- Chains --

    async def get_chains(self) -> Any:
        """Get all supported chains."""
        return await self._get("/v1/chains")

    async def get_chain(self, chain_id: int) -> Any:
        """Get chain stats by ID."""
        return await self._get(f"/v1/chains/{chain_id}")

    # -- Pools --

    async def get_pools(self, *, chain_id: Optional[int] = None, factory_address: Optional[str] = None, protocol: Optional[str] = None, token: Optional[str] = None, sort_by: Optional[str] = None, min_tvl: Optional[float] = None, min_volume_24h: Optional[float] = None, limit: Optional[int] = None, offset: Optional[int] = None, with_incentives: Optional[bool] = None, include_token_price: Optional[bool] = None) -> Any:
        """Get pools with optional filters."""
        return await self._get("/v1/pools", _build_params({"chainId": chain_id, "factoryAddress": factory_address, "protocol": protocol, "token": token, "sortBy": sort_by, "minTvl": min_tvl, "minVolume24h": min_volume_24h, "limit": limit, "offset": offset, "withIncentives": with_incentives, "includeTokenPrice": include_token_price}))

    async def get_pool(self, chain_id: int, pool_address: str, *, factory_address: Optional[str] = None, with_incentives: Optional[bool] = None) -> Any:
        """Get pool detail."""
        return await self._get(f"/v1/pools/{chain_id}/{pool_address}", _build_params({"factoryAddress": factory_address, "withIncentives": with_incentives}))

    async def get_pool_historical(self, chain_id: int, pool_address: str, *, start_time: Optional[int] = None, end_time: Optional[int] = None) -> Any:
        """Get pool historical data."""
        return await self._get(f"/v1/pools/{chain_id}/{pool_address}/historical", _build_params({"startTime": start_time, "endTime": end_time}))

    async def get_pool_ticks(self, chain_id: int, pool_address: str, *, factory_address: Optional[str] = None) -> Any:
        """Get pool ticks."""
        return await self._get(f"/v1/pools/{chain_id}/{pool_address}/ticks", _build_params({"factoryAddress": factory_address}))

    async def get_pool_transactions(self, chain_id: int, pool_address: str, *, factory_address: Optional[str] = None, start_time: Optional[int] = None, end_time: Optional[int] = None, limit: Optional[int] = None, offset: Optional[int] = None) -> Any:
        """Get pool transactions."""
        return await self._get(f"/v1/pools/{chain_id}/{pool_address}/transactions", _build_params({"factoryAddress": factory_address, "startTime": start_time, "endTime": end_time, "limit": limit, "offset": offset}))

    # -- Positions --

    async def get_positions(self, wallet: str, *, position_status: Optional[str] = None, protocols: Optional[Sequence[str]] = None, chain_ids: Optional[Sequence[int]] = None, include_closed_position: Optional[bool] = None, include_spam_position: Optional[bool] = None, offset: Optional[int] = None, limit: Optional[int] = None, order_by: Optional[str] = None, order_desc: Optional[bool] = None) -> Any:
        """Get user positions."""
        return await self._get("/v1/positions", _build_params({"wallet": wallet, "positionStatus": position_status, "protocols[]": protocols, "chainIds[]": chain_ids, "includeClosedPosition": include_closed_position, "includeSpamPosition": include_spam_position, "offset": offset, "limit": limit, "orderBy": order_by, "orderDesc": order_desc}))

    async def get_position(self, chain_id: int, position_id: str, *, wallet: Optional[str] = None) -> Any:
        """Get position detail."""
        return await self._get(f"/v1/positions/{chain_id}/{position_id}", _build_params({"wallet": wallet}))

    async def get_position_performance(self, chain_id: int, position_id: str, *, wallet: Optional[str] = None, timeframe: Optional[str] = None) -> Any:
        """Get position historical performance."""
        return await self._get(f"/v1/positions/{chain_id}/{position_id}/historicalPerformance", _build_params({"wallet": wallet, "timeframe": timeframe}))

    async def get_position_transactions(self, chain_id: int, position_id: str, *, wallet: Optional[str] = None, start_timestamp: Optional[int] = None, end_timestamp: Optional[int] = None) -> Any:
        """Get position transactions."""
        return await self._get(f"/v1/positions/{chain_id}/{position_id}/transactions", _build_params({"wallet": wallet, "startTimestamp": start_timestamp, "endTimestamp": end_timestamp}))

    # -- Protocols --

    async def get_protocols(self) -> Any:
        """Get supported protocols."""
        return await self._get("/v1/protocols")

    # -- Strategies --

    async def get_strategies(self, wallet: str, *, status: Optional[str] = None, page: Optional[int] = None, per_page: Optional[int] = None) -> Any:
        """Get strategies by wallet."""
        return await self._get("/v1/strategies", _build_params({"wallet": wallet, "status": status, "page": page, "perPage": per_page}))

    async def get_strategy_positions(self, strategy_id: str, *, page: Optional[int] = None, per_page: Optional[int] = None) -> Any:
        """Get positions by strategy."""
        return await self._get(f"/v1/strategies/{strategy_id}/positions", _build_params({"page": page, "perPage": per_page}))
