/**
 * @blinksgg/canvas — Full Feature Demo (v0.15 — All Features Wired)
 *
 * Showcases ALL major canvas features including:
 * - Minimap, lasso/rect selection overlay, edge animations
 * - Tree, grid & force auto-layout with animation
 * - Node grouping/ungrouping with GroupNode component
 * - Edge label editing
 * - Search & filter nodes
 * - Touch radial FAB (TouchActionButton)
 * - Toast notifications for undo/redo feedback
 * - Command palette with keyboard shortcuts
 * - Gesture System v2 with onAction callback
 * - AlignmentGuides for snap alignment
 * - NodeContextMenu (right-click)
 * - NodePorts with connection points
 * - SettingsPanel with event-action presets
 * - Virtualization stats (visible/total)
 * - LockedNodeOverlay (double-tap node → detail view)
 * - Arrow key navigation
 * - Drag state indicator
 * - Graph structure stats via useCanvasGraph
 */

import React, { useEffect, useCallback, useState, useRef } from 'react';
import { Provider as JotaiProvider, useAtom, useSetAtom, useAtomValue } from 'jotai';
import Graph from 'graphology';
import {
    Canvas,
    ViewportControls,
    CanvasStyleProvider,
    CommandProvider,
    CommandLine,
    CommandFeedbackOverlay,
    registerBuiltinCommands,
    openCommandLineAtom,
    graphAtom,
    graphUpdateVersionAtom,
    nodePositionUpdateCounterAtom,
    canvasVersion,
    snapEnabledAtom,
    snapGridSizeAtom,
    toggleSnapAtom,
    useCanvasSelection,
    useCanvasViewport,
    useFitToBounds,
    FitToBoundsMode,
    useCanvasHistory,
    useGlobalKeyboard,
    Minimap,
    SelectionOverlay,
    GroupNode,
    searchQueryAtom,
    setSearchQueryAtom,
    clearSearchAtom,
    searchResultCountAtom,
    highlightedSearchIndexAtom,
    nextSearchResultAtom,
    prevSearchResultAtom,
    groupSelectedNodesAtom,
    ungroupNodesAtom,
    selectedNodeIdsAtom,
    CanvasAnimations,
    TouchActionButton,
    CanvasToast,
    useTreeLayout,
    useGridLayout,
    showToastAtom,
    gesturesV2,
    // newly wired
    AlignmentGuides,
    NodeContextMenu,
    NodePorts,
    SettingsPanel,
    LockedNodeOverlay,
    useVirtualization,
    useForceLayout,
    useCanvasDrag,
    useCanvasGraph,
    useArrowKeyNavigation,
    lockNodeAtom,
    unlockNodeAtom,
    lockedNodeIdAtom,
    type GraphNodeAttributes,
    type GraphEdgeAttributes,
    type NodeRenderProps,
    type CreateNodePayload,
    type PortDefinition,
} from '@blinksgg/canvas';

import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Separator } from '@/components/ui/separator';
import {
    Command, Maximize, Undo2, Redo2, Magnet,
    MousePointerClick, Move, Pencil, Keyboard,
    Trash2, Copy, X, Search, TreePine, Grid3x3,
    Group, Ungroup, ChevronUp, ChevronDown,
    Settings, Eye, EyeOff, Network, ArrowUpDown,
    Lock, GripVertical,
} from 'lucide-react';

type InputEvent = gesturesV2.InputEvent;
type ResolvedAction = gesturesV2.ResolvedAction;

// ── constants ──────────────────────────────────────────────────────────
registerBuiltinCommands();

const COLORS = ['#6366f1', '#f97316', '#22c55e', '#3b82f6', '#ec4899', '#eab308', '#14b8a6', '#f43f5e', '#a855f7', '#ef4444'];
const W = 160, H = 80;

const DEMO_PORTS: PortDefinition[] = [
    { id: 'in', label: 'In', side: 'left', type: 'input', position: 0.5 },
    { id: 'out', label: 'Out', side: 'right', type: 'output', position: 0.5 },
];

