<?php

/**
 * EkstraPAY — Credit Card API SDK (PHP, dependency-free, cURL)
 * ===========================================================
 *
 * Server-to-server client for merchants. Handles HMAC-SHA256 request signing
 * exactly as the backend (CCApiAuth) expects.
 *
 * Quick start:
 *
 *   require 'EkstraPayCC.php';
 *
 *   $cc = new EkstraPayCC(
 *       'YOUR_API_KEY',
 *       'YOUR_API_TOKEN',
 *       'https://v2.ekstrapay.com/api'
 *   );
 *
 *   try {
 *       $res = $cc->createPayment([
 *           'amount'             => 250.75,
 *           'order_id'           => 'ORD-1001',
 *           'customer_ref'       => 'cust-123',
 *           'tc_no'              => '11111111111',
 *           'first_name'         => 'Ali',
 *           'last_name'          => 'Veli',
 *           'return_url_success' => 'https://merchant.com/ok',
 *           'return_url_fail'    => 'https://merchant.com/fail',
 *       ]);
 *       // 3D          -> $res['redirect_url']
 *       // native/link -> $res['pay_screen']['pay_url']
 *       // KYC         -> $res['kyc']['code']
 *   } catch (EkstraPayException $e) {
 *       echo $e->getMessage() . ' (' . $e->errorCode . ')';
 *   }
 *
 * Signing (matches server byte-for-byte):
 *   cleaned   = drop empty('')/null, trim strings
 *   json_str  = json_encode(cleaned, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES);
 *               empty -> "[]"
 *   message   = api_key|timestamp|nonce|json_str
 *   signature = hash_hmac('sha256', message, api_token)
 *   body sent = json_str
 */

class EkstraPayException extends \Exception
{
    /** @var int|null HTTP status */
    public $httpCode;
    /** @var string|null e.g. "CC-AUTH-005" */
    public $errorCode;
    /** @var mixed */
    public $data;

    public function __construct($message, $httpCode = null, $errorCode = null, $data = null)
    {
        parent::__construct($message);
        $this->httpCode = $httpCode;
        $this->errorCode = $errorCode;
        $this->data = $data;
    }
}

class EkstraPayCC
{
    private $apiKey;
    private $apiToken;
    private $baseUrl;
    private $timeout;

    public function __construct($apiKey, $apiToken, $baseUrl = 'https://v2.ekstrapay.com/api', $timeout = 30)
    {
        if (!$apiKey || !$apiToken) {
            throw new \InvalidArgumentException('apiKey and apiToken are required');
        }
        $this->apiKey = $apiKey;
        $this->apiToken = $apiToken;
        $this->baseUrl = rtrim($baseUrl, '/');
        $this->timeout = $timeout;
    }

    // ── Public API ──────────────────────────────────────────────

    public function createPayment(array $body): array
    {
        return $this->post('/v1/cc/payment/create', $body);
    }

    public function checkPayment(string $transactionId, bool $sync = false): array
    {
        return $this->post('/v1/cc/payment/check', ['transaction_id' => $transactionId, 'sync' => $sync]);
    }

    public function cancelPayment(string $transactionId): array
    {
        return $this->post('/v1/cc/payment/cancel', ['transaction_id' => $transactionId]);
    }

    public function detailPayment(string $transactionId): array
    {
        return $this->post('/v1/cc/payment/detail', ['transaction_id' => $transactionId]);
    }

    // ── Callback verification ───────────────────────────────────

    /**
     * Verify an incoming callback's `hash`. Returns true if authentic.
     *
     * Hash = sha256 of these fields joined by ":" IN THIS ORDER, with
     * null/missing optional fields as "" (positions preserved):
     *   transaction_id : order_id : customer_ref : tc_no : amount : final_amount : status
     *
     * Usage:
     *   $payload = json_decode(file_get_contents('php://input'), true);
     *   if (!EkstraPayCC::verifyCallback($payload)) { http_response_code(400); exit; }
     */
    public static function verifyCallback(array $payload): bool
    {
        $s = static fn($v) => $v === null ? '' : (string) $v;

        $parts = [
            $s($payload['transaction_id'] ?? null),
            $s($payload['order_id'] ?? null),
            $s($payload['customer_ref'] ?? null),
            $s($payload['tc_no'] ?? null),
            $s($payload['amount'] ?? null),
            $s($payload['final_amount'] ?? null),
            $s($payload['status'] ?? null),
        ];
        $expected = hash('sha256', implode(':', $parts));
        return hash_equals($expected, $s($payload['hash'] ?? null));
    }

    // ── Internals ───────────────────────────────────────────────

    private function clean(array $body): array
    {
        $cleaned = [];
        foreach ($body as $key => $val) {
            if (is_string($val)) {
                $val = trim($val);
            }
            if ($val === '' || $val === null) {
                continue;
            }
            $cleaned[$key] = $val;
        }
        return $cleaned;
    }

    private function canonicalJson(array $cleaned): string
    {
        if (empty($cleaned)) {
            return '[]';
        }
        return json_encode($cleaned, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
    }

    private function post(string $path, array $body): array
    {
        $cleaned = $this->clean($body);
        $jsonStr = $this->canonicalJson($cleaned);

        $ts = (string) time();
        $nonce = bin2hex(random_bytes(16));
        $message = $this->apiKey . '|' . $ts . '|' . $nonce . '|' . $jsonStr;
        $sig = hash_hmac('sha256', $message, $this->apiToken);

        $ch = curl_init($this->baseUrl . $path);
        curl_setopt_array($ch, [
            CURLOPT_POST => true,
            CURLOPT_POSTFIELDS => $jsonStr,           // send the exact signed canonical string
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_TIMEOUT => $this->timeout,
            CURLOPT_HTTPHEADER => [
                'Content-Type: application/json',
                'Accept: application/json',
                'X-API-Key: ' . $this->apiKey,
                'X-Timestamp: ' . $ts,
                'X-Nonce: ' . $nonce,
                'X-Signature: ' . $sig,
            ],
        ]);

        $raw = curl_exec($ch);
        if ($raw === false) {
            $err = curl_error($ch);
            curl_close($ch);
            throw new EkstraPayException('Connection error: ' . $err);
        }
        $httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);

        $payload = json_decode($raw, true) ?: [];

        if (($payload['status'] ?? null) === 'error' || $httpCode >= 400) {
            $data = $payload['data'] ?? [];
            throw new EkstraPayException(
                $payload['description'] ?? $payload['message'] ?? 'Request failed',
                $httpCode,
                is_array($data) ? ($data['error_code'] ?? null) : null,
                $data
            );
        }

        return $payload['data'] ?? [];
    }
}
