"""Клиент API Coinetic для Python 3.9+ (только стандартная библиотека).

Coinetic — торговое название; технические имена (модуль cryptopay, класс CryptopayClient, схема
подписи CP-HMAC) не меняются. CoineticClient / CoineticError — те же классы.
"""
import hashlib
import hmac
import json
import secrets
import time
import urllib.error
import urllib.request


class CryptopayError(Exception):
    def __init__(self, status, code, message):
        super().__init__(message)
        self.status, self.code = status, code


def sign_request(secret: str, method: str, path_with_query: str, ts: int, nonce: str, body: bytes = b"") -> str:
    body_hash = hashlib.sha256(body).hexdigest()
    msg = f"{method}\n{path_with_query}\n{ts}\n{nonce}\n{body_hash}".encode()
    return hmac.new(secret.encode(), msg, hashlib.sha256).hexdigest()


class _NoRedirect(urllib.request.HTTPRedirectHandler):
    """urllib по умолчанию следует редиректам (в отличие от остальных клиентов): подписанный
    запрос не должен уходить на другой адрес, поэтому любой 3xx — ошибка."""

    def redirect_request(self, req, fp, code, msg, headers, newurl):
        raise CryptopayError(code, "redirect", f"HTTP {code}: редирект не выполняется")


_opener = urllib.request.build_opener(_NoRedirect)


def _error_from_body(status: int, body: bytes) -> CryptopayError:
    """Ошибка API — JSON {"error":{...}}; прокси может вернуть HTML (502) — тоже CryptopayError."""
    err = {}
    try:
        data = json.loads(body or b"{}")
        if isinstance(data, dict) and isinstance(data.get("error"), dict):
            err = data["error"]
    except ValueError:  # JSONDecodeError и UnicodeDecodeError
        pass
    return CryptopayError(status, err.get("code", "http_error"), err.get("message", f"HTTP {status}"))


class CryptopayClient:
    def __init__(self, base_url: str, key_id: str, secret: str, timeout: float = 10.0):
        self.base_url, self.key_id, self.secret, self.timeout = base_url.rstrip("/"), key_id, secret, timeout

    def _request(self, method, path, body=None, idempotency_key=None):
        raw = b"" if body is None else json.dumps(body, ensure_ascii=False, separators=(",", ":")).encode()
        ts, nonce = int(time.time()), secrets.token_hex(16)
        headers = {
            "Authorization": f"CP-HMAC key={self.key_id}, ts={ts}, nonce={nonce}, sig={sign_request(self.secret, method, path, ts, nonce, raw)}",
            "Accept": "application/json",
        }
        if body is not None:
            headers["Content-Type"] = "application/json"
        if idempotency_key:
            headers["Idempotency-Key"] = idempotency_key
        req = urllib.request.Request(self.base_url + path, data=raw if body is not None else None, method=method, headers=headers)
        try:
            with _opener.open(req, timeout=self.timeout) as r:
                status, data = r.status, r.read()
        except urllib.error.HTTPError as e:
            raise _error_from_body(e.code, e.read()) from None
        except urllib.error.URLError as e:
            raise CryptopayError(0, "network", str(e.reason)) from None
        try:
            return json.loads(data)
        except ValueError:
            raise CryptopayError(status, "bad_response", f"HTTP {status}: ответ не JSON") from None

    def create_invoice(self, invoice: dict, idempotency_key: str) -> dict:
        return self._request("POST", "/v1/invoices", invoice, idempotency_key)

    def get_invoice(self, invoice_id: str) -> dict:
        return self._request("GET", f"/v1/invoices/{invoice_id}")


def verify_webhook(secret: str, headers: dict, raw_body: bytes, now: float = None, max_skew: int = 300) -> dict:
    """headers — с ключами в нижнем регистре. Возвращает событие или бросает CryptopayError."""
    sig, ts = headers.get("x-webhook-signature", ""), headers.get("x-webhook-timestamp", "")
    if not sig or not ts.isdigit():
        raise CryptopayError(400, "no_signature", "нет подписи")
    if abs((now or time.time()) - int(ts)) > max_skew:
        raise CryptopayError(400, "stale", "метка времени вне окна")
    want = "v1=" + hmac.new(secret.encode(), ts.encode() + b"." + raw_body, hashlib.sha256).hexdigest()
    if not any(hmac.compare_digest(part.strip(), want) for part in sig.split(",")):
        raise CryptopayError(401, "bad_signature", "подпись не совпадает")
    return json.loads(raw_body)


PAID_STATUSES = {"paid", "overpaid"}


# Псевдонимы по торговому названию (те же классы).
CoineticClient = CryptopayClient
CoineticError = CryptopayError
