using ZB.MOM.WW.ScadaBridge.Commons.Messages.Streaming;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;

namespace ZB.MOM.WW.ScadaBridge.CentralUI.Components.Pages.Deployment;

/// <summary>
/// Pure (no Blazor/DI) builder that turns a flat list of streamed attribute (and
/// alarm) events into a collapsible composition forest of
/// <see cref="DebugTreeNode"/>. Path-qualified canonical names are split on
/// <c>'.'</c> to derive branch nodes; the terminal segment carries the payload.
/// </summary>
public static class DebugTreeBuilder
{
    private const char Separator = '.';

    /// <summary>
    /// Build the attribute composition forest.
    /// </summary>
    /// <param name="attributes">Latest attribute value events (one per attribute).</param>
    /// <param name="filter">
    /// Optional name-contains filter (case-insensitive, matched against
    /// <see cref="AttributeValueChanged.AttributeName"/>). Null/empty/whitespace
    /// keeps everything; matching leaves carry along their ancestor branches.
    /// </param>
    /// <returns>The root nodes of the attribute composition forest.</returns>
    /// <remarks>
    /// The caller is expected to pass at most one <see cref="AttributeValueChanged"/>
    /// per <see cref="AttributeValueChanged.AttributeName"/>. The DebugView page
    /// enforces this by keying a dictionary on the attribute name before calling this
    /// method. Passing duplicate names would produce sibling leaves with identical
    /// keys under the same parent branch.
    /// </remarks>
    public static IReadOnlyList<DebugTreeNode> BuildAttributeTree(
        IEnumerable<AttributeValueChanged> attributes, string? filter)
    {
        ArgumentNullException.ThrowIfNull(attributes);

        var hasFilter = !string.IsNullOrWhiteSpace(filter);

        // Roots keyed by the first path segment; branch nodes deduped by full prefix.
        var roots = new List<DebugTreeNode>();
        var branchByKey = new Dictionary<string, DebugTreeNode>(StringComparer.Ordinal);

        foreach (var attr in attributes)
        {
            if (hasFilter &&
                attr.AttributeName.IndexOf(filter!, StringComparison.OrdinalIgnoreCase) < 0)
            {
                continue;
            }

            var segments = attr.AttributeName.Split(Separator);
            var parent = WalkBranches(segments, segments.Length - 1, roots, branchByKey);
            parent.Add(new DebugTreeNode
            {
                Key = attr.AttributeName,
                Segment = segments[^1],
                Attribute = attr,
            });
        }

        SortAndRollUp(roots);
        return roots.AsReadOnly();
    }

    /// <summary>
    /// Build the alarm composition forest from a flat list of <see cref="AlarmStateChanged"/>.
    /// </summary>
    /// <param name="alarms">Latest alarm-state events (computed alarms, native conditions, placeholders).</param>
    /// <param name="filter">
    /// Optional name-contains filter (case-insensitive). An alarm is kept when the filter
    /// is a substring of any of <see cref="AlarmStateChanged.AlarmName"/>,
    /// <see cref="AlarmStateChanged.SourceReference"/>, or
    /// <see cref="AlarmStateChanged.NativeSourceCanonicalName"/>. Null/empty/whitespace
    /// keeps everything; kept items carry along their ancestor branches and, for native
    /// conditions, their binding node.
    /// </param>
    /// <returns>The root nodes of the alarm composition forest.</returns>
    /// <remarks>
    /// Two flavours are placed differently:
    /// <list type="bullet">
    ///   <item><b>Computed</b> (<see cref="AlarmKind.Computed"/>): <see cref="AlarmStateChanged.AlarmName"/>
    ///   is a path-qualified canonical name; the alarm becomes a LEAF at that path, exactly
    ///   like an attribute.</item>
    ///   <item><b>Native</b> (OPC UA / MxAccess): the condition belongs to a source BINDING
    ///   identified by <see cref="AlarmStateChanged.NativeSourceCanonicalName"/> — itself a
    ///   path-qualified canonical name placed as a branch with
    ///   <see cref="DebugTreeNode.IsNativeBinding"/> set. Non-placeholder events become
    ///   condition children (keyed <c>canonical::sourceReference</c>); a placeholder event
    ///   only materialises the (childless) binding node so the page can render
    ///   "no active conditions".</item>
    /// </list>
    /// </remarks>
    public static IReadOnlyList<DebugTreeNode> BuildAlarmTree(
        IEnumerable<AlarmStateChanged> alarms, string? filter)
    {
        ArgumentNullException.ThrowIfNull(alarms);

        var hasFilter = !string.IsNullOrWhiteSpace(filter);

        // Roots keyed by the first path segment; branch nodes (and native binding
        // nodes) deduped by their full accumulated prefix / canonical name.
        var roots = new List<DebugTreeNode>();
        var branchByKey = new Dictionary<string, DebugTreeNode>(StringComparer.Ordinal);

        foreach (var alarm in alarms)
        {
            if (hasFilter && !MatchesFilter(alarm, filter!))
            {
                continue;
            }

            if (alarm.Kind == AlarmKind.Computed)
            {
                AddComputedLeaf(alarm, roots, branchByKey);
            }
            else
            {
                AddNativeCondition(alarm, roots, branchByKey);
            }
        }

        SortAndRollUpAlarms(roots);
        return roots.AsReadOnly();
    }

    /// <summary>True when the filter is a case-insensitive substring of any of the alarm's name fields.</summary>
    private static bool MatchesFilter(AlarmStateChanged alarm, string filter)
        => alarm.AlarmName.IndexOf(filter, StringComparison.OrdinalIgnoreCase) >= 0
            || alarm.SourceReference.IndexOf(filter, StringComparison.OrdinalIgnoreCase) >= 0
            || alarm.NativeSourceCanonicalName.IndexOf(filter, StringComparison.OrdinalIgnoreCase) >= 0;

