/**
 * unit-handoff.js - structured autonomous unit closeout schema.
 *
 * Purpose: preserve the useful Droid closeout fields as a small, validated
 * contract that later units can consume without parsing prose summaries.
 *
 * Consumer: SF autonomous unit finalizers and follow-on unit prompts that need
 * a durable summary of what the previous unit changed, verified, and left open.
 */

const ARRAY_FIELDS = [
	"filesChanged",
	"testsAdded",
	"commandsRun",
	"knownFailures",
	"leftUndone",
	"fulfilledAssertions",
];

const STRING_ARRAY_FIELDS = new Set([
	"filesChanged",
	"testsAdded",
	"knownFailures",
	"leftUndone",
	"fulfilledAssertions",
]);

const COMMAND_STATUSES = new Set([
	"passed",
	"failed",
	"skipped",
	"not-run",
	"unknown",
]);

const VERIFICATION_STATUSES = new Set([
	"passed",
	"failed",
	"partial",
	"blocked",
	"not-run",
	"unknown",
]);

function isPlainObject(value) {
	return (
		value !== null &&
		typeof value === "object" &&
		(Object.getPrototypeOf(value) === Object.prototype ||
			Object.getPrototypeOf(value) === null)
	);
}

function normalizeString(value) {
	return typeof value === "string" ? value.trim() : "";
}

function normalizeStringArray(values) {
	return values.map((value) => value.trim()).filter(Boolean);
}

function normalizeCommandEntry(entry) {
	const normalized = {
		command: entry.command.trim(),
		status: normalizeString(entry.status) || "unknown",
	};
	if ("exitCode" in entry) {
		normalized.exitCode = entry.exitCode;
	}
	if ("cwd" in entry && normalizeString(entry.cwd)) {
		normalized.cwd = entry.cwd.trim();
	}
	if ("notes" in entry && normalizeString(entry.notes)) {
		normalized.notes = entry.notes.trim();
	}
	if ("durationMs" in entry) {
		normalized.durationMs = entry.durationMs;
	}
	return normalized;
}

function validateCommandEntry(entry, index, errors) {
	if (!isPlainObject(entry)) {
		errors.push(`commandsRun[${index}] must be an object`);
		return;
	}
	if (typeof entry.command !== "string" || entry.command.trim() === "") {
		errors.push(`commandsRun[${index}].command must be a non-empty string`);
	}
	if (
		"status" in entry &&
		(typeof entry.status !== "string" || !COMMAND_STATUSES.has(entry.status))
	) {
		errors.push(
			`commandsRun[${index}].status must be one of ${Array.from(COMMAND_STATUSES).join(", ")}`,
		);
	}
	if (
		"exitCode" in entry &&
		(!Number.isInteger(entry.exitCode) || entry.exitCode < 0)
	) {
		errors.push(
			`commandsRun[${index}].exitCode must be a non-negative integer`,
		);
	}
	if ("cwd" in entry && typeof entry.cwd !== "string") {
		errors.push(`commandsRun[${index}].cwd must be a string`);
	}
	if ("notes" in entry && typeof entry.notes !== "string") {
		errors.push(`commandsRun[${index}].notes must be a string`);
	}
	if (
		"durationMs" in entry &&
		(!Number.isFinite(entry.durationMs) || entry.durationMs < 0)
	) {
		errors.push(
			`commandsRun[${index}].durationMs must be a non-negative number`,
		);
	}
}

/**
 * Validate a candidate unit handoff and return every schema violation.
 *
 * Purpose: fail malformed closeout payloads before they become durable unit
 * state that future agents trust as verified work history.
 *
 * Consumer: normalizeUnitHandoff and any caller that wants to show structured
 * validation errors without throwing.
 */
export function validateUnitHandoff(candidate) {
	const errors = [];
	if (!isPlainObject(candidate)) {
		return {
			ok: false,
			errors: ["handoff must be an object"],
		};
	}

	if ("summary" in candidate && typeof candidate.summary !== "string") {
		errors.push("summary must be a string");
	}
	if (
		"verificationStatus" in candidate &&
		(typeof candidate.verificationStatus !== "string" ||
			!VERIFICATION_STATUSES.has(candidate.verificationStatus))
	) {
		errors.push(
			`verificationStatus must be one of ${Array.from(VERIFICATION_STATUSES).join(", ")}`,
		);
	}

	for (const field of ARRAY_FIELDS) {
		if (field in candidate && !Array.isArray(candidate[field])) {
			errors.push(`${field} must be an array`);
		}
	}

	for (const field of STRING_ARRAY_FIELDS) {
		if (!Array.isArray(candidate[field])) {
			continue;
		}
		candidate[field].forEach((entry, index) => {
			if (typeof entry !== "string") {
				errors.push(`${field}[${index}] must be a string`);
			}
		});
	}

	if (Array.isArray(candidate.commandsRun)) {
		candidate.commandsRun.forEach((entry, index) => {
			validateCommandEntry(entry, index, errors);
		});
	}

	return {
		ok: errors.length === 0,
		errors,
	};
}

/**
 * Normalize a validated unit handoff into SF's canonical closeout shape.
 *
 * Purpose: give autonomous follow-on units a predictable structured payload
 * instead of letting missing fields, whitespace, or prose-only closeouts drift.
 *
 * Consumer: autonomous unit closeout writers before persisting handoff records
 * and prompt builders before injecting prior-unit context.
 */
export function normalizeUnitHandoff(candidate = {}) {
	const validation = validateUnitHandoff(candidate);
	if (!validation.ok) {
		throw new TypeError(
			`Invalid unit handoff: ${validation.errors.join("; ")}`,
		);
	}

	return {
		summary: normalizeString(candidate.summary),
		filesChanged: normalizeStringArray(candidate.filesChanged ?? []),
		testsAdded: normalizeStringArray(candidate.testsAdded ?? []),
		commandsRun: (candidate.commandsRun ?? []).map(normalizeCommandEntry),
		knownFailures: normalizeStringArray(candidate.knownFailures ?? []),
		leftUndone: normalizeStringArray(candidate.leftUndone ?? []),
		fulfilledAssertions: normalizeStringArray(
			candidate.fulfilledAssertions ?? [],
		),
		verificationStatus:
			normalizeString(candidate.verificationStatus) || "unknown",
	};
}
