from __future__ import annotations

from dataclasses import dataclass
from typing import Any, Generic, TypeVar

import httpx
from pydantic import BaseModel, TypeAdapter

from uforia.api.models import (
    ApiMeta,
    CompositeDecompositionResponse,
    CompositeSignalSeriesResponse,
    CompositeSignalSummary,
    CompositeTransitionResponse,
    DatasetDetail,
    DatasetRowsResponse,
    DatasetSummary,
    EngineSummary,
    ExecutionAccountSummary,
    ExecutionBalanceRecord,
    ExecutionFillRecord,
    ExecutionHealthRecord,
    ExecutionMarketSummary,
    ExecutionOrderRecord,
    ExecutionPositionRecord,
    ExecutionVenueSummary,
    OptionsDashboardResponse,
    OptionsDashboardSummary,
    OptionsSignalSeriesResponse,
    OptionsSignalSummary,
    PerpsDashboardResponse,
    PerpsDashboardSummary,
    PerpsSignalSeriesResponse,
    PerpsSignalSummary,
    PriceSeriesResponse,
    PriceSummary,
    RunRecord,
    RunsSummary,
    SignalSeriesResponse,
    SignalSummary,
    SystemHealth,
)

from .errors import TerminalError

T = TypeVar("T")


@dataclass(frozen=True)
class ApiResponse(Generic[T]):
    data: T
    meta: ApiMeta
    warnings: list[str]


