/**
 * Graph derived atoms
 *
 * Read-only UI atoms derived from the Graphology graph.
 * Provides node/edge state for rendering with structural equality caching.
 */

import { atom } from 'jotai';
import { atomFamily } from 'jotai-family';
import type {
	NodeId,
	EdgeKey,
	GraphNodeAttributes,
	GraphEdgeAttributes,
	UINodeState,
	UIEdgeState,
	EdgeCreationState,
} from './types';
import { graphAtom, graphUpdateVersionAtom, currentGraphIdAtom, edgeCreationAtom, draggingNodeIdAtom } from './graph-store';
import { nodePositionAtomFamily, nodePositionUpdateCounterAtom } from './graph-position';
import { panAtom, zoomAtom, viewportRectAtom } from './viewport-store';
import { collapsedGroupsAtom, collapsedEdgeRemapAtom } from './group-store';

// --- Derived Node Atoms ---

/**
 * Highest z-index among nodes
 */
export const highestZIndexAtom = atom((get) => {
	get(graphUpdateVersionAtom);
	const graph = get(graphAtom);
	let maxZ = 0;
	graph.forEachNode((_node, attributes) => {
		if (attributes.zIndex > maxZ) {
			maxZ = attributes.zIndex;
		}
	});
	return maxZ;
});

/**
 * All UI nodes with current positions
 *
 * Uses structural equality: if every entry is reference-equal to the
 * previous result, the same array is returned to avoid downstream re-renders.
 */
const _prevUiNodesByGraph = new WeakMap<object, UINodeState[]>();
export const uiNodesAtom = atom<UINodeState[]>((get) => {
	get(graphUpdateVersionAtom);
	const graph = get(graphAtom);
	const currentDraggingId = get(draggingNodeIdAtom);
	const collapsed = get(collapsedGroupsAtom);
	const nodes: UINodeState[] = [];
	graph.forEachNode((nodeId, attributes) => {
		// Skip nodes whose parent (or ancestor) is collapsed
		if (collapsed.size > 0) {
			let current = nodeId;
			let hidden = false;
			while (true) {
				if (!graph.hasNode(current)) break;
				const pid = (graph.getNodeAttributes(current) as GraphNodeAttributes).parentId;
				if (!pid) break;
				if (collapsed.has(pid)) { hidden = true; break; }
				current = pid;
			}
			if (hidden) return;
		}
		const position = get(nodePositionAtomFamily(nodeId));
		nodes.push({
			...attributes,
			id: nodeId,
			position,
			isDragging: nodeId === currentDraggingId,
		});
	});

	// Structural equality: return previous array if length and entries match
	const prev = _prevUiNodesByGraph.get(graph) ?? [];
	if (
		nodes.length === prev.length &&
		nodes.every((n, i) => n.id === prev[i].id && n.position === prev[i].position && n.isDragging === prev[i].isDragging)
	) {
		return prev;
	}
	_prevUiNodesByGraph.set(graph, nodes);
	return nodes;
});

/**
 * All node keys
 */
export const nodeKeysAtom = atom((get) => {
	get(graphUpdateVersionAtom);
	const graph = get(graphAtom);
	return graph.nodes();
});

/**
 * Per-node UI state
 */
export const nodeFamilyAtom = atomFamily(
	(nodeId: NodeId) =>
		atom((get): UINodeState | null => {
			get(graphUpdateVersionAtom);
			const graph = get(graphAtom);

			if (!graph.hasNode(nodeId)) {
				return null;
			}

			const attributes = graph.getNodeAttributes(nodeId) as GraphNodeAttributes;
			const position = get(nodePositionAtomFamily(nodeId));
			const currentDraggingId = get(draggingNodeIdAtom);

			return {
				...attributes,
				id: nodeId,
				position,
				isDragging: nodeId === currentDraggingId,
			};
		}),
	(a, b) => a === b,
);

// --- Derived Edge Atoms ---

/**
 * All edge keys
 */
export const edgeKeysAtom = atom((get) => {
	get(graphUpdateVersionAtom);
	const graph = get(graphAtom);
	return graph.edges();
});

/**
 * Edge keys including temp edge during creation
 */
export const edgeKeysWithTempEdgeAtom = atom((get) => {
	const keys = get(edgeKeysAtom);
	const edgeCreation = get(edgeCreationAtom);
	if (edgeCreation.isCreating) {
		return [...keys, 'temp-creating-edge'];
	}
	return keys;
});

/**
 * Per-edge UI state
 *
 * Structural equality cache: stores previous result per edge key.
 * Returns previous object when all fields match — prevents downstream re-renders.
 */
