// Quality Intelligence dispatcher (Epic #270, Issue #279).
//
// Routes a Quality Intelligence model call through the existing gateway ModelPort. The
// dispatcher COMPOSES — it never instantiates a provider SDK. It performs (in order):
//   1. capability check                       (M1: capabilityGate)
//   2. budget reservation                     (M2: budget)
//   3. prompt segmentation                    (M1: promptSegmentation)
//   4. cache lookup                           (M2: replayCache)
//   5. wire-prompt assembly                   (this file, pure)
//   6. ModelPort.call(...) with composed signal and profile timeout (this file)
//   7. cache store (if cacheable)             (M2: replayCache)
//   8. error -> qi/* safe-error shape         (M1: safeError)
// The ModelPort itself is the gateway's existing ProviderAdapter — proven at the type
// level by the parameter signature.
import { composeCancellationSignal } from "./cancellation.js";
import { isExhausted, reserveBudget } from "./budget.js";
import { assertProfileCompatibleWithModel } from "./capabilityGate.js";
import { buildPromptSegments, } from "./promptSegmentation.js";
import { deriveReplayCacheKey, isCacheable, } from "./replayCache.js";
import { QualityIntelligenceSafeErrorException, makeBudgetExhaustedError, makeCancelledError, makeProviderError, makeTimeoutError, } from "./safeError.js";
function assembleMessages(segments) {
    const evidenceBlock = segments.evidenceUntrusted.length === 0
        ? ""
        : segments.evidenceUntrusted
            .map((e) => `<qi-evidence kind="${e.kind}">${e.value}</qi-evidence>`)
            .join("\n");
    const userContent = evidenceBlock === ""
        ? segments.instructionTrusted
        : `${segments.instructionTrusted}\n${evidenceBlock}`;
    return Object.freeze([
        Object.freeze({ role: "system", content: segments.systemTrusted }),
        Object.freeze({ role: "user", content: userContent }),
    ]);
}
function buildGatewayRequest(modelId, segments, signal) {
    return Object.freeze({
        modelId,
        messages: assembleMessages(segments),
        stream: false,
        cancellationSignal: signal,
    });
}
function classifyAndThrow(profileId, timeoutMs, reasonKind, caught) {
    if (caught instanceof QualityIntelligenceSafeErrorException) {
        throw caught;
    }
    if (reasonKind === "timeout") {
        throw new QualityIntelligenceSafeErrorException(makeTimeoutError(profileId, timeoutMs));
    }
    if (reasonKind === "external") {
        throw new QualityIntelligenceSafeErrorException(makeCancelledError(profileId));
    }
    throw new QualityIntelligenceSafeErrorException(makeProviderError(profileId));
}
async function invokePort(ctx) {
    const handle = composeCancellationSignal(ctx.profile.timeoutMsHint, ctx.externalSignal);
    try {
        const request = buildGatewayRequest(ctx.modelId, ctx.segments, handle.signal);
        return await ctx.port.call(request, ctx.providerConfig);
    }
    catch (caught) {
        classifyAndThrow(ctx.profile.id, ctx.profile.timeoutMsHint, handle.reasonKind(), caught);
    }
    finally {
        handle.dispose();
    }
}
export async function dispatchQualityIntelligenceRequest(args) {
    assertProfileCompatibleWithModel(args.profile, args.model);
    if (isExhausted(args.budget)) {
        throw new QualityIntelligenceSafeErrorException(makeBudgetExhaustedError(args.profile.id));
    }
    const nextBudget = reserveBudget(args.budget, args.profile.tokenBudgetHint);
    const segments = buildPromptSegments(args.profile, args.instruction, args.evidence);
    const cacheKey = await deriveReplayCacheKey(args.profile, segments, args.model.id);
    if (isCacheable(args.profile)) {
        const cached = args.cache.get(cacheKey);
        if (cached !== undefined) {
            return Object.freeze({
                response: cached,
                budget: nextBudget,
                cacheHit: true,
            });
        }
    }
    const response = await invokePort({
        profile: args.profile,
        port: args.port,
        providerConfig: args.providerConfig,
        modelId: args.model.id,
        segments,
        externalSignal: args.signal,
    });
    if (isCacheable(args.profile)) {
        args.cache.set(cacheKey, response);
    }
    return Object.freeze({
        response,
        budget: nextBudget,
        cacheHit: false,
    });
}
