/**
 * Governed AI runtime — the single execution path that turns the Settings > AI
 * configuration into actual model calls.
 *
 * Everything that wants to run an LLM inside Actuate should go through here so
 * that one DB-backed config drives behavior end-to-end:
 *  - feature toggles are ENFORCED (a disabled feature can't run),
 *  - per-feature model routing is RESOLVED ({@link resolveFeatureModel}),
 *  - the API key is resolved from env or the encrypted store,
 *  - the monthly token budget is checked (when `disableWhenExceeded`),
 *  - real token usage is RECORDED so the usage meter reflects reality.
 *
 * It is intentionally provider-agnostic: the provider adapters
 * (`./providers/*`) speak HTTP, this module decides *whether* and *with what*
 * to call them.
 */
import { applyBrandVoice } from './brand-voice.js';
import { resolveWireModelId } from './catalog.js';
import { getAIConfig, resolveProviderKey } from './config-store.js';
import { getModels } from './models.js';
import { generateText as adapterGenerateText } from './providers/index.js';
import { AdapterHttpError } from './providers/http.js';
import { resolveFeatureModel } from './resolve.js';
import { modelDisallowsSampling, samplingParamProviderMessage } from './sampling.js';
import { getUsageSummary } from './usage.js';
function err(code, message) {
    return { ok: false, code, message };
}
/**
 * Is a configured AI feature turned on? Unknown keys default to enabled so that
 * callers gating on a key the config doesn't track yet don't silently break.
 */
export async function isAiFeatureEnabled(db, featureKey) {
    const config = await getAIConfig(db);
    const feature = config.settings.features.find((f) => f.key === featureKey);
    return feature ? feature.enabled : true;
}
/**
 * Resolve provider + model + key + budget for a feature, enforcing the toggle.
 * Pure resolution — performs no model call. Returns a discriminated result so
 * callers can branch (e.g. fall back to a legacy provider on `no_provider`).
 */
export async function resolveFeatureRuntime(db, featureKey) {
    const config = await getAIConfig(db);
    const feature = config.settings.features.find((f) => f.key === featureKey);
    if (feature && !feature.enabled) {
        return err('feature_disabled', `The "${feature.label}" AI feature is turned off.`);
    }
    const catalog = await getModels(db);
    const resolved = resolveFeatureModel(config, featureKey, catalog);
    if (!resolved.modelId) {
        return err('no_model', 'No AI model is configured. Choose a default model in Settings > AI.');
    }
    if (!resolved.providerId) {
        return err('no_provider', 'No connected AI provider can serve the selected model.');
    }
    const connection = config.providers.find((p) => p.id === resolved.providerId);
    if (!connection) {
        return err('no_provider', 'No AI provider connection is configured for this model.');
    }
    if (connection.enabled === false) {
        return err('no_provider', `The "${connection.displayName}" provider is disabled.`);
    }
    const apiKey = await resolveProviderKey(connection);
    if (!apiKey) {
        return err('no_key', `No API key is set for the "${connection.displayName}" provider.`);
    }
    // Budget gate: only blocks when the operator opted into hard enforcement.
    const budget = config.settings.budget;
    if (budget.disableWhenExceeded && typeof budget.monthlyTokenLimit === 'number') {
        const usage = await getUsageSummary(db);
        if (usage.totalTokens >= budget.monthlyTokenLimit) {
            return err('budget_exceeded', 'The monthly AI token budget has been reached. Raise the limit in Settings > AI.');
        }
    }
    const providerModelId = resolveWireModelId(resolved.model?.providerModelId ?? stripProviderPrefix(resolved.modelId, connection));
    return {
        ok: true,
        featureKey,
        providerKey: connection.provider,
        connectionId: connection.id,
        baseUrl: connection.baseUrl,
        apiKey,
        modelId: resolved.modelId,
        providerModelId,
        disallowsSampling: modelDisallowsSampling(providerModelId, resolved.model?.capabilities),
    };
}
/** `openai:gpt-4o` -> `gpt-4o`; leaves bare ids untouched. */
function stripProviderPrefix(modelId, connection) {
    const prefix = `${connection.provider}:`;
    return modelId.startsWith(prefix) ? modelId.slice(prefix.length) : modelId;
}
/**
 * Run a text generation for a feature through the governed path: resolve →
 * enforce → call → record usage. Returns a discriminated result; never throws
 * for the expected "not configured / disabled / budget" cases (those are
 * {@link RuntimeError}). Genuine provider/transport failures surface as a
 * `provider_error` RuntimeError too, so callers have a single shape to handle.
 */
export async function generateTextForFeature(db, featureKey, input) {
    const resolved = await resolveFeatureRuntime(db, featureKey);
    if (!resolved.ok)
        return resolved;
    // Inject the active brand voice into the system prompt when the caller opts
    // in (content-writing surfaces do; metadata/structured ones don't).
    let system = input.system;
    if (input.useBrandVoice) {
        const config = await getAIConfig(db);
        system = applyBrandVoice(system, config.settings.brandVoice);
    }
    try {
        const temperature = typeof input.temperature === 'number' && !resolved.disallowsSampling
            ? input.temperature
            : undefined;
        const result = await adapterGenerateText(resolved.providerKey, {
            apiKey: resolved.apiKey,
            baseUrl: resolved.baseUrl,
            fetchImpl: input.fetchImpl,
            timeoutMs: input.timeoutMs,
        }, {
            model: resolved.providerModelId,
            prompt: input.prompt,
            system,
            maxTokens: input.maxTokens,
            temperature,
        });
        await recordUsage(db, {
            featureKey,
            modelId: resolved.modelId,
            providerKey: resolved.providerKey,
            userId: input.userId,
            usage: result.usage,
        });
        return {
            ok: true,
            text: result.text,
            usage: result.usage,
            modelId: resolved.modelId,
            providerModelId: resolved.providerModelId,
        };
    }
    catch (e) {
        const raw = e instanceof AdapterHttpError
            ? e.message
            : e instanceof Error
                ? e.message
                : 'The AI provider request failed.';
        const message = samplingParamProviderMessage(raw) ?? raw;
        return err('provider_error', message);
    }
}
/**
 * Record a usage entry in the audit log. Best-effort: a logging failure must
 * never fail the generation that already succeeded. The field names match what
 * {@link getUsageSummary} reads (`details.totalTokensUsed` + prompt/completion).
 */
async function recordUsage(db, entry) {
    const d = db;
    if (!d?.auditLog?.create)
        return;
    try {
        await d.auditLog.create({
            data: {
                event: 'ai_usage',
                userId: entry.userId ?? 'system',
                timestamp: new Date(),
                details: {
                    source: 'ai-runtime',
                    feature: entry.featureKey,
                    model: entry.modelId,
                    provider: entry.providerKey,
                    totalTokensUsed: entry.usage.totalTokens,
                    promptTokens: entry.usage.promptTokens,
                    completionTokens: entry.usage.completionTokens,
                },
            },
        });
    }
    catch {
        // Swallow — usage metering is observational, not transactional.
    }
}
//# sourceMappingURL=runtime.js.map