<?php
declare(strict_types=1);

/**
 * DecryptionService — SCLS Audit Entry Decryption
 *
 * Decrypts AES-256-CBC encrypted audit statements using per-node keys.
 * Keys are loaded from local vault files with in-memory caching.
 *
 * Encryption format:
 *   encrypted_statement = base64( AES-256-CBC( gzencode( json ) ) )
 *   encryption_iv       = hex( 16-byte IV )
 *   hmac_signature      = HMAC-SHA256( base64_ciphertext, node_key )
 */

if (!defined('DASHBOARD_ROOT')) {
    http_response_code(403);
    exit('Direct access denied.');
}

class DecryptionService
{
    private PDO $engineDb;

    /** @var array<string, array{key: string, expires: int}> In-memory key cache */
    private array $keyCache = [];

    private const KEY_CACHE_TTL    = 300;    // 5 minutes
    private const KEY_DIR          = '/opt/sysc/engine/vault/node_keys';
    private const CIPHER           = 'AES-256-CBC';
    private const EXPECTED_KEY_LEN = 64;     // 32 bytes hex-encoded

    public function __construct(PDO $engineDb)
    {
        $this->engineDb = $engineDb;
    }

    /**
     * Decrypt an audit entry row.
     *
     * @param  array $auditEntry  Row from sec_audit_log
     * @return array              Decrypted payload + metadata
     * @throws \RuntimeException  On any crypto or validation failure
     */
    public function decrypt(array $auditEntry): array
    {
        $required = ['node_id', 'encrypted_statement', 'encryption_iv', 'hmac_signature'];
        foreach ($required as $field) {
            if (empty($auditEntry[$field])) {
                throw new \RuntimeException("Missing required field: {$field}");
            }
        }

        $nodeId       = $auditEntry['node_id'];
        $encryptedB64 = $auditEntry['encrypted_statement'];
        $ivHex        = $auditEntry['encryption_iv'];
        $storedHmac   = $auditEntry['hmac_signature'];
        $version      = (int)($auditEntry['encryption_version'] ?? 1);

        // 1. Fetch node key
        $keyHex = $this->fetchVaultKey($nodeId);

        // 2. Verify HMAC integrity before decryption
        $computedHmac = hash_hmac('sha256', $encryptedB64, hex2bin($keyHex));
        if (!hash_equals($storedHmac, $computedHmac)) {
            throw new \RuntimeException('HMAC verification failed — data may be tampered');
        }

        // 3. Decode ciphertext
        $encrypted = base64_decode($encryptedB64, true);
        if ($encrypted === false) {
            throw new \RuntimeException('Base64 decode failed for encrypted_statement');
        }

        // 4. Decode IV
        $iv = hex2bin($ivHex);
        if ($iv === false || strlen($iv) !== 16) {
            throw new \RuntimeException('Invalid encryption IV (expected 16 bytes hex)');
        }

        // 5. Decode key
        $key = hex2bin($keyHex);
        if ($key === false || strlen($key) !== 32) {
            throw new \RuntimeException('Invalid node key length');
        }

        // 6. Decrypt
        $decrypted = openssl_decrypt($encrypted, self::CIPHER, $key, OPENSSL_RAW_DATA, $iv);
        if ($decrypted === false) {
            throw new \RuntimeException('AES-256-CBC decryption failed');
        }

        // 7. Decompress (gzdecode; fall back to raw if not compressed)
        $json = @gzdecode($decrypted);
        if ($json === false) {
            $json = $decrypted;
        }

        // 8. Parse JSON
        $statement = json_decode($json, true);
        if (json_last_error() !== JSON_ERROR_NONE) {
            // Return raw text if not valid JSON
            $statement = ['raw_text' => $json];
        }

        return [
            'decrypted'    => true,
            'statement'    => $statement,
            'node_id'      => $nodeId,
            'event_uuid'   => $auditEntry['event_uuid'] ?? null,
            'tool_id'      => $auditEntry['tool_id'] ?? null,
            'tool_name'    => $auditEntry['tool_name'] ?? null,
            'enc_version'  => $version,
            'decrypted_at' => date('c'),
        ];
    }

    /**
     * Fetch a node's vault key from local file, with TTL cache.
     *
     * @param  string $nodeId  6-digit node ID
     * @return string          64-char hex key
     * @throws \RuntimeException
     */
    public function fetchVaultKey(string $nodeId): string
    {
        // Validate node ID format (prevent directory traversal)
        if (!preg_match('/^\d{6}$/', $nodeId)) {
            throw new \RuntimeException('Invalid node ID format');
        }

        // Check cache
        if (isset($this->keyCache[$nodeId])) {
            $cached = $this->keyCache[$nodeId];
            if ($cached['expires'] > time()) {
                return $cached['key'];
            }
            unset($this->keyCache[$nodeId]);
        }

        // Primary: local key file
        $keyFile = self::KEY_DIR . '/' . $nodeId . '.key';
        if (!file_exists($keyFile)) {
            throw new \RuntimeException("Node key file not found: {$nodeId}");
        }
        if (!is_readable($keyFile)) {
            throw new \RuntimeException("Node key file not readable: {$nodeId}");
        }

        $keyHex = trim(file_get_contents($keyFile));

        // Validate key format
        if (strlen($keyHex) !== self::EXPECTED_KEY_LEN || !ctype_xdigit($keyHex)) {
            throw new \RuntimeException("Invalid key format for node {$nodeId}");
        }

        // Cache it
        $this->keyCache[$nodeId] = [
            'key'     => $keyHex,
            'expires' => time() + self::KEY_CACHE_TTL,
        ];

        return $keyHex;
    }

    /**
     * Clear the in-memory key cache.
     */
    public function clearCache(): void
    {
        $this->keyCache = [];
    }
}
