using System.Globalization;
using Microsoft.AspNetCore.Components;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Audit;

namespace ZB.MOM.WW.ScadaBridge.CentralUI.Components.Audit;

/// <summary>
/// Recursive Blazor tree component for the execution-chain view (Audit Log
/// ParentExecutionId feature).
///
/// <para>
/// <b>Flat list → tree.</b> The repository / query service returns the chain as
/// a FLAT <see cref="ExecutionTreeNode"/> list (one per distinct execution). The
/// root instance (<see cref="Depth"/> == 0) assembles it once in
/// <see cref="OnParametersSet"/>: it groups by <see cref="ExecutionTreeNode.ExecutionId"/>,
/// links each node to its parent via <see cref="ExecutionTreeNode.ParentExecutionId"/>,
/// and identifies the roots (nodes whose parent is null or not present in the
/// list — a purged/ghost parent). Nested instances skip assembly: the parent
/// hands each child subtree down pre-built via <see cref="PreBuiltRoots"/>.
/// </para>
///
/// <para>
/// <b>Cycle safety.</b> The <c>ParentExecutionId</c> graph is acyclic by
/// construction, but the UI must not infinite-loop on corrupt data. Assembly
/// tracks visited <see cref="ExecutionTreeNode.ExecutionId"/> values while
/// walking children, so a node is attached to the tree at most once — a cycle
/// (A→B, B→A) is broken at the first revisit and every execution still renders
/// exactly once.
/// </para>
///
/// <para>
/// <b>Presentation.</b> Each node shows the short execution id (a link to
/// <c>/audit/log?executionId={id}</c>), row count, channels/statuses, source
/// site/instance, and time span. A stub node (<see cref="ExecutionTreeNode.RowCount"/>
/// == 0) is marked "No audited actions". The node the user arrived from
/// (<see cref="ArrivedFromExecutionId"/>) is highlighted. Nodes with children
/// are expandable; all nodes start expanded so the whole chain is visible.
/// </para>
/// </summary>
public partial class ExecutionTree
{
    /// <summary>
    /// One assembled subtree: a node plus its already-linked child subtrees.
    /// Recursive — children are themselves <see cref="Subtree"/> values.
    /// </summary>
    /// <param name="Node">The execution this subtree is rooted at.</param>
    /// <param name="Children">
    /// Child subtrees, ordered by <c>(FirstOccurredAtUtc ?? DateTime.MaxValue,
    /// ExecutionId)</c> — earliest first-occurrence time first, stub nodes
    /// (null timestamp) last, with <c>ExecutionId</c> breaking ties.
    /// </param>
    public sealed record Subtree(ExecutionTreeNode Node, IReadOnlyList<Subtree> Children);

    /// <summary>
    /// The flat node list to assemble into a tree. Supplied on the ROOT
    /// instance only (<see cref="Depth"/> == 0); nested instances receive
    /// <see cref="PreBuiltRoots"/> instead.
    /// </summary>
    [Parameter] public IReadOnlyList<ExecutionTreeNode>? Nodes { get; set; }

    /// <summary>
    /// Pre-assembled child subtrees, threaded down from a parent
    /// <see cref="ExecutionTree"/> so nested instances render without
    /// re-running the flat-list assembly. Null / unused on the root instance.
    /// </summary>
    [Parameter] public IReadOnlyList<Subtree>? PreBuiltRoots { get; set; }

    /// <summary>
    /// The execution the user drilled in from — its node is visually
    /// highlighted so the user keeps their bearings within the chain.
    /// </summary>
    [Parameter] public Guid ArrivedFromExecutionId { get; set; }

    /// <summary>
    /// Nesting depth. 0 on the root instance (which owns flat-list assembly);
    /// each recursive child increments it. Used purely to pick the assembly
    /// path and to tag the root <c>&lt;ul&gt;</c> for styling.
    /// </summary>
    [Parameter] public int Depth { get; set; }

    /// <summary>
    /// Raised when a node is double-clicked, carrying that node's
    /// <see cref="ExecutionTreeNode.ExecutionId"/>. The same callback is
    /// threaded unchanged into every recursive child instance, so a
    /// double-click on a node at any depth invokes the root-supplied handler
    /// (used to open the node detail modal).
    /// </summary>
    [Parameter] public EventCallback<Guid> OnNodeActivated { get; set; }

    // The subtrees this instance renders: assembled from Nodes on the root,
    // or taken straight from PreBuiltRoots on a nested instance.
    private IReadOnlyList<Subtree> _rootsToRender = Array.Empty<Subtree>();

    // The Nodes reference the current _rootsToRender was assembled from. Used
    // to skip a redundant re-assembly when OnParametersSet fires for an
    // unrelated parameter change (the flat list itself is unchanged).
    private IReadOnlyList<ExecutionTreeNode>? _assembledFrom;

    // Per-execution expand/collapse state. Absent => expanded (the default):
    // the whole chain is shown on arrival so the user sees the full picture.
    private readonly HashSet<Guid> _collapsed = new();

    /// <inheritdoc />
    protected override void OnParametersSet()
    {
        // Nested instance: the parent already assembled our subtrees.
        if (Depth > 0)
        {
            _rootsToRender = PreBuiltRoots ?? Array.Empty<Subtree>();
            return;
        }

        // Root instance: assemble the flat list into a tree. Re-assemble only
        // when the Nodes reference itself changes — OnParametersSet also fires
        // for unrelated parameter changes (e.g. ArrivedFromExecutionId), and
        // re-running assembly then would needlessly rebuild an identical tree.
        if (!ReferenceEquals(Nodes, _assembledFrom))
        {
            _assembledFrom = Nodes;
            _rootsToRender = BuildForest(Nodes ?? Array.Empty<ExecutionTreeNode>());
        }
    }

