"""Wrapper around ``mcp-sentiment``.

Cerbero Bite uses one tool from this service: ``get_cross_exchange_funding``,
the input to the directional bias of ``compute_bias`` (see
``docs/01-strategy-rules.md §3.1``).

The MCP server returns the *raw period funding rate* of each exchange
(Binance/Bybit/OKX use an 8-hour funding period; Hyperliquid uses 1
hour). The wrapper converts each value to an annualised fraction
using the period that exchange actually settles on, then returns the
median across the available venues. Exchanges that did not return a
quote (``None``) are skipped.
"""

from __future__ import annotations

import statistics
from decimal import Decimal
from typing import Literal

from pydantic import BaseModel, ConfigDict

from cerbero_bite.clients._base import HttpToolClient
from cerbero_bite.clients._exceptions import McpDataAnomalyError

__all__ = ["EXCHANGE_PERIODS_PER_YEAR", "LiquidationHeatmap", "SentimentClient"]


# Funding settlement frequency per year. 1095 = 365 × 3 (8-hour funding).
EXCHANGE_PERIODS_PER_YEAR: dict[str, int] = {
    "binance": 1095,
    "bybit": 1095,
    "okx": 1095,
    "hyperliquid": 8760,  # hourly funding
}


SqueezeRiskLevel = Literal["low", "medium", "high"]


class LiquidationHeatmap(BaseModel):
    """Heuristic liquidation pressure snapshot for a single asset."""

    model_config = ConfigDict(frozen=True, extra="ignore")

    asset: str
    avg_funding_rate: Decimal | None
    oi_delta_pct_4h: Decimal | None
    oi_delta_pct_24h: Decimal | None
    long_squeeze_risk: SqueezeRiskLevel
    short_squeeze_risk: SqueezeRiskLevel

    @property
    def has_high_squeeze_risk(self) -> bool:
        return self.long_squeeze_risk == "high" or self.short_squeeze_risk == "high"


class SentimentClient:
    SERVICE = "sentiment"

    def __init__(self, http: HttpToolClient) -> None:
        if http.service != self.SERVICE:
            raise ValueError(
                f"SentimentClient requires service '{self.SERVICE}', got '{http.service}'"
            )
        self._http = http

    async def funding_cross_median_annualized(self, asset: str) -> Decimal:
        """Return the median annualised funding rate across known venues.

        Raises :class:`McpDataAnomalyError` when the snapshot lacks any
        quote — without funding data Bite cannot compute a directional
        bias and must skip the cycle.
        """
        raw = await self._http.call(
            "get_cross_exchange_funding", {"assets": [asset.upper()]}
        )
        snapshot = (raw.get("snapshot") or {}).get(asset.upper())
        if not isinstance(snapshot, dict):
            raise McpDataAnomalyError(
                f"sentiment snapshot missing for {asset}",
                service=self.SERVICE,
                tool="get_cross_exchange_funding",
            )

        annualized: list[Decimal] = []
        for venue, periods in EXCHANGE_PERIODS_PER_YEAR.items():
            value = snapshot.get(venue)
            if value is None:
                continue
            annualized.append(Decimal(str(value)) * Decimal(periods))

        if not annualized:
            raise McpDataAnomalyError(
                f"no funding venues responded for {asset}",
                service=self.SERVICE,
                tool="get_cross_exchange_funding",
            )

        # statistics.median works on Decimal: it returns an averaged
        # Decimal for even counts, which is exactly what we want.
        return Decimal(str(statistics.median(annualized)))

    async def liquidation_heatmap(self, asset: str) -> LiquidationHeatmap:
        """Return the heuristic liquidation pressure snapshot for ``asset``.

        Cerbero Bite uses ``has_high_squeeze_risk`` as an entry-time
        filter (§2.8): when either side is flagged ``high`` we skip the
        cycle to avoid selling premium right before a likely shock.
        """
        raw = await self._http.call(
            "get_liquidation_heatmap", {"asset": asset.upper()}
        )
        if not isinstance(raw, dict):
            raise McpDataAnomalyError(
                "liquidation_heatmap: unexpected response shape",
                service=self.SERVICE,
                tool="get_liquidation_heatmap",
            )

        def _maybe_dec(value: object) -> Decimal | None:
            return None if value is None else Decimal(str(value))

        long_risk = str(raw.get("long_squeeze_risk") or "low")
        short_risk = str(raw.get("short_squeeze_risk") or "low")
        if long_risk not in ("low", "medium", "high"):
            long_risk = "low"
        if short_risk not in ("low", "medium", "high"):
            short_risk = "low"

        return LiquidationHeatmap(
            asset=str(raw.get("asset") or asset).upper(),
            avg_funding_rate=_maybe_dec(raw.get("avg_funding_rate")),
            oi_delta_pct_4h=_maybe_dec(raw.get("oi_delta_pct_4h")),
            oi_delta_pct_24h=_maybe_dec(raw.get("oi_delta_pct_24h")),
            long_squeeze_risk=long_risk,  # type: ignore[arg-type]
            short_squeeze_risk=short_risk,  # type: ignore[arg-type]
        )
