"""Low-level SDK — 1:1 typed wrappers around every gateway endpoint."""

from __future__ import annotations

import json
import logging
from collections.abc import AsyncIterator
from typing import Any

import httpx

from .auth import AuthManager
from .models import (
    ChatEvent,
    CreditsInfo,
    DepositRequest,
    HooksRequest,
    LimitOrderRequest,
    SwapRequest,
    TaskResponse,
    TaskStatusResponse,
    TokenInfo,
    WithdrawRequest,
)

logger = logging.getLogger(__name__)

DEFAULT_BASE_URL = "https://api.fereai.xyz"


class FereAPI:
    """Low-level async client for the FereAI Gateway API.

    Each method maps directly to one REST endpoint.

    Usage::

        api = FereAPI(agent_name="my-bot")
        async with api:
            await api.authenticate()
            credits = await api.get_credits()
    """

    def __init__(
        self,
        agent_name: str,
        base_url: str = DEFAULT_BASE_URL,
        key_path: str | None = None,
        timeout: int = 120,
    ) -> None:
        self.base_url = base_url.rstrip("/")
        self._auth = AuthManager(
            agent_name, base_url=self.base_url, key_path=key_path
        )
        self._client = httpx.AsyncClient(timeout=timeout)

    async def __aenter__(self):
        return self

    async def __aexit__(self, *exc):
        await self.close()

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

    # ----------------------------------------------------------
    # Auth
    # ----------------------------------------------------------

    async def authenticate(self) -> TokenInfo:
        """Register (if needed) and obtain a bearer token."""
        token = await self._auth.ensure_token(self._client)
        return TokenInfo(token=token, expires_in=3600)

    async def _headers(self) -> dict[str, str]:
        await self._auth.ensure_token(self._client)
        return self._auth.auth_headers()

    async def _authed_get(self, path: str, params: dict | None = None) -> Any:
        headers = await self._headers()
        resp = await self._client.get(
            f"{self.base_url}{path}",
            headers=headers,
            params=params,
        )
        if resp.status_code == 401:
            self._auth.clear_token()
            headers = await self._headers()
            resp = await self._client.get(
                f"{self.base_url}{path}",
                headers=headers,
                params=params,
            )
        resp.raise_for_status()
        return resp.json()

    async def _authed_post(
        self,
        path: str,
        json_data: dict,
        params: dict | None = None,
    ) -> Any:
        headers = await self._headers()
        resp = await self._client.post(
            f"{self.base_url}{path}",
            headers=headers,
            json=json_data,
            params=params,
        )
        if resp.status_code == 401:
            self._auth.clear_token()
            headers = await self._headers()
            resp = await self._client.post(
                f"{self.base_url}{path}",
                headers=headers,
                json=json_data,
                params=params,
            )
        resp.raise_for_status()
        return resp.json()

    async def _authed_delete(self, path: str) -> Any:
        headers = await self._headers()
        resp = await self._client.delete(
            f"{self.base_url}{path}", headers=headers
        )
        if resp.status_code == 401:
            self._auth.clear_token()
            headers = await self._headers()
            resp = await self._client.delete(
                f"{self.base_url}{path}", headers=headers
            )
        resp.raise_for_status()
        return resp.json()

    # ----------------------------------------------------------
    # Chat
    # ----------------------------------------------------------

    async def chat(
        self,
        query: str,
        thread_id: str | None = None,
        agent: str = "ProAgent",
    ) -> AsyncIterator[ChatEvent]:
        """POST /v1/chat — returns an async iterator of SSE events.

        Uses a 1200s read timeout because research queries (especially
        ProDeepResearchAgent) can take up to 20 minutes with long gaps
        between SSE events while the server plans and researches.
        """
        headers = await self._headers()
        body: dict[str, Any] = {
            "query": query,
            "stream": True,
            "agent": agent,
        }
        if thread_id:
            body["thread_id"] = thread_id

        chat_timeout = httpx.Timeout(
            connect=30.0, read=1200.0, write=30.0, pool=30.0
        )
        async with self._client.stream(
            "POST",
            f"{self.base_url}/v1/chat",
            json=body,
            headers=headers,
            timeout=chat_timeout,
        ) as resp:
            resp.raise_for_status()
            event_type = ""
            async for line in resp.aiter_lines():
                if line.startswith("event:"):
                    event_type = line[6:].strip()
                elif line.startswith("data:"):
                    raw = line[5:].strip()
                    if not raw:
                        continue
                    try:
                        data = json.loads(raw)
                    except (json.JSONDecodeError, ValueError):
                        logger.debug(
                            "Unparseable SSE data: %r", raw
                        )
                        continue
                    yield ChatEvent(
                        event=event_type, data=data
                    )

    async def get_threads(self, skip: int = 0, limit: int = 10) -> list[dict]:
        """GET /v1/chat/threads"""
        return await self._authed_get(
            "/v1/chat/threads",
            params={"skip": skip, "limit": limit},
        )

    async def get_thread(self, thread_id: str) -> list[dict]:
        """GET /v1/chat/threads/{thread_id}"""
        return await self._authed_get(f"/v1/chat/threads/{thread_id}")

    # ----------------------------------------------------------
    # Trading
    # ----------------------------------------------------------

    async def create_swap(
        self,
        request: SwapRequest,
        wait: bool = False,
        timeout: int = 60,
    ) -> TaskResponse | dict:
        """POST /v1/swap"""
        params = {}
        if wait:
            params = {"wait": "true", "timeout": str(timeout)}
        data = await self._authed_post(
            "/v1/swap", request.to_dict(), params=params
        )
        if wait:
            return data
        return TaskResponse(
            task_id=data["task_id"],
            status=data.get("status", "queued"),
            poll_url=data.get("poll_url"),
            notification_stream=data.get("notification_stream"),
        )

    async def create_limit_order(
        self,
        request: LimitOrderRequest,
        wait: bool = False,
        timeout: int = 60,
    ) -> TaskResponse | dict:
        """POST /v1/limit-orders"""
        params = {}
        if wait:
            params = {"wait": "true", "timeout": str(timeout)}
        data = await self._authed_post(
            "/v1/limit-orders", request.to_dict(), params=params
        )
        if wait:
            return data
        return TaskResponse(
            task_id=data["task_id"],
            status=data.get("status", "queued"),
            poll_url=data.get("poll_url"),
            notification_stream=data.get("notification_stream"),
        )

    async def get_limit_orders(self, status: str | None = None) -> list[dict]:
        """GET /v1/limit-orders"""
        params = {}
        if status:
            params["status"] = status
        return await self._authed_get("/v1/limit-orders", params=params)

    async def get_limit_order(self, order_id: str) -> dict:
        """GET /v1/limit-orders/{order_id}"""
        return await self._authed_get(f"/v1/limit-orders/{order_id}")

    async def cancel_limit_order(self, order_id: str) -> dict:
        """DELETE /v1/limit-orders/{order_id}"""
        return await self._authed_delete(f"/v1/limit-orders/{order_id}")

    async def set_hooks(self, request: HooksRequest) -> dict:
        """POST /v1/hooks"""
        return await self._authed_post("/v1/hooks", request.to_dict())

    # ----------------------------------------------------------
    # Wallets
    # ----------------------------------------------------------

    async def get_wallets(self) -> dict:
        """GET /v1/wallets"""
        return await self._authed_get("/v1/wallets")

    async def get_holdings(self) -> dict:
        """GET /v1/holdings"""
        return await self._authed_get("/v1/holdings")

    # ----------------------------------------------------------
    # Earn
    # ----------------------------------------------------------

    async def get_earn_info(self) -> dict:
        """GET /v1/earn (public, no auth required)"""
        resp = await self._client.get(f"{self.base_url}/v1/earn")
        resp.raise_for_status()
        return resp.json()

    async def deposit(
        self,
        request: DepositRequest,
        wait: bool = False,
        timeout: int = 60,
    ) -> TaskResponse | dict:
        """POST /v1/earn/deposit"""
        params = {}
        if wait:
            params = {"wait": "true", "timeout": str(timeout)}
        data = await self._authed_post(
            "/v1/earn/deposit", request.to_dict(), params=params
        )
        if wait:
            return data
        return TaskResponse(
            task_id=data["task_id"],
            status=data.get("status", "queued"),
            poll_url=data.get("poll_url"),
            notification_stream=data.get("notification_stream"),
        )

    async def withdraw(
        self,
        request: WithdrawRequest,
        wait: bool = False,
        timeout: int = 60,
    ) -> TaskResponse | dict:
        """POST /v1/earn/withdraw"""
        params = {}
        if wait:
            params = {"wait": "true", "timeout": str(timeout)}
        data = await self._authed_post(
            "/v1/earn/withdraw", request.to_dict(), params=params
        )
        if wait:
            return data
        return TaskResponse(
            task_id=data["task_id"],
            status=data.get("status", "queued"),
            poll_url=data.get("poll_url"),
            notification_stream=data.get("notification_stream"),
        )

    async def get_positions(self) -> Any:
        """GET /v1/earn/positions"""
        return await self._authed_get("/v1/earn/positions")

    # ----------------------------------------------------------
    # Tasks
    # ----------------------------------------------------------

    async def get_task(self, task_id: str) -> TaskStatusResponse:
        """GET /v1/tasks/{task_id}"""
        data = await self._authed_get(f"/v1/tasks/{task_id}")
        return TaskStatusResponse(
            task_id=data["task_id"],
            status=data["status"],
            result=data.get("result"),
        )

    # ----------------------------------------------------------
    # Notifications
    # ----------------------------------------------------------

    async def get_notifications(
        self,
        limit: int = 10,
        offset: int = 0,
        type: str | None = None,
    ) -> Any:
        """GET /v1/notifications"""
        params: dict = {"limit": limit, "offset": offset}
        if type:
            params["type"] = type
        return await self._authed_get("/v1/notifications", params=params)

    async def notification_stream(
        self,
    ) -> AsyncIterator[ChatEvent]:
        """GET /v1/notifications/stream — SSE stream."""
        headers = await self._headers()
        async with self._client.stream(
            "GET",
            f"{self.base_url}/v1/notifications/stream",
            headers=headers,
        ) as resp:
            resp.raise_for_status()
            event_type = ""
            async for line in resp.aiter_lines():
                if line.startswith("event:"):
                    event_type = line[6:].strip()
                elif line.startswith("data:"):
                    raw = line[5:].strip()
                    if not raw:
                        continue
                    try:
                        data = json.loads(raw)
                    except (json.JSONDecodeError, ValueError):
                        logger.debug("Unparseable SSE data: %r", raw)
                        continue
                    yield ChatEvent(event=event_type, data=data)

    # ----------------------------------------------------------
    # Credits / User / Chains
    # ----------------------------------------------------------

    async def get_credits(self) -> CreditsInfo:
        """GET /v1/credits"""
        data = await self._authed_get("/v1/credits")
        return CreditsInfo(credits_available=data.get("credits_available"))

    async def get_user(self) -> dict:
        """GET /v1/user"""
        return await self._authed_get("/v1/user")

    async def get_chains(self) -> Any:
        """GET /v1/chains (public, no auth required)"""
        resp = await self._client.get(f"{self.base_url}/v1/chains")
        resp.raise_for_status()
        return resp.json()