// ── FPS hook ───────────────────────────────────────────────────────────
function useFps() {
    const [fps, setFps] = useState(0);
    const f = useRef(0), t = useRef(performance.now());
    useEffect(() => {
        let id: number;
        const tick = () => {
            f.current++;
            const now = performance.now();
            if (now - t.current >= 1000) {
                setFps(Math.round((f.current * 1000) / (now - t.current)));
                f.current = 0; t.current = now;
            }
            id = requestAnimationFrame(tick);
        };
        id = requestAnimationFrame(tick);
        return () => cancelAnimationFrame(id);
    }, []);
    return fps;
}

// ── Node UI ────────────────────────────────────────────────────────────
const DemoNode = React.memo(function DemoNode({ node, isSelected }: NodeRenderProps) {
    if (node.dbData?.node_type === 'group') {
        return <GroupNode node={node} isSelected={isSelected} />;
    }

    return (
        <div className="relative w-full h-full">
            <div className={`
                w-full h-full flex flex-col items-center justify-center gap-1
                rounded-lg border px-3 py-2 cursor-grab
                transition-all duration-150
                ${isSelected
                    ? 'bg-indigo-500/20 border-indigo-400 shadow-[0_0_20px_rgba(99,102,241,0.3)]'
                    : 'bg-secondary/50 border-border/50 shadow-sm hover:border-border'}
            `}>
                <span className="text-sm font-semibold text-foreground truncate max-w-full">
                    {node.label || `Node ${node.id}`}
                </span>
                <span className="text-[10px] font-mono text-muted-foreground">
                    {Math.round(node.x)}, {Math.round(node.y)}
                </span>
            </div>
            <NodePorts
                nodeId={node.id}
                nodeX={node.x}
                nodeY={node.y}
                nodeWidth={node.width}
                nodeHeight={node.height}
                ports={DEMO_PORTS}
                sides={['left', 'right']}
            />
        </div>
    );
});

// ── Graph factory ──────────────────────────────────────────────────────
function makeGraph(count: number) {
    const g = new Graph<GraphNodeAttributes, GraphEdgeAttributes>({ multi: true, allowSelfLoops: true, type: 'directed' });
    const now = new Date().toISOString();
    const cols = Math.max(Math.ceil(Math.sqrt(count * 1.5)), 5);

    const hasGroup = count >= 10;
    if (hasGroup) {
        const gx = 0, gy = -180;
        g.addNode('group-0', {
            x: gx, y: gy, size: 10, width: 380, height: 280, color: '#6366f1', label: 'Group A', zIndex: -1,
            dbData: {
                id: 'group-0', graph_id: 'demo', label: 'Group A', node_type: 'group', configuration: null,
                ui_properties: { x: gx, y: gy, width: 380, height: 280, color: '#6366f1' }, created_at: now, updated_at: now
            }
        });
    }

    for (let i = 0; i < count; i++) {
        const col = i % cols, row = Math.floor(i / cols);
        const x = col * 220 + (Math.random() - .5) * 40;
        const y = row * 140 + (Math.random() - .5) * 30;
        const color = COLORS[i % COLORS.length];
        const id = `n${i}`;
        g.addNode(id, {
            x, y, size: 10, width: W, height: H, color, label: `Node ${i}`, zIndex: 0,
            dbData: {
                id, graph_id: 'demo', label: `Node ${i}`, node_type: 'default', configuration: null,
                ui_properties: { x, y, width: W, height: H, color }, created_at: now, updated_at: now
            }
        });
    }
    for (let i = 0; i < count; i++) {
        for (let c = 0; c < 1 + Math.floor(Math.random() * 2); c++) {
            const t = Math.min(i + 1 + Math.floor(Math.random() * cols), count - 1);
            if (t !== i) {
                const eid = `e${i}-${t}-${c}`;
                if (!g.hasEdge(eid))
                    g.addEdgeWithKey(eid, `n${i}`, `n${t}`, {
                        color: COLORS[i % COLORS.length],
                        label: i % 5 === 0 ? `edge ${i}→${t}` : undefined,
                        dbData: {
                            id: eid, graph_id: 'demo', source_node_id: `n${i}`, target_node_id: `n${t}`,
                            label: i % 5 === 0 ? `edge ${i}→${t}` : null,
                            edge_type: 'default', configuration: null, filter_condition: null, ui_properties: null, created_at: now, updated_at: now
                        },
                    } as unknown as GraphEdgeAttributes);
            }
        }
    }
    return g;
}

