/**
 * unit-lineage.js - autonomous unit worker/session lineage helpers.
 *
 * Purpose: preserve Droid's useful worker-session accounting so SF can explain
 * which sessions claimed, ran, completed, or failed a unit.
 *
 * Consumer: autonomous dispatch projections, progress events, and future
 * recovery screens that need lineage without parsing logs.
 */

const LINEAGE_STATUSES = new Set([
	"selected",
	"started",
	"completed",
	"failed",
	"cancelled",
	"blocked",
]);

function stringOrNull(value) {
	return typeof value === "string" && value.trim().length > 0
		? value.trim()
		: null;
}

function uniqueStringList(value) {
	const values = Array.isArray(value) ? value : [];
	return [...new Set(values.map(stringOrNull).filter((item) => item !== null))];
}

/**
 * Normalize a unit lineage record.
 *
 * Purpose: keep worker-session lineage deterministic and deduplicated before
 * it is projected into runtime state or machine progress events.
 *
 * Consumer: recordUnitLineageEvent and future dispatch projection writers.
 */
export function normalizeUnitLineage(record = {}) {
	const workerSessionIds = uniqueStringList(record.workerSessionIds);
	const currentWorkerSessionId = stringOrNull(record.currentWorkerSessionId);
	const completedWorkerSessionId = stringOrNull(
		record.completedWorkerSessionId,
	);
	const failedWorkerSessionIds = uniqueStringList(
		record.failedWorkerSessionIds,
	);
	if (
		currentWorkerSessionId &&
		!workerSessionIds.includes(currentWorkerSessionId)
	) {
		workerSessionIds.push(currentWorkerSessionId);
	}
	if (
		completedWorkerSessionId &&
		!workerSessionIds.includes(completedWorkerSessionId)
	) {
		workerSessionIds.push(completedWorkerSessionId);
	}
	for (const id of failedWorkerSessionIds) {
		if (!workerSessionIds.includes(id)) workerSessionIds.push(id);
	}
	return {
		unitType: stringOrNull(record.unitType),
		unitId: stringOrNull(record.unitId),
		status: LINEAGE_STATUSES.has(record.status) ? record.status : "selected",
		workerSessionIds,
		currentWorkerSessionId,
		completedWorkerSessionId,
		failedWorkerSessionIds,
		events: Array.isArray(record.events) ? record.events : [],
	};
}

/**
 * Append one worker/session lineage event to a unit lineage record.
 *
 * Purpose: model Droid-style worker lifecycle accounting as structured state
 * while leaving persistence and transport to SF's existing DB/journal layers.
 *
 * Consumer: future autonomous dispatch hooks and tests.
 *
 * @param record - existing lineage record
 * @param event - event to record. Supports: status, workerSessionId, spawnId,
 *   note, stderr (truncated to 1000 chars if provided). The stderr field carries
 *   spawn-failure diagnostic content from the parallel orchestrator watchdog.
 */
export function recordUnitLineageEvent(record = {}, event = {}) {
	const current = normalizeUnitLineage(record);
	const status = LINEAGE_STATUSES.has(event.status) ? event.status : "selected";
	const workerSessionId = stringOrNull(event.workerSessionId);
	// Truncate stderr to 1000 chars to keep lineage records bounded.
	const stderr =
		typeof event.stderr === "string" && event.stderr.length > 0
			? event.stderr.slice(-1000)
			: null;
	const next = {
		...current,
		unitType: stringOrNull(event.unitType) ?? current.unitType,
		unitId: stringOrNull(event.unitId) ?? current.unitId,
		status,
		events: [
			...current.events,
			{
				ts: event.ts ?? new Date().toISOString(),
				status,
				workerSessionId,
				spawnId: stringOrNull(event.spawnId),
				note: stringOrNull(event.note),
				...(stderr ? { stderr } : {}),
			},
		],
	};
	if (workerSessionId && !next.workerSessionIds.includes(workerSessionId)) {
		next.workerSessionIds.push(workerSessionId);
	}
	if (status === "started") next.currentWorkerSessionId = workerSessionId;
	if (status === "completed") {
		next.completedWorkerSessionId = workerSessionId;
		next.currentWorkerSessionId = null;
	}
	if (status === "failed" && workerSessionId) {
		next.failedWorkerSessionIds = uniqueStringList([
			...next.failedWorkerSessionIds,
			workerSessionId,
		]);
		next.currentWorkerSessionId = null;
	}
	if (status === "blocked" || status === "cancelled") {
		next.currentWorkerSessionId = null;
	}
	return normalizeUnitLineage(next);
}
