<?php

declare(strict_types=1);

final class MyronPublicInteractionTokenService
{
    private const DEFAULT_TTL_SECONDS = 300;
    private const ALLOWED_PURPOSES = ['form', 'quiz'];

    public function issue(string $purpose, string $resource, int $ttlSeconds = self::DEFAULT_TTL_SECONDS): array
    {
        $purpose = $this->normalizePurpose($purpose);
        $resource = $this->normalizeResource($resource);
        $expiresAt = time() + max(30, min(900, $ttlSeconds));
        $payload = [
            'purpose' => $purpose,
            'resource' => $resource,
            'expiresAt' => $expiresAt,
            'nonce' => bin2hex(random_bytes(12)),
        ];
        $encoded = self::base64UrlEncode(json_encode($payload, JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES));
        $signature = self::base64UrlEncode(hash_hmac('sha256', $encoded, $this->secret(), true));

        return [
            'token' => $encoded . '.' . $signature,
            'expiresAt' => gmdate(DATE_ATOM, $expiresAt),
        ];
    }

    public function requireValid(?string $token, string $purpose, string $resource): void
    {
        $purpose = $this->normalizePurpose($purpose);
        $resource = $this->normalizeResource($resource);
        [$encoded, $signature] = array_pad(explode('.', trim((string) $token), 2), 2, '');
        if ($encoded === '' || $signature === '') {
            throw new MyronCsrfFailureException('Security check failed. Refresh the page and try again.');
        }

        $expected = self::base64UrlEncode(hash_hmac('sha256', $encoded, $this->secret(), true));
        if (!hash_equals($expected, $signature)) {
            throw new MyronCsrfFailureException('Security check failed. Refresh the page and try again.');
        }

        $decoded = self::base64UrlDecode($encoded);
        $payload = is_string($decoded) ? json_decode($decoded, true) : null;
        if (
            !is_array($payload)
            || (string) ($payload['purpose'] ?? '') !== $purpose
            || (string) ($payload['resource'] ?? '') !== $resource
            || (int) ($payload['expiresAt'] ?? 0) < time()
            || trim((string) ($payload['nonce'] ?? '')) === ''
        ) {
            throw new MyronCsrfFailureException('Security check failed. Refresh the page and try again.');
        }
    }

    public function requireValidQuizTokenWithLegacySessionFallback(?string $token, string $resource): string
    {
        try {
            $this->requireValid($token, 'quiz', $resource);
            return 'signed_public_interaction';
        } catch (MyronCsrfFailureException) {
            // Rolling first-party Module compatibility only: older quiz renderers emit the
            // session token. myronRequireCsrf remains exact-session and constant-time.
            myronRequireCsrf($token);
            return 'legacy_session';
        }
    }

    private function normalizePurpose(string $purpose): string
    {
        $purpose = strtolower(trim($purpose));
        if (!in_array($purpose, self::ALLOWED_PURPOSES, true)) {
            throw new InvalidArgumentException('Unsupported public interaction purpose.');
        }
        return $purpose;
    }

    private function normalizeResource(string $resource): string
    {
        $resource = trim($resource);
        if ($resource === '' || strlen($resource) > 160 || preg_match('/^[A-Za-z0-9_-]+$/', $resource) !== 1) {
            throw new InvalidArgumentException('Invalid public interaction resource.');
        }
        return $resource;
    }

    private function secret(): string
    {
        $secret = trim((string) getenv('MYRON_APP_KEY'));
        if ($secret === '') {
            throw new RuntimeException('MYRON_APP_KEY is required for public interaction tokens.');
        }
        return $secret;
    }

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

    private static function base64UrlDecode(string $value): string|false
    {
        $padding = strlen($value) % 4;
        if ($padding !== 0) {
            $value .= str_repeat('=', 4 - $padding);
        }
        return base64_decode(strtr($value, '-_', '+/'), true);
    }
}

function myronPublicInteractionTokenService(): MyronPublicInteractionTokenService
{
    static $service = null;
    if (!$service instanceof MyronPublicInteractionTokenService) {
        $service = new MyronPublicInteractionTokenService();
    }
    return $service;
}
