// Domain-aware construction of the genealogy "family forest" (#724).
//
// This module owns every piece of genealogy knowledge the layout needs —
// spouse runs, birth-year ordering, structural-owner selection, intra-family
// marriage resolution and cross-links — and flattens it into the abstract
// { id, width, children } nodes that the domain-agnostic tidyTree.ts packs.
// buildLayout.ts orchestrates the two and maps run positions back to persons.

import type { components } from '$lib/generated/api';

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

/**
 * A family unit = one bloodline carrier (the primary) plus the spouse(s)
 * absorbed into its run. `members[0]` is the primary; the rest are spouses in
 * marriage-year order. `children` are the units anchored by the couple's
 * offspring (the forest hierarchy).
 */
export type Unit = {
	id: string; // == primary's id; also the tidyTree node id
	members: string[]; // run order: [primary, ...absorbed spouses]
	children: Unit[];
};

/**
 * A parent→child edge whose child is NOT positioned under that parent (the
 * child lives in a spouse's run elsewhere). `sameLevel` is true when the
 * displaced parent can be ordered adjacent to the child's anchor (a short,
 * solid connector — the intra-family "adjacency" case); false marks a genuine
 * cross-level link that must render with a distinct dash.
 */
export type CrossLink = { parentId: string; childId: string; sameLevel: boolean };

export type FamilyForest = { roots: Unit[]; crossLinks: CrossLink[] };

const ROOT_GROUP = '__ROOT__';

/**
 * Choose the structural owner of a couple: the spouse who keeps the bloodline
 * (hierarchy) position. Earlier birth year wins; a missing birth year sorts
 * last; ties break on the stable id. Shared by the cycle, cross-link and
 * intra-family paths so the rule is defined exactly once.
 */
export function pickStructuralOwner(
	a: { id: string; birthYear?: number | null },
	b: { id: string; birthYear?: number | null }
): string {
	const ra = ownerRank(a);
	const rb = ownerRank(b);
	if (ra !== rb) return ra < rb ? a.id : b.id;
	return a.id <= b.id ? a.id : b.id;
}

// Lower is "more owning". Missing birth year → +Infinity (sorts last).
function ownerRank(p: { birthYear?: number | null }): number {
	return p.birthYear == null ? Number.POSITIVE_INFINITY : p.birthYear;
}

/**
 * Build the family forest: assign every person to exactly one unit (a primary,
 * or a spouse absorbed into a primary's run), wire the parent/child hierarchy,
 * and record displaced parent edges as cross-links. The packer (tidyTree) and
 * the orchestrator (buildLayout) consume the result; this module holds all the
 * genealogy semantics.
 */
