/**
 * Canvas Serializer
 *
 * JSON export/import of the full canvas state: nodes, edges, positions,
 * groups, and viewport. Enables copy-paste between canvases, file-based
 * backup, and template workflows.
 *
 * All functions are pure and operate on a Jotai store — no React required.
 *
 * @since 1.3.0
 */

import type { createStore } from 'jotai';
import Graph from 'graphology';
import { graphAtom, graphUpdateVersionAtom, graphOptions, currentGraphIdAtom } from './graph-store';
import { nodePositionAtomFamily, nodePositionUpdateCounterAtom } from './graph-position';
import { zoomAtom, panAtom } from './viewport-store';
import { collapsedGroupsAtom } from './group-store';
import type { GraphNodeAttributes, GraphEdgeAttributes, DBGraphNode, DBGraphEdge, Json } from './types';

type Store = ReturnType<typeof createStore>;

// =============================================================================
// Snapshot Schema
// =============================================================================

/** Version of the snapshot schema. Bump on breaking format changes. */
export const SNAPSHOT_VERSION = 1 as const;

/**
 * Serialized node — portable representation of a single canvas node.
 */
export interface SerializedNode {
	id: string;
	position: { x: number; y: number };
	dimensions: { width: number; height: number };
	size: number;
	color: string;
	zIndex: number;
	label?: string;
	parentId?: string;
	dbData: DBGraphNode;
}

/**
 * Serialized edge — portable representation of a single canvas edge.
 */
export interface SerializedEdge {
	key: string;
	sourceId: string;
	targetId: string;
	attributes: {
		weight?: number;
		type?: 'solid' | 'dashed';
		color?: string;
		label?: string;
	};
	dbData: DBGraphEdge;
}

/**
 * Serialized group relationship.
 */
export interface SerializedGroup {
	nodeId: string;
	parentId: string;
	isCollapsed: boolean;
}

/**
 * Complete canvas snapshot — portable JSON schema.
 */
export interface CanvasSnapshot {
	version: typeof SNAPSHOT_VERSION;
	exportedAt: string;
	nodes: SerializedNode[];
	edges: SerializedEdge[];
	groups: SerializedGroup[];
	viewport: {
		zoom: number;
		pan: { x: number; y: number };
	};
	metadata?: Record<string, unknown>;
}

/**
 * Options for importing a snapshot.
 */
export interface ImportOptions {
	/** Whether to clear the current graph first (default: true) */
	clearExisting?: boolean;
	/** Shift all imported node positions by this offset */
	offsetPosition?: { x: number; y: number };
	/** Generate new UUIDs for all nodes/edges to avoid conflicts (default: false) */
	remapIds?: boolean;
}

/**
 * Result of snapshot validation.
 */
export interface ValidationResult {
	valid: boolean;
	errors: string[];
}

// =============================================================================
// Export
// =============================================================================

/**
 * Export the current canvas state to a portable JSON snapshot.
 *
 * Reads the Graphology graph, all node positions, group state, and viewport
 * from the given Jotai store.
 */
export function exportGraph(store: Store, metadata?: Record<string, unknown>): CanvasSnapshot {
	const graph = store.get(graphAtom);
	const zoom = store.get(zoomAtom);
	const pan = store.get(panAtom);
	const collapsed = store.get(collapsedGroupsAtom);

	const nodes: SerializedNode[] = [];
	const groups: SerializedGroup[] = [];
	const seenGroupParents = new Set<string>();

	graph.forEachNode((nodeId, attrs) => {
		const a = attrs as GraphNodeAttributes;
		nodes.push({
			id: nodeId,
			position: { x: a.x, y: a.y },
			dimensions: { width: a.width, height: a.height },
			size: a.size,
			color: a.color,
			zIndex: a.zIndex,
			label: a.label,
			parentId: a.parentId,
			dbData: a.dbData,
		});

		if (a.parentId) {
			const key = `${nodeId}:${a.parentId}`;
			if (!seenGroupParents.has(key)) {
				seenGroupParents.add(key);
				groups.push({
					nodeId,
					parentId: a.parentId,
					isCollapsed: collapsed.has(a.parentId),
				});
			}
		}
	});

	const edges: SerializedEdge[] = [];
	graph.forEachEdge((key, attrs, source, target) => {
		const a = attrs as GraphEdgeAttributes;
		edges.push({
			key,
			sourceId: source,
			targetId: target,
			attributes: {
				weight: a.weight,
				type: a.type,
				color: a.color,
				label: a.label,
			},
			dbData: a.dbData,
		});
	});

	return {
		version: SNAPSHOT_VERSION,
		exportedAt: new Date().toISOString(),
		nodes,
		edges,
		groups,
		viewport: {
			zoom,
			pan: { ...pan },
		},
		metadata,
	};
}

// =============================================================================
// Import
// =============================================================================

/**
 * Import a snapshot into the Jotai store, rebuilding the full graph state.
 */
