/**
 * Lineage highlighting for the Stammbaum (#703).
 *
 * Pure, DOM-free traversal over the family graph: given the relationship edges
 * and a selected root person, it returns the set of people whose nodes stay at
 * full strength (the root's full pedigree upward, full descendant tree downward,
 * and every spouse of those blood people) plus a predicate that decides whether
 * a connector between two people is active. Everyone and every connector outside
 * the active set is rendered dimmed by the presentation layer.
 *
 * Kept beside `buildLayout.ts` and free of Svelte/DOM so it is unit-testable in
 * the node project and the components stay presentation-only.
 */
import type { components } from '$lib/generated/api';

type RelationshipDTO = components['schemas']['RelationshipDTO'];

/**
 * Opacity applied to dimmed node outlines/labels and connectors. 0.45 keeps names
 * legible against bg-surface in both themes (dark mode dims already-light mint, the
 * riskier case) while clearly de-emphasised, and works as a lightness cue so the cue
 * does not rely on hue (WCAG 1.4.1 / NFR-A11Y-001). The dim is applied to the node's
 * outline + labels only — the card fill stays opaque, see StammbaumNode.svelte.
 */
export const DIMMED_OPACITY = 0.45;

/** Adjacency index over the family graph, built once per edge set. */
export type LineageIndex = {
	parentToChildren: Map<string, string[]>;
	childToParents: Map<string, string[]>;
	spouses: Map<string, Set<string>>;
};

export type LineageHighlight = {
	/** Ids of people rendered at full strength while the root is selected. */
	active: Set<string>;
	/** A connector is active only when both people it joins are active. */
	isConnectorActive: (aId: string, bId: string) => boolean;
};

function pushTo(map: Map<string, string[]>, key: string, value: string): void {
	const list = map.get(key);
	if (list) list.push(value);
	else map.set(key, [value]);
}

function addSpouse(map: Map<string, Set<string>>, a: string, b: string): void {
	const set = map.get(a);
	if (set) set.add(b);
	else map.set(a, new Set([b]));
}

/**
 * Build the adjacency index from the raw edges. Only `PARENT_OF` and `SPOUSE_OF`
 * shape a bloodline; `SIBLING_OF` and the social relation types are ignored, so a
 * sibling never enters the active set. Spouse pairs are stored symmetrically.
 */
export function buildLineageIndex(edges: RelationshipDTO[]): LineageIndex {
	const parentToChildren = new Map<string, string[]>();
	const childToParents = new Map<string, string[]>();
	const spouses = new Map<string, Set<string>>();

	for (const edge of edges) {
		if (edge.relationType === 'PARENT_OF') {
			pushTo(parentToChildren, edge.personId, edge.relatedPersonId);
			pushTo(childToParents, edge.relatedPersonId, edge.personId);
		} else if (edge.relationType === 'SPOUSE_OF') {
			addSpouse(spouses, edge.personId, edge.relatedPersonId);
			addSpouse(spouses, edge.relatedPersonId, edge.personId);
		}
	}

	return { parentToChildren, childToParents, spouses };
}

/**
 * Collect every id reachable from `start` along `adjacency`, into `into`. The
 * `into.has(id)` check doubles as the visited guard, so a cyclic `PARENT_OF`
 * chain terminates instead of recursing unbounded (REQ-STAMMBAUM-04 / AC10).
 */
function collectReachable(
	start: string,
	adjacency: Map<string, string[]>,
	into: Set<string>
): void {
	// Seed with the start's neighbours and add the start unconditionally: the
	// start may already be in `into` from an earlier walk (the root is shared by
	// the ancestor and descendant passes), and a pre-visited start must still be
	// expanded rather than short-circuited.
	into.add(start);
	const stack = [...(adjacency.get(start) ?? [])];
	while (stack.length > 0) {
		const id = stack.pop()!;
		if (into.has(id)) continue;
		into.add(id);
		for (const next of adjacency.get(id) ?? []) stack.push(next);
	}
}

/**
 * Compute the active set for the selected `rootId`: the root, all ancestors, all
 * descendants, and every spouse of those blood people (spouses are active leaves
 * — we never climb into a married-in spouse's own bloodline).
 */
export function highlightLineage(index: LineageIndex, rootId: string): LineageHighlight {
	const blood = new Set<string>();
	collectReachable(rootId, index.childToParents, blood);
	collectReachable(rootId, index.parentToChildren, blood);

	const active = new Set(blood);
	for (const id of blood) {
		for (const spouse of index.spouses.get(id) ?? []) active.add(spouse);
	}

	return {
		active,
		isConnectorActive: (aId, bId) => active.has(aId) && active.has(bId)
	};
}
