<script lang="ts">
	import { tick } from 'svelte';
	import type { Node, Edge } from '@xyflow/svelte';
	import { useNodes, useEdges } from '@xyflow/svelte';
	import { useSvelteFlow } from '@xyflow/svelte';
	import { breakpointsState } from '$lib/state/breakpoints.svelte';
	import { viewState } from '$lib/state/view.svelte';

	let { initialNodes }: { initialNodes: Node[] } = $props();

	const DEFAULT_WIDTH = breakpointsState.isMobile
		? typeof window !== 'undefined'
			? window.innerWidth - 32
			: 390
		: 600;
	const DEFAULT_HEIGHT = 100;
	const H_SPACING = 60;
	const V_SPACING = 60;

	const { fitView } = useSvelteFlow();
	const nodesStore = useNodes();
	const edgesStore = useEdges();

	let lastLayoutKey = $state<string | null>(null);
	let isFirstLayout = $state(true);
	let lastDraggable = $state(viewState.draggable);
	let lastFitViewRequest = $state(0);

	// ─── Helpers ────────────────────────────────────────────────────────

	function getMeasuredHeight(node: Node): number {
		return node.measured?.height ?? DEFAULT_HEIGHT;
	}

	function getMeasuredWidth(node: Node): number {
		return node.measured?.width ?? DEFAULT_WIDTH;
	}

	/** Side children are placed to the RIGHT of their parent (same row). */
	function isSideChild(nodeId: string, nodeMap: Map<string, Node>): boolean {
		return nodeMap.get(nodeId)?.type === 'user-follow-up';
	}

	// ─── Layout key (triggers re-layout on structural / measurement changes) ───

	const layoutKey = $derived(
		(() => {
			const nodes = nodesStore.current;
			const edges = edgesStore.current;
			const ns = nodes.length === 0 ? initialNodes : nodes;
			const es = nodes.length === 0 ? [] : edges;
			const nodeIds = ns
				.map((n) => n.id)
				.sort()
				.join(',');
			const edgeKeys = es
				.map((e) => `${e.source}-${e.target}`)
				.sort()
				.join(',');
			const dims = ns
				.map((n) => `${n.id}:${n.measured?.width ?? 0}x${n.measured?.height ?? 0}`)
				.sort()
				.join(',');
			return `${ns.length}-${es.length}-${nodeIds}-${edgeKeys}-${dims}`;
		})()
	);

	// ─── Fit view helper ────────────────────────────────────────────────

	function doFitView(opts?: { animate?: boolean; forceAnimate?: boolean; force?: boolean }) {
		if (!viewState.draggable && !opts?.force) return;
		const nodes = nodesStore.current;
		if (nodes.length === 0) return;

		const shouldAnimate = opts?.forceAnimate || (opts?.animate !== false && !isFirstLayout);
		fitView({
			maxZoom: breakpointsState.isMobile ? 1 : 1.15,
			minZoom: 0.7,
			padding: breakpointsState.isMobile ? 0 : 0.15,
			...(shouldAnimate ? { interpolate: 'smooth' as const, duration: 250 } : {}),
			nodes: nodes.map((n) => ({ id: n.id }))
		});
	}

	$effect(() => {
		const key = layoutKey;
		if (key === lastLayoutKey) return;
		lastLayoutKey = key;

		const nodes = nodesStore.current;
		const edges = edgesStore.current;
		const ns = nodes.length === 0 ? initialNodes : nodes;
		const es = nodes.length === 0 ? [] : edges;

		runLayout(ns, es);
	});

	// When draggable switches to true: fitView after DOM settles (no blink)
	$effect(() => {
		const draggable = viewState.draggable;
		if (draggable === lastDraggable) return;
		lastDraggable = draggable;
		if (!draggable) return;
		tick().then(() => {
			requestAnimationFrame(() => doFitView({ forceAnimate: true }));
		});
	});

	// Respond to explicit fitView requests (e.g. button click in PanelCanvasActions)
	$effect(() => {
		const req = viewState.fitViewRequest;
		if (req === 0 || req === lastFitViewRequest) return;
		lastFitViewRequest = req;
		tick().then(() => {
			requestAnimationFrame(() => doFitView({ forceAnimate: true, force: true }));
		});
	});

	// ─── Layout algorithm ──────────────────────────────────────────────
	//
	// Two-phase block-based tree layout that guarantees zero overlap.
	//
	// Each node's subtree is split into two non-overlapping horizontal zones:
	//
	//   Block A (left)  — user centered above assistants, shared user centered below.
	//   Block B (right) — side-children (user-follow-up) and their full subtrees.
	//
	//   ┌──────────────Block A──────────────┐  ┌─────Block B─────┐
	//   │            ┌────────┐             │  │  ┌────────────┐ │
	//   │            │  USER  │             │  │  │  Side node │ │
	//   │            └────────┘             │  │  └────────────┘ │
	//   │  ┌──────────┐  ┌──────────┐       │  │  ┌──────────┐   │
	//   │  │  ASS 1   │  │  ASS 2   │       │  │  │ Side sub │   │
	//   │  └──────────┘  └──────────┘       │  │  └──────────┘   │
	//   │            ┌────────┐             │  └─────────────────┘
	//   │            │  USER  │             │
	//   │            └────────┘             │
	//   └───────────────────────────────────┘
	//
	// Phase 1 (bottom-up): compute the bounding-box extent of every subtree.
	// Phase 2 (top-down):  assign positions, centering parents above children.
	//

	type Extent = {
		/** Total bounding-box width of the entire subtree. */
		width: number;
		/** Total bounding-box height of the entire subtree. */
		height: number;
		/** Width of Block A only (node + below-children column). */
		blockAWidth: number;
	};

	function computeLayout(nodes: Node[], edges: Edge[]): Node[] {
		if (nodes.length === 0) return [];

		const nodeMap = new Map(nodes.map((n) => [n.id, n]));

		// Build parent → children adjacency from edges
		const childrenOf = new Map<string, string[]>();
		const parentsOf = new Map<string, string[]>();
		const hasParent = new Set<string>();

		for (const e of edges) {
			hasParent.add(e.target);
			const list = childrenOf.get(e.source) ?? [];
			list.push(e.target);
			childrenOf.set(e.source, list);
			const parents = parentsOf.get(e.target) ?? [];
			parents.push(e.source);
			parentsOf.set(e.target, parents);
		}

		// Move shared children (user node with multiple assistant parents) to grandparent.
		// They get placed centered below the assistants row.
		const sharedBelowOf = new Map<string, string[]>();
		for (const [childId, parents] of parentsOf) {
			if (parents.length < 2) continue;
			// All parents must share the same parent (grandparent)
			const firstParent = parents[0];
			const grandparentId = (parentsOf.get(firstParent) ?? [])[0];
			if (!grandparentId || !parents.every((p) => (parentsOf.get(p) ?? []).includes(grandparentId)))
				continue;
			const gpChildren = childrenOf.get(grandparentId) ?? [];
			if (!parents.every((p) => gpChildren.includes(p))) continue;
			// Remove child from each parent
			for (const p of parents) {
				const list = childrenOf.get(p) ?? [];
				childrenOf.set(
					p,
					list.filter((id) => id !== childId)
				);
			}
			const list = sharedBelowOf.get(grandparentId) ?? [];
			list.push(childId);
			sharedBelowOf.set(grandparentId, list);
		}

		const roots = nodes.filter((n) => !hasParent.has(n.id));

		// ── Phase 1: compute extents bottom-up ──────────────────────────

		const extents = new Map<string, Extent>();

		function computeExtent(nodeId: string): Extent {
			if (extents.has(nodeId)) return extents.get(nodeId)!;

			const node = nodeMap.get(nodeId);
			const nodeW = node ? getMeasuredWidth(node) : DEFAULT_WIDTH;
			const nodeH = node ? getMeasuredHeight(node) : DEFAULT_HEIGHT;

			const children = childrenOf.get(nodeId) ?? [];
			const sideIds = children.filter((id) => isSideChild(id, nodeMap));
			const belowIds = children.filter((id) => !isSideChild(id, nodeMap));
			const sharedBelowIds = sharedBelowOf.get(nodeId) ?? [];

			// Block A — node width vs. combined below-children width
			let belowTotalW = 0;
			let belowMaxH = 0;

			for (let i = 0; i < belowIds.length; i++) {
				const ext = computeExtent(belowIds[i]);
				if (i > 0) belowTotalW += H_SPACING;
				belowTotalW += ext.width;
				belowMaxH = Math.max(belowMaxH, ext.height);
			}

			// Shared children (e.g. user below assistants) go centered under the row
			let sharedBelowW = 0;
			let sharedBelowH = 0;
			for (let i = 0; i < sharedBelowIds.length; i++) {
				const ext = computeExtent(sharedBelowIds[i]);
				if (i > 0) sharedBelowW += H_SPACING;
				sharedBelowW += ext.width;
				sharedBelowH = Math.max(sharedBelowH, ext.height);
			}

			const rowH = belowIds.length > 0 ? nodeH + V_SPACING + belowMaxH : nodeH;
			const blockAW = Math.max(nodeW, belowTotalW, sharedBelowIds.length > 0 ? sharedBelowW : 0);
			const blockAH = sharedBelowIds.length > 0 ? rowH + V_SPACING + sharedBelowH : rowH;

			// Block B — side-children and their subtrees
			let blockBW = 0;
			let blockBH = 0;

			for (let i = 0; i < sideIds.length; i++) {
				const ext = computeExtent(sideIds[i]);
				if (i > 0) blockBW += H_SPACING;
				blockBW += ext.width;
				blockBH = Math.max(blockBH, ext.height);
			}

			const totalW = sideIds.length > 0 ? blockAW + H_SPACING + blockBW : blockAW;
			const totalH = Math.max(blockAH, blockBH);

			const extent: Extent = { width: totalW, height: totalH, blockAWidth: blockAW };
			extents.set(nodeId, extent);
			return extent;
		}

		// Compute all extents (roots first, then any orphans)
		for (const r of roots) computeExtent(r.id);
		for (const n of nodes) {
			if (!extents.has(n.id)) computeExtent(n.id);
		}

		// ── Phase 2: assign positions top-down ──────────────────────────

		const positions = new Map<string, { x: number; y: number }>();

		function placeSubtree(nodeId: string, allocX: number, y: number) {
			const node = nodeMap.get(nodeId);
			const nodeW = node ? getMeasuredWidth(node) : DEFAULT_WIDTH;
			const nodeH = node ? getMeasuredHeight(node) : DEFAULT_HEIGHT;

			const children = childrenOf.get(nodeId) ?? [];
			const sideIds = children.filter((id) => isSideChild(id, nodeMap));
			const belowIds = children.filter((id) => !isSideChild(id, nodeMap));

			const { blockAWidth } = extents.get(nodeId)!;

			// Center the node horizontally within Block A
			positions.set(nodeId, {
				x: allocX + (blockAWidth - nodeW) / 2,
				y
			});

			// Place below-children (assistants), centered within Block A
			if (belowIds.length > 0) {
				let belowTotalW = 0;
				for (let i = 0; i < belowIds.length; i++) {
					if (i > 0) belowTotalW += H_SPACING;
					belowTotalW += extents.get(belowIds[i])!.width;
				}

				const childY = y + nodeH + V_SPACING;
				let childX = allocX + (blockAWidth - belowTotalW) / 2;

				for (const cid of belowIds) {
					placeSubtree(cid, childX, childY);
					childX += extents.get(cid)!.width + H_SPACING;
				}
			}

			// Place shared below-children (user node) centered below the assistants row
			const sharedBelowIds = sharedBelowOf.get(nodeId) ?? [];
			if (sharedBelowIds.length > 0 && belowIds.length > 0) {
				let belowTotalW = 0;
				for (let i = 0; i < belowIds.length; i++) {
					if (i > 0) belowTotalW += H_SPACING;
					belowTotalW += extents.get(belowIds[i])!.width;
				}
				const rowY = y + nodeH + V_SPACING;
				const rowHeight = Math.max(...belowIds.map((id) => extents.get(id)!.height));
				const sharedY = rowY + rowHeight + V_SPACING;

				let sharedTotalW = 0;
				for (let i = 0; i < sharedBelowIds.length; i++) {
					if (i > 0) sharedTotalW += H_SPACING;
					sharedTotalW += extents.get(sharedBelowIds[i])!.width;
				}
				const sharedCenterX = allocX + (blockAWidth - sharedTotalW) / 2;
				let sharedX = sharedCenterX;
				for (const sid of sharedBelowIds) {
					placeSubtree(sid, sharedX, sharedY);
					sharedX += extents.get(sid)!.width + H_SPACING;
				}
			}

			// Place side-children to the right of Block A (non-overlapping zone)
			let sideX = allocX + blockAWidth + H_SPACING;
			for (const sid of sideIds) {
				placeSubtree(sid, sideX, y);
				sideX += extents.get(sid)!.width + H_SPACING;
			}
		}

		// Lay out root nodes (non-welcome) side by side
		let rootX = 0;
		for (const r of roots) {
			placeSubtree(r.id, rootX, 0);
			rootX += extents.get(r.id)!.width + H_SPACING;
		}

		// Safety net for any disconnected / orphan nodes
		for (const node of nodes) {
			if (positions.has(node.id)) continue;
			const maxY = Math.max(0, ...Array.from(positions.values()).map((p) => p.y));
			positions.set(node.id, { x: 0, y: maxY + DEFAULT_HEIGHT + V_SPACING });
		}

		return nodes.map((n) => {
			const pos = positions.get(n.id) ?? { x: 0, y: 0 };
			return { ...n, position: pos };
		});
	}

	// ─── Apply layout to the store ─────────────────────────────────────

	function runLayout(nodes: Node[], edges: Edge[]) {
		const result = computeLayout(nodes, edges);

		const current = nodesStore.current;
		if (current.length === 0) {
			nodesStore.set(result);
		} else {
			const posMap = new Map(result.map((n) => [n.id, n.position]));
			const dataMap = new Map(result.map((n) => [n.id, n.data]));
			nodesStore.update((prev) =>
				prev.map((n) => {
					const pos = posMap.get(n.id);
					const data = dataMap.get(n.id);
					const updated = pos ? { ...n, position: pos } : n;
					return data ? { ...updated, data } : updated;
				})
			);
		}

		// Only fit the view on the very first layout (component mount).
		// Subsequent layout updates (new nodes, resizes) reposition nodes
		// without moving the camera — the user stays in control of the viewport.
		if (isFirstLayout) {
			tick().then(() => {
				requestAnimationFrame(() => {
					doFitView({ animate: false });
					isFirstLayout = false;
				});
			});
		} else {
			isFirstLayout = false;
		}
	}
</script>