// ── Search Bar ──────────────────────────────────────────────────────────
function SearchBar() {
    const searchQuery = useAtomValue(searchQueryAtom);
    const setSearch = useSetAtom(setSearchQueryAtom);
    const clearSearch = useSetAtom(clearSearchAtom);
    const resultCount = useAtomValue(searchResultCountAtom);
    const highlightedIndex = useAtomValue(highlightedSearchIndexAtom);
    const nextResult = useSetAtom(nextSearchResultAtom);
    const prevResult = useSetAtom(prevSearchResultAtom);

    return (
        <div className="flex flex-col gap-1.5">
            <div className="flex items-center gap-1">
                <div className="relative flex-1">
                    <Search className="absolute left-2 top-1/2 -translate-y-1/2 w-3 h-3 text-muted-foreground" />
                    <input
                        type="text"
                        placeholder="Search nodes..."
                        value={searchQuery}
                        onChange={e => setSearch(e.target.value)}
                        className="w-full bg-secondary text-foreground border border-border rounded-md pl-7 pr-7 py-1 text-xs focus:outline-none focus:ring-1 focus:ring-indigo-500"
                    />
                    {searchQuery && (
                        <button
                            onClick={() => clearSearch()}
                            className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
                        >
                            <X className="w-3 h-3" />
                        </button>
                    )}
                </div>
            </div>
            {searchQuery && (
                <div className="flex items-center justify-between text-[10px] text-muted-foreground">
                    <span>{resultCount} match{resultCount !== 1 ? 'es' : ''}</span>
                    {resultCount > 0 && (
                        <div className="flex items-center gap-0.5">
                            <span>{highlightedIndex + 1}/{resultCount}</span>
                            <button onClick={() => prevResult()} className="p-0.5 hover:bg-secondary rounded">
                                <ChevronUp className="w-3 h-3" />
                            </button>
                            <button onClick={() => nextResult()} className="p-0.5 hover:bg-secondary rounded">
                                <ChevronDown className="w-3 h-3" />
                            </button>
                        </div>
                    )}
                </div>
            )}
        </div>
    );
}

// ── Layout Controls ─────────────────────────────────────────────────────
function LayoutControls() {
    const { applyLayout: applyTree, isRunning: isTreeRunning } = useTreeLayout({ duration: 500 });
    const { applyLayout: applyGrid, isRunning: isGridRunning } = useGridLayout({ duration: 500 });
    const { applyForceLayout, isRunning: isForceRunning } = useForceLayout();
    const isAnimating = isTreeRunning || isGridRunning || isForceRunning;
    const showToast = useSetAtom(showToastAtom);

    return (
        <div className="flex flex-col gap-1">
            <div className="flex gap-1">
                <Button
                    variant="ghost" size="sm" className="flex-1 gap-1 text-xs"
                    disabled={isAnimating}
                    onClick={() => { applyTree(); showToast('Applied tree layout'); }}
                >
                    <TreePine className="w-3.5 h-3.5" /> Tree
                </Button>
                <Button
                    variant="ghost" size="sm" className="flex-1 gap-1 text-xs"
                    disabled={isAnimating}
                    onClick={() => { applyGrid(); showToast('Applied grid layout'); }}
                >
                    <Grid3x3 className="w-3.5 h-3.5" /> Grid
                </Button>
            </div>
            <Button
                variant="ghost" size="sm" className="gap-1 text-xs"
                disabled={isAnimating}
                onClick={() => { applyForceLayout(); showToast('Applied force layout'); }}
            >
                <Network className="w-3.5 h-3.5" /> Force-Directed
            </Button>
        </div>
    );
}

