using System.Collections.Concurrent;
using Opc.Ua;
using Opc.Ua.Server;
using ZB.MOM.WW.OtOpcUa.Commons.OpcUa;

namespace ZB.MOM.WW.OtOpcUa.OpcUaServer;

/// <summary>
///     Custom OPC UA <see cref="CustomNodeManager2"/> that owns the writable address space for
///     the OtOpcUa server. Variable nodes are created lazily on first <see cref="WriteValue"/>
///     under the manager's namespace; subsequent writes update the existing node's Value +
///     StatusCode + SourceTimestamp and notify subscribed clients via the standard
///     <c>ClearChangeMasks</c> path.
///
///     This is the F10b production wiring behind the v2 <see cref="IOpcUaAddressSpaceSink"/>
///     seam — once a <see cref="SdkAddressSpaceSink"/> is bound, OpcUaPublishActor's writes
///     materialise as real OPC UA Variable updates that clients can browse + subscribe to.
///
///     Node-id encoding uses the manager's default namespace + the caller-supplied string id
///     as the identifier portion (e.g. <c>"ns=2;s=eq-1/temp"</c>). Equipment-folder hierarchy
///     and OPC UA type metadata still come from the Phase7Applier / EquipmentNodeWalker
///     integration (F14b, tracked under #85) — this manager treats every id as a flat
///     <see cref="BaseDataVariableState"/> under the namespace root.
/// </summary>
public sealed class OtOpcUaNodeManager : CustomNodeManager2
{
    public const string DefaultNamespaceUri = "https://zb.com/otopcua/ns";

    private readonly ConcurrentDictionary<string, BaseDataVariableState> _variables = new(StringComparer.Ordinal);
    private readonly ConcurrentDictionary<string, FolderState> _folders = new(StringComparer.Ordinal);
    private FolderState? _root;

    /// <summary>Initializes a new instance of the <see cref="OtOpcUaNodeManager"/> class with the OPC UA server and configuration.</summary>
    /// <param name="server">The OPC UA server instance.</param>
    /// <param name="configuration">The application configuration.</param>
    public OtOpcUaNodeManager(IServerInternal server, ApplicationConfiguration configuration)
        : base(server, configuration, DefaultNamespaceUri)
    {
        // SystemContext is initialised by the base ctor.
    }

    /// <summary>Gets the count of variable nodes currently managed.</summary>
    public int VariableCount => _variables.Count;
    /// <summary>Gets the count of folder nodes currently managed.</summary>
    public int FolderCount => _folders.Count;

    /// <summary>
    ///     Apply a value write from <see cref="IOpcUaAddressSpaceSink.WriteValue"/>. Creates the
    ///     variable node on first call; subsequent calls update Value + StatusCode +
    ///     SourceTimestamp and call <c>ClearChangeMasks</c> so subscribed clients see the change.
    /// </summary>
    /// <param name="nodeId">The node identifier of the variable.</param>
    /// <param name="value">The new value to write.</param>
    /// <param name="quality">The OPC UA quality status code.</param>
    /// <param name="sourceTimestampUtc">The timestamp of the value in UTC.</param>
    public void WriteValue(string nodeId, object? value, OpcUaQuality quality, DateTime sourceTimestampUtc)
    {
        ArgumentException.ThrowIfNullOrEmpty(nodeId);
        var variable = _variables.GetOrAdd(nodeId, CreateVariable);

        lock (Lock)
        {
            variable.Value = value;
            variable.StatusCode = StatusFromQuality(quality);
            variable.Timestamp = sourceTimestampUtc;
            variable.ClearChangeMasks(SystemContext, includeChildren: false);
        }
    }

    /// <summary>Apply an alarm-state write. Surfaced as a two-element Variable carrying
    /// <c>[active, acknowledged]</c> — proper <c>AlarmConditionState</c> + event firing
    /// comes when the F14b walker integration lands and registers real condition nodes.</summary>
    /// <param name="alarmNodeId">The node identifier of the alarm variable.</param>
    /// <param name="active">Whether the alarm is currently active.</param>
    /// <param name="acknowledged">Whether the alarm has been acknowledged.</param>
    /// <param name="sourceTimestampUtc">The timestamp of the alarm state change in UTC.</param>
    public void WriteAlarmState(string alarmNodeId, bool active, bool acknowledged, DateTime sourceTimestampUtc)
    {
        ArgumentException.ThrowIfNullOrEmpty(alarmNodeId);
        var variable = _variables.GetOrAdd(alarmNodeId, CreateVariable);

        lock (Lock)
        {
            variable.Value = new[] { active, acknowledged };
            variable.StatusCode = StatusCodes.Good;
            variable.Timestamp = sourceTimestampUtc;
            variable.ClearChangeMasks(SystemContext, includeChildren: false);
        }
    }

