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

namespace ZB.MOM.WW.ScadaBridge.TemplateEngine;

/// <summary>
/// Walks inheritance and composition chains to resolve effective template members.
/// Produces canonical (path-qualified) names for composed module members.
/// </summary>
public static class TemplateResolver
{
    /// <summary>
    /// Represents a resolved member from any point in the inheritance/composition hierarchy.
    /// </summary>
    public sealed record ResolvedTemplateMember
    {
        /// <summary>
        /// Gets the canonical (path-qualified) name of the member.
        /// </summary>
        public string CanonicalName { get; init; }
        /// <summary>
        /// Gets the member type: "Attribute", "Alarm", or "Script".
        /// </summary>
        public string MemberType { get; init; }
        /// <summary>
        /// Gets the ID of the template that originally defined this member.
        /// </summary>
        public int SourceTemplateId { get; init; }
        /// <summary>
        /// Gets the ID of the member within its source template.
        /// </summary>
        public int MemberId { get; init; }
        /// <summary>
        /// Gets a value indicating whether this member is locked from override.
        /// </summary>
        public bool IsLocked { get; init; }
        /// <summary>
        /// Gets the module path for composed members, or null for direct members.
        /// </summary>
        public string? ModulePath { get; init; }

        /// <summary>
        /// Initializes a new instance of the <see cref="ResolvedTemplateMember"/> record.
        /// </summary>
        /// <param name="canonicalName">The canonical name of the member.</param>
        /// <param name="memberType">The member type: "Attribute", "Alarm", or "Script".</param>
        /// <param name="sourceTemplateId">The ID of the source template.</param>
        /// <param name="memberId">The member ID within the source template.</param>
        /// <param name="isLocked">Whether the member is locked from override.</param>
        /// <param name="modulePath">The module path for composed members; null for direct members.</param>
        public ResolvedTemplateMember(string canonicalName, string memberType, int sourceTemplateId, int memberId, bool isLocked, string? modulePath = null)
        {
            CanonicalName = canonicalName;
            MemberType = memberType;
            SourceTemplateId = sourceTemplateId;
            MemberId = memberId;
            IsLocked = isLocked;
            ModulePath = modulePath;
        }
    }

    /// <summary>
    /// Resolves all effective members for a template, walking inheritance and composition chains.
    /// Child members override parent members of the same canonical name (unless locked).
    /// </summary>
    /// <param name="templateId">The template ID to resolve members for.</param>
    /// <param name="allTemplates">The complete list of all templates for lookups.</param>
    /// <returns>A read-only list of resolved members for the template.</returns>
    public static IReadOnlyList<ResolvedTemplateMember> ResolveAllMembers(
        int templateId,
        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);
        if (!lookup.TryGetValue(templateId, out var template))
            return Array.Empty<ResolvedTemplateMember>();

        // Build inheritance chain from root to leaf (root first, child last)
        var chain = BuildInheritanceChain(templateId, lookup);

        // Start with root members, apply overrides from each child
        var effectiveMembers = new Dictionary<string, ResolvedTemplateMember>(StringComparer.Ordinal);

        foreach (var tmpl in chain)
        {
            // Direct members
            AddDirectMembers(tmpl, prefix: null, effectiveMembers);

            // Composed module members
            foreach (var composition in tmpl.Compositions)
            {
                if (lookup.TryGetValue(composition.ComposedTemplateId, out var composedTemplate))
                {
                    AddComposedMembers(composedTemplate, composition.InstanceName, lookup, effectiveMembers, new HashSet<int>());
                }
            }
        }

        return effectiveMembers.Values.ToList();
    }

    /// <summary>
    /// Gets the inheritance chain from root ancestor to the specified template.
    /// </summary>
    /// <remarks>
    /// The parent walk uses the <see cref="int?"/>
    /// <see cref="Template.ParentTemplateId"/> directly — only a missing
    /// (<c>null</c>) value means "no parent". The legacy <c>0</c>-as-no-parent
    /// sentinel that was removed from <c>CycleDetector</c> in the
    /// fix had silently truncated chains whenever a real
    /// template id of 0 appeared (e.g. import-staging / not-yet-saved rows);
    /// the duplicate-tolerant <c>BuildLookup</c> upstream means an Id of 0 is
    /// a valid node here and must walk the chain like any other.
    /// </remarks>
    /// <param name="templateId">The template ID to build the chain for.</param>
    /// <param name="lookup">A dictionary mapping template IDs to templates.</param>
    /// <returns>A read-only list of templates from root to the specified template.</returns>
    public static IReadOnlyList<Template> BuildInheritanceChain(
        int templateId,
        IReadOnlyDictionary<int, Template> lookup)
    {
        var chain = new List<Template>();
        int? currentId = templateId;
        var visited = new HashSet<int>();

        while (currentId.HasValue && lookup.TryGetValue(currentId.Value, out var current))
        {
            if (!visited.Add(currentId.Value))
                break; // Safety: cycle detected

            chain.Add(current);
            currentId = current.ParentTemplateId;
        }

        chain.Reverse(); // Root first
        return chain;
    }

    /// <summary>
    /// Finds a member by canonical name in the resolved member set.
    /// Used to check override/lock constraints.
    /// </summary>
    /// <param name="canonicalName">The canonical name of the member to find.</param>
    /// <param name="parentTemplateId">The template ID to resolve members from.</param>
    /// <param name="allTemplates">The complete list of all templates for lookups.</param>
    /// <returns>The resolved member if found; null otherwise.</returns>
    public static ResolvedTemplateMember? FindMemberByCanonicalName(
        string canonicalName,
        int parentTemplateId,
        IReadOnlyList<Template> allTemplates)
    {
        var members = ResolveAllMembers(parentTemplateId, allTemplates);
        return members.FirstOrDefault(m => m.CanonicalName == canonicalName);
    }

    private static void AddDirectMembers(
        Template template,
        string? prefix,
        Dictionary<string, ResolvedTemplateMember> effectiveMembers)
    {
        foreach (var attr in template.Attributes)
        {
            var canonicalName = prefix == null ? attr.Name : $"{prefix}.{attr.Name}";
            effectiveMembers[canonicalName] = new ResolvedTemplateMember(
                canonicalName, "Attribute", template.Id, attr.Id, attr.IsLocked, prefix);
        }

        foreach (var alarm in template.Alarms)
        {
            var canonicalName = prefix == null ? alarm.Name : $"{prefix}.{alarm.Name}";
            effectiveMembers[canonicalName] = new ResolvedTemplateMember(
                canonicalName, "Alarm", template.Id, alarm.Id, alarm.IsLocked, prefix);
        }

        foreach (var script in template.Scripts)
        {
            var canonicalName = prefix == null ? script.Name : $"{prefix}.{script.Name}";
            effectiveMembers[canonicalName] = new ResolvedTemplateMember(
                canonicalName, "Script", template.Id, script.Id, script.IsLocked, prefix);
        }
    }

    private static void AddComposedMembers(
        Template template,
        string prefix,
        Dictionary<int, Template> lookup,
        Dictionary<string, ResolvedTemplateMember> effectiveMembers,
        HashSet<int> visited)
    {
        if (!visited.Add(template.Id))
            return;

        AddDirectMembers(template, prefix, effectiveMembers);

        foreach (var composition in template.Compositions)
        {
            if (lookup.TryGetValue(composition.ComposedTemplateId, out var nested))
            {
                var nestedPrefix = $"{prefix}.{composition.InstanceName}";
                AddComposedMembers(nested, nestedPrefix, lookup, effectiveMembers, visited);
            }
        }
    }
}
