"""
Perpetual Binary API - Python SDK

Official SDK for the CyMetica Perpetual Binary Trading API.

Usage:
    from perpetual_sdk import PerpetualAPI, PerpetualWebSocket

    # REST API
    api = PerpetualAPI()
    status = await api.get_market_status("ETH")
    print(f"Epoch {status['epoch']}: YES={status['yes_price']}")

    # WebSocket
    async with PerpetualWebSocket("ETH") as ws:
        async for msg in ws:
            if msg["type"] == "status":
                print(f"Time left: {msg['data']['time_remaining']}s")

@see https://cymetica.com/perpetual/api
@version 1.0.0
"""

from __future__ import annotations

import asyncio
import json
from enum import Enum
from typing import Any, AsyncIterator, Callable, Literal, Optional, TypedDict, Union

import aiohttp

# ============================================
# CONSTANTS
# ============================================

BASE_URL = "https://cymetica.com/api/v1/perpetual"
WS_URL = "wss://cymetica.com/api/v1/perpetual/ws"

CHAIN_ID = 8453  # Base L2
EPOCH_DURATION = 300  # 5 minutes
TRADING_WINDOW = 270  # 4.5 minutes

ACTIVE_MARKETS = ["ETH", "SOL", "DOGE", "AAPL_V2", "NVDA_V2", "TSLA_V2", "LINK_V2"]


# ============================================
# TYPE DEFINITIONS
# ============================================


class AssetType(str, Enum):
    CRYPTO = "crypto"
    STOCK = "stock"
    BOND = "bond"
    COMMODITY = "commodity"
    FOREX = "forex"
    DERIVATIVE = "derivative"
    STRUCTURED_PRODUCT = "structured_product"
    FUND = "fund"
    PREDICTION_CONTRACT = "prediction_contract"
    RWA = "rwa"
    INDEX = "index"
    SYNTHETIC = "synthetic"
    CUSTOM = "custom"


class Asset(TypedDict):
    symbol: str
    name: str
    asset_type: Literal["crypto", "stock", "forex", "commodity"]
    sector: Optional[str]
    market_cap: float
    oracle_tier: int
    has_market: bool
    market_address: Optional[str]
    logo_url: Optional[str]
    coingecko_id: Optional[str]


class Market(TypedDict):
    symbol: str
    name: str
    asset_type: str
    market_address: str
    yes_token: str
    no_token: str
    collateral_token: str
    oracle_tier: int
    epoch_duration: int
    chain_id: int


class MarketStatus(TypedDict):
    symbol: str
    market_address: str
    epoch: int
    start_price: str
    time_remaining: int
    trading_active: bool
    yes_price: str
    no_price: str
    yes_supply: str
    no_supply: str
    timestamp: str


class OrderbookLevel(TypedDict):
    price: str
    size: str
    total: str


class Orderbook(TypedDict):
    symbol: str
    market_address: str
    bids: list[OrderbookLevel]
    asks: list[OrderbookLevel]
    timestamp: str


class DexLinks(TypedDict):
    dexscreener: str
    geckoterminal: str
    basescan_token: str
    aerodrome: str


class MRTInfo(TypedDict):
    token_address: str
    token_symbol: str
    token_name: str
    pool_address: str
    is_graduated: bool
    total_raised: str
    graduation_progress: int
    dex_links: DexLinks


# WebSocket Message Types


class WSStatusData(TypedDict):
    epoch: int
    start_price: str
    time_remaining: int
    trading_active: bool
    yes_price: str
    no_price: str
    yes_supply: str
    no_supply: str


class WSStatusMessage(TypedDict):
    type: Literal["status"]
    symbol: str
    data: WSStatusData
    timestamp: str


class WSPriceData(TypedDict):
    price: str
    change_24h: str


class WSPriceMessage(TypedDict):
    type: Literal["price"]
    symbol: str
    data: WSPriceData
    timestamp: str


class WSEpochResolvedData(TypedDict):
    epoch: int
    outcome: Literal["YES", "NO"]
    start_price: str
    end_price: str
    yes_payout: str
    no_payout: str


class WSEpochResolvedMessage(TypedDict):
    type: Literal["epoch_resolved"]
    symbol: str
    data: WSEpochResolvedData
    timestamp: str


class WSErrorMessage(TypedDict):
    type: Literal["error"]
    message: str
    timestamp: str


WSMessage = Union[WSStatusMessage, WSPriceMessage, WSEpochResolvedMessage, WSErrorMessage]


# Error Types