// ── Group Controls ──────────────────────────────────────────────────────
function GroupControls() {
    const groupSelected = useSetAtom(groupSelectedNodesAtom);
    const ungroupNodes = useSetAtom(ungroupNodesAtom);
    const selectedIds = useAtomValue(selectedNodeIdsAtom);
    const [graph] = useAtom(graphAtom);
    const bumpVer = useSetAtom(graphUpdateVersionAtom);
    const sel = useCanvasSelection();
    const showToast = useSetAtom(showToastAtom);

    const handleGroup = useCallback(() => {
        const nodeIds = [...selectedIds];
        if (nodeIds.length < 2) return;
        const groupId = `group-${Date.now()}`;
        const now = new Date().toISOString();
        graph.addNode(groupId, {
            x: 0, y: 0, size: 10, width: 400, height: 300, color: '#6366f1', label: 'Group', zIndex: -1,
            dbData: {
                id: groupId, graph_id: 'demo', label: 'Group', node_type: 'group', configuration: null,
                ui_properties: { x: 0, y: 0, width: 400, height: 300, color: '#6366f1' }, created_at: now, updated_at: now
            }
        });
        groupSelected({ nodeIds, groupNodeId: groupId });
        showToast('Grouped nodes');
    }, [selectedIds, graph, groupSelected, showToast]);

    const handleUngroup = useCallback(() => {
        for (const nodeId of selectedIds) {
            if (graph.hasNode(nodeId)) {
                const attrs = graph.getNodeAttributes(nodeId);
                if (attrs.dbData?.node_type === 'group') {
                    ungroupNodes(nodeId);
                }
                if (attrs.parentId) {
                    ungroupNodes(attrs.parentId);
                }
            }
        }
        bumpVer((v: number) => v + 1);
        showToast('Ungrouped nodes');
    }, [selectedIds, graph, ungroupNodes, bumpVer, showToast]);

    return (
        <div className="flex gap-1">
            <Button
                variant="ghost" size="sm" className="flex-1 gap-1 text-xs"
                disabled={sel.count < 2}
                onClick={handleGroup}
            >
                <Group className="w-3.5 h-3.5" /> Group
            </Button>
            <Button
                variant="ghost" size="sm" className="flex-1 gap-1 text-xs"
                disabled={sel.count === 0}
                onClick={handleUngroup}
            >
                <Ungroup className="w-3.5 h-3.5" /> Ungroup
            </Button>
        </div>
    );
}

// ── Virtualization Stats ──────────────────────────────────────────────────
function VirtualizationStats() {
    const virt = useVirtualization();
    return (
        <div className="flex flex-col gap-1.5">
            <div className="flex items-center justify-between">
                <span className="text-[10px] text-muted-foreground">Virtualization</span>
                <Button
                    variant="ghost" size="sm" className="h-5 px-1.5 text-[10px]"
                    onClick={virt.toggle}
                >
                    {virt.enabled ? <Eye className="w-3 h-3" /> : <EyeOff className="w-3 h-3" />}
                    <span className="ml-1">{virt.enabled ? 'ON' : 'OFF'}</span>
                </Button>
            </div>
            {virt.enabled && (
                <div className="flex flex-wrap gap-1">
                    <Badge variant="secondary" className="text-[10px]">
                        {virt.visibleNodes}/{virt.totalNodes} nodes
                    </Badge>
                    <Badge variant="secondary" className="text-[10px]">
                        {virt.visibleEdges}/{virt.totalEdges} edges
                    </Badge>
                    <Badge variant="secondary" className="text-[10px]">
                        {virt.culledNodes} culled
                    </Badge>
                </div>
            )}
        </div>
    );
}

// ── Drag Indicator ────────────────────────────────────────────────────────
function DragIndicator() {
    const { isDragging, draggingNodeId } = useCanvasDrag();
    if (!isDragging) return null;
    return (
        <div className="fixed top-3 left-1/2 -translate-x-1/2 z-[300] px-3 py-1.5 rounded-full bg-indigo-600/90 text-white text-xs font-medium backdrop-blur-sm shadow-lg flex items-center gap-1.5">
            <GripVertical className="w-3 h-3" />
            Dragging {draggingNodeId ?? 'nodes'}
        </div>
    );
}

// ── Arrow Key Navigation Handler ─────────────────────────────────────────
function ArrowKeyNav() {
    useArrowKeyNavigation();
    return null;
}

