import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { ReactFlow, Controls, MiniMap, useNodesState, useEdgesState, 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 { Tooltip, TooltipContent, TooltipTrigger, } from "../../components/ui/tooltip";
import { Circle, PlayCircle, Clock3, Eye, CheckCircle2, ArrowUp, ArrowDown, Minus, Flame, GitBranch, RotateCcw, Loader2, Plus, Trash2, } from "lucide-react";
const NODE_WIDTH = 220;
const NODE_HEIGHT = 80;
const SAVE_DEBOUNCE_MS = 500;
const EDGE_BLOCKED_BY = "#f97316";
const EDGE_CHILD_TASK = "#0d9488";
const EDGE_RELATED = "#94a3b8";
const STATUS_STYLES = {
    Todo: { bg: "bg-slate-100", text: "text-slate-600", icon: Circle },
    InProgress: { bg: "bg-blue-100", text: "text-blue-700", icon: PlayCircle },
    Waiting: { bg: "bg-amber-100", text: "text-amber-700", icon: Clock3 },
    Review: { bg: "bg-purple-100", text: "text-purple-700", icon: Eye },
    Done: { bg: "bg-green-100", text: "text-green-700", icon: CheckCircle2 },
};
const PRIORITY_CONFIG = {
    Critical: { icon: Flame, color: "text-red-500", label: "Critical" },
    High: { icon: ArrowUp, color: "text-orange-500", label: "High" },
    Medium: { icon: Minus, color: "text-yellow-500", label: "Medium" },
    Low: { icon: ArrowDown, color: "text-slate-400", label: "Low" },
};
const HANDLE_CLASS = "bg-slate-300! border-slate-400! 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 priorityCfg = data.priority ? PRIORITY_CONFIG[data.priority] : null;
    const PriorityIcon = priorityCfg?.icon;
    const actionButtonClass = "nodrag nopan absolute z-20 flex h-5 w-5 items-center justify-center rounded-full border border-slate-200 bg-white text-slate-500 shadow-sm transition-colors hover:border-blue-200 hover:bg-blue-50 hover:text-blue-600";
    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 nodeSurfaceClass = data.isSelected
        ? "ring-primary border-primary/50 ring-2 bg-white"
        : isDecisionPoint
            ? "border-violet-200 bg-violet-50/70 hover:border-violet-300 hover:shadow-md"
            : data.isBlocked
                ? "border-red-200 bg-red-50/70"
                : data.status === "Done"
                    ? "border-emerald-200 bg-emerald-50/80"
                    : "border-slate-200 bg-white hover:border-slate-300 hover:shadow-md";
    return (_jsxs("div", { className: cn("relative cursor-pointer rounded-lg border px-3 py-2 shadow-sm transition-all", nodeSurfaceClass), style: { width: NODE_WIDTH }, "data-testid": `dependency-graph-node-${data.taskId}`, children: [_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 items-start justify-between gap-1", children: [_jsxs("div", { className: "flex min-w-0 flex-1 items-center justify-between gap-1", children: [_jsxs("div", { className: "flex min-w-0 items-center gap-1", children: [_jsx("span", { className: "truncate text-[10px] font-medium text-slate-400", children: data.taskKey || data.taskId.slice(0, 8).toUpperCase() }), isDecisionPoint ? (_jsxs("span", { className: "inline-flex items-center gap-0.5 rounded-full bg-violet-100 px-1 py-0.5 text-[9px] font-semibold uppercase tracking-wide text-violet-700", title: "Decision Point", children: [_jsx(GitBranch, { className: "h-2.5 w-2.5" }), "DP"] })) : null] }), PriorityIcon && (_jsx(PriorityIcon, { className: cn("h-3 w-3 shrink-0", priorityCfg.color) }))] }), data.onRemoveTask && (_jsx("button", { type: "button", className: "-mt-px -mr-0.5 shrink-0 rounded p-0.5 text-slate-400 transition-colors hover:bg-red-50 hover:text-red-600", "aria-label": "Remove task", "data-testid": `dependency-graph-remove-task-${data.taskId}`, onClick: (event) => {
                            event.stopPropagation();
                            data.onRemoveTask?.(data.taskId);
                        }, children: _jsx(Trash2, { className: "h-2.5 w-2.5", strokeWidth: 1.5 }) }))] }), _jsx("div", { className: "mt-0.5 truncate text-xs font-medium text-slate-800", children: data.title }), _jsxs("div", { className: "mt-1 flex items-center gap-1.5", children: [executionDisplay ? (_jsxs(Badge, { variant: executionDisplay.variant, className: cn("h-5 max-w-full px-1.5 text-[10px] font-bold tracking-wider uppercase", executionDisplay.className), children: [executionDisplay.showSpinner && (_jsx(Loader2, { className: "mr-1 h-2.5 w-2.5 animate-spin" })), executionDisplay.label] })) : (_jsxs("span", { className: cn("inline-flex items-center gap-0.5 rounded-full px-1.5 py-0 text-[10px] font-medium", statusStyle.bg, statusStyle.text), children: [_jsx(StatusIcon, { className: "h-2.5 w-2.5" }), getTaskStatusLabel(status)] })), data.assigneeDisplayName && (_jsxs("span", { className: "truncate text-[10px] text-slate-400", children: ["@", data.assigneeDisplayName] }))] }), 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.75 }) }) }), _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.75 }) }) }), _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,
};
const HIERARCHY_CHILD_INDENT = 24;
const HIERARCHY_CHILD_GAP = 32;
const HIERARCHY_SUBTREE_GAP = 80;
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: "left" }
        : { source: "right", target: "left" };
}
/**
 * Compound layout for the hierarchy strategy: each parent–children group is
 * laid out vertically (parent on top, children stacked below with a slight
 * indent), and then the resulting subtree "super-nodes" are arranged
 * in the selected orientation via Dagre using the dependency edges between
 * them.
 */
