using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;

namespace ZB.MOM.WW.OtOpcUa.Core.OpcUa;

/// <summary>
///     Materializes the canonical Unified Namespace browse tree (Area → Line → Equipment) for a
///     cluster from its <c>UnsArea</c> / <c>UnsLine</c> / <c>Equipment</c> rows, plus the
///     per-equipment VirtualTag + ScriptedAlarm nodes.
///     <para>
///     <b>v3:</b> equipment no longer binds a driver or authors tags. Raw tags are surfaced into
///     equipment by <c>UnsTagReference</c> and materialized as the UNS-namespace fan-out of their
///     backing raw nodes — that projection lands with the dual-namespace address space (Batch 4),
///     NOT here. This walker therefore emits the equipment hierarchy + VirtualTags + ScriptedAlarms
///     only; raw-tag reference variables are intentionally absent (the address space is deliberately
///     dark until Batch 4). It remains a pure, SDK-free helper retained for unit-test support; real
///     server deployments compose through the <c>AddressSpaceComposer → Applier → Sink → NodeManager</c>
///     chain.
///     </para>
/// </summary>
public static class EquipmentNodeWalker
{
    /// <summary>
    ///     Walk <paramref name="content"/> into <paramref name="namespaceBuilder"/>: Area → Line →
    ///     Equipment folders, identifier properties + the OPC 40010 Identification sub-folder per
    ///     equipment, and the equipment's VirtualTag + ScriptedAlarm variable nodes.
    /// </summary>
    /// <param name="namespaceBuilder">The builder scoped to the UNS namespace root.</param>
    /// <param name="content">Pre-loaded + pre-filtered rows for a single cluster.</param>
    public static void Walk(IAddressSpaceBuilder namespaceBuilder, EquipmentNamespaceContent content)
    {
        ArgumentNullException.ThrowIfNull(namespaceBuilder);
        ArgumentNullException.ThrowIfNull(content);

        var linesByArea = content.Lines
            .GroupBy(l => l.UnsAreaId, StringComparer.OrdinalIgnoreCase)
            .ToDictionary(g => g.Key, g => g.OrderBy(l => l.Name, StringComparer.Ordinal).ToList(), StringComparer.OrdinalIgnoreCase);

        var equipmentByLine = content.Equipment
            .GroupBy(e => e.UnsLineId, StringComparer.OrdinalIgnoreCase)
            .ToDictionary(g => g.Key, g => g.OrderBy(e => e.Name, StringComparer.Ordinal).ToList(), StringComparer.OrdinalIgnoreCase);

        var virtualTagsByEquipment = (content.VirtualTags ?? [])
            .Where(v => v.Enabled)
            .GroupBy(v => v.EquipmentId, StringComparer.OrdinalIgnoreCase)
            .ToDictionary(g => g.Key, g => g.OrderBy(v => v.Name, StringComparer.Ordinal).ToList(), StringComparer.OrdinalIgnoreCase);

        var scriptedAlarmsByEquipment = (content.ScriptedAlarms ?? [])
            .Where(a => a.Enabled)
            .GroupBy(a => a.EquipmentId, StringComparer.OrdinalIgnoreCase)
            .ToDictionary(g => g.Key, g => g.OrderBy(a => a.Name, StringComparer.Ordinal).ToList(), StringComparer.OrdinalIgnoreCase);

        foreach (var area in content.Areas.OrderBy(a => a.Name, StringComparer.Ordinal))
        {
            var areaBuilder = namespaceBuilder.Folder(area.Name, area.Name);
            if (!linesByArea.TryGetValue(area.UnsAreaId, out var areaLines)) continue;

            foreach (var line in areaLines)
            {
                var lineBuilder = areaBuilder.Folder(line.Name, line.Name);
                if (!equipmentByLine.TryGetValue(line.UnsLineId, out var lineEquipment)) continue;

                foreach (var equipment in lineEquipment)
                {
                    var equipmentBuilder = lineBuilder.Folder(equipment.Name, equipment.Name);
                    AddIdentifierProperties(equipmentBuilder, equipment);
                    IdentificationFolderBuilder.Build(equipmentBuilder, equipment);

                    if (virtualTagsByEquipment.TryGetValue(equipment.EquipmentId, out var vTags))
                        foreach (var vtag in vTags)
                            AddVirtualTagVariable(equipmentBuilder, vtag);

                    if (scriptedAlarmsByEquipment.TryGetValue(equipment.EquipmentId, out var alarms))
                        foreach (var alarm in alarms)
                            AddScriptedAlarmVariable(equipmentBuilder, alarm);
                }
            }
        }
    }

