namespace ZB.MOM.WW.OtOpcUa.Commons.Types;

/// <summary>
///     Builds the per-equipment reference map — <c>equipmentId → (effectiveName → backing-tag RawPath)</c> —
///     the single authority both compose seams (<c>AddressSpaceComposer</c> + <c>DeploymentArtifact</c>)
///     and the draft validator use to resolve <c>{{equip}}/&lt;RefName&gt;</c> script paths. A reference's
///     <b>effective name</b> is its <c>DisplayNameOverride</c> else the backing raw tag's <c>Name</c>; the
///     RawPath is computed through the shared <see cref="RawPathResolver"/> (the same identity authority the
///     rest of v3 uses), so the entity side (authoring/validation) and the artifact-decode side agree
///     byte-for-byte.
///     <para>Input-shape agnostic + pure: callers flatten their references (EF entities on one side,
///     artifact JSON on the other) into the same primitive tuple + tag lookup, so the join, the
///     effective-name rule, and the collision policy live ONLY here. References are consumed in
///     <c>UnsTagReferenceId</c>-ordinal order and effective-name collisions keep the FIRST — a valid draft
///     has none (the uniqueness validator rejects them), and the deterministic first-wins keeps the two
///     seams parity-stable on any input.</para>
/// </summary>
public static class EquipmentReferenceMap
{
    /// <summary>A flattened UNS tag reference: which equipment references which raw tag under an optional override.</summary>
    /// <param name="UnsTagReferenceId">Stable logical id — the ordering key for deterministic first-wins.</param>
    /// <param name="EquipmentId">The referencing equipment.</param>
    /// <param name="TagId">The backing raw tag.</param>
    /// <param name="DisplayNameOverride">Optional effective-name override; <see langword="null"/> = use the raw tag's Name.</param>
    public readonly record struct ReferenceRow(string UnsTagReferenceId, string EquipmentId, string TagId, string? DisplayNameOverride);

    /// <summary>The backing raw tag's identity inputs for its RawPath + effective name.</summary>
    /// <param name="Name">The raw tag's leaf name (also the default effective name).</param>
    /// <param name="DeviceId">The tag's owning device id.</param>
    /// <param name="TagGroupId">The tag's owning tag-group id, or <see langword="null"/> when directly under the device.</param>
    public readonly record struct TagRow(string Name, string DeviceId, string? TagGroupId);

    /// <summary>
    ///     Build the reference map. For each reference (ordered by <c>UnsTagReferenceId</c>), resolve the
    ///     backing tag's RawPath via <paramref name="resolver"/> and key it under the effective name within
    ///     the owning equipment. References whose backing tag is unknown or whose RawPath cannot be built
    ///     (broken chain) are skipped. Effective-name collisions within an equipment keep the first.
    /// </summary>
    /// <param name="references">The flattened UNS tag references (any order; sorted internally).</param>
    /// <param name="tagsById">Backing raw tags keyed by <c>TagId</c>.</param>
    /// <param name="resolver">The shared RawPath resolver over the raw topology.</param>
    /// <returns><c>equipmentId → (effectiveName → RawPath)</c>. Equipments with no resolvable reference are absent.</returns>
    public static IReadOnlyDictionary<string, IReadOnlyDictionary<string, string>> Build(
        IEnumerable<ReferenceRow> references,
        IReadOnlyDictionary<string, TagRow> tagsById,
        RawPathResolver resolver)
    {
        ArgumentNullException.ThrowIfNull(references);
        ArgumentNullException.ThrowIfNull(tagsById);
        ArgumentNullException.ThrowIfNull(resolver);

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

        foreach (var r in references.OrderBy(x => x.UnsTagReferenceId, StringComparer.Ordinal))
        {
            if (string.IsNullOrEmpty(r.EquipmentId) || string.IsNullOrEmpty(r.TagId)) continue;
            if (!tagsById.TryGetValue(r.TagId, out var tag)) continue;
            var effectiveName = string.IsNullOrEmpty(r.DisplayNameOverride) ? tag.Name : r.DisplayNameOverride!;
            if (string.IsNullOrEmpty(effectiveName)) continue;
            var rawPath = resolver.TryBuildTagPath(tag.DeviceId, tag.TagGroupId, tag.Name);
            if (rawPath is null) continue; // broken chain — dropped (deploy gate rejects invalid names)

            if (!byEquip.TryGetValue(r.EquipmentId, out var map))
                byEquip[r.EquipmentId] = map = new Dictionary<string, string>(StringComparer.Ordinal);
            map.TryAdd(effectiveName, rawPath); // first-wins on collision (validator rejects real collisions)
        }

        return byEquip.ToDictionary(
            kv => kv.Key,
            kv => (IReadOnlyDictionary<string, string>)kv.Value,
            StringComparer.Ordinal);
    }

    /// <summary>The set of effective names a single equipment's references contribute (for the authoring
    /// gate + Monaco completion, which need only the names — not the RawPaths).</summary>
    /// <param name="references">The flattened UNS tag references for (typically) one equipment.</param>
    /// <param name="tagNameById">Backing raw tag Name keyed by <c>TagId</c>.</param>
    /// <returns>The distinct effective names, ordinal.</returns>
    public static IReadOnlySet<string> EffectiveNames(
        IEnumerable<ReferenceRow> references,
        IReadOnlyDictionary<string, string> tagNameById)
    {
        ArgumentNullException.ThrowIfNull(references);
        ArgumentNullException.ThrowIfNull(tagNameById);
        var names = new HashSet<string>(StringComparer.Ordinal);
        foreach (var r in references)
        {
            var effectiveName = r.DisplayNameOverride
                ?? (tagNameById.TryGetValue(r.TagId, out var n) ? n : null);
            if (!string.IsNullOrEmpty(effectiveName)) names.Add(effectiveName);
        }
        return names;
    }
}
