/**
 * Loopback-only, read-only admin HTTP API for the drawlatch-ui dashboard.
 *
 * SECURITY non-negotiables (mirrored in /admin.test.ts):
 *   - Never serializes Session.channel (AES keys) or ResolvedRoute.secrets.
 *   - Never returns process.env values; only key names. Secret presence is
 *     reported via `isSecretSetForCaller()` (booleans only).
 *   - Caller `env` mappings (e.g. "GITHUB_TOKEN": "${ACME_GITHUB_TOKEN}") are
 *     reduced to key-name lists — the value strings are NOT returned.
 *   - No mutations: every endpoint is GET.
 *   - No CORS. Loopback IS the trust boundary; the UI's local backend proxies.
 */
import express from 'express';
import fs from 'node:fs';
import { getCallerKeysDir, getServerKeysDir, getEnvFilePath, getRemoteConfigPath, } from '../shared/config.js';
import { listConnectionTemplates, loadConnection } from '../shared/connections.js';
import { isSecretSetForCaller } from '../shared/env-utils.js';
import { callerFingerprint } from '../shared/crypto/key-manager.js';
import path from 'node:path';
export function createAdminRouter(deps) {
    const router = express.Router();
    // ── /admin/meta ────────────────────────────────────────────────────────
    router.get('/meta', (_req, res) => {
        res.json({
            version: deps.version,
            port: deps.port,
            pid: process.pid,
            startedAt: deps.startedAt,
            uptimeSec: Math.floor((Date.now() - deps.startedAt) / 1000),
            configPath: getRemoteConfigPath(),
            callerKeysDir: getCallerKeysDir(),
            serverKeysDir: getServerKeysDir(),
            envFilePath: getEnvFilePath(),
        });
    });
    // ── /admin/health ──────────────────────────────────────────────────────
    router.get('/health', (_req, res) => {
        const statuses = deps.ingestorManager().getAllStatuses();
        const counts = { connected: 0, error: 0, starting: 0, stopped: 0 };
        for (const s of statuses) {
            if (s.state === 'connected')
                counts.connected++;
            else if (s.state === 'error')
                counts.error++;
            else if (s.state === 'starting' || s.state === 'reconnecting')
                counts.starting++;
            else
                counts.stopped++;
        }
        res.json({
            status: 'ok',
            activeSessions: deps.getSessionsSnapshot().length,
            ingestorCounts: counts,
            uptimeSec: Math.floor((Date.now() - deps.startedAt) / 1000),
        });
    });
    // ── /admin/callers ─────────────────────────────────────────────────────
    // Returns caller config metadata only — env values are reduced to key names.
    router.get('/callers', (_req, res) => {
        const config = deps.loadConfig();
        const callersKeysDir = getCallerKeysDir();
        const out = Object.entries(config.callers).map(([alias, caller]) => {
            const keysDirExists = fs.existsSync(path.join(callersKeysDir, alias));
            let fingerprint = null;
            try {
                fingerprint = keysDirExists ? callerFingerprint(alias) : null;
            }
            catch {
                fingerprint = null;
            }
            return {
                alias,
                name: caller.name ?? null,
                connections: caller.connections,
                // Key names ONLY — never the mapping value strings.
                envKeys: Object.keys(caller.env ?? {}),
                fingerprint,
                keysDirExists,
            };
        });
        res.json(out);
    });
    // ── /admin/connections ─────────────────────────────────────────────────
    // listConnectionTemplates already returns names-only — safe as-is.
    router.get('/connections', (_req, res) => {
        res.json(listConnectionTemplates());
    });
    // ── /admin/callers/:alias/connections ──────────────────────────────────
    router.get('/callers/:alias/connections', (req, res) => {
        const config = deps.loadConfig();
        if (!(req.params.alias in config.callers)) {
            res.status(404).json({ error: `Unknown caller: ${req.params.alias}` });
            return;
        }
        const caller = config.callers[req.params.alias];
        const templates = listConnectionTemplates();
        const tplMap = new Map(templates.map((t) => [t.alias, t]));
        const customMap = new Map((config.connectors ?? []).map((c) => [c.alias, c]));
        const out = caller.connections.map((connectionAlias) => {
            const tpl = tplMap.get(connectionAlias);
            const customRoute = customMap.get(connectionAlias);
            const isCustom = !tpl;
            const requiredNames = tpl?.requiredSecrets ?? [];
            const optionalNames = tpl?.optionalSecrets ?? [];
            const requiredSecrets = requiredNames.map((name) => ({
                name,
                present: isSecretSetForCaller(name, req.params.alias, caller.env),
            }));
            const optionalSecrets = optionalNames.map((name) => ({
                name,
                present: isSecretSetForCaller(name, req.params.alias, caller.env),
            }));
            // Resolve listenerConfig so we can identify ListenerConfigField.type === 'secret'
            // fields and strip them from the params projection. Without this, a future
            // template that declares a 'secret' field would silently leak the value via
            // the ...ov.params spread below.
            let listenerConfig;
            if (customRoute) {
                listenerConfig = customRoute.listenerConfig;
            }
            else if (tpl) {
                try {
                    listenerConfig = loadConnection(connectionAlias).listenerConfig;
                }
                catch {
                    listenerConfig = undefined;
                }
            }
            const secretFieldKeys = new Set((listenerConfig?.fields ?? []).filter((f) => f.type === 'secret').map((f) => f.key));
            // Multi-instance projection — drop anything that could carry secrets.
            const instancesMap = caller.listenerInstances?.[connectionAlias] ?? {};
            const instances = Object.entries(instancesMap).map(([instanceId, ov]) => {
                const safeParams = ov.params !== undefined
                    ? Object.fromEntries(Object.entries(ov.params).filter(([k]) => !secretFieldKeys.has(k)))
                    : undefined;
                return {
                    instanceId,
                    enabled: ov.disabled !== true,
                    params: {
                        ...(ov.intents !== undefined && { intents: ov.intents }),
                        ...(ov.eventFilter !== undefined && { eventFilter: ov.eventFilter }),
                        ...(ov.guildIds !== undefined && { guildIds: ov.guildIds }),
                        ...(ov.channelIds !== undefined && { channelIds: ov.channelIds }),
                        ...(ov.userIds !== undefined && { userIds: ov.userIds }),
                        ...(ov.bufferSize !== undefined && { bufferSize: ov.bufferSize }),
                        ...(ov.intervalMs !== undefined && { intervalMs: ov.intervalMs }),
                        ...(ov.disabled !== undefined && { disabled: ov.disabled }),
                        // Listener params (board IDs, subreddit names, etc.) are user-set
                        // configuration, not secrets. Pass through verbatim — except for any
                        // field whose schema marks it as type: 'secret', which is filtered above.
                        ...(safeParams !== undefined && safeParams),
                    },
                };
            });
            // `enabled`: a connection is enabled by default; explicit disabled
            // override (single-instance) toggles it off.
            const overrideDisabled = caller.ingestorOverrides?.[connectionAlias]?.disabled === true;
            return {
                connectionAlias,
                enabled: !overrideDisabled,
                isCustom,
                requiredSecrets,
                optionalSecrets,
                hasIngestor: tpl?.hasIngestor ?? customMap.get(connectionAlias)?.ingestor !== undefined,
                instances,
            };
        });
        res.json(out);
    });
    // ── /admin/ingestors ───────────────────────────────────────────────────
    router.get('/ingestors', (_req, res) => {
        const statuses = deps.ingestorManager().getAllStatuses();
        // getAllStatuses already augments with callerAlias; connection and
        // instanceId come from base IngestorStatus.
        const out = statuses.map((s) => ({
            callerAlias: s.callerAlias,
            connection: s.connection,
            ...(s.instanceId !== undefined && { instanceId: s.instanceId }),
            type: s.type,
            state: s.state,
            bufferedEvents: s.bufferedEvents,
            totalEventsReceived: s.totalEventsReceived,
            lastEventAt: s.lastEventAt,
            ...(s.error !== undefined && { error: s.error }),
            ...(s.webhookRegistration !== undefined && {
                webhookRegistration: s.webhookRegistration,
            }),
        }));
        res.json(out);
    });
    // ── /admin/sessions ────────────────────────────────────────────────────
    // SessionSnapshot already strips channel + resolvedRoutes — pass it through.
    router.get('/sessions', (_req, res) => {
        res.json(deps.getSessionsSnapshot());
    });
    // ── /admin/secrets ─────────────────────────────────────────────────────
    // Flat join of (caller × connection × secret), with `present` computed via
    // isSecretSetForCaller() — never the actual env value.
    router.get('/secrets', (_req, res) => {
        const config = deps.loadConfig();
        const tplMap = new Map(listConnectionTemplates().map((t) => [t.alias, t]));
        const out = [];
        for (const [callerAlias, caller] of Object.entries(config.callers)) {
            for (const connectionAlias of caller.connections) {
                const tpl = tplMap.get(connectionAlias);
                if (!tpl)
                    continue; // custom connector — no template-defined secrets to enumerate
                for (const name of tpl.requiredSecrets) {
                    out.push({
                        callerAlias,
                        connection: connectionAlias,
                        name,
                        required: true,
                        present: isSecretSetForCaller(name, callerAlias, caller.env),
                    });
                }
                for (const name of tpl.optionalSecrets) {
                    out.push({
                        callerAlias,
                        connection: connectionAlias,
                        name,
                        required: false,
                        present: isSecretSetForCaller(name, callerAlias, caller.env),
                    });
                }
            }
        }
        res.json(out);
    });
    return router;
}
//# sourceMappingURL=admin.js.map