namespace ZB.MOM.WW.OtOpcUa.Core.VirtualTags;

/// <summary>
///     Directed dependency graph over tag paths. Nodes are tag paths (either driver
///     tags — leaves — or virtual tags — internal nodes). Edges run from a virtual tag
///     to each tag it reads via <c>ctx.GetTag(...)</c>. Supports cycle detection at
///     publish time and topological sort for evaluation ordering.
/// </summary>
/// <remarks>
///     <para>
///         Cycle detection uses Tarjan's strongly-connected-components algorithm,
///         iterative implementation (no recursion) so deeply-nested graphs can't blow
///         the stack. A cycle of length > 1 (or a self-loop) is a publish-time error;
///         the engine refuses to load such a config.
///     </para>
///     <para>
///         Topological sort uses Kahn's algorithm. The output order guarantees that when
///         tag X depends on tag Y, Y appears before X — so a change cascade starting at
///         Y can evaluate the full downstream closure in one serial pass without needing
///         a second iteration.
///     </para>
///     <para>
///         Missing leaf dependencies (a virtual tag reads a driver tag that doesn't
///         exist in the live config) are NOT rejected here — the graph treats any
///         unregistered path as an implicit leaf. Leaf validity is a separate concern
///         handled at engine-load time against the authoritative tag catalog.
///     </para>
/// </remarks>
public sealed class DependencyGraph
{
    private readonly Dictionary<string, HashSet<string>> _dependsOn = new(StringComparer.Ordinal);
    private readonly Dictionary<string, HashSet<string>> _dependents = new(StringComparer.Ordinal);

    // Shared empty set returned from DirectDependencies / DirectDependents on a miss.
    // The CascadeAsync DFS and the Kahn topological sort both call DirectDependents
    // per leaf per pass; allocating a fresh HashSet each time would churn the GC on
    // every change-cascade event. Returning a shared immutable-via-convention empty
    // set is safe because callers only enumerate (the IReadOnlySet contract).
    private static readonly IReadOnlySet<string> EmptySet = new HashSet<string>(StringComparer.Ordinal);

    // Cached topological rank — built lazily by TransitiveDependentsInOrder and
    // invalidated whenever the graph is mutated (Add / Clear). Avoids re-running
    // a full O(V+E) Kahn pass on every change-cascade event.
    private Dictionary<string, int>? _cachedRank;

    /// <summary>
    ///     Register a node and the set of tags it depends on. Idempotent — re-adding
    ///     the same node overwrites the prior dependency set, so re-publishing an edited
    ///     script works without a separate "remove" call.
    /// </summary>
    /// <param name="nodeId">The node identifier.</param>
    /// <param name="dependsOn">The set of tags this node depends on.</param>
    public void Add(string nodeId, IReadOnlySet<string> dependsOn)
    {
        if (string.IsNullOrWhiteSpace(nodeId)) throw new ArgumentException("Node id required.", nameof(nodeId));
        if (dependsOn is null) throw new ArgumentNullException(nameof(dependsOn));

        // Remove any prior dependents pointing at the previous version of this node.
        if (_dependsOn.TryGetValue(nodeId, out var previous))
        {
            foreach (var dep in previous)
            {
                if (_dependents.TryGetValue(dep, out var set))
                    set.Remove(nodeId);
            }
        }

        _dependsOn[nodeId] = new HashSet<string>(dependsOn, StringComparer.Ordinal);
        foreach (var dep in dependsOn)
        {
            if (!_dependents.TryGetValue(dep, out var set))
                _dependents[dep] = set = new HashSet<string>(StringComparer.Ordinal);
            set.Add(nodeId);
        }
        _cachedRank = null; // graph mutated — invalidate cached rank
    }

    /// <summary>Tag paths <paramref name="nodeId"/> directly reads.</summary>
    /// <param name="nodeId">The node identifier.</param>
    /// <returns>The set of tags this node directly depends on.</returns>
    public IReadOnlySet<string> DirectDependencies(string nodeId) =>
        _dependsOn.TryGetValue(nodeId, out var set) ? set : EmptySet;

    /// <summary>
    ///     Tags whose evaluation depends on <paramref name="nodeId"/> — i.e. when
    ///     <paramref name="nodeId"/> changes, these need to re-evaluate. Direct only;
    ///     transitive propagation falls out of the topological sort.
    /// </summary>
    /// <param name="nodeId">The node identifier.</param>
    /// <returns>The set of tags that directly depend on this node.</returns>
    public IReadOnlySet<string> DirectDependents(string nodeId) =>
        _dependents.TryGetValue(nodeId, out var set) ? set : EmptySet;

