<?php

namespace App\Services\Wireguard;

use RuntimeException;
use SensitiveParameter;

/**
 * Encrypts a stored WireGuard config at rest.
 *
 * Deliberately NOT Laravel's Crypt: that uses APP_KEY, which every part of the
 * app already holds and which ends up in more places than it should. VPN
 * configs are credentials for the management network, so they get their own
 * key (VPN_CONFIG_KEY) that can be rotated without invalidating sessions,
 * signed URLs and everything else APP_KEY protects.
 *
 * The ciphertext carries a version prefix so a future key rotation can tell
 * old records apart instead of guessing.
 *
 * What this does NOT protect against: someone who has both the database and
 * the .env. There is no way around that on a single box — it buys protection
 * against a database-only leak (a dump, a backup, a stray replica).
 */
final class ConfigVault
{
    private const VERSION = 'v1';

    public static function available(): bool
    {
        return self::rawKey() !== null;
    }

    public static function encrypt(#[SensitiveParameter] string $plaintext): string
    {
        $key = self::rawKey() ?? throw new RuntimeException(
            'VPN_CONFIG_KEY is not set — refusing to store a VPN config in plaintext.'
        );

        $nonce = random_bytes(SODIUM_CRYPTO_SECRETBOX_NONCEBYTES);

        return self::VERSION.':'.base64_encode($nonce.sodium_crypto_secretbox($plaintext, $nonce, $key));
    }

    public static function decrypt(string $ciphertext): ?string
    {
        $key = self::rawKey();
        if ($key === null || ! str_starts_with($ciphertext, self::VERSION.':')) {
            return null;
        }

        $raw = base64_decode(substr($ciphertext, strlen(self::VERSION) + 1), true);
        if ($raw === false || strlen($raw) <= SODIUM_CRYPTO_SECRETBOX_NONCEBYTES) {
            return null;
        }

        $plaintext = sodium_crypto_secretbox_open(
            substr($raw, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES),
            substr($raw, 0, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES),
            $key,
        );

        // False means the key is wrong or the record was tampered with. Both are
        // "you cannot have this", not an exception to swallow somewhere upstream.
        return $plaintext === false ? null : $plaintext;
    }

    /** @return non-empty-string|null */
    private static function rawKey(): ?string
    {
        $configured = (string) config('admin_access.vpn_config_key', '');
        if ($configured === '') {
            return null;
        }

        $key = base64_decode(str_starts_with($configured, 'base64:') ? substr($configured, 7) : $configured, true);

        return $key !== false && strlen($key) === SODIUM_CRYPTO_SECRETBOX_KEYBYTES ? $key : null;
    }
}
