<?php
/**
 * license-php/lib/LicenseCrypto.php
 * Cryptographic Engine for Presensi License Server.
 * Standards: RSA-2048, RS256, SHA-256, Base64URL.
 */

class LicenseCrypto {
    /**
     * Base64URL encode
     */
    public static function base64UrlEncode($data) {
        return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
    }

    /**
     * Base64URL decode
     */
    public static function base64UrlDecode($data) {
        $pad = strlen($data) % 4;
        if ($pad) {
            $data .= str_repeat('=', 4 - $pad);
        }
        return base64_decode(strtr($data, '-_', '+/'));
    }

    /**
     * Clean and normalize domain name string.
     */
    public static function cleanDomain($rawDomain) {
        if (!$rawDomain) return '';
        $d = strtolower(trim((string)$rawDomain));
        $d = preg_replace('/^https?:\/\//i', '', $d);
        $d = preg_replace('/\/.*$/', '', $d);
        $d = preg_replace('/:\d+$/', '', $d);
        $d = preg_replace('/^www\./i', '', $d);
        return trim($d);
    }

    /**
     * Generates a 25-character cryptographic license key: XXXX-XXXX-XXXX-XXXX-XXXX
     * Avoids ambiguous characters (0, O, 1, I).
     */
    public static function generateLicenseKey($prefix = 'SMAN') {
        $charset = '23456789ABCDEFGHJKLMNPQRSTUVWXYZ';
        $charsetLen = strlen($charset);
        $chunks = [];

        // First chunk
        $firstChunk = strtoupper(preg_replace('/[^A-Z0-9]/i', '', $prefix));
        $firstChunk = substr($firstChunk, 0, 4);
        while (strlen($firstChunk) < 4) {
            $rand = random_int(0, $charsetLen - 1);
            $firstChunk .= $charset[$rand];
        }
        $chunks[] = $firstChunk;

        // 4 remaining chunks
        for ($i = 0; $i < 4; $i++) {
            $chunk = '';
            for ($j = 0; $j < 4; $j++) {
                $rand = random_int(0, $charsetLen - 1);
                $chunk .= $charset[$rand];
            }
            $chunks[] = $chunk;
        }

        return implode('-', $chunks);
    }

    /**
     * Hashes raw license key with SHA-256 hex.
     */
    public static function hashKey($key) {
        return hash('sha256', strtoupper(trim((string)$key)));
    }

    /**
     * Signs a JWT payload using RSA-2048 private key (RS256).
     * @param array $payload
     * @param string $privateKeyPem
     * @return string Signed JWT token
     */
    public static function signToken(array $payload, $privateKeyPem) {
        $header = ['alg' => 'RS256', 'typ' => 'JWT'];
        $encodedHeader = self::base64UrlEncode(json_encode($header, JSON_UNESCAPED_SLASHES));
        $encodedPayload = self::base64UrlEncode(json_encode($payload, JSON_UNESCAPED_SLASHES));
        $dataToSign = "{$encodedHeader}.{$encodedPayload}";

        $privKeyResource = openssl_pkey_get_private($privateKeyPem);
        if (!$privKeyResource) {
            throw new RuntimeException('INVALID_PRIVATE_KEY: Failed to parse RSA private key.');
        }

        $signature = '';
        $success = openssl_sign($dataToSign, $signature, $privKeyResource, OPENSSL_ALGO_SHA256);
        if (!$success) {
            throw new RuntimeException('SIGNING_FAILED: OpenSSL failed to sign token payload.');
        }

        $encodedSignature = self::base64UrlEncode($signature);
        return "{$dataToSign}.{$encodedSignature}";
    }

    /**
     * Verifies an RS256 JWT token using RSA-2048 public key.
     * @param string $token
     * @param string $publicKeyPem
     * @return array ['valid' => bool, 'payload' => array|null, 'reason' => string|null]
     */
    public static function verifyToken($token, $publicKeyPem) {
        if (!$token || !is_string($token)) {
            return ['valid' => false, 'reason' => 'Token missing'];
        }

        $parts = explode('.', trim($token));
        if (count($parts) !== 3) {
            return ['valid' => false, 'reason' => 'Invalid token structure'];
        }

        list($encodedHeader, $encodedPayload, $encodedSignature) = $parts;

        $headerJson = self::base64UrlDecode($encodedHeader);
        $header = json_decode($headerJson, true);
        if (!$header || ($header['alg'] ?? '') !== 'RS256') {
            return ['valid' => false, 'reason' => 'Forbidden or invalid algorithm (strictly RS256 required)'];
        }

        $pubKeyResource = openssl_pkey_get_public($publicKeyPem);
        if (!$pubKeyResource) {
            return ['valid' => false, 'reason' => 'Invalid public key format'];
        }

        $dataToVerify = "{$encodedHeader}.{$encodedPayload}";
        $rawSignature = self::base64UrlDecode($encodedSignature);

        $verifyResult = openssl_verify($dataToVerify, $rawSignature, $pubKeyResource, OPENSSL_ALGO_SHA256);
        if ($verifyResult !== 1) {
            return ['valid' => false, 'reason' => 'Cryptographic signature mismatch'];
        }

        $payloadJson = self::base64UrlDecode($encodedPayload);
        $payload = json_decode($payloadJson, true);
        if (!is_array($payload)) {
            return ['valid' => false, 'reason' => 'Malformed payload JSON'];
        }

        return ['valid' => true, 'payload' => $payload];
    }
}
