// Quality Intelligence review-state companion store (Epic #270, Issue #282).
//
// The run manifest is immutable evidence; the candidate artifact is the generated body. The MUTABLE
// human-review/lifecycle decisions (per-candidate + run-level approve / reject / request-changes,
// plus an append-only audit log) live in a third companion artifact `<runId>.review.json`, managed
// through the generic contained JSON artifact store from keiko-evidence. Default state is "open" —
// nothing is approved by default (#282 AC). All reads tolerate a missing artifact.
import { createNodeContainedJsonArtifactStore, } from "@oscharko-dev/keiko-evidence";
import { QualityIntelligence } from "@oscharko-dev/keiko-contracts";
export const QI_REVIEW_SCHEMA_VERSION = 1;
const REVIEW_SUFFIX = ".review.json";
const REVIEW_STATES = new Set(QualityIntelligence.QUALITY_INTELLIGENCE_REVIEW_STATES);
const isReviewState = (value) => typeof value === "string" && REVIEW_STATES.has(value);
const parseArtifact = (value) => {
    if (typeof value !== "object" || value === null)
        return undefined;
    const record = value;
    if (record.qiReviewSchemaVersion !== QI_REVIEW_SCHEMA_VERSION)
        return undefined;
    if (typeof record.runId !== "string" || !isReviewState(record.runState))
        return undefined;
    if (typeof record.candidateStates !== "object" || record.candidateStates === null)
        return undefined;
    return value;
};
const storeFor = (evidenceDir) => createNodeContainedJsonArtifactStore(evidenceDir, REVIEW_SUFFIX, { parse: parseArtifact });
export const loadRunReviewState = (runId, evidenceDir) => storeFor(evidenceDir).load(runId);
export const runReviewStateOf = (artifact) => artifact?.runState ?? "open";
export const candidateReviewStateOf = (artifact, candidateId) => {
    const state = artifact?.candidateStates[candidateId];
    return isReviewState(state) ? state : "open";
};
// ─── Mutation (used by the review-action route, Issue #282) ─────────────────────
const ACTION_TARGET = {
    approve: "approved",
    reject: "rejected",
    "request-changes": "changes-requested",
    reopen: "open",
    withdraw: "withdrawn",
};
const emptyArtifact = (runId, now) => ({
    qiReviewSchemaVersion: QI_REVIEW_SCHEMA_VERSION,
    runId,
    runState: "open",
    candidateStates: {},
    auditLog: [],
    lastUpdatedAt: now,
});
/**
 * Apply a review decision and persist the updated artifact. Pure transition + append-only audit
 * entry; returns the new artifact. The caller is responsible for authorising the action.
 */
export const applyReviewDecision = (input) => {
    const current = loadRunReviewState(input.runId, input.evidenceDir) ?? emptyArtifact(input.runId, input.now);
    const toState = ACTION_TARGET[input.action];
    const isCandidate = input.scope === "candidate" && input.candidateId !== undefined;
    const fromState = isCandidate
        ? candidateReviewStateOf(current, input.candidateId)
        : current.runState;
    const candidateStates = isCandidate
        ? { ...current.candidateStates, [input.candidateId]: toState }
        : current.candidateStates;
    const audit = {
        at: input.now,
        action: input.action,
        scope: input.scope,
        ...(isCandidate ? { candidateId: input.candidateId } : {}),
        reviewerLabel: input.reviewerLabel,
        fromState,
        toState,
    };
    const next = {
        qiReviewSchemaVersion: QI_REVIEW_SCHEMA_VERSION,
        runId: input.runId,
        runState: isCandidate ? current.runState : toState,
        candidateStates,
        auditLog: [...current.auditLog, audit],
        lastUpdatedAt: input.now,
    };
    storeFor(input.evidenceDir).record(input.runId, next);
    return next;
};
/**
 * Append an append-only `edit` audit entry for an inline candidate edit. Review state is NOT
 * transitioned — `fromState`/`toState` are the candidate's existing review state. Persists and
 * returns the updated review artifact (created empty on first use).
 */
export const appendEditAudit = (input) => {
    const current = loadRunReviewState(input.runId, input.evidenceDir) ?? emptyArtifact(input.runId, input.now);
    const state = candidateReviewStateOf(current, input.candidateId);
    const audit = {
        at: input.now,
        action: "edit",
        scope: "candidate",
        candidateId: input.candidateId,
        reviewerLabel: input.reviewerLabel,
        fromState: state,
        toState: state,
    };
    const next = {
        ...current,
        auditLog: [...current.auditLog, audit],
        lastUpdatedAt: input.now,
    };
    storeFor(input.evidenceDir).record(input.runId, next);
    return next;
};
