/**
 * Node Renderer Component
 *
 * Renders visible nodes from the graph, positioned absolutely.
 * Uses viewport virtualization - only nodes within visible bounds are rendered.
 */

import React, { type ErrorInfo } from 'react';
import { useAtomValue } from 'jotai';
import { nodePositionAtomFamily } from '../core/graph-position';
import { nodeFamilyAtom } from '../core/graph-derived';
import { visibleNodeKeysAtom } from '../core/virtualization-store';
import type { NodeId, UINodeState, UINodeProperties, NodeRenderProps } from '../core/types';
import { getNodeTypeComponent, FallbackNodeTypeComponent } from '../core/node-type-registry';
import { Node } from './Node';
import { NodeErrorBoundary } from './NodeErrorBoundary';

export interface NodeRendererProps {
	/**
	 * Render function for custom node content.
	 * When omitted, the node type registry is used to resolve components
	 * by `nodeData.dbData.node_type`. Falls back to FallbackNodeTypeComponent.
	 */
	renderNode?: (props: NodeRenderProps) => React.ReactNode;
	/** Callback to persist node changes */
	onNodePersist?: (nodeId: NodeId, graphId: string, uiProperties: UINodeProperties) => Promise<void>;
	/** Callback when persist fails */
	onNodePersistError?: (nodeId: NodeId, error: Error) => void;
	/** Callback when a node fails to render */
	onNodeRenderError?: (nodeId: NodeId, error: Error, errorInfo: ErrorInfo) => void;
	/** Custom fallback UI for errored nodes */
	renderErrorFallback?: (props: { nodeId: string; error: Error; reset: () => void }) => React.ReactNode;
	/** Optional wrapper component around nodes */
	nodeWrapper?: React.ComponentType<{ children: React.ReactNode; nodeData: UINodeState }>;
}

/**
 * Renders visible nodes from the graph (virtualized).
 * Only nodes within the viewport bounds are rendered for performance.
 */
export function NodeRenderer({
	renderNode,
	onNodePersist,
	onNodePersistError,
	onNodeRenderError,
	renderErrorFallback,
	nodeWrapper,
}: NodeRendererProps) {
	const nodeKeys = useAtomValue(visibleNodeKeysAtom);

	return (
		<>
			{nodeKeys.map((key) => (
				<NodeItem
					key={key}
					nodeKey={key}
					renderNode={renderNode}
					onNodePersist={onNodePersist}
					onNodePersistError={onNodePersistError}
					onNodeRenderError={onNodeRenderError}
					renderErrorFallback={renderErrorFallback}
					nodeWrapper={nodeWrapper}
				/>
			))}
		</>
	);
}

interface NodeItemProps {
	nodeKey: string;
	renderNode?: (props: NodeRenderProps) => React.ReactNode;
	onNodePersist?: (nodeId: NodeId, graphId: string, uiProperties: UINodeProperties) => Promise<void>;
	onNodePersistError?: (nodeId: NodeId, error: Error) => void;
	onNodeRenderError?: (nodeId: NodeId, error: Error, errorInfo: ErrorInfo) => void;
	renderErrorFallback?: (props: { nodeId: string; error: Error; reset: () => void }) => React.ReactNode;
	nodeWrapper?: React.ComponentType<{ children: React.ReactNode; nodeData: UINodeState }>;
}

/**
 * Create a render function that bridges NodeRenderProps → NodeTypeComponent.
 * Looks up the component from the node type registry, falling back to FallbackNodeTypeComponent.
 */
function resolveRenderNode(nodeData: UINodeState): (props: NodeRenderProps) => React.ReactNode {
	const Component = getNodeTypeComponent(nodeData.dbData.node_type) ?? FallbackNodeTypeComponent;
	return (props: NodeRenderProps) => <Component nodeData={props.node} isResizing={props.isResizing} />;
}

function NodeItem({
	nodeKey,
	renderNode,
	onNodePersist,
	onNodePersistError,
	onNodeRenderError,
	renderErrorFallback,
	nodeWrapper,
}: NodeItemProps) {
	const nodeData = useAtomValue(nodeFamilyAtom(nodeKey));
	const position = useAtomValue(nodePositionAtomFamily(nodeKey));

	if (!nodeData) return null;

	// Use provided renderNode, or resolve from node type registry
	const effectiveRenderNode = renderNode ?? resolveRenderNode(nodeData);

	return (
		<div
			style={{
				position: 'absolute',
				left: position.x,
				top: position.y,
				transform: 'translate(0, 0)',
			}}
		>
			<NodeErrorBoundary
				nodeId={nodeKey}
				onError={onNodeRenderError}
				renderFallback={renderErrorFallback}
			>
				<Node
					nodeData={nodeData}
					renderContent={effectiveRenderNode}
					onPersist={onNodePersist}
					onPersistError={onNodePersistError}
					wrapper={nodeWrapper}
				/>
			</NodeErrorBoundary>
		</div>
	);
}
