using Opc.Ua;
using Opc.Ua.Client;
using ZB.MOM.WW.OtOpcUa.Commons.Browsing;
using ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient;

namespace ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Browser;

/// <summary>
///     Live one-level-per-call browse session over a remote OPC UA server. Created by
///     <c>OpcUaClientBrowser</c> on picker open and owned by the AdminUI's
///     <c>BrowseSessionRegistry</c>; the registry's TTL reaper disposes idle sessions.
/// </summary>
internal sealed class OpcUaClientBrowseSession : IBrowseSession
{
    private readonly ISession _session;
    private readonly NamespaceMap _nsMap;
    private readonly NodeId _rootNodeId;
    private readonly SemaphoreSlim _gate = new(1, 1);
    private volatile bool _disposed;

    /// <summary>
    ///     Construct a browse session bound to an already-connected <paramref name="session"/>.
    /// </summary>
    /// <param name="session">The OPC UA client session to browse against.</param>
    /// <param name="nsMap">Namespace snapshot taken at connect time; used to render outbound
    ///     NodeIds in the server-stable <c>nsu=…</c> form.</param>
    /// <param name="rootNodeId">The node under which <see cref="RootAsync"/> browses one level.</param>
    internal OpcUaClientBrowseSession(ISession session, NamespaceMap nsMap, NodeId rootNodeId)
    {
        _session = session;
        _nsMap = nsMap;
        _rootNodeId = rootNodeId;
    }

    /// <inheritdoc />
    public Guid Token { get; } = Guid.NewGuid();

    /// <inheritdoc />
    public DateTime LastUsedUtc { get; private set; } = DateTime.UtcNow;

    /// <inheritdoc />
    public Task<IReadOnlyList<BrowseNode>> RootAsync(CancellationToken cancellationToken)
        => BrowseOneLevelAsync(_rootNodeId, cancellationToken);

    /// <inheritdoc />
    // Throws ArgumentException when nodeId cannot be resolved against the live session's
    // namespace table.
    public Task<IReadOnlyList<BrowseNode>> ExpandAsync(string nodeId, CancellationToken cancellationToken)
    {
        if (!NamespaceMap.TryResolve(_session, nodeId, out var resolved))
            throw new ArgumentException(
                $"Cannot resolve NodeId '{nodeId}' against the live session.", nameof(nodeId));
        return BrowseOneLevelAsync(resolved, cancellationToken);
    }

    /// <inheritdoc />
    public Task<IReadOnlyList<AttributeInfo>> AttributesAsync(string nodeId, CancellationToken cancellationToken)
    {
        LastUsedUtc = DateTime.UtcNow;
        return Task.FromResult<IReadOnlyList<AttributeInfo>>(Array.Empty<AttributeInfo>());
    }

    /// <summary>Issue a single-level Browse (plus continuation-point follow-ups) under the
    /// given parent node. <see cref="Session.BrowseAsync"/> is not thread-safe, so all calls
    /// serialize through <see cref="_gate"/>.</summary>
    private async Task<IReadOnlyList<BrowseNode>> BrowseOneLevelAsync(NodeId parent, CancellationToken ct)
    {
        ObjectDisposedException.ThrowIf(_disposed, this);
        await _gate.WaitAsync(ct).ConfigureAwait(false);
        try
        {
            var descriptions = new BrowseDescriptionCollection
            {
                new()
                {
                    NodeId = parent,
                    BrowseDirection = BrowseDirection.Forward,
                    ReferenceTypeId = ReferenceTypeIds.HierarchicalReferences,
                    IncludeSubtypes = true,
                    NodeClassMask = (uint)(NodeClass.Object | NodeClass.Variable),
                    ResultMask = (uint)(BrowseResultMask.BrowseName | BrowseResultMask.DisplayName
                                        | BrowseResultMask.NodeClass),
                },
            };

            var resp = await _session.BrowseAsync(
                requestHeader: null,
                view: null,
                requestedMaxReferencesPerNode: 0,
                nodesToBrowse: descriptions,
                ct: ct).ConfigureAwait(false);

            if (resp.Results.Count == 0)
            {
                LastUsedUtc = DateTime.UtcNow;
                return Array.Empty<BrowseNode>();
            }

            var result = resp.Results[0];
            var refs = result.References;

            // Follow browse continuation points so folders larger than the server's per-call
            // cap aren't silently truncated.
            var cp = result.ContinuationPoint;
            while (cp is { Length: > 0 })
            {
                var next = await _session.BrowseNextAsync(
                    requestHeader: null,
                    releaseContinuationPoints: false,
                    continuationPoints: [cp],
                    ct: ct).ConfigureAwait(false);

                if (next.Results.Count == 0) break;
                var nextResult = next.Results[0];
                if (nextResult.References is { Count: > 0 })
                    refs.AddRange(nextResult.References);
                cp = nextResult.ContinuationPoint;
            }

            LastUsedUtc = DateTime.UtcNow;

            var nodes = new List<BrowseNode>(refs.Count);
            foreach (var rf in refs)
                nodes.Add(ToBrowseNode(rf));
            return nodes;
        }
        finally
        {
            _gate.Release();
        }
    }

    /// <summary>Project a single <see cref="ReferenceDescription"/> into the driver-agnostic
    /// <see cref="BrowseNode"/> shape, encoding the outbound NodeId in the stable
    /// <c>nsu=…</c> form so the picker survives a remote namespace-table reorder.</summary>
    private BrowseNode ToBrowseNode(ReferenceDescription rf)
    {
        var childId = ExpandedNodeId.ToNodeId(rf.NodeId, _session.NamespaceUris);
        var isObject = rf.NodeClass == NodeClass.Object;
        var displayName = rf.DisplayName?.Text
                          ?? rf.BrowseName?.Name
                          ?? childId.ToString()
                          ?? "(unnamed)";
        return new BrowseNode(
            NodeId: _nsMap.ToStableReference(childId),
            DisplayName: displayName,
            Kind: isObject ? BrowseNodeKind.Folder : BrowseNodeKind.Leaf,
            HasChildrenHint: isObject);
    }

    /// <summary>Idempotent best-effort dispose: closes the underlying session if it's a
    /// concrete <see cref="Session"/>, disposes it, and disposes the gate. Close errors are
    /// swallowed because the registry reaper may be racing a remote disconnect.</summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    public async ValueTask DisposeAsync()
    {
        if (_disposed) return;
        _disposed = true;

        if (_session is Session s)
        {
            try { await s.CloseAsync().ConfigureAwait(false); }
            catch { /* best-effort */ }
        }

        try { _session.Dispose(); }
        catch { /* best-effort */ }

        try { _gate.Dispose(); }
        catch { /* best-effort */ }
    }
}
