/**
 * uok/purpose-coherence.js — pure verdict logic for the
 * `purpose-coherence-gate` pre-dispatch gate.
 *
 * Doctrine (ADR-0000): SF is a purpose-to-software compiler. Before every
 * autonomous dispatch we walk the purpose chain — milestone vision →
 * slice goal trace → task purpose trace — and refuse to dispatch when
 * the chain is broken at the milestone-vision root (degraded-vision).
 *
 * Restoration, not new work: the eight-PDD purpose gate ADR-0000
 * already requires this; the autonomous loop just stopped enforcing it.
 *
 * # Outcome ladder
 *
 *   - "fail" — milestone vision missing/empty. SF cannot dispatch
 *     without a purpose root; this is a hard stop.
 *   - "warn" — vision present, but the slice or task trace columns
 *     (added by parallel agents in P2/P3) are missing or NULL. We
 *     surface the gap to the operator but allow dispatch so legacy
 *     DBs / pre-migration projects don't hard-break on day one. This
 *     is the documented downgrade path in the parent contract.
 *   - "pass" — full chain present (vision + slice traces vision +
 *     task traces slice). No active slice / task is also a "pass":
 *     the gate is permissive when there is nothing to trace yet
 *     (e.g. dispatch is about to create the slice).
 *
 * # Schema-coupling guard
 *
 * `slice.traces_vision_fragment` lands in P2 and `task.purpose_trace`
 * lands in P3 — both in parallel to this gate. The gate must run
 * cleanly against legacy DBs from before those migrations apply. We
 * detect the missing-column case via the better-sqlite3 "no such
 * column" error and treat it the same as a NULL value (warn, do not
 * fail). The columnExists() helper that schema migrations use sits in
 * sf-db-schema.js and is not exported; we do not import it here both
 * to avoid coupling and because catching the SqliteError is sufficient
 * and survives future column moves.
 */

/**
 * Compute the verdict for the purpose-coherence-gate, given a fetcher
 * that already knows how to read the relevant rows.
 *
 * Pure function for unit-testability. The runtime adapter
 * (evaluatePurposeCoherence below) wires this up to sf-db.
 *
 * @param {object} input
 * @param {{ id: string; vision?: string | null } | null | undefined} input.milestone
 * @param {{ id: string; traces_vision_fragment?: string | null } | null | undefined} input.slice
 * @param {{ id: string; purpose_trace?: string | null } | null | undefined} input.task
 * @param {{ sliceTraceColumnMissing?: boolean; taskTraceColumnMissing?: boolean }} [input.schema]
 *
 * @returns {{
 *   outcome: "pass" | "warn" | "fail",
 *   failureClass: "none" | "policy",
 *   rationale: string,
 *   findings?: string,
 * }}
 */
export function computePurposeCoherenceVerdict(input) {
	const milestone = input?.milestone ?? null;
	const slice = input?.slice ?? null;
	const task = input?.task ?? null;
	const schema = input?.schema ?? {};

	// 1. Milestone vision is the purpose root. Without it, SF cannot dispatch.
	const vision =
		typeof milestone?.vision === "string" ? milestone.vision.trim() : "";
	if (!milestone || vision.length === 0) {
		return {
			outcome: "fail",
			failureClass: "policy",
			rationale: "milestone vision missing; SF cannot dispatch without purpose",
			findings: milestone?.id
				? `milestone=${milestone.id} vision=<empty>`
				: "no active milestone",
		};
	}

	// 2. Slice trace. If slice is active, it should trace back to a vision
	//    fragment. NULL or missing column → warn (downgrade path for
	//    pre-migration DBs and pre-restoration slices).
	if (slice) {
		const sliceTrace =
			typeof slice.traces_vision_fragment === "string"
				? slice.traces_vision_fragment.trim()
				: "";
		if (schema.sliceTraceColumnMissing) {
			return {
				outcome: "warn",
				failureClass: "policy",
				rationale:
					"slice.traces_vision_fragment column not yet present in this DB; allowing dispatch but the purpose chain is unverified",
				findings: `milestone=${milestone.id} slice=${slice.id} column=traces_vision_fragment status=missing`,
			};
		}
		if (sliceTrace.length === 0) {
			return {
				outcome: "warn",
				failureClass: "policy",
				rationale:
					"slice goal does not trace to milestone vision (run plan-milestone with vision-aware prompt)",
				findings: `milestone=${milestone.id} slice=${slice.id} traces_vision_fragment=<null>`,
			};
		}
	}

	// 3. Task trace. Same downgrade for missing column / NULL.
	if (task) {
		const taskTrace =
			typeof task.purpose_trace === "string" ? task.purpose_trace.trim() : "";
		if (schema.taskTraceColumnMissing) {
			return {
				outcome: "warn",
				failureClass: "policy",
				rationale:
					"task.purpose_trace column not yet present in this DB; allowing dispatch but the purpose chain is unverified",
				findings: `milestone=${milestone.id} task=${task.id} column=purpose_trace status=missing`,
			};
		}
		if (taskTrace.length === 0) {
			return {
				outcome: "warn",
				failureClass: "policy",
				rationale: "task does not trace to slice goal",
				findings: `milestone=${milestone.id} task=${task.id} purpose_trace=<null>`,
			};
		}
	}

	return {
		outcome: "pass",
		failureClass: "none",
		rationale: "purpose chain intact (milestone vision → slice → task)",
	};
}