    /// <summary>
    ///     Adds the operator-facing identifiers as OPC UA properties on the Equipment node.
    ///     EquipmentId + EquipmentUuid + MachineCode are always populated; ZTag + SAPID are skipped
    ///     when null to avoid empty-string noise in the browse tree.
    /// </summary>
    private static void AddIdentifierProperties(IAddressSpaceBuilder equipmentBuilder, Equipment equipment)
    {
        equipmentBuilder.AddProperty("EquipmentId", DriverDataType.String, equipment.EquipmentId);
        equipmentBuilder.AddProperty("EquipmentUuid", DriverDataType.String, equipment.EquipmentUuid.ToString());
        equipmentBuilder.AddProperty("MachineCode", DriverDataType.String, equipment.MachineCode);
        if (!string.IsNullOrEmpty(equipment.ZTag))
            equipmentBuilder.AddProperty("ZTag", DriverDataType.String, equipment.ZTag);
        if (!string.IsNullOrEmpty(equipment.SAPID))
            equipmentBuilder.AddProperty("SAPID", DriverDataType.String, equipment.SAPID);
    }

    /// <summary>
    ///     Parse a data-type name (stored as the <see cref="DriverDataType"/> enum name) into the
    ///     enum value; unknown names fall back to <see cref="DriverDataType.String"/>.
    /// </summary>
    private static DriverDataType ParseDriverDataType(string raw) =>
        Enum.TryParse<DriverDataType>(raw, ignoreCase: true, out var parsed) ? parsed : DriverDataType.String;

    /// <summary>Emit a <see cref="VirtualTag"/> row as a <see cref="NodeSourceKind.Virtual"/> variable node.</summary>
    private static void AddVirtualTagVariable(IAddressSpaceBuilder equipmentBuilder, VirtualTag vtag)
    {
        var attr = new DriverAttributeInfo(
            FullName: vtag.VirtualTagId,
            DriverDataType: ParseDriverDataType(vtag.DataType),
            IsArray: false,
            ArrayDim: null,
            SecurityClass: SecurityClassification.FreeAccess,
            IsHistorized: vtag.Historize,
            IsAlarm: false,
            WriteIdempotent: false,
            Source: NodeSourceKind.Virtual,
            VirtualTagId: vtag.VirtualTagId,
            ScriptedAlarmId: null);
        equipmentBuilder.Variable(vtag.Name, vtag.Name, attr);
    }

    /// <summary>Emit a <see cref="ScriptedAlarm"/> row as a <see cref="NodeSourceKind.ScriptedAlarm"/> variable node.</summary>
    private static void AddScriptedAlarmVariable(IAddressSpaceBuilder equipmentBuilder, ScriptedAlarm alarm)
    {
        var attr = new DriverAttributeInfo(
            FullName: alarm.ScriptedAlarmId,
            DriverDataType: DriverDataType.Boolean,
            IsArray: false,
            ArrayDim: null,
            SecurityClass: SecurityClassification.FreeAccess,
            IsHistorized: false,
            IsAlarm: true,
            WriteIdempotent: false,
            Source: NodeSourceKind.ScriptedAlarm,
            VirtualTagId: null,
            ScriptedAlarmId: alarm.ScriptedAlarmId);
        equipmentBuilder.Variable(alarm.Name, alarm.Name, attr);
    }
}

/// <summary>
///     Pre-loaded + pre-filtered snapshot of one cluster's UNS content (Area / Line / Equipment +
///     per-equipment VirtualTags / ScriptedAlarms). v3: raw-tag references (<c>UnsTagReference</c>)
///     are materialized by the Batch-4 dual-namespace fan-out, not by this walker, so they are not
///     part of this snapshot.
/// </summary>
public sealed record EquipmentNamespaceContent(
    IReadOnlyList<UnsArea> Areas,
    IReadOnlyList<UnsLine> Lines,
    IReadOnlyList<Equipment> Equipment,
    IReadOnlyList<VirtualTag>? VirtualTags = null,
    IReadOnlyList<ScriptedAlarm>? ScriptedAlarms = null);
