// Connected-context evidence persistence (Issue #187). This records what Keiko inspected and
// what metadata reached the model without persisting query text or excerpt content.
import { createHash } from "node:crypto";
import { HARNESS_VERSION, } from "@oscharko-dev/keiko-contracts";
import { buildEvidenceReport } from "./report.js";
import { createAuditRedactor, deepRedactStrings } from "./redaction.js";
import { EVIDENCE_SCHEMA_VERSION, } from "./types.js";
function sha256Hex(value) {
    return createHash("sha256").update(value).digest("hex");
}
function redactString(redact, value) {
    return redact(value);
}
function byteLength(value) {
    return Buffer.byteLength(value, "utf8");
}
function numberRecord(value) {
    const out = {};
    for (const [key, entry] of Object.entries(value)) {
        if (typeof entry === "number") {
            out[key] = entry;
        }
    }
    return out;
}
function contextOf(input, redact) {
    return {
        workspaceRoot: redactString(redact, input.workspaceRoot),
        totalCandidates: input.pack.files.length + input.pack.omitted.length,
        usedBytes: input.pack.usage.excerptBytes,
        budgetBytes: input.pack.budget.excerptBytesMax,
        droppedForBudget: input.pack.omitted.filter((entry) => entry.reason === "budget-exhausted")
            .length,
        entries: [],
    };
}
function excerptOf(excerpt, redact) {
    const redactedContent = redactString(redact, excerpt.content);
    return {
        atomStableId: redactString(redact, excerpt.atom.stableId),
        scopePath: redactString(redact, excerpt.atom.scopePath),
        ...(excerpt.atom.lineRange === undefined ? {} : { lineRange: excerpt.atom.lineRange }),
        score: excerpt.atom.score,
        provenanceKind: excerpt.atom.provenance.kind,
        tool: redactString(redact, excerpt.atom.provenance.tool),
        queryFingerprint: redactString(redact, excerpt.atom.provenance.queryFingerprint),
        redactionState: excerpt.atom.redactionState,
        contentBytes: excerpt.contentBytes,
        contentSha256: sha256Hex(redactedContent),
    };
}
function fileOf(file, redact) {
    const excerpts = file.excerpts.map((excerpt) => excerptOf(excerpt, redact));
    return {
        scopePath: redactString(redact, file.scopePath),
        role: file.role,
        selectionReason: redactString(redact, file.selectionReason),
        excerptCount: file.excerpts.length,
        excerptBytes: file.excerpts.reduce((total, excerpt) => total + excerpt.contentBytes, 0),
        excerpts,
    };
}
function toolsUsed(pack, redact) {
    const tools = new Set(["model-gateway"]);
    for (const file of pack.files) {
        for (const excerpt of file.excerpts) {
            tools.add(redactString(redact, excerpt.atom.provenance.tool));
        }
    }
    return [...tools].sort();
}
function scopeOf(input, redact) {
    return {
        schemaVersion: input.pack.scope.schemaVersion,
        scopeIdHash: sha256Hex(redactString(redact, input.pack.scope.scopeId)),
        scopeKind: input.pack.scope.kind,
        selectedPathCount: input.pack.scope.relativePaths.length,
        selectedPaths: input.pack.scope.relativePaths.map((path) => redactString(redact, path)),
    };
}
function queryOf(input, redact) {
    const safeQueryText = redactString(redact, input.pack.query.text);
    return {
        kind: input.pack.query.kind,
        queryTextHash: sha256Hex(safeQueryText),
        queryTextBytes: byteLength(safeQueryText),
        maxResults: input.pack.query.maxResults,
        caseSensitive: input.pack.query.caseSensitive,
    };
}
function summaryOf(input) {
    return {
        fileCount: input.pack.files.length,
        citationCount: input.citationCount,
        omittedCount: input.pack.omitted.length,
        uncertaintyCount: input.pack.uncertainty.length,
        elapsedMs: input.elapsedMs,
    };
}
function connectedContextOf(input, redact) {
    return {
        packSchemaVersion: input.pack.schemaVersion,
        packStableIdHash: sha256Hex(redactString(redact, input.pack.stableId)),
        chatIdHash: input.chatId === undefined ? undefined : sha256Hex(redactString(redact, input.chatId)),
        modelRequest: {
            sentToModel: true,
            excerptContentPersisted: false,
        },
        scope: scopeOf(input, redact),
        query: queryOf(input, redact),
        budget: {
            usage: numberRecord(input.pack.usage),
            limits: numberRecord(input.pack.budget),
        },
        files: input.pack.files.map((file) => fileOf(file, redact)),
        omitted: input.pack.omitted.map((entry) => ({
            scopePath: redactString(redact, entry.scopePath),
            reason: entry.reason,
        })),
        uncertainty: input.pack.uncertainty.map((entry) => ({
            kind: entry.kind,
            impactedAtomCount: entry.impactedAtomIds.length,
        })),
        toolsUsed: toolsUsed(input.pack, redact),
        summary: summaryOf(input),
    };
}
function buildConnectedContextEvidenceManifest(input, costClassResolver, redact = (value) => value) {
    const identityDurationMs = Math.max(0, input.finishedAt - input.startedAt);
    return {
        evidenceSchemaVersion: EVIDENCE_SCHEMA_VERSION,
        run: {
            runId: input.runId,
            fingerprint: sha256Hex(redactString(redact, input.pack.stableId)),
            harnessVersion: HARNESS_VERSION,
            taskType: "connected-context",
            outcome: "completed",
            startedAt: input.startedAt,
            finishedAt: input.finishedAt,
            durationMs: identityDurationMs,
        },
        model: {
            modelId: input.modelId,
            costClass: costClassResolver?.(input.modelId) ?? "unknown",
        },
        usageTotals: {
            promptTokens: input.pack.usage.modelInputTokens,
            completionTokens: input.pack.usage.modelOutputTokens,
            requestCount: 1,
            totalLatencyMs: input.elapsedMs,
        },
        context: contextOf(input, redact),
        stateTransitions: [],
        toolCalls: [],
        commandExecutions: [],
        connectedContext: connectedContextOf(input, redact),
    };
}
export function persistConnectedContextEvidence(input, ctx) {
    const redactor = createAuditRedactor({ additionalSecrets: ctx.additionalSecrets ?? [] }, ctx.env);
    const manifest = buildConnectedContextEvidenceManifest(input, ctx.costClassResolver, redactor);
    const safeManifest = deepRedactStrings(manifest, redactor);
    const location = ctx.store.put(safeManifest.run.runId, JSON.stringify(safeManifest, null, 2));
    return { manifest: safeManifest, location, report: buildEvidenceReport(safeManifest, location) };
}