    /// <summary>Place a computed alarm as a leaf at its path-qualified canonical name.</summary>
    private static void AddComputedLeaf(
        AlarmStateChanged alarm,
        List<DebugTreeNode> roots,
        Dictionary<string, DebugTreeNode> branchByKey)
    {
        var segments = alarm.AlarmName.Split(Separator);
        var parent = WalkBranches(segments, segments.Length - 1, roots, branchByKey);

        parent.Add(new DebugTreeNode
        {
            Key = alarm.AlarmName,
            Segment = segments[^1],
            Alarm = alarm,
        });
    }

    /// <summary>
    /// Place a native condition under its source-binding branch node. A placeholder only
    /// materialises the (childless) binding node; a real condition adds a child leaf keyed
    /// <c>canonical::sourceReference</c>.
    /// </summary>
    private static void AddNativeCondition(
        AlarmStateChanged alarm,
        List<DebugTreeNode> roots,
        Dictionary<string, DebugTreeNode> branchByKey)
    {
        if (string.IsNullOrEmpty(alarm.NativeSourceCanonicalName)) return;

        var canonical = alarm.NativeSourceCanonicalName;
        var segments = canonical.Split(Separator);

        // Walk/create the ancestor branches above the binding node, then get-or-create
        // the binding node itself (deduped by canonical name, flagged IsNativeBinding).
        var parent = WalkBranches(segments, segments.Length - 1, roots, branchByKey);

        if (!branchByKey.TryGetValue(canonical, out var binding))
        {
            binding = new DebugTreeNode
            {
                Key = canonical,
                Segment = segments[^1],
                IsNativeBinding = true,
            };
            branchByKey[canonical] = binding;
            parent.Add(binding);
        }

        // Placeholder: leave the binding node childless ("no active conditions").
        if (alarm.IsConfiguredPlaceholder)
        {
            return;
        }

        binding.Children.Add(new DebugTreeNode
        {
            Key = canonical + "::" + alarm.SourceReference,
            Segment = alarm.SourceReference,
            Alarm = alarm,
        });
    }

    /// <summary>
    /// Walk (creating as needed) the branch nodes for the first <paramref name="count"/>
    /// segments, deduping by accumulated prefix, and return the child list at that depth.
    /// </summary>
    private static List<DebugTreeNode> WalkBranches(
        string[] segments,
        int count,
        List<DebugTreeNode> roots,
        Dictionary<string, DebugTreeNode> branchByKey)
    {
        var currentLevel = roots;
        var prefix = string.Empty;

        for (var i = 0; i < count; i++)
        {
            prefix = prefix.Length == 0 ? segments[i] : prefix + Separator + segments[i];

            if (!branchByKey.TryGetValue(prefix, out var branch))
            {
                branch = new DebugTreeNode { Key = prefix, Segment = segments[i] };
                branchByKey[prefix] = branch;
                currentLevel.Add(branch);
            }

            currentLevel = branch.Children;
        }

        return currentLevel;
    }

    /// <summary>
    /// Recursively sort each node's children by <see cref="DebugTreeNode.Segment"/>
    /// (ordinal) and roll up alarm state post-order: <see cref="DebugTreeNode.WorstState"/>
    /// is <see cref="AlarmState.Active"/> when any descendant alarm leaf/condition is Active
    /// (placeholders excluded), and <see cref="DebugTreeNode.ActiveCount"/> tallies those
    /// active descendants. A native binding node rolls up over its condition children the
    /// same way. (Bad-quality is attribute-only and left default for alarm nodes.)
    /// </summary>
    private static void SortAndRollUpAlarms(List<DebugTreeNode> nodes)
    {
        nodes.Sort(static (a, b) => string.CompareOrdinal(a.Segment, b.Segment));

        foreach (var node in nodes)
        {
            // A leaf's own contribution: Active and not a configured placeholder.
            var ownActive = IsActiveAlarm(node);
            var activeCount = ownActive ? 1 : 0;

            if (node.Children.Count > 0)
            {
                SortAndRollUpAlarms(node.Children);
                activeCount += node.Children.Sum(static c => c.ActiveCount);
            }

            node.ActiveCount = activeCount;
            node.WorstState = activeCount > 0 ? AlarmState.Active : AlarmState.Normal;
        }
    }

    private static bool IsActiveAlarm(DebugTreeNode node)
        => node.Alarm is { State: AlarmState.Active, IsConfiguredPlaceholder: false };

    /// <summary>
    /// Recursively sort each node's children by <see cref="DebugTreeNode.Segment"/>
    /// (ordinal) and roll up bad-quality: a node has bad quality when its own
    /// attribute is off-Good or any descendant leaf is off-Good. Post-order so a
    /// child's roll-up is settled before its parent reads it.
    /// </summary>
    private static void SortAndRollUp(List<DebugTreeNode> nodes)
    {
        nodes.Sort(static (a, b) => string.CompareOrdinal(a.Segment, b.Segment));

        foreach (var node in nodes)
        {
            // Leaf's own quality (branches normally have no attribute).
            var bad = node.Attribute is not null && !IsGood(node.Attribute.Quality);

            if (node.Children.Count > 0)
            {
                SortAndRollUp(node.Children);
                bad = bad || node.Children.Any(static c => c.HasBadQuality);
            }

            node.HasBadQuality = bad;
        }
    }

    private static bool IsGood(string quality)
        => string.Equals(quality, "Good", StringComparison.Ordinal);
}