// ── Locked Node Detail Overlay ───────────────────────────────────────────
function LockedOverlay() {
    const lockedId = useAtomValue(lockedNodeIdAtom);
    const unlock = useSetAtom(unlockNodeAtom);
    const [graph] = useAtom(graphAtom);

    if (!lockedId) return null;

    const nodeAttrs = graph.hasNode(lockedId) ? graph.getNodeAttributes(lockedId) : null;

    return (
        <LockedNodeOverlay
            pages={[
                { key: 'details', label: 'Details' },
                { key: 'connections', label: 'Connections' },
            ]}
            renderContent={(ctx) => {
                if (!nodeAttrs) return <div className="p-6 text-muted-foreground">Node not found</div>;
                if (ctx.currentPage === 'connections') {
                    const edges = graph.hasNode(lockedId) ? graph.edges(lockedId) : [];
                    return (
                        <div className="p-6 space-y-2">
                            <h3 className="text-lg font-semibold">{nodeAttrs.label} — Connections</h3>
                            <div className="space-y-1">
                                {edges.length === 0 && <p className="text-sm text-muted-foreground">No connections</p>}
                                {edges.map(eid => {
                                    const src = graph.source(eid);
                                    const tgt = graph.target(eid);
                                    return (
                                        <div key={eid} className="text-sm text-muted-foreground">
                                            {src} → {tgt}
                                        </div>
                                    );
                                })}
                            </div>
                        </div>
                    );
                }
                return (
                    <div className="p-6 space-y-3">
                        <h3 className="text-lg font-semibold">{nodeAttrs.label}</h3>
                        <div className="grid grid-cols-2 gap-2 text-sm">
                            <span className="text-muted-foreground">ID</span>
                            <span className="font-mono">{lockedId}</span>
                            <span className="text-muted-foreground">Position</span>
                            <span className="font-mono">{Math.round(nodeAttrs.x)}, {Math.round(nodeAttrs.y)}</span>
                            <span className="text-muted-foreground">Size</span>
                            <span className="font-mono">{nodeAttrs.width} x {nodeAttrs.height}</span>
                            <span className="text-muted-foreground">Color</span>
                            <span className="flex items-center gap-1.5">
                                <span className="w-3 h-3 rounded-full inline-block" style={{ background: nodeAttrs.color }} />
                                <span className="font-mono">{nodeAttrs.color}</span>
                            </span>
                            <span className="text-muted-foreground">Type</span>
                            <span className="font-mono">{nodeAttrs.dbData?.node_type ?? 'default'}</span>
                        </div>
                    </div>
                );
            }}
            renderCloseButton={() => (
                <button
                    onClick={() => unlock()}
                    className="absolute top-4 right-4 p-2 rounded-lg bg-secondary hover:bg-secondary/80 text-foreground z-10"
                >
                    <X className="w-4 h-4" />
                </button>
            )}
        />
    );
}

// ── Context Menu State ──────────────────────────────────────────────────
function ContextMenuWrapper({ contextMenu, onClose, onDelete }: {
    contextMenu: { nodeId: string; x: number; y: number } | null;
    onClose: () => void;
    onDelete: (nodeId: string) => void;
}) {
    if (!contextMenu) return null;
    return (
        <NodeContextMenu
            open={true}
            onOpenChange={(open) => { if (!open) onClose(); }}
            nodeId={contextMenu.nodeId}
            actions={[
                {
                    key: 'delete',
                    label: 'Delete Node',
                    destructive: true,
                    onClick: (ctx) => { onDelete(ctx.nodeId); onClose(); },
                },
                {
                    key: 'lock',
                    label: 'View Details',
                    onClick: () => { onClose(); },
                },
            ]}
        />
    );
}