    /// <summary>
    /// Assembles the flat <see cref="ExecutionTreeNode"/> list into a forest of
    /// <see cref="Subtree"/> values. There is normally exactly one root (the
    /// chain's topmost ancestor); the method returns a list to stay total if
    /// the input ever contains disjoint fragments. A fully-cyclic feed has no
    /// real root, so each remaining cyclic component is seeded with a fallback
    /// root after the main pass — every execution in <paramref name="nodes"/>
    /// is therefore placed in the forest exactly once.
    /// </summary>
    private static IReadOnlyList<Subtree> BuildForest(IReadOnlyList<ExecutionTreeNode> nodes)
    {
        if (nodes.Count == 0)
        {
            return Array.Empty<Subtree>();
        }

        // De-dupe defensively: the repository emits one node per execution, but
        // a corrupt feed could repeat an id. First write wins.
        var byId = new Dictionary<Guid, ExecutionTreeNode>();
        foreach (var node in nodes)
        {
            byId.TryAdd(node.ExecutionId, node);
        }

        // Children grouped by parent id. A node whose parent is null or absent
        // from the list (a purged/ghost parent) is a root.
        var childrenByParent = new Dictionary<Guid, List<ExecutionTreeNode>>();
        var roots = new List<ExecutionTreeNode>();
        foreach (var node in byId.Values)
        {
            if (node.ParentExecutionId is { } parentId && byId.ContainsKey(parentId))
            {
                if (!childrenByParent.TryGetValue(parentId, out var bucket))
                {
                    bucket = new List<ExecutionTreeNode>();
                    childrenByParent[parentId] = bucket;
                }
                bucket.Add(node);
            }
            else
            {
                roots.Add(node);
            }
        }

        var visited = new HashSet<Guid>();
        var forest = roots
            .OrderBy(SortKey)
            .Select(root => BuildSubtree(root, childrenByParent, visited))
            .ToList();

        // Cycle guard: if the input is fully cyclic every node has a present
        // parent, so a cyclic component contributes no entry to `roots`. Any
        // execution still missing from `visited` after the pass above belongs
        // to such a component (a corrupt feed may contain several independent
        // cycles, e.g. A↔B and C↔D). Seed the lowest-ordered unvisited id of
        // each remaining component as an extra root and assemble it, looping
        // until every node has been placed — so every execution renders.
        while (visited.Count < byId.Count)
        {
            var fallbackRoot = byId.Values
                .Where(n => !visited.Contains(n.ExecutionId))
                .OrderBy(SortKey)
                .First();
            forest.Add(BuildSubtree(fallbackRoot, childrenByParent, visited));
        }

        return forest;
    }

    /// <summary>
    /// Recursively builds one <see cref="Subtree"/>, tracking
    /// <paramref name="visited"/> so a cyclic flat list cannot drive unbounded
    /// recursion — a node already attached is never descended into again.
    /// </summary>
    private static Subtree BuildSubtree(
        ExecutionTreeNode node,
        IReadOnlyDictionary<Guid, List<ExecutionTreeNode>> childrenByParent,
        HashSet<Guid> visited)
    {
        visited.Add(node.ExecutionId);

        var children = new List<Subtree>();
        if (childrenByParent.TryGetValue(node.ExecutionId, out var directChildren))
        {
            foreach (var child in directChildren.OrderBy(SortKey))
            {
                // Cycle / DAG guard: skip any execution already placed in the
                // tree so each renders exactly once and recursion terminates.
                if (visited.Contains(child.ExecutionId))
                {
                    continue;
                }
                children.Add(BuildSubtree(child, childrenByParent, visited));
            }
        }

        return new Subtree(node, children);
    }

    // Stable child ordering: earliest activity first; stub nodes (null
    // timestamp) sort last; ExecutionId breaks ties so rendering is
    // deterministic across requests.
    private static (DateTime, Guid) SortKey(ExecutionTreeNode node)
        => (node.FirstOccurredAtUtc ?? DateTime.MaxValue, node.ExecutionId);

    private bool IsExpanded(Guid executionId) => !_collapsed.Contains(executionId);

    private void ToggleExpand(Guid executionId)
    {
        if (!_collapsed.Remove(executionId))
        {
            _collapsed.Add(executionId);
        }
    }

    /// <summary>Audit Log deep link filtered to one execution's rows.</summary>
    private static string AuditLogUrl(Guid executionId)
        => $"/audit/log?executionId={executionId}";

    /// <summary>First 8 hex digits — the short-id presentation used across the Audit UI.</summary>
    private static string ShortId(Guid value)
    {
        var n = value.ToString("N");
        return n.Length >= 8 ? n[..8] : n;
    }

    /// <summary>
    /// Renders the [first, last] occurrence span. Both null on a stub node
    /// (handled by the caller); a single-row execution shows one timestamp.
    /// </summary>
    private static string FormatSpan(DateTime? firstUtc, DateTime? lastUtc)
    {
        if (firstUtc is null && lastUtc is null)
        {
            return "—";
        }

        var first = firstUtc ?? lastUtc!.Value;
        var last = lastUtc ?? firstUtc!.Value;
        var firstText = Iso(first);
        if (first == last)
        {
            return firstText;
        }
        return $"{firstText} → {Iso(last)}";
    }

    // Audit timestamps are UTC by system convention, so the value is formatted
    // with a literal 'Z' suffix without re-tagging its DateTimeKind.
    private static string Iso(DateTime utc)
        => utc.ToString("yyyy-MM-dd HH:mm:ss'Z'", CultureInfo.InvariantCulture);
}
