<?php

namespace WPSN\Modules\Licensing;

use WPSN\Core\Settings;

defined('ABSPATH') || exit;

/**
 * Signature verification for licensing / billing / update payloads.
 *
 * Supported algorithms:
 * - RS256 (recommended): RSA signature over canonical JSON
 * - HS256 (dev-only): HMAC over canonical JSON
 *
 * Public key resolution order for RS256:
 * 1) kid-specific filter
 * 2) licensing.public_key_pem setting (single PEM or JSON map keyed by kid)
 * 3) constants / legacy filters
 */
final class LicenseSignatureVerifier
{
    private ?Settings $settings;

    public function __construct(?Settings $settings = null)
    {
        $this->settings = $settings;
    }

    /**
     * @param array<string,mixed> $payload
     */
    public function verify(array $payload, string $signatureB64, string $alg = '', string $kid = ''): bool
    {
        $signatureB64 = trim($signatureB64);
        if ($signatureB64 === '') {
            return false;
        }

        $alg = strtolower(trim($alg));
        if ($alg === '') {
            $alg = 'hs256';
        }

        $kid = trim($kid);

        /**
         * Allow custom verification (RSA, Ed25519, JWT, etc.).
         *
         * Filter signature:
         *  wpsn/license/verify_signature(?bool $result, array $payload, string $signatureB64, string $alg, string $kid)
         */
        $pre = apply_filters('wpsn/license/verify_signature', null, $payload, $signatureB64, $alg, $kid);
        if (is_bool($pre)) {
            return $pre;
        }

        $json = $this->canonicalJson($payload);
        if ($json === '') {
            return false;
        }

        $sigRaw = base64_decode($signatureB64, true);
        if (!is_string($sigRaw) || $sigRaw === '') {
            return false;
        }

        if ($alg === 'rs256') {
            if (!function_exists('openssl_verify')) {
                return false;
            }

            $pub = $this->getRs256PublicKeyPem($kid);
            if ($pub === '') {
                return false;
            }

            $key = openssl_pkey_get_public($pub);
            if (!$key) {
                return false;
            }

            $ok = openssl_verify($json, $sigRaw, $key, OPENSSL_ALGO_SHA256);
            return $ok === 1;
        }

        if ($alg === 'hs256') {
            $secret = $this->getHs256Secret();
            if ($secret === '') {
                return false;
            }

            $expected = hash_hmac('sha256', $json, $secret, true);
            if (function_exists('hash_equals')) {
                return hash_equals($expected, $sigRaw);
            }

            return $expected === $sigRaw;
        }

        return false;
    }

    public function hasPublicKey(string $kid = ''): bool
    {
        return $this->getRs256PublicKeyPem($kid) !== '';
    }

    public function hasHs256Secret(): bool
    {
        return $this->getHs256Secret() !== '';
    }

    /**
     * Canonical JSON for signing/verifying.
     */
    private function canonicalJson(array $payload): string
    {
        $canon = $this->canonicalize($payload);
        $json = wp_json_encode($canon);
        return is_string($json) ? $json : '';
    }

    /**
     * Public key PEM for RS256 verification.
     */
    private function getRs256PublicKeyPem(string $kid = ''): string
    {
        $kid = trim($kid);

        $pem = apply_filters('wpsn/license/public_key_pem_by_kid', '', $kid);
        if (is_string($pem) && trim($pem) !== '') {
            return $this->normalizePem($pem);
        }

        $pem = $this->pemFromSettings($kid);
        if ($pem !== '') {
            return $pem;
        }

        $pem = defined('WPSN_LICENSE_PUBLIC_KEY_PEM') ? (string) WPSN_LICENSE_PUBLIC_KEY_PEM : '';
        $pem = $this->normalizePem($pem);
        if ($pem !== '') {
            return $pem;
        }

        $pem = (string) apply_filters('wpsn/license/public_key_pem', '', $kid);
        $pem = $this->normalizePem($pem);
        if ($pem !== '') {
            return $pem;
        }

        // Back-compat (dev): older constant/filter name.
        $pem = defined('WPSN_LICENSE_PUBLIC_KEY') ? (string) WPSN_LICENSE_PUBLIC_KEY : '';
        $pem = $this->normalizePem($pem);
        if ($pem !== '') {
            return $pem;
        }

        $pem = (string) apply_filters('wpsn/license/public_key', '', $kid);
        return $this->normalizePem($pem);
    }

    /**
     * Secret for HS256 (dev-only).
     */
    private function getHs256Secret(): string
    {
        $secret = defined('WPSN_LICENSE_HS256_SECRET') ? (string) WPSN_LICENSE_HS256_SECRET : '';
        $secret = trim($secret);
        if ($secret !== '') {
            return $secret;
        }

        // Back-compat: WPSN_LICENSE_PUBLIC_KEY was sometimes reused as HS256 secret.
        $secret = defined('WPSN_LICENSE_PUBLIC_KEY') ? (string) WPSN_LICENSE_PUBLIC_KEY : '';
        $secret = trim($secret);
        if ($secret !== '') {
            return $secret;
        }

        $secret = (string) apply_filters('wpsn/license/hs256_secret', '');
        $secret = trim($secret);
        if ($secret !== '') {
            return $secret;
        }

        $secret = (string) apply_filters('wpsn/license/public_key', '');
        return trim($secret);
    }

    private function pemFromSettings(string $kid = ''): string
    {
        if (!$this->settings) {
            return '';
        }

        $raw = (string) $this->settings->get('licensing.public_key_pem', '');
        $raw = trim($raw);
        if ($raw === '') {
            return '';
        }

        if ($kid !== '' && (strpos($raw, '{') === 0 || strpos($raw, '[') === 0)) {
            $decoded = json_decode($raw, true);
            if (is_array($decoded)) {
                foreach ([$kid, 'default', '_default'] as $key) {
                    if (!isset($decoded[$key]) || !is_scalar($decoded[$key])) {
                        continue;
                    }
                    $pem = $this->normalizePem((string) $decoded[$key]);
                    if ($pem !== '') {
                        return $pem;
                    }
                }
            }
        }

        return $this->normalizePem($raw);
    }

    private function normalizePem(string $pem): string
    {
        $pem = trim($pem);
        if ($pem === '') {
            return '';
        }

        if (strpos($pem, '\\n') !== false && strpos($pem, "\n") === false) {
            $pem = str_replace('\\n', "\n", $pem);
        }

        $pem = str_replace(["\r\n", "\r"], "\n", $pem);
        return trim($pem);
    }

    /**
     * @param mixed $v
     * @return mixed
     */
    private function canonicalize($v)
    {
        if (is_array($v)) {
            if ($this->isAssoc($v)) {
                ksort($v);
            }
            foreach ($v as $k => $vv) {
                $v[$k] = $this->canonicalize($vv);
            }
        }

        return $v;
    }

    /**
     * @param array<mixed> $arr
     */
    private function isAssoc(array $arr): bool
    {
        $keys = array_keys($arr);
        return array_keys($keys) !== $keys;
    }
}
