"""
Billing — przelicznik USD→SAT i live BTC rate (CoinGecko, cache 60s).

API publiczne:
    await usd_to_sat(usd) -> int        # zwraca sat-y; lazy-fetch kursu
    await get_btc_usd_rate() -> float   # surowy kurs BTC/USD (≈78000)
    set_btc_usd_rate(rate)              # do testów — ustawia kurs ręcznie

Dlaczego CoinGecko: darmowe, bez klucza, akurat dla 1 zapytania na minutę
mieści się w limicie. Cache (`_RATE_TTL_S=60`) chroni nas przed pingowaniem
ich przy każdej turze agenta.
"""

from __future__ import annotations

import asyncio
import logging
import time
from typing import Any

import httpx

logger = logging.getLogger("mcp_nostr_explorer.billing")

COINGECKO_URL = "https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=usd"
_RATE_TTL_S = 60.0
_RATE_FETCH_TIMEOUT_S = 5.0
SATS_PER_BTC = 100_000_000

# Module-state — _kurs i _kurs_at są chronione przez _kurs_lock; testy mogą
# nadpisać przez set_btc_usd_rate(...).
_kurs: float | None = None
_kurs_at: float = 0.0
_kurs_lock = asyncio.Lock()


def set_btc_usd_rate(rate: float | None) -> None:
    """Ustawia kurs ręcznie (do testów). None resetuje cache → następne
    `get_btc_usd_rate` znów pobierze przez HTTP."""
    global _kurs, _kurs_at
    _kurs = rate
    _kurs_at = time.monotonic() if rate is not None else 0.0


async def _pobierz_kurs() -> float:
    async with httpx.AsyncClient(timeout=_RATE_FETCH_TIMEOUT_S) as cli:
        r = await cli.get(COINGECKO_URL)
        r.raise_for_status()
        data: dict[str, Any] = r.json()
    rate = data["bitcoin"]["usd"]
    if not isinstance(rate, (int, float)) or rate <= 0:
        raise ValueError(f"CoinGecko: niepoprawny kurs BTC/USD: {rate!r}")
    return float(rate)


async def get_btc_usd_rate() -> float:
    """Zwraca aktualny kurs BTC/USD. Cache 60s; w razie błędu HTTP zwraca
    ostatni znany kurs (jeśli był), inaczej raise."""
    global _kurs, _kurs_at
    now = time.monotonic()
    if _kurs is not None and (now - _kurs_at) < _RATE_TTL_S:
        return _kurs
    async with _kurs_lock:
        # Double-check po wejściu w lock (concurrent fetchers).
        now = time.monotonic()
        if _kurs is not None and (now - _kurs_at) < _RATE_TTL_S:
            return _kurs
        try:
            rate = await _pobierz_kurs()
            _kurs = rate
            _kurs_at = time.monotonic()
            return rate
        except Exception as e:
            if _kurs is not None:
                logger.warning(
                    f"CoinGecko niedostępny ({type(e).__name__}: {e}) — "
                    f"używam ostatniego znanego kursu ${_kurs:.0f}"
                )
                return _kurs
            raise


async def usd_to_sat(usd: float) -> int:
    """Konwertuje USD na sat-y po aktualnym kursie BTC/USD. ROUND_HALF_UP
    żeby najmniejsze obciążenia i tak schodziły z konta (1 sat to 1 sat).

    `usd <= 0` → 0; sensowne dla turow gdzie cost==0 (BYOK)."""
    if usd <= 0:
        return 0
    rate = await get_btc_usd_rate()
    return int(round(usd * SATS_PER_BTC / rate))
