import { getGlobal, updateGlobal } from '../../actions.js';
import { logEvent } from '../../security/audit.js';
import { ACTUATE_TAG_ROOT, actuateTag, cachedRead } from '../../cache/http.js';
import { getClientIp } from '../../security/client-ip.js';
import { getActuateConfig } from '../../config/runtime.js';
import { guardedDb } from '../guarded-db.js';
import { json, jsonWithCache, errorResponse, internalError, hasModel, requireAuth, requireGlobalScope, requireAdminScope, buildActionContext, ADMIN_ROLES, requireRole, diffScalarKeys, } from '../route-helpers.js';
export function registerAiSettingsRoutes(router) {
    const db = guardedDb;
    // ---------------------------------------------------------------------------
    // Settings > AI — provider connections, dynamic model catalog, feature flags.
    // Backed by the `__ai_config__` config document (see `../ai/config-store.ts`).
    // ADMIN-gated. Provider API keys are encrypted at rest and NEVER returned.
    // ---------------------------------------------------------------------------
    const AI_PROVIDER_KEYS = ['openai', 'anthropic', 'xai', 'custom'];
    function isAiProviderKey(value) {
        return typeof value === 'string' && AI_PROVIDER_KEYS.includes(value);
    }
    function defaultProviderName(provider) {
        switch (provider) {
            case 'openai':
                return 'OpenAI';
            case 'anthropic':
                return 'Anthropic';
            case 'xai':
                return 'xAI (Grok)';
            default:
                return 'Custom provider';
        }
    }
    router.get('/ai/settings', async (request) => {
        try {
            const auth = await requireAuth(request);
            if (auth.error)
                return auth.error;
            const adminErr = requireAdminScope(auth.session);
            if (adminErr)
                return adminErr;
            const { getAIConfig, toPublicConfig, getModels, resolveModelOrUnavailable } = await import('../../ai/index.js');
            const config = await getAIConfig(db());
            const pub = toPublicConfig(config);
            const catalog = await getModels(db());
            const defaultModel = resolveModelOrUnavailable(config.settings.defaultModelId, catalog);
            return json({
                data: {
                    settings: pub.settings,
                    providers: pub.providers,
                    defaultModel,
                    updatedAt: config.updatedAt ?? null,
                    updatedBy: config.updatedBy ?? null,
                },
            });
        }
        catch (err) {
            return internalError(err, 'ai/settings GET');
        }
    });
    router.put('/ai/settings', async (request) => {
        try {
            const auth = await requireAuth(request);
            if (auth.error)
                return auth.error;
            const adminErr = requireAdminScope(auth.session);
            if (adminErr)
                return adminErr;
            const body = (await request.json().catch(() => null));
            if (!body || typeof body !== 'object') {
                return errorResponse('Request body must be an object', 400);
            }
            const { getAIConfig, putAIConfig, getModels, normalizeBrandVoice } = await import('../../ai/index.js');
            const config = await getAIConfig(db());
            const catalog = await getModels(db());
            // Validate the selected default provider exists.
            if (body.defaultProviderId &&
                !config.providers.some((p) => p.id === body.defaultProviderId)) {
                return errorResponse('Selected provider is not connected', 400);
            }
            // Validate the selected default model is in the catalog and available.
            if (body.defaultModelId) {
                const model = catalog.find((m) => m.id === body.defaultModelId || m.providerModelId === body.defaultModelId);
                if (!model)
                    return errorResponse('Selected model is not in the catalog', 400);
                if (model.status === 'unavailable') {
                    return errorResponse('Selected model is currently unavailable', 400);
                }
            }
            // Validate budget.
            let monthlyTokenLimit = config.settings.budget.monthlyTokenLimit;
            if (body.budget && 'monthlyTokenLimit' in body.budget) {
                const v = body.budget.monthlyTokenLimit;
                if (v === null)
                    monthlyTokenLimit = undefined;
                else if (typeof v === 'number' && Number.isFinite(v) && v >= 0)
                    monthlyTokenLimit = v;
                else
                    return errorResponse('monthlyTokenLimit must be a non-negative number or null', 400);
            }
            // Validate per-feature model/provider routing overrides. Empty string and
            // null both clear the override (revert to the default model).
            const featurePatches = body.features ?? [];
            for (const patch of featurePatches) {
                if (patch.modelId != null && patch.modelId !== '') {
                    const m = catalog.find((x) => x.id === patch.modelId || x.providerModelId === patch.modelId);
                    if (!m)
                        return errorResponse(`Model "${patch.modelId}" is not in the catalog`, 400);
                    if (m.status === 'unavailable') {
                        return errorResponse(`Model "${patch.modelId}" is currently unavailable`, 400);
                    }
                }
                if (patch.providerId != null &&
                    patch.providerId !== '' &&
                    !config.providers.some((p) => p.id === patch.providerId)) {
                    return errorResponse(`Provider "${patch.providerId}" is not connected`, 400);
                }
            }
            // Apply feature patches by key (ignore unknown keys). Each patch may toggle
            // `enabled` and/or set the per-feature model/provider routing override.
            const patchByKey = new Map(featurePatches.map((f) => [f.key, f]));
            const clearOverride = (v) => v == null || v === '' ? undefined : v;
            const nextFeatures = config.settings.features.map((f) => {
                const patch = patchByKey.get(f.key);
                if (!patch)
                    return f;
                const next = { ...f };
                if (typeof patch.enabled === 'boolean')
                    next.enabled = patch.enabled;
                if ('modelId' in patch)
                    next.modelId = clearOverride(patch.modelId);
                if ('providerId' in patch)
                    next.providerId = clearOverride(patch.providerId);
                return next;
            });
            const changedFeatures = config.settings.features
                .map((f) => {
                const patch = patchByKey.get(f.key);
                if (!patch)
                    return null;
                const enabledChanged = typeof patch.enabled === 'boolean' && patch.enabled !== f.enabled;
                const nextModelId = 'modelId' in patch ? clearOverride(patch.modelId) : f.modelId;
                const modelChanged = 'modelId' in patch && (nextModelId ?? null) !== (f.modelId ?? null);
                if (!enabledChanged && !modelChanged)
                    return null;
                return {
                    key: f.key,
                    ...(enabledChanged ? { enabledFrom: f.enabled, enabledTo: patch.enabled } : {}),
                    ...(modelChanged ? { modelFrom: f.modelId ?? null, modelTo: nextModelId ?? null } : {}),
                };
            })
                .filter((x) => x !== null);
            // Brand voice: `null` clears it, an object replaces it (normalized +
            // length-clamped), omitted leaves the stored profile untouched.
            let nextBrandVoice = config.settings.brandVoice;
            if ('brandVoice' in body) {
                if (body.brandVoice === null)
                    nextBrandVoice = undefined;
                else if (body.brandVoice && typeof body.brandVoice === 'object') {
                    nextBrandVoice = {
                        ...normalizeBrandVoice(body.brandVoice),
                        updatedAt: new Date().toISOString(),
                        updatedBy: auth.session.userId,
                    };
                }
                else {
                    return errorResponse('brandVoice must be an object or null', 400);
                }
            }
            const nextSettings = {
                ...config.settings,
                defaultProviderId: body.defaultProviderId === null
                    ? undefined
                    : (body.defaultProviderId ?? config.settings.defaultProviderId),
                defaultModelId: body.defaultModelId === null
                    ? undefined
                    : (body.defaultModelId ?? config.settings.defaultModelId),
                features: nextFeatures,
                budget: { ...config.settings.budget, monthlyTokenLimit },
                brandVoice: nextBrandVoice,
            };
            const saved = await putAIConfig(db(), { ...config, settings: nextSettings }, auth.session.userId);
            try {
                await logEvent({
                    event: 'update_ai_settings',
                    userId: auth.session.userId,
                    details: {
                        defaultProviderId: nextSettings.defaultProviderId ?? null,
                        defaultModelId: nextSettings.defaultModelId ?? null,
                        monthlyTokenLimit: monthlyTokenLimit ?? null,
                        changedFeatures,
                        brandVoiceEnabled: nextBrandVoice?.enabled ?? false,
                        source: 'settings.ai',
                    },
                });
            }
            catch {
                // Audit must never block the write.
            }
            const { toPublicConfig } = await import('../../ai/index.js');
            return json({ data: toPublicConfig(saved) });
        }
        catch (err) {
            return internalError(err, 'ai/settings PUT');
        }
    });
    router.post('/ai/providers', async (request) => {
        try {
            const auth = await requireAuth(request);
            if (auth.error)
                return auth.error;
            const adminErr = requireAdminScope(auth.session);
            if (adminErr)
                return adminErr;
            const body = (await request.json().catch(() => null));
            if (!body || !isAiProviderKey(body.provider)) {
                return errorResponse('A valid `provider` is required', 400);
            }
            const provider = body.provider;
            if (provider === 'custom') {
                if (!body.baseUrl || typeof body.baseUrl !== 'string') {
                    return errorResponse('A `baseUrl` is required for a custom provider', 400);
                }
                const { validateWebhookUrl } = await import('../../security/webhook.js');
                const check = validateWebhookUrl(body.baseUrl);
                if (!check.valid)
                    return errorResponse(`Invalid baseUrl: ${check.error ?? 'unsafe'}`, 400);
            }
            const { getAIConfig, putAIConfig, encryptProviderKey, getEnvApiKey, resolveProviderKey, toPublicProvider, } = await import('../../ai/index.js');
            const { testConnection } = await import('../../ai/providers/index.js');
            const config = await getAIConfig(db());
            const now = new Date().toISOString();
            const envManaged = Boolean(getEnvApiKey(provider));
            let apiKeyEncrypted;
            let apiKeyLast4;
            if (!envManaged && typeof body.apiKey === 'string' && body.apiKey.trim()) {
                const enc = await encryptProviderKey(body.apiKey.trim());
                apiKeyEncrypted = enc.apiKeyEncrypted;
                apiKeyLast4 = enc.apiKeyLast4;
            }
            const connection = {
                id: crypto.randomUUID(),
                provider,
                displayName: (typeof body.displayName === 'string' && body.displayName.trim()) ||
                    defaultProviderName(provider),
                enabled: body.enabled !== false,
                status: 'unknown',
                apiKeyEncrypted,
                apiKeyLast4,
                baseUrl: provider === 'custom' ? body.baseUrl : undefined,
                organizationId: body.organizationId?.trim() || undefined,
                projectId: body.projectId?.trim() || undefined,
                lastTestedAt: undefined,
                createdAt: now,
                updatedAt: now,
                updatedBy: auth.session.userId,
            };
            // Optional test-before-save.
            if (body.test) {
                const key = await resolveProviderKey(connection);
                const result = await testConnection(provider, {
                    apiKey: key ?? '',
                    baseUrl: connection.baseUrl,
                    organizationId: connection.organizationId,
                    projectId: connection.projectId,
                });
                connection.status = result.status;
                connection.lastTestedAt = result.testedAt;
            }
            const saved = await putAIConfig(db(), { ...config, providers: [...config.providers, connection] }, auth.session.userId);
            try {
                await logEvent({
                    event: 'create_ai_provider',
                    userId: auth.session.userId,
                    details: {
                        providerId: connection.id,
                        provider,
                        envManaged,
                        tested: Boolean(body.test),
                        status: connection.status,
                        source: 'settings.ai',
                    },
                });
            }
            catch {
                /* never block on audit */
            }
            const created = saved.providers.find((p) => p.id === connection.id);
            return json({ data: toPublicProvider(created) }, 201);
        }
        catch (err) {
            return internalError(err, 'ai/providers POST');
        }
    });
    router.put('/ai/providers/:id', async (request, params) => {
        try {
            const auth = await requireAuth(request);
            if (auth.error)
                return auth.error;
            const adminErr = requireAdminScope(auth.session);
            if (adminErr)
                return adminErr;
            const body = (await request.json().catch(() => null));
            if (!body || typeof body !== 'object') {
                return errorResponse('Request body must be an object', 400);
            }
            const { getAIConfig, putAIConfig, encryptProviderKey, getEnvApiKey, toPublicProvider } = await import('../../ai/index.js');
            const config = await getAIConfig(db());
            const idx = config.providers.findIndex((p) => p.id === params.id);
            if (idx === -1)
                return errorResponse('Provider connection not found', 404);
            const prev = config.providers[idx];
            if (prev.provider === 'custom' && typeof body.baseUrl === 'string') {
                const { validateWebhookUrl } = await import('../../security/webhook.js');
                const check = validateWebhookUrl(body.baseUrl);
                if (!check.valid)
                    return errorResponse(`Invalid baseUrl: ${check.error ?? 'unsafe'}`, 400);
            }
            const envManaged = Boolean(getEnvApiKey(prev.provider));
            let { apiKeyEncrypted, apiKeyLast4 } = prev;
            let keyChanged = false;
            if (!envManaged && body.apiKey !== undefined) {
                if (body.apiKey === null || body.apiKey === '') {
                    apiKeyEncrypted = undefined;
                    apiKeyLast4 = undefined;
                    keyChanged = true;
                }
                else if (typeof body.apiKey === 'string' && body.apiKey.trim()) {
                    const enc = await encryptProviderKey(body.apiKey.trim());
                    apiKeyEncrypted = enc.apiKeyEncrypted;
                    apiKeyLast4 = enc.apiKeyLast4;
                    keyChanged = true;
                }
            }
            const updated = {
                ...prev,
                displayName: typeof body.displayName === 'string' && body.displayName.trim()
                    ? body.displayName.trim()
                    : prev.displayName,
                enabled: typeof body.enabled === 'boolean' ? body.enabled : prev.enabled,
                baseUrl: prev.provider === 'custom' && typeof body.baseUrl === 'string'
                    ? body.baseUrl
                    : prev.baseUrl,
                organizationId: body.organizationId === null
                    ? undefined
                    : (body.organizationId?.trim() ?? prev.organizationId),
                projectId: body.projectId === null ? undefined : (body.projectId?.trim() ?? prev.projectId),
                apiKeyEncrypted,
                apiKeyLast4,
                // A new key invalidates the previous test result.
                status: keyChanged ? 'unknown' : prev.status,
                lastTestedAt: keyChanged ? undefined : prev.lastTestedAt,
                updatedAt: new Date().toISOString(),
                updatedBy: auth.session.userId,
            };
            const nextProviders = [...config.providers];
            nextProviders[idx] = updated;
            const saved = await putAIConfig(db(), { ...config, providers: nextProviders }, auth.session.userId);
            try {
                await logEvent({
                    event: 'update_ai_provider',
                    userId: auth.session.userId,
                    details: {
                        providerId: prev.id,
                        provider: prev.provider,
                        keyChanged,
                        source: 'settings.ai',
                    },
                });
            }
            catch {
                /* never block on audit */
            }
            const out = saved.providers.find((p) => p.id === prev.id);
            return json({ data: toPublicProvider(out) });
        }
        catch (err) {
            return internalError(err, 'ai/providers PUT');
        }
    });
    router.delete('/ai/providers/:id', async (request, params) => {
        try {
            const auth = await requireAuth(request);
            if (auth.error)
                return auth.error;
            const adminErr = requireAdminScope(auth.session);
            if (adminErr)
                return adminErr;
            const id = params.id;
            if (!id)
                return errorResponse('Provider connection not found', 404);
            const { getAIConfig, putAIConfig } = await import('../../ai/index.js');
            const config = await getAIConfig(db());
            const exists = config.providers.some((p) => p.id === id);
            if (!exists)
                return errorResponse('Provider connection not found', 404);
            const nextCatalog = { ...(config.catalog ?? {}) };
            delete nextCatalog[id];
            // Clear default selectors that pointed at the removed provider.
            const clearedProvider = config.settings.defaultProviderId === id;
            const nextSettings = clearedProvider
                ? { ...config.settings, defaultProviderId: undefined, defaultModelId: undefined }
                : config.settings;
            await putAIConfig(db(), {
                ...config,
                providers: config.providers.filter((p) => p.id !== id),
                catalog: nextCatalog,
                settings: nextSettings,
            }, auth.session.userId);
            try {
                await logEvent({
                    event: 'delete_ai_provider',
                    userId: auth.session.userId,
                    details: { providerId: id, source: 'settings.ai' },
                });
            }
            catch {
                /* never block on audit */
            }
            return json({ data: { id: params.id, deleted: true } });
        }
        catch (err) {
            return internalError(err, 'ai/providers DELETE');
        }
    });
    router.post('/ai/providers/:id/test', async (request, params) => {
        try {
            const auth = await requireAuth(request);
            if (auth.error)
                return auth.error;
            const adminErr = requireAdminScope(auth.session);
            if (adminErr)
                return adminErr;
            const { getAIConfig, putAIConfig, resolveProviderKey, toPublicProvider } = await import('../../ai/index.js');
            const { testConnection } = await import('../../ai/providers/index.js');
            const config = await getAIConfig(db());
            const idx = config.providers.findIndex((p) => p.id === params.id);
            if (idx === -1)
                return errorResponse('Provider connection not found', 404);
            const conn = config.providers[idx];
            const key = await resolveProviderKey(conn);
            const result = await testConnection(conn.provider, {
                apiKey: key ?? '',
                baseUrl: conn.baseUrl,
                organizationId: conn.organizationId,
                projectId: conn.projectId,
            });
            const updated = { ...conn, status: result.status, lastTestedAt: result.testedAt };
            const nextProviders = [...config.providers];
            nextProviders[idx] = updated;
            const saved = await putAIConfig(db(), { ...config, providers: nextProviders }, auth.session.userId);
            try {
                await logEvent({
                    event: 'test_ai_provider',
                    userId: auth.session.userId,
                    details: {
                        providerId: conn.id,
                        provider: conn.provider,
                        status: result.status,
                        source: 'settings.ai',
                    },
                });
            }
            catch {
                /* never block on audit */
            }
            const out = saved.providers.find((p) => p.id === conn.id);
            return json({ data: { provider: toPublicProvider(out), result } });
        }
        catch (err) {
            return internalError(err, 'ai/providers test');
        }
    });
    router.post('/ai/providers/:id/sync-models', async (request, params) => {
        try {
            const auth = await requireAuth(request);
            if (auth.error)
                return auth.error;
            const adminErr = requireAdminScope(auth.session);
            if (adminErr)
                return adminErr;
            const id = params.id;
            if (!id)
                return errorResponse('Provider connection not found', 404);
            const { syncModels } = await import('../../ai/index.js');
            const result = await syncModels(db(), id).catch((err) => {
                if (err instanceof Error && /not found/i.test(err.message))
                    return null;
                throw err;
            });
            if (!result)
                return errorResponse('Provider connection not found', 404);
            try {
                await logEvent({
                    event: 'sync_ai_models',
                    userId: auth.session.userId,
                    details: {
                        providerId: id,
                        modelCount: result.models.length,
                        fromFallback: result.fromFallback,
                        source: 'settings.ai',
                    },
                });
            }
            catch {
                /* never block on audit */
            }
            return json({
                data: {
                    models: result.models,
                    fromFallback: result.fromFallback,
                    warning: result.warning ?? null,
                },
            });
        }
        catch (err) {
            return internalError(err, 'ai/providers sync-models');
        }
    });
    router.get('/ai/models', async (request) => {
        try {
            const auth = await requireAuth(request);
            if (auth.error)
                return auth.error;
            const adminErr = requireAdminScope(auth.session);
            if (adminErr)
                return adminErr;
            const url = new URL(request.url);
            const connectionId = url.searchParams.get('connectionId') ?? undefined;
            const provider = url.searchParams.get('provider') ?? undefined;
            const { getModels } = await import('../../ai/index.js');
            const models = await getModels(db(), { connectionId, provider });
            return json({ data: { models } });
        }
        catch (err) {
            return internalError(err, 'ai/models GET');
        }
    });
    router.get('/ai/usage/summary', async (request) => {
        try {
            const auth = await requireAuth(request);
            if (auth.error)
                return auth.error;
            const adminErr = requireAdminScope(auth.session);
            if (adminErr)
                return adminErr;
            const { getUsageSummary } = await import('../../ai/index.js');
            const summary = await getUsageSummary(db());
            return json({ data: summary });
        }
        catch (err) {
            return internalError(err, 'ai/usage/summary GET');
        }
    });
    router.put('/security', async (request) => {
        try {
            const auth = await requireAuth(request);
            if (auth.error)
                return auth.error;
            const adminErr = requireAdminScope(auth.session);
            if (adminErr)
                return adminErr;
            const body = (await request.json());
            const { parseSecuritySettings, validateSecuritySettings, mfaEnableLockoutError } = await import('../../security/center/settings.js');
            const { getStoredSecuritySettings, getSecurityCenterData } = await import('../../security/center/gather.js');
            const incoming = parseSecuritySettings({ security: body });
            const issues = validateSecuritySettings(incoming);
            const errors = issues.filter((i) => i.severity === 'error');
            if (errors.length > 0) {
                return json({ error: errors[0].message, issues }, 400);
            }
            const d = db();
            const current = await getStoredSecuritySettings(d);
            // Lockout-safe: enabling org-wide MFA requires the acting admin to have
            // their own MFA configured. Enforced server-side — never trust the client.
            const enabling = incoming.mfa.required && !current.mfa.required;
            if (enabling) {
                let viewerHasMfa = false;
                let totalAdmins = 0;
                if (hasModel(d, 'user')) {
                    const viewer = await d.user
                        .findUnique({ where: { id: auth.session.userId }, select: { totpEnabled: true } })
                        .catch(() => null);
                    viewerHasMfa = Boolean(viewer?.totpEnabled);
                    totalAdmins = await d.user
                        .count({ where: { role: 'ADMIN', isActive: true } })
                        .catch(() => 0);
                }
                const lockErr = mfaEnableLockoutError({ enabling: true, viewerHasMfa, totalAdmins });
                if (lockErr) {
                    return json({ error: lockErr, code: 'MFA_LOCKOUT_RISK' }, 409);
                }
            }
            // Stamp `requiredAt` when MFA requirement flips on (drives the grace
            // window); preserve it across saves while it stays on; clear when off.
            const requiredAt = incoming.mfa.required
                ? current.mfa.required
                    ? current.mfa.requiredAt
                    : new Date().toISOString()
                : undefined;
            const securityBlob = {
                mfa: {
                    required: incoming.mfa.required,
                    gracePeriodDays: incoming.mfa.gracePeriodDays,
                    ...(requiredAt ? { requiredAt } : {}),
                },
                session: { maxConcurrentSessions: incoming.session.maxConcurrentSessions },
                updatedAt: new Date().toISOString(),
                updatedBy: auth.session.userId,
            };
            const ctx = buildActionContext(auth.session, d);
            const prevGlobal = (await getGlobal('settings', ctx).catch(() => ({}))) ?? {};
            await updateGlobal('settings', { ...prevGlobal, security: securityBlob }, ctx);
            const changes = [];
            if (current.mfa.required !== incoming.mfa.required)
                changes.push({ key: 'mfa.required', from: current.mfa.required, to: incoming.mfa.required });
            if (current.mfa.gracePeriodDays !== incoming.mfa.gracePeriodDays)
                changes.push({
                    key: 'mfa.gracePeriodDays',
                    from: current.mfa.gracePeriodDays,
                    to: incoming.mfa.gracePeriodDays,
                });
            if (current.session.maxConcurrentSessions !== incoming.session.maxConcurrentSessions)
                changes.push({
                    key: 'session.maxConcurrentSessions',
                    from: current.session.maxConcurrentSessions,
                    to: incoming.session.maxConcurrentSessions,
                });
            if (changes.length > 0) {
                void logEvent({
                    event: 'security_settings_changed',
                    userId: auth.session.userId,
                    ipAddress: getClientIp(request),
                    details: {
                        source: 'database',
                        environment: process.env.VERCEL_ENV ?? process.env.NODE_ENV ?? null,
                        changes,
                    },
                });
            }
            const data = await getSecurityCenterData({
                db: d,
                userId: auth.session.userId,
                currentIp: getClientIp(request),
            });
            return json({ data });
        }
        catch (err) {
            return internalError(err, 'security PUT');
        }
    });
    router.post('/security/sessions/revoke-all', async (request) => {
        try {
            const auth = await requireAuth(request);
            if (auth.error)
                return auth.error;
            const d = db();
            if (!hasModel(d, 'session'))
                return json({ data: { revoked: 0 } });
            // Revoke every OTHER active session for the caller — their current
            // session stays valid so they aren't logged out of the admin they're in.
            const result = await d.session.updateMany({
                where: {
                    userId: auth.session.userId,
                    id: { not: auth.session.sessionId },
                    revokedAt: null,
                },
                data: { revokedAt: new Date() },
            });
            const revoked = typeof result?.count === 'number' ? result.count : 0;
            void logEvent({
                event: 'sessions_revoked_all',
                userId: auth.session.userId,
                ipAddress: getClientIp(request),
                details: { count: revoked, scope: 'self_other' },
            });
            return json({ data: { revoked } });
        }
        catch (err) {
            return internalError(err, 'security/sessions/revoke-all');
        }
    });
    router.get('/globals/:slug', async (request, params) => {
        try {
            const auth = await requireAuth(request);
            if (auth.error)
                return auth.error;
            const scopeErr = requireGlobalScope(auth.session, params.slug);
            if (scopeErr)
                return scopeErr;
            return cachedRead(request, {
                cache: getActuateConfig()?.cache,
                tags: [ACTUATE_TAG_ROOT, actuateTag('global', params.slug)],
                wrap: jsonWithCache,
                build: async () => {
                    const ctx = buildActionContext(auth.session, db());
                    const global = await getGlobal(params.slug, ctx);
                    // Globals are upsert-on-write: a slug with no row yet (fresh install,
                    // never-saved settings) is a valid EMPTY state, not an error. Return
                    // {} so the admin editor (e.g. the Settings page) loads cleanly before
                    // the first save, and the next PUT creates the document. The public
                    // `/public/globals/:slug` route keeps its config-gated 404 for
                    // genuinely undeclared globals.
                    return { data: global ?? {} };
                },
            });
        }
        catch (err) {
            return internalError(err);
        }
    });
    router.put('/globals/:slug', async (request, params) => {
        try {
            const auth = await requireAuth(request);
            if (auth.error)
                return auth.error;
            const scopeErr = requireGlobalScope(auth.session, params.slug);
            if (scopeErr)
                return scopeErr;
            if (!auth.session.apiKey) {
                const roleErr = requireRole(auth.session.role, ADMIN_ROLES);
                if (roleErr)
                    return roleErr;
            }
            const body = (await request.json());
            const ctx = buildActionContext(auth.session, db());
            const previous = await getGlobal(params.slug, ctx).catch(() => null);
            const global = await updateGlobal(params.slug, body, ctx);
            // Audit settings changes (title/tagline/URL/locale/timezone/etc.) with a
            // per-key before/after diff so the audit trail records what changed and
            // who changed it. Scalar values only — we never log nested secret blobs.
            // Nested config blobs (`appearance`, `brand`) are logged as a changed
            // key (not their contents) so appearance/branding edits are auditable
            // without dumping asset IDs or theme objects into the log.
            try {
                const changes = diffScalarKeys(previous ?? {}, body);
                const prev = (previous ?? {});
                for (const key of ['appearance', 'brand', 'security']) {
                    if (key in body && JSON.stringify(prev[key]) !== JSON.stringify(body[key])) {
                        changes.push({ key, from: '[changed]', to: '[changed]' });
                    }
                }
                if (changes.length > 0) {
                    void logEvent({
                        event: 'settings_changed',
                        userId: auth.session.userId,
                        ipAddress: getClientIp(request),
                        details: {
                            global: params.slug,
                            source: 'database',
                            environment: process.env.VERCEL_ENV ?? process.env.NODE_ENV ?? null,
                            changes,
                        },
                    });
                }
            }
            catch {
                // Audit failures must never block the write.
            }
            return json({ data: global });
        }
        catch (err) {
            return internalError(err);
        }
    });
}
//# sourceMappingURL=ai-settings.js.map