/**
 * Settings repo — read/write secrets and plain values through a single API.
 *
 * Secret values are AES-256-GCM encrypted at rest. They're stored in the same
 * `appSettings` table as plain values, but wrapped in `{ _enc: "base64..." }`
 * so the JSON column itself never contains raw keys.
 *
 * Falls back to env-vars if `envFallback` is declared on the key and the DB
 * value is empty — useful for CI/local-dev where you may not want to write
 * keys into the SQLite file.
 */

import { getAppSetting, setAppSetting } from '@/lib/db/repositories';
import { encrypt, decrypt, EncryptionError } from './crypto';
import { getEnvFallback, isSecretKey, SETTINGS_CATALOGUE } from './keys';

interface EncryptedEnvelope {
  _enc: string;
}

function isEnvelope(v: unknown): v is EncryptedEnvelope {
  return Boolean(
    v && typeof v === 'object' && '_enc' in v && typeof (v as EncryptedEnvelope)._enc === 'string',
  );
}

/**
 * Read a secret. Decrypts on the fly. Falls back to env-var if the catalogue
 * declares `envFallback` and the DB value is missing.
 *
 * Returns `undefined` if neither DB nor env has the value. Throws
 * `EncryptionError('integrity')` if the stored value can't be decrypted —
 * callers should surface this as a Settings UI alert rather than swallowing
 * it, because silent fallback to env would mask a key-rotation accident.
 */
export function getSecret(key: string): string | undefined {
  const raw = getAppSetting<unknown>(key);
  if (isEnvelope(raw)) {
    try {
      return decrypt(raw._enc);
    } catch (err) {
      if (err instanceof EncryptionError && err.kind === 'integrity') throw err;
      // Other crypto errors (config, format) → fall back to env so the app
      // remains usable in dev when APP_ENCRYPTION_KEY isn't set yet.
    }
  } else if (typeof raw === 'string' && raw.length > 0) {
    // Tolerate legacy plain-text secrets — should not happen in fresh
    // installs, but during migrations we may see them.
    return raw;
  }
  return getEnvFallback(key);
}

/**
 * Write a secret. Encrypts before storing. Passing an empty string deletes
 * the DB row (caller will then see only the env fallback, if any).
 */
export function setSecret(key: string, value: string): void {
  if (!isSecretKey(key)) {
    throw new Error(
      `setSecret called with non-secret key "${key}" — declare it in SETTINGS_CATALOGUE with kind: "secret" first.`,
    );
  }
  if (value.length === 0) {
    setAppSetting(key, null);
    return;
  }
  setAppSetting(key, { _enc: encrypt(value) });
}

/**
 * Read a plain (non-encrypted) setting. Thin wrapper over `getAppSetting`
 * that exists for symmetry with `getSecret` and to discourage callers from
 * reaching into the lib/db layer.
 */
export function getPlain<T = unknown>(key: string): T | undefined {
  return getAppSetting<T>(key);
}

export function setPlain(key: string, value: unknown): void {
  if (isSecretKey(key)) {
    throw new Error(
      `setPlain called with secret key "${key}" — use setSecret() to encrypt before storing.`,
    );
  }
  setAppSetting(key, value);
}

/**
 * Convenience: try DB first, then env, then default. Used at adapter init
 * where callers want a single string and don't care about the source.
 */
export function getStringOrDefault(key: string, defaultValue: string): string {
  const fromDb = isSecretKey(key) ? getSecret(key) : getPlain<string>(key);
  if (typeof fromDb === 'string' && fromDb.length > 0) return fromDb;
  return defaultValue;
}

/** Useful for the Settings UI: which keys exist and which kind are they. */
export function listCatalogueKeys(): Array<{
  key: string;
  kind: 'plain' | 'secret';
  description?: string;
  hasValue: boolean;
}> {
  return Object.values(SETTINGS_CATALOGUE).map((def) => {
    const hasValue =
      def.kind === 'secret' ? getSecret(def.key) !== undefined : getPlain(def.key) !== undefined;
    return { key: def.key, kind: def.kind, description: def.description, hasValue };
  });
}