export function importGraph(
	store: Store,
	snapshot: CanvasSnapshot,
	options: ImportOptions = {},
): void {
	const { clearExisting = true, offsetPosition, remapIds = false } = options;

	// Build ID remap table if needed
	const idMap = new Map<string, string>();
	if (remapIds) {
		for (const node of snapshot.nodes) {
			idMap.set(node.id, crypto.randomUUID());
		}
		for (const edge of snapshot.edges) {
			idMap.set(edge.key, crypto.randomUUID());
		}
	}
	const remap = (id: string): string => idMap.get(id) ?? id;

	// Clear or reuse graph
	let graph: Graph<GraphNodeAttributes, GraphEdgeAttributes>;
	if (clearExisting) {
		graph = new Graph<GraphNodeAttributes, GraphEdgeAttributes>(graphOptions);
	} else {
		graph = store.get(graphAtom);
	}

	// Offset helper
	const ox = offsetPosition?.x ?? 0;
	const oy = offsetPosition?.y ?? 0;

	// Add nodes
	for (const node of snapshot.nodes) {
		const nodeId = remap(node.id);
		const parentId = node.parentId ? remap(node.parentId) : undefined;

		const dbData: DBGraphNode = remapIds
			? { ...node.dbData, id: nodeId }
			: node.dbData;

		const attrs: GraphNodeAttributes = {
			x: node.position.x + ox,
			y: node.position.y + oy,
			width: node.dimensions.width,
			height: node.dimensions.height,
			size: node.size,
			color: node.color,
			zIndex: node.zIndex,
			label: node.label,
			parentId,
			dbData,
		};

		graph.addNode(nodeId, attrs);
	}

	// Add edges
	for (const edge of snapshot.edges) {
		const edgeKey = remap(edge.key);
		const sourceId = remap(edge.sourceId);
		const targetId = remap(edge.targetId);

		// Skip edges whose endpoints don't exist
		if (!graph.hasNode(sourceId) || !graph.hasNode(targetId)) continue;

		const dbData: DBGraphEdge = remapIds
			? {
					...edge.dbData,
					id: edgeKey,
					source_node_id: sourceId,
					target_node_id: targetId,
				}
			: edge.dbData;

		const attrs: GraphEdgeAttributes = {
			weight: edge.attributes.weight,
			type: edge.attributes.type,
			color: edge.attributes.color,
			label: edge.attributes.label,
			dbData,
		};

		graph.addEdgeWithKey(edgeKey, sourceId, targetId, attrs);
	}

	// Apply graph to store
	store.set(graphAtom, graph);
	store.set(graphUpdateVersionAtom, (v) => v + 1);
	store.set(nodePositionUpdateCounterAtom, (c) => c + 1);

	// Restore collapsed groups
	const collapsedSet = new Set<string>();
	for (const group of snapshot.groups) {
		if (group.isCollapsed) {
			collapsedSet.add(remap(group.parentId));
		}
	}
	store.set(collapsedGroupsAtom, collapsedSet);

	// Restore viewport
	store.set(zoomAtom, snapshot.viewport.zoom);
	store.set(panAtom, { ...snapshot.viewport.pan });
}

// =============================================================================
// Validation
// =============================================================================

/**
 * Validate that an unknown value conforms to the CanvasSnapshot schema.
 * Performs runtime type checks on all required fields.
 */
export function validateSnapshot(data: unknown): ValidationResult {
	const errors: string[] = [];

	if (!data || typeof data !== 'object') {
		return { valid: false, errors: ['Snapshot must be a non-null object'] };
	}

	const obj = data as Record<string, unknown>;

	// Version
	if (obj.version !== SNAPSHOT_VERSION) {
		errors.push(`Expected version ${SNAPSHOT_VERSION}, got ${String(obj.version)}`);
	}

	// exportedAt
	if (typeof obj.exportedAt !== 'string') {
		errors.push('Missing or invalid "exportedAt" (expected ISO string)');
	}

	// nodes
	if (!Array.isArray(obj.nodes)) {
		errors.push('Missing or invalid "nodes" (expected array)');
	} else {
		for (let i = 0; i < obj.nodes.length; i++) {
			const node = obj.nodes[i] as Record<string, unknown>;
			if (!node || typeof node !== 'object') {
				errors.push(`nodes[${i}]: expected object`);
				continue;
			}
			if (typeof node.id !== 'string') errors.push(`nodes[${i}]: missing "id"`);
			if (!node.position || typeof node.position !== 'object') errors.push(`nodes[${i}]: missing "position"`);
			if (!node.dimensions || typeof node.dimensions !== 'object') errors.push(`nodes[${i}]: missing "dimensions"`);
			if (!node.dbData || typeof node.dbData !== 'object') errors.push(`nodes[${i}]: missing "dbData"`);
		}
	}

	// edges
	if (!Array.isArray(obj.edges)) {
		errors.push('Missing or invalid "edges" (expected array)');
	} else {
		for (let i = 0; i < obj.edges.length; i++) {
			const edge = obj.edges[i] as Record<string, unknown>;
			if (!edge || typeof edge !== 'object') {
				errors.push(`edges[${i}]: expected object`);
				continue;
			}
			if (typeof edge.key !== 'string') errors.push(`edges[${i}]: missing "key"`);
			if (typeof edge.sourceId !== 'string') errors.push(`edges[${i}]: missing "sourceId"`);
			if (typeof edge.targetId !== 'string') errors.push(`edges[${i}]: missing "targetId"`);
			if (!edge.dbData || typeof edge.dbData !== 'object') errors.push(`edges[${i}]: missing "dbData"`);
		}
	}

	// groups
	if (!Array.isArray(obj.groups)) {
		errors.push('Missing or invalid "groups" (expected array)');
	}

	// viewport
	if (!obj.viewport || typeof obj.viewport !== 'object') {
		errors.push('Missing or invalid "viewport" (expected object)');
	} else {
		const vp = obj.viewport as Record<string, unknown>;
		if (typeof vp.zoom !== 'number') errors.push('viewport: missing "zoom"');
		if (!vp.pan || typeof vp.pan !== 'object') errors.push('viewport: missing "pan"');
	}

	return { valid: errors.length === 0, errors };
}
