import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import { useCallback, useEffect, useMemo, useRef, useState, } from "react";
import { flushSync } from "react-dom";
import { ReactFlow, Controls, MiniMap, BaseEdge, useEdgesState, useNodesState, Handle, Position, MarkerType, } from "@xyflow/react";
import Dagre from "@dagrejs/dagre";
import "@xyflow/react/dist/style.css";
import { toast } from "sonner";
import { normalizeTaskStatus, getTaskStatusLabel } from "../taskStatus";
import { getTaskExecutionDisplayFromTask } from "../taskExecutionStatus";
import { cn } from "../../lib/utils";
import { saveGraphLayout } from "../services/taskService";
import { Button } from "../../components/ui/button";
import { Badge } from "../../components/ui/badge";
import { MultiSwitchButton } from "../../components/ui/multi-switch-button";
import { Tooltip, TooltipContent, TooltipTrigger, } from "../../components/ui/tooltip";
import { Circle, PlayCircle, Clock3, Eye, CheckCircle2, GitBranch, RotateCcw, Loader2, Plus, Maximize2, Minimize2, SquareSquare, AlertCircle, MoveHorizontal, MoveVertical, } from "lucide-react";
import { DeleteIcon } from "../../components/ui/delete-icon";
import { TaskAssigneeBadge } from "../components/TaskAssigneeBadge";
const NODE_WIDTH = 220;
const NODE_HEIGHT = 96;
const SAVE_DEBOUNCE_MS = 500;
const EDGE_BLOCKED_BY = "var(--color-component-shared)";
const EDGE_CHILD_TASK = "var(--color-component-suggestions)";
const EDGE_RELATED = "var(--color-neutral-grey-50)";
const STATUS_STYLES = {
    Todo: { bg: "bg-neutral-grey-5", text: "text-neutral-grey-50", icon: Circle },
    InProgress: {
        bg: "bg-feedback-blue-light",
        text: "text-feedback-blue",
        icon: PlayCircle,
    },
    Waiting: {
        bg: "bg-feedback-orange-light",
        text: "text-feedback-orange",
        icon: Clock3,
    },
    Review: { bg: "bg-highlight-10", text: "text-highlight-100", icon: Eye },
    Done: {
        bg: "bg-feedback-green-light",
        text: "text-feedback-green",
        icon: CheckCircle2,
    },
};
const PRIORITY_BAR_STYLES = {
    Critical: "bg-feedback-red-strong",
    High: "bg-component-shared",
    Medium: "bg-feedback-blue",
    Low: "bg-neutral-grey-50",
};
const PRIORITY_SELECTED_STYLES = {
    Critical: "border-feedback-red-strong ring-feedback-red-strong",
    High: "border-component-shared ring-component-shared",
    Medium: "border-feedback-blue ring-feedback-blue",
    Low: "border-neutral-grey-50 ring-neutral-grey-50",
};
const DEFAULT_SELECTED_STYLE = "border-feedback-blue ring-feedback-blue";
const PRIORITY_HOVER_STYLES = {
    Critical: "hover:border-feedback-red-strong/50",
    High: "hover:border-component-shared/50",
    Medium: "hover:border-feedback-blue/50",
    Low: "hover:border-neutral-grey-50/50",
};
const DEFAULT_HOVER_STYLE = "hover:border-neutral-grey-10/50";
const HANDLE_CLASS = "bg-neutral-grey-10! border-border-default! w-2! h-2!";
function TaskGraphNode({ data }) {
    const status = data.status;
    const statusStyle = STATUS_STYLES[status] || STATUS_STYLES.Todo;
    const StatusIcon = statusStyle.icon;
    const executionDisplay = data.showExecutionStateBadge
        ? getTaskExecutionDisplayFromTask({
            taskStatus: status,
            executionState: data.executionState,
            isBlocked: data.isBlocked,
        })
        : null;
    const priorityBarStyle = data.priority
        ? PRIORITY_BAR_STYLES[data.priority]
        : "bg-neutral-grey-10";
    const hoverStyle = data.priority
        ? PRIORITY_HOVER_STYLES[data.priority]
        : DEFAULT_HOVER_STYLE;
    const actionButtonClass = "nodrag nopan absolute z-20 flex h-5 w-5 items-center justify-center rounded-full border border-border-default bg-white text-neutral-grey-50 shadow-sm transition-colors hover:border-feedback-blue hover:bg-feedback-blue-light hover:text-feedback-blue";
    const dependentButtonClass = data.dependencySourceHandle === "right"
        ? "top-1/2 right-0 translate-x-[calc(100%+6px)] -translate-y-1/2"
        : "bottom-0 left-1/2 -translate-x-1/2 translate-y-[calc(100%+6px)]";
    const dependentTooltipSide = data.dependencySourceHandle === "right" ? "right" : "bottom";
    const subtaskButtonClass = data.hierarchySourceHandle === "right"
        ? "top-1/2 right-0 translate-x-[calc(100%+6px)] -translate-y-1/2"
        : "bottom-0 left-1/2 -translate-x-1/2 translate-y-[calc(100%+6px)]";
    const subtaskTooltipSide = data.hierarchySourceHandle === "right" ? "right" : "bottom";
    const isDecisionPoint = String(data.taskType ?? "").toLowerCase() === "decisionpoint";
    const selectedStyle = data.priority
        ? PRIORITY_SELECTED_STYLES[data.priority]
        : DEFAULT_SELECTED_STYLE;
    const nodeSurfaceClass = data.isSelected
        ? cn("shadow-lg ring-2", selectedStyle)
        : hoverStyle;
    return (_jsxs("div", { className: cn("group relative cursor-pointer overflow-visible rounded-lg border bg-white p-3 text-left shadow-sm transition-all hover:shadow-md", nodeSurfaceClass), style: { width: NODE_WIDTH }, "data-testid": `dependency-graph-node-${data.taskId}`, children: [_jsx("div", { className: cn("absolute inset-y-0 left-0 w-1 rounded-l-lg", priorityBarStyle) }), _jsx(Handle, { type: "target", position: Position.Left, id: "left", className: HANDLE_CLASS, "data-testid": `dependency-graph-target-left-${data.taskId}` }), _jsx(Handle, { type: "target", position: Position.Top, id: "top", className: HANDLE_CLASS, "data-testid": `dependency-graph-target-top-${data.taskId}` }), _jsxs("div", { className: "flex flex-col gap-2.5", children: [_jsxs("div", { className: "flex items-start justify-between gap-2", children: [_jsxs("div", { className: "min-w-0", children: [_jsx("div", { className: "text-muted-foreground mb-1 flex items-center gap-1 text-[10px] font-medium tracking-widest", children: _jsx("span", { className: "truncate", children: data.taskKey || data.taskId.slice(0, 8).toUpperCase() }) }), _jsx("div", { className: "line-clamp-2 text-sm leading-snug font-medium", children: data.title })] }), _jsxs("div", { className: "flex shrink-0 items-center gap-1", children: [isDecisionPoint ? (_jsx(GitBranch, { strokeWidth: 1, className: "text-highlight-100 h-4 w-4", "aria-label": "Decision Point" })) : null, data.priority === "Critical" ? (_jsx(AlertCircle, { strokeWidth: 1, className: "text-feedback-red h-4 w-4", "aria-label": "Critical priority" })) : null, data.onRemoveTask && (_jsx("button", { type: "button", className: "text-neutral-grey-50 hover:bg-neutral-grey-5 hover:text-neutral-grey-50 -mt-px -mr-0.5 shrink-0 rounded p-0.5 transition-colors", "aria-label": "Remove task", "data-testid": `dependency-graph-remove-task-${data.taskId}`, onClick: (event) => {
                                            event.stopPropagation();
                                            data.onRemoveTask?.(data.taskId);
                                        }, children: _jsx(DeleteIcon, { size: "xs" }) }))] })] }), _jsxs("div", { className: "mt-0.5 flex flex-wrap items-center gap-2", children: [isDecisionPoint ? (_jsx(Badge, { variant: "outline", size: "sm", className: "text-2xs border-highlight-100 bg-highlight-10 text-highlight-100", children: "Decision Point" })) : null, data.assigneeDisplayName && (_jsx(TaskAssigneeBadge, { assigneeType: data.assigneeType, label: data.assigneeDisplayName })), executionDisplay ? (_jsxs(Badge, { variant: executionDisplay.variant, size: "sm", className: cn("max-w-full font-bold", executionDisplay.className), title: executionDisplay.needsAttention
                                    ? "Assigned agent requires attention"
                                    : undefined, children: [executionDisplay.showSpinner && (_jsx(Loader2, { strokeWidth: 1, className: "mr-1 h-2.5 w-2.5 animate-spin" })), executionDisplay.label] })) : (_jsxs(Badge, { variant: "outline", size: "sm", className: cn("border-transparent font-bold", statusStyle.bg, statusStyle.text), children: [_jsx(StatusIcon, {}), getTaskStatusLabel(status)] }))] })] }), data.onAddDependentTaskFromNode && (_jsxs(Tooltip, { delayDuration: 250, children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx("button", { type: "button", className: cn(actionButtonClass, dependentButtonClass), "aria-label": "Add dependent task", "data-testid": `dependency-graph-add-dependent-task-${data.taskId}`, onClick: (event) => {
                                event.stopPropagation();
                                data.onSelect(data.taskId);
                                data.onAddDependentTaskFromNode?.(data.taskId);
                            }, onMouseDown: (event) => {
                                event.stopPropagation();
                            }, children: _jsx(Plus, { className: "h-3 w-3", strokeWidth: 1 }) }) }), _jsx(TooltipContent, { side: dependentTooltipSide, sideOffset: 8, children: "Add dependent task" })] })), data.onAddChildTaskFromNode && (_jsxs(Tooltip, { delayDuration: 250, children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx("button", { type: "button", className: cn(actionButtonClass, subtaskButtonClass), "aria-label": "Add subtask", "data-testid": `dependency-graph-add-subtask-${data.taskId}`, onClick: (event) => {
                                event.stopPropagation();
                                data.onSelect(data.taskId);
                                data.onAddChildTaskFromNode?.(data.taskId);
                            }, onMouseDown: (event) => {
                                event.stopPropagation();
                            }, children: _jsx(Plus, { className: "h-3 w-3", strokeWidth: 1 }) }) }), _jsx(TooltipContent, { side: subtaskTooltipSide, sideOffset: 8, children: "Add subtask" })] })), _jsx(Handle, { type: "source", position: Position.Right, id: "right", className: HANDLE_CLASS, "data-testid": `dependency-graph-source-right-${data.taskId}` }), _jsx(Handle, { type: "source", position: Position.Bottom, id: "bottom", className: HANDLE_CLASS, "data-testid": `dependency-graph-source-bottom-${data.taskId}` })] }));
}
const nodeTypes = {
    taskNode: TaskGraphNode,
};
function getHierarchyOrthogonalPath({ sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition, }) {
    const sameColumn = Math.abs(sourceX - targetX) < 1;
    const sameRow = Math.abs(sourceY - targetY) < 1;
    if (sameColumn || sameRow) {
        return `M ${sourceX} ${sourceY} L ${targetX} ${targetY}`;
    }
    if (sourcePosition === Position.Bottom && targetPosition === Position.Top) {
        const midY = sourceY + (targetY - sourceY) / 2;
        return `M ${sourceX} ${sourceY} L ${sourceX} ${midY} L ${targetX} ${midY} L ${targetX} ${targetY}`;
    }
    if (sourcePosition === Position.Right && targetPosition === Position.Left) {
        const midX = sourceX + (targetX - sourceX) / 2;
        return `M ${sourceX} ${sourceY} L ${midX} ${sourceY} L ${midX} ${targetY} L ${targetX} ${targetY}`;
    }
    const useHorizontalBend = Math.abs(targetX - sourceX) >= Math.abs(targetY - sourceY);
    if (useHorizontalBend) {
        const midX = sourceX + (targetX - sourceX) / 2;
        return `M ${sourceX} ${sourceY} L ${midX} ${sourceY} L ${midX} ${targetY} L ${targetX} ${targetY}`;
    }
    const midY = sourceY + (targetY - sourceY) / 2;
    return `M ${sourceX} ${sourceY} L ${sourceX} ${midY} L ${targetX} ${midY} L ${targetX} ${targetY}`;
}
function HierarchyOrthogonalEdge({ id, sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition, markerEnd, markerStart, style, label, labelStyle, labelShowBg, labelBgStyle, labelBgPadding, labelBgBorderRadius, interactionWidth, }) {
    const path = getHierarchyOrthogonalPath({
        sourceX,
        sourceY,
        targetX,
        targetY,
        sourcePosition,
        targetPosition,
    });
    return (_jsx(BaseEdge, { id: id, path: path, markerEnd: markerEnd, markerStart: markerStart, style: style, label: label, labelStyle: labelStyle, labelShowBg: labelShowBg, labelBgStyle: labelBgStyle, labelBgPadding: labelBgPadding, labelBgBorderRadius: labelBgBorderRadius, interactionWidth: interactionWidth }));
}
const edgeTypes = {
    hierarchyOrthogonal: HierarchyOrthogonalEdge,
};
const HIERARCHY_CHILD_GAP = 32;
const HIERARCHY_SUBTREE_GAP = 80;
function normalizeGraphNodeSize(size) {
    if (!size ||
        typeof size.width !== "number" ||
        typeof size.height !== "number" ||
        !Number.isFinite(size.width) ||
        !Number.isFinite(size.height) ||
        size.width <= 0 ||
        size.height <= 0) {
        return null;
    }
    return {
        width: Math.max(NODE_WIDTH, size.width),
        height: Math.max(NODE_HEIGHT, size.height),
    };
}
function getGraphNodeSize(nodeId, nodeSizeOverrides) {
    return (normalizeGraphNodeSize(nodeSizeOverrides?.get(nodeId)) ?? {
        width: NODE_WIDTH,
        height: NODE_HEIGHT,
    });
}
function getMeasuredNodeSizeOverrides(nodes) {
    if (!nodes?.length)
        return undefined;
    const sizes = new Map();
    for (const node of nodes) {
        const measuredNode = node;
        const measuredSize = normalizeGraphNodeSize(measuredNode.measured ?? {
            width: measuredNode.width,
            height: measuredNode.height,
        });
        if (measuredSize) {
            sizes.set(node.id, measuredSize);
        }
    }
    return sizes.size > 0 ? sizes : undefined;
}
function normalizeGraphOrientation(orientation) {
    return orientation === "vertical" ? "vertical" : "horizontal";
}
function getDependencyHandles(orientation) {
    return orientation === "horizontal"
        ? { source: "right", target: "left" }
        : { source: "bottom", target: "top" };
}
function getHierarchyHandles(orientation, autoLayoutStrategy) {
    if (autoLayoutStrategy !== "hierarchy") {
        return getDependencyHandles(orientation);
    }
    return orientation === "horizontal"
        ? { source: "bottom", target: "top" }
        : { source: "right", target: "left" };
}
/**
 * Compound layout for the hierarchy strategy: each parent-child group is
 * laid out perpendicular to the selected orientation, and then the resulting
 * subtree "super-nodes" are arranged via Dagre using the dependency edges
 * between them. Dagre receives root-anchored bounds so the top-level cards in a
 * dependency chain stay aligned even when one of them has a deeper child tree.
 */
