<?php
/**
 * DPoP helper for native OAuth-bound MakePay tokens.
 */

namespace IPS\makepay\Dpop;

if (!\defined('\IPS\SUITE_UNIQUE_KEY')) {
    exit;
}

class _Dpop
{
    public static function createKeyPair(): array
    {
        $resource = openssl_pkey_new([
            'curve_name' => 'prime256v1',
            'private_key_type' => OPENSSL_KEYTYPE_EC,
        ]);

        if ($resource === false || !openssl_pkey_export($resource, $privatePem)) {
            throw new \RuntimeException('Unable to create MakePay DPoP key pair.');
        }

        $details = openssl_pkey_get_details($resource);
        $ec = $details['ec'] ?? [];
        $jwk = [
            'crv' => 'P-256',
            'kty' => 'EC',
            'x' => self::base64Url((string) ($ec['x'] ?? '')),
            'y' => self::base64Url((string) ($ec['y'] ?? '')),
        ];

        return [
            'privatePem' => $privatePem,
            'publicJwk' => $jwk,
            'jkt' => self::thumbprint($jwk),
        ];
    }

    public static function proof(string $privatePem, array $publicJwk, string $method, string $url, ?string $accessToken = null): string
    {
        $header = [
            'alg' => 'ES256',
            'jwk' => $publicJwk,
            'typ' => 'dpop+jwt',
        ];
        $payload = [
            'htu' => $url,
            'htm' => strtoupper($method),
            'iat' => time(),
            'jti' => bin2hex(random_bytes(16)),
        ];

        if ($accessToken) {
            $payload['ath'] = self::base64Url(hash('sha256', $accessToken, true));
        }

        $signingInput = self::base64Url(json_encode($header, JSON_UNESCAPED_SLASHES)) .
            '.' .
            self::base64Url(json_encode($payload, JSON_UNESCAPED_SLASHES));

        if (!openssl_sign($signingInput, $signature, $privatePem, OPENSSL_ALGO_SHA256)) {
            throw new \RuntimeException('Unable to sign MakePay DPoP proof.');
        }

        return $signingInput . '.' . self::base64Url(self::derToJose($signature));
    }

    public static function thumbprint(array $jwk): string
    {
        $json = json_encode([
            'crv' => $jwk['crv'],
            'kty' => $jwk['kty'],
            'x' => $jwk['x'],
            'y' => $jwk['y'],
        ], JSON_UNESCAPED_SLASHES);

        return self::base64Url(hash('sha256', (string) $json, true));
    }

    private static function derToJose(string $der): string
    {
        $offset = 3;
        $rLength = ord($der[$offset]);
        $r = substr($der, $offset + 1, $rLength);
        $offset += $rLength + 2;
        $sLength = ord($der[$offset - 1]);
        $s = substr($der, $offset, $sLength);

        return str_pad(ltrim($r, "\x00"), 32, "\x00", STR_PAD_LEFT) .
            str_pad(ltrim($s, "\x00"), 32, "\x00", STR_PAD_LEFT);
    }

    private static function base64Url(string $value): string
    {
        return rtrim(strtr(base64_encode($value), '+/', '-_'), '=');
    }
}
