"""
TradeStation API HTTP Client.

Wraps httpx.AsyncClient with automatic bearer token injection,
error handling, and streaming support for TradeStation's newline-delimited
JSON streaming endpoints.
"""

import asyncio
import json
import logging
from typing import Any

import httpx

from .auth import TokenManager

logger = logging.getLogger(__name__)

BASE_URL = "https://api.tradestation.com"

# HTTP status code to human-readable error descriptions
ERROR_DESCRIPTIONS = {
    400: "Bad Request — The request was malformed or contained invalid parameters.",
    401: "Unauthorized — Access token is missing, invalid, or expired.",
    403: "Forbidden — You do not have permission to access this resource.",
    404: "Not Found — The requested resource does not exist.",
    429: "Rate Limited — Too many requests. Please slow down.",
    503: "Service Unavailable — TradeStation API is temporarily unavailable.",
    504: "Gateway Timeout — The request timed out upstream.",
}


class TradeStationClient:
    """Async HTTP client for the TradeStation API.

    Automatically injects bearer tokens, handles errors, and supports
    both standard request/response and streaming endpoints.
    """

    def __init__(self, token_manager: TokenManager):
        self._token_manager = token_manager
        self._http: httpx.AsyncClient | None = None

    async def _ensure_client(self) -> httpx.AsyncClient:
        """Lazily create the httpx async client."""
        if self._http is None or self._http.is_closed:
            self._http = httpx.AsyncClient(
                base_url=BASE_URL,
                timeout=httpx.Timeout(30.0, connect=10.0),
            )
        return self._http

    def _get_headers(self) -> dict[str, str]:
        """Build request headers with a valid bearer token."""
        token = self._token_manager.get_access_token()
        return {
            "Authorization": f"Bearer {token}",
            "Accept": "application/json",
        }

    async def _handle_response(self, resp: httpx.Response) -> Any:
        """Parse response, raising descriptive errors for non-200 status codes."""
        if resp.status_code == 200:
            return resp.json()

        error_desc = ERROR_DESCRIPTIONS.get(resp.status_code, "Unexpected error")
        try:
            body = resp.json()
            detail = body.get("Message", body.get("error", resp.text))
        except Exception:
            detail = resp.text

        error_msg = f"TradeStation API error {resp.status_code}: {error_desc}\nDetail: {detail}"
        logger.error(error_msg)
        raise TradeStationAPIError(resp.status_code, error_msg)

    # ── Standard HTTP Methods ──────────────────────────────────────

    async def get(self, path: str, params: dict[str, Any] | None = None) -> Any:
        """Make a GET request to the TradeStation API."""
        client = await self._ensure_client()
        resp = await client.get(path, headers=self._get_headers(), params=params)
        return await self._handle_response(resp)

    async def post(self, path: str, json_body: dict[str, Any] | None = None) -> Any:
        """Make a POST request to the TradeStation API."""
        client = await self._ensure_client()
        headers = self._get_headers()
        headers["Content-Type"] = "application/json"
        resp = await client.post(path, headers=headers, json=json_body)
        return await self._handle_response(resp)

    async def put(self, path: str, json_body: dict[str, Any] | None = None) -> Any:
        """Make a PUT request to the TradeStation API."""
        client = await self._ensure_client()
        headers = self._get_headers()
        headers["Content-Type"] = "application/json"
        resp = await client.put(path, headers=headers, json=json_body)
        return await self._handle_response(resp)

    async def delete(self, path: str) -> Any:
        """Make a DELETE request to the TradeStation API."""
        client = await self._ensure_client()
        resp = await client.delete(path, headers=self._get_headers())
        return await self._handle_response(resp)

    # ── Streaming ──────────────────────────────────────────────────

    async def stream_get(
        self,
        path: str,
        params: dict[str, Any] | None = None,
        duration_seconds: int = 5,
        max_items: int = 100,
    ) -> list[dict[str, Any]]:
        """Stream data from a TradeStation streaming endpoint.

        Opens a GET request with streaming, collects newline-delimited JSON
        objects for `duration_seconds`, then closes the connection and returns
        all collected items.

        Args:
            path: API path (e.g. "/v3/marketdata/stream/quotes/MSFT").
            params: Optional query parameters.
            duration_seconds: How long to collect streaming data (default 5s).
            max_items: Maximum number of items to collect before stopping early.

        Returns:
            List of parsed JSON objects received during the collection window.
        """
        results: list[dict[str, Any]] = []
        client = await self._ensure_client()
        headers = self._get_headers()

        try:
            async with client.stream(
                "GET", path, headers=headers, params=params, timeout=httpx.Timeout(duration_seconds + 10, connect=10.0)
            ) as resp:
                if resp.status_code != 200:
                    error_desc = ERROR_DESCRIPTIONS.get(resp.status_code, "Unexpected error")
                    body = b""
                    async for chunk in resp.aiter_bytes():
                        body += chunk
                        if len(body) > 4096:
                            break
                    raise TradeStationAPIError(
                        resp.status_code,
                        f"Stream error {resp.status_code}: {error_desc}\n{body.decode(errors='replace')}",
                    )

                deadline = asyncio.get_event_loop().time() + duration_seconds
                buffer = ""

                async for chunk in resp.aiter_text():
                    buffer += chunk
                    while "\n" in buffer:
                        line, buffer = buffer.split("\n", 1)
                        line = line.strip()
                        if not line:
                            continue
                        try:
                            obj = json.loads(line)
                            results.append(obj)
                        except json.JSONDecodeError:
                            logger.debug("Skipping non-JSON line: %s", line[:200])

                        if len(results) >= max_items:
                            return results

                    if asyncio.get_event_loop().time() >= deadline:
                        break

        except httpx.ReadTimeout:
            logger.debug("Stream read timed out after %ds (expected)", duration_seconds)
        except httpx.HTTPError as e:
            if results:
                logger.debug("Stream ended with error after collecting %d items: %s", len(results), e)
            else:
                raise

        return results

    async def close(self):
        """Close the underlying HTTP client."""
        if self._http and not self._http.is_closed:
            await self._http.aclose()


class TradeStationAPIError(Exception):
    """Raised when the TradeStation API returns a non-200 response."""

    def __init__(self, status_code: int, message: str):
        self.status_code = status_code
        super().__init__(message)
