/**
 * Graph mutations
 *
 * Write atoms for graph CRUD operations, drag lifecycle,
 * edge animations, split/merge, and DB sync.
 */

import { atom, WritableAtom } from 'jotai';
import Graph from 'graphology';
import type {
	EdgeKey,
	NodePosition,
	GraphNodeAttributes,
	GraphEdgeAttributes,
	UIEdgeState,
	DBGraphNode,
	DBGraphEdge,
	UINodePropertiesFromDB,
	UIEdgePropertiesFromDB,
} from './types';
import {
	graphAtom,
	graphUpdateVersionAtom,
	graphOptions,
	currentGraphIdAtom,
	draggingNodeIdAtom,
	preDragNodeAttributesAtom,
	edgeCreationAtom,
} from './graph-store';
import {
	nodePositionAtomFamily,
	nodePositionUpdateCounterAtom,
	cleanupNodePositionAtom,
} from './graph-position';
import { highestZIndexAtom, edgeFamilyAtom } from './graph-derived';
import { autoResizeGroupAtom } from './group-store';
import { pushHistoryAtom } from './history-store';
import { createDebug } from '../utils/debug';
import { prefersReducedMotionAtom } from './reduced-motion-store';

const debug = createDebug('graph:mutations');

// --- Node Action Atoms ---

/**
 * Start dragging a node
 */
export const startNodeDragAtom = atom(null, (get, set, { nodeId }: { nodeId: string }) => {
	const graph = get(graphAtom);
	if (!graph.hasNode(nodeId)) return;

	const currentAttributes = graph.getNodeAttributes(nodeId) as GraphNodeAttributes;
	set(preDragNodeAttributesAtom, JSON.parse(JSON.stringify(currentAttributes)));

	const currentHighestZIndex = get(highestZIndexAtom);
	const newZIndex = currentHighestZIndex + 1;

	graph.setNodeAttribute(nodeId, 'zIndex', newZIndex);
	set(draggingNodeIdAtom, nodeId);
});

/**
 * End dragging a node
 */
export const endNodeDragAtom = atom(null, (get, set, _payload?: { nodeId?: string }) => {
	const currentDraggingId = get(draggingNodeIdAtom);
	if (currentDraggingId) {
		debug('Node %s drag ended', currentDraggingId);

		// Auto-resize parent group if the dragged node belongs to one
		const graph = get(graphAtom);
		if (graph.hasNode(currentDraggingId)) {
			const parentId = graph.getNodeAttribute(currentDraggingId, 'parentId') as string | undefined;
			if (parentId) {
				set(autoResizeGroupAtom, parentId);
			}
		}
	}
	set(draggingNodeIdAtom, null);
	set(preDragNodeAttributesAtom, null);
});

/**
 * Optimistically delete a node from local graph
 */
export const optimisticDeleteNodeAtom = atom(null, (get, set, { nodeId }: { nodeId: string }) => {
	const graph = get(graphAtom);
	if (graph.hasNode(nodeId)) {
		graph.dropNode(nodeId);
		set(cleanupNodePositionAtom, nodeId);
		set(graphAtom, graph.copy());
		debug('Optimistically deleted node %s', nodeId);
	}
});

/**
 * Optimistically delete an edge from local graph
 */
export const optimisticDeleteEdgeAtom = atom(null, (get, set, { edgeKey }: { edgeKey: EdgeKey }) => {
	const graph = get(graphAtom);
	if (graph.hasEdge(edgeKey)) {
		graph.dropEdge(edgeKey);
		set(graphAtom, graph.copy());
		debug('Optimistically deleted edge %s', edgeKey);
	}
});

// --- Local Graph Mutations ---

/**
 * Add a node directly to local graph
 */
export const addNodeToLocalGraphAtom = atom(null, (get, set, newNode: DBGraphNode) => {
	const graph = get(graphAtom);

	if (graph.hasNode(newNode.id)) {
		debug('Node %s already exists, skipping', newNode.id);
		return;
	}

	const uiProps = (newNode.ui_properties || {}) as UINodePropertiesFromDB;
	const attributes: GraphNodeAttributes = {
		x: typeof uiProps.x === 'number' ? uiProps.x : Math.random() * 800,
		y: typeof uiProps.y === 'number' ? uiProps.y : Math.random() * 600,
		size: typeof uiProps.size === 'number' ? uiProps.size : 15,
		width: typeof uiProps.width === 'number' ? uiProps.width : 500,
		height: typeof uiProps.height === 'number' ? uiProps.height : 500,
		color: typeof uiProps.color === 'string' ? uiProps.color : '#ccc',
		label: newNode.label || newNode.node_type || newNode.id,
		zIndex: typeof uiProps.zIndex === 'number' ? uiProps.zIndex : 0,
		dbData: newNode,
	};

	debug('Adding node %s to local graph at (%d, %d)', newNode.id, attributes.x, attributes.y);
	graph.addNode(newNode.id, attributes);
	// Position is derived from graph - just update graph and trigger re-render
	set(graphAtom, graph.copy());
	set(graphUpdateVersionAtom, (v) => v + 1);
	set(nodePositionUpdateCounterAtom, (c) => c + 1);
});


// Re-export all edge and advanced atoms for backward compat
export {
	swapEdgeAtomicAtom,
	departingEdgesAtom,
	EDGE_ANIMATION_DURATION,
	removeEdgeWithAnimationAtom,
	editingEdgeLabelAtom,
	updateEdgeLabelAtom,
} from './graph-mutations-edges';

