"""Async DepthSignal API client."""

from __future__ import annotations

import asyncio
import json
import random
from typing import Any, AsyncGenerator, Dict, List, Optional
from urllib.parse import quote, urlparse

import httpx

from depthsignal.exceptions import (
    APIConnectionError,
    AuthenticationError,
    DepthSignalError,
    RateLimitError,
    TierAccessError,
)
from depthsignal.models import (
    AggregatedCompositesData,
    AggregatedFeaturesData,
    ArbitrageData,
    BasisData,
    CVDData,
    CascadeRiskData,
    CompositeSignals,
    ConsensusData,
    CorrelationData,
    DepthProfileData,
    DepthResilienceData,
    DerivativesData,
    DriftData,
    ExecutionCostData,
    FlowAssetData,
    FlowOverview,
    HistoricalCompositesData,
    HistoricalFeaturesData,
    LargeTradeData,
    LiquidationData,
    MarketProtectionData,
    OrderbookFeatures,
    PositioningData,
    RegimeData,
    SentimentData,
    SpotFuturesData,
    SpreadEventsData,
    SupportResistanceZones,
    SymbolInfo,
    VenueSelectionData,
    WhaleFlowData,
)


class AsyncDepthSignal:
    """Async DepthSignal API client.

    Args:
        api_key: Your DepthSignal API key
        base_url: API base URL (default: https://api.depthsignal.io)
        timeout: Request timeout in seconds (default: 10)
        max_retries: Retry count for 429/503/network errors (default: 2, set 0 to disable)

    .. note::
        Use ``async with`` for automatic resource cleanup::

            async with AsyncDepthSignal(api_key="your-key") as ds:
                features = await ds.orderbook_features("BTCUSDT")

    Example::

        ds = AsyncDepthSignal(api_key="your-api-key")

        # Get orderbook features
        features = await ds.orderbook_features("BTCUSDT")
        print(features.features["binance"]["vpin"])

        # Get composite signals
        composites = await ds.composites("BTCUSDT")
        print(composites.composites.get("directional_pressure"))

        await ds.close()
    """

    def __init__(
        self,
        api_key: str,
        base_url: str = "https://api.depthsignal.io",
        timeout: float = 10.0,
        max_retries: int = 2,
    ):
        if isinstance(api_key, str):
            api_key = api_key.strip()
        if not api_key:
            raise ValueError("api_key is required")

        self._api_key = api_key
        self._base_url = base_url.rstrip("/")

        # Enforce HTTPS in production to prevent credential leakage
        parsed = urlparse(self._base_url)
        if parsed.scheme != "https" and parsed.hostname not in ("localhost", "127.0.0.1"):
            raise ValueError(
                "base_url must use HTTPS for security. "
                "Use http:// only with localhost for development."
            )

        self._max_retries = max(0, max_retries)
        self._client = httpx.AsyncClient(
            base_url=self._base_url,
            headers={"X-API-Key": api_key},
            timeout=timeout,
        )

    async def close(self) -> None:
        """Close the HTTP client."""
        await self._client.aclose()

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

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

    async def _request(self, method: str, path: str, **kwargs: Any) -> dict:
        """Make an API request and return JSON response.

        Wraps all httpx exceptions into DepthSignal exception types.
        Retries on 429 (with Retry-After) and 503 up to ``max_retries`` times.
        """
        last_exc: Exception | None = None
        for attempt in range(1 + self._max_retries):
            try:
                resp = await self._client.request(method, path, **kwargs)
            except httpx.ConnectError as e:
                last_exc = APIConnectionError(
                    f"Could not connect to {self._base_url}: {e}"
                )
                if attempt < self._max_retries:
                    delay = min(2 ** attempt, 4) * (0.5 + random.random() * 0.5)
                    await asyncio.sleep(delay)
                    continue
                raise last_exc
            except httpx.TimeoutException as e:
                last_exc = APIConnectionError(f"Request timed out: {e}")
                if attempt < self._max_retries:
                    delay = min(2 ** attempt, 4) * (0.5 + random.random() * 0.5)
                    await asyncio.sleep(delay)
                    continue
                raise last_exc
            except httpx.HTTPError as e:
                raise DepthSignalError(0, f"HTTP error: {type(e).__name__}") from None

            if resp.status_code == 429:
                retry_after = resp.headers.get("Retry-After")
                retry_seconds = None
                if retry_after:
                    try:
                        retry_seconds = int(retry_after)
                    except ValueError:
                        pass
                if attempt < self._max_retries and retry_seconds is not None:
                    await asyncio.sleep(min(retry_seconds, 30))
                    continue
                raise RateLimitError(
                    "Rate limit exceeded", retry_after=retry_seconds
                )

            if resp.status_code == 503 and attempt < self._max_retries:
                delay = min(2 ** attempt, 4) * (0.5 + random.random() * 0.5)
                await asyncio.sleep(delay)
                continue

            break

        if resp.status_code == 401:
            try:
                detail = resp.json().get("detail", "Authentication failed")
            except Exception:
                detail = "Authentication failed"
            raise AuthenticationError(detail)

        if resp.status_code == 403:
            try:
                detail = resp.json().get("detail", "Access denied")
            except Exception:
                detail = "Access denied"
            raise TierAccessError(detail)

        if resp.status_code >= 400:
            try:
                detail = resp.json().get("detail", resp.text)
            except Exception:
                detail = resp.text
            raise DepthSignalError(resp.status_code, detail)

        try:
            return resp.json()
        except Exception:
            raise DepthSignalError(
                resp.status_code, "Server returned non-JSON response"
            )

    async def _get(self, path: str, **kwargs: Any) -> dict:
        return await self._request("GET", path, **kwargs)

    # --- Orderbook ---

    async def orderbook_features(self, symbol: str) -> OrderbookFeatures:
        """Get orderbook microstructure features for a symbol.

        Args:
            symbol: Trading pair (e.g., "BTCUSDT")

        Returns:
            OrderbookFeatures with per-exchange feature values
        """
        data = await self._get(f"/v1/orderbook/features/{quote(symbol, safe='')}")
        return OrderbookFeatures.from_dict(data)

    async def composites(self, symbol: str) -> CompositeSignals:
        """Get composite signals for a symbol (Pro+ tier).

        Args:
            symbol: Trading pair

        Returns:
            CompositeSignals with directional_pressure, liquidity_risk, etc.
        """
        data = await self._get(f"/v1/orderbook/composite/{quote(symbol, safe='')}")
        return CompositeSignals.from_dict(data)

    async def support_resistance(self, symbol: str) -> SupportResistanceZones:
        """Get support/resistance zones for a symbol (Pro+ tier).

        Args:
            symbol: Trading pair

        Returns:
            SupportResistanceZones with zone list
        """
        data = await self._get(f"/v1/orderbook/support-resistance/{quote(symbol, safe='')}")
        return SupportResistanceZones.from_dict(data)

    # --- Flow Intelligence ---

    async def flow_overview(self) -> FlowOverview:
        """Get exchange connectivity overview (Enterprise tier).

        Returns:
            FlowOverview with per-exchange status
        """
        data = await self._get("/v1/flow/overview")
        return FlowOverview.from_dict(data)

    async def flow_asset(self, symbol: str) -> FlowAssetData:
        """Get cross-exchange flow data for a symbol (Enterprise tier).

        Args:
            symbol: Trading pair

        Returns:
            FlowAssetData with per-exchange flow comparison
        """
        data = await self._get(f"/v1/flow/asset/{quote(symbol, safe='')}")
        return FlowAssetData.from_dict(data)

    async def liquidations(self, symbol: str) -> LiquidationData:
        """Get liquidation data for a symbol (Pro+ tier).

        Args:
            symbol: Trading pair

        Returns:
            LiquidationData with rate, imbalance, cascade risk
        """
        data = await self._get(f"/v1/flow/liquidations/{quote(symbol, safe='')}")
        return LiquidationData.from_dict(data)

    async def large_trades(self, symbol: str) -> LargeTradeData:
        """Get large trade / whale activity for a symbol (Pro+ tier).

        Args:
            symbol: Trading pair

        Returns:
            LargeTradeData with trade count, buy/sell ratio, institutional score
        """
        data = await self._get(f"/v1/flow/large-trades/{quote(symbol, safe='')}")
        return LargeTradeData.from_dict(data)

    async def cvd(self, symbol: str) -> CVDData:
        """Get CVD (Cumulative Volume Delta) for a symbol (Pro+ tier).

        Args:
            symbol: Trading pair

        Returns:
            CVDData with delta metrics
        """
        data = await self._get(f"/v1/flow/cvd/{quote(symbol, safe='')}")
        return CVDData.from_dict(data)

    async def spot_futures(self, symbol: str) -> SpotFuturesData:
        """Get spot-futures comparison for a symbol (Enterprise tier).

        Args:
            symbol: Trading pair

        Returns:
            SpotFuturesData with basis spread, depth divergence, OFI lead-lag
        """
        data = await self._get(f"/v1/flow/spot-futures/{quote(symbol, safe='')}")
        return SpotFuturesData.from_dict(data)

    async def derivatives(self, symbol: str = "BTCUSDT") -> DerivativesData:
        """Get derivatives consensus (Enterprise tier).

        Args:
            symbol: Trading pair (default: BTCUSDT)

        Returns:
            DerivativesData with funding agreement, OI divergence, L/S consensus
        """
        data = await self._get("/v1/flow/derivatives", params={"symbol": symbol})
        return DerivativesData.from_dict(data)

    async def positioning(self, symbol: str) -> PositioningData:
        """Get crowd positioning data (Enterprise tier).

        Args:
            symbol: Trading pair

        Returns:
            PositioningData with positioning metrics
        """
        data = await self._get(f"/v1/flow/positioning/{quote(symbol, safe='')}")
        return PositioningData.from_dict(data)

    async def sentiment(self, symbol: str) -> SentimentData:
        """Get aggregated market sentiment for a symbol (Pro+ tier).

        Combines Fear & Greed, community sentiment, and price momentum.

        Args:
            symbol: Trading pair

        Returns:
            SentimentData with sentiment metrics
        """
        data = await self._get(f"/v1/flow/sentiment/{quote(symbol, safe='')}")
        return SentimentData.from_dict(data)

    # --- Historical ---

    async def historical_features(
        self,
        symbol: str,
        exchange: Optional[str] = None,
        hours: Optional[int] = None,
        limit: Optional[int] = None,
    ) -> HistoricalFeaturesData:
        """Get historical orderbook features for a symbol (Pro+ tier).

        Args:
            symbol: Trading pair
            exchange: Filter by exchange (e.g. "binance")
            hours: Number of hours of history
            limit: Maximum rows to return

        Returns:
            HistoricalFeaturesData with symbol, data list, and count
        """
        params: Dict[str, Any] = {}
        if exchange:
            params["exchange"] = exchange
        if hours is not None:
            params["hours"] = str(hours)
        if limit is not None:
            params["limit"] = str(limit)
        data = await self._get(f"/v1/historical/features/{quote(symbol, safe='')}", params=params)
        return HistoricalFeaturesData.from_dict(data)

    async def historical_composites(
        self,
        symbol: str,
        hours: Optional[int] = None,
        limit: Optional[int] = None,
    ) -> HistoricalCompositesData:
        """Get historical composite signals for a symbol (Pro+ tier).

        Args:
            symbol: Trading pair
            hours: Number of hours of history
            limit: Maximum rows to return

        Returns:
            HistoricalCompositesData with symbol, data list, and count
        """
        params: Dict[str, Any] = {}
        if hours is not None:
            params["hours"] = str(hours)
        if limit is not None:
            params["limit"] = str(limit)
        data = await self._get(f"/v1/historical/composites/{quote(symbol, safe='')}", params=params)
        return HistoricalCompositesData.from_dict(data)

    # --- Aggregated Timeframes ---

    async def aggregated_features(
        self,
        symbol: str,
        timeframe: Optional[str] = None,
    ) -> AggregatedFeaturesData:
        """Get rolling-averaged orderbook features over a timeframe (Pro+ tier).

        Args:
            symbol: Trading pair
            timeframe: Aggregation window (e.g. "5m", "1h", "4h", "1d")

        Returns:
            AggregatedFeaturesData with averaged features
        """
        params: Dict[str, Any] = {}
        if timeframe is not None:
            params["timeframe"] = timeframe
        return AggregatedFeaturesData.from_dict(
            await self._get(f"/v1/aggregated/features/{quote(symbol, safe='')}", params=params or None)
        )

    async def aggregated_composites(
        self,
        symbol: str,
        timeframe: Optional[str] = None,
    ) -> AggregatedCompositesData:
        """Get rolling-averaged composite signals over a timeframe (Pro+ tier).

        Args:
            symbol: Trading pair
            timeframe: Aggregation window (e.g. "5m", "1h", "4h", "1d")

        Returns:
            AggregatedCompositesData with averaged composites
        """
        params: Dict[str, Any] = {}
        if timeframe is not None:
            params["timeframe"] = timeframe
        return AggregatedCompositesData.from_dict(
            await self._get(f"/v1/aggregated/composites/{quote(symbol, safe='')}", params=params or None)
        )

    # --- Discovery ---

    async def symbols(self) -> List[SymbolInfo]:
        """List all available symbols and their data types.

        Returns:
            List of SymbolInfo objects
        """
        data = await self._get("/v1/symbols")
        if isinstance(data, list):
            return [SymbolInfo.from_dict(item) for item in data]
        symbols_dict = data.get("symbols", {})
        if isinstance(symbols_dict, list):
            return [SymbolInfo.from_dict(item) for item in symbols_dict]
        return [SymbolInfo.from_dict({"symbol": sym, **info}) for sym, info in symbols_dict.items()]

    async def tiers(self) -> dict:
        """Get available tiers and pricing (no auth required).

        Returns:
            Dict with tier configuration
        """
        return await self._get("/v1/tiers")

    # --- Advanced Analytics ---

    async def regime(self, symbol: str) -> RegimeData:
        """Get market regime classification (Pro+).

        Returns statistical categorization of current market conditions
        based on Hidden Markov Model analysis.
        """
        data = await self._get(f"/v1/regime/{quote(symbol, safe='')}")
        return RegimeData.from_dict(data)

    async def drift(self, symbol: str) -> DriftData:
        """Get distribution drift scores (Pro+).

        Measures how current data patterns deviate from baselines.
        """
        data = await self._get(f"/v1/drift/{quote(symbol, safe='')}")
        return DriftData.from_dict(data)

    async def cascade_risk(self, symbol: str) -> CascadeRiskData:
        """Get liquidation cascade risk (Starter+).

        Computed from exchange liquidation feeds and depth analysis.
        """
        data = await self._get(f"/v1/cascade-risk/{quote(symbol, safe='')}")
        return CascadeRiskData.from_dict(data)

    async def consensus(self, symbol: str) -> ConsensusData:
        """Get multi-model consensus index (Enterprise).

        Statistical agreement score. Not a trade recommendation.
        """
        data = await self._get(f"/v1/consensus/{quote(symbol, safe='')}")
        return ConsensusData.from_dict(data)

    async def arbitrage(self, symbol: Optional[str] = None) -> ArbitrageData:
        """Get cross-exchange arbitrage spreads (Enterprise).

        Returns price discrepancy data between exchanges.
        """
        path = f"/v1/arbitrage/{quote(symbol, safe='')}" if symbol else "/v1/arbitrage"
        data = await self._get(path)
        return ArbitrageData.from_dict(data)

    async def market_protection(self, symbol: str) -> MarketProtectionData:
        """Get market protection scores (Enterprise).

        8 microstructure anomaly detectors for data quality assessment.
        """
        data = await self._get(f"/v1/market-protection/{quote(symbol, safe='')}")
        return MarketProtectionData.from_dict(data)

    async def depth_profile(self, symbol: str) -> DepthProfileData:
        """Get orderbook depth profile (Starter+).

        Cumulative liquidity at standardized price bands.
        """
        data = await self._get(f"/v1/depth-profile/{quote(symbol, safe='')}")
        return DepthProfileData.from_dict(data)

    async def basis(self, symbol: str) -> BasisData:
        """Get spot-futures basis analysis (Pro+).

        Cross-market structural comparison.
        """
        data = await self._get(f"/v1/basis/{quote(symbol, safe='')}")
        return BasisData.from_dict(data)

    # --- Venue Analytics ---

    async def venue_selection(self, symbol: str) -> VenueSelectionData:
        """Get venue selection ranking by tightest spread (Pro+ tier).

        Compares bid-ask spreads across all exchanges and recommends
        the optimal venue for execution.

        Args:
            symbol: Trading pair

        Returns:
            VenueSelectionData with ranked venues and recommendation
        """
        data = await self._get(f"/v1/venue-selection/{quote(symbol, safe='')}")
        return VenueSelectionData.from_dict(data)

    async def depth_resilience(self, symbol: str) -> DepthResilienceData:
        """Get depth resilience scores per exchange (Starter+ tier).

        Shows how much volume exists within 10bp and 50bp of mid-price.
        Higher depth means more resilience to large orders.

        Args:
            symbol: Trading pair

        Returns:
            DepthResilienceData with per-exchange resilience scores
        """
        data = await self._get(f"/v1/depth-resilience/{quote(symbol, safe='')}")
        return DepthResilienceData.from_dict(data)

    async def spread_events(self, symbol: str, threshold: Optional[float] = None) -> SpreadEventsData:
        """Get spread widening events (Starter+ tier).

        Detects exchanges where spreads have widened significantly
        compared to normal conditions.

        Args:
            symbol: Trading pair
            threshold: Spread widening threshold multiplier (default: 3.0)

        Returns:
            SpreadEventsData with active widening events
        """
        params: Dict[str, Any] = {}
        if threshold is not None:
            params["threshold"] = str(threshold)
        data = await self._get(f"/v1/spread-events/{quote(symbol, safe='')}", params=params or None)
        return SpreadEventsData.from_dict(data)

    async def execution_cost(self, symbol: str, trade_size_usd: Optional[float] = None) -> ExecutionCostData:
        """Get execution cost comparison across exchanges (Enterprise tier).

        Estimates trading costs using spread and market impact for a
        given trade size.

        Args:
            symbol: Trading pair
            trade_size_usd: Trade size in USD for cost estimation

        Returns:
            ExecutionCostData with per-exchange cost breakdown
        """
        params: Dict[str, Any] = {}
        if trade_size_usd is not None:
            params["trade_size_usd"] = str(trade_size_usd)
        data = await self._get(f"/v1/execution-cost/{quote(symbol, safe='')}", params=params or None)
        return ExecutionCostData.from_dict(data)

    async def correlation(self, symbol1: str, symbol2: str) -> CorrelationData:
        """Get cross-asset correlation between two symbols (Pro+ tier).

        Computes Pearson correlation from spread patterns across
        common exchanges as a proxy for price co-movement.

        Args:
            symbol1: First trading pair
            symbol2: Second trading pair

        Returns:
            CorrelationData with correlation coefficient and interpretation
        """
        data = await self._get(
            f"/v1/correlation/{quote(symbol1, safe='')}/{quote(symbol2, safe='')}"
        )
        return CorrelationData.from_dict(data)

    async def whale_flow(self, symbol: str) -> WhaleFlowData:
        """Get aggregated whale activity across all exchanges (Enterprise tier).

        Combines whale bid/ask fractions and net direction from every
        exchange into a unified whale flow signal.

        Args:
            symbol: Trading pair

        Returns:
            WhaleFlowData with aggregate whale direction and per-exchange breakdown
        """
        data = await self._get(f"/v1/whale-flow/{quote(symbol, safe='')}")
        return WhaleFlowData.from_dict(data)

    # --- Streaming ---

    async def stream(
        self,
        symbol: str,
        features: Optional[List[str]] = None,
    ) -> AsyncGenerator[dict, None]:
        """Open a real-time SSE stream for a symbol (Enterprise tier).

        Connects to the DepthSignal streaming endpoint and yields parsed
        JSON events as they arrive. Each event contains a ``type`` field
        (``"features"``, ``"composites"``, ``"liquidations"``, or
        ``"heartbeat"``) and a ``data`` payload.

        The generator keeps the connection open until the caller breaks
        out of the loop or the server closes the stream.

        Args:
            symbol: Trading pair to stream
            features: Optional list of feature types to filter (reserved
                for future server-side filtering)

        Yields:
            Dict with ``type``, ``symbol``, and ``data`` fields

        Example::

            async with AsyncDepthSignal(api_key="your-key") as ds:
                async for event in ds.stream("BTCUSDT"):
                    print(event["type"], event.get("data", {}).get("symbol"))
                    if should_stop():
                        break
        """
        path = f"/v1/stream/{quote(symbol, safe='')}"
        params: Dict[str, str] = {}
        if features:
            params["features"] = ",".join(features)

        url = f"{self._base_url}{path}"
        if params:
            from urllib.parse import urlencode
            url += f"?{urlencode(params)}"

        async with httpx.AsyncClient(
            timeout=httpx.Timeout(connect=10.0, read=None, write=10.0, pool=10.0),
        ) as stream_client:
            async with stream_client.stream(
                "GET",
                url,
                headers={
                    "X-API-Key": self._api_key,
                    "Accept": "text/event-stream",
                },
            ) as resp:
                if resp.status_code == 401:
                    raise AuthenticationError()
                if resp.status_code == 403:
                    raise TierAccessError("Real-time streaming requires Enterprise tier")
                if resp.status_code >= 400:
                    raise DepthSignalError(resp.status_code, "Stream connection failed")

                buffer = ""
                async for chunk in resp.aiter_text():
                    buffer += chunk
                    # Normalize line endings
                    buffer = buffer.replace("\r\n", "\n").replace("\r", "\n")

                    # SSE events are delimited by double newlines
                    while "\n\n" in buffer:
                        raw_event, buffer = buffer.split("\n\n", 1)
                        parsed = self._parse_sse_event(raw_event)
                        if parsed is not None:
                            yield parsed

    @staticmethod
    def _parse_sse_event(raw: str) -> Optional[dict]:
        """Parse a single SSE event block into a dict.

        Returns None for non-data events (comments, empty events).
        """
        data_lines: List[str] = []
        for line in raw.split("\n"):
            if line.startswith("data:"):
                payload = line[5:].lstrip()
                data_lines.append(payload)
            # Ignore event:, id:, retry: fields — we pass data through

        if not data_lines:
            return None

        data_str = "\n".join(data_lines)
        try:
            return json.loads(data_str)
        except (json.JSONDecodeError, ValueError):
            return {"raw": data_str}

    # --- Health ---

    async def health(self) -> dict:
        """Check API health status.

        Returns:
            Dict with service health info
        """
        return await self._get("/v1/health")