class ErrorCode(str, Enum):
    MARKET_NOT_FOUND = "MARKET_NOT_FOUND"
    MARKET_CLOSED = "MARKET_CLOSED"
    INVALID_PARAMETER = "INVALID_PARAMETER"
    INVALID_AMOUNT = "INVALID_AMOUNT"
    INSUFFICIENT_BALANCE = "INSUFFICIENT_BALANCE"
    RATE_LIMIT_EXCEEDED = "RATE_LIMIT_EXCEEDED"
    INTERNAL_SERVER_ERROR = "INTERNAL_SERVER_ERROR"


class APIError(TypedDict):
    code: str
    message: str
    details: Optional[dict[str, Any]]


class APIErrorResponse(TypedDict):
    success: Literal[False]
    error: APIError


# ============================================
# EXCEPTIONS
# ============================================


class PerpetualAPIError(Exception):
    """Base exception for API errors."""

    def __init__(self, code: str, message: str, details: Optional[dict] = None):
        self.code = code
        self.message = message
        self.details = details or {}
        super().__init__(f"{code}: {message}")


class MarketNotFoundError(PerpetualAPIError):
    """Raised when market is not found."""

    def __init__(self, symbol: str):
        super().__init__("MARKET_NOT_FOUND", f"Market '{symbol}' not found", {"symbol": symbol})


class RateLimitError(PerpetualAPIError):
    """Raised when rate limit is exceeded."""

    def __init__(self, retry_after: int = 60):
        self.retry_after = retry_after
        super().__init__(
            "RATE_LIMIT_EXCEEDED",
            f"Rate limit exceeded. Retry after {retry_after}s",
            {"retry_after": retry_after},
        )


# ============================================
# API CLIENT
# ============================================


class PerpetualAPI:
    """
    Async client for the Perpetual Binary Trading API.

    Usage:
        api = PerpetualAPI()
        markets = await api.get_markets()
        status = await api.get_market_status("ETH")
    """

    def __init__(self, base_url: str = BASE_URL):
        self.base_url = base_url
        self._session: Optional[aiohttp.ClientSession] = None

    async def _get_session(self) -> aiohttp.ClientSession:
        if self._session is None or self._session.closed:
            self._session = aiohttp.ClientSession()
        return self._session

    async def close(self) -> None:
        """Close the HTTP session."""
        if self._session and not self._session.closed:
            await self._session.close()

    async def __aenter__(self) -> "PerpetualAPI":
        return self

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

    async def _request(self, method: str, path: str, **kwargs) -> Any:
        """Make an HTTP request."""
        session = await self._get_session()
        url = f"{self.base_url}{path}"

        async with session.request(method, url, **kwargs) as resp:
            if resp.status == 429:
                retry_after = int(resp.headers.get("Retry-After", 60))
                raise RateLimitError(retry_after)

            data = await resp.json()

            if resp.status == 404:
                raise MarketNotFoundError(path.split("/")[-1])

            if resp.status >= 400:
                error = data.get("error", {})
                raise PerpetualAPIError(
                    error.get("code", "UNKNOWN_ERROR"),
                    error.get("message", "Unknown error"),
                    error.get("details"),
                )

            return data

    async def get_assets(
        self,
        asset_type: Optional[str] = None,
        search: Optional[str] = None,
        page: int = 1,
        page_size: int = 50,
    ) -> list[Asset]:
        """
        Get list of tradeable assets.

        Args:
            asset_type: Filter by type (crypto, stock, forex, commodity)
            search: Search by symbol or name
            page: Page number (default: 1)
            page_size: Results per page (default: 50)

        Returns:
            List of Asset objects
        """
        params = {"page": page, "page_size": page_size}
        if asset_type:
            params["asset_type"] = asset_type
        if search:
            params["search"] = search

        return await self._request("GET", "/assets", params=params)

    async def get_markets(self) -> list[Market]:
        """
        Get list of active markets.

        Returns:
            List of Market objects with contract addresses
        """
        return await self._request("GET", "/markets")

    async def get_market_status(self, symbol: str) -> MarketStatus:
        """
        Get current status for a market.

        Args:
            symbol: Market symbol (e.g., "ETH", "NVDA_V2")

        Returns:
            MarketStatus with epoch, prices, and trading state
        """
        return await self._request("GET", f"/markets/{symbol}/status")

    async def get_orderbook(self, symbol: str) -> Orderbook:
        """
        Get orderbook for a market.

        Args:
            symbol: Market symbol

        Returns:
            Orderbook with bids and asks
        """
        return await self._request("GET", f"/markets/{symbol}/orderbook")

    async def get_mrt(self, symbol: str) -> MRTInfo:
        """
        Get MRT token info for a market.

        Args:
            symbol: Market symbol

        Returns:
            MRTInfo with token address, pool, and DEX links
        """
        return await self._request("GET", f"/markets/{symbol}/mrt")


