using ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates;

namespace ZB.MOM.WW.ScadaBridge.TemplateEngine;

/// <summary>
/// Detects naming collisions across composed module members using canonical (path-qualified) names.
/// Two members from different composed modules collide if they produce the same canonical name.
/// Members from different module instance names cannot collide because the prefix differentiates them.
/// </summary>
public static class CollisionDetector
{
    /// <summary>
    /// Represents a resolved member with its canonical name and origin.
    /// </summary>
    public sealed record ResolvedMember(
        string CanonicalName,
        string MemberType, // "Attribute", "Alarm", "Script"
        string OriginDescription);

    /// <summary>
    /// Detects naming collisions among all members (direct + composed) of a template.
    /// </summary>
    /// <param name="template">The template to check.</param>
    /// <param name="allTemplates">All templates in the system (for resolving composed templates).</param>
    /// <returns>List of collision descriptions. Empty if no collisions.</returns>
    public static IReadOnlyList<string> DetectCollisions(
        Template template,
        IReadOnlyList<Template> allTemplates)
    {
        // Duplicate-tolerant lookup (see CycleDetector.BuildLookup): a plain
        // ToDictionary(t => t.Id) throws if two templates share an Id (e.g.
        // not-yet-saved templates carrying Id 0).
        var lookup = CycleDetector.BuildLookup(allTemplates);
        var allMembers = new List<ResolvedMember>();

        // Collect direct (top-level) members. Inherited placeholder rows are skipped:
        // the inheritance walk below re-adds them under the parent's origin.
        CollectDirectMembers(template, prefix: null, originPrefix: template.Name, allMembers, skipInherited: true);

        // Collect members from composed modules recursively
        foreach (var composition in template.Compositions)
        {
            if (lookup.TryGetValue(composition.ComposedTemplateId, out var composedTemplate))
            {
                CollectComposedMembers(
                    composedTemplate,
                    prefix: composition.InstanceName,
                    lookup,
                    allMembers,
                    visited: new HashSet<int>());
            }
        }

        // Collect inherited members (walk parent chain)
        CollectInheritedMembers(template, lookup, allMembers, new HashSet<int> { template.Id });

        // Detect duplicates by canonical name
        var collisions = new List<string>();
        var grouped = allMembers.GroupBy(m => m.CanonicalName, StringComparer.Ordinal);

        foreach (var group in grouped)
        {
            var members = group.ToList();
            if (members.Count > 1)
            {
                // Only report collision if members come from different origins
                var distinctOrigins = members.Select(m => m.OriginDescription).Distinct().ToList();
                if (distinctOrigins.Count > 1)
                {
                    var origins = string.Join(", ", members.Select(m => $"{m.MemberType} from {m.OriginDescription}"));
                    collisions.Add($"Naming collision on '{group.Key}': {origins}.");
                }
            }
        }

        return collisions;
    }

    /// <param name="skipInherited">
    /// When <c>true</c>, <c>IsInherited</c> placeholder rows are skipped. Those rows
    /// are materialized copies of members the template inherits from its parent
    /// chain, and they are ALSO re-added — under the parent's name — by
    /// <see cref="CollectInheritedMembers"/>. Counting both yielded two distinct
    /// origins for the same canonical name and a spurious collision, which blocked
    /// every attempt to add an attribute/composition to a derived template.
    /// Pass <c>false</c> only for the composed-module walk, where
    /// placeholder rows are the sole representation of a derived module's inherited
    /// members (that walk does not climb the composed template's parent chain).
    /// </param>
    private static void CollectDirectMembers(
        Template template,
        string? prefix,
        string originPrefix,
        List<ResolvedMember> members,
        bool skipInherited)
    {
        foreach (var attr in template.Attributes)
        {
            if (skipInherited && attr.IsInherited) continue;
            var canonicalName = prefix == null ? attr.Name : $"{prefix}.{attr.Name}";
            members.Add(new ResolvedMember(canonicalName, "Attribute", originPrefix));
        }

        foreach (var alarm in template.Alarms)
        {
            if (skipInherited && alarm.IsInherited) continue;
            var canonicalName = prefix == null ? alarm.Name : $"{prefix}.{alarm.Name}";
            members.Add(new ResolvedMember(canonicalName, "Alarm", originPrefix));
        }

        foreach (var script in template.Scripts)
        {
            if (skipInherited && script.IsInherited) continue;
            var canonicalName = prefix == null ? script.Name : $"{prefix}.{script.Name}";
            members.Add(new ResolvedMember(canonicalName, "Script", originPrefix));
        }
    }

    private static void CollectComposedMembers(
        Template template,
        string prefix,
        Dictionary<int, Template> lookup,
        List<ResolvedMember> members,
        HashSet<int> visited)
    {
        // `visited` is the current RECURSION PATH, not a global visited-set: an id is
        // added on entry and REMOVED on exit (finally), so a template legitimately
        // composed into more than one slot is re-descended for each slot. The early
        // return fires only on a genuine cycle — an id already on the ACTIVE path.
        // (A global visited-set would blind the detector to every collision reachable
        // only through the second-and-later slot of a repeatedly-composed template.)
        if (!visited.Add(template.Id))
            return;

        try
        {
            // Add direct members of this composed template with the prefix. Inherited
            // placeholder rows are KEPT here: the composed-module walk does not climb the
            // composed template's parent chain, so the placeholders are the only
            // representation of a derived module's inherited members.
            CollectDirectMembers(template, prefix, $"module '{prefix}'", members, skipInherited: false);

            // Recurse into nested compositions
            foreach (var composition in template.Compositions)
            {
                if (lookup.TryGetValue(composition.ComposedTemplateId, out var nested))
                {
                    var nestedPrefix = $"{prefix}.{composition.InstanceName}";
                    CollectComposedMembers(nested, nestedPrefix, lookup, members, visited);
                }
            }
        }
        finally
        {
            visited.Remove(template.Id);
        }
    }

    private static void CollectInheritedMembers(
        Template template,
        Dictionary<int, Template> lookup,
        List<ResolvedMember> members,
        HashSet<int> visited)
    {
        if (!template.ParentTemplateId.HasValue)
            return;

        if (!lookup.TryGetValue(template.ParentTemplateId.Value, out var parent))
            return;

        if (!visited.Add(parent.Id))
            return;

        // Inherited direct members (no prefix). Skip the parent's OWN inherited
        // placeholders too: the next recursion adds the grandparent's real rows, so
        // counting the parent's copies would re-introduce the collision one level up.
        CollectDirectMembers(parent, prefix: null, $"parent '{parent.Name}'", members, skipInherited: true);

        // Inherited composed modules
        foreach (var composition in parent.Compositions)
        {
            if (lookup.TryGetValue(composition.ComposedTemplateId, out var composedTemplate))
            {
                CollectComposedMembers(
                    composedTemplate,
                    composition.InstanceName,
                    lookup,
                    members,
                    new HashSet<int>());
            }
        }

        // Continue up the inheritance chain
        CollectInheritedMembers(parent, lookup, members, visited);
    }
}
