using System;
using System.Collections.Generic;
using System.Linq;

using UnityEditor;
using UnityEngine;


namespace AIRuntime.Editor
{
    internal sealed class AIRuntimeKanbanGraphNode
    {
        public string Id;

        public string Title;

        public string ParentTaskId;

        public List<string> DependsOnTaskIds =
            new List<string>();

        public List<string> AssociatedTaskIds =
            new List<string>();

        public string BlockedByTaskId;

        public string Status;

        public string Priority;

        public string AssignedAgent;

        public string ReviewStatus;

        public bool IsDecompositionDraft;

        public bool IsRunning;
    }


    internal sealed class AIRuntimeKanbanGraphAssociationEdge
    {
        public string FirstTaskId;

        public string SecondTaskId;
    }


    internal sealed class AIRuntimeKanbanGraphLayoutUpdate
    {
        public Dictionary<string, Vector2> Positions =
            new Dictionary<string, Vector2>(
                StringComparer.Ordinal);

        public bool DidAutoLayout;

        public bool ShouldFit;
    }


    internal static class AIRuntimeKanbanGraphLayout
    {
        internal const float HorizontalSpacing =
            320f;

        internal const float VerticalSpacing =
            156f;


        internal static Dictionary<string, Vector2> CalculatePositions(
            IReadOnlyList<AIRuntimeKanbanGraphNode> nodes)
        {
            return AIRuntimeKanbanForceLayout.Calculate(nodes);
        }


        internal static Dictionary<string, Vector2> ReconcilePositions(
            IReadOnlyList<AIRuntimeKanbanGraphNode> nodes,
            IReadOnlyDictionary<string, Vector2> savedPositions,
            bool forceAutoLayout)
        {
            var ids = nodes.Where(n => n != null && !string.IsNullOrWhiteSpace(n.Id))
                .Select(n => n.Id).Distinct(StringComparer.Ordinal).ToArray();
            if (!forceAutoLayout && savedPositions != null && ids.All(savedPositions.ContainsKey))
                return ids.ToDictionary(id => id, id => savedPositions[id], StringComparer.Ordinal);
            Dictionary<string, Vector2> automatic =
                CalculatePositions(
                    nodes);

            if (forceAutoLayout)
                return automatic;

            return automatic.ToDictionary(
                pair =>
                    pair.Key,
                pair =>
                    savedPositions !=
                        null &&
                    savedPositions.TryGetValue(
                        pair.Key,
                        out Vector2 savedPosition)
                        ? savedPosition
                        : pair.Value,
                StringComparer.Ordinal);
        }


    }


