namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip;

/// <summary>
///     Groups a ReadAsync batch of full-references into whole-UDT reads where
///     possible. A group is emitted for every parent UDT tag whose declared
///     <see cref="AbCipStructureMember"/>s produced a valid offset map AND at least two of
///     its members appear in the batch; every other reference stays in the per-tag fallback
///     list that <see cref="AbCipDriver.ReadAsync"/> runs through its existing read path.
///     Pure function — the planner never touches the runtime + never reads the PLC.
/// </summary>
/// <remarks>
///     The grouped offsets come from <see cref="AbCipUdtMemberLayout"/>, which assumes the
///     controller lays members out in declaration order. Studio 5000 does not guarantee that,
///     so grouping is gated behind <see cref="AbCipDriverOptions.EnableDeclarationOnlyUdtGrouping"/>:
///     when grouping is disabled every member falls back to its own per-tag read.
/// </remarks>
public static class AbCipUdtReadPlanner
{
    /// <summary>
    ///     Split <paramref name="requests"/> into whole-UDT groups + per-tag leftovers.
    ///     <paramref name="tagsByName"/> is the driver's <c>_tagsByRawPath</c> map (RawPath → def) —
    ///     both parent UDT rows and their fanned-out member rows live there. Tag lookup honours the
    ///     passed dictionary's comparer (v3: case-sensitive Ordinal RawPaths); the internal
    ///     parent-name grouping is OrdinalIgnoreCase. When
    ///     <paramref name="enableDeclarationOnlyGrouping"/> is <c>false</c> no groups are
    ///     formed — every reference goes to the per-tag fallback path so member decoding never
    ///     relies on declaration-order offsets that may not match the controller layout.
    /// </summary>
    /// <param name="requests">The list of tag references to read.</param>
    /// <param name="tagsByName">Dictionary mapping tag names to their definitions.</param>
    /// <param name="enableDeclarationOnlyGrouping">Whether to enable UDT member grouping based on declaration order.</param>
    /// <returns>The resulting plan, containing whole-UDT read groups and per-tag fallback reads.</returns>
    public static AbCipUdtReadPlan Build(
        IReadOnlyList<string> requests,
        IReadOnlyDictionary<string, AbCipTagDefinition> tagsByName,
        bool enableDeclarationOnlyGrouping = false)
    {
        ArgumentNullException.ThrowIfNull(requests);
        ArgumentNullException.ThrowIfNull(tagsByName);

        var fallback = new List<AbCipUdtReadFallback>(requests.Count);
        var byParent = new Dictionary<string, List<AbCipUdtReadMember>>(StringComparer.OrdinalIgnoreCase);

        if (!enableDeclarationOnlyGrouping)
        {
            for (var i = 0; i < requests.Count; i++)
                fallback.Add(new AbCipUdtReadFallback(i, requests[i]));
            return new AbCipUdtReadPlan([], fallback);
        }

        for (var i = 0; i < requests.Count; i++)
        {
            var name = requests[i];
            if (!tagsByName.TryGetValue(name, out var def))
            {
                fallback.Add(new AbCipUdtReadFallback(i, name));
                continue;
            }

            var (parentName, memberName) = SplitParentMember(name);
            if (parentName is null || memberName is null
                || !tagsByName.TryGetValue(parentName, out var parent)
                || parent.DataType != AbCipDataType.Structure
                || parent.Members is not { Count: > 0 })
            {
                fallback.Add(new AbCipUdtReadFallback(i, name));
                continue;
            }

            var offsets = AbCipUdtMemberLayout.TryBuild(parent.Members);
            if (offsets is null || !offsets.TryGetValue(memberName, out var offset))
            {
                fallback.Add(new AbCipUdtReadFallback(i, name));
                continue;
            }

            if (!byParent.TryGetValue(parentName, out var members))
            {
                members = new List<AbCipUdtReadMember>();
                byParent[parentName] = members;
            }
            members.Add(new AbCipUdtReadMember(i, def, offset));
        }

        // A single-member group saves nothing (one whole-UDT read replaces one per-member read)
        // — demote to fallback to avoid paying the cost of reading the full UDT buffer only to
        // pull one field out.
        var groups = new List<AbCipUdtReadGroup>(byParent.Count);
        foreach (var (parentName, members) in byParent)
        {
            if (members.Count < 2)
            {
                foreach (var m in members)
                    fallback.Add(new AbCipUdtReadFallback(m.OriginalIndex, m.Definition.Name));
                continue;
            }
            groups.Add(new AbCipUdtReadGroup(parentName, tagsByName[parentName], members));
        }

        return new AbCipUdtReadPlan(groups, fallback);
    }

    private static (string? Parent, string? Member) SplitParentMember(string reference)
    {
        var dot = reference.IndexOf('.');
        if (dot <= 0 || dot == reference.Length - 1) return (null, null);
        return (reference[..dot], reference[(dot + 1)..]);
    }
}

/// <summary>A planner output: grouped UDT reads + per-tag fallbacks.</summary>
public sealed record AbCipUdtReadPlan(
    IReadOnlyList<AbCipUdtReadGroup> Groups,
    IReadOnlyList<AbCipUdtReadFallback> Fallbacks);

/// <summary>One UDT parent whose members were batched into a single read.</summary>
public sealed record AbCipUdtReadGroup(
    string ParentName,
    AbCipTagDefinition ParentDefinition,
    IReadOnlyList<AbCipUdtReadMember> Members);

/// <summary>
///     One member inside an <see cref="AbCipUdtReadGroup"/>. <c>OriginalIndex</c> is the
///     slot in the caller's request list so the decoded value lands at the correct output
///     offset. <c>Definition</c> is the fanned-out member-level tag definition. <c>Offset</c>
///     is the byte offset within the parent UDT buffer where this member lives.
/// </summary>
public sealed record AbCipUdtReadMember(int OriginalIndex, AbCipTagDefinition Definition, int Offset);

/// <summary>A reference that falls back to the per-tag read path.</summary>
public sealed record AbCipUdtReadFallback(int OriginalIndex, string Reference);