// ── Sidebar ────────────────────────────────────────────────────────────
function Sidebar({ onSetNodes, onToggleSettings, showSettings }: {
    onSetNodes: (n: number) => void;
    onToggleSettings: () => void;
    showSettings: boolean;
}) {
    const fps = useFps();
    const sel = useCanvasSelection();
    const vp = useCanvasViewport();
    const { nodeCount, edgeCount } = useCanvasGraph();
    const { undo, redo, canUndo, canRedo } = useCanvasHistory({ enableKeyboardShortcuts: true });
    const { fitToBounds } = useFitToBounds();
    const openCmd = useSetAtom(openCommandLineAtom);
    const snap = useAtomValue(snapEnabledAtom);
    const toggleSnap = useSetAtom(toggleSnapAtom);
    const gridSize = useAtomValue(snapGridSizeAtom);

    const fpsColor = fps >= 50 ? 'text-green-400' : fps >= 30 ? 'text-yellow-400' : 'text-red-400';

    return (
        <aside className="fixed top-0 left-0 bottom-0 w-64 bg-background/95 backdrop-blur-xl border-r border-border z-[200] flex flex-col overflow-y-auto">
            <div className="p-4 pb-3 border-b border-border">
                <p className="text-[11px] font-mono text-muted-foreground">
                    @blinksgg/canvas v{canvasVersion.version}
                </p>
                <h1 className="text-base font-bold mt-1">Feature Demo</h1>
            </div>

            <div className="px-4 py-3 border-b border-border">
                <span className={`text-3xl font-bold ${fpsColor}`}>{fps}</span>
                <span className="text-xs text-muted-foreground ml-1">FPS</span>
            </div>

            <SidebarSection title="Graph">
                <div className="flex flex-wrap gap-1.5">
                    <Badge variant="secondary">{nodeCount} nodes</Badge>
                    <Badge variant="secondary">{edgeCount} edges</Badge>
                    <Badge variant="secondary">{sel.count} selected</Badge>
                    <Badge variant="secondary">{(vp.zoom * 100).toFixed(0)}%</Badge>
                </div>
            </SidebarSection>

            <SidebarSection title="Performance">
                <VirtualizationStats />
            </SidebarSection>

            <SidebarSection title="Search">
                <SearchBar />
            </SidebarSection>

            <SidebarSection title="Node Count">
                <div className="flex flex-wrap gap-1">
                    {[10, 50, 100, 200, 500, 1000].map(n => (
                        <Button key={n} size="sm"
                            variant={nodeCount === n ? 'default' : 'secondary'}
                            className="h-7 px-2.5 text-xs font-mono"
                            onClick={() => onSetNodes(n)}>{n}</Button>
                    ))}
                </div>
            </SidebarSection>

            <SidebarSection title="Auto Layout">
                <LayoutControls />
            </SidebarSection>

            <SidebarSection title="Grouping">
                <GroupControls />
            </SidebarSection>

            <SidebarSection title="Actions">
                <div className="flex flex-col gap-1">
                    <Button variant="ghost" size="sm" className="justify-between" onClick={() => openCmd()}>
                        <span className="flex items-center gap-2"><Command className="w-3.5 h-3.5" /> Command Palette</span>
                        <kbd className="text-[10px] text-muted-foreground bg-secondary px-1.5 py-0.5 rounded">/</kbd>
                    </Button>
                    <Button variant="ghost" size="sm" className="justify-start gap-2" onClick={() => fitToBounds(FitToBoundsMode.Graph)}>
                        <Maximize className="w-3.5 h-3.5" /> Fit to View
                    </Button>
                    <div className="flex gap-1">
                        <Button variant="ghost" size="sm" className="flex-1 gap-1" disabled={!canUndo} onClick={undo}>
                            <Undo2 className="w-3.5 h-3.5" /> Undo
                        </Button>
                        <Button variant="ghost" size="sm" className="flex-1 gap-1" disabled={!canRedo} onClick={redo}>
                            <Redo2 className="w-3.5 h-3.5" /> Redo
                        </Button>
                    </div>
                    <Button variant="ghost" size="sm" className="justify-start gap-2" onClick={toggleSnap}>
                        <Magnet className="w-3.5 h-3.5" />
                        Snap: {snap ? 'ON' : 'OFF'} ({gridSize}px)
                    </Button>
                    <Button
                        variant={showSettings ? 'default' : 'ghost'}
                        size="sm" className="justify-start gap-2"
                        onClick={onToggleSettings}
                    >
                        <Settings className="w-3.5 h-3.5" /> Settings Panel
                    </Button>
                </div>
            </SidebarSection>

            <Separator />

            <div className="px-4 py-3 mt-auto text-[11px] text-muted-foreground leading-relaxed">
                <p className="font-semibold text-xs text-foreground mb-1.5">Controls</p>
                <div className="space-y-0.5">
                    <p><Move className="w-3 h-3 inline mr-1" /> Drag bg → Pan</p>
                    <p><MousePointerClick className="w-3 h-3 inline mr-1" /> Dbl-tap node → detail view</p>
                    <p><Lock className="w-3 h-3 inline mr-1" /> Right-click → context menu</p>
                    <p><Pencil className="w-3 h-3 inline mr-1" /> Pencil → Lasso</p>
                    <p><Keyboard className="w-3 h-3 inline mr-1" /> / → Palette · Ctrl+Z → Undo</p>
                    <p><Copy className="w-3 h-3 inline mr-1" /> Ctrl+C/V/D → Copy/Paste/Dupe</p>
                    <p><Trash2 className="w-3 h-3 inline mr-1" /> Del → Remove selected</p>
                    <p><Search className="w-3 h-3 inline mr-1" /> Ctrl+F → Search nodes</p>
                    <p><ArrowUpDown className="w-3 h-3 inline mr-1" /> Arrow keys → Navigate nodes</p>
                    <p><TreePine className="w-3 h-3 inline mr-1" /> Tree/Grid/Force → Auto layout</p>
                </div>
            </div>
        </aside>
    );
}

