<?php

declare(strict_types=1);

namespace PpPhp\Core\Licensing;

/**
 * One string a customer can paste.
 *
 * Asking someone to enter a licence key, then a server URL, then a public key is
 * three chances to get it wrong. An activation code carries all three:
 *
 *     PPK1.<base64url({"u":serverUrl,"k":publicKeyB64,"l":licenceKey})>
 *
 * The public key inside is not taken on faith — LicenseClient checks it against
 * the key stamped into the source (LicenseTrust), so a code pointing at someone
 * else's "licence server" is refused on a released build.
 */
final class ActivationCode
{
    public const PREFIX = 'PPK1';

    /**
     * Canonical form of a licence key.
     *
     * Deliberately defined HERE rather than on LicenseServer: a client bundle
     * ships no server code, and a customer pasting their activation code must
     * not depend on a class that only exists on the vendor's install.
     */
    public static function normaliseKey(string $key): string
    {
        return strtoupper(trim($key));
    }

    /**
     * @param array<int,string> $mirrors backup addresses tried if $serverUrl is down
     */
    public static function encode(string $serverUrl, string $publicKeyB64, string $licenseKey, array $mirrors = []): string
    {
        $m = [];
        foreach ($mirrors as $u) {
            $u = rtrim(trim((string) $u), '/');
            if ($u !== '' && $u !== rtrim(trim($serverUrl), '/') && !in_array($u, $m, true)) $m[] = $u;
        }
        $payload = [
            'u' => rtrim(trim($serverUrl), '/'),
            'k' => trim($publicKeyB64),
            'l' => self::normaliseKey($licenseKey),
        ];
        // Only carried when there is something to carry — keeps the string the
        // customer pastes as short as it can be.
        if ($m !== []) $payload['m'] = $m;

        return self::PREFIX . '.' . LicenseProtocol::b64u((string) json_encode($payload, JSON_UNESCAPED_SLASHES));
    }

    /**
     * @return array{url:string,publicKey:string,licenseKey:string,endpoints:array<int,string>}|null
     */
    public static function decode(string $code): ?array
    {
        $code = trim(preg_replace('/\s+/', '', $code) ?? '');
        $parts = explode('.', $code, 2);
        if (count($parts) !== 2 || $parts[0] !== self::PREFIX) return null;

        $data = json_decode(LicenseProtocol::unb64u($parts[1]), true);
        if (!is_array($data)) return null;

        $url = rtrim((string) ($data['u'] ?? ''), '/');
        $key = (string) ($data['k'] ?? '');
        $lic = (string) ($data['l'] ?? '');
        if ($url === '' || $key === '' || $lic === '') return null;

        // Only http(s), and only an absolute URL — the client will POST to this.
        $scheme = strtolower((string) parse_url($url, PHP_URL_SCHEME));
        if (!in_array($scheme, ['http', 'https'], true) || parse_url($url, PHP_URL_HOST) === null) {
            return null;
        }
        $raw = base64_decode($key, true);
        if (!is_string($raw) || strlen($raw) !== SODIUM_CRYPTO_SIGN_PUBLICKEYBYTES) return null;

        // Mirrors get the same scheme/host validation as the primary — an
        // activation code is untrusted input until every address in it checks out.
        $endpoints = [$url];
        foreach ((array) ($data['m'] ?? []) as $u) {
            $u = rtrim(trim((string) $u), '/');
            if ($u === '' || in_array($u, $endpoints, true)) continue;
            $s = strtolower((string) parse_url($u, PHP_URL_SCHEME));
            if (!in_array($s, ['http', 'https'], true) || parse_url($u, PHP_URL_HOST) === null) continue;
            $endpoints[] = $u;
            if (count($endpoints) >= 4) break;
        }

        return ['url' => $url, 'publicKey' => $key,
                'licenseKey' => self::normaliseKey($lic), 'endpoints' => $endpoints];
    }
}
