// Quality Intelligence generation port (Epic #270, Issue #279).
//
// Backs the abstract `QualityIntelligenceGenerationPort` (consumed by the model-routed workflow run
// entry) with the REAL Keiko Model Gateway via `deps.modelPortFactory` — the same seam the
// Conversation Center / grounded-QA path uses. Applies the #279 capability gate before any request
// is built (unsupported models fail before a network call), assembles trusted system + instruction
// segments separately from `<qi-evidence>`-delimited untrusted source text, and scrubs evidence so
// it cannot break the delimiter or smuggle control characters (Issue #284). No provider SDK import.
import { QualityIntelligence as MgQI, findCapability, findConfiguredCapability, QualityIntelligenceSafeErrorException, } from "@oscharko-dev/keiko-model-gateway";
import { QualityIntelligenceHardening, QualityIntelligenceGeneration, } from "@oscharko-dev/keiko-quality-intelligence";
const BASELINE_GENERATION_OUTPUT = JSON.stringify({ testCases: [] });
export class QiGenerationError extends Error {
    code;
    constructor(code, message) {
        super(message);
        this.code = code;
        this.name = "QiGenerationError";
    }
}
function capabilityFor(deps, modelId) {
    return deps.config === undefined
        ? findCapability(modelId)
        : findConfiguredCapability(deps.config, modelId);
}
// Strip C0/C1 control characters (keep tab/LF/CR) via a code-point scan so the `no-control-regex`
// rule stays satisfied; then neutralise any literal evidence delimiter so untrusted text cannot
// close the <qi-evidence> block early.
function scrubEvidenceText(text) {
    const normalised = text.normalize("NFKC");
    let out = "";
    for (const ch of normalised) {
        const cp = ch.codePointAt(0);
        if (cp === undefined)
            continue;
        const isStrippable = (cp <= 0x1f && cp !== 0x09 && cp !== 0x0a && cp !== 0x0d) ||
            cp === 0x7f ||
            (cp >= 0x80 && cp <= 0x9f);
        if (!isStrippable)
            out += ch;
    }
    return out.replace(/<\/?qi-evidence/giu, "[evidence]");
}
function buildEvidenceBlocks(evidence) {
    return evidence
        .map((e) => {
        const kind = e.kind.replace(/[^a-z0-9-]/giu, "");
        return `<qi-evidence index="${String(e.index)}" kind="${kind}">\n${scrubEvidenceText(e.text)}\n</qi-evidence>`;
    })
        .join("\n");
}
function buildMessages(args) {
    const blocks = buildEvidenceBlocks(args.evidence);
    const userContent = `${args.instruction}\n\n${blocks}`;
    const size = QualityIntelligenceHardening.assertPromptSize(userContent);
    if (!size.ok) {
        throw new QiGenerationError("QI_PROMPT_TOO_LARGE", "The assembled prompt exceeds the model token budget; reduce the source size.");
    }
    return Object.freeze([
        Object.freeze({ role: "system", content: args.systemPrompt }),
        Object.freeze({ role: "user", content: userContent }),
    ]);
}
/** Apply the qi:test-design capability gate and resolve the model port (Epic #761 / #279). */
function resolveGenerationModel(deps, target) {
    const { modelId } = target;
    const capability = capabilityFor(deps, modelId);
    if (capability === undefined) {
        throw new QiGenerationError("QI_MODEL_NOT_CONFIGURED", "The selected model is not configured.");
    }
    const profile = MgQI.getQualityIntelligenceTaskProfile("qi:test-design");
    try {
        MgQI.assertProfileCompatibleWithModel(profile, capability);
    }
    catch (error) {
        if (error instanceof QualityIntelligenceSafeErrorException) {
            throw new QiGenerationError("QI_MODEL_INCOMPATIBLE", "The selected model cannot generate test cases because it is not a compatible chat model.");
        }
        throw error;
    }
    const model = deps.modelPortFactory(modelId);
    if (model === undefined) {
        throw new QiGenerationError("QI_MODEL_UNAVAILABLE", "The model gateway is not available.");
    }
    return {
        model,
        modelId,
        useResponseFormat: capability.supportsResponseFormat === true,
        useSeed: capability.supportsSeeding === true && target.requestedSeed !== undefined,
        requestedSeed: target.requestedSeed,
    };
}
function normalizeTarget(target) {
    if (typeof target === "string") {
        return { kind: "model", modelId: target };
    }
    return target;
}
function createBaselineGenerationPort() {
    return {
        generate: () => Promise.resolve({
            rawText: BASELINE_GENERATION_OUTPUT,
            modelCallCount: 0,
        }),
    };
}
function buildGenerationRequest(modelId, messages, useResponseFormat, useSeed, requestedSeed) {
    return {
        modelId,
        messages,
        stream: false,
        ...(useSeed && requestedSeed !== undefined ? { seed: requestedSeed } : {}),
        ...(useResponseFormat
            ? {
                responseFormat: {
                    type: "json_schema",
                    schema: { ...QualityIntelligenceGeneration.QI_TEST_DESIGN_RESPONSE_SCHEMA },
                },
            }
            : {}),
    };
}
function buildModelParameters(useResponseFormat, useSeed, requestedSeed) {
    const modelParameters = {};
    if (useResponseFormat)
        modelParameters.responseFormat = "json_schema";
    if (useSeed && requestedSeed !== undefined)
        modelParameters.seed = requestedSeed;
    return Object.keys(modelParameters).length > 0 ? modelParameters : undefined;
}
function createModelGenerationPort(resolved) {
    const { model, modelId, useResponseFormat, useSeed, requestedSeed } = resolved;
    return {
        generate: async (args) => {
            const messages = buildMessages(args);
            const signal = args.signal ?? new AbortController().signal;
            const request = buildGenerationRequest(modelId, messages, useResponseFormat, useSeed, requestedSeed);
            const response = await model.call(request, signal);
            const modelParameters = buildModelParameters(useResponseFormat, useSeed, requestedSeed);
            return {
                rawText: response.content,
                modelCallCount: 1,
                modelId,
                seedUsed: useSeed && requestedSeed !== undefined ? requestedSeed : null,
                ...(modelParameters !== undefined ? { modelParameters } : {}),
            };
        },
    };
}
export function createQiGenerationPort(deps, target) {
    const normalized = normalizeTarget(target);
    if (normalized.kind === "baseline") {
        return createBaselineGenerationPort();
    }
    return createModelGenerationPort(resolveGenerationModel(deps, normalized));
}
