<?php

namespace App\Services\License;

/**
 * Decrypts the embedded Envato Personal Access Token at runtime.
 *
 * The token itself lives in config/license.php as a base64 ciphertext
 * blob. This class is the only place that knows how to turn that blob
 * back into the raw PAT — used by EnvatoVerifier when it needs to
 * call api.envato.com.
 *
 * ─ Why we don't use Laravel's Crypt::decrypt ──────────────────────
 *   Crypt uses config('app.key'), which is generated freshly per
 *   install. If we encrypted with our APP_KEY and shipped the
 *   ciphertext, the customer's APP_KEY would never match — decryption
 *   would always fail. So we use a hardcoded internal key derived via
 *   sha256 from a constant string. This key is the same on every
 *   install of every customer, which lets the ciphertext from
 *   config/license.php round-trip everywhere.
 *
 * ─ Threat model ───────────────────────────────────────────────────
 *   This is security through obscurity. A motivated attacker who
 *   reads this file + config/license.php CAN extract the PAT. We
 *   accept that — the PAT only grants read-only access to the
 *   seller's Envato sales data, not anything destructive. The goal
 *   is to raise the bar above plaintext-in-source so casual
 *   inspection doesn't leak the token.
 */
class TokenVault
{
    /**
     * Hardcoded key derivation constant. Bumping the version suffix
     * (`v1` → `v2`) requires re-generating every embedded ciphertext
     * because old blobs won't decrypt under the new key. Do not change
     * casually.
     */
    private const KEY_MATERIAL = 'mailtrixy::license::vault::v1::do-not-change';

    /**
     * Decrypt the Envato PAT from config('license.envato_token_ciphertext').
     *
     * Throws a runtime exception (not the raw OpenSSL error) when the
     * decryption fails — callers want a single failure mode they can
     * report to the admin without leaking internal details.
     */
    public static function envatoToken(): string
    {
        $ciphertext = (string) config('license.envato_token_ciphertext', '');
        if ($ciphertext === '') {
            throw new \RuntimeException('License token is not configured. Reinstall the platform or contact support.');
        }

        return self::decrypt($ciphertext);
    }

    /**
     * Encrypt a fresh token. Only useful when the seller rotates their
     * PAT — run this once locally, paste the result into
     * config/license.php → envato_token_ciphertext. Exposed publicly
     * so a Tinker session can call it; never wired to a request.
     */
    public static function encrypt(string $plain): string
    {
        $key = self::deriveKey();
        $iv  = random_bytes(12);
        $tag = '';
        $ct  = openssl_encrypt($plain, 'aes-256-gcm', $key, OPENSSL_RAW_DATA, $iv, $tag);
        if ($ct === false) {
            throw new \RuntimeException('Token encryption failed.');
        }
        // Bundle = IV (12 bytes) || GCM tag (16 bytes) || ciphertext.
        // We pack all three so decrypt() doesn't need separate storage.
        return base64_encode($iv . $tag . $ct);
    }

    /**
     * Decrypt a bundle produced by encrypt().
     */
    public static function decrypt(string $bundle): string
    {
        $raw = base64_decode($bundle, true);
        if ($raw === false || strlen($raw) < 29) {
            // 12 (IV) + 16 (tag) + at least 1 byte ciphertext = 29 min.
            throw new \RuntimeException('License token ciphertext is malformed.');
        }
        $iv  = substr($raw, 0, 12);
        $tag = substr($raw, 12, 16);
        $ct  = substr($raw, 28);

        $key   = self::deriveKey();
        $plain = openssl_decrypt($ct, 'aes-256-gcm', $key, OPENSSL_RAW_DATA, $iv, $tag);
        if ($plain === false) {
            // Most common cause: key drift across versions, or the
            // config file was hand-edited. Either way the surface
            // failure is the same — we can't talk to Envato.
            throw new \RuntimeException('License token could not be decrypted. Please reinstall.');
        }
        return $plain;
    }

    /**
     * sha256 of KEY_MATERIAL gives us a 32-byte key, exactly what
     * AES-256 needs. Inline so callers can't accidentally use the
     * wrong key via a parameter override.
     */
    private static function deriveKey(): string
    {
        return hash('sha256', self::KEY_MATERIAL, true);
    }
}
