from __future__ import annotations

from dataclasses import dataclass
from decimal import Decimal
from typing import Any, Protocol

import requests

COINGECKO_MARKETS_URL = "https://api.coingecko.com/api/v3/coins/markets"

COINGECKO_IDS: dict[str, str] = {
    "BTC": "bitcoin",
    "ETH": "ethereum",
    "SOL": "solana",
    "LINK": "chainlink",
    "WLD": "worldcoin-wld",
    "SUI": "sui",
    "ENA": "ethena",
    "BCH": "bitcoin-cash",
    "HYPE": "hyperliquid",
}


def _d(value: Any, default: str = "0") -> Decimal:
    try:
        if value is None:
            return Decimal(default)
        return Decimal(str(value))
    except Exception:
        return Decimal(default)


class FundamentalsTransport(Protocol):
    def get(self, url: str, *, params: dict[str, Any], timeout: int = 10) -> Any: ...


class RequestsFundamentalsTransport:
    def get(self, url: str, *, params: dict[str, Any], timeout: int = 10) -> Any:
        response = requests.get(url, params=params, timeout=timeout)
        response.raise_for_status()
        return response.json()


@dataclass(frozen=True)
class CoinFundamentals:
    coin: str
    coingecko_id: str
    market_cap_usd: Decimal
    fdv_usd: Decimal
    total_volume_usd: Decimal
    market_cap_rank: int | None
    price_change_24h_pct: Decimal
    price_change_7d_pct: Decimal
    circulating_supply: Decimal
    total_supply: Decimal
    price_change_1h_pct: Decimal = Decimal("0")
    price_change_30d_pct: Decimal = Decimal("0")
    ath_usd: Decimal = Decimal("0")
    atl_usd: Decimal = Decimal("0")
    fdv_to_market_cap: Decimal = Decimal("0")
    categories: tuple[str, ...] = ()
    source: str = "coingecko_markets"
    status: str = "loaded"

    def to_dict(self) -> dict[str, Any]:
        return {
            "source": self.source,
            "status": self.status,
            "coingecko_id": self.coingecko_id,
            "market_cap_usd": str(self.market_cap_usd),
            "fdv_usd": str(self.fdv_usd),
            "total_volume_usd": str(self.total_volume_usd),
            "market_cap_rank": self.market_cap_rank,
            "price_change_24h_pct": str(self.price_change_24h_pct),
            "price_change_7d_pct": str(self.price_change_7d_pct),
            "price_change_1h_pct": str(self.price_change_1h_pct),
            "price_change_30d_pct": str(self.price_change_30d_pct),
            "circulating_supply": str(self.circulating_supply),
            "total_supply": str(self.total_supply),
            "ath_usd": str(self.ath_usd),
            "atl_usd": str(self.atl_usd),
            "fdv_to_market_cap": str(self.fdv_to_market_cap),
            "categories": list(self.categories),
        }


@dataclass(frozen=True)
class FundamentalsSnapshot:
    fundamentals: dict[str, CoinFundamentals]
    source: str = "coingecko_markets"
    status: str = "loaded"
    error: str | None = None

    def for_coin(self, coin: str) -> dict[str, Any]:
        row = self.fundamentals.get(coin.upper())
        if row is None:
            return {"source": self.source, "status": "not_loaded", "reason": "coin_not_available"}
        return row.to_dict()

    def to_dict(self) -> dict[str, Any]:
        return {
            "source": self.source,
            "status": self.status,
            "error": self.error,
            "coins": {coin: row.to_dict() for coin, row in sorted(self.fundamentals.items())},
        }


def build_unavailable_fundamentals_snapshot(*, reason: str) -> FundamentalsSnapshot:
    return FundamentalsSnapshot(fundamentals={}, status="unavailable", error=reason)


class CoinGeckoFundamentalsClient:
    """Read-only CoinGecko fundamentals adapter.

    This client fetches public market/fundamental fields only. It never handles
    secrets and never has execution authority.
    """

    def __init__(self, transport: FundamentalsTransport | None = None) -> None:
        self.transport = transport or RequestsFundamentalsTransport()

    def collect(self, coins: tuple[str, ...]) -> FundamentalsSnapshot:
        requested = {coin.upper() for coin in coins}
        id_to_coin = {coingecko_id: coin for coin, coingecko_id in COINGECKO_IDS.items() if coin in requested}
        if not id_to_coin:
            return FundamentalsSnapshot(fundamentals={}, status="empty")
        params = {
            "vs_currency": "usd",
            "ids": ",".join(sorted(id_to_coin)),
            "order": "market_cap_desc",
            "per_page": len(id_to_coin),
            "page": 1,
            "sparkline": "false",
            "price_change_percentage": "1h,24h,7d,30d",
        }
        try:
            rows = self.transport.get(COINGECKO_MARKETS_URL, params=params, timeout=10)
        except Exception as exc:
            return build_unavailable_fundamentals_snapshot(reason=type(exc).__name__)
        if not isinstance(rows, list):
            return build_unavailable_fundamentals_snapshot(reason="unexpected_response_shape")
        fundamentals: dict[str, CoinFundamentals] = {}
        for row in rows:
            if not isinstance(row, dict):
                continue
            coin = id_to_coin.get(str(row.get("id", "")))
            if not coin:
                continue
            rank_raw = row.get("market_cap_rank")
            try:
                rank = int(rank_raw) if rank_raw is not None else None
            except Exception:
                rank = None
            fundamentals[coin] = CoinFundamentals(
                coin=coin,
                coingecko_id=str(row.get("id") or COINGECKO_IDS[coin]),
                market_cap_usd=_d(row.get("market_cap")),
                fdv_usd=_d(row.get("fully_diluted_valuation")),
                total_volume_usd=_d(row.get("total_volume")),
                market_cap_rank=rank,
                price_change_24h_pct=_d(row.get("price_change_percentage_24h_in_currency", row.get("price_change_percentage_24h"))),
                price_change_7d_pct=_d(row.get("price_change_percentage_7d_in_currency")),
                circulating_supply=_d(row.get("circulating_supply")),
                total_supply=_d(row.get("total_supply")),
                price_change_1h_pct=_d(row.get("price_change_percentage_1h_in_currency")),
                price_change_30d_pct=_d(row.get("price_change_percentage_30d_in_currency")),
                ath_usd=_d(row.get("ath")),
                atl_usd=_d(row.get("atl")),
                fdv_to_market_cap=(_d(row.get("fully_diluted_valuation")) / _d(row.get("market_cap")) if _d(row.get("market_cap")) > 0 else Decimal("0")),
                categories=(),
            )
        return FundamentalsSnapshot(fundamentals=fundamentals)
