<?php
// Клиент API Coinetic для PHP 8.1+ (readonly-свойства; расширения curl и json).
declare(strict_types=1);
// Coinetic — торговое название; пространство имён Cryptopay и схема подписи CP-HMAC не меняются.

namespace Cryptopay;

final class CryptopayException extends \RuntimeException
{
    public function __construct(public readonly int $status, public readonly string $errorCode, string $message)
    {
        parent::__construct($message);
    }
}

function sign_request(string $secret, string $method, string $pathWithQuery, int $ts, string $nonce, string $body = ''): string
{
    $msg = $method . "\n" . $pathWithQuery . "\n" . $ts . "\n" . $nonce . "\n" . hash('sha256', $body);
    return hash_hmac('sha256', $msg, $secret);
}

/** $headers — с ключами в нижнем регистре. Возвращает событие или бросает CryptopayException. */
function verify_webhook(string $secret, array $headers, string $rawBody, ?int $now = null, int $maxSkew = 300): array
{
    $sig = $headers['x-webhook-signature'] ?? '';
    $ts = $headers['x-webhook-timestamp'] ?? '';
    if ($sig === '' || !ctype_digit($ts)) {
        throw new CryptopayException(400, 'no_signature', 'нет подписи');
    }
    if (abs(($now ?? time()) - (int)$ts) > $maxSkew) {
        throw new CryptopayException(400, 'stale', 'метка времени вне окна');
    }
    $want = 'v1=' . hash_hmac('sha256', $ts . '.' . $rawBody, $secret);
    foreach (explode(',', $sig) as $part) {
        if (hash_equals($want, trim($part))) {
            return json_decode($rawBody, true, 64, JSON_THROW_ON_ERROR);
        }
    }
    throw new CryptopayException(401, 'bad_signature', 'подпись не совпадает');
}

const PAID_STATUSES = ['paid', 'overpaid'];

final class Client
{
    public function __construct(private string $baseUrl, private string $keyId, private string $secret, private int $timeout = 10)
    {
        $this->baseUrl = rtrim($baseUrl, '/');
    }

    public function createInvoice(array $invoice, string $idempotencyKey): array
    {
        // Пустой массив PHP кодируется как [] — API ждёт объект: приводим metadata к объекту
        // всегда, в том числе пустой (['metadata' => []] -> {"metadata":{}}).
        if (array_key_exists('metadata', $invoice) && is_array($invoice['metadata'])) {
            $invoice['metadata'] = (object)$invoice['metadata'];
        }
        return $this->request('POST', '/v1/invoices', $invoice, $idempotencyKey);
    }

    public function getInvoice(string $invoiceId): array
    {
        return $this->request('GET', '/v1/invoices/' . rawurlencode($invoiceId));
    }

    private function request(string $method, string $path, ?array $body = null, ?string $idem = null): array
    {
        $raw = $body === null ? '' : json_encode($body, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR);
        $ts = time();
        $nonce = bin2hex(random_bytes(16));
        $headers = [
            'Authorization: CP-HMAC key=' . $this->keyId . ', ts=' . $ts . ', nonce=' . $nonce
                . ', sig=' . sign_request($this->secret, $method, $path, $ts, $nonce, $raw),
            'Accept: application/json',
        ];
        if ($body !== null) {
            $headers[] = 'Content-Type: application/json';
        }
        if ($idem !== null) {
            $headers[] = 'Idempotency-Key: ' . $idem;
        }
        $ch = curl_init($this->baseUrl . $path);
        curl_setopt_array($ch, [
            CURLOPT_CUSTOMREQUEST => $method,
            CURLOPT_HTTPHEADER => $headers,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_TIMEOUT => $this->timeout,
            CURLOPT_FOLLOWLOCATION => false,
        ]);
        if ($body !== null) {
            curl_setopt($ch, CURLOPT_POSTFIELDS, $raw);
        }
        $resp = curl_exec($ch);
        if ($resp === false) {
            $e = curl_error($ch);
            curl_close($ch);
            throw new CryptopayException(0, 'network', $e);
        }
        $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
        curl_close($ch);
        $data = json_decode((string)$resp, true) ?? [];
        if ($status >= 400) {
            $err = $data['error'] ?? [];
            throw new CryptopayException($status, $err['code'] ?? 'http_error', $err['message'] ?? "HTTP $status");
        }
        return $data;
    }
}
