// Shared Quality Intelligence run-lifecycle runtime (Epic #270, Issue #273, ADR-0023 D6).
//
// Behaviour-preserving extraction of the run-event emission, stage tracking, cancellation, and
// evidence-persistence helpers shared by every QI run entry (the deterministic test-design /
// coverage / validation / refinement entries in `runEntries.ts` and the model-routed test-design
// entry in `modelRoutedTestDesign.ts`). NO new scheduler, NO event bus: this composes the existing
// Harness world and emits the versioned QI run-event envelope from `@oscharko-dev/keiko-contracts`.
import { QualityIntelligence as QI } from "@oscharko-dev/keiko-contracts";
import { recordQualityIntelligenceRun, } from "@oscharko-dev/keiko-evidence";
import { QualityIntelligenceSafeErrorException } from "@oscharko-dev/keiko-model-gateway";
import { buildAtomCoverageStatuses, buildCoverageMap, regressionDefault, } from "@oscharko-dev/keiko-quality-intelligence";
import { QUALITY_INTELLIGENCE_DEFAULT_RETENTION_PROFILE_ID } from "@oscharko-dev/keiko-evidence";
import { composeStageCancellation, isCancelled } from "./cancellation.js";
export const DEFAULT_CLOCK = Object.freeze({
    nowIso: () => new Date().toISOString(),
});
export function makeContext(init) {
    return {
        descriptor: init.descriptor,
        plan: init.plan,
        sink: init.sink,
        clock: init.clock ?? DEFAULT_CLOCK,
        limits: init.limits ?? init.descriptor.defaultLimits,
        profile: init.policyProfile ?? regressionDefault,
        signal: init.signal,
        sequence: 0,
        modelGatewayCallCount: 0,
    };
}
function nextSequence(ctx) {
    const value = ctx.sequence;
    ctx.sequence = value + 1;
    return value;
}
export function emit(ctx, payload) {
    const event = Object.freeze({
        eventSchemaVersion: QI.QUALITY_INTELLIGENCE_EVENT_SCHEMA_VERSION,
        runId: ctx.plan.id,
        sequence: nextSequence(ctx),
        timestamp: ctx.clock.nowIso(),
        payload: Object.freeze(payload),
    });
    ctx.sink.emit(event);
}
export function emitQueuedAndStarted(ctx) {
    emit(ctx, { kind: "run:queued" });
    emit(ctx, { kind: "run:started" });
}
function assertStageRegistered(descriptor, stageName) {
    if (!descriptor.stageNames.includes(stageName)) {
        throw new Error(`Stage "${stageName}" is not declared by descriptor ${descriptor.workflowId}`);
    }
}
export class StageCancelledError extends Error {
    constructor() {
        super("Quality Intelligence run cancelled before completion");
        this.name = "StageCancelledError";
    }
}
export function checkCancelled(ctx) {
    if (isCancelled(ctx.signal)) {
        throw new StageCancelledError();
    }
}
export function safeReasonSummary(error) {
    if (error instanceof QualityIntelligenceSafeErrorException) {
        return `qi-safe-error: ${error.safe.code}`;
    }
    if (error instanceof Error) {
        const firstLine = error.message.split("\n")[0] ?? error.name;
        return firstLine.slice(0, 200);
    }
    return "unknown-error";
}
export async function withStage(ctx, stageName, body) {
    assertStageRegistered(ctx.descriptor, stageName);
    checkCancelled(ctx);
    emit(ctx, { kind: "stage:started", stageName });
    const handle = composeStageCancellation(ctx.signal);
    try {
        const result = await body();
        checkCancelled(ctx);
        emit(ctx, { kind: "stage:completed", stageName });
        return result;
    }
    catch (caught) {
        if (caught instanceof StageCancelledError) {
            throw caught;
        }
        if (isCancelled(ctx.signal)) {
            // The stage work threw because the run was cancelled mid-flight (e.g. the model call's
            // AbortSignal fired and the gateway rejected). That is a cancellation, not a stage failure:
            // do not emit stage:failed, and let the finaliser classify the run as "cancelled".
            throw new StageCancelledError();
        }
        emit(ctx, { kind: "stage:failed", stageName, reasonSummary: safeReasonSummary(caught) });
        throw caught;
    }
    finally {
        handle.dispose();
    }
}
// ─── Candidate / finding helpers ─────────────────────────────────────────────
export function truncateCandidates(candidates, limit) {
    return candidates.length <= limit ? candidates : Object.freeze(candidates.slice(0, limit));
}
export function truncateFindings(findings, limit) {
    return findings.length <= limit ? findings : Object.freeze(findings.slice(0, limit));
}
/**
 * Project per-atom coverage statuses into persistable matrix rows (Epic #734). When the caller
 * supplies `excerptByAtomId` (atom id -> already-redacted excerpt, see `buildRequirementExcerpt`),
 * each row additionally carries `requirementExcerptRedacted` so coverage/traceability surfaces are
 * auditor-readable (#790); rows without a map entry stay refs-only.
 */
