import { createHash, randomUUID } from "node:crypto";
import { pendingHostCallResumeIdentityMatches } from "../snapshot/policy.js";
export class HostCallResumabilityError extends Error {
    action;
    callId;
    lifecycle;
    constructor(record, action, message) {
        super(message);
        this.name = "HostCallResumabilityError";
        this.action = action;
        this.callId = record.id;
        this.lifecycle = record.lifecycle;
    }
}
export class HostCallJournal {
    sourceHash;
    resumeProvider;
    runId;
    nextCall = 1;
    records;
    restored;
    constructor(sourceHash, records = [], resumeProvider) {
        this.sourceHash = sourceHash;
        this.resumeProvider = resumeProvider;
        this.runId = records[0]?.runId ?? randomUUID();
        this.records = records.map((record) => structuredClone(record));
        validateRestoredRecords(this.records, this.runId, sourceHash);
        this.restored = [...this.records];
    }
    issue(input) {
        const restored = this.restored[0];
        if (restored !== undefined) {
            const restoredOrdinal = readCallOrdinal(restored);
            if (this.nextCall < restoredOrdinal) {
                return { record: this.createRecord(input), restored: false };
            }
            if (!callIdentityMatches(restored, input)) {
                throw new HostCallResumabilityError(restored, "reset", `Host call ${restored.id} does not match the next restored invocation; reset is required.`);
            }
            this.restored.shift();
            this.nextCall += 1;
            return { record: restored, restored: true };
        }
        return { record: this.createRecord(input), restored: false };
    }
    createRecord(input) {
        const record = {
            id: `${this.runId}:${this.nextCall++}`,
            runId: this.runId,
            sourceHash: this.sourceHash,
            moduleId: input.moduleId,
            operation: input.operation,
            argumentDigest: input.argumentDigest,
            policy: input.policy,
            lifecycle: "created"
        };
        this.records.push(record);
        this.records.sort((left, right) => readCallOrdinal(left) - readCallOrdinal(right));
        return record;
    }
    start(record) {
        record.lifecycle = "running";
    }
    settle(record, outcome) {
        if (record.lifecycle === "cancelled")
            return;
        record.outcome = outcome;
        record.lifecycle = "settled";
    }
    consume(record) {
        if (record.lifecycle === "consumed") {
            throw new HostCallResumabilityError(record, "reset", `Host call ${record.id} result was already consumed; reset is required.`);
        }
        if (record.lifecycle !== "settled")
            return;
        record.lifecycle = "consumed";
    }
    cancel(record, reason) {
        if (record.lifecycle === "settled" || record.lifecycle === "consumed")
            return;
        record.outcome = { status: "rejected", reason };
        record.lifecycle = "cancelled";
    }
    async reconcile(record) {
        if (record.lifecycle === "settled" && record.outcome !== undefined)
            return record.outcome;
        if (record.lifecycle === "consumed") {
            throw new HostCallResumabilityError(record, "reset", `Host call ${record.id} result was already consumed; reset is required.`);
        }
        if (record.lifecycle === "cancelled") {
            throw new HostCallResumabilityError(record, "reset", `Host call ${record.id} was cancelled; reset is required.`);
        }
        if (record.policy === "re-issue") {
            throw new HostCallResumabilityError(record, "reset", `Host call ${record.id} must be re-issued by the runtime.`);
        }
        if (this.resumeProvider === undefined) {
            throw new HostCallResumabilityError(record, "external-reconciliation", `Host call ${record.id} may have executed before process death; external reconciliation is required.`);
        }
        const { id, outcome: ignoredOutcome, ...request } = record;
        void ignoredOutcome;
        const proof = await this.resumeProvider({
            ...request,
            callId: id,
            requirement: "external-reconciliation"
        });
        validateProof(record, proof);
        this.settle(record, proof.outcome);
        return proof.outcome;
    }
    snapshot() {
        return this.records
            .filter((record) => record.policy === "read-side-effect" ||
            record.lifecycle === "created" ||
            record.lifecycle === "running" ||
            record.lifecycle === "cancelled")
            .map((record) => structuredClone(record));
    }
}
function validateRestoredRecords(records, runId, sourceHash) {
    const ids = new Set();
    let previousOrdinal = 0;
    for (const record of records) {
        if (record.runId !== runId || !record.id.startsWith(`${runId}:`)) {
            throw new HostCallResumabilityError(record, "reset", `Host call ${record.id} does not belong to restored run ${runId}; reset is required.`);
        }
        if (record.sourceHash !== sourceHash) {
            throw new HostCallResumabilityError(record, "reset", `Host call ${record.id} does not match the restored source; reset is required.`);
        }
        if (ids.has(record.id)) {
            throw new HostCallResumabilityError(record, "reset", `Host call ${record.id} appears more than once; reset is required.`);
        }
        const ordinal = readCallOrdinal(record);
        if (ordinal <= previousOrdinal) {
            throw new HostCallResumabilityError(record, "reset", `Host call ${record.id} is out of order; reset is required.`);
        }
        previousOrdinal = ordinal;
        ids.add(record.id);
    }
}
function readCallOrdinal(record) {
    const ordinal = Number(record.id.slice(record.id.lastIndexOf(":") + 1));
    if (!Number.isSafeInteger(ordinal) || ordinal < 1) {
        throw new HostCallResumabilityError(record, "reset", `Host call ${record.id} has an invalid ordinal; reset is required.`);
    }
    return ordinal;
}
function callIdentityMatches(record, input) {
    return (record.moduleId === input.moduleId &&
        record.operation === input.operation &&
        record.argumentDigest === input.argumentDigest &&
        record.policy === input.policy);
}
export function digestHostCallArguments(args) {
    return createHash("sha256").update(stableStringify(args)).digest("hex");
}
function validateProof(record, proof) {
    if (!pendingHostCallResumeIdentityMatches({
        argumentDigest: record.argumentDigest,
        callId: record.id,
        moduleId: record.moduleId,
        operation: record.operation,
        sourceHash: record.sourceHash
    }, proof)) {
        throw new HostCallResumabilityError(record, "external-reconciliation", `External result proof does not match host call ${record.id}.`);
    }
}
function stableStringify(value) {
    const seen = new WeakSet();
    return JSON.stringify(normalize(value, seen));
}
function normalize(value, seen) {
    if (value === undefined)
        return { $type: "undefined" };
    if (typeof value === "number" && !Number.isFinite(value))
        return { $type: "number", value: String(value) };
    if (value === null || typeof value !== "object")
        return value;
    if (seen.has(value))
        throw new TypeError("Host call arguments cannot contain cycles.");
    seen.add(value);
    try {
        if (Array.isArray(value))
            return value.map((entry) => normalize(entry, seen));
        return Object.fromEntries(Object.keys(value)
            .sort()
            .map((key) => [key, normalize(value[key], seen)]));
    }
    finally {
        seen.delete(value);
    }
}