export function buildFamilyForest(nodes: PersonNodeDTO[], edges: RelationshipDTO[]): FamilyForest {
	const byId = new Map(nodes.map((n) => [n.id, n]));
	const allIds = new Set(nodes.map((n) => n.id));
	const parentToChildren = new Map<string, string[]>();
	const childToParents = new Map<string, string[]>();
	const spouses = new Map<string, Set<string>>();
	const spouseYear = new Map<string, number | undefined>();

	for (const e of edges) {
		// Unknown-id guard for BOTH edge types: an edge to an id outside the node
		// list is dropped, never dereferenced into an undefined position.
		if (!allIds.has(e.personId) || !allIds.has(e.relatedPersonId)) continue;
		if (e.relationType === 'PARENT_OF') {
			push(parentToChildren, e.personId, e.relatedPersonId);
			push(childToParents, e.relatedPersonId, e.personId);
		} else if (e.relationType === 'SPOUSE_OF') {
			addToSet(spouses, e.personId, e.relatedPersonId);
			addToSet(spouses, e.relatedPersonId, e.personId);
			spouseYear.set(
				pairKey(e.personId, e.relatedPersonId),
				e.fromDate ? Number(e.fromDate.slice(0, 4)) : undefined
			);
		}
	}

	const hasParents = (id: string) => (childToParents.get(id)?.length ?? 0) > 0;

	// --- Absorption: decide who is absorbed into whose run. ---
	const absorbedInto = new Map<string, string>();
	const runOwner = (id: string): string => {
		let cur = id;
		const seen = new Set([cur]);
		while (absorbedInto.has(cur)) {
			cur = absorbedInto.get(cur)!;
			if (seen.has(cur)) break; // defensive: never loop on a self-referential chain
			seen.add(cur);
		}
		return cur;
	};
	const owners = new Set<string>();
	const canAbsorb = (x: string) => !absorbedInto.has(x) && !owners.has(x);

	for (const [a, b] of marriagesInOrder(spouses)) {
		if (runOwner(a) === runOwner(b)) continue;
		const aP = hasParents(a);
		const bP = hasParents(b);
		let owner: string;
		let absorbed: string;
		if (aP === bP) {
			// Both parented (intra-family) or both parentless (dual-loose founders):
			// the structural owner keeps the bloodline position.
			owner = pickStructuralOwner(byId.get(a)!, byId.get(b)!);
			absorbed = owner === a ? b : a;
		} else if (aP) {
			owner = a;
			absorbed = b;
		} else {
			owner = b;
			absorbed = a;
		}
		if (canAbsorb(absorbed)) {
			const o = runOwner(owner);
			absorbedInto.set(absorbed, o);
			owners.add(o);
		} else if (canAbsorb(owner)) {
			const o = runOwner(absorbed);
			absorbedInto.set(owner, o);
			owners.add(o);
		}
		// else: both already fixed in other runs — the marriage still draws a
		// plain spouse line via the connector; no absorption is forced.
	}

	// --- Unit membership ---
	const unitOf = new Map<string, string>(); // person → primary id
	for (const n of nodes) unitOf.set(n.id, runOwner(n.id));
	const primaries = nodes.map((n) => n.id).filter((id) => unitOf.get(id) === id);

	const absorbedOf = new Map<string, string[]>();
	for (const k of absorbedInto.keys()) push(absorbedOf, runOwner(k), k);

	const membersOf = new Map<string, string[]>();
	for (const p of primaries) {
		const spousesOfP = (absorbedOf.get(p) ?? [])
			.slice()
			.sort((x, y) => spouseRun(p, x, y, spouseYear, byId));
		membersOf.set(p, [p, ...spousesOfP]);
	}

	// hierarchy parent unit of a primary: the unit of its structural-owner parent
	// (married co-parents share a unit anyway, so the choice is only material for
	// the rare unmarried-co-parent case).
	const hierParentUnit = (primary: string): string | null => {
		const parents = childToParents.get(primary) ?? [];
		if (parents.length === 0) return null;
		let chosen = parents[0];
		for (const pa of parents.slice(1))
			chosen = pickStructuralOwner(byId.get(chosen)!, byId.get(pa)!);
		const u = unitOf.get(chosen)!;
		return u === primary ? null : u; // guard: a self-parent edge cannot anchor
	};
	const groupKey = (primary: string) => hierParentUnit(primary) ?? ROOT_GROUP;

	// --- Build Unit objects + hierarchy ---
	const unitObj = new Map<string, Unit>();
	for (const p of primaries) unitObj.set(p, { id: p, members: membersOf.get(p)!, children: [] });
	const roots: Unit[] = [];
	for (const p of primaries) {
		const hp = hierParentUnit(p);
		if (hp == null) roots.push(unitObj.get(p)!);
		else unitObj.get(hp)!.children.push(unitObj.get(p)!);
	}

	// Sibling/branch order: birthYear ASC NULLS LAST → displayName → id. Applied
	// to roots and to every unit's children so x never depends on Map order.
	const branchOrder = (u: Unit, v: Unit) => {
		const a = byId.get(u.id)!;
		const b = byId.get(v.id)!;
		const ya = a.birthYear ?? Number.POSITIVE_INFINITY;
		const yb = b.birthYear ?? Number.POSITIVE_INFINITY;
		if (ya !== yb) return ya - yb;
		return a.displayName.localeCompare(b.displayName) || (u.id < v.id ? -1 : 1);
	};
	roots.sort(branchOrder);
	for (const u of unitObj.values()) u.children.sort(branchOrder);

	// --- Cross-links: displaced parent → absorbed-spouse edges. ---
	const crossLinks: CrossLink[] = [];
	for (const k of absorbedInto.keys()) {
		const owner = runOwner(k);
		const hu = hierParentUnit(owner);
		for (const pa of childToParents.get(k) ?? []) {
			const sameLevel = hu != null && groupKey(unitOf.get(pa)!) === groupKey(hu);
			crossLinks.push({ parentId: pa, childId: k, sameLevel });
		}
	}

	return { roots, crossLinks };
}

// Spouse-run comparator: marriage year ASC NULLS LAST, then displayName, then id.
function spouseRun(
	primary: string,
	x: string,
	y: string,
	spouseYear: Map<string, number | undefined>,
	byId: Map<string, PersonNodeDTO>
): number {
	const yx = spouseYear.get(pairKey(primary, x)) ?? Number.POSITIVE_INFINITY;
	const yy = spouseYear.get(pairKey(primary, y)) ?? Number.POSITIVE_INFINITY;
	if (yx !== yy) return yx - yy;
	const nx = byId.get(x)?.displayName ?? '';
	const ny = byId.get(y)?.displayName ?? '';
	return nx.localeCompare(ny) || (x < y ? -1 : 1);
}

// Unique undirected marriages in a deterministic (pair-key) order.
function marriagesInOrder(spouses: Map<string, Set<string>>): [string, string][] {
	const seen = new Set<string>();
	const out: [string, string][] = [];
	for (const a of [...spouses.keys()].sort()) {
		for (const b of [...spouses.get(a)!].sort()) {
			const k = pairKey(a, b);
			if (seen.has(k)) continue;
			seen.add(k);
			out.push(a < b ? [a, b] : [b, a]);
		}
	}
	out.sort((m1, m2) => (pairKey(m1[0], m1[1]) < pairKey(m2[0], m2[1]) ? -1 : 1));
	return out;
}

function push<K, V>(map: Map<K, V[]>, key: K, value: V) {
	const arr = map.get(key);
	if (arr) arr.push(value);
	else map.set(key, [value]);
}

function addToSet<K, V>(map: Map<K, Set<V>>, key: K, value: V) {
	const s = map.get(key);
	if (s) s.add(value);
	else map.set(key, new Set([value]));
}

function pairKey(a: string, b: string): string {
	return a < b ? `${a}|${b}` : `${b}|${a}`;
}

export type { PersonNodeDTO };