export {
	dropTargetNodeIdAtom,
	splitNodeAtom,
	mergeNodesAtom,
} from './graph-mutations-advanced';

// --- DB Sync Atom ---

/**
 * Load/sync graph data from database
 * Merges DB data with local state, preserving positions during drag
 */
export const loadGraphFromDbAtom: WritableAtom<null, [DBGraphNode[], DBGraphEdge[]], void> = atom(null, (get, set, fetchedNodes, fetchedEdges) => {
	debug('========== START SYNC ==========');
	debug('Fetched nodes: %d, edges: %d', fetchedNodes.length, fetchedEdges.length);

	const currentGraphId = get(currentGraphIdAtom);

	// Validate data belongs to current graph
	if (fetchedNodes.length > 0 && fetchedNodes[0].graph_id !== currentGraphId) {
		debug('Skipping sync - data belongs to different graph');
		return;
	}

	const existingGraph = get(graphAtom);

	const isDragging = get(draggingNodeIdAtom) !== null;

	if (isDragging) {
		debug('Skipping sync - drag in progress');
		return;
	}

	// Detect graph switch
	const existingNodeIds = new Set(existingGraph.nodes());
	const fetchedNodeIds = new Set(fetchedNodes.map((n) => n.id));
	const hasAnyCommonNodes = Array.from(existingNodeIds).some((id) => fetchedNodeIds.has(id));

	let graph: Graph<GraphNodeAttributes, GraphEdgeAttributes>;

	if (hasAnyCommonNodes && existingNodeIds.size > 0) {
		debug('Merging DB data into existing graph');
		graph = existingGraph.copy();
	} else {
		debug('Creating fresh graph (graph switch detected)');
		graph = new Graph<GraphNodeAttributes, GraphEdgeAttributes>(graphOptions);
	}

	const fetchedEdgeIds = new Set(fetchedEdges.map((e) => e.id));

	// Remove deleted nodes
	if (hasAnyCommonNodes && existingNodeIds.size > 0) {
		graph.forEachNode((nodeId) => {
			if (!fetchedNodeIds.has(nodeId)) {
				debug('Removing deleted node: %s', nodeId);
				graph.dropNode(nodeId);
				nodePositionAtomFamily.remove(nodeId);
			}
		});
	}

	// Merge/add nodes
	fetchedNodes.forEach((node) => {
		const uiProps = (node.ui_properties || {}) as UINodePropertiesFromDB;
		const newX = typeof uiProps.x === 'number' ? uiProps.x : Math.random() * 800;
		const newY = typeof uiProps.y === 'number' ? uiProps.y : Math.random() * 600;

		if (graph.hasNode(node.id)) {
			const currentAttrs = graph.getNodeAttributes(node.id) as GraphNodeAttributes;
			const attributes: GraphNodeAttributes = {
				x: newX,
				y: newY,
				size: typeof uiProps.size === 'number' ? uiProps.size : currentAttrs.size,
				width: typeof uiProps.width === 'number' ? uiProps.width : (currentAttrs.width ?? 500),
				height: typeof uiProps.height === 'number' ? uiProps.height : (currentAttrs.height ?? 500),
				color: typeof uiProps.color === 'string' ? uiProps.color : currentAttrs.color,
				label: node.label || node.node_type || node.id,
				zIndex: typeof uiProps.zIndex === 'number' ? uiProps.zIndex : currentAttrs.zIndex,
				dbData: node,
			};

			graph.replaceNodeAttributes(node.id, attributes);
		} else {
			const attributes: GraphNodeAttributes = {
				x: newX,
				y: newY,
				size: typeof uiProps.size === 'number' ? uiProps.size : 15,
				width: typeof uiProps.width === 'number' ? uiProps.width : 500,
				height: typeof uiProps.height === 'number' ? uiProps.height : 500,
				color: typeof uiProps.color === 'string' ? uiProps.color : '#ccc',
				label: node.label || node.node_type || node.id,
				zIndex: typeof uiProps.zIndex === 'number' ? uiProps.zIndex : 0,
				dbData: node,
			};
			graph.addNode(node.id, attributes);
		}
	});

	// Remove deleted edges
	graph.forEachEdge((edgeId) => {
		if (!fetchedEdgeIds.has(edgeId)) {
			debug('Removing deleted edge: %s', edgeId);
			graph.dropEdge(edgeId);
		}
	});

	// Merge/add edges
	fetchedEdges.forEach((edge) => {
		if (graph.hasNode(edge.source_node_id) && graph.hasNode(edge.target_node_id)) {
			const uiProps = (edge.ui_properties || {}) as UIEdgePropertiesFromDB;
			const attributes: GraphEdgeAttributes = {
				type: (typeof uiProps.style === 'string' ? uiProps.style : 'solid') as 'solid' | 'dashed',
				color: typeof uiProps.color === 'string' ? uiProps.color : '#999',
				label: edge.edge_type ?? undefined,
				weight: typeof uiProps.weight === 'number' ? uiProps.weight : 1,
				dbData: edge,
			};

			if (graph.hasEdge(edge.id)) {
				graph.replaceEdgeAttributes(edge.id, attributes);
			} else {
				try {
					graph.addEdgeWithKey(edge.id, edge.source_node_id, edge.target_node_id, attributes);
				} catch (e) {
					debug('Failed to add edge %s: %o', edge.id, e);
				}
			}
		}
	});

	set(graphAtom, graph);
	set(graphUpdateVersionAtom, (v) => v + 1);
	debug('========== SYNC COMPLETE ==========');
	debug('Final graph: %d nodes, %d edges', graph.order, graph.size);
});