    /// <summary>
    ///     Full transitive dependent closure of <paramref name="nodeId"/> in topological
    ///     order (direct dependents first, then their dependents, and so on). Used by the
    ///     change-trigger dispatcher to schedule the right sequence of re-evaluations
    ///     when a single upstream value changes.
    /// </summary>
    /// <param name="nodeId">The node identifier.</param>
    /// <returns>The list of all transitive dependents in topological order.</returns>
    public IReadOnlyList<string> TransitiveDependentsInOrder(string nodeId)
    {
        if (string.IsNullOrWhiteSpace(nodeId)) return [];

        var result = new List<string>();
        var visited = new HashSet<string>(StringComparer.Ordinal);

        // Reuse the cached rank to avoid an O(V+E) Kahn pass on every change event.
        // The cache is invalidated whenever the graph is mutated (Add / Clear), so it
        // is always consistent with the current graph structure.
        var rank = GetOrBuildRank();

        // DFS from the changed node collecting every reachable dependent.
        var stack = new Stack<string>();
        stack.Push(nodeId);
        while (stack.Count > 0)
        {
            var cur = stack.Pop();
            foreach (var dep in DirectDependents(cur))
            {
                if (visited.Add(dep))
                {
                    result.Add(dep);
                    stack.Push(dep);
                }
            }
        }

        // Sort by topological rank so when re-evaluation runs serial, earlier entries
        // are computed before later entries that might depend on them.
        result.Sort((a, b) =>
        {
            var ra = rank.TryGetValue(a, out var va) ? va : int.MaxValue;
            var rb = rank.TryGetValue(b, out var vb) ? vb : int.MaxValue;
            return ra.CompareTo(rb);
        });
        return result;
    }

    private Dictionary<string, int> GetOrBuildRank()
    {
        if (_cachedRank is not null) return _cachedRank;
        var order = TopologicalSort();
        var rank = new Dictionary<string, int>(order.Count, StringComparer.Ordinal);
        for (var i = 0; i < order.Count; i++) rank[order[i]] = i;
        _cachedRank = rank;
        return rank;
    }

    /// <summary>Iterable of every registered node id (inputs-only tags excluded).</summary>
    public IReadOnlyCollection<string> RegisteredNodes => _dependsOn.Keys;

    /// <summary>
    ///     Produce an evaluation order where every node appears after all its
    ///     dependencies. Throws <see cref="DependencyCycleException"/> if any cycle
    ///     exists. Implemented via Kahn's algorithm.
    /// </summary>
    /// <returns>The nodes in dependency-respecting evaluation order.</returns>
    public IReadOnlyList<string> TopologicalSort()
    {
        // Kahn's framing: edge u -> v means "u must come before v". For dependencies,
        // if X depends on Y, Y must come before X, so the edge runs Y -> X and X has
        // an incoming edge from Y. inDegree[X] = count of X's registered (virtual) deps
        // — leaf driver-tag deps don't contribute to ordering since they're never emitted.
        var inDegree = new Dictionary<string, int>(StringComparer.Ordinal);
        foreach (var node in _dependsOn.Keys) inDegree[node] = 0;
        foreach (var kv in _dependsOn)
        {
            var nodeId = kv.Key;
            foreach (var dep in kv.Value)
            {
                if (_dependsOn.ContainsKey(dep))
                    inDegree[nodeId]++;
            }
        }

        var ready = new Queue<string>(inDegree.Where(kv => kv.Value == 0).Select(kv => kv.Key));
        var result = new List<string>();
        while (ready.Count > 0)
        {
            var n = ready.Dequeue();
            result.Add(n);
            // In our edge direction (node -> deps), removing n means decrementing in-degree
            // of every node that DEPENDS on n.
            foreach (var dependent in DirectDependents(n))
            {
                if (inDegree.TryGetValue(dependent, out var d))
                {
                    inDegree[dependent] = d - 1;
                    if (inDegree[dependent] == 0) ready.Enqueue(dependent);
                }
            }
        }

        if (result.Count != inDegree.Count)
        {
            var cycles = DetectCycles();
            throw new DependencyCycleException(cycles);
        }
        return result;
    }

