// AC3 = "unseeded loose spouse whose parents are in the graph" (ADR-026).
// Returns the IDs of every captured person matching the predicate. A non-
// empty result is the documented revisit trigger for the dagre deferral
// decision: the canonical-fixture unit suite asserts an empty result against
// the committed JSON, and `frontend/scripts/capture-network-fixture.mjs`
// soft-warns to stderr (does NOT fail capture) so a human-in-the-loop notices
// when the layout branch becomes reachable.
//
// Lives as a plain ESM .mjs module so both the Node-run capture script and
// the TypeScript validator/test suite can import it without a build step —
// single source of truth for the predicate.

/**
 * @typedef {{ relationType?: string, personId?: string, relatedPersonId?: string }} Edge
 * @typedef {{ id?: string, generation?: number | null }} Node
 * @typedef {{ nodes?: Node[], edges?: Edge[] }} NetworkShape
 */

/**
 * @param {NetworkShape} network
 * @returns {string[]}
 */
export function findAc3Candidates(network) {
	const nodes = Array.isArray(network.nodes) ? network.nodes : [];
	const edges = Array.isArray(network.edges) ? network.edges : [];
	const generationById = new Map();
	for (const n of nodes) {
		if (n.id) generationById.set(n.id, n.generation);
	}

	const hasSpouseEdge = new Set();
	const seededParentByChild = new Map();
	for (const e of edges) {
		if (!e.personId || !e.relatedPersonId) continue;
		if (e.relationType === 'SPOUSE_OF') {
			hasSpouseEdge.add(e.personId);
			hasSpouseEdge.add(e.relatedPersonId);
			continue;
		}
		if (e.relationType === 'PARENT_OF') {
			const parentGen = generationById.get(e.personId);
			if (parentGen != null) seededParentByChild.set(e.relatedPersonId, true);
		}
	}

	const matches = [];
	for (const n of nodes) {
		if (!n.id) continue;
		if (n.generation != null) continue;
		if (!seededParentByChild.get(n.id)) continue;
		if (!hasSpouseEdge.has(n.id)) continue;
		matches.push(n.id);
	}
	return matches;
}