    /// <summary>
    ///     Ensure a folder node exists at <paramref name="folderNodeId"/> with the given display
    ///     name, parented under <paramref name="parentNodeId"/> (or the namespace root when null).
    ///     #85 — used by <see cref="Phase7Applier"/> to materialise the UNS Area/Line/Equipment
    ///     folder hierarchy. Idempotent: the second call with the same id returns the cached
    ///     folder so adding child variables under it still works.
    /// </summary>
    /// <param name="folderNodeId">The node identifier of the folder.</param>
    /// <param name="parentNodeId">The node identifier of the parent folder; null to use the namespace root.</param>
    /// <param name="displayName">The display name of the folder.</param>
    public void EnsureFolder(string folderNodeId, string? parentNodeId, string displayName)
    {
        ArgumentException.ThrowIfNullOrEmpty(folderNodeId);
        ArgumentException.ThrowIfNullOrEmpty(displayName);

        if (_folders.ContainsKey(folderNodeId)) return;

        lock (Lock)
        {
            if (_folders.ContainsKey(folderNodeId)) return;

            var parent = ResolveParentFolder(parentNodeId);
            var folder = new FolderState(parent)
            {
                NodeId = new NodeId(folderNodeId, NamespaceIndex),
                BrowseName = new QualifiedName(folderNodeId, NamespaceIndex),
                DisplayName = displayName,
                EventNotifier = EventNotifiers.None,
                TypeDefinitionId = ObjectTypeIds.FolderType,
                ReferenceTypeId = ReferenceTypeIds.Organizes,
            };
            parent.AddChild(folder);
            AddPredefinedNode(SystemContext, folder);
            _folders[folderNodeId] = folder;
        }
    }

    /// <summary>
    ///     Ensure a Variable node exists at <paramref name="variableNodeId"/> parented under
    ///     <paramref name="parentFolderNodeId"/> (or root when null). Initial value=null, quality=Bad,
    ///     timestamp=epoch — <see cref="WriteValue"/> fills these in once driver data flows.
    ///     Idempotent. Materialises Galaxy / SystemPlatform tags so they're browseable before the
    ///     Galaxy driver issues SubscribeBulk.
    /// </summary>
    /// <param name="variableNodeId">The node identifier of the variable.</param>
    /// <param name="parentFolderNodeId">The node identifier of the parent folder; null to use the namespace root.</param>
    /// <param name="displayName">The display name of the variable.</param>
    /// <param name="dataType">The OPC UA data type name (e.g., "Boolean", "Int32", "String").</param>
    public void EnsureVariable(string variableNodeId, string? parentFolderNodeId, string displayName, string dataType)
    {
        ArgumentException.ThrowIfNullOrEmpty(variableNodeId);
        ArgumentException.ThrowIfNullOrEmpty(displayName);

        // If already present, leave it alone (idempotent re-applies).
        if (_variables.ContainsKey(variableNodeId)) return;

        lock (Lock)
        {
            if (_variables.ContainsKey(variableNodeId)) return;

            var parent = ResolveParentFolder(parentFolderNodeId);
            var variable = new BaseDataVariableState(parent)
            {
                NodeId = new NodeId(variableNodeId, NamespaceIndex),
                BrowseName = new QualifiedName(variableNodeId, NamespaceIndex),
                DisplayName = displayName,
                TypeDefinitionId = VariableTypeIds.BaseDataVariableType,
                ReferenceTypeId = ReferenceTypeIds.Organizes,
                DataType = ResolveBuiltInDataType(dataType),
                ValueRank = ValueRanks.Scalar,
                AccessLevel = AccessLevels.CurrentRead,
                UserAccessLevel = AccessLevels.CurrentRead,
                Historizing = false,
                Value = null,
                StatusCode = StatusCodes.BadWaitingForInitialData,
                Timestamp = DateTime.MinValue,
            };
            parent.AddChild(variable);
            AddPredefinedNode(SystemContext, variable);
            _variables[variableNodeId] = variable;
        }
    }