function isMissingColumnError(err, columnName) {
	if (!err) return false;
	const msg = String(err.message ?? err);
	return msg.includes("no such column") && msg.includes(columnName);
}

/**
 * Read a single column for a slice; return { value, columnMissing }.
 *
 * Best-effort wrapper that distinguishes "column doesn't exist on this
 * legacy DB" from "row missing entirely" from "value is NULL". The gate
 * only differentiates the column-missing case; the rest collapse to
 * NULL semantics in computePurposeCoherenceVerdict.
 */
function readSliceTrace(db, milestoneId, sliceId) {
	try {
		const row = db
			.prepare(
				"SELECT traces_vision_fragment FROM slices WHERE milestone_id = :mid AND id = :sid",
			)
			.get({ ":mid": milestoneId, ":sid": sliceId });
		return {
			value: row?.["traces_vision_fragment"] ?? null,
			columnMissing: false,
		};
	} catch (err) {
		if (isMissingColumnError(err, "traces_vision_fragment")) {
			return { value: null, columnMissing: true };
		}
		throw err;
	}
}

function readTaskTrace(db, milestoneId, sliceId, taskId) {
	try {
		const row = db
			.prepare(
				"SELECT purpose_trace FROM tasks WHERE milestone_id = :mid AND slice_id = :sid AND id = :tid",
			)
			.get({ ":mid": milestoneId, ":sid": sliceId, ":tid": taskId });
		return { value: row?.["purpose_trace"] ?? null, columnMissing: false };
	} catch (err) {
		if (isMissingColumnError(err, "purpose_trace")) {
			return { value: null, columnMissing: true };
		}
		throw err;
	}
}

/**
 * Adapter from pre-dispatch state to a verdict. Reads the DB directly
 * for `vision` / `traces_vision_fragment` / `purpose_trace` because the
 * legacy row mappers (rowToMilestone/Slice/Task) don't surface the new
 * columns; this also lets us trap the missing-column SqliteError on
 * pre-P2/P3 databases without forcing a schema bump here.
 *
 * @param {object} opts
 * @param {{ activeMilestone: { id: string } | null,
 *           activeSlice: { id: string } | null,
 *           activeTask: { id: string } | null }} opts.state
 * @param {{ prepare: Function } | null | undefined} opts.db   better-sqlite3 handle
 */
export function evaluatePurposeCoherence(opts) {
	const state = opts?.state;
	if (!state) {
		return computePurposeCoherenceVerdict({ milestone: null });
	}
	const db = opts?.db ?? null;
	const milestoneId = state.activeMilestone?.id ?? null;
	const sliceId = state.activeSlice?.id ?? null;
	const taskId = state.activeTask?.id ?? null;

	if (!milestoneId || !db) {
		return computePurposeCoherenceVerdict({
			milestone: milestoneId ? { id: milestoneId, vision: null } : null,
		});
	}

	let milestoneRow = null;
	try {
		milestoneRow = db
			.prepare("SELECT id, vision FROM milestones WHERE id = :id")
			.get({ ":id": milestoneId });
	} catch {
		// If even the milestone row read fails (e.g. db closed mid-flight)
		// degrade to "no active milestone" — the outer pre-dispatch flow
		// will surface the underlying DB issue separately.
		milestoneRow = null;
	}

	let sliceInfo = null;
	let sliceColumnMissing = false;
	if (sliceId) {
		const { value, columnMissing } = readSliceTrace(db, milestoneId, sliceId);
		sliceInfo = { id: sliceId, traces_vision_fragment: value };
		sliceColumnMissing = columnMissing;
	}

	let taskInfo = null;
	let taskColumnMissing = false;
	if (sliceId && taskId) {
		const { value, columnMissing } = readTaskTrace(
			db,
			milestoneId,
			sliceId,
			taskId,
		);
		taskInfo = { id: taskId, purpose_trace: value };
		taskColumnMissing = columnMissing;
	}

	return computePurposeCoherenceVerdict({
		milestone: milestoneRow
			? { id: milestoneRow.id, vision: milestoneRow.vision }
			: { id: milestoneId, vision: null },
		slice: sliceInfo,
		task: taskInfo,
		schema: {
			sliceTraceColumnMissing: sliceColumnMissing,
			taskTraceColumnMissing: taskColumnMissing,
		},
	});
}

export const PURPOSE_COHERENCE_GATE_ID = "purpose-coherence-gate";