# ============================================
# WEBSOCKET CLIENT
# ============================================


class PerpetualWebSocket:
    """
    WebSocket client for real-time market updates.

    Usage:
        async with PerpetualWebSocket("ETH") as ws:
            async for msg in ws:
                if msg["type"] == "status":
                    print(f"Time: {msg['data']['time_remaining']}s")
    """

    def __init__(
        self,
        symbol: str,
        ws_url: str = WS_URL,
        ping_interval: int = 10,
        on_message: Optional[Callable[[WSMessage], None]] = None,
    ):
        self.symbol = symbol
        self.ws_url = f"{ws_url}/{symbol}"
        self.ping_interval = ping_interval
        self.on_message = on_message
        self._session: Optional[aiohttp.ClientSession] = None
        self._ws: Optional[aiohttp.ClientWebSocketResponse] = None
        self._ping_task: Optional[asyncio.Task] = None

    async def connect(self) -> None:
        """Establish WebSocket connection."""
        self._session = aiohttp.ClientSession()
        self._ws = await self._session.ws_connect(self.ws_url)

        # Start ping loop
        self._ping_task = asyncio.create_task(self._ping_loop())

    async def close(self) -> None:
        """Close WebSocket connection."""
        if self._ping_task:
            self._ping_task.cancel()
            try:
                await self._ping_task
            except asyncio.CancelledError:
                pass

        if self._ws:
            await self._ws.close()

        if self._session:
            await self._session.close()

    async def __aenter__(self) -> "PerpetualWebSocket":
        await self.connect()
        return self

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

    async def _ping_loop(self) -> None:
        """Send pings to receive frequent status updates."""
        while True:
            await asyncio.sleep(self.ping_interval)
            if self._ws and not self._ws.closed:
                await self._ws.send_str("ping")

    async def __aiter__(self) -> AsyncIterator[WSMessage]:
        """Iterate over incoming messages."""
        if not self._ws:
            raise RuntimeError("WebSocket not connected. Use 'async with' or call connect()")

        async for msg in self._ws:
            if msg.type == aiohttp.WSMsgType.TEXT:
                if msg.data == "pong":
                    continue

                data = json.loads(msg.data)

                if self.on_message:
                    self.on_message(data)

                yield data

            elif msg.type in (aiohttp.WSMsgType.CLOSED, aiohttp.WSMsgType.ERROR):
                break

    async def send_ping(self) -> None:
        """Send ping to trigger immediate status update."""
        if self._ws and not self._ws.closed:
            await self._ws.send_str("ping")


# ============================================
# HELPER FUNCTIONS
# ============================================


async def get_market_status(symbol: str) -> MarketStatus:
    """Quick helper to get market status."""
    async with PerpetualAPI() as api:
        return await api.get_market_status(symbol)


async def get_all_markets() -> list[Market]:
    """Quick helper to get all active markets."""
    async with PerpetualAPI() as api:
        return await api.get_markets()


def parse_price(price_str: str) -> float:
    """Parse price string to float."""
    return float(price_str)


def is_trading_active(status: MarketStatus) -> bool:
    """Check if trading is active for a market."""
    return status["trading_active"] and status["time_remaining"] > 0


# ============================================
# EXAMPLE USAGE
# ============================================

if __name__ == "__main__":

    async def main():
        # REST API example
        api = PerpetualAPI()

        try:
            # Get all markets
            markets = await api.get_markets()
            print(f"Active markets: {[m['symbol'] for m in markets]}")

            # Get ETH status
            status = await api.get_market_status("ETH")
            print("\nETH Market Status:")
            print(f"  Epoch: {status['epoch']}")
            print(f"  YES Price: {status['yes_price']}")
            print(f"  NO Price: {status['no_price']}")
            print(f"  Time Remaining: {status['time_remaining']}s")
            print(f"  Trading Active: {status['trading_active']}")

        finally:
            await api.close()

        # WebSocket example (limited to 3 messages)
        print("\nWebSocket streaming (3 messages):")
        count = 0
        async with PerpetualWebSocket("ETH") as ws:
            await ws.send_ping()  # Trigger immediate status
            async for msg in ws:
                if msg["type"] == "status":
                    data = msg["data"]
                    print(f"  Epoch {data['epoch']}: {data['time_remaining']}s left")
                    count += 1
                    if count >= 3:
                        break

    asyncio.run(main())