export function toCoverageMatrixRows(statuses, excerptByAtomId) {
    return Object.freeze(statuses.map((s) => {
        const excerpt = excerptByAtomId?.get(String(s.atomId));
        return Object.freeze({
            atomId: String(s.atomId),
            status: s.status,
            confidence: s.confidence,
            coveringCandidateIds: Object.freeze(s.coveringCandidateIds.map(String)),
            ...(excerpt !== undefined ? { requirementExcerptRedacted: excerpt } : {}),
        });
    }));
}
/**
 * Build the persistable coverage matrix for a run from its atoms + candidates. Used by every run
 * path so coverage is persisted consistently (the scripted paths previously discarded it).
 */
export function coverageMatrixFor(runId, atoms, candidates, excerptByAtomId) {
    const coverageMap = buildCoverageMap({ runId, atoms, candidates });
    return toCoverageMatrixRows(buildAtomCoverageStatuses(atoms, coverageMap), excerptByAtomId);
}
export function emitCandidateProposed(ctx, candidates) {
    for (const candidate of candidates) {
        emit(ctx, {
            kind: "candidate:proposed",
            candidateId: candidate.id,
            derivedFromAtomIds: candidate.derivedFromAtomIds,
        });
    }
}
export function emitFindingsRecorded(ctx, findings) {
    for (const finding of findings) {
        emit(ctx, { kind: "finding:recorded", findingId: finding.id });
    }
}
function mapFindingsToRows(findings) {
    return Object.freeze(findings.map((f) => Object.freeze({
        id: String(f.id),
        kind: f.kind,
        severity: f.severity,
        summaryRedacted: f.summary,
        ...(f.candidateId !== undefined ? { candidateId: String(f.candidateId) } : {}),
    })));
}
export function persistRun(args) {
    const findingRows = mapFindingsToRows(args.findings);
    const evidenceRefs = args.evidenceRefs ?? Object.freeze([]);
    const input = {
        runId: String(args.ctx.plan.id),
        planAt: args.ctx.plan.requestedAt,
        completedAt: args.completedAt,
        status: args.status,
        policyProfileIds: Object.freeze([args.ctx.profile.id]),
        retentionPolicyId: QUALITY_INTELLIGENCE_DEFAULT_RETENTION_PROFILE_ID,
        modelGatewayCallCount: args.ctx.modelGatewayCallCount,
        totals: Object.freeze({
            candidates: args.candidatesCount,
            findings: findingRows.length,
            exports: 0,
        }),
        findings: findingRows,
        exports: Object.freeze([]),
        evidenceRefs,
        provenanceRefs: args.provenanceRefs,
        coverageMatrix: args.coverageMatrix,
        ...(args.qualityScore !== undefined ? { qualityScore: args.qualityScore } : {}),
        ...(args.sourceFingerprints !== undefined
            ? { sourceFingerprints: args.sourceFingerprints }
            : {}),
        ...(args.atomFingerprints !== undefined ? { atomFingerprints: args.atomFingerprints } : {}),
        ...(args.modelId !== undefined ? { modelId: args.modelId } : {}),
        ...(args.modelParameters !== undefined ? { modelParameters: args.modelParameters } : {}),
        ...(args.seedUsed !== undefined ? { seedUsed: args.seedUsed } : {}),
    };
    return recordQualityIntelligenceRun(input, { store: args.evidenceStore });
}
export function finaliseFailureOrCancellation(ctx, caught, args) {
    // A StageCancelledError, or any error raised while the run signal is aborted, is a cancellation
    // (defense-in-depth: an abort-induced rejection that bypassed withStage must not look like a failure).
    if (caught instanceof StageCancelledError || isCancelled(ctx.signal)) {
        emit(ctx, { kind: "run:cancelled" });
        return Object.freeze({
            runId: ctx.plan.id,
            workflowId: ctx.descriptor.workflowId,
            status: "cancelled",
            eventsEmitted: ctx.sequence,
            modelGatewayCallCount: ctx.modelGatewayCallCount,
        });
    }
    const reasonSummary = safeReasonSummary(caught);
    emit(ctx, { kind: "run:failed", reasonSummary });
    let evidence;
    try {
        evidence = persistRun({
            ctx,
            status: "failed",
            candidatesCount: args.candidatesCount,
            findings: args.findings,
            evidenceRefs: args.evidenceRefs,
            provenanceRefs: args.provenanceRefs,
            completedAt: ctx.clock.nowIso(),
            evidenceStore: args.evidenceStore,
        });
    }
    catch {
        evidence = undefined;
    }
    return Object.freeze({
        runId: ctx.plan.id,
        workflowId: ctx.descriptor.workflowId,
        status: "failed",
        eventsEmitted: ctx.sequence,
        modelGatewayCallCount: ctx.modelGatewayCallCount,
        evidence,
        reasonSummary,
    });
}
