/**
 * Settings API routes — Phase 7 of the secrets refactor.
 *
 * Exposes the non-secret half of the encrypted store: registry-classified
 * `*-setting` keys (model.id, model.provider, r2.bucket, search.google.cx,
 * etc.). Secret keys are NOT served here — they live behind `/api/secrets`,
 * which never returns plaintext values.
 *
 * Owner-only via the existing `isWorkspaceOwner()` helper. Matches the
 * authorization matrix in plan §3: workspace settings are
 * workspace-owner-only; members get 403.
 *
 * The routes register unconditionally now that Phase 6 has shipped.
 * Historically they were gated on `STUDIOGRAPH_NEW_STORE === '1'`; that
 * flag was removed once the encrypted store became the unconditional
 * source of truth. Handlers still return 503 if the store singleton
 * isn't initialized for some reason (test harness, early boot).
 *
 * Every mutation goes through the underlying SettingsStore, which audits
 * `set`/`delete` automatically. Audit reads land via the `/api/audit`
 * endpoint in audit-api.ts.
 *
 * After the scope-collapse refactor, requests carry no `scope` /
 * `scope_id` — Studiograph is single-workspace-per-process. Any legacy
 * `scope` query param or body field is silently ignored.
 */
import { isWorkspaceOwner } from '../../services/auth-service.js';
import { getAllConsoleUrls, getKeyDef, getSecretStore, hasSecretStore, listWorkspaceSettingsSchema, notifySecretMutation, } from '../../core/secrets/index.js';
/**
 * Owner-only gate. Mirrors `requireAdmin()` in config-api.ts but is
 * stricter: API-key callers without a `req.user` are accepted (matches
 * existing pattern — Bearer token = admin-equivalent), so the only
 * rejection path is "logged-in non-owner user".
 */
function requireOwner(req, authService) {
    if (!authService.hasUsers())
        return true;
    const user = req.user;
    if (!user)
        return true; // API key — admin-equivalent
    return isWorkspaceOwner(user.role);
}
function actorFromReq(req) {
    const user = req.user;
    const ip = req.headers['x-forwarded-for']?.split(',')[0]?.trim()
        || req.ip;
    const userAgent = req.headers['user-agent'];
    if (user) {
        return {
            type: 'user',
            userId: String(user.id),
            ip,
            userAgent,
        };
    }
    return {
        type: 'system',
        systemId: 'api-token',
        ip,
        userAgent,
    };
}
export async function registerSettingsApiRoutes(fastify, authService) {
    // ── GET /api/settings ────────────────────────────────────────────────────
    // List current setting values. Returns the parsed value as JSON (plain
    // text — settings are non-secret by classification).
    fastify.get('/api/settings', async (req, reply) => {
        if (!requireOwner(req, authService)) {
            return reply.status(403).send({ error: 'Workspace owner access required' });
        }
        if (!hasSecretStore()) {
            return reply.status(503).send({ error: 'Settings store not initialized' });
        }
        const store = getSecretStore();
        const rows = store.list();
        // Hydrate JSON values + decorate with registry metadata so the UI
        // knows what redaction mode applies. Phase 12d (2026-05-12) removed
        // the `source`/`env_alias` fields — env-shadow no longer exists for
        // workspace-* keys (resolver is store-only), so they were dead weight.
        const items = rows.map((r) => {
            const def = getKeyDef(r.key);
            let value;
            try {
                value = JSON.parse(r.valueJson);
            }
            catch {
                value = r.valueJson;
            }
            return {
                key: r.key,
                value,
                classification: def?.classification,
                redact: def?.redact,
                updated_at: r.updatedAt,
                updated_by: r.updatedBy,
            };
        });
        return reply.send({ items });
    });
    // ── GET /api/settings/schema ─────────────────────────────────────────────
    // Static schema for every registered workspace-setting. Drives dropdowns
    // and other constraint-aware inputs in the settings panels — single
    // source of truth, so adding a provider to the registry's zod enum
    // automatically updates the UI without a second edit.
    //
    // Separate from GET /api/settings (which returns *stored values*) because
    // the schema is registry-shape, not store-shape: it lists every key the
    // user CAN set, including ones with no row in the store yet.
    fastify.get('/api/settings/schema', async (req, reply) => {
        if (!requireOwner(req, authService)) {
            return reply.status(403).send({ error: 'Workspace owner access required' });
        }
        return reply.send({
            items: listWorkspaceSettingsSchema(),
            consoleUrls: getAllConsoleUrls(),
        });
    });
    // ── PUT /api/settings/:key ───────────────────────────────────────────────
    // Write a setting. Registry-validated: the key must be registered AND
    // classified as a workspace-setting. Secret keys reject with 400 —
    // they must go through /api/secrets.
    fastify.put('/api/settings/:key', async (req, reply) => {
        if (!requireOwner(req, authService)) {
            return reply.status(403).send({ error: 'Workspace owner access required' });
        }
        if (!hasSecretStore()) {
            return reply.status(503).send({ error: 'Settings store not initialized' });
        }
        const { key } = req.params;
        const { value } = req.body ?? {};
        if (value === undefined) {
            return reply.status(400).send({ error: 'body.value is required' });
        }
        const def = getKeyDef(key);
        if (!def) {
            return reply.status(404).send({ error: `Unknown key "${key}"` });
        }
        if (def.classification.endsWith('-secret')) {
            return reply.status(400).send({
                error: `Key "${key}" is classified as a secret — use PUT /api/secrets/${encodeURIComponent(key)}`,
            });
        }
        if (def.classification === 'bootstrap') {
            return reply.status(400).send({
                error: `Key "${key}" is bootstrap-class — set via environment variable, not API`,
            });
        }
        // Registry validator catches malformed shapes (e.g. invalid enum for
        // model.provider). Surfaces a 400 with the zod message.
        const parsed = def.validate.safeParse(value);
        if (!parsed.success) {
            return reply.status(400).send({
                error: 'Validation failed',
                details: parsed.error.issues.map((i) => i.message),
            });
        }
        const store = getSecretStore();
        const meta = await store.set({ key, value: parsed.data }, actorFromReq(req));
        notifySecretMutation(key);
        return reply.send({ ok: true, meta });
    });
    // ── DELETE /api/settings/:key ────────────────────────────────────────────
    // Remove a setting. Audit row written. Returns { removed: bool } so
    // the UI can show "already absent" without an error path.
    fastify.delete('/api/settings/:key', async (req, reply) => {
        if (!requireOwner(req, authService)) {
            return reply.status(403).send({ error: 'Workspace owner access required' });
        }
        if (!hasSecretStore()) {
            return reply.status(503).send({ error: 'Settings store not initialized' });
        }
        const { key } = req.params;
        const store = getSecretStore();
        const removed = await store.delete(key, actorFromReq(req));
        if (removed)
            notifySecretMutation(key);
        return reply.send({ removed });
    });
}
//# sourceMappingURL=settings-api.js.map