    /// <summary>Map a Tag.DataType string ("Boolean", "Int32", "Float", "Double", "String",
    /// "DateTime") to the OPC UA built-in NodeId. Unknown names fall back to BaseDataType
    /// (matches CreateVariable's default for lazy-created nodes).</summary>
    private static NodeId ResolveBuiltInDataType(string dataType) => dataType switch
    {
        "Boolean" => DataTypeIds.Boolean,
        "SByte" => DataTypeIds.SByte,
        "Byte" => DataTypeIds.Byte,
        "Int16" => DataTypeIds.Int16,
        "UInt16" => DataTypeIds.UInt16,
        "Int32" => DataTypeIds.Int32,
        "UInt32" => DataTypeIds.UInt32,
        "Int64" => DataTypeIds.Int64,
        "UInt64" => DataTypeIds.UInt64,
        "Float" => DataTypeIds.Float,
        "Double" => DataTypeIds.Double,
        "String" => DataTypeIds.String,
        "DateTime" => DataTypeIds.DateTime,
        _ => DataTypeIds.BaseDataType,
    };

    /// <summary>Clear every registered variable + folder from the address space. Phase7Applier
    /// calls this when Equipment/Alarm topology changes; the populator then re-adds via
    /// EnsureFolder + WriteValue on the next pass.</summary>
    public void RebuildAddressSpace()
    {
        lock (Lock)
        {
            foreach (var v in _variables.Values)
            {
                v.Parent?.RemoveChild(v);
                PredefinedNodes?.Remove(v.NodeId);
            }
            _variables.Clear();

            foreach (var f in _folders.Values)
            {
                f.Parent?.RemoveChild(f);
                PredefinedNodes?.Remove(f.NodeId);
            }
            _folders.Clear();
        }
    }

    private FolderState ResolveParentFolder(string? parentNodeId)
    {
        if (string.IsNullOrEmpty(parentNodeId)) return _root!;
        return _folders.TryGetValue(parentNodeId, out var existing) ? existing : _root!;
    }

    /// <inheritdoc />
    public override void CreateAddressSpace(IDictionary<NodeId, IList<IReference>> externalReferences)
    {
        lock (Lock)
        {
            base.CreateAddressSpace(externalReferences);

            // Create one root folder under Objects/ for every variable we mint to hang under.
            _root = new FolderState(null)
            {
                NodeId = new NodeId("OtOpcUa", NamespaceIndex),
                BrowseName = new QualifiedName("OtOpcUa", NamespaceIndex),
                DisplayName = "OtOpcUa",
                EventNotifier = EventNotifiers.None,
                TypeDefinitionId = ObjectTypeIds.FolderType,
            };
            _root.AddReference(ReferenceTypeIds.Organizes, isInverse: true, ObjectIds.ObjectsFolder);

            if (!externalReferences.TryGetValue(ObjectIds.ObjectsFolder, out var refs))
            {
                refs = new List<IReference>();
                externalReferences[ObjectIds.ObjectsFolder] = refs;
            }
            refs.Add(new NodeStateReference(ReferenceTypeIds.Organizes, isInverse: false, _root.NodeId));

            AddPredefinedNode(SystemContext, _root);
        }
    }

    private BaseDataVariableState CreateVariable(string nodeId)
    {
        var v = new BaseDataVariableState(_root)
        {
            NodeId = new NodeId(nodeId, NamespaceIndex),
            BrowseName = new QualifiedName(nodeId, NamespaceIndex),
            DisplayName = nodeId,
            TypeDefinitionId = VariableTypeIds.BaseDataVariableType,
            ReferenceTypeId = ReferenceTypeIds.Organizes,
            DataType = DataTypeIds.BaseDataType,
            ValueRank = ValueRanks.Scalar,
            AccessLevel = AccessLevels.CurrentRead,
            UserAccessLevel = AccessLevels.CurrentRead,
            Historizing = false,
        };
        _root?.AddChild(v);
        AddPredefinedNode(SystemContext, v);
        return v;
    }

    private static StatusCode StatusFromQuality(OpcUaQuality quality) => quality switch
    {
        OpcUaQuality.Good => StatusCodes.Good,
        OpcUaQuality.Uncertain => StatusCodes.Uncertain,
        _ => StatusCodes.Bad,
    };
}
