// Wave 2 BFF handler dependencies (ADR-0011 D5/D8/D9). The Wave 1 skeleton's `UiServerDeps` carried
// only the static-serving + CSP + port fields; the JSON/SSE handlers additionally need the resolved
// gateway config (for the config inspector and for building a ModelPort), an evidence store, a live
// redactor, the process env, and the in-memory run registry. Every field here is OPTIONAL so the
// 3-arg `createUiServer({ staticRoot, csp, port })` form still compiles and the Wave 1 server tests
// pass unchanged; the handlers degrade gracefully (no config → 400 NO_MODEL on a run, null config on
// the inspector; no store → an empty evidence list).
import { createDefaultChatCapability, loadConfigFromFile, parseGatewayConfig, } from "@oscharko-dev/keiko-model-gateway";
import { GatewayError, Gateway } from "@oscharko-dev/keiko-model-gateway";
import { GatewayModelPort } from "@oscharko-dev/keiko-harness";
import { createAuditRedactor } from "@oscharko-dev/keiko-evidence";
import { resolveCostClass } from "@oscharko-dev/keiko-model-gateway";
import { writeSideFile } from "@oscharko-dev/keiko-evidence";
import { deepRedactStrings } from "@oscharko-dev/keiko-evidence";
import { keikoApiKeySecretValues } from "@oscharko-dev/keiko-security";
import { nodeWorkspaceFs } from "@oscharko-dev/keiko-workspace/internal/fs";
import { createNodeEvidenceStore, resolveEvidenceDir } from "@oscharko-dev/keiko-evidence";
import { dirname, join } from "node:path";
import { createRunRegistry } from "./runs.js";
import { buildUiStoreOverDatabase, openNodeUiDatabase, resolveUiDbPath, } from "./store/index.js";
import { createTerminalExecutionManager } from "./terminal.js";
import { createBrowserSessionManager } from "@oscharko-dev/keiko-tools";
import {} from "@oscharko-dev/keiko-memory-vault";
import { createBffMemoryVault } from "./memory-handlers.js";
import { createMemoryAuditHandler } from "./memory-audit-handler.js";
import { createConsolidationJobRegistry, } from "./memory-consolidation-registry.js";
import { createRelationshipStorePort, } from "./relationship-handlers.js";
import { resolveGroundingLimits, } from "@oscharko-dev/keiko-contracts/bff-wire";
function envModelToken(modelId) {
    return modelId.replace(/[^A-Za-z0-9]/g, "_").toUpperCase();
}
function envModelIdFromApiKeyName(name) {
    const prefix = "KEIKO_MODEL_";
    const suffix = "_API_KEY";
    if (!name.startsWith(prefix) || !name.endsWith(suffix)) {
        return undefined;
    }
    const token = name.slice(prefix.length, -suffix.length);
    return token.length === 0 ? undefined : token.toLowerCase().replace(/_/g, "-");
}
function hasEnvProvider(modelId, env) {
    const token = envModelToken(modelId);
    const baseUrl = env[`KEIKO_MODEL_${token}_BASE_URL`];
    const apiKey = env[`KEIKO_MODEL_${token}_API_KEY`];
    return baseUrl !== undefined && baseUrl.length > 0 && apiKey !== undefined && apiKey.length > 0;
}
function envModelIds(env) {
    const modelIds = [];
    for (const key of Object.keys(env)) {
        const modelId = envModelIdFromApiKeyName(key);
        if (modelId !== undefined && hasEnvProvider(modelId, env)) {
            modelIds.push(modelId);
        }
    }
    return Array.from(new Set(modelIds));
}
function resolveEnvOnlyConfig(env) {
    const providers = envModelIds(env).map((modelId) => ({
        modelId,
        baseUrl: "",
        apiKey: "",
        capability: createDefaultChatCapability(modelId),
    }));
    if (providers.length === 0) {
        return undefined;
    }
    try {
        return parseGatewayConfig({ providers }, env);
    }
    catch (error) {
        if (error instanceof GatewayError) {
            return undefined;
        }
        throw error;
    }
}
function localGatewayConfigPath(uiDbPath) {
    return join(dirname(uiDbPath), "keiko.config.json");
}
// Loads the config without leaking the path or any secret on failure: a missing/invalid config file
// falls back to KEIKO_MODEL_* env wiring when present, otherwise it is a normal "no config" state.
function resolveConfig(configPath, env, localConfigPath) {
    if (configPath === undefined) {
        let config;
        try {
            config = loadConfigFromFile(localConfigPath, env);
        }
        catch (error) {
            if (error instanceof GatewayError) {
                config = resolveEnvOnlyConfig(env);
            }
            else {
                throw error;
            }
        }
        return { config, configPresent: config !== undefined };
    }
    try {
        return { config: loadConfigFromFile(configPath, env), configPresent: true };
    }
    catch (error) {
        if (error instanceof GatewayError) {
            const config = resolveEnvOnlyConfig(env);
            return { config, configPresent: config !== undefined };
        }
        throw error;
    }
}
function createRuntimeGatewayConfig(initial, initialPresent, storagePath) {
    let config = initial;
    let present = initialPresent;
    return {
        storagePath,
        current: () => config,
        present: () => present,
        set(next, nextPresent) {
            config = next;
            present = nextPresent;
        },
    };
}
export function currentGatewayConfig(deps) {
    return deps.gatewayConfig?.current() ?? deps.config;
}
export function currentGatewayConfigPresent(deps) {
    return deps.gatewayConfig?.present() ?? deps.configPresent;
}
function parseEnvOnlyEgressConfig(env) {
    try {
        return parseGatewayConfig({
            providers: [
                {
                    modelId: "keiko-egress-probe",
                    baseUrl: "http://127.0.0.1",
                    apiKey: "keiko-egress-probe-key",
                },
            ],
        }, env).egress;
    }
    catch (error) {
        if (error instanceof GatewayError) {
            return undefined;
        }
        throw error;
    }
}
export function currentGatewayEgressConfig(deps) {
    return (deps.gatewayConfig?.current()?.egress ??
        deps.config?.egress ??
        parseEnvOnlyEgressConfig(deps.env));
}
// Module-level: read KEIKO_GROUNDING_* env overrides ONCE at load (mirrors KEIKO_MODEL_* env
// reads). Each value is parsed as a positive integer; unparseable values are silently ignored so
// misconfigured env does not prevent the server from starting.
function parseEnvPositiveInt(raw) {
    if (raw === undefined)
        return undefined;
    const n = Number.parseInt(raw, 10);
    return Number.isFinite(n) && n >= 1 ? n : undefined;
}
const ENV_GROUNDING_OVERRIDES = (() => {
    const env = process.env;
    const partial = {};
    const maxConnectedSources = parseEnvPositiveInt(env.KEIKO_GROUNDING_MAX_CONNECTED_SOURCES);
    if (maxConnectedSources !== undefined)
        partial.maxConnectedSources = maxConnectedSources;
    const maxLocalKnowledgeSources = parseEnvPositiveInt(env.KEIKO_GROUNDING_MAX_LOCAL_KNOWLEDGE_SOURCES);
    if (maxLocalKnowledgeSources !== undefined)
        partial.maxLocalKnowledgeSources = maxLocalKnowledgeSources;
    const maxPromptReferences = parseEnvPositiveInt(env.KEIKO_GROUNDING_MAX_PROMPT_REFERENCES);
    if (maxPromptReferences !== undefined)
        partial.maxPromptReferences = maxPromptReferences;
    const maxExcerptChars = parseEnvPositiveInt(env.KEIKO_GROUNDING_MAX_EXCERPT_CHARS);
    if (maxExcerptChars !== undefined)
        partial.maxExcerptChars = maxExcerptChars;
    const referenceBudget = parseEnvPositiveInt(env.KEIKO_GROUNDING_REFERENCE_BUDGET);
    if (referenceBudget !== undefined)
        partial.referenceBudget = referenceBudget;
    const hybridMaxCandidates = parseEnvPositiveInt(env.KEIKO_GROUNDING_HYBRID_MAX_CANDIDATES);
    if (hybridMaxCandidates !== undefined)
        partial.hybridMaxCandidates = hybridMaxCandidates;
    const hybridMaxExcerptBytes = parseEnvPositiveInt(env.KEIKO_GROUNDING_HYBRID_MAX_EXCERPT_BYTES);
    if (hybridMaxExcerptBytes !== undefined)
        partial.hybridMaxExcerptBytes = hybridMaxExcerptBytes;
    return partial;
})();
// Resolves the effective grounding limits at call time: file config → env overrides → ceilings.
// Env overrides win over file config. Re-reads currentGatewayConfig each call so runtime config
// updates (e.g. first-run UI onboarding) are honored immediately. Never stored as a frozen field.
export function currentGroundingLimits(deps) {
    const fileGrounding = currentGatewayConfig(deps)?.grounding;
    return resolveGroundingLimits({ ...fileGrounding, ...ENV_GROUNDING_OVERRIDES });
}
function configSecretValues(config) {
    // Epic #177 audit: include both `apiKey` and `baseUrl` so error-message and evidence
    // redaction can scrub the provider URL the user (or the provider's response) might echo
    // back. `baseUrl` is not a credential per se, but pairing it with the apiKey reveals the
    // backend topology and gives an attacker a place to direct probes; the matrix's security
    // section claims both shapes are scrubbed at the BFF boundary.
    if (config === undefined)
        return [];
    const out = [];
    const addEgressTopology = (egress) => {
        if (egress === undefined)
            return;
        if (egress.httpProxy !== undefined)
            out.push(egress.httpProxy);
        if (egress.httpsProxy !== undefined)
            out.push(egress.httpsProxy);
        if (egress.caBundlePath !== undefined)
            out.push(egress.caBundlePath);
    };
    addEgressTopology(config.egress);
    for (const provider of config.providers) {
        out.push(provider.apiKey, provider.baseUrl);
        addEgressTopology(provider.egress);
    }
    return out;
}
function egressSecretValues(egress) {
    if (egress === undefined)
        return [];
    return [egress.httpProxy, egress.httpsProxy, egress.caBundlePath].filter((value) => value !== undefined);
}
function redactionSecrets(env, config, egress = config?.egress) {
    return Array.from(new Set([
        ...keikoApiKeySecretValues(env),
        ...configSecretValues(config),
        ...egressSecretValues(egress),
    ]));
}
function runtimeRedactionSecrets(env, runtimeConfig) {
    const config = runtimeConfig.current();
    return redactionSecrets(env, config, config?.egress ?? parseEnvOnlyEgressConfig(env));
}
function runtimeRedactString(env, runtimeConfig) {
    return (value) => createAuditRedactor({ additionalSecrets: runtimeRedactionSecrets(env, runtimeConfig) }, env)(value);
}
// Builds the live-payload redactor from the configured redaction settings + env. No new regex: this
// reuses `createAuditRedactor` (escaped literals + audited gateway patterns) wrapped by
// `deepRedactStrings` so every string leaf of a serialized payload is scrubbed.
export function buildRedactor(env, config) {
    const redactString = createAuditRedactor({
        additionalSecrets: redactionSecrets(env, config, config?.egress ?? parseEnvOnlyEgressConfig(env)),
    }, env);
    return (value) => deepRedactStrings(value, redactString);
}
export function currentRedactionSecrets(deps) {
    return redactionSecrets(deps.env, currentGatewayConfig(deps), currentGatewayEgressConfig(deps));
}
// The production ModelPort factory: a GatewayModelPort over a Gateway built from the resolved
// config (mirrors the CLI's `new GatewayModelPort(new Gateway(config))`). Returns undefined when no
// config was resolved so the run route answers 400 NO_MODEL rather than constructing a broken port.
function defaultModelPortFactory(runtimeConfig) {
    return () => {
        const config = runtimeConfig.current();
        if (config === undefined) {
            return undefined;
        }
        return new GatewayModelPort(new Gateway(config));
    };
}
function buildTerminalManager(options) {
    return createTerminalExecutionManager({
        store: options.store,
        evidenceStore: options.evidenceStore,
        processEnv: options.env,
        redactor: (value) => {
            const redacted = options.liveRedactor(value);
            return typeof redacted === "string" ? redacted : value;
        },
    });
}
// ADR-0019 direction rule 3c: the tools package cannot import src/audit. The BFF injects the
// cost-class resolver and a side-file writer that closes over the resolved evidenceDir + the
// nodeWorkspaceFs realpath-containment port, so the browser session manager stays self-contained
// against contracts + security + workspace only.
function buildBrowserManager(options) {
    return createBrowserSessionManager({
        evidenceDir: options.evidenceDir,
        evidenceStore: options.evidenceStore,
        redactor: options.redactor,
        costClassResolver: resolveCostClass,
        sideFileWriter: (basename, bytes, runId) => writeSideFile(options.evidenceDir, runId, basename, bytes, { fs: nodeWorkspaceFs }),
    });
}
function buildMemoryVault(redactString, evidenceStore, env) {
    return createBffMemoryVault(redactString, 
    // #214 — wire every successful vault mutation into the audit ledger. The handler
    // shares the same redactString closure as the live-payload redactor so audit
    // summaries inherit the same secret-shape scrubbing as wire traffic.
    createMemoryAuditHandler({ evidenceStore, redactString }), env);
}
// Issue #539: the relationship engine runs server-authoritative scope checks on every route.
// In the loopback `keiko ui` BFF there is exactly one workspace per process; the resolver
// returns that workspace identifier from `KEIKO_WORKSPACE_ID` (set), or a stable default
// otherwise. The constant matches the empty-but-non-zero-length contract of `scope()` so
// every route resolves a workspaceId instead of returning 403.
const DEFAULT_LOOPBACK_WORKSPACE_ID = "local";
function resolveLoopbackWorkspaceId(env) {
    const explicit = env.KEIKO_WORKSPACE_ID;
    if (typeof explicit === "string" && explicit.length > 0)
        return explicit;
    return DEFAULT_LOOPBACK_WORKSPACE_ID;
}
// When no UiStore is injected, open one DatabaseSync against the resolved UI-DB and share it
// with the relationship-engine store so V5 sibling tables share the UI-store transaction model
// (issue #539, storage.md §3.1). When tests inject a UiStore we leave `relationship` undefined;
// relationship-engine tests inject their own deps.
function composePersistence(injected, resolvedUiDbPath, redactString, env) {
    if (injected !== undefined)
        return { store: injected, relationship: undefined };
    const db = openNodeUiDatabase(resolvedUiDbPath);
    const store = buildUiStoreOverDatabase(db, { redactString });
    const relationship = {
        scopeResolver: () => ({
            workspaceId: resolveLoopbackWorkspaceId(env),
        }),
        store: createRelationshipStorePort({ db, redactString }),
    };
    return { store, relationship };
}
function buildPeripherals(options, uiStore, evidenceStore, redactString, liveRedactor) {
    return {
        terminal: buildTerminalManager({
            store: uiStore,
            evidenceStore,
            env: options.env,
            liveRedactor,
        }),
        browser: buildBrowserManager({
            evidenceDir: resolveEvidenceDir(options.evidenceDir, options.env),
            evidenceStore,
            redactor: liveRedactor,
        }),
        memoryVault: buildMemoryVault(redactString, evidenceStore, options.env),
    };
}
// Assembles the handler deps for the real `keiko ui` process, mirroring the CLI config/evidence
// wiring (loadConfigFromFile / resolveEvidenceDir / createNodeEvidenceStore). The UI store is
// created at the resolved UI-DB path (explicit → KEIKO_UI_DATA_DIR → ~/.keiko/keiko-ui.db) unless
// an injected store is supplied (tests).
export function buildUiHandlerDeps(options) {
    const resolvedUiDbPath = resolveUiDbPath(options.uiDbPath, options.env);
    const runtimeConfigPath = localGatewayConfigPath(resolvedUiDbPath);
    const { config, configPresent } = resolveConfig(options.configPath, options.env, runtimeConfigPath);
    const runtimeConfig = createRuntimeGatewayConfig(config, configPresent, runtimeConfigPath);
    const evidenceStore = createNodeEvidenceStore(resolveEvidenceDir(options.evidenceDir, options.env));
    const redactString = runtimeRedactString(options.env, runtimeConfig);
    const liveRedactor = (value) => deepRedactStrings(value, redactString);
    const { store: uiStore, relationship } = composePersistence(options.store, resolvedUiDbPath, redactString, options.env);
    const peripherals = buildPeripherals(options, uiStore, evidenceStore, redactString, liveRedactor);
    return {
        config,
        configPresent,
        evidenceStore,
        evidenceDir: resolveEvidenceDir(options.evidenceDir, options.env),
        env: options.env,
        redactor: liveRedactor,
        registry: options.registry ?? createRunRegistry(),
        modelPortFactory: options.modelPortFactory ?? defaultModelPortFactory(runtimeConfig),
        redactionSecrets: runtimeRedactionSecrets(options.env, runtimeConfig),
        store: uiStore,
        uiDbPath: resolvedUiDbPath,
        gatewayConfig: runtimeConfig,
        gatewaySetupTester: options.gatewaySetupTester,
        gatewayModelDiscovery: options.gatewayModelDiscovery,
        ...peripherals,
        consolidationJobs: createConsolidationJobRegistry(),
        ...(relationship === undefined ? {} : { relationship }),
    };
}