function layoutGraphHierarchy(nodes, edges, orientation, nodeSizeOverrides) {
    if (nodes.length === 0)
        return { nodes, edges };
    const childrenOf = new Map();
    const parentOf = new Map();
    for (const edge of edges) {
        if (edge.data?.relationship !== "hierarchy")
            continue;
        parentOf.set(edge.target, edge.source);
        const kids = childrenOf.get(edge.source) ?? [];
        kids.push(edge.target);
        childrenOf.set(edge.source, kids);
    }
    const nodeOrder = new Map(nodes.map((node, index) => [node.id, index]));
    const dependencyEdges = edges.filter((edge) => edge.data?.relationship === "dependency");
    const subtreeNodeIds = new Map();
    function getSubtreeNodeIds(nodeId) {
        const cached = subtreeNodeIds.get(nodeId);
        if (cached)
            return cached;
        const result = new Set();
        const stack = [nodeId];
        while (stack.length > 0) {
            const currentId = stack.pop();
            if (result.has(currentId))
                continue;
            result.add(currentId);
            for (const childId of childrenOf.get(currentId) ?? []) {
                stack.push(childId);
            }
        }
        subtreeNodeIds.set(nodeId, result);
        return result;
    }
    function orderHierarchyChildren(parentId, children) {
        if (children.length < 2)
            return children;
        const originalOrder = new Map(children.map((childId, index) => [childId, index]));
        const parentSubtree = getSubtreeNodeIds(parentId);
        const branchByNodeId = new Map();
        for (const childId of children) {
            for (const nodeId of getSubtreeNodeIds(childId)) {
                branchByNodeId.set(nodeId, childId);
            }
        }
        const outgoingBranchEdges = new Map();
        const incomingBranchCounts = new Map(children.map((childId) => [childId, 0]));
        const externalContinuationRank = new Map(children.map((childId) => [childId, Number.POSITIVE_INFINITY]));
        for (const edge of dependencyEdges) {
            const sourceBranch = branchByNodeId.get(edge.source);
            if (!sourceBranch)
                continue;
            const targetBranch = branchByNodeId.get(edge.target);
            if (targetBranch && targetBranch !== sourceBranch) {
                const outgoing = outgoingBranchEdges.get(sourceBranch) ?? new Set();
                if (!outgoing.has(targetBranch)) {
                    outgoing.add(targetBranch);
                    outgoingBranchEdges.set(sourceBranch, outgoing);
                    incomingBranchCounts.set(targetBranch, (incomingBranchCounts.get(targetBranch) ?? 0) + 1);
                }
            }
            if (!parentSubtree.has(edge.target)) {
                externalContinuationRank.set(sourceBranch, Math.min(externalContinuationRank.get(sourceBranch) ??
                    Number.POSITIVE_INFINITY, nodeOrder.get(edge.target) ?? Number.MAX_SAFE_INTEGER));
            }
        }
        const compareBranches = (a, b) => {
            const continuationDelta = (externalContinuationRank.get(a) ?? Number.POSITIVE_INFINITY) -
                (externalContinuationRank.get(b) ?? Number.POSITIVE_INFINITY);
            if (continuationDelta !== 0)
                return continuationDelta;
            return ((nodeOrder.get(a) ?? originalOrder.get(a) ?? Number.MAX_SAFE_INTEGER) -
                (nodeOrder.get(b) ?? originalOrder.get(b) ?? Number.MAX_SAFE_INTEGER));
        };
        const ordered = [];
        const orderedSet = new Set();
        const ready = children
            .filter((childId) => (incomingBranchCounts.get(childId) ?? 0) === 0)
            .sort(compareBranches);
        while (ready.length > 0) {
            const childId = ready.shift();
            if (orderedSet.has(childId))
                continue;
            ordered.push(childId);
            orderedSet.add(childId);
            for (const nextChildId of outgoingBranchEdges.get(childId) ?? []) {
                const nextIncomingCount = (incomingBranchCounts.get(nextChildId) ?? 0) - 1;
                incomingBranchCounts.set(nextChildId, nextIncomingCount);
                if (nextIncomingCount === 0) {
                    ready.push(nextChildId);
                    ready.sort(compareBranches);
                }
            }
        }
        return [
            ...ordered,
            ...children
                .filter((childId) => !orderedSet.has(childId))
                .sort(compareBranches),
        ];
    }
    for (const [parentId, children] of childrenOf) {
        childrenOf.set(parentId, orderHierarchyChildren(parentId, children));
    }
    const roots = nodes.filter((n) => !parentOf.has(n.id));
    function addTranslatedPositions(target, positions, offsetX, offsetY) {
        for (const [nodeId, position] of positions) {
            target.set(nodeId, {
                x: offsetX + position.x,
                y: offsetY + position.y,
            });
        }
    }
    function measureSubtree(nodeId) {
        const nodeSize = getGraphNodeSize(nodeId, nodeSizeOverrides);
        const children = childrenOf.get(nodeId) ?? [];
        if (children.length === 0) {
            return {
                positions: new Map([[nodeId, { x: 0, y: 0 }]]),
                width: nodeSize.width,
                height: nodeSize.height,
            };
        }
        const childLayouts = children.map((childId) => measureSubtree(childId));
        const positions = new Map();
        if (orientation === "horizontal") {
            const childRowWidth = childLayouts.reduce((sum, child) => sum + child.width, 0) +
                HIERARCHY_CHILD_GAP * (childLayouts.length - 1);
            const childRowHeight = Math.max(...childLayouts.map((child) => child.height));
            const width = Math.max(nodeSize.width, childRowWidth);
            const height = nodeSize.height + HIERARCHY_CHILD_GAP + childRowHeight;
            positions.set(nodeId, { x: (width - nodeSize.width) / 2, y: 0 });
            let childX = (width - childRowWidth) / 2;
            for (const childLayout of childLayouts) {
                addTranslatedPositions(positions, childLayout.positions, childX, nodeSize.height + HIERARCHY_CHILD_GAP);
                childX += childLayout.width + HIERARCHY_CHILD_GAP;
            }
            return {
                positions,
                width,
                height,
            };
        }
        const childColumnWidth = Math.max(...childLayouts.map((child) => child.width));
        const childColumnHeight = childLayouts.reduce((sum, child) => sum + child.height, 0) +
            HIERARCHY_CHILD_GAP * (childLayouts.length - 1);
        const width = nodeSize.width + HIERARCHY_CHILD_GAP + childColumnWidth;
        const height = Math.max(nodeSize.height, childColumnHeight);
        positions.set(nodeId, { x: 0, y: (height - nodeSize.height) / 2 });
        let childY = (height - childColumnHeight) / 2;
        for (const childLayout of childLayouts) {
            addTranslatedPositions(positions, childLayout.positions, nodeSize.width + HIERARCHY_CHILD_GAP, childY);
            childY += childLayout.height + HIERARCHY_CHILD_GAP;
        }
        return {
            positions,
            width,
            height,
        };
    }
    const subtrees = new Map();
    for (const root of roots) {
        subtrees.set(root.id, measureSubtree(root.id));
    }
    const nodeToRoot = new Map();
    for (const [rootId, data] of subtrees) {
        for (const nodeId of data.positions.keys()) {
            nodeToRoot.set(nodeId, rootId);
        }
    }
    const g = new Dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({}));
    g.setGraph({
        rankdir: orientation === "horizontal" ? "LR" : "TB",
        nodesep: 40,
        ranksep: HIERARCHY_SUBTREE_GAP,
    });
    for (const [rootId, data] of subtrees) {
        const rootPosition = data.positions.get(rootId) ?? { x: 0, y: 0 };
        const rootSize = getGraphNodeSize(rootId, nodeSizeOverrides);
        const rootCenterX = rootPosition.x + rootSize.width / 2;
        const rootCenterY = rootPosition.y + rootSize.height / 2;
        g.setNode(rootId, {
            width: Math.max(rootCenterX, data.width - rootCenterX) * 2,
            height: Math.max(rootCenterY, data.height - rootCenterY) * 2,
        });
    }
    const addedSuperEdges = new Set();
    for (const edge of edges) {
        if (edge.data?.relationship !== "dependency")
            continue;
        const srcRoot = nodeToRoot.get(edge.source);
        const tgtRoot = nodeToRoot.get(edge.target);
        if (srcRoot && tgtRoot && srcRoot !== tgtRoot) {
            const key = `${srcRoot}->${tgtRoot}`;
            if (!addedSuperEdges.has(key)) {
                addedSuperEdges.add(key);
                g.setEdge(srcRoot, tgtRoot);
            }
        }
    }
    Dagre.layout(g);
    const finalPositions = new Map();
    for (const [rootId, data] of subtrees) {
        const superNode = g.node(rootId);
        const rootPosition = data.positions.get(rootId) ?? { x: 0, y: 0 };
        const rootSize = getGraphNodeSize(rootId, nodeSizeOverrides);
        const offsetX = superNode.x - rootPosition.x - rootSize.width / 2;
        const offsetY = superNode.y - rootPosition.y - rootSize.height / 2;
        for (const [nodeId, localPos] of data.positions) {
            finalPositions.set(nodeId, {
                x: offsetX + localPos.x,
                y: offsetY + localPos.y,
            });
        }
    }
    return {
        nodes: nodes.map((node) => ({
            ...node,
            position: finalPositions.get(node.id) ?? node.position,
        })),
        edges,
    };
}
function layoutGraph(nodes, edges, orientation, autoLayoutStrategy, nodeSizeOverrides) {
    if (autoLayoutStrategy === "hierarchy" &&
        edges.some((e) => e.data?.relationship === "hierarchy")) {
        return layoutGraphHierarchy(nodes, edges, orientation, nodeSizeOverrides);
    }
    const g = new Dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({}));
    g.setGraph({
        rankdir: orientation === "horizontal" ? "LR" : "TB",
        nodesep: 40,
        ranksep: 60,
    });
    for (const node of nodes) {
        g.setNode(node.id, getGraphNodeSize(node.id, nodeSizeOverrides));
    }
    for (const edge of edges) {
        g.setEdge(edge.source, edge.target);
    }
    Dagre.layout(g);
    const layoutedNodes = nodes.map((node) => {
        const dagNode = g.node(node.id);
        const nodeSize = getGraphNodeSize(node.id, nodeSizeOverrides);
        return {
            ...node,
            position: {
                x: dagNode.x - nodeSize.width / 2,
                y: dagNode.y - nodeSize.height / 2,
            },
        };
    });
    return { nodes: layoutedNodes, edges };
}
function buildGraph(tasks, dependencies, selectedTaskId, selectedDependencyId, blockedTaskIds, orientation, showExecutionStateBadges, autoLayoutStrategy, onSelect, onAddDependentTaskFromNode, onAddChildTaskFromNode, onRemoveTask, nodeSizeOverrides) {
    const taskMap = new Map();
    for (const t of tasks)
        taskMap.set(t.taskId, t);
    const visibleTasks = tasks;
    const childRelationshipKeys = new Set(visibleTasks
        .filter((task) => !!task.parentTaskId)
        .map((task) => `${task.parentTaskId}->${task.taskId}`));
    const dependencyHandles = getDependencyHandles(orientation);
    const hierarchyHandles = getHierarchyHandles(orientation, autoLayoutStrategy);
    const nodes = visibleTasks.map((task) => ({
        id: task.taskId,
        type: "taskNode",
        position: { x: 0, y: 0 },
        data: {
            taskId: task.taskId,
            taskKey: task.taskKey ?? null,
            title: task.title,
            taskType: task.taskType ?? null,
            status: normalizeTaskStatus(task.status, task.executionState),
            executionState: task.executionState,
            showExecutionStateBadge: showExecutionStateBadges,
            priority: task.priority ?? null,
            assigneeType: task.assigneeType ?? null,
            assigneeDisplayName: task.assigneeDisplayName ?? null,
            isSelected: task.taskId === selectedTaskId,
            isBlocked: blockedTaskIds.has(task.taskId),
            orientation,
            dependencySourceHandle: dependencyHandles.source,
            hierarchySourceHandle: hierarchyHandles.source,
            onSelect,
            onAddDependentTaskFromNode,
            onAddChildTaskFromNode,
            onRemoveTask,
        },
    }));
    const depSourceHandle = dependencyHandles.source;
    const depTargetHandle = dependencyHandles.target;
    const hierSourceHandle = hierarchyHandles.source;
    const hierTargetHandle = hierarchyHandles.target;
    const dependencyEdgeType = autoLayoutStrategy === "hierarchy" ? "step" : "smoothstep";
    const hierarchyEdgeType = autoLayoutStrategy === "hierarchy" ? "hierarchyOrthogonal" : "smoothstep";
    const dependencyEdges = dependencies
        .filter((dep) => taskMap.has(dep.taskId) &&
        taskMap.has(dep.dependsOnTaskId) &&
        !childRelationshipKeys.has(`${dep.dependsOnTaskId}->${dep.taskId}`))
        .map((dep) => {
        const isBlockedBy = dep.dependencyType === "BlockedBy";
        const strokeColor = isBlockedBy ? EDGE_BLOCKED_BY : EDGE_RELATED;
        const sourceTaskType = String(taskMap.get(dep.dependsOnTaskId)?.taskType ?? "").toLowerCase();
        const isDecisionEdge = isBlockedBy && sourceTaskType === "decisionpoint";
        const isSelected = dep.taskId === selectedTaskId &&
            dep.dependsOnTaskId === selectedDependencyId;
        return {
            id: dep.dependencyId,
            source: dep.dependsOnTaskId,
            target: dep.taskId,
            sourceHandle: depSourceHandle,
            targetHandle: depTargetHandle,
            type: dependencyEdgeType,
            animated: false,
            style: {
                stroke: isSelected ? "var(--color-neutral-grey-100)" : strokeColor,
                strokeWidth: isSelected ? 3 : isBlockedBy ? 2 : 1.5,
                strokeDasharray: isDecisionEdge ? "6 4" : undefined,
            },
            markerEnd: {
                type: MarkerType.ArrowClosed,
                color: isSelected ? "var(--color-neutral-grey-100)" : strokeColor,
                width: 16,
                height: 16,
            },
            label: isBlockedBy
                ? isDecisionEdge
                    ? "decision"
                    : undefined
                : "related",
            labelStyle: {
                fontSize: 10,
                fill: isDecisionEdge ? EDGE_BLOCKED_BY : EDGE_RELATED,
            },
            data: {
                relationship: "dependency",
                taskId: dep.taskId,
                dependsOnTaskId: dep.dependsOnTaskId,
            },
            selectable: true,
        };
    });
    const hierarchyEdges = visibleTasks
        .filter((task) => !!task.parentTaskId && taskMap.has(task.parentTaskId))
        .map((task) => ({
        id: `parent:${task.parentTaskId}:${task.taskId}`,
        source: task.parentTaskId,
        target: task.taskId,
        sourceHandle: hierSourceHandle,
        targetHandle: hierTargetHandle,
        type: hierarchyEdgeType,
        style: {
            stroke: "var(--color-component-default)",
            strokeWidth: 1.5,
        },
        markerEnd: {
            type: MarkerType.ArrowClosed,
            color: "var(--color-component-default)",
            width: 16,
            height: 16,
        },
        data: {
            relationship: "hierarchy",
        },
    }));
    const edges = [...dependencyEdges, ...hierarchyEdges];
    if (nodes.length === 0)
        return { nodes: [], edges: [] };
    return layoutGraph(nodes, edges, orientation, autoLayoutStrategy, nodeSizeOverrides);
}
export function buildDependencyGraphLayout(args) {
    return buildGraph(args.tasks, args.dependencies, null, null, new Set(), args.orientation ?? "horizontal", false, args.autoLayoutStrategy ?? "hierarchy", () => undefined);
}
function applySavedLayout(nodes, savedLayout) {
    if (!savedLayout?.nodes?.length) {
        return nodes;
    }
    const positionsByTaskId = new Map(savedLayout.nodes.map((node) => [
        node.taskId,
        {
            x: node.x,
            y: node.y,
        },
    ]));
    return nodes.map((node) => ({
        ...node,
        position: positionsByTaskId.get(node.id) ?? node.position,
    }));
}
function preserveNodePositions(previousNodes, nextNodes) {
    const previousPositions = new Map(previousNodes.map((node) => [node.id, node.position]));
    return nextNodes.map((node) => ({
        ...node,
        position: previousPositions.get(node.id) ?? node.position,
    }));
}
function buildChildRelationshipKeys(tasks) {
    return new Set(tasks
        .filter((task) => !!task.parentTaskId)
        .map((task) => `${task.parentTaskId}->${task.taskId}`));
}
function isHierarchyConnectionValid(connection, tasks, orientation, autoLayoutStrategy) {
    if (autoLayoutStrategy !== "hierarchy") {
        return false;
    }
    const { source, sourceHandle, target, targetHandle } = connection;
    if (!source || !target || source === target) {
        return false;
    }
    if (sourceHandle !== getHierarchyHandles(orientation, autoLayoutStrategy).source) {
        return false;
    }
    if (targetHandle && targetHandle !== "left" && targetHandle !== "top") {
        return false;
    }
    const validTaskIds = new Set(tasks.map((task) => task.taskId));
    if (!validTaskIds.has(source) || !validTaskIds.has(target)) {
        return false;
    }
    const parentByTaskId = new Map(tasks.map((task) => [task.taskId, task.parentTaskId ?? null]));
    let ancestorTaskId = parentByTaskId.get(source) ?? null;
    while (ancestorTaskId) {
        if (ancestorTaskId === target) {
            return false;
        }
        ancestorTaskId = parentByTaskId.get(ancestorTaskId) ?? null;
    }
    return !buildChildRelationshipKeys(tasks).has(`${source}->${target}`);
}
function isDependencyConnectionValid(connection, tasks, dependencies, orientation) {
    const { source, sourceHandle, target, targetHandle } = connection;
    if (!source || !target || source === target) {
        return false;
    }
    if (sourceHandle !== getDependencyHandles(orientation).source) {
        return false;
    }
    if (targetHandle && targetHandle !== "left" && targetHandle !== "top") {
        return false;
    }
    const validTaskIds = new Set(tasks.map((task) => task.taskId));
    if (!validTaskIds.has(source) || !validTaskIds.has(target)) {
        return false;
    }
    if (dependencies.some((dependency) => dependency.dependsOnTaskId === source && dependency.taskId === target)) {
        return false;
    }
    return !buildChildRelationshipKeys(tasks).has(`${source}->${target}`);
}
function isGraphConnectionValid(connection, tasks, dependencies, orientation, autoLayoutStrategy) {
    const hierarchySourceHandle = getHierarchyHandles(orientation, "hierarchy").source;
    if (autoLayoutStrategy === "hierarchy" &&
        connection.sourceHandle === hierarchySourceHandle) {
        return isHierarchyConnectionValid(connection, tasks, orientation, autoLayoutStrategy);
    }
    return isDependencyConnectionValid(connection, tasks, dependencies, orientation);
}
function createLayoutSignature(layoutKey, nodes, viewport, orientation) {
    return JSON.stringify({
        layoutKey,
        orientation,
        nodes: nodes
            .map((node) => ({
            taskId: node.id,
            x: node.position.x,
            y: node.position.y,
        }))
            .sort((a, b) => a.taskId.localeCompare(b.taskId)),
        viewport: viewport
            ? {
                x: viewport.x,
                y: viewport.y,
                zoom: viewport.zoom,
            }
            : null,
    });
}
function createGraphSyncSignature(nodes, edges) {
    return JSON.stringify({
        nodes: nodes
            .map((node) => ({
            id: node.id,
            x: node.position.x,
            y: node.position.y,
            selected: node.data.isSelected,
            status: node.data.status,
            executionState: node.data.executionState ?? null,
            priority: node.data.priority ?? null,
            blocked: node.data.isBlocked,
            taskType: node.data.taskType ?? null,
            assigneeType: node.data.assigneeType ?? null,
            assignee: node.data.assigneeDisplayName ?? null,
        }))
            .sort((a, b) => a.id.localeCompare(b.id)),
        edges: edges
            .map((edge) => ({
            id: edge.id,
            source: edge.source,
            target: edge.target,
            sourceHandle: edge.sourceHandle ?? null,
            targetHandle: edge.targetHandle ?? null,
        }))
            .sort((a, b) => a.id.localeCompare(b.id)),
    });
}
export function DependencyGraphView(props) {
    const { projectId, layoutKey, tasks, dependencies, savedLayout, selectedTaskId, selectedDependencyId = null, onSelectTask, onSelectDependency, onClearDependencySelection, permission, canPersistLayout: canPersistLayoutOverride, onPersistLayout, onLayoutSaved, highlightBlockedTasks = true, showExecutionStateBadges = true, emptyStateTitle = "No tasks to visualize", emptyStateDescription = "Create tasks in this project to see them in the dependency graph.", orientation = "vertical", autoLayoutStrategy = "allEdges", showOrientationToggle = false, layoutSaveDebounceMs = SAVE_DEBOUNCE_MS, onAddDependentTaskFromNode, onAddChildTaskFromNode, onRemoveTask, allowDependencyConnect = false, onCreateDependency, onCreateChildRelationship, miniMapWidth, miniMapHeight, showMiniMap, isMaximized = false, onToggleMaximized, } = props;
    const canPersistLayout = typeof canPersistLayoutOverride === "boolean"
        ? canPersistLayoutOverride
        : permission === "Owner" || permission === "Editor";
    const effectiveLayoutKey = layoutKey ?? projectId ?? "graph";
    const fallbackOrientation = normalizeGraphOrientation(orientation);
    const savedLayoutOrientation = savedLayout
        ? normalizeGraphOrientation(savedLayout.orientation ?? fallbackOrientation)
        : null;
    const [transientOrientation, setTransientOrientation] = useState(null);
    const effectiveOrientation = transientOrientation ?? savedLayoutOrientation ?? fallbackOrientation;
    const taskMap = useMemo(() => {
        const m = new Map();
        for (const t of tasks)
            m.set(t.taskId, t);
        return m;
    }, [tasks]);
    const blockedTaskIds = useMemo(() => {
        if (!highlightBlockedTasks) {
            return new Set();
        }
        const blocked = new Set();
        for (const task of tasks) {
            if ((task.executionBlockedByProjectIds?.length ?? 0) > 0) {
                blocked.add(task.taskId);
            }
        }
        for (const dep of dependencies) {
            if (dep.dependencyType !== "BlockedBy")
                continue;
            const blocker = taskMap.get(dep.dependsOnTaskId);
            if (!blocker ||
                normalizeTaskStatus(blocker.status, blocker.executionState) !== "Done") {
                blocked.add(dep.taskId);
            }
        }
        return blocked;
    }, [dependencies, highlightBlockedTasks, taskMap]);
    const onSelect = useCallback((taskId) => onSelectTask(taskId), [onSelectTask]);
    const { nodes: initialNodes, edges: initialEdges } = useMemo(() => buildGraph(tasks, dependencies, selectedTaskId, selectedDependencyId, blockedTaskIds, effectiveOrientation, showExecutionStateBadges, autoLayoutStrategy, onSelect, onAddDependentTaskFromNode, onAddChildTaskFromNode, onRemoveTask), [
        tasks,
        dependencies,
        selectedTaskId,
        selectedDependencyId,
        blockedTaskIds,
        effectiveOrientation,
        showExecutionStateBadges,
        autoLayoutStrategy,
        onSelect,
        onAddDependentTaskFromNode,
        onAddChildTaskFromNode,
        onRemoveTask,
    ]);
    const [nodes, setNodes, onNodesChangeBase] = useNodesState(initialNodes);
    const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
    const nodesRef = useRef(nodes);
    const suppressNodeSelectionUntilRef = useRef(0);
    const onNodesChange = useCallback((changes) => {
        onNodesChangeBase(changes);
        const hasPositionChange = changes.some((c) => c.type === "position" && c.position);
        if (hasPositionChange) {
            nodesRef.current = nodesRef.current.map((n) => {
                const change = changes.find((c) => c.type === "position" && c.id === n.id && c.position);
                return change && change.type === "position" && change.position
                    ? { ...n, position: change.position }
                    : n;
            });
        }
    }, [onNodesChangeBase]);
    const reactFlowRef = useRef(null);
    const graphContainerRef = useRef(null);
    const [graphContainerSize, setGraphContainerSize] = useState(null);
    useEffect(() => {
        const node = graphContainerRef.current;
        if (!node || typeof ResizeObserver === "undefined")
            return;
        const observer = new ResizeObserver((entries) => {
            for (const entry of entries) {
                const { width, height } = entry.contentRect;
                setGraphContainerSize({ width, height });
            }
        });
        observer.observe(node);
        return () => observer.disconnect();
    }, []);
    const MINIMAP_HIDE_BELOW_WIDTH = 600;
    const MINIMAP_MIN_WIDTH = 240;
    const MINIMAP_MAX_WIDTH = 400;
    const MINIMAP_MIN_HEIGHT = 180;
    const MINIMAP_MAX_HEIGHT = 300;
    const MINIMAP_SCALE = 0.36;
    const responsiveMiniMap = useMemo(() => {
        if (!graphContainerSize)
            return null;
        const { width, height } = graphContainerSize;
        if (width < MINIMAP_HIDE_BELOW_WIDTH) {
            return { hide: true, width: 0, height: 0 };
        }
        const w = Math.round(Math.min(MINIMAP_MAX_WIDTH, Math.max(MINIMAP_MIN_WIDTH, width * MINIMAP_SCALE)));
        const h = Math.round(Math.min(MINIMAP_MAX_HEIGHT, Math.max(MINIMAP_MIN_HEIGHT, height * MINIMAP_SCALE)));
        return { hide: false, width: w, height: h };
    }, [graphContainerSize]);
    const effectiveShowMiniMap = typeof showMiniMap === "boolean"
        ? showMiniMap
        : !(responsiveMiniMap?.hide ?? false);
    const explicitMiniMapSize = miniMapWidth != null && miniMapHeight != null
        ? { width: miniMapWidth, height: miniMapHeight }
        : null;
    const effectiveMiniMapSize = explicitMiniMapSize ??
        (responsiveMiniMap && !responsiveMiniMap.hide
            ? { width: responsiveMiniMap.width, height: responsiveMiniMap.height }
            : null);
    const saveTimeoutRef = useRef(null);
    const pendingViewportRef = useRef(null);
    const fitViewOnNextSyncRef = useRef(false);
    const skipNextMoveEndSaveRef = useRef(false);
    const pendingSaveOrientationRef = useRef(null);
    // Hide the graph briefly while a relayout-then-fit cycle runs, so the user
    // doesn't see one frame of new node positions rendered with the old
    // (orientation-stale) viewport zoom before fitView snaps to the new bounds.
    const [isFittingView, setIsFittingView] = useState(false);
    const fittingViewClearRafRef = useRef({ outer: null, inner: null });
    const beginFittingView = useCallback(() => {
        setIsFittingView(true);
        const handles = fittingViewClearRafRef.current;
        if (handles.outer !== null)
            cancelAnimationFrame(handles.outer);
        if (handles.inner !== null)
            cancelAnimationFrame(handles.inner);
        // Two rAFs: the inner rAF inside restoreViewport calls fitView on the
        // first frame; we wait an additional frame for the resulting paint
        // before un-hiding.
        handles.outer = requestAnimationFrame(() => {
            handles.outer = null;
            handles.inner = requestAnimationFrame(() => {
                handles.inner = null;
                setIsFittingView(false);
            });
        });
    }, []);
    useEffect(() => {
        return () => {
            const handles = fittingViewClearRafRef.current;
            if (handles.outer !== null)
                cancelAnimationFrame(handles.outer);
            if (handles.inner !== null)
                cancelAnimationFrame(handles.inner);
        };
    }, []);
    const lastSavedSignatureRef = useRef(null);
    const lastAppliedLayoutKeyRef = useRef(null);
    const lastAppliedLayoutSignatureRef = useRef(null);
    const lastAppliedGraphSignatureRef = useRef(null);
    const lastAppliedNodeIdSignatureRef = useRef(null);
    const savedLayoutSignature = useMemo(() => savedLayout
        ? JSON.stringify({
            orientation: savedLayoutOrientation,
            updatedAt: savedLayout.updatedAt ?? null,
            nodes: savedLayout.nodes ?? [],
            viewport: savedLayout.viewport ?? null,
        })
        : "null", [savedLayout, savedLayoutOrientation]);
    const focusGraphContainer = useCallback(() => {
        const activeElement = document.activeElement;
        if (activeElement) {
            const tagName = activeElement.tagName.toLowerCase();
            if (tagName === "input" ||
                tagName === "textarea" ||
                tagName === "select" ||
                activeElement.isContentEditable) {
                activeElement.blur();
            }
        }
        graphContainerRef.current?.focus();
    }, []);
    useEffect(() => {
        setTransientOrientation(null);
        pendingSaveOrientationRef.current = null;
    }, [effectiveLayoutKey]);
    useEffect(() => {
        if (transientOrientation &&
            savedLayoutOrientation &&
            transientOrientation === savedLayoutOrientation) {
            setTransientOrientation(null);
        }
    }, [savedLayoutOrientation, transientOrientation]);
    const persistLayout = useCallback(async (nextNodes, viewport, nextOrientation) => {
        if (!canPersistLayout) {
            return;
        }
        const signature = createLayoutSignature(effectiveLayoutKey, nextNodes, viewport, nextOrientation);
        if (signature === lastSavedSignatureRef.current) {
            return;
        }
        try {
            const layoutPayload = {
                nodes: nextNodes.map((node) => ({
                    taskId: node.id,
                    x: node.position.x,
                    y: node.position.y,
                })),
                orientation: nextOrientation,
                viewport: viewport == null
                    ? null
                    : {
                        x: viewport.x,
                        y: viewport.y,
                        zoom: viewport.zoom,
                    },
            };
            const nextLayout = onPersistLayout
                ? await onPersistLayout(layoutPayload)
                : projectId
                    ? await (async () => {
                        const result = await saveGraphLayout({
                            projectId,
                            nodes: layoutPayload.nodes,
                            viewport: layoutPayload.viewport,
                            orientation: layoutPayload.orientation,
                        });
                        if (result.type !== "success") {
                            toast.error(result.summary || "Failed to save graph layout");
                            return null;
                        }
                        return result.data ?? null;
                    })()
                    : layoutPayload;
            lastSavedSignatureRef.current = signature;
            if (nextLayout) {
                const incomingSignature = JSON.stringify({
                    orientation: normalizeGraphOrientation(nextLayout.orientation ?? nextOrientation),
                    updatedAt: nextLayout.updatedAt ?? null,
                    nodes: nextLayout.nodes ?? [],
                    viewport: nextLayout.viewport ?? null,
                });
                lastAppliedLayoutSignatureRef.current = incomingSignature;
            }
            onLayoutSaved?.(nextLayout ?? null);
        }
        catch (error) {
            toast.error(error instanceof Error
                ? error.message
                : "Failed to save graph layout");
        }
    }, [
        canPersistLayout,
        effectiveLayoutKey,
        effectiveOrientation,
        onLayoutSaved,
        onPersistLayout,
        projectId,
    ]);
    const queueLayoutSave = useCallback((nextNodes, viewport, nextOrientation = effectiveOrientation) => {
        if (!canPersistLayout) {
            return;
        }
        const signature = createLayoutSignature(effectiveLayoutKey, nextNodes, viewport, nextOrientation);
        if (signature === lastSavedSignatureRef.current) {
            return;
        }
        if (saveTimeoutRef.current !== null) {
            window.clearTimeout(saveTimeoutRef.current);
            saveTimeoutRef.current = null;
        }
        if (layoutSaveDebounceMs === 0) {
            void persistLayout(nextNodes, viewport, nextOrientation);
            return;
        }
        saveTimeoutRef.current = window.setTimeout(() => {
            saveTimeoutRef.current = null;
            void persistLayout(nextNodes, viewport, nextOrientation);
        }, layoutSaveDebounceMs);
    }, [
        canPersistLayout,
        effectiveLayoutKey,
        effectiveOrientation,
        layoutSaveDebounceMs,
        persistLayout,
    ]);
    const restoreViewport = useCallback(() => {
        const instance = reactFlowRef.current;
        if (!instance || nodesRef.current.length === 0) {
            return;
        }
        if (pendingViewportRef.current) {
            const viewport = pendingViewportRef.current;
            pendingViewportRef.current = null;
            skipNextMoveEndSaveRef.current = true;
            window.requestAnimationFrame(() => {
                reactFlowRef.current?.setViewport(viewport, { duration: 0 });
                lastSavedSignatureRef.current = createLayoutSignature(effectiveLayoutKey, nodesRef.current, viewport, effectiveOrientation);
            });
            return;
        }
        if (!fitViewOnNextSyncRef.current) {
            return;
        }
        fitViewOnNextSyncRef.current = false;
        skipNextMoveEndSaveRef.current = true;
        window.requestAnimationFrame(() => {
            reactFlowRef.current?.fitView({ padding: 0.2 });
            if (!pendingSaveOrientationRef.current) {
                return;
            }
            const nextOrientation = pendingSaveOrientationRef.current;
            pendingSaveOrientationRef.current = null;
            const viewport = reactFlowRef.current?.getViewport() ?? null;
            queueLayoutSave(nodesRef.current, viewport, nextOrientation);
        });
    }, [effectiveLayoutKey, effectiveOrientation, queueLayoutSave]);
    useEffect(() => {
        const shouldApplySavedLayout = lastAppliedLayoutKeyRef.current !== effectiveLayoutKey ||
            lastAppliedLayoutSignatureRef.current !== savedLayoutSignature;
        const nextNodeIdSignature = initialNodes.map((node) => node.id).join("|");
        const savedLayoutNodeCount = savedLayout?.nodes?.length ?? 0;
        const shouldAutoLayoutForHierarchy = autoLayoutStrategy === "hierarchy" &&
            (savedLayoutNodeCount < initialNodes.length ||
                (lastAppliedNodeIdSignatureRef.current !== null &&
                    lastAppliedNodeIdSignatureRef.current !== nextNodeIdSignature));
        const nextNodes = shouldApplySavedLayout
            ? shouldAutoLayoutForHierarchy
                ? initialNodes
                : applySavedLayout(initialNodes, savedLayout)
            : shouldAutoLayoutForHierarchy
                ? initialNodes
                : preserveNodePositions(nodesRef.current, initialNodes);
        const nextGraphSignature = createGraphSyncSignature(nextNodes, initialEdges);
        if (!shouldApplySavedLayout &&
            !shouldAutoLayoutForHierarchy &&
            nextGraphSignature === lastAppliedGraphSignatureRef.current) {
            return;
        }
        setNodes(nextNodes);
        setEdges(initialEdges);
        nodesRef.current = nextNodes;
        lastAppliedGraphSignatureRef.current = nextGraphSignature;
        lastAppliedNodeIdSignatureRef.current = nextNodeIdSignature;
        if (shouldApplySavedLayout) {
            lastAppliedLayoutKeyRef.current = effectiveLayoutKey;
            lastAppliedLayoutSignatureRef.current = savedLayoutSignature;
            pendingViewportRef.current = shouldAutoLayoutForHierarchy
                ? null
                : (savedLayout?.viewport ?? null);
            fitViewOnNextSyncRef.current =
                shouldAutoLayoutForHierarchy || !savedLayout?.viewport;
            if (!savedLayout?.viewport || shouldAutoLayoutForHierarchy) {
                lastSavedSignatureRef.current = savedLayout?.nodes?.length
                    ? createLayoutSignature(effectiveLayoutKey, nextNodes, null, effectiveOrientation)
                    : null;
            }
            if (shouldAutoLayoutForHierarchy) {
                pendingSaveOrientationRef.current = effectiveOrientation;
            }
            restoreViewport();
            return;
        }
        if (shouldAutoLayoutForHierarchy) {
            pendingViewportRef.current = null;
            fitViewOnNextSyncRef.current = true;
            pendingSaveOrientationRef.current = effectiveOrientation;
            restoreViewport();
        }
    }, [
        autoLayoutStrategy,
        initialEdges,
        initialNodes,
        effectiveLayoutKey,
        effectiveOrientation,
        projectId,
        restoreViewport,
        savedLayout,
        savedLayoutSignature,
        setEdges,
        setNodes,
    ]);
    useEffect(() => {
        return () => {
            if (saveTimeoutRef.current !== null) {
                window.clearTimeout(saveTimeoutRef.current);
                saveTimeoutRef.current = null;
            }
        };
    }, []);
    const hasVisibleNodes = initialNodes.length > 0;
    const handleAutoLayout = useCallback(() => {
        const measuredNodeSizes = getMeasuredNodeSizeOverrides(reactFlowRef.current?.getNodes() ??
            nodesRef.current);
        const autoLayout = buildGraph(tasks, dependencies, selectedTaskId, selectedDependencyId, blockedTaskIds, effectiveOrientation, showExecutionStateBadges, autoLayoutStrategy, onSelect, onAddDependentTaskFromNode, onAddChildTaskFromNode, onRemoveTask, measuredNodeSizes);
        beginFittingView();
        flushSync(() => {
            setNodes(autoLayout.nodes);
            setEdges(autoLayout.edges);
        });
        nodesRef.current = autoLayout.nodes;
        reactFlowRef.current?.fitView({ padding: 0.2 });
        fitViewOnNextSyncRef.current = false;
        skipNextMoveEndSaveRef.current = true;
        pendingSaveOrientationRef.current = effectiveOrientation;
        const viewport = reactFlowRef.current?.getViewport() ?? null;
        queueLayoutSave(autoLayout.nodes, viewport, effectiveOrientation);
    }, [
        beginFittingView,
        blockedTaskIds,
        dependencies,
        effectiveOrientation,
        onSelect,
        showExecutionStateBadges,
        autoLayoutStrategy,
        onRemoveTask,
        onAddDependentTaskFromNode,
        onAddChildTaskFromNode,
        queueLayoutSave,
        selectedTaskId,
        selectedDependencyId,
        setEdges,
        setNodes,
        tasks,
    ]);
    const isValidConnection = useCallback((connection) => allowDependencyConnect
        ? isGraphConnectionValid(connection, tasks, dependencies, effectiveOrientation, autoLayoutStrategy)
        : false, [
        allowDependencyConnect,
        autoLayoutStrategy,
        dependencies,
        effectiveOrientation,
        tasks,
    ]);
    const handleConnect = useCallback((connection) => {
        if (!allowDependencyConnect) {
            return;
        }
        const { source, sourceHandle, target } = connection;
        if (!source || !target) {
            return;
        }
        if (!isGraphConnectionValid(connection, tasks, dependencies, effectiveOrientation, autoLayoutStrategy)) {
            return;
        }
        suppressNodeSelectionUntilRef.current = performance.now() + 150;
        if (autoLayoutStrategy === "hierarchy" &&
            sourceHandle ===
                getHierarchyHandles(effectiveOrientation, "hierarchy").source) {
            onCreateChildRelationship?.(source, target);
        }
        else {
            onCreateDependency?.(source, target);
        }
        window.requestAnimationFrame(() => {
            onSelectTask(target);
        });
    }, [
        allowDependencyConnect,
        autoLayoutStrategy,
        dependencies,
        effectiveOrientation,
        onCreateChildRelationship,
        onCreateDependency,
        onSelectTask,
        tasks,
    ]);
    const handleOrientationChange = useCallback((nextOrientation) => {
        const measuredNodeSizes = getMeasuredNodeSizeOverrides(reactFlowRef.current?.getNodes() ?? nodesRef.current);
        const autoLayout = buildGraph(tasks, dependencies, selectedTaskId, selectedDependencyId, blockedTaskIds, nextOrientation, showExecutionStateBadges, autoLayoutStrategy, onSelect, onAddDependentTaskFromNode, onAddChildTaskFromNode, onRemoveTask, measuredNodeSizes);
        // Hide while we relayout-then-fit so the user can't see a paint where
        // the new node positions are still being viewed through the old
        // (orientation-stale) viewport.
        beginFittingView();
        // flushSync forces React (and ReactFlow's @xyflow/react store, which is
        // wired through useNodesState) to commit the new positions before this
        // line returns. After commit the browser has a layout to measure; we
        // then call fitView synchronously so the very next paint already shows
        // the correct viewport — no rAF gap.
        flushSync(() => {
            setTransientOrientation(nextOrientation);
            setNodes(autoLayout.nodes);
            setEdges(autoLayout.edges);
        });
        nodesRef.current = autoLayout.nodes;
        reactFlowRef.current?.fitView({ padding: 0.2 });
        // Tell the deferred restoreViewport rAF (and the moveEnd handler that
        // fitView's setViewport will trigger) that the fit has already been
        // applied — we don't want a second fit to fight this one.
        fitViewOnNextSyncRef.current = false;
        skipNextMoveEndSaveRef.current = true;
        pendingSaveOrientationRef.current = nextOrientation;
        const viewport = reactFlowRef.current?.getViewport() ?? null;
        queueLayoutSave(autoLayout.nodes, viewport, nextOrientation);
    }, [
        autoLayoutStrategy,
        beginFittingView,
        blockedTaskIds,
        dependencies,
        onAddChildTaskFromNode,
        onAddDependentTaskFromNode,
        onRemoveTask,
        onSelect,
        queueLayoutSave,
        selectedDependencyId,
        selectedTaskId,
        setEdges,
        setNodes,
        showExecutionStateBadges,
        tasks,
    ]);
    return (_jsxs("div", { className: "flex h-full min-h-0 flex-col", children: [_jsxs("div", { className: "flex shrink-0 items-center justify-between gap-3 px-3 pt-2", children: [hasVisibleNodes && highlightBlockedTasks ? (_jsxs("div", { className: "text-neutral-grey-50 flex items-center gap-2 text-[11px]", children: [_jsxs("span", { className: "inline-flex items-center gap-1", children: [_jsx("span", { className: "bg-component-shared inline-block h-0.5 w-4 rounded-sm" }), "Blocked by"] }), _jsxs("span", { className: "inline-flex items-center gap-1", children: [_jsx("span", { className: "bg-neutral-grey-50 inline-block h-0.5 w-4 rounded-sm" }), "Related"] }), _jsxs("span", { className: "inline-flex items-center gap-1", children: [_jsx("span", { className: "bg-feedback-blue inline-block h-0.5 w-4 rounded-sm" }), "Child task"] })] })) : (_jsx("div", {})), hasVisibleNodes && (_jsxs("div", { className: "flex items-center gap-2", children: [showOrientationToggle && (_jsx(MultiSwitchButton, { value: effectiveOrientation, options: [
                                    {
                                        value: "horizontal",
                                        label: "Horizontal",
                                        icon: MoveHorizontal,
                                        testId: "dependency-graph-orientation-horizontal",
                                    },
                                    {
                                        value: "vertical",
                                        label: "Vertical",
                                        icon: MoveVertical,
                                        testId: "dependency-graph-orientation-vertical",
                                    },
                                ], onValueChange: handleOrientationChange, className: "inline-grid w-auto", optionClassName: "h-8 px-3", "aria-label": "Dependency graph orientation", testId: "dependency-graph-orientation-toggle" })), canPersistLayout && (_jsxs(Button, { type: "button", variant: "outline", size: "sm", className: "h-8 px-3", onClick: handleAutoLayout, children: [_jsx(RotateCcw, { strokeWidth: 1, className: "mr-1.5 h-4 w-4" }), "Auto layout"] }))] }))] }), _jsx("div", { className: "min-h-0 flex-1", ref: graphContainerRef, tabIndex: -1, style: { visibility: isFittingView ? "hidden" : "visible" }, children: !hasVisibleNodes ? (_jsx("div", { className: "flex h-full items-center justify-center", children: _jsxs("div", { className: "flex max-w-sm flex-col items-center gap-3 text-center", children: [_jsx("div", { className: "bg-neutral-grey-5 flex h-12 w-12 items-center justify-center rounded-full", children: _jsx(GitBranch, { strokeWidth: 1, className: "text-neutral-grey-50 h-6 w-6" }) }), _jsxs("div", { children: [_jsx("p", { className: "text-neutral-grey-50 text-sm font-medium", children: emptyStateTitle }), _jsx("p", { className: "text-2xs text-neutral-grey-50 mt-1", children: emptyStateDescription })] })] }) })) : (_jsxs(ReactFlow, { nodes: nodes, edges: edges, onNodesChange: onNodesChange, onEdgesChange: onEdgesChange, onConnect: handleConnect, onPaneClick: () => {
                        focusGraphContainer();
                        onClearDependencySelection?.();
                    }, onEdgeClick: (_event, edge) => {
                        focusGraphContainer();
                        if (edge.data?.relationship === "dependency" &&
                            edge.data.taskId &&
                            edge.data.dependsOnTaskId) {
                            onSelectDependency?.(edge.data.taskId, edge.data.dependsOnTaskId);
                        }
                    }, onNodeClick: (_event, node) => {
                        focusGraphContainer();
                        if (performance.now() < suppressNodeSelectionUntilRef.current) {
                            return;
                        }
                        onClearDependencySelection?.();
                        onSelectTask(node.id);
                    }, onNodeDragStart: (_event, node) => {
                        focusGraphContainer();
                        if (performance.now() < suppressNodeSelectionUntilRef.current) {
                            return;
                        }
                        onClearDependencySelection?.();
                        onSelectTask(node.id);
                    }, onInit: (instance) => {
                        reactFlowRef.current = instance;
                        restoreViewport();
                    }, onNodeDragStop: () => {
                        const allNodes = reactFlowRef.current?.getNodes() ?? nodesRef.current;
                        nodesRef.current = allNodes;
                        queueLayoutSave(allNodes, reactFlowRef.current?.getViewport() ?? null);
                    }, onMoveEnd: (_event, viewport) => {
                        if (skipNextMoveEndSaveRef.current) {
                            skipNextMoveEndSaveRef.current = false;
                            return;
                        }
                        queueLayoutSave(nodesRef.current, viewport);
                    }, nodeTypes: nodeTypes, edgeTypes: edgeTypes, minZoom: 0.2, maxZoom: 2, proOptions: { hideAttribution: true }, nodesDraggable: canPersistLayout, nodesConnectable: allowDependencyConnect, isValidConnection: isValidConnection, elementsSelectable: true, children: [_jsxs(Controls, { showInteractive: false, showFitView: false, className: "border-border-default! rounded-lg! shadow-md!", children: [_jsx("button", { type: "button", className: "react-flow__controls-button react-flow__controls-fitview border-border-default text-neutral-grey-50 hover:bg-neutral-grey-5 hover:text-neutral-grey-100 flex h-6 w-6 items-center justify-center border-t bg-white transition-colors", title: "Fit view", "aria-label": "Fit view", onClick: (event) => {
                                        event.stopPropagation();
                                        reactFlowRef.current?.fitView({ padding: 0.2 });
                                    }, children: _jsx(SquareSquare, { className: "shrink-0", strokeWidth: 1, style: {
                                            fill: "none",
                                            width: "14px",
                                            height: "14px",
                                            maxWidth: "none",
                                            maxHeight: "none",
                                        } }) }), onToggleMaximized ? (_jsx("button", { type: "button", className: "react-flow__controls-button border-border-default text-neutral-grey-50 hover:bg-neutral-grey-5 hover:text-neutral-grey-100 flex h-6 w-6 items-center justify-center border-t bg-white transition-colors", "aria-label": isMaximized
                                        ? "Restore task editor size"
                                        : "Maximize task editor", title: isMaximized
                                        ? "Restore task editor size"
                                        : "Maximize task editor", "data-testid": "dependency-graph-toggle-maximize", onClick: (event) => {
                                        event.stopPropagation();
                                        onToggleMaximized();
                                    }, children: isMaximized ? (_jsx(Minimize2, { style: {
                                            fill: "none",
                                            width: "14px",
                                            height: "14px",
                                            maxWidth: "none",
                                            maxHeight: "none",
                                        }, className: "shrink-0", strokeWidth: 1 })) : (_jsx(Maximize2, { style: {
                                            fill: "none",
                                            width: "14px",
                                            height: "14px",
                                            maxWidth: "none",
                                            maxHeight: "none",
                                        }, className: "shrink-0", strokeWidth: 1 })) })) : null] }), effectiveShowMiniMap ? (_jsx(MiniMap, { ...(effectiveMiniMapSize ?? {}), style: effectiveMiniMapSize
                                ? {
                                    width: effectiveMiniMapSize.width,
                                    height: effectiveMiniMapSize.height,
                                }
                                : undefined, pannable: true, zoomable: true, nodeColor: (node) => {
                                const d = node.data;
                                if (d?.isBlocked)
                                    return "var(--color-feedback-red-light)";
                                const status = d?.status;
                                switch (status) {
                                    case "Done":
                                        return "var(--color-feedback-green-light)";
                                    case "InProgress":
                                        return "var(--color-component-default-drop-zone)";
                                    case "Review":
                                        return "var(--color-component-layout-drop-zone)";
                                    case "Waiting":
                                        return "var(--color-feedback-orange-light)";
                                    default:
                                        return "var(--color-neutral-grey-10)";
                                }
                            }, maskColor: "color-mix(in srgb, var(--color-neutral-grey-100) 8%, transparent)", className: "border-border-default! rounded-lg! opacity-60 shadow-md! transition-opacity duration-200 hover:opacity-100" })) : null] })) })] }));
}
//# sourceMappingURL=DependencyGraphView.js.map