#!/usr/bin/env node
// Local-only. Never invoked from CI. Re-run intentionally; commit the
// resulting JSON in one atomic commit.
//
// Captures the current /api/network response into the canonical fixture used
// by buildLayout.test.ts. Asserts a minimum shape so a silently-empty backend
// can't write a vacuous fixture.

import { writeFileSync, mkdirSync } from 'node:fs';
import { dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { randomUUID } from 'node:crypto';

import { findAc3Candidates } from '../src/lib/person/genealogy/__fixtures__/findAc3Candidates.mjs';

const BACKEND_URL = process.env.BACKEND_URL ?? 'http://localhost:8080';
const EMAIL = process.env.CAPTURE_EMAIL;
const PASSWORD = process.env.CAPTURE_PASSWORD;

// Preflight guards: this script writes the canonical Stammbaum fixture from
// a *running* backend with admin-shaped credentials. Two slips would be
// silent disasters — running with default creds against staging/prod, or
// running with a typo'd BACKEND_URL that happens to resolve. Refuse both
// before sending a single byte.
preflight();

function preflight() {
	const failures = [];
	if (!EMAIL) {
		failures.push('CAPTURE_EMAIL must be set explicitly (no default).');
	}
	if (!PASSWORD) {
		failures.push('CAPTURE_PASSWORD must be set explicitly (no default).');
	}
	if (!isLocalhost(BACKEND_URL)) {
		failures.push(
			`BACKEND_URL must point at localhost / 127.0.0.1 (got: ${BACKEND_URL}). ` +
				'This script is local-only.'
		);
	}
	if (failures.length > 0) {
		console.error('Preflight failed:');
		for (const f of failures) console.error(`  - ${f}`);
		process.exit(2);
	}
}

function isLocalhost(url) {
	try {
		const host = new URL(url).hostname;
		return host === 'localhost' || host === '127.0.0.1' || host === '::1';
	} catch {
		return false;
	}
}

const HERE = dirname(fileURLToPath(import.meta.url));
const FIXTURE_PATH = `${HERE}/../src/lib/person/genealogy/__fixtures__/stammbaum.json`;

// Sanity floors — calibrated against the canonical dataset (May 2026: 62
// nodes, 5 generations, 28 SPOUSE_OF edges, 1 multi-spouse person). The point
// is catching a silently empty backend or a structurally-regressed snapshot,
// not strict size validation; raise these only if the canonical graph grows
// substantially. The matching gates in
// src/lib/person/genealogy/__fixtures__/validateFixture.ts cover unit-test
// coverage; the floors are intentionally duplicated as numeric constants
// (the AC3 revisit predicate, by contrast, now lives in one shared module).
const MIN_NODES = 50;
const MIN_GENERATIONS = 5;
const MIN_SPOUSE_OF_EDGES = 1;
const MIN_MULTI_SPOUSE_PERSONS = 1;

function parseSetCookies(headers) {
	const out = new Map();
	const raw = headers.getSetCookie?.() ?? [];
	for (const line of raw) {
		const [pair] = line.split(';');
		const eq = pair.indexOf('=');
		if (eq < 0) continue;
		out.set(pair.slice(0, eq).trim(), pair.slice(eq + 1).trim());
	}
	return out;
}

function serialiseCookies(jar) {
	return [...jar.entries()].map(([k, v]) => `${k}=${v}`).join('; ');
}

async function login(jar) {
	const xsrf = randomUUID();
	jar.set('XSRF-TOKEN', xsrf);
	const res = await fetch(`${BACKEND_URL}/api/auth/login`, {
		method: 'POST',
		headers: {
			'Content-Type': 'application/json',
			'X-XSRF-TOKEN': xsrf,
			Cookie: serialiseCookies(jar)
		},
		body: JSON.stringify({ email: EMAIL, password: PASSWORD })
	});
	if (!res.ok) {
		throw new Error(`Login failed: ${res.status} ${await res.text()}`);
	}
	for (const [k, v] of parseSetCookies(res.headers)) jar.set(k, v);
}

async function fetchNetwork(jar) {
	const res = await fetch(`${BACKEND_URL}/api/network`, {
		headers: { Cookie: serialiseCookies(jar) }
	});
	if (!res.ok) {
		throw new Error(`GET /api/network failed: ${res.status} ${await res.text()}`);
	}
	return res.json();
}

function validate(network) {
	const nodes = Array.isArray(network.nodes) ? network.nodes : [];
	const edges = Array.isArray(network.edges) ? network.edges : [];
	const spouseEdges = edges.filter((e) => e.relationType === 'SPOUSE_OF');
	const generations = new Set(nodes.map((n) => n.generation).filter((g) => g != null));
	const multiSpousePersons = countMultiSpousePersons(spouseEdges);

	const failures = [];
	if (nodes.length < MIN_NODES) {
		failures.push(`expected >= ${MIN_NODES} nodes, got ${nodes.length}`);
	}
	if (generations.size < MIN_GENERATIONS) {
		failures.push(`expected >= ${MIN_GENERATIONS} distinct generations, got ${generations.size}`);
	}
	if (spouseEdges.length < MIN_SPOUSE_OF_EDGES) {
		failures.push(`expected >= ${MIN_SPOUSE_OF_EDGES} SPOUSE_OF edges, got ${spouseEdges.length}`);
	}
	if (multiSpousePersons < MIN_MULTI_SPOUSE_PERSONS) {
		failures.push(
			`expected >= ${MIN_MULTI_SPOUSE_PERSONS} person with multiple SPOUSE_OF edges, ` +
				`got ${multiSpousePersons}`
		);
	}
	if (failures.length > 0) {
		throw new Error(`Sanity gates failed:\n  - ${failures.join('\n  - ')}`);
	}

	return {
		nodes: nodes.length,
		edges: edges.length,
		spouseEdges: spouseEdges.length,
		generations: [...generations].sort((a, b) => a - b),
		multiSpousePersons
	};
}

function countMultiSpousePersons(spouseEdges) {
	const partners = new Map();
	for (const e of spouseEdges) {
		if (!e.personId || !e.relatedPersonId) continue;
		mapAddToSet(partners, e.personId, e.relatedPersonId);
		mapAddToSet(partners, e.relatedPersonId, e.personId);
	}
	let count = 0;
	for (const set of partners.values()) {
		if (set.size >= 2) count += 1;
	}
	return count;
}

function mapAddToSet(map, key, value) {
	const s = map.get(key);
	if (s) s.add(value);
	else map.set(key, new Set([value]));
}

function writeFixture(network) {
	mkdirSync(dirname(FIXTURE_PATH), { recursive: true });
	writeFileSync(FIXTURE_PATH, JSON.stringify(network, null, '\t') + '\n', 'utf8');
}

async function main() {
	const jar = new Map();
	console.error(`Capturing /api/network from ${BACKEND_URL} as ${EMAIL} ...`);
	await login(jar);
	const network = await fetchNetwork(jar);
	const stats = validate(network);
	writeFixture(network);
	console.error(
		`Wrote ${FIXTURE_PATH}\n  nodes: ${stats.nodes}\n  edges: ${stats.edges}\n  SPOUSE_OF edges: ${stats.spouseEdges}\n  multi-spouse persons: ${stats.multiSpousePersons}\n  generations: G${stats.generations.join(', G')}`
	);
	warnIfAc3Reachable(network);
}

function warnIfAc3Reachable(network) {
	const candidates = findAc3Candidates(network);
	if (candidates.length === 0) return;
	// Soft, non-blocking. The ADR-026 deferral decision says: revisit the
	// dagre adoption choice as soon as the canonical fixture starts to
	// contain a parented unseeded spouse. This warning is that signal —
	// fail-open so a recapture still writes the fixture (the human needs
	// to see the data to decide), but loud enough that nobody can miss it.
	console.error('');
	console.error(
		`⚠  AC3 revisit trigger reached (ADR-026): ${candidates.length} unseeded ` +
			'person(s) with a seeded parent AND a SPOUSE_OF edge in the captured graph.'
	);
	console.error(`   Candidates: ${candidates.join(', ')}`);
	console.error(
		'   Action: re-evaluate the dagre adoption deferral. See ADR-026 §Notes ' +
			'for the assessor + cadence.'
	);
}

main().catch((err) => {
	console.error(err.message);
	process.exit(1);
});