    /// <summary>
    /// Free-canvas network view of the AIRuntime Kanban task graph.
    /// Task data remains owned by the Kanban board; only visual node
    /// positions are stored by this window.
    /// </summary>
    public sealed class AIRuntimeKanbanGraphWindow :
        EditorWindow
    {
        private const int CurrentAutoLayoutPolicyVersion =
            2;

        private const float NodeWidth =
            180f;

        private const float NodeHeight =
            76f;

        private const float MinimumZoom =
            0.32f;

        private const float MaximumZoom =
            1.8f;

        private string _hoveredTaskId;

        [Serializable]
        private sealed class SavedNodePosition
        {
            public string TaskId;

            public Vector2 Position;
        }

        [NonSerialized]
        private AIRuntimeKanbanSession _owner;

        [SerializeField]
        private Vector2 _pan =
            new Vector2(
                100f,
                100f);

        [SerializeField]
        private float _zoom =
            1f;

        [SerializeField]
        private bool _showHierarchy =
            true;

        [SerializeField]
        private bool _showDependencies =
            true;

        [SerializeField]
        private bool _showAssociations =
            true;

        [SerializeField]
        private List<SavedNodePosition> _savedPositions =
            new List<SavedNodePosition>();

        [SerializeField]
        private int _autoLayoutPolicyVersion;

        private IReadOnlyList<AIRuntimeKanbanGraphNode> _nodes =
            Array.Empty<AIRuntimeKanbanGraphNode>();

        [SerializeField]
        private string _topologySignature =
            string.Empty;

        private string _draggingNodeId =
            string.Empty;

        private bool _isPanning;

        private int _canvasControlId;

        private bool _fitOnNextRepaint;

        private Rect _lastCanvasRect;

        private double _nextRefreshAt;

        private double _nextAnimationAt;

        private GUIStyle _nodeTitleStyle;

        private GUIStyle _nodeMetaStyle;

        private GUIStyle _nodeStatusStyle;

        private GUIStyle _nodePriorityStyle;


        [MenuItem("Window/AIRuntime/Kanban Graph")]
        public static void OpenFromMenu()
        {
            AIRuntimeKanbanSession[] boards =
                Resources.FindObjectsOfTypeAll<AIRuntimeKanbanSession>();

            if (boards.Length ==
                0)
            {
                AIRuntimeKanbanSession.Open();

                boards =
                    Resources.FindObjectsOfTypeAll<AIRuntimeKanbanSession>();
            }

            Open(
                boards.Length >
                0
                    ? boards[0]
                    : null);
        }


        internal static void Open(
            AIRuntimeKanbanSession owner)
        {
            AIRuntimeKanbanGraphWindow window =
                GetWindow<AIRuntimeKanbanGraphWindow>();

            window._owner =
                owner;

            window.titleContent =
                new GUIContent(
                    "Kanban Graph");

            window.minSize =
                new Vector2(
                    520f,
                    320f);

            window.Show();
            window.Focus();
            window.RefreshGraph();
        }


        private void OnEnable()
        {
            EditorApplication.update -= AnimateGraph;
            EditorApplication.update += AnimateGraph;
            titleContent =
                new GUIContent(
                    "Kanban Graph");

            minSize =
                new Vector2(
                    520f,
                    320f);

            RecoverOwner();
            RefreshGraph();
        }


        private void OnDisable()
        {
            EditorApplication.update -= AnimateGraph;
        }

        private void OnGUI()
        {
            EnsureStyles();
            DrawToolbar();

            Rect canvasRect =
                GUILayoutUtility.GetRect(
                    1f,
                    100000f,
                    1f,
                    100000f,
                    GUILayout.ExpandWidth(
                        true),
                    GUILayout.ExpandHeight(
                        true));

            _lastCanvasRect =
                canvasRect;

            EditorGUI.DrawRect(
                canvasRect,
                EditorGUIUtility.isProSkin
                    ? new Color(
                        0.105f,
                        0.11f,
                        0.12f)
                    : new Color(
                        0.82f,
                        0.83f,
                        0.85f));

            if (_owner ==
                null)
            {
                DrawMissingBoardMessage(
                    canvasRect);

                return;
            }

            if (_nodes ==
                null)
            {
                RefreshGraph();
            }

            EnsureNodePositions();
            HandleCanvasInput(
                canvasRect);

            _hoveredTaskId = canvasRect.Contains(Event.current.mousePosition)
                ? FindNodeAt(Event.current.mousePosition - canvasRect.position) : null;

            GUI.BeginGroup(
                canvasRect);

            Rect localCanvas =
                new Rect(
                    Vector2.zero,
                    canvasRect.size);

            // A quiet canvas makes network clusters and labels easier to read.
            DrawEdges();
            DrawNodes();
            DrawLegend(
                localCanvas);

            GUI.EndGroup();

            if (_fitOnNextRepaint &&
                Event.current.type ==
                EventType.Repaint)
            {
                _fitOnNextRepaint =
                    false;

                FitToView(
                    canvasRect.size);

                Repaint();
            }
        }


        private void DrawToolbar()
        {
            using (
                new EditorGUILayout.HorizontalScope(
                    EditorStyles.toolbar))
            {
                GUILayout.Label(
                    "Task Graph",
                    EditorStyles.boldLabel);

                if (GUILayout.Button(
                        "Fit",
                        EditorStyles.toolbarButton,
                        GUILayout.Width(
                            38f)))
                {
                    _fitOnNextRepaint =
                        true;
                }

                if (GUILayout.Button(
                        "Auto Layout",
                        EditorStyles.toolbarButton,
                        GUILayout.Width(
                            76f)))
                {
                    ApplyAutoLayout();
                    _fitOnNextRepaint =
                        true;
                }

                GUILayout.Space(
                    8f);

                _showHierarchy =
                    GUILayout.Toggle(
                        _showHierarchy,
                        "Hierarchy",
                        EditorStyles.toolbarButton,
                        GUILayout.Width(
                            70f));

                _showDependencies =
                    GUILayout.Toggle(
                        _showDependencies,
                        "Dependencies",
                        EditorStyles.toolbarButton,
                        GUILayout.Width(
                            88f));

                _showAssociations =
                    GUILayout.Toggle(
                        _showAssociations,
                        "Associations",
                        EditorStyles.toolbarButton,
                        GUILayout.Width(
                            84f));

                GUILayout.FlexibleSpace();

                GUILayout.Label(
                    $"{_nodes?.Count ?? 0} tasks",
                    EditorStyles.miniLabel);

                if (GUILayout.Button(
                        "−",
                        EditorStyles.toolbarButton,
                        GUILayout.Width(
                            22f)))
                {
                    ZoomAround(
                        _zoom /
                        1.2f,
                        GetCanvasCenter());
                }

                if (GUILayout.Button(
                        $"{Mathf.RoundToInt(_zoom * 100f)}%",
                        EditorStyles.toolbarButton,
                        GUILayout.Width(
                            48f)))
                {
                    ZoomAround(
                        1f,
                        GetCanvasCenter());
                }

                if (GUILayout.Button(
                        "+",
                        EditorStyles.toolbarButton,
                        GUILayout.Width(
                            22f)))
                {
                    ZoomAround(
                        _zoom *
                        1.2f,
                        GetCanvasCenter());
                }
            }
        }


        private void DrawMissingBoardMessage(
            Rect canvasRect)
        {
            Rect messageRect =
                new Rect(
                    canvasRect.center.x -
                    180f,
                    canvasRect.center.y -
                    42f,
                    360f,
                    24f);

            GUI.Label(
                messageRect,
                "Open the AIRuntime Kanban board to view its task graph.",
                EditorStyles.centeredGreyMiniLabel);

            Rect buttonRect =
                new Rect(
                    canvasRect.center.x -
                    55f,
                    canvasRect.center.y -
                    10f,
                    110f,
                    24f);

            if (GUI.Button(
                    buttonRect,
                    "Open Kanban"))
            {
                AIRuntimeKanbanSession.Open();
                RecoverOwner();
                RefreshGraph();
            }
        }


        private void DrawGrid(
            Rect canvasRect)
        {
            float gridSize =
                40f *
                _zoom;

            if (gridSize <
                8f)
            {
                return;
            }

            float startX =
                Mathf.Repeat(
                    _pan.x,
                    gridSize);

            float startY =
                Mathf.Repeat(
                    _pan.y,
                    gridSize);

            Color minorColor =
                EditorGUIUtility.isProSkin
                    ? new Color(
                        1f,
                        1f,
                        1f,
                        0.035f)
                    : new Color(
                        0f,
                        0f,
                        0f,
                        0.055f);

            for (float x = startX; x < canvasRect.width; x += gridSize)
            {
                EditorGUI.DrawRect(
                    new Rect(
                        x,
                        0f,
                        1f,
                        canvasRect.height),
                    minorColor);
            }

            for (float y = startY; y < canvasRect.height; y += gridSize)
            {
                EditorGUI.DrawRect(
                    new Rect(
                        0f,
                        y,
                        canvasRect.width,
                        1f),
                    minorColor);
            }
        }


        private void DrawEdges()
        {
            Dictionary<string, AIRuntimeKanbanGraphNode> byId =
                _nodes
                    .Where(
                        node =>
                            node !=
                                null)
                    .ToDictionary(
                        node =>
                            node.Id,
                        node =>
                            node,
                        StringComparer.Ordinal);

            string selectedTaskId =
                _hoveredTaskId ?? _owner.GetSelectedKanbanTaskId();

            Handles.BeginGUI();

            if (_showAssociations)
            {
                foreach (
                    AIRuntimeKanbanGraphAssociationEdge edge
                    in GetVisibleAssociationEdges(
                        _nodes))
                {
                    DrawEdge(
                        edge.FirstTaskId,
                        edge.SecondTaskId,
                        new Color(
                            0.68f,
                            0.48f,
                            0.86f,
                            0.52f),
                        string.Equals(
                            selectedTaskId,
                            edge.FirstTaskId,
                            StringComparison.Ordinal) ||
                        string.Equals(
                            selectedTaskId,
                            edge.SecondTaskId,
                            StringComparison.Ordinal),
                        drawArrow: false);
                }
            }

            foreach (AIRuntimeKanbanGraphNode node in _nodes)
            {
                if (_showHierarchy &&
                    !string.IsNullOrWhiteSpace(
                        node.ParentTaskId) &&
                    byId.ContainsKey(
                        node.ParentTaskId))
                {
                    DrawEdge(
                        node.ParentTaskId,
                        node.Id,
                        new Color(
                            0.24f,
                            0.72f,
                            0.82f,
                            0.9f),
                        string.Equals(
                            selectedTaskId,
                            node.ParentTaskId,
                            StringComparison.Ordinal) ||
                        string.Equals(
                            selectedTaskId,
                            node.Id,
                            StringComparison.Ordinal));
                }

                if (!_showDependencies)
                {
                    continue;
                }

                HashSet<string> dependencies =
                    new HashSet<string>(
                        node.DependsOnTaskIds ??
                        new List<string>(),
                        StringComparer.Ordinal);

                foreach (string dependencyId in dependencies)
                {
                    if (string.IsNullOrWhiteSpace(
                            dependencyId) ||
                        !byId.ContainsKey(
                            dependencyId))
                    {
                        continue;
                    }

                    DrawEdge(
                        dependencyId,
                        node.Id,
                        new Color(
                            0.96f,
                            0.59f,
                            0.18f,
                            0.92f),
                        string.Equals(
                            selectedTaskId,
                            dependencyId,
                            StringComparison.Ordinal) ||
                        string.Equals(
                            selectedTaskId,
                            node.Id,
                            StringComparison.Ordinal));
                }

                if (!string.IsNullOrWhiteSpace(
                        node.BlockedByTaskId) &&
                    !dependencies.Contains(
                        node.BlockedByTaskId) &&
                    byId.ContainsKey(
                        node.BlockedByTaskId))
                {
                    DrawEdge(
                        node.BlockedByTaskId,
                        node.Id,
                        new Color(
                            0.93f,
                            0.25f,
                            0.24f,
                            0.95f),
                        string.Equals(
                            selectedTaskId,
                            node.BlockedByTaskId,
                            StringComparison.Ordinal) ||
                        string.Equals(
                            selectedTaskId,
                            node.Id,
                            StringComparison.Ordinal));
                }
            }

            Handles.EndGUI();
        }


        private void DrawEdge(
            string fromTaskId,
            string toTaskId,
            Color color,
            bool highlighted,
            bool drawArrow = true)
        {
            if (string.Equals(
                    fromTaskId,
                    toTaskId,
                    StringComparison.Ordinal))
            {
                return;
            }

            Rect from =
                GetNodeRect(
                    fromTaskId);

            Rect to =
                GetNodeRect(
                    toTaskId);

            Vector2 fromCenter = new Vector2(from.center.x, from.y + 18f * _zoom);
            Vector2 toCenter = new Vector2(to.center.x, to.y + 18f * _zoom);
            Vector2 direction = (toCenter - fromCenter).normalized;
            Vector2 start = fromCenter + direction * Mathf.Max(7f, 12f * _zoom);
            Vector2 end = toCenter - direction * Mathf.Max(7f, 12f * _zoom);
            Vector2 startTangent = Vector2.Lerp(start, end, .33f);
            Vector2 endTangent = Vector2.Lerp(start, end, .67f);
            color.a *= highlighted ? 1f : .5f;
            Handles.DrawBezier(
                start,
                end,
                startTangent,
                endTangent,
                highlighted
                    ? Color.Lerp(
                        color,
                        Color.white,
                        0.35f)
                    : color,
                null,
                highlighted
                    ? 3.5f
                    : 2f);

            if (!drawArrow)
            {
                return;
            }

            Vector2 arrowDirection =
                (end -
                 endTangent).normalized;

            Vector2 perpendicular =
                new Vector2(
                    -arrowDirection.y,
                    arrowDirection.x);

            float arrowLength =
                highlighted
                    ? 11f
                    : 9f;

            Handles.color =
                color;

            Handles.DrawAAConvexPolygon(
                end,
                end -
                arrowDirection *
                arrowLength +
                perpendicular *
                arrowLength *
                0.48f,
                end -
                arrowDirection *
                arrowLength -
                perpendicular *
                arrowLength *
                0.48f);
        }


        private void DrawNodes()
        {
            string selectedTaskId =
                _owner.GetSelectedKanbanTaskId();

            Dictionary<string, int> childCounts =
                _nodes
                    .Where(
                        node =>
                            !string.IsNullOrWhiteSpace(
                                node.ParentTaskId))
                    .GroupBy(
                        node =>
                            node.ParentTaskId,
                        StringComparer.Ordinal)
                    .ToDictionary(
                        group =>
                            group.Key,
                        group =>
                            group.Count(),
                        StringComparer.Ordinal);

            IEnumerable<AIRuntimeKanbanGraphNode> drawOrder =
                _nodes.OrderBy(
                    node =>
                        string.Equals(
                            node.Id,
                            selectedTaskId,
                            StringComparison.Ordinal)
                            ? 1
                            : 0);

            foreach (AIRuntimeKanbanGraphNode node in drawOrder)
            {
                DrawNode(
                    node,
                    childCounts.TryGetValue(
                        node.Id,
                        out int childCount)
                        ? childCount
                        : 0,
                    string.Equals(
                        node.Id,
                        selectedTaskId,
                        StringComparison.Ordinal));
            }
        }


        private void DrawNode(AIRuntimeKanbanGraphNode node, int childCount, bool selected)
        {
            selected = selected || node.Id == _hoveredTaskId;
            EnsureStyles();
            Rect rect = GetNodeRect(node.Id);
            Vector2 center = new Vector2(rect.center.x, rect.y + 18f * _zoom);
            float radius = Mathf.Clamp((8f + Mathf.Sqrt(childCount) * 2f) * _zoom, 5f, 19f);
            float labelRadius = radius;
            var color = GetStatusColor(node.Status);
            float pulse = .5f + .5f * Mathf.Sin((float)(EditorApplication.timeSinceStartup % 1.5d) * Mathf.PI * 2f / 1.5f);
            Handles.BeginGUI();
            bool crashed = string.Equals(node.Status, "Crashed", StringComparison.Ordinal);
            if (node.IsRunning)
            {
                // Keep activity legible without overpowering neighbouring nodes.
                Handles.color = new Color(1f, .72f, .18f, .45f + .4f * pulse);
                Handles.DrawWireDisc(center, Vector3.forward, radius + 3f + 2f * pulse);
                color = Color.Lerp(color, new Color(1f, .78f, .3f), .15f + .2f * pulse);
            }
            else if (crashed)
            {
                Handles.color = new Color(1f, 0f, 0f, .6f);
                Handles.DrawSolidDisc(center, Vector3.forward, radius + 6f);
                Handles.color = new Color(1f, 0f, 0f, 1f);
                Handles.DrawWireDisc(center, Vector3.forward, radius + 3f);
            }
            if (selected)
            {
                Handles.color = new Color(.45f, .8f, 1f, .25f);
                Handles.DrawSolidDisc(center, Vector3.forward, radius + 6);
            }
            Handles.color = color;
            Handles.DrawSolidDisc(center, Vector3.forward, radius);
            if (node.IsDecompositionDraft || selected)
            {
                Handles.color = node.IsDecompositionDraft ? new Color(1f, .75f, .3f) : Color.white;
                Handles.DrawWireDisc(center, Vector3.forward, radius + 2);
            }
            Handles.EndGUI();
            _nodeTitleStyle.alignment = TextAnchor.UpperCenter;
            _nodeTitleStyle.fontSize = Mathf.Clamp(Mathf.RoundToInt(12 * _zoom), 10, 18);
            float labelWidth = Mathf.Max(130, rect.width);
            string title = string.IsNullOrWhiteSpace(node.Title) ? "Untitled task" : node.Title;
            float labelHeight = _nodeTitleStyle.CalcHeight(new GUIContent(title), labelWidth);
            var label = new Rect(center.x - labelWidth / 2, center.y + labelRadius + 5, labelWidth, labelHeight);
            Color previousTitleColor = _nodeTitleStyle.normal.textColor;
            if (crashed || node.IsRunning)
            {
                if (crashed)
                {
                    Rect panel = new Rect(label.x - 5f, label.y - 3f, label.width + 10f, label.height + 21f);
                    EditorGUI.DrawRect(panel, new Color(.9f, 0f, 0f, 1f));
                    _nodeTitleStyle.normal.textColor = Color.white;
                }
                var badgeStyle = new GUIStyle(EditorStyles.boldLabel)
                {
                    alignment = TextAnchor.MiddleCenter,
                    fontSize = 11
                };
                badgeStyle.normal.textColor = crashed ? Color.white : new Color(.85f, .67f, .32f);
                GUI.Label(new Rect(label.x, label.yMax, label.width, 18f),
                    crashed ? "CRASHED" : "Running", badgeStyle);
            }
            GUI.Label(label, title, _nodeTitleStyle);
            _nodeTitleStyle.normal.textColor = previousTitleColor;
            if (selected && _zoom >= .8f)
            {
                _nodeMetaStyle.alignment = TextAnchor.UpperCenter;
                GUI.Label(new Rect(label.x, label.yMax + (crashed || node.IsRunning ? 20f : 0f), label.width, 20), node.Status + " · " + node.AssignedAgent, _nodeMetaStyle);
            }
            GUI.Label(new Rect(label.x, rect.y, label.width, label.yMax - rect.y),
                new GUIContent(string.Empty, BuildNodeTooltip(node)), GUIStyle.none);
        }

        private void DrawLegend(
            Rect canvasRect)
        {
            Rect legend =
                new Rect(
                    12f,
                    canvasRect.height -
                    28f,
                    Mathf.Min(
                        650f,
                        canvasRect.width -
                        24f),
                    20f);

            EditorGUI.DrawRect(
                legend,
                EditorGUIUtility.isProSkin
                    ? new Color(
                        0f,
                        0f,
                        0f,
                        0.42f)
                    : new Color(
                        1f,
                        1f,
                        1f,
                        0.72f));

            GUI.Label(
                new Rect(
                    legend.x +
                    7f,
                    legend.y,
                    legend.width -
                    14f,
                    legend.height),
                "Drag nodes • drag empty canvas to pan • scroll to zoom • double-click a task for details   " +
                "Cyan: hierarchy   Orange: dependency   Red: blocked   Purple: association",
                EditorStyles.miniLabel);
        }


        private void HandleCanvasInput(
            Rect canvasRect)
        {
            Event current =
                Event.current;

            _canvasControlId =
                GUIUtility.GetControlID(
                    "AIRuntimeKanbanGraphCanvas".GetHashCode(),
                    FocusType.Passive,
                    canvasRect);

            bool ownsMouse =
                GUIUtility.hotControl ==
                _canvasControlId;

            if (!canvasRect.Contains(
                    current.mousePosition) &&
                !ownsMouse)
            {
                return;
            }

            Vector2 localMouse =
                current.mousePosition -
                canvasRect.position;

            if (current.type ==
                    EventType.ScrollWheel)
            {
                ZoomAround(
                    _zoom *
                    Mathf.Pow(
                        1.1f,
                        -current.delta.y),
                    localMouse);

                current.Use();
                return;
            }

            if (current.type ==
                    EventType.ContextClick)
            {
                ShowCanvasMenu();
                current.Use();
                return;
            }

            if (current.type ==
                    EventType.MouseDown &&
                (current.button ==
                     0 ||
                 current.button ==
                     2))
            {
                string hitTaskId =
                    current.button ==
                    0 &&
                    !current.alt
                        ? FindNodeAt(
                            localMouse)
                        : string.Empty;

                if (!string.IsNullOrWhiteSpace(
                        hitTaskId))
                {
                    _draggingNodeId =
                        hitTaskId;

                    _owner.SelectKanbanTaskFromGraph(
                        hitTaskId,
                        current.clickCount >=
                        2);
                }
                else
                {
                    _isPanning =
                        true;
                }

                GUIUtility.hotControl =
                    _canvasControlId;

                current.Use();
                Repaint();
                return;
            }

            if (current.type ==
                    EventType.MouseDrag &&
                ownsMouse)
            {
                if (!string.IsNullOrWhiteSpace(
                        _draggingNodeId))
                {
                    SetNodePosition(
                        _draggingNodeId,
                        GetNodePosition(
                            _draggingNodeId) +
                        current.delta /
                        _zoom);
                }
                else if (_isPanning)
                {
                    _pan +=
                        current.delta;
                }

                current.Use();
                Repaint();
                return;
            }

            if ((current.type ==
                     EventType.MouseUp ||
                 current.rawType ==
                     EventType.MouseUp) &&
                ownsMouse)
            {
                _draggingNodeId =
                    string.Empty;

                _isPanning =
                    false;

                GUIUtility.hotControl =
                    0;

                current.Use();
                Repaint();
            }
        }


        private void ShowCanvasMenu()
        {
            GenericMenu menu =
                new GenericMenu();

            menu.AddItem(
                new GUIContent(
                    "Fit to View"),
                false,
                () =>
                    _fitOnNextRepaint =
                        true);

            menu.AddItem(
                new GUIContent(
                    "Auto Layout"),
                false,
                () =>
                {
                    ApplyAutoLayout();
                    _fitOnNextRepaint =
                        true;
                });

            menu.AddSeparator(
                string.Empty);

            menu.AddItem(
                new GUIContent(
                    "Show Hierarchy"),
                _showHierarchy,
                () =>
                    _showHierarchy =
                        !_showHierarchy);

            menu.AddItem(
                new GUIContent(
                    "Show Dependencies"),
                _showDependencies,
                () =>
                    _showDependencies =
                        !_showDependencies);

            menu.AddItem(
                new GUIContent(
                    "Show Associations"),
                _showAssociations,
                () =>
                    _showAssociations =
                        !_showAssociations);

            menu.ShowAsContext();
        }


        private string FindNodeAt(
            Vector2 localMouse)
        {
            string selectedTaskId =
                _owner.GetSelectedKanbanTaskId();

            foreach (
                AIRuntimeKanbanGraphNode node
                in _nodes
                    .OrderByDescending(
                        candidate =>
                            string.Equals(
                                candidate.Id,
                                selectedTaskId,
                                StringComparison.Ordinal)))
            {
                if (GetNodeHitRect(
                        node.Id)
                    .Contains(
                        localMouse))
                {
                    return node.Id;
                }
            }

            return string.Empty;
        }


        private void RefreshGraph()
        {
            if (_owner ==
                null)
            {
                RecoverOwner();
            }

            if (_owner ==
                null)
            {
                _nodes =
                    Array.Empty<AIRuntimeKanbanGraphNode>();

                return;
            }

            IReadOnlyList<AIRuntimeKanbanGraphNode> latest =
                _owner.GetKanbanGraphNodes();

            string signature =
                BuildTopologySignature(
                    latest);

            // Refresh live status without rebuilding an unchanged layout on every clock tick.
            if (string.Equals(_topologySignature, signature, StringComparison.Ordinal) &&
                _autoLayoutPolicyVersion >= CurrentAutoLayoutPolicyVersion)
            {
                _nodes = latest;
                return;
            }
            Dictionary<string, Vector2> savedPositions =
                GetSavedPositionMap();

            AIRuntimeKanbanGraphLayoutUpdate layoutUpdate =
                ReconcileTopologyLayout(
                    _topologySignature,
                    signature,
                    savedPositions,
                    latest,
                    _autoLayoutPolicyVersion <
                        CurrentAutoLayoutPolicyVersion);

            _nodes =
                latest;

            _topologySignature =
                signature;

            _autoLayoutPolicyVersion =
                CurrentAutoLayoutPolicyVersion;

            ReplaceSavedPositions(
                layoutUpdate.Positions);

            if (layoutUpdate.ShouldFit)
            {
                _fitOnNextRepaint =
                    true;
            }

            Repaint();
        }


        internal static AIRuntimeKanbanGraphLayoutUpdate ReconcileTopologyLayout(
            string previousSignature,
            string currentSignature,
            IReadOnlyDictionary<string, Vector2> savedPositions,
            IReadOnlyList<AIRuntimeKanbanGraphNode> nodes,
            bool requiresPolicyUpgrade)
        {
            IReadOnlyCollection<string> savedTaskIds =
                savedPositions?.Keys.ToList() ??
                new List<string>();

            bool shouldAutoLayout =
                requiresPolicyUpgrade ||
                ShouldAutoLayoutTopologyChange(
                    previousSignature,
                    currentSignature,
                    savedTaskIds,
                    nodes);

            // A full reflow is required when decomposition grows the graph.
            // Keeping old coordinates while assigning each new node from a
            // newly centered layout compresses siblings until their cards
            // overlap and cover the dependency edges.
            return new AIRuntimeKanbanGraphLayoutUpdate
            {
                Positions =
                    AIRuntimeKanbanGraphLayout.ReconcilePositions(
                        nodes,
                        savedPositions,
                        shouldAutoLayout),

                DidAutoLayout =
                    shouldAutoLayout,

                ShouldFit =
                    shouldAutoLayout
            };
        }


        internal static string BuildTopologySignature(
            IReadOnlyList<AIRuntimeKanbanGraphNode> nodes)
        {
            return string.Join(
                "|",
                (nodes ??
                 Array.Empty<AIRuntimeKanbanGraphNode>())
                .Where(
                    node =>
                        node !=
                            null &&
                        !string.IsNullOrWhiteSpace(
                            node.Id))
                .OrderBy(
                    node =>
                        node.Id,
                    StringComparer.Ordinal)
                .Select(
                    BuildNodeTopologySignature));
        }


        internal static IReadOnlyList<AIRuntimeKanbanGraphAssociationEdge>
            GetVisibleAssociationEdges(
                IReadOnlyList<AIRuntimeKanbanGraphNode> nodes)
        {
            Dictionary<string, AIRuntimeKanbanGraphNode> byId =
                (nodes ??
                 Array.Empty<AIRuntimeKanbanGraphNode>())
                .Where(
                    node =>
                        node !=
                            null &&
                        !string.IsNullOrWhiteSpace(
                            node.Id))
                .GroupBy(
                    node =>
                        node.Id,
                    StringComparer.Ordinal)
                .ToDictionary(
                    group =>
                        group.Key,
                    group =>
                        group.First(),
                    StringComparer.Ordinal);

            HashSet<string> representedPairs =
                new HashSet<string>(
                    StringComparer.Ordinal);

            foreach (AIRuntimeKanbanGraphNode node in byId.Values)
            {
                AddGraphPair(
                    representedPairs,
                    node.Id,
                    node.ParentTaskId,
                    byId);

                foreach (
                    string dependencyId
                    in node.DependsOnTaskIds ??
                       new List<string>())
                {
                    AddGraphPair(
                        representedPairs,
                        node.Id,
                        dependencyId,
                        byId);
                }

                AddGraphPair(
                    representedPairs,
                    node.Id,
                    node.BlockedByTaskId,
                    byId);
            }

            Dictionary<string, AIRuntimeKanbanGraphAssociationEdge> associations =
                new Dictionary<string, AIRuntimeKanbanGraphAssociationEdge>(
                    StringComparer.Ordinal);

            foreach (AIRuntimeKanbanGraphNode node in byId.Values)
            {
                foreach (
                    string associatedTaskId
                    in node.AssociatedTaskIds ??
                       new List<string>())
                {
                    string key =
                        BuildGraphPairKey(
                            node.Id,
                            associatedTaskId,
                            byId);

                    if (string.IsNullOrEmpty(
                            key) ||
                        representedPairs.Contains(
                            key) ||
                        associations.ContainsKey(
                            key))
                    {
                        continue;
                    }

                    string[] pair =
                        key.Split(
                            new[]
                            {
                                '\u001f'
                            },
                            StringSplitOptions.None);

                    associations[key] =
                        new AIRuntimeKanbanGraphAssociationEdge
                        {
                            FirstTaskId =
                                pair[0],

                            SecondTaskId =
                                pair[1]
                        };
                }
            }

            return associations
                .OrderBy(
                    pair =>
                        pair.Key,
                    StringComparer.Ordinal)
                .Select(
                    pair =>
                        pair.Value)
                .ToList();
        }


        private static void AddGraphPair(
            ISet<string> pairs,
            string firstTaskId,
            string secondTaskId,
            IReadOnlyDictionary<string, AIRuntimeKanbanGraphNode> byId)
        {
            string key =
                BuildGraphPairKey(
                    firstTaskId,
                    secondTaskId,
                    byId);

            if (!string.IsNullOrEmpty(
                    key))
            {
                pairs.Add(
                    key);
            }
        }


        private static string BuildGraphPairKey(
            string firstTaskId,
            string secondTaskId,
            IReadOnlyDictionary<string, AIRuntimeKanbanGraphNode> byId)
        {
            if (string.IsNullOrWhiteSpace(
                    firstTaskId) ||
                string.IsNullOrWhiteSpace(
                    secondTaskId) ||
                string.Equals(
                    firstTaskId,
                    secondTaskId,
                    StringComparison.Ordinal) ||
                !byId.ContainsKey(
                    firstTaskId) ||
                !byId.ContainsKey(
                    secondTaskId))
            {
                return string.Empty;
            }

            return string.CompareOrdinal(
                       firstTaskId,
                       secondTaskId) <
                   0
                ? firstTaskId +
                  "\u001f" +
                  secondTaskId
                : secondTaskId +
                  "\u001f" +
                  firstTaskId;
        }


        private static string BuildNodeTopologySignature(
            AIRuntimeKanbanGraphNode node)
        {
            string associations = string.Join(",", (node.AssociatedTaskIds ?? new List<string>())
                .Where(id => !string.IsNullOrWhiteSpace(id) && id != node.Id)
                .Distinct(StringComparer.Ordinal).OrderBy(id => id, StringComparer.Ordinal));
            HashSet<string> dependencyIds =
                new HashSet<string>(
                    (node.DependsOnTaskIds ??
                     new List<string>())
                    .Where(
                        id =>
                            !string.IsNullOrWhiteSpace(
                                id)),
                    StringComparer.Ordinal);

            string dependencies =
                string.Join(
                    ",",
                    dependencyIds
                    .OrderBy(
                        id =>
                            id,
                        StringComparer.Ordinal));

            string effectiveBlockerId =
                dependencyIds.Contains(
                    node.BlockedByTaskId ??
                    string.Empty)
                    ? string.Empty
                    : node.BlockedByTaskId ??
                      string.Empty;

            return
                $"{node.Id}>{node.ParentTaskId}:{dependencies}:{effectiveBlockerId}:{associations}";
        }


        internal static bool ShouldAutoLayoutTopologyChange(
            string previousSignature,
            string currentSignature,
            IReadOnlyCollection<string> savedTaskIds,
            IReadOnlyList<AIRuntimeKanbanGraphNode> nodes)
        {
            HashSet<string> currentTaskIds =
                new HashSet<string>(
                    (nodes ??
                     Array.Empty<AIRuntimeKanbanGraphNode>())
                    .Where(
                        node =>
                            node !=
                                null &&
                            !string.IsNullOrWhiteSpace(
                                node.Id))
                    .Select(
                        node =>
                            node.Id),
                    StringComparer.Ordinal);

            HashSet<string> positionedTaskIds =
                new HashSet<string>(
                    (savedTaskIds ??
                     Array.Empty<string>())
                    .Where(
                        taskId =>
                            !string.IsNullOrWhiteSpace(
                                taskId)),
                    StringComparer.Ordinal);

            bool nodeSetChanged =
                !positionedTaskIds.SetEquals(
                    currentTaskIds);

            bool knownTopologyChanged =
                !string.IsNullOrWhiteSpace(
                    previousSignature) &&
                !string.Equals(
                    previousSignature,
                    currentSignature,
                    StringComparison.Ordinal);

            // An empty previous signature is the first refresh after a
            // domain/window reload. If every task already has a saved
            // coordinate, preserve the user's manual canvas arrangement.
            return nodeSetChanged ||
                   knownTopologyChanged;
        }


        private void EnsureNodePositions()
        {
            ReplaceSavedPositions(
                AIRuntimeKanbanGraphLayout.ReconcilePositions(
                    _nodes,
                    GetSavedPositionMap(),
                    forceAutoLayout: false));
        }


        private void ApplyAutoLayout()
        {
            ReplaceSavedPositions(
                AIRuntimeKanbanGraphLayout.CalculatePositions(
                    _nodes));

            Repaint();
        }


        private Dictionary<string, Vector2> GetSavedPositionMap()
        {
            return (_savedPositions ??
                    new List<SavedNodePosition>())
                .Where(
                    saved =>
                        saved !=
                            null &&
                        !string.IsNullOrWhiteSpace(
                            saved.TaskId))
                .GroupBy(
                    saved =>
                        saved.TaskId,
                    StringComparer.Ordinal)
                .ToDictionary(
                    group =>
                        group.Key,
                    group =>
                        group.Last().Position,
                    StringComparer.Ordinal);
        }


        private void ReplaceSavedPositions(
            IReadOnlyDictionary<string, Vector2> positions)
        {
            _savedPositions =
                (positions ??
                 new Dictionary<string, Vector2>(
                     StringComparer.Ordinal))
                    .Select(
                        pair =>
                            new SavedNodePosition
                            {
                                TaskId =
                                    pair.Key,

                                Position =
                                    pair.Value
                            })
                    .ToList();
        }


        private void FitToView(
            Vector2 canvasSize)
        {
            if (_savedPositions ==
                    null ||
                _savedPositions.Count ==
                    0 ||
                canvasSize.x <=
                    80f ||
                canvasSize.y <=
                    80f)
            {
                return;
            }

            float minX =
                _savedPositions.Min(
                    saved =>
                        saved.Position.x);

            float minY =
                _savedPositions.Min(
                    saved =>
                        saved.Position.y);

            float maxX =
                _savedPositions.Max(
                    saved =>
                        saved.Position.x) +
                NodeWidth;

            float maxY =
                _savedPositions.Max(
                    saved =>
                        saved.Position.y) +
                NodeHeight;

            Rect bounds =
                Rect.MinMaxRect(
                    minX,
                    minY,
                    maxX,
                    maxY);

            float fittedZoom =
                Mathf.Min(
                    (canvasSize.x -
                     80f) /
                    Mathf.Max(
                        1f,
                        bounds.width),
                    (canvasSize.y -
                     80f) /
                    Mathf.Max(
                        1f,
                        bounds.height));

            _zoom =
                Mathf.Clamp(
                    fittedZoom,
                    MinimumZoom,
                    Mathf.Min(
                        MaximumZoom,
                        1.25f));

            _pan =
                canvasSize *
                0.5f -
                bounds.center *
                _zoom;
        }


        private void ZoomAround(
            float requestedZoom,
            Vector2 canvasPoint)
        {
            float nextZoom =
                Mathf.Clamp(
                    requestedZoom,
                    MinimumZoom,
                    MaximumZoom);

            if (Mathf.Approximately(
                    nextZoom,
                    _zoom))
            {
                return;
            }

            Vector2 worldPoint =
                (canvasPoint -
                 _pan) /
                _zoom;

            _zoom =
                nextZoom;

            _pan =
                canvasPoint -
                worldPoint *
                _zoom;

            Repaint();
        }


        private Vector2 GetCanvasCenter()
        {
            return _lastCanvasRect.size *
                   0.5f;
        }


        private Rect GetNodeRect(
            string taskId)
        {
            Vector2 screenPosition =
                _pan +
                GetNodePosition(
                    taskId) *
                _zoom;

            return new Rect(
                screenPosition,
                new Vector2(
                    NodeWidth,
                    NodeHeight) *
                _zoom);
        }


        private Rect GetNodeHitRect(string id)
        {
            var rect = GetNodeRect(id);
            float width = Mathf.Max(130, rect.width);
            return new Rect(rect.center.x - width / 2, rect.y, width, Mathf.Max(rect.height, 70));
        }
        private Vector2 GetNodePosition(
            string taskId)
        {
            SavedNodePosition saved =
                _savedPositions?.FirstOrDefault(
                    candidate =>
                        string.Equals(
                            candidate.TaskId,
                            taskId,
                            StringComparison.Ordinal));

            return saved?.Position ??
                   Vector2.zero;
        }


        private void SetNodePosition(
            string taskId,
            Vector2 position)
        {
            SavedNodePosition saved =
                _savedPositions.FirstOrDefault(
                    candidate =>
                        string.Equals(
                            candidate.TaskId,
                            taskId,
                            StringComparison.Ordinal));

            if (saved ==
                null)
            {
                saved =
                    new SavedNodePosition
                    {
                        TaskId =
                            taskId
                    };

                _savedPositions.Add(
                    saved);
            }

            saved.Position =
                position;
        }


        private void AnimateGraph()
        {
            if (EditorApplication.timeSinceStartup < _nextAnimationAt ||
                _nodes == null || !_nodes.Any(node => node.IsRunning)) return;
            _nextAnimationAt = EditorApplication.timeSinceStartup + 1d / 30d;
            Repaint();
        }

        private void OnInspectorUpdate()
        {
            if (EditorApplication.timeSinceStartup >=
                _nextRefreshAt)
            {
                _nextRefreshAt =
                    EditorApplication.timeSinceStartup +
                    0.5d;

                RefreshGraph();
            }

            Repaint();
        }


        private void RecoverOwner()
        {
            AIRuntimeKanbanSession[] boards =
                Resources.FindObjectsOfTypeAll<AIRuntimeKanbanSession>();

            _owner =
                boards.Length >
                0
                    ? boards[0]
                    : null;
        }


        private void EnsureStyles()
        {
            if (_nodeTitleStyle ==
                null)
            {
                _nodeTitleStyle =
                    new GUIStyle()
                    {
                        wordWrap =
                            true,

                        clipping =
                            TextClipping.Clip,

                        alignment =
                            TextAnchor.UpperLeft
                    };

            }

            if (_nodeMetaStyle ==
                null)
            {
                _nodeMetaStyle =
                    new GUIStyle()
                    {
                        clipping =
                            TextClipping.Clip,

                        alignment =
                            TextAnchor.MiddleLeft
                    };

            }

            if (_nodeStatusStyle ==
                null)
            {
                _nodeStatusStyle =
                    new GUIStyle()
                    {
                        alignment =
                            TextAnchor.MiddleLeft,

                        clipping =
                            TextClipping.Clip
                    };

            }

            if (_nodePriorityStyle ==
                null)
            {
                _nodePriorityStyle =
                    new GUIStyle(
                        _nodeStatusStyle)
                    {
                        alignment =
                            TextAnchor.MiddleRight
                    };
            }

            _nodeTitleStyle.fontSize =
                Mathf.Clamp(
                    Mathf.RoundToInt(
                        12f *
                        _zoom),
                    8,
                    20);

            _nodeMetaStyle.fontSize =
                Mathf.Clamp(
                    Mathf.RoundToInt(
                        9f *
                        _zoom),
                    7,
                    14);

            int headerFontSize =
                Mathf.Clamp(
                    Mathf.RoundToInt(
                        10f *
                        _zoom),
                    7,
                    15);

            _nodeStatusStyle.fontSize =
                headerFontSize;

            _nodePriorityStyle.fontSize =
                headerFontSize;

            Color titleColor =
                EditorGUIUtility.isProSkin
                    ? new Color(
                        0.92f,
                        0.94f,
                        0.97f,
                        1f)
                    : new Color(
                        0.10f,
                        0.12f,
                        0.15f,
                        1f);

            Color metaColor =
                EditorGUIUtility.isProSkin
                    ? new Color(
                        0.70f,
                        0.74f,
                        0.80f,
                        1f)
                    : new Color(
                        0.28f,
                        0.31f,
                        0.36f,
                        1f);

            SetTextColor(
                _nodeTitleStyle,
                titleColor);

            SetTextColor(
                _nodeMetaStyle,
                metaColor);

            SetTextColor(
                _nodeStatusStyle,
                Color.white);

            SetTextColor(
                _nodePriorityStyle,
                Color.white);
        }


        private static void SetTextColor(
            GUIStyle style,
            Color color)
        {
            style.normal.textColor =
                color;

            style.hover.textColor =
                color;

            style.active.textColor =
                color;

            style.focused.textColor =
                color;

            style.onNormal.textColor =
                color;

            style.onHover.textColor =
                color;

            style.onActive.textColor =
                color;

            style.onFocused.textColor =
                color;
        }


        private static void DrawBorder(
            Rect rect,
            Color color,
            float thickness)
        {
            EditorGUI.DrawRect(
                new Rect(
                    rect.x,
                    rect.y,
                    rect.width,
                    thickness),
                color);

            EditorGUI.DrawRect(
                new Rect(
                    rect.x,
                    rect.yMax -
                    thickness,
                    rect.width,
                    thickness),
                color);

            EditorGUI.DrawRect(
                new Rect(
                    rect.x,
                    rect.y,
                    thickness,
                    rect.height),
                color);

            EditorGUI.DrawRect(
                new Rect(
                    rect.xMax -
                    thickness,
                    rect.y,
                    thickness,
                    rect.height),
                color);
        }


        private static string BuildNodeTooltip(
            AIRuntimeKanbanGraphNode node)
        {
            List<string> lines =
                new List<string>
                {
                    node.Title ??
                    "Untitled task",

                    $"Status: {node.Status}",

                    $"Priority: {node.Priority}",

                    $"Agent: {(string.IsNullOrWhiteSpace(node.AssignedAgent) ? "Unassigned" : node.AssignedAgent)}"
                };

            if (!string.IsNullOrWhiteSpace(
                    node.ReviewStatus))
            {
                lines.Add(
                    $"Review: {node.ReviewStatus}");
            }

            if (node.IsDecompositionDraft)
            {
                lines.Add(
                    "Decomposition draft");
            }

            int associationCount =
                (node.AssociatedTaskIds ??
                 new List<string>())
                .Where(
                    id =>
                        !string.IsNullOrWhiteSpace(
                            id) &&
                        !string.Equals(
                            id,
                            node.Id,
                            StringComparison.Ordinal))
                .Distinct(
                    StringComparer.Ordinal)
                .Count();

            if (associationCount >
                0)
            {
                lines.Add(
                    $"Associations: {associationCount}");
            }

            return string.Join(
                "\n",
                lines);
        }


        private static Color GetStatusColor(
            string status)
        {
            switch (status)
            {
                case "Todo":
                    return new Color(
                        0.20f,
                        0.48f,
                        0.82f);

                case "InProgress":
                    return new Color(
                        0.88f,
                        0.55f,
                        0.12f);

                case "Review":
                    return new Color(
                        0.55f,
                        0.34f,
                        0.78f);

                case "Done":
                    return new Color(
                        0.22f,
                        0.62f,
                        0.35f);

                case "Crashed":
                    return new Color(1f, 0f, 0f);

                case "Failed":
                    return new Color(
                        0.75f,
                        0.18f,
                        0.2f);

                default:
                    return new Color(
                        0.38f,
                        0.42f,
                        0.47f);
            }
        }
    }
}