const _edgeCacheByGraph = new WeakMap<object, Map<string, UIEdgeState>>();
function getEdgeCache(graph: object): Map<string, UIEdgeState> {
	let cache = _edgeCacheByGraph.get(graph);
	if (!cache) {
		cache = new Map();
		_edgeCacheByGraph.set(graph, cache);
	}
	return cache;
}
export const edgeFamilyAtom = atomFamily(
	(key: EdgeKey) =>
		atom((get): UIEdgeState | null => {
			// Position reactivity flows through nodePositionAtomFamily(sourceId/targetId)
			// with structural equality caching — only connected edges re-render on drag.
			// graphUpdateVersionAtom handles non-position attribute changes (label, color)
			// and is NOT bumped during drag, so it won't cause O(E) re-renders.
			get(graphUpdateVersionAtom);

			if (key === 'temp-creating-edge') {
				const edgeCreationState = get(edgeCreationAtom);
				const graph = get(graphAtom);

				if (edgeCreationState.isCreating && edgeCreationState.sourceNodeId && edgeCreationState.targetPosition) {
					const sourceNodeAttrs = graph.getNodeAttributes(edgeCreationState.sourceNodeId);
					const sourceNodePosition = get(nodePositionAtomFamily(edgeCreationState.sourceNodeId));

					const pan = get(panAtom);
					const zoom = get(zoomAtom);
					const viewportRect = get(viewportRectAtom);

					if (sourceNodeAttrs && viewportRect) {
						const mouseX = edgeCreationState.targetPosition.x - viewportRect.left;
						const mouseY = edgeCreationState.targetPosition.y - viewportRect.top;
						const worldTargetX = (mouseX - pan.x) / zoom;
						const worldTargetY = (mouseY - pan.y) / zoom;

						const tempEdge: UIEdgeState = {
							key: 'temp-creating-edge',
							sourceId: edgeCreationState.sourceNodeId,
							targetId: 'temp-cursor',
							sourcePosition: sourceNodePosition,
							targetPosition: { x: worldTargetX, y: worldTargetY },
							sourceNodeSize: sourceNodeAttrs.size,
							sourceNodeWidth: sourceNodeAttrs.width,
							sourceNodeHeight: sourceNodeAttrs.height,
							targetNodeSize: 0,
							targetNodeWidth: 0,
							targetNodeHeight: 0,
							type: 'dashed',
							color: '#FF9800',
							weight: 2,
							label: undefined,
							dbData: {
								id: 'temp-creating-edge',
								graph_id: get(currentGraphIdAtom) || '',
								source_node_id: edgeCreationState.sourceNodeId,
								target_node_id: 'temp-cursor',
								edge_type: 'temp',
								filter_condition: null,
								ui_properties: null,
								data: null,
								created_at: new Date().toISOString(),
								updated_at: new Date().toISOString(),
							},
						};
						return tempEdge;
					}
				}
				return null;
			}

			const graph = get(graphAtom);
			if (!graph.hasEdge(key)) {
				getEdgeCache(graph).delete(key);
				return null;
			}

			const sourceId = graph.source(key);
			const targetId = graph.target(key);
			const attributes = graph.getEdgeAttributes(key);

			// Re-route edges through collapsed groups: if a source/target is inside
			// a collapsed group, use the group node's position and dimensions instead
			const remap = get(collapsedEdgeRemapAtom);
			const effectiveSourceId = remap.get(sourceId) ?? sourceId;
			const effectiveTargetId = remap.get(targetId) ?? targetId;

			if (!graph.hasNode(effectiveSourceId) || !graph.hasNode(effectiveTargetId)) {
				getEdgeCache(graph).delete(key);
				return null;
			}

			const sourceAttributes = graph.getNodeAttributes(effectiveSourceId);
			const targetAttributes = graph.getNodeAttributes(effectiveTargetId);

			const sourcePosition = get(nodePositionAtomFamily(effectiveSourceId));
			const targetPosition = get(nodePositionAtomFamily(effectiveTargetId));

			if (sourceAttributes && targetAttributes) {
				const next: UIEdgeState = {
					...(attributes as GraphEdgeAttributes),
					key,
					sourceId: effectiveSourceId,
					targetId: effectiveTargetId,
					sourcePosition,
					targetPosition,
					sourceNodeSize: sourceAttributes.size,
					targetNodeSize: targetAttributes.size,
					sourceNodeWidth: sourceAttributes.width ?? sourceAttributes.size,
					sourceNodeHeight: sourceAttributes.height ?? sourceAttributes.size,
					targetNodeWidth: targetAttributes.width ?? targetAttributes.size,
					targetNodeHeight: targetAttributes.height ?? targetAttributes.size,
				};

				// Structural equality: return cached object if all fields match
				const edgeCache = getEdgeCache(graph);
				const prev = edgeCache.get(key);
				if (
					prev &&
					prev.sourcePosition === next.sourcePosition &&
					prev.targetPosition === next.targetPosition &&
					prev.sourceId === next.sourceId &&
					prev.targetId === next.targetId &&
					prev.type === next.type &&
					prev.color === next.color &&
					prev.weight === next.weight &&
					prev.label === next.label &&
					prev.sourceNodeSize === next.sourceNodeSize &&
					prev.targetNodeSize === next.targetNodeSize &&
					prev.sourceNodeWidth === next.sourceNodeWidth &&
					prev.sourceNodeHeight === next.sourceNodeHeight &&
					prev.targetNodeWidth === next.targetNodeWidth &&
					prev.targetNodeHeight === next.targetNodeHeight
				) {
					return prev;
				}

				edgeCache.set(key, next);
				return next;
			}
			getEdgeCache(graph).delete(key);
			return null;
		}),
	(a, b) => a === b,
);
