/**
 * AUDIT-4 (2026-05-23) — at-rest encryption for sensitive
 * SystemSetting values.
 *
 * `SystemSetting` is a generic key/value table. Operator-set secrets
 * (the canonical example is `smtp_password`) live there in plain text,
 * which means a DB leak hands them to the attacker. This module
 * provides the canonical list of "secret keys" plus thin wrappers
 * around the secret-vault for write and read paths.
 *
 * Lazy-upgrade pattern: read uses decryptIfNeeded to transparently
 * handle legacy plaintext rows. Write always encrypts the value when
 * the key is in SECRET_KEYS. Existing rows upgrade on next save.
 *
 * To rotate the at-rest list (e.g. add a new operator-managed secret
 * later), add the key here. No schema migration needed.
 */
import { encrypt, decryptIfNeeded } from '@/lib/crypto/secret-vault';

/** Keys whose stored SystemSetting.value is treated as a secret. */
export const SECRET_SETTING_KEYS: ReadonlySet<string> = new Set([
    'smtp_password',
    // Webhook/bot credentials previously returned in cleartext by GET
    // /api/settings to any session (incl. VIEWER) and stored plaintext at
    // rest. Now masked on the wire + encrypted at rest (lazy-upgrade on next
    // save). All read paths route through decryptIfNeeded/decryptIfSecret.
    'telegram_bot_token',
    'slack_webhook_url',
    'discord_webhook_url',
    // Future: anywhere the operator configures a credential through
    // SystemSetting (OpenAI key, etc.) belongs in this set. Keep the list
    // explicit; do not auto-detect by name pattern — too easy for a future
    // contributor to add a secret without realising it'll be plaintext at rest.
]);

/** Token returned by the settings API in place of a secret value. */
export const MASKED_SECRET_PLACEHOLDER = '__sentinel_secret_unchanged__';

export function isSecretKey(key: string): boolean {
    return SECRET_SETTING_KEYS.has(key);
}

/**
 * If the key is a secret, encrypt the value before writing. No-op
 * for non-secret keys (returned unchanged).
 */
export function encryptIfSecret(key: string, value: string): string {
    if (!isSecretKey(key)) return value;
    if (!value) return value; // empty stays empty
    return encrypt(value);
}

/**
 * If the key is a secret, decrypt the stored value before returning.
 * Falls back gracefully for legacy plaintext rows (decryptIfNeeded
 * checks the envelope prefix). Returns value as-is for non-secret keys.
 */
export function decryptIfSecret(key: string, value: string): string {
    if (!isSecretKey(key)) return value;
    return decryptIfNeeded(value);
}

/**
 * Public-API masking — when the settings GET endpoint returns the
 * map to the client, secret keys are replaced with the placeholder
 * so the actual value never crosses the wire. The frontend treats
 * the placeholder as "leave the saved value alone unless the user
 * typed something new"; the POST endpoint short-circuits when the
 * incoming value equals the placeholder.
 */
export function maskIfSecret(key: string, value: string): string {
    if (!isSecretKey(key)) return value;
    if (!value) return value;
    return MASKED_SECRET_PLACEHOLDER;
}
