<?php
/**
 * Response Signer
 *
 * Signs REST API responses to prevent tampering and replay attacks.
 *
 * @package Jeremias\WcLicensedProduct\Api
 */

declare(strict_types=1);

namespace Jeremias\WcLicensedProduct\Api;

/**
 * Signs license API responses using HMAC-SHA256
 *
 * The security model:
 * 1. Server generates a unique signature for each response using HMAC-SHA256
 * 2. Signature includes a timestamp to prevent replay attacks
 * 3. Client verifies the signature using a shared secret
 * 4. Invalid signatures cause the client to reject the response
 */
final class ResponseSigner
{
    private string $serverSecret;

    public function __construct()
    {
        $this->serverSecret = defined('WC_LICENSE_SERVER_SECRET')
            ? WC_LICENSE_SERVER_SECRET
            : '';
    }

    /**
     * Register WordPress hooks
     */
    public function register(): void
    {
        add_filter('rest_post_dispatch', [$this, 'signResponse'], 10, 3);
    }

    /**
     * Sign REST API response
     *
     * @param \WP_REST_Response $response The response object
     * @param \WP_REST_Server   $server   The REST server
     * @param \WP_REST_Request  $request  The request object
     * @return \WP_REST_Response
     */
    public function signResponse($response, $server, $request)
    {
        // Only sign license API responses
        if (!$this->shouldSign($request)) {
            return $response;
        }

        $data = $response->get_data();
        $licenseKey = $request->get_param('license_key');

        if (empty($licenseKey) || !is_array($data) || empty($this->serverSecret)) {
            return $response;
        }

        $headers = $this->createSignatureHeaders($data, $licenseKey);

        foreach ($headers as $name => $value) {
            $response->header($name, $value);
        }

        return $response;
    }

    /**
     * Check if request should be signed
     */
    private function shouldSign(\WP_REST_Request $request): bool
    {
        $route = $request->get_route();

        return str_starts_with($route, '/wc-licensed-product/v1/validate')
            || str_starts_with($route, '/wc-licensed-product/v1/status')
            || str_starts_with($route, '/wc-licensed-product/v1/activate');
    }

    /**
     * Create signature headers for response
     *
     * @param array  $data       The response data
     * @param string $licenseKey The license key from the request
     * @return array Associative array of headers
     */
    private function createSignatureHeaders(array $data, string $licenseKey): array
    {
        $timestamp = time();
        $signingKey = $this->deriveKey($licenseKey);

        // Recursively sort keys for consistent ordering (required by client implementation)
        $data = $this->recursiveKeySort($data);

        // Build signature payload
        $payload = $timestamp . ':' . json_encode(
            $data,
            JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE
        );

        return [
            'X-License-Signature' => hash_hmac('sha256', $payload, $signingKey),
            'X-License-Timestamp' => (string) $timestamp,
        ];
    }

    /**
     * Recursively sort array keys alphabetically
     *
     * @param mixed $data The data to sort
     * @return mixed The sorted data
     */
    private function recursiveKeySort(mixed $data): mixed
    {
        if (!is_array($data)) {
            return $data;
        }

        // Check if array is associative (has string keys)
        $isAssociative = array_keys($data) !== range(0, count($data) - 1);

        if ($isAssociative) {
            ksort($data);
        }

        // Recursively sort nested arrays
        foreach ($data as $key => $value) {
            $data[$key] = $this->recursiveKeySort($value);
        }

        return $data;
    }

    /**
     * Derive a unique signing key for a license
     *
     * Uses HKDF-like key derivation to create a unique signing key
     * for each license key, preventing cross-license signature attacks.
     *
     * @param string $licenseKey The license key
     * @return string The derived signing key (hex encoded)
     */
    private function deriveKey(string $licenseKey): string
    {
        return self::deriveCustomerSecret($licenseKey, $this->serverSecret);
    }

    /**
     * Derive a customer-specific secret from a license key
     *
     * This secret is unique per license and can be shared with the customer
     * to verify signed API responses. Each customer gets their own secret
     * derived from their license key.
     *
     * @param string $licenseKey   The customer's license key
     * @param string $serverSecret The server's master secret
     * @return string The derived secret (64 hex characters)
     */
    public static function deriveCustomerSecret(string $licenseKey, string $serverSecret): string
    {
        // HKDF-like key derivation
        $prk = hash_hmac('sha256', $licenseKey, $serverSecret, true);

        return hash_hmac('sha256', $prk . "\x01", $serverSecret);
    }

    /**
     * Get the customer secret for a license key using the configured server secret
     *
     * @param string $licenseKey The customer's license key
     * @return string|null The derived secret, or null if server secret is not configured
     */
    public static function getCustomerSecretForLicense(string $licenseKey): ?string
    {
        $serverSecret = defined('WC_LICENSE_SERVER_SECRET') ? WC_LICENSE_SERVER_SECRET : '';

        if (empty($serverSecret)) {
            return null;
        }

        return self::deriveCustomerSecret($licenseKey, $serverSecret);
    }

    /**
     * Check if response signing is enabled
     *
     * @return bool True if server secret is configured
     */
    public static function isSigningEnabled(): bool
    {
        return defined('WC_LICENSE_SERVER_SECRET') && !empty(WC_LICENSE_SERVER_SECRET);
    }
}