    /// <summary>
    ///     Returns every strongly-connected component of size &gt; 1 + every self-loop.
    ///     Empty list means the graph is a DAG. Useful for surfacing every cycle in one
    ///     rejection pass so operators see all of them, not just one at a time.
    /// </summary>
    /// <returns>Every strongly-connected component of size greater than 1, plus every self-loop; empty when the graph is a DAG.</returns>
    public IReadOnlyList<IReadOnlyList<string>> DetectCycles()
    {
        // Iterative Tarjan's SCC. Avoids recursion so deep graphs don't StackOverflow.
        var index = 0;
        var indexOf = new Dictionary<string, int>(StringComparer.Ordinal);
        var lowlinkOf = new Dictionary<string, int>(StringComparer.Ordinal);
        var onStack = new HashSet<string>(StringComparer.Ordinal);
        var sccStack = new Stack<string>();
        var cycles = new List<IReadOnlyList<string>>();

        foreach (var root in _dependsOn.Keys)
        {
            if (indexOf.ContainsKey(root)) continue;

            var work = new Stack<(string node, IEnumerator<string> iter)>();
            indexOf[root] = index;
            lowlinkOf[root] = index;
            index++;
            onStack.Add(root);
            sccStack.Push(root);
            work.Push((root, _dependsOn[root].GetEnumerator()));

            while (work.Count > 0)
            {
                var (v, iter) = work.Peek();
                if (iter.MoveNext())
                {
                    var w = iter.Current;
                    if (!_dependsOn.ContainsKey(w))
                        continue; // leaf — not part of any cycle with us
                    if (!indexOf.ContainsKey(w))
                    {
                        indexOf[w] = index;
                        lowlinkOf[w] = index;
                        index++;
                        onStack.Add(w);
                        sccStack.Push(w);
                        work.Push((w, _dependsOn[w].GetEnumerator()));
                    }
                    else if (onStack.Contains(w))
                    {
                        lowlinkOf[v] = Math.Min(lowlinkOf[v], indexOf[w]);
                    }
                }
                else
                {
                    // v fully explored — unwind
                    work.Pop();
                    if (lowlinkOf[v] == indexOf[v])
                    {
                        var component = new List<string>();
                        string w;
                        do
                        {
                            w = sccStack.Pop();
                            onStack.Remove(w);
                            component.Add(w);
                        } while (w != v);

                        if (component.Count > 1 || _dependsOn[v].Contains(v))
                            cycles.Add(component);
                    }
                    else if (work.Count > 0)
                    {
                        var parent = work.Peek().node;
                        lowlinkOf[parent] = Math.Min(lowlinkOf[parent], lowlinkOf[v]);
                    }
                }
            }
        }
        return cycles;
    }

    /// <summary>
    ///     Clears all nodes and dependencies from the graph.
    /// </summary>
    public void Clear()
    {
        _dependsOn.Clear();
        _dependents.Clear();
        _cachedRank = null; // graph cleared — invalidate cached rank
    }
}

/// <summary>Thrown when <see cref="DependencyGraph.TopologicalSort"/> finds one or more cycles.</summary>
public sealed class DependencyCycleException : Exception
{
    /// <summary>
    ///     Gets the cycles detected in the dependency graph.
    /// </summary>
    public IReadOnlyList<IReadOnlyList<string>> Cycles { get; }

    /// <summary>
    ///     Initializes a new instance of the <see cref="DependencyCycleException"/> class.
    /// </summary>
    /// <param name="cycles">The cycles detected in the dependency graph.</param>
    public DependencyCycleException(IReadOnlyList<IReadOnlyList<string>> cycles)
        : base(BuildMessage(cycles))
    {
        Cycles = cycles;
    }

    private static string BuildMessage(IReadOnlyList<IReadOnlyList<string>> cycles)
    {
        // Render each cycle as a comma-separated list of MEMBERS rather than an arrowed
        // edge path. Tarjan's algorithm returns SCC members in stack-pop order, which is
        // not guaranteed to be a valid edge sequence — for an SCC larger than two nodes
        // the previously-emitted "A -> B -> C -> A" rendering could list edges that do
        // not exist, sending operators looking for the wrong edge. Member framing avoids
        // implying an order or set of edges.
        var lines = cycles.Select(c =>
            "  - cycle members: " + string.Join(", ", c));
        return "Virtual-tag dependency graph contains cycle(s):\n" + string.Join("\n", lines);
    }
}