function layoutGraphHierarchy(nodes, edges, orientation) {
    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 roots = nodes.filter((n) => !parentOf.has(n.id));
    function measureSubtree(nodeId, x, y, positions) {
        positions.set(nodeId, { x, y });
        const children = childrenOf.get(nodeId) ?? [];
        if (children.length === 0) {
            return { width: NODE_WIDTH, height: NODE_HEIGHT };
        }
        let childY = y + NODE_HEIGHT + HIERARCHY_CHILD_GAP;
        let maxRight = x + NODE_WIDTH;
        for (const childId of children) {
            const result = measureSubtree(childId, x + HIERARCHY_CHILD_INDENT, childY, positions);
            childY += result.height + HIERARCHY_CHILD_GAP;
            maxRight = Math.max(maxRight, x + HIERARCHY_CHILD_INDENT + result.width);
        }
        return {
            width: maxRight - x,
            height: childY - HIERARCHY_CHILD_GAP - y,
        };
    }
    const subtrees = new Map();
    for (const root of roots) {
        const positions = new Map();
        const dims = measureSubtree(root.id, 0, 0, positions);
        subtrees.set(root.id, { positions, ...dims });
    }
    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) {
        g.setNode(rootId, { width: data.width, height: data.height });
    }
    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 offsetX = superNode.x - data.width / 2;
        const offsetY = superNode.y - data.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) {
    if (autoLayoutStrategy === "hierarchy" &&
        edges.some((e) => e.data?.relationship === "hierarchy")) {
        return layoutGraphHierarchy(nodes, edges, orientation);
    }
    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, { width: NODE_WIDTH, height: NODE_HEIGHT });
    }
    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);
        return {
            ...node,
            position: {
                x: dagNode.x - NODE_WIDTH / 2,
                y: dagNode.y - NODE_HEIGHT / 2,
            },
        };
    });
    return { nodes: layoutedNodes, edges };
}
function buildGraph(tasks, dependencies, selectedTaskId, selectedDependencyId, blockedTaskIds, orientation, showExecutionStateBadges, autoLayoutStrategy, onSelect, onAddDependentTaskFromNode, onAddChildTaskFromNode, onRemoveTask) {
    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,
            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 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: "smoothstep",
            animated: false,
            style: {
                stroke: isSelected ? "#0f172a" : strokeColor,
                strokeWidth: isSelected ? 3 : isBlockedBy ? 2 : 1.5,
                strokeDasharray: isDecisionEdge ? "6 4" : undefined,
            },
            markerEnd: {
                type: MarkerType.ArrowClosed,
                color: isSelected ? "#0f172a" : 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: "smoothstep",
        style: {
            stroke: "#3b82f6",
            strokeWidth: 1.5,
        },
        markerEnd: {
            type: MarkerType.ArrowClosed,
            color: "#3b82f6",
            width: 16,
            height: 16,
        },
        data: {
            relationship: "hierarchy",
        },
    }));
    const edges = [...dependencyEdges, ...hierarchyEdges];
    if (nodes.length === 0)
        return { nodes: [], edges: [] };
    return layoutGraph(nodes, edges, orientation, autoLayoutStrategy);
}
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,
            blocked: node.data.isBlocked,
            taskType: node.data.taskType ?? 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 = true, } = 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 saveTimeoutRef = useRef(null);
    const pendingViewportRef = useRef(null);
    const fitViewOnNextSyncRef = useRef(false);
    const skipNextMoveEndSaveRef = useRef(false);
    const pendingSaveOrientationRef = useRef(null);
    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 autoLayout = buildGraph(tasks, dependencies, selectedTaskId, selectedDependencyId, blockedTaskIds, effectiveOrientation, showExecutionStateBadges, autoLayoutStrategy, onSelect, onAddDependentTaskFromNode, onAddChildTaskFromNode, onRemoveTask);
        setNodes(autoLayout.nodes);
        setEdges(autoLayout.edges);
        nodesRef.current = autoLayout.nodes;
        fitViewOnNextSyncRef.current = true;
        pendingSaveOrientationRef.current = effectiveOrientation;
        restoreViewport();
    }, [
        blockedTaskIds,
        dependencies,
        effectiveOrientation,
        onSelect,
        showExecutionStateBadges,
        autoLayoutStrategy,
        onRemoveTask,
        onAddDependentTaskFromNode,
        onAddChildTaskFromNode,
        restoreViewport,
        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) => {
        if (nextOrientation === effectiveOrientation) {
            return;
        }
        setTransientOrientation(nextOrientation);
        const autoLayout = buildGraph(tasks, dependencies, selectedTaskId, selectedDependencyId, blockedTaskIds, nextOrientation, showExecutionStateBadges, autoLayoutStrategy, onSelect, onAddDependentTaskFromNode, onAddChildTaskFromNode, onRemoveTask);
        setNodes(autoLayout.nodes);
        setEdges(autoLayout.edges);
        nodesRef.current = autoLayout.nodes;
        fitViewOnNextSyncRef.current = true;
        pendingSaveOrientationRef.current = nextOrientation;
        restoreViewport();
    }, [
        autoLayoutStrategy,
        blockedTaskIds,
        dependencies,
        effectiveOrientation,
        onAddChildTaskFromNode,
        onAddDependentTaskFromNode,
        onRemoveTask,
        onSelect,
        restoreViewport,
        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: "flex items-center gap-2 text-[11px] text-slate-400", children: [_jsxs("span", { className: "inline-flex items-center gap-1", children: [_jsx("span", { className: "inline-block h-0.5 w-4 rounded-sm bg-orange-400" }), "Blocked by"] }), _jsxs("span", { className: "inline-flex items-center gap-1", children: [_jsx("span", { className: "inline-block h-0.5 w-4 rounded-sm bg-slate-400" }), "Related"] }), _jsxs("span", { className: "inline-flex items-center gap-1", children: [_jsx("span", { className: "inline-block h-0.5 w-4 rounded-sm bg-blue-500" }), "Child task"] })] })) : (_jsx("div", {})), hasVisibleNodes && (_jsxs("div", { className: "flex items-center gap-2", children: [showOrientationToggle && (_jsxs("div", { className: "inline-flex rounded-md border border-slate-200 bg-white p-0.5", "data-testid": "dependency-graph-orientation-toggle", children: [_jsx("button", { type: "button", className: cn("rounded px-2.5 py-1 text-xs font-medium transition-colors", effectiveOrientation === "horizontal"
                                            ? "bg-slate-900 text-white"
                                            : "text-slate-500 hover:bg-slate-100 hover:text-slate-700"), "data-testid": "dependency-graph-orientation-horizontal", onClick: () => handleOrientationChange("horizontal"), children: "Horizontal" }), _jsx("button", { type: "button", className: cn("rounded px-2.5 py-1 text-xs font-medium transition-colors", effectiveOrientation === "vertical"
                                            ? "bg-slate-900 text-white"
                                            : "text-slate-500 hover:bg-slate-100 hover:text-slate-700"), "data-testid": "dependency-graph-orientation-vertical", onClick: () => handleOrientationChange("vertical"), children: "Vertical" })] })), canPersistLayout && (_jsxs(Button, { type: "button", variant: "outline", size: "sm", className: "h-8 px-3", onClick: handleAutoLayout, children: [_jsx(RotateCcw, { className: "mr-1.5 h-4 w-4" }), "Auto layout"] }))] }))] }), _jsx("div", { className: "min-h-0 flex-1", ref: graphContainerRef, tabIndex: -1, 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: "flex h-12 w-12 items-center justify-center rounded-full bg-slate-100", children: _jsx(GitBranch, { className: "h-6 w-6 text-slate-400" }) }), _jsxs("div", { children: [_jsx("p", { className: "text-sm font-medium text-slate-600", children: emptyStateTitle }), _jsx("p", { className: "mt-1 text-xs text-slate-400", 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, minZoom: 0.2, maxZoom: 2, proOptions: { hideAttribution: true }, nodesDraggable: canPersistLayout, nodesConnectable: allowDependencyConnect, isValidConnection: isValidConnection, elementsSelectable: true, children: [_jsx(Controls, { showInteractive: false, className: "rounded-lg! border-slate-200! shadow-md!" }), showMiniMap ? (_jsx(MiniMap, { ...(miniMapWidth != null && miniMapHeight != null
                                ? { width: miniMapWidth, height: miniMapHeight }
                                : {}), pannable: true, zoomable: true, nodeColor: (node) => {
                                const d = node.data;
                                if (d?.isBlocked)
                                    return "#fecaca";
                                const status = d?.status;
                                switch (status) {
                                    case "Done":
                                        return "#bbf7d0";
                                    case "InProgress":
                                        return "#93c5fd";
                                    case "Review":
                                        return "#c4b5fd";
                                    case "Waiting":
                                        return "#fcd34d";
                                    default:
                                        return "#cbd5e1";
                                }
                            }, maskColor: "rgba(0, 0, 0, 0.08)", className: "rounded-lg! border-slate-200! shadow-md!" })) : null] })) })] }));
}
//# sourceMappingURL=DependencyGraphView.js.map