using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
using ZB.MOM.WW.OtOpcUa.Core.Authorization;
using ZB.MOM.WW.OtOpcUa.Core.OpcUa;

namespace ZB.MOM.WW.OtOpcUa.Server.Security;

/// <summary>
///     Builds the <see cref="NodeScope"/> path index consumed by <see cref="NodeScopeResolver"/>
///     from a Config-DB snapshot of a single published generation. Runs once per generation
///     (or on every generation change) at the Server bootstrap layer; the produced index is
///     immutable + hot-path readable per ADR-001 Task B.
/// </summary>
/// <remarks>
///     <para>The index key is the driver-side full reference (<c>Tag.TagConfig</c>) — the same
///     string the dispatch layer passes to <see cref="NodeScopeResolver.Resolve"/>. The value
///     is a <see cref="NodeScope"/> with every UNS level populated:
///     <c>ClusterId / NamespaceId / UnsAreaId / UnsLineId / EquipmentId / TagId</c>. Tag rows
///     with null <c>EquipmentId</c> (SystemPlatform-namespace Galaxy tags per decision #120)
///     are excluded from the index — the cluster-only fallback path in the resolver handles
///     them without needing an index entry.</para>
///
///     <para>Duplicate keys are not expected but would be indicative of corrupt data — the
///     builder throws <see cref="InvalidOperationException"/> on collision so a config drift
///     surfaces at bootstrap instead of producing silently-last-wins scopes at dispatch.</para>
/// </remarks>
public static class ScopePathIndexBuilder
{
    /// <summary>
    ///     Build a fullReference → NodeScope index from the four Config-DB collections for a
    ///     single namespace. Callers must filter inputs to a single
    ///     <see cref="Namespace"/> + the same <see cref="ConfigGeneration"/> upstream.
    /// </summary>
    /// <param name="clusterId">Owning cluster — populates <see cref="NodeScope.ClusterId"/>.</param>
    /// <param name="namespaceId">Owning namespace — populates <see cref="NodeScope.NamespaceId"/>.</param>
    /// <param name="content">Pre-loaded rows for the namespace.</param>
    public static IReadOnlyDictionary<string, NodeScope> Build(
        string clusterId,
        string namespaceId,
        EquipmentNamespaceContent content)
    {
        ArgumentException.ThrowIfNullOrWhiteSpace(clusterId);
        ArgumentException.ThrowIfNullOrWhiteSpace(namespaceId);
        ArgumentNullException.ThrowIfNull(content);

        var areaByLine = content.Lines.ToDictionary(l => l.UnsLineId, l => l.UnsAreaId, StringComparer.OrdinalIgnoreCase);
        var lineByEquipment = content.Equipment.ToDictionary(e => e.EquipmentId, e => e.UnsLineId, StringComparer.OrdinalIgnoreCase);

        var index = new Dictionary<string, NodeScope>(StringComparer.Ordinal);

        foreach (var tag in content.Tags)
        {
            // Null EquipmentId = SystemPlatform-namespace tag per decision #110 — skip; the
            // cluster-only resolver fallback handles those without needing an index entry.
            if (string.IsNullOrEmpty(tag.EquipmentId)) continue;

            // Broken FK — Tag references a missing Equipment row. Skip rather than crash;
            // sp_ValidateDraft should have caught this at publish, so any drift here is
            // unexpected but non-fatal.
            if (!lineByEquipment.TryGetValue(tag.EquipmentId, out var lineId)) continue;
            if (!areaByLine.TryGetValue(lineId, out var areaId)) continue;

            var scope = new NodeScope
            {
                ClusterId = clusterId,
                NamespaceId = namespaceId,
                UnsAreaId = areaId,
                UnsLineId = lineId,
                EquipmentId = tag.EquipmentId,
                TagId = tag.TagConfig,
                Kind = NodeHierarchyKind.Equipment,
            };

            if (!index.TryAdd(tag.TagConfig, scope))
                throw new InvalidOperationException(
                    $"Duplicate fullReference '{tag.TagConfig}' in Equipment namespace '{namespaceId}'. " +
                    "Config data is corrupt — two Tag rows produced the same wire-level address.");
        }

        return index;
    }
}
