<?php
declare(strict_types=1);

namespace App\Security;

use RuntimeException;

final class SecretVault
{
    private string $path;
    private CryptoKeyring $keyring;

    public function __construct(?CryptoKeyring $keyring = null, ?string $path = null)
    {
        $config = require BASE_PATH . '/config/security_crypto.php';
        $this->keyring = $keyring ?? new CryptoKeyring($config);
        $this->path = $path ?? (string)$config['vault_path'];
    }

    public function get(string $name, ?string $default = null): ?string
    {
        $entries = $this->readAll();
        return array_key_exists($name, $entries) ? (string)$entries[$name] : $default;
    }

    public function require(string $name): string
    {
        $value = $this->get($name);
        if ($value === null || $value === '') throw new RuntimeException('Secret requis absent : ' . $name);
        return $value;
    }

    public function set(string $name, string $value): void
    {
        $this->assertName($name);
        $entries = $this->readAll();
        $entries[$name] = $value;
        $this->writeAll($entries);
    }

    public function delete(string $name): void
    {
        $entries = $this->readAll();
        unset($entries[$name]);
        $this->writeAll($entries);
    }

    /** @return list<string> */
    public function names(): array
    {
        $names = array_keys($this->readAll());
        sort($names, SORT_STRING);
        return $names;
    }

    public function rotate(): void
    {
        $this->writeAll($this->readAll());
    }

    /** @return array<string,string> */
    private function readAll(): array
    {
        if (!is_file($this->path)) return [];
        $document = json_decode((string)file_get_contents($this->path), true);
        if (!is_array($document) || ($document['format'] ?? '') !== 'AXIOM-VAULT-1') {
            throw new RuntimeException('Coffre de secrets invalide.');
        }
        $nonce = base64_decode((string)($document['nonce'] ?? ''), true);
        $cipher = base64_decode((string)($document['ciphertext'] ?? ''), true);
        if ($nonce === false || strlen($nonce) !== SODIUM_CRYPTO_SECRETBOX_NONCEBYTES || $cipher === false) {
            throw new RuntimeException('Coffre de secrets corrompu.');
        }
        foreach ($this->keyring->candidates('secret-vault') as $candidate) {
            $plain = sodium_crypto_secretbox_open($cipher, $nonce, $candidate['key']);
            if ($plain === false) continue;
            $entries = json_decode($plain, true);
            if (!is_array($entries)) throw new RuntimeException('Contenu du coffre invalide.');
            return array_map('strval', $entries);
        }
        throw new RuntimeException('Impossible de déchiffrer le coffre avec les clés autorisées.');
    }

    /** @param array<string,string> $entries */
    private function writeAll(array $entries): void
    {
        ksort($entries, SORT_STRING);
        $dir = dirname($this->path);
        if (!is_dir($dir) && !mkdir($dir, 0700, true) && !is_dir($dir)) {
            throw new RuntimeException('Impossible de créer le stockage privé du coffre.');
        }
        $nonce = random_bytes(SODIUM_CRYPTO_SECRETBOX_NONCEBYTES);
        $plain = json_encode($entries, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR);
        $cipher = sodium_crypto_secretbox($plain, $nonce, $this->keyring->current('secret-vault'));
        $document = [
            'format' => 'AXIOM-VAULT-1',
            'key_version' => $this->keyring->currentVersion(),
            'updated_at' => gmdate(DATE_ATOM),
            'nonce' => base64_encode($nonce),
            'ciphertext' => base64_encode($cipher),
        ];
        $tmp = $this->path . '.tmp.' . bin2hex(random_bytes(4));
        if (file_put_contents($tmp, json_encode($document, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES), LOCK_EX) === false) {
            throw new RuntimeException('Écriture du coffre impossible.');
        }
        @chmod($tmp, 0600);
        if (!rename($tmp, $this->path)) { @unlink($tmp); throw new RuntimeException('Activation du coffre impossible.'); }
        @chmod($this->path, 0600);
    }

    private function assertName(string $name): void
    {
        if (!preg_match('/^[A-Z][A-Z0-9_.-]{2,127}$/', $name)) {
            throw new RuntimeException('Nom de secret invalide. Utilisez des majuscules, chiffres, points, tirets ou underscores.');
        }
    }
}
