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

namespace ZB.MOM.WW.ScadaBridge.TemplateEngine;

/// <summary>
/// Detects cycles in template inheritance and composition graphs.
/// Covers: self-inheritance, circular inheritance chains, self-composition,
/// circular composition chains, and cross-graph (inheritance + composition) cycles.
/// </summary>
public static class CycleDetector
{
    /// <summary>
    /// Builds an Id-keyed lookup that tolerates duplicate Ids in the input list
    /// (e.g. multiple not-yet-saved templates all carrying Id 0). On a duplicate
    /// the first occurrence wins — graph walks only need one representative node
    /// per Id, and a real cycle through any duplicate would still be reachable.
    /// A plain <c>ToDictionary(t =&gt; t.Id)</c> would instead throw ArgumentException.
    /// </summary>
    /// <param name="allTemplates">All templates to build lookup from.</param>
    /// <returns>A dictionary keyed by template Id; on duplicate Ids the first occurrence wins.</returns>
    internal static Dictionary<int, Template> BuildLookup(IReadOnlyList<Template> allTemplates)
    {
        var lookup = new Dictionary<int, Template>();
        foreach (var t in allTemplates)
            lookup.TryAdd(t.Id, t);
        return lookup;
    }

    /// <summary>
    /// Checks whether setting <paramref name="parentId"/> as the parent of template
    /// <paramref name="templateId"/> would introduce an inheritance cycle.
    /// </summary>
    /// <param name="templateId">The template being modified.</param>
    /// <param name="parentId">The proposed parent template ID.</param>
    /// <param name="allTemplates">All templates for cycle detection.</param>
    /// <returns>A description of the cycle if one would be created, or null if safe.</returns>
    public static string? DetectInheritanceCycle(
        int templateId,
        int parentId,
        IReadOnlyList<Template> allTemplates)
    {
        if (templateId == parentId)
        {
            var selfName = allTemplates.FirstOrDefault(t => t.Id == templateId)?.Name ?? templateId.ToString();
            return $"Template '{selfName}' cannot inherit from itself.";
        }

        // Walk the inheritance chain from the proposed parent upward.
        // If we arrive back at templateId, there is a cycle.
        var lookup = BuildLookup(allTemplates);
        var visited = new HashSet<int> { templateId };
        var chain = new List<string>();

        var templateName = lookup.TryGetValue(templateId, out var tmpl) ? tmpl.Name : templateId.ToString();
        chain.Add(templateName);

        // ParentTemplateId is int? — a missing value (not 0) means "no parent",
        // so a template with a real Id of 0 is walked like any other node.
        int? currentId = parentId;
        while (currentId.HasValue)
        {
            if (!lookup.TryGetValue(currentId.Value, out var current))
                break;

            chain.Add(current.Name);

            if (visited.Contains(currentId.Value))
            {
                return $"Inheritance cycle detected: {string.Join(" -> ", chain)}.";
            }

            visited.Add(currentId.Value);
            currentId = current.ParentTemplateId;
        }

        return null;
    }

    /// <summary>
    /// Checks whether adding a composition of <paramref name="composedTemplateId"/> into
    /// <paramref name="templateId"/> would introduce a composition cycle.
    /// </summary>
    /// <param name="templateId">The template being modified.</param>
    /// <param name="composedTemplateId">The template to compose.</param>
    /// <param name="allTemplates">All templates for cycle detection.</param>
    /// <returns>A description of the cycle if one would be created, or null if safe.</returns>
    public static string? DetectCompositionCycle(
        int templateId,
        int composedTemplateId,
        IReadOnlyList<Template> allTemplates)
    {
        if (templateId == composedTemplateId)
        {
            var selfName = allTemplates.FirstOrDefault(t => t.Id == templateId)?.Name ?? templateId.ToString();
            return $"Template '{selfName}' cannot compose itself.";
        }

        var lookup = BuildLookup(allTemplates);

        // BFS/DFS from composedTemplateId through all its compositions.
        // If we reach templateId, that's a cycle.
        var visited = new HashSet<int>();
        var queue = new Queue<int>();
        queue.Enqueue(composedTemplateId);

        while (queue.Count > 0)
        {
            var currentId = queue.Dequeue();
            if (currentId == templateId)
            {
                var tmplName = lookup.TryGetValue(templateId, out var t1) ? t1.Name : templateId.ToString();
                var composedName = lookup.TryGetValue(composedTemplateId, out var t2) ? t2.Name : composedTemplateId.ToString();
                return $"Composition cycle detected: '{tmplName}' -> '{composedName}' -> ... -> '{tmplName}'.";
            }

            if (!visited.Add(currentId))
                continue;

            if (!lookup.TryGetValue(currentId, out var current))
                continue;

            foreach (var comp in current.Compositions)
            {
                queue.Enqueue(comp.ComposedTemplateId);
            }
        }

        return null;
    }

    /// <summary>
    /// Detects cross-graph cycles that span both inheritance and composition edges.
    /// A cross-graph cycle exists when following any combination of inheritance (parent)
    /// and composition edges from a template leads back to itself.
    /// </summary>
    /// <param name="templateId">The template being modified.</param>
    /// <param name="proposedParentId">Optional proposed parent template ID.</param>
    /// <param name="proposedComposedTemplateId">Optional proposed composition template ID.</param>
    /// <param name="allTemplates">All templates for cycle detection.</param>
    /// <returns>A description of the cycle if found, or null if safe.</returns>
    public static string? DetectCrossGraphCycle(
        int templateId,
        int? proposedParentId,
        int? proposedComposedTemplateId,
        IReadOnlyList<Template> allTemplates)
    {
        var lookup = BuildLookup(allTemplates);

        // Build adjacency: for each template, collect all reachable templates
        // via inheritance (parent) and composition edges.
        // We temporarily add the proposed edge and check for reachability back to templateId.

        var visited = new HashSet<int>();
        var queue = new Queue<int>();

        // Seed with proposed targets. A null proposed id means "no edge"; a value
        // of 0 is a legitimate Id, so only HasValue gates enqueuing.
        if (proposedParentId.HasValue)
            queue.Enqueue(proposedParentId.Value);

        if (proposedComposedTemplateId.HasValue)
            queue.Enqueue(proposedComposedTemplateId.Value);

        while (queue.Count > 0)
        {
            var currentId = queue.Dequeue();
            if (currentId == templateId)
            {
                var tmplName = lookup.TryGetValue(templateId, out var t) ? t.Name : templateId.ToString();
                return $"Cross-graph cycle detected involving template '{tmplName}'.";
            }

            if (!visited.Add(currentId))
                continue;

            if (!lookup.TryGetValue(currentId, out var current))
                continue;

            // Follow inheritance edge (int? — missing value means no parent)
            if (current.ParentTemplateId.HasValue)
                queue.Enqueue(current.ParentTemplateId.Value);

            // Follow composition edges
            foreach (var comp in current.Compositions)
            {
                queue.Enqueue(comp.ComposedTemplateId);
            }
        }

        return null;
    }
}
