<?php

namespace Licenzo\Services;

// Prevent direct access
if (!defined('ABSPATH')) {
    exit;
}

/**
 * Activation Token Service
 *
 * Generates and verifies HMAC-signed activation tokens (JWT-like).
 * Tokens are long-lived credentials bound to a specific activation + site fingerprint.
 * Format: base64url(payload).base64url(hmac_sha256_signature)
 *
 * Token payload claims:
 *   v   - token version (for future format migration)
 *   lid - license ID
 *   aid - activation ID
 *   pid - product ID
 *   vid - variation ID
 *   fp  - site fingerprint (MD5 of site URL, binds token to the activating site)
 *   iat - issued at timestamp (Unix)
 */
class ActivationTokenService
{
    /**
     * Current token version. Increment when payload format changes.
     */
    private const TOKEN_VERSION = 1;

    /**
     * Header name constants for SDK ↔ server communication.
     */
    public const HEADER_LICENSE_KEY = 'X-Licenzo-License-Key';
    public const HEADER_ACTIVATION_TOKEN = 'X-Licenzo-Activation-Token';
    public const HEADER_FINGERPRINT = 'X-Licenzo-Fingerprint';
    public const HEADER_PRODUCT_UUID = 'X-Licenzo-Product';
    public const HEADER_CHANNEL = 'X-Licenzo-Channel';

    /**
     * Generate an HMAC-signed activation token.
     *
     * @param int    $licenseId
     * @param int    $activationId
     * @param int    $productId
     * @param int    $variationId
     * @param string $fingerprint  Site fingerprint (MD5 of site URL)
     * @return string The signed token string (base64url payload . base64url signature)
     */
    public function generate(
        int $licenseId,
        int $activationId,
        int $productId,
        int $variationId,
        string $fingerprint
    ): string {
        $payload = [
            'v'   => self::TOKEN_VERSION,
            'lid' => $licenseId,
            'aid' => $activationId,
            'pid' => $productId,
            'vid' => $variationId,
            'fp'  => sanitize_text_field($fingerprint),
            'iat' => time(),
        ];

        $encodedPayload = $this->base64UrlEncode(wp_json_encode($payload));
        $signature = hash_hmac('sha256', $encodedPayload, $this->secret());

        return $encodedPayload . '.' . $signature;
    }

    /**
     * Verify and decode an activation token.
     *
     * Returns the decoded payload array on success, or null on failure.
     * Does NOT check activation status in the database — caller must do that.
     *
     * @param string $token
     * @return array<string,mixed>|null Decoded claims or null if invalid.
     */
    public function verify(string $token): ?array
    {
        $token = trim($token);
        if ($token === '' || strpos($token, '.') === false) {
            return null;
        }

        $parts = explode('.', $token, 2);
        if (count($parts) !== 2) {
            return null;
        }

        [$encodedPayload, $signature] = $parts;
        if ($encodedPayload === '' || $signature === '') {
            return null;
        }

        // Verify HMAC signature
        $expected = hash_hmac('sha256', $encodedPayload, $this->secret());
        if (!hash_equals($expected, $signature)) {
            return null;
        }

        // Decode payload
        $json = $this->base64UrlDecode($encodedPayload);
        $payload = json_decode($json, true);
        if (!is_array($payload)) {
            return null;
        }

        // Validate token version
        if ((int) ($payload['v'] ?? 0) !== self::TOKEN_VERSION) {
            return null;
        }

        // Validate required claims exist and are positive integers
        foreach (['lid', 'aid', 'pid', 'vid'] as $claim) {
            if (!isset($payload[$claim]) || (int) $payload[$claim] <= 0) {
                return null;
            }
        }

        // Validate fingerprint is present
        if (empty($payload['fp']) || !is_string($payload['fp'])) {
            return null;
        }

        // Validate issued-at is a reasonable timestamp
        if (!isset($payload['iat']) || (int) $payload['iat'] <= 0) {
            return null;
        }

        // Cast to correct types
        return [
            'v'   => (int) $payload['v'],
            'lid' => (int) $payload['lid'],
            'aid' => (int) $payload['aid'],
            'pid' => (int) $payload['pid'],
            'vid' => (int) $payload['vid'],
            'fp'  => (string) $payload['fp'],
            'iat' => (int) $payload['iat'],
        ];
    }

    /**
     * Verify an activation token and check that the fingerprint matches.
     *
     * @param string $token
     * @param string $fingerprint The fingerprint from the request header.
     * @return array<string,mixed>|null Decoded claims or null if invalid/fingerprint mismatch.
     */
    public function verifyWithFingerprint(string $token, string $fingerprint): ?array
    {
        $claims = $this->verify($token);
        if ($claims === null) {
            return null;
        }

        $fingerprint = trim(sanitize_text_field($fingerprint));
        if ($fingerprint === '' || !hash_equals($claims['fp'], $fingerprint)) {
            return null;
        }

        return $claims;
    }

    /**
     * Token signing secret. Uses WP auth salt for uniqueness per installation.
     */
    private function secret(): string
    {
        if (function_exists('wp_salt')) {
            $salt = (string) wp_salt('auth');
            if ($salt !== '') {
                return 'licenzo-activation-token:' . $salt;
            }
        }

        if (defined('AUTH_SALT') && AUTH_SALT !== '') {
            return 'licenzo-activation-token:' . (string) AUTH_SALT;
        }

        throw new \RuntimeException('No cryptographic salt available. Ensure wp-config.php defines AUTH_SALT.');
    }

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

    private function base64UrlDecode(string $value): string
    {
        $remainder = strlen($value) % 4;
        if ($remainder > 0) {
            $value .= str_repeat('=', 4 - $remainder);
        }

        return (string) base64_decode(strtr($value, '-_', '+/'));
    }
}
