"""Async client for the Talarion prediction market maker API."""

from __future__ import annotations

from datetime import datetime
from typing import Any
from uuid import UUID

import httpx

from talarion.exceptions import (
    AuthenticationError,
    ConflictError,
    NotFoundError,
    QuoteProcessingError,
    ServerError,
    TalarionError,
    ValidationError,
)
from talarion.models import (
    CancelResult,
    Instrument,
    OperationResult,
    OperationSequence,
    Quote,
    RelayResult,
    SafeOperation,
    Settlement,
    SubmitResult,
    Trade,
    UserPnl,
)

from talarion._constants import DEFAULT_BASE_URL


class TalarionClient:
    """Async client for the Talarion prediction market maker API.

    Usage::

        async with TalarionClient(api_key="tlrn_...") as client:
            quote = await client.get_quote("instrument-id", buy_yes=True, amount=10)
    """

    def __init__(
        self,
        api_key: str,
        *,
        base_url: str = DEFAULT_BASE_URL,
        timeout: float = 30.0,
        httpx_client: httpx.AsyncClient | None = None,
    ):
        self._api_key = api_key
        self._base_url = base_url.rstrip("/")
        self._owns_client = httpx_client is None
        self._client = httpx_client or httpx.AsyncClient(
            base_url=self._base_url,
            headers={"x-api-key": self._api_key},
            timeout=timeout,
        )

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

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

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

    # -- Internal helpers ------------------------------------------------------

    @staticmethod
    def _raise_for_status(resp: httpx.Response) -> None:
        if resp.status_code == 200:
            return
        detail = None
        try:
            detail = resp.json().get("detail")
        except Exception:
            pass
        msg = detail or resp.text or f"HTTP {resp.status_code}"
        match resp.status_code:
            case 202:
                raise QuoteProcessingError(msg, 202, detail)
            case 400:
                raise ValidationError(msg, 400, detail)
            case 401:
                raise AuthenticationError(msg, 401, detail)
            case 404:
                raise NotFoundError(msg, 404, detail)
            case 409:
                raise ConflictError(msg, 409, detail)
            case code if code >= 500:
                raise ServerError(msg, code, detail)
            case _:
                raise TalarionError(msg, resp.status_code, detail)

    @staticmethod
    def _parse_datetime(value: str) -> datetime:
        return datetime.fromisoformat(value)

    @staticmethod
    def _parse_instrument(data: dict) -> Instrument:
        return Instrument(
            instrument_id=data["instrument_id"],
            title=data["title"],
            rules=data["rules"],
            start_time=datetime.fromisoformat(data["start_time"]),
            resolution_time=datetime.fromisoformat(data["resolution_time"]),
        )

    @staticmethod
    def _parse_quote(data: dict) -> Quote:
        return Quote(
            instrument_id=data["instrument_id"],
            user_id=data.get("user_id"),
            amount=data["amount"],
            price=data["price"],
            buy_yes=data["buy_yes"],
            expiry_utc=datetime.fromisoformat(data["expiry_utc"]),
        )

    @staticmethod
    def _parse_safe_operation(data: dict) -> SafeOperation:
        return SafeOperation(
            operation_id=UUID(data["operation_id"]),
            description=data["description"],
            eip_712_tx=data["eip_712_tx"],
        )

    @classmethod
    def _parse_operation_sequence(cls, data: dict) -> OperationSequence:
        return OperationSequence(
            sequence_id=UUID(data["sequence_id"]),
            operations=[cls._parse_safe_operation(op) for op in data["operations"]],
        )

    @classmethod
    def _parse_operation_result(cls, data: dict) -> OperationResult:
        return OperationResult(
            operation=cls._parse_safe_operation(data["operation"]),
            successful=data["successful"],
            transaction_hash=data.get("transaction_hash"),
        )

    @staticmethod
    def _parse_trade(data: dict) -> Trade:
        return Trade(
            instrument_id=data["instrument_id"],
            user_id=data["user_id"],
            amount=data["amount"],
            price=data["price"],
            status=data.get("status", "pending"),
        )

    # -- Instruments -----------------------------------------------------------

    async def generate_instrument(
        self,
        query: str,
        resolution_time: datetime,
        *,
        user_id: str | None = None,
        tag: str | None = None,
    ) -> Instrument:
        """Generate a new prediction market from a natural-language query."""
        body: dict[str, Any] = {
            "query": query,
            "resolution_time": resolution_time.isoformat(),
        }
        if user_id is not None:
            body["user_id"] = user_id
        if tag is not None:
            body["tag"] = tag
        resp = await self._client.post("/api/instrument/generate", json=body)
        self._raise_for_status(resp)
        return self._parse_instrument(resp.json())

    async def get_instrument(self, instrument_id: str) -> Instrument:
        """Fetch details of an existing instrument."""
        resp = await self._client.get(f"/api/instrument/get/{instrument_id}")
        self._raise_for_status(resp)
        return self._parse_instrument(resp.json())

    async def get_settlement(self, instrument_id: str) -> Settlement:
        """Get the settlement value for a resolved instrument."""
        resp = await self._client.get(f"/api/instrument/{instrument_id}/settlement")
        self._raise_for_status(resp)
        data = resp.json()
        return Settlement(
            instrument_id=data["instrument_id"],
            settlement_value=data["settlement_value"],
        )

    # -- Quotes & Orders -------------------------------------------------------

    async def get_quote(
        self,
        instrument_id: str,
        buy_yes: bool,
        *,
        amount: float | None = None,
        dollars: float | None = None,
        user_id: str | None = None,
    ) -> Quote:
        """Request a price quote for an instrument.

        Provide either ``amount`` (number of contracts) or ``dollars``
        (dollar notional), but not both.
        """
        body: dict[str, Any] = {
            "instrument_id": instrument_id,
            "buy_yes": buy_yes,
        }
        if amount is not None:
            body["amount"] = amount
        if dollars is not None:
            body["dollars"] = dollars
        if user_id is not None:
            body["user_id"] = user_id
        resp = await self._client.post("/api/order/quote", json=body)
        self._raise_for_status(resp)
        return self._parse_quote(resp.json())

    async def submit_order(
        self,
        instrument_id: str,
        user_id: str,
        caller_address: str,
        amount: float,
        price: float,
        buy_yes: bool,
        *,
        funder_address: str | None = None,
        starting_nonce: int | None = None,
    ) -> SubmitResult:
        """Submit a trade order at the quoted price.

        Returns EIP-712 typed data that must be signed and relayed.
        """
        body: dict[str, Any] = {
            "instrument_id": instrument_id,
            "user_id": user_id,
            "caller_address": caller_address,
            "amount": amount,
            "price": price,
            "buy_yes": buy_yes,
        }
        if funder_address is not None:
            body["funder_address"] = funder_address
        if starting_nonce is not None:
            body["starting_nonce"] = starting_nonce
        resp = await self._client.post("/api/order/submit", json=body)
        self._raise_for_status(resp)
        data = resp.json()
        return SubmitResult(
            trade_id=data["trade_id"],
            instrument_bytes_hex=data["instrument_bytes_hex"],
            size_int=data["size_int"],
            price_int=data["price_int"],
            buyer_checksum=data["buyer_checksum"],
            seller_checksum=data["seller_checksum"],
            salt_hex=data["salt_hex"],
            operation_sequence=self._parse_operation_sequence(data["operation_sequence"]) if data.get("operation_sequence") else None,
        )

    async def cancel_order(
        self,
        trade_id: str,
        caller_address: str,
        *,
        funder_address: str | None = None,
    ) -> CancelResult:
        """Cancel a pending trade. Returns EIP-712 operations to sign and relay."""
        body: dict[str, Any] = {
            "trade_id": trade_id,
            "caller_address": caller_address,
        }
        if funder_address is not None:
            body["funder_address"] = funder_address
        resp = await self._client.post("/api/order/cancel", json=body)
        self._raise_for_status(resp)
        data = resp.json()
        return CancelResult(
            trade_id=data["trade_id"],
            operation_sequence=self._parse_operation_sequence(data["operation_sequence"]) if data.get("operation_sequence") else None,
        )

    async def relay(
        self,
        sequence_id: UUID,
        signatures: dict[UUID, str],
    ) -> RelayResult:
        """Submit signed EIP-712 operations for on-chain execution.

        Args:
            sequence_id: The sequence ID from a submit or cancel result.
            signatures: Map of operation_id -> hex-encoded signature.
        """
        body: dict[str, Any] = {
            "sequence_id": str(sequence_id),
            "signatures": {str(k): v for k, v in signatures.items()},
        }
        resp = await self._client.post("/api/order/relay", json=body)
        self._raise_for_status(resp)
        data = resp.json()
        return RelayResult(
            successful=data["successful"],
            results=[self._parse_operation_result(r) for r in data["results"]],
        )

    # -- Trades ----------------------------------------------------------------

    async def get_trade(self, trade_id: str) -> Trade:
        """Fetch a single trade by ID."""
        resp = await self._client.get(f"/api/trade/get/{trade_id}")
        self._raise_for_status(resp)
        return self._parse_trade(resp.json())

    async def list_trades(
        self,
        *,
        status: str | None = None,
        user_id: str | None = None,
        instrument_id: str | None = None,
    ) -> list[Trade]:
        """List trades with optional filters."""
        params: dict[str, str] = {}
        if status is not None:
            params["status"] = status
        if user_id is not None:
            params["user_id"] = user_id
        if instrument_id is not None:
            params["instrument_id"] = instrument_id
        resp = await self._client.get("/api/trades", params=params)
        self._raise_for_status(resp)
        return [self._parse_trade(t) for t in resp.json()["trades"]]

    # -- Users -----------------------------------------------------------------

    async def get_user_trades(
        self,
        user_id: str,
        *,
        status: str | None = None,
    ) -> list[Trade]:
        """List trades for a specific user."""
        params: dict[str, str] = {"user_id": user_id}
        if status is not None:
            params["status"] = status
        resp = await self._client.get("/api/user/trades", params=params)
        self._raise_for_status(resp)
        data = resp.json()
        # Backend returns {"trades": {"trades": [...]}} (nested GetTradesResponse)
        return [self._parse_trade(t) for t in data["trades"]["trades"]]

    async def get_user_pnl(self, user_id: str) -> UserPnl:
        """Get a user's total realized P&L."""
        resp = await self._client.get(
            "/api/user/realized-pnl", params={"user_id": user_id}
        )
        self._raise_for_status(resp)
        data = resp.json()
        return UserPnl(
            user_id=data["user_id"],
            total_realized_pnl=data["total_realized_pnl"],
        )