"""Клиент Crypto Pay API (@CryptoBot)."""

from __future__ import annotations

import hashlib
import hmac
import json
from typing import Any, Optional

import requests

from server.config import CRYPTOBOT_API, CRYPTOBOT_TOKEN, PANEL_DOMAIN

CRYPTOBOT_WEBHOOK_PORT = 5001
_TIMEOUT = 25
_HEADERS_BASE = {
    "Accept": "application/json",
    "User-Agent": "OZPanel/1.0",
}


class CryptoBotError(RuntimeError):
    def __init__(self, message: str, *, code: Optional[int] = None, name: str = ""):
        super().__init__(message)
        self.code = code
        self.name = name or ""


def cryptobot_configured() -> bool:
    return bool(str(CRYPTOBOT_TOKEN or "").strip())


def cryptobot_webhook_url() -> str:
    domain = str(PANEL_DOMAIN or "").strip().rstrip("/")
    if not domain:
        return ""
    if domain.startswith("http://") or domain.startswith("https://"):
        base = domain
    else:
        base = f"https://{domain}:{CRYPTOBOT_WEBHOOK_PORT}"
    return f"{base}/api/pay/cryptobot"


def verify_cryptobot_signature(body: bytes, signature: str, token: Optional[str] = None) -> bool:
    secret_src = str(token if token is not None else CRYPTOBOT_TOKEN or "").strip()
    if not secret_src or not signature:
        return False
    secret = hashlib.sha256(secret_src.encode("utf-8")).digest()
    expected = hmac.new(secret, body, hashlib.sha256).hexdigest()
    got = str(signature).strip().lower()
    return hmac.compare_digest(expected, got)


def _error_text(payload: Any, raw: str = "") -> tuple[str, Optional[int], str]:
    err = payload.get("error") if isinstance(payload, dict) else None
    if isinstance(err, dict):
        name = str(err.get("name") or "")
        code = err.get("code")
        try:
            code = int(code) if code is not None else None
        except (TypeError, ValueError):
            code = None
        if name == "AMOUNT_TOO_SMALL":
            return "Сумма слишком маленькая для Crypto Bot", code, name
        if name == "AMOUNT_TOO_BIG":
            return "Сумма слишком большая для Crypto Bot", code, name
        if name:
            return f"Crypto Bot: {name}", code, name
    if isinstance(err, str) and err.strip():
        return err.strip(), None, ""
    snippet = " ".join(str(raw or "").split())[:180]
    if snippet:
        return f"Ошибка Crypto Bot: {snippet}", None, ""
    return "Ошибка Crypto Bot", None, ""


def _parse_response(method: str, response: requests.Response) -> Any:
    raw = response.text or ""
    try:
        payload = response.json()
    except ValueError:
        print(f"cryptobot {method} non-json {response.status_code}: {raw[:400]}")
        raise CryptoBotError("Crypto Bot вернул не JSON")
    if not isinstance(payload, dict) or not payload.get("ok"):
        message, code, name = _error_text(payload if isinstance(payload, dict) else {}, raw)
        print(f"cryptobot {method} {response.status_code}: {message} {raw[:400]}")
        raise CryptoBotError(message, code=code, name=name)
    return payload.get("result")


def cryptobot_call(method: str, **params: Any) -> Any:
    token = str(CRYPTOBOT_TOKEN or "").strip()
    if not token:
        raise CryptoBotError("Crypto Bot не настроен")
    query = {key: value for key, value in params.items() if value is not None}
    url = f"{str(CRYPTOBOT_API or 'https://pay.crypt.bot/api').rstrip('/')}/{method.lstrip('/')}"
    headers = {
        **_HEADERS_BASE,
        "Crypto-Pay-API-Token": token,
        "Content-Type": "application/json",
    }
    try:
        response = requests.post(url, headers=headers, json=query, timeout=_TIMEOUT)
    except requests.RequestException as exc:
        print(f"cryptobot {method} network: {exc}")
        raise CryptoBotError(f"Crypto Bot недоступен ({exc.__class__.__name__})") from exc
    return _parse_response(method, response)


def invoice_items(result: Any) -> list[dict]:
    if isinstance(result, list):
        return [item for item in result if isinstance(item, dict)]
    if isinstance(result, dict):
        items = result.get("items")
        if isinstance(items, list):
            return [item for item in items if isinstance(item, dict)]
        if result.get("invoice_id") is not None:
            return [result]
    return []


def _invoice_params(amount: float, payload: str, description: str) -> dict[str, Any]:
    return {
        "amount": f"{float(amount):.2f}",
        "description": str(description or "")[:1024],
        "payload": str(payload or "")[:4096],
        "hidden_message": "Баланс зачислен. Вернитесь в OZ Panel.",
        "allow_comments": False,
        "allow_anonymous": False,
        "expires_in": 1800,
    }


def create_fiat_invoice(amount: float, payload: str, description: str) -> dict:
    params = _invoice_params(amount, payload, description)
    try:
        result = cryptobot_call(
            "createInvoice",
            currency_type="fiat",
            fiat="USD",
            accepted_assets="USDT,TON,BTC,ETH,LTC,BNB,TRX,USDC",
            **params,
        )
    except CryptoBotError as exc:
        name = (exc.name or str(exc)).upper()
        if "FIAT" not in name and "CURRENCY" not in name:
            raise
        print(f"cryptobot fiat invoice failed ({exc}), fallback USDT")
        result = cryptobot_call(
            "createInvoice",
            currency_type="crypto",
            asset="USDT",
            **params,
        )
    if not isinstance(result, dict):
        raise CryptoBotError("Crypto Bot не вернул счёт")
    return result


def get_invoices_by_ids(invoice_ids: list[int]) -> list[dict]:
    ids = [str(int(item)) for item in invoice_ids if item not in (None, "")]
    if not ids:
        return []
    result = cryptobot_call("getInvoices", invoice_ids=",".join(ids))
    return invoice_items(result)


def set_cryptobot_webhook(url: str) -> Any:
    return cryptobot_call("setWebhook", url=url)