class UforiaApiClient:
    def __init__(self, base_url: str, timeout_seconds: float) -> None:
        self._client = httpx.Client(
            base_url=base_url.rstrip("/"),
            timeout=timeout_seconds,
            headers={"accept": "application/json"},
        )

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

    def _request(self, path: str, params: dict[str, Any] | None = None) -> dict[str, Any]:
        try:
            response = self._client.get(path, params={k: v for k, v in (params or {}).items() if v is not None})
        except httpx.TimeoutException as exc:
            raise TerminalError(f"Request timed out for {path}") from exc
        except httpx.HTTPError as exc:
            raise TerminalError(f"Unable to reach Uforia API at {self._client.base_url!s}: {exc}") from exc
        if response.status_code != 200:
            detail = response.text.strip() or response.reason_phrase
            raise TerminalError(f"API request failed for {path}: HTTP {response.status_code} - {detail}")
        try:
            payload = response.json()
        except ValueError as exc:
            raise TerminalError(f"Received invalid JSON from {path}") from exc
        if not isinstance(payload, dict) or "data" not in payload:
            raise TerminalError(f"Received unexpected payload from {path}")
        return payload

    def _parse(self, path: str, model: type[BaseModel], params: dict[str, Any] | None = None) -> ApiResponse[Any]:
        payload = self._request(path, params=params)
        return ApiResponse(
            data=model.model_validate(payload["data"]),
            meta=ApiMeta.model_validate(payload.get("meta", {})),
            warnings=TypeAdapter(list[str]).validate_python(payload.get("warnings", [])),
        )

    def _parse_many(self, path: str, adapter: TypeAdapter[Any], params: dict[str, Any] | None = None) -> ApiResponse[Any]:
        payload = self._request(path, params=params)
        return ApiResponse(
            data=adapter.validate_python(payload["data"]),
            meta=ApiMeta.model_validate(payload.get("meta", {})),
            warnings=TypeAdapter(list[str]).validate_python(payload.get("warnings", [])),
        )

    def get_system_health(self) -> ApiResponse[SystemHealth]:
        return self._parse("/api/v1/system/health", SystemHealth)

    def get_signals(self) -> ApiResponse[list[SignalSummary]]:
        return self._parse_many("/api/v1/signals", TypeAdapter(list[SignalSummary]))

    def get_signal_latest(self, signal_id: str, source: str | None = None) -> ApiResponse[SignalSummary]:
        return self._parse(f"/api/v1/signals/{signal_id}/latest", SignalSummary, {"source": source})

    def get_signal_series(
        self,
        signal_id: str,
        source: str | None = None,
        start: str | None = None,
        end: str | None = None,
        quality: str | None = None,
        max_points: int | None = None,
    ) -> ApiResponse[SignalSeriesResponse]:
        return self._parse(
            f"/api/v1/signals/{signal_id}/series",
            SignalSeriesResponse,
            {
                "source": source,
                "start": start,
                "end": end,
                "quality": quality,
                "max_points": max_points,
            },
        )

    def get_options_dashboards(self) -> ApiResponse[list[OptionsDashboardSummary]]:
        return self._parse_many("/api/v1/options/dashboards", TypeAdapter(list[OptionsDashboardSummary]))

    def get_options_dashboard_latest(
        self,
        dashboard_id: str,
        underlying: str | None = None,
    ) -> ApiResponse[OptionsDashboardSummary]:
        return self._parse(
            f"/api/v1/options/{dashboard_id}/latest",
            OptionsDashboardSummary,
            {"underlying": underlying},
        )

    def get_options_dashboard_series(
        self,
        dashboard_id: str,
        underlying: str | None = None,
        start: str | None = None,
        end: str | None = None,
        quality: str | None = None,
        max_points: int | None = None,
    ) -> ApiResponse[OptionsDashboardResponse]:
        return self._parse(
            f"/api/v1/options/{dashboard_id}/series",
            OptionsDashboardResponse,
            {
                "underlying": underlying,
                "start": start,
                "end": end,
                "quality": quality,
                "max_points": max_points,
            },
        )

    def get_options_signals(self) -> ApiResponse[list[OptionsSignalSummary]]:
        return self._parse_many("/api/v1/options/signals", TypeAdapter(list[OptionsSignalSummary]))

    def get_options_signal_latest(
        self,
        signal_id: str,
        underlying: str | None = None,
    ) -> ApiResponse[OptionsSignalSummary]:
        return self._parse(
            f"/api/v1/options/signals/{signal_id}/latest",
            OptionsSignalSummary,
            {"underlying": underlying},
        )

    def get_options_signal_series(
        self,
        signal_id: str,
        underlying: str | None = None,
        start: str | None = None,
        end: str | None = None,
        quality: str | None = None,
        max_points: int | None = None,
    ) -> ApiResponse[OptionsSignalSeriesResponse]:
        return self._parse(
            f"/api/v1/options/signals/{signal_id}/series",
            OptionsSignalSeriesResponse,
            {
                "underlying": underlying,
                "start": start,
                "end": end,
                "quality": quality,
                "max_points": max_points,
            },
        )

    def get_perps_dashboards(self) -> ApiResponse[list[PerpsDashboardSummary]]:
        return self._parse_many("/api/v1/perps/dashboards", TypeAdapter(list[PerpsDashboardSummary]))

    def get_perps_dashboard_latest(
        self,
        dashboard_id: str,
        asset: str | None = None,
        venue_scope: str | None = None,
    ) -> ApiResponse[PerpsDashboardSummary]:
        return self._parse(
            f"/api/v1/perps/{dashboard_id}/latest",
            PerpsDashboardSummary,
            {"asset": asset, "venue_scope": venue_scope},
        )

    def get_perps_dashboard_series(
        self,
        dashboard_id: str,
        asset: str | None = None,
        venue_scope: str | None = None,
        start: str | None = None,
        end: str | None = None,
        quality: str | None = None,
        max_points: int | None = None,
    ) -> ApiResponse[PerpsDashboardResponse]:
        return self._parse(
            f"/api/v1/perps/{dashboard_id}/series",
            PerpsDashboardResponse,
            {
                "asset": asset,
                "venue_scope": venue_scope,
                "start": start,
                "end": end,
                "quality": quality,
                "max_points": max_points,
            },
        )

    def get_perps_signals(self) -> ApiResponse[list[PerpsSignalSummary]]:
        return self._parse_many("/api/v1/perps/signals", TypeAdapter(list[PerpsSignalSummary]))

    def get_perps_signal_latest(
        self,
        signal_id: str,
        asset: str | None = None,
        venue_scope: str | None = None,
    ) -> ApiResponse[PerpsSignalSummary]:
        return self._parse(
            f"/api/v1/perps/signals/{signal_id}/latest",
            PerpsSignalSummary,
            {"asset": asset, "venue_scope": venue_scope},
        )

    def get_perps_signal_series(
        self,
        signal_id: str,
        asset: str | None = None,
        venue_scope: str | None = None,
        start: str | None = None,
        end: str | None = None,
        quality: str | None = None,
        max_points: int | None = None,
    ) -> ApiResponse[PerpsSignalSeriesResponse]:
        return self._parse(
            f"/api/v1/perps/signals/{signal_id}/series",
            PerpsSignalSeriesResponse,
            {
                "asset": asset,
                "venue_scope": venue_scope,
                "start": start,
                "end": end,
                "quality": quality,
                "max_points": max_points,
            },
        )

    def get_composite_signals(self) -> ApiResponse[list[CompositeSignalSummary]]:
        return self._parse_many("/api/v1/composite/signals", TypeAdapter(list[CompositeSignalSummary]))

    def get_composite_signal_latest(self, signal_id: str) -> ApiResponse[CompositeSignalSummary]:
        return self._parse(f"/api/v1/composite/signals/{signal_id}/latest", CompositeSignalSummary)

    def get_composite_signal_series(
        self,
        signal_id: str,
        start: str | None = None,
        end: str | None = None,
        quality: str | None = None,
        max_points: int | None = None,
    ) -> ApiResponse[CompositeSignalSeriesResponse]:
        return self._parse(
            f"/api/v1/composite/signals/{signal_id}/series",
            CompositeSignalSeriesResponse,
            {"start": start, "end": end, "quality": quality, "max_points": max_points},
        )

    def get_composite_decomposition(
        self,
        signal_id: str,
        start: str | None = None,
        end: str | None = None,
        quality: str | None = None,
        max_points: int | None = None,
    ) -> ApiResponse[CompositeDecompositionResponse]:
        return self._parse(
            f"/api/v1/composite/decomposition/{signal_id}",
            CompositeDecompositionResponse,
            {"start": start, "end": end, "quality": quality, "max_points": max_points},
        )

    def get_composite_transitions(
        self,
        signal_id: str,
        start: str | None = None,
        end: str | None = None,
        quality: str | None = None,
        max_points: int | None = None,
    ) -> ApiResponse[CompositeTransitionResponse]:
        return self._parse(
            f"/api/v1/composite/transitions/{signal_id}",
            CompositeTransitionResponse,
            {"start": start, "end": end, "quality": quality, "max_points": max_points},
        )

    def get_prices(self) -> ApiResponse[list[PriceSummary]]:
        return self._parse_many("/api/v1/prices", TypeAdapter(list[PriceSummary]))

    def get_price_series(
        self,
        asset: str | None = None,
        venue_scope: str | None = None,
        timeframe: str | None = None,
        start: str | None = None,
        end: str | None = None,
        max_points: int | None = None,
    ) -> ApiResponse[PriceSeriesResponse]:
        return self._parse(
            "/api/v1/prices/series",
            PriceSeriesResponse,
            {
                "asset": asset,
                "venue_scope": venue_scope,
                "timeframe": timeframe,
                "start": start,
                "end": end,
                "max_points": max_points,
            },
        )

    def get_datasets(self) -> ApiResponse[list[DatasetSummary]]:
        return self._parse_many("/api/v1/datasets", TypeAdapter(list[DatasetSummary]))

    def get_dataset(self, dataset_id: str) -> ApiResponse[DatasetDetail]:
        return self._parse(f"/api/v1/datasets/{dataset_id}", DatasetDetail)

    def get_dataset_rows(self, dataset_id: str, params: dict[str, Any]) -> ApiResponse[DatasetRowsResponse]:
        return self._parse(f"/api/v1/datasets/{dataset_id}/rows", DatasetRowsResponse, params)

    def get_runs(self, params: dict[str, Any] | None = None) -> ApiResponse[list[RunRecord]]:
        return self._parse_many("/api/v1/runs", TypeAdapter(list[RunRecord]), params)

    def get_runs_summary(self) -> ApiResponse[RunsSummary]:
        return self._parse("/api/v1/runs/summary", RunsSummary)

    def get_engines(self) -> ApiResponse[list[EngineSummary]]:
        return self._parse_many("/api/v1/engines", TypeAdapter(list[EngineSummary]))

    def get_execution_venues(self) -> ApiResponse[list[ExecutionVenueSummary]]:
        return self._parse_many("/api/v1/execution/venues", TypeAdapter(list[ExecutionVenueSummary]))

    def get_execution_markets(self, venue: str | None = None, asset: str | None = None) -> ApiResponse[list[ExecutionMarketSummary]]:
        return self._parse_many(
            "/api/v1/execution/markets",
            TypeAdapter(list[ExecutionMarketSummary]),
            {"venue": venue, "asset": asset},
        )

    def get_execution_accounts(self) -> ApiResponse[list[ExecutionAccountSummary]]:
        return self._parse_many("/api/v1/execution/accounts", TypeAdapter(list[ExecutionAccountSummary]))

    def get_execution_orders(self, params: dict[str, Any] | None = None) -> ApiResponse[list[ExecutionOrderRecord]]:
        return self._parse_many("/api/v1/execution/orders", TypeAdapter(list[ExecutionOrderRecord]), params)

    def get_execution_fills(self, params: dict[str, Any] | None = None) -> ApiResponse[list[ExecutionFillRecord]]:
        return self._parse_many("/api/v1/execution/fills", TypeAdapter(list[ExecutionFillRecord]), params)

    def get_execution_positions(self, params: dict[str, Any] | None = None) -> ApiResponse[list[ExecutionPositionRecord]]:
        return self._parse_many(
            "/api/v1/execution/positions",
            TypeAdapter(list[ExecutionPositionRecord]),
            params,
        )

    def get_execution_balances(self, params: dict[str, Any] | None = None) -> ApiResponse[list[ExecutionBalanceRecord]]:
        return self._parse_many("/api/v1/execution/balances", TypeAdapter(list[ExecutionBalanceRecord]), params)

    def get_execution_health(self) -> ApiResponse[list[ExecutionHealthRecord]]:
        return self._parse_many("/api/v1/execution/health", TypeAdapter(list[ExecutionHealthRecord]))