function SidebarSection({ title, children }: { title: string; children: React.ReactNode }) {
    return (
        <div className="px-4 py-3 border-b border-border">
            <p className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground mb-2">{title}</p>
            {children}
        </div>
    );
}

// ── Global keyboard handler ─────────────────────────────────────────────
function KeyboardHandler() { useGlobalKeyboard(); return null; }

// ── Main canvas with gesture v2 ─────────────────────────────────────────
let nodeCounter = 0;

function DemoCanvas() {
    const [graph] = useAtom(graphAtom);
    const setGraph = useSetAtom(graphAtom);
    const bumpVer = useSetAtom(graphUpdateVersionAtom);
    const bumpPos = useSetAtom(nodePositionUpdateCounterAtom);
    const [nodeCount, setNodeCount] = useState(20);
    const [showSettings, setShowSettings] = useState(false);
    const [contextMenu, setContextMenu] = useState<{ nodeId: string; x: number; y: number } | null>(null);
    const lockNode = useSetAtom(lockNodeAtom);
    const showToast = useSetAtom(showToastAtom);

    const bump = useCallback(() => {
        bumpVer((v: number) => v + 1);
        bumpPos((c: number) => c + 1);
    }, [bumpVer, bumpPos]);

    const loadGraph = useCallback((count: number) => {
        nodeCounter = count;
        setGraph(makeGraph(count));
        bump();
    }, [setGraph, bump]);

    useEffect(() => { loadGraph(nodeCount); }, []);

    const handleSetNodes = useCallback((n: number) => {
        setNodeCount(n);
        loadGraph(n);
    }, [loadGraph]);

    // Add node helper
    const addNodeAt = useCallback((x: number, y: number, label?: string) => {
        const id = `n${nodeCounter++}`;
        const color = COLORS[Math.floor(Math.random() * COLORS.length)];
        const lbl = label ?? `Node ${nodeCounter - 1}`;
        graph.addNode(id, {
            x: x - 80, y: y - 40, size: 10, width: W, height: H, color, label: lbl, zIndex: 0,
            dbData: {
                id, graph_id: 'demo', label: lbl, node_type: 'default', configuration: null,
                ui_properties: { x: x - 80, y: y - 40, width: W, height: H, color },
                created_at: new Date().toISOString(), updated_at: new Date().toISOString()
            }
        });
        bump();
        setNodeCount(graph.order);
    }, [graph, bump]);

    // Delete node helper
    const deleteNode = useCallback((nodeId: string) => {
        if (graph.hasNode(nodeId)) { graph.dropNode(nodeId); bump(); setNodeCount(graph.order); }
    }, [graph, bump]);

    // ── Unified onAction handler (gesture system v2) ──
    const onAction = useCallback((event: InputEvent, resolution: ResolvedAction) => {
        const { actionId } = resolution;
        // Only handle pointer gesture events (skip keyboard)
        if (event.kind === 'key') return;
        const pe = event as gesturesV2.PointerGestureEvent;
        const { subject, worldPosition } = pe;

        switch (actionId) {
            case 'create-node':
                addNodeAt(worldPosition.x, worldPosition.y);
                break;
            case 'context-menu': {
                if (subject.kind === 'node') {
                    setContextMenu({ nodeId: subject.nodeId, x: worldPosition.x, y: worldPosition.y });
                } else {
                    console.log('[action] context-menu background', worldPosition);
                }
                break;
            }
            case 'fit-to-view':
                if (subject.kind === 'node') {
                    lockNode({ nodeId: subject.nodeId });
                    showToast(`Viewing ${subject.nodeId}`);
                }
                break;
            case 'toggle-lock':
                if (subject.kind === 'node') {
                    lockNode({ nodeId: subject.nodeId });
                    showToast(`Locked ${subject.nodeId}`);
                }
                break;
            default:
                if (pe.phase === 'instant' || pe.phase === 'start') {
                    console.log('[action]', actionId, subject.kind, pe.type);
                }
        }
    }, [addNodeAt, lockNode, showToast]);

    // Command system callbacks
    const cmdCreateNode = useCallback(async (payload: CreateNodePayload) => {
        const props = payload.ui_properties as { x?: number; y?: number } | undefined;
        addNodeAt(props?.x ?? 0, props?.y ?? 0, payload.label);
    }, [addNodeAt]);

    const cmdDeleteNode = useCallback(async (nodeId: string) => deleteNode(nodeId), [deleteNode]);

    return (
        <CommandProvider onCreateNode={cmdCreateNode} onDeleteNode={cmdDeleteNode}>
            <KeyboardHandler />
            <ArrowKeyNav />
            <DragIndicator />

            <div className="w-screen h-screen pl-64 box-border">
                <Canvas
                    renderNode={(props) => <DemoNode {...props} />}
                    style={{ background: 'linear-gradient(145deg, #0f172a 0%, #1e1b4b 50%, #0f172a 100%)' }}
                    onAction={onAction}
                    onSelectionChange={useCallback((selectedNodeIds: Set<string>, selectedEdgeId: string | null) => {
                        console.log('[selection]', { nodes: [...selectedNodeIds], edge: selectedEdgeId });
                    }, [])}
                    onViewportChange={useCallback((viewport: { zoom: number; pan: { x: number; y: number } }) => {
                        console.log('[viewport]', { zoom: viewport.zoom.toFixed(2), pan: viewport.pan });
                    }, [])}
                    onDragStart={useCallback((nodeIds: string[]) => {
                        console.log('[drag:start]', nodeIds);
                    }, [])}
                    onDragEnd={useCallback((nodeIds: string[], positions: Map<string, { x: number; y: number }>) => {
                        console.log('[drag:end]', nodeIds, Object.fromEntries(positions));
                    }, [])}
                >
                    <ViewportControls />
                    <CommandFeedbackOverlay />
                    <Minimap position="bottom-right" />
                    <SelectionOverlay />
                    <CanvasAnimations />
                    <CanvasToast />
                    <TouchActionButton position="bottom-left" />
                    <AlignmentGuides />
                </Canvas>
            </div>

            <CommandLine />
            <LockedOverlay />
            <ContextMenuWrapper
                contextMenu={contextMenu}
                onClose={() => setContextMenu(null)}
                onDelete={deleteNode}
            />

            {showSettings && (
                <div className="fixed top-0 right-0 bottom-0 w-80 bg-background/95 backdrop-blur-xl border-l border-border z-[200] overflow-y-auto">
                    <SettingsPanel
                        className="p-4"
                        headerClassName="text-sm font-semibold mb-4"
                        contentClassName="space-y-3"
                        rowClassName="flex items-center justify-between py-1.5"
                        labelClassName="text-xs text-muted-foreground"
                        selectClassName="bg-secondary border border-border rounded px-2 py-1 text-xs"
                        buttonClassName="px-2 py-1 text-xs rounded bg-secondary hover:bg-secondary/80"
                        presetClassName="mb-4"
                        presetSelectClassName="bg-secondary border border-border rounded px-2 py-1 text-xs w-full"
                        presetButtonsClassName="flex gap-1 mt-2"
                        groupClassName="border border-border rounded-lg p-3 mb-3"
                        groupHeadingClassName="text-xs font-semibold uppercase tracking-wider text-muted-foreground mb-2"
                        onClose={() => setShowSettings(false)}
                    />
                </div>
            )}

            <Sidebar
                onSetNodes={handleSetNodes}
                onToggleSettings={() => setShowSettings(s => !s)}
                showSettings={showSettings}
            />
        </CommandProvider>
    );
}

// ── App root ───────────────────────────────────────────────────────────
export function App() {
    return (
        <JotaiProvider>
            <CanvasStyleProvider isDark={true}>
                <DemoCanvas />
            </CanvasStyleProvider>
        </JotaiProvider>
    );
}
