using System.Collections.Concurrent;
using ZB.MOM.WW.MxGateway.Client;
using ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy;
using ZB.MOM.WW.OtOpcUa.Commons.Browsing;

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

/// <summary>
///     Lazy Galaxy browse over <see cref="GalaxyRepositoryClient.BrowseAsync"/>.
///     <see cref="RootAsync"/> returns the top-level <see cref="LazyBrowseNode"/>s
///     directly from the gateway; <see cref="ExpandAsync"/> fetches the direct children
///     of a previously-handed-out node via <see cref="LazyBrowseNode.ExpandAsync"/>
///     (one wire call per click, paginated internally by the client). Attribute fetches
///     are per-object via <c>DiscoverHierarchyAsync(MaxDepth=0, IncludeAttributes=true)</c>.
///     Owns the supplied <see cref="GalaxyRepositoryClient"/> and disposes it best-effort.
/// </summary>
internal sealed class GalaxyBrowseSession : IBrowseSession
{
    private readonly GalaxyRepositoryClient _client;
    private readonly ConcurrentDictionary<string, LazyBrowseNode> _byTagName = new(StringComparer.Ordinal);
    private readonly SemaphoreSlim _rootGate = new(1, 1);
    private volatile bool _disposed;
    private IReadOnlyList<LazyBrowseNode>? _roots;

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

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

    /// <summary>
    ///     Initializes a new session wrapping a connected repository client. The factory
    ///     in <c>GalaxyDriverBrowser</c> constructs the client via
    ///     <see cref="GalaxyRepositoryClient.Create"/> and hands it off here for the
    ///     session's lifetime.
    /// </summary>
    /// <param name="client">Galaxy repository client to query for browse and attributes.</param>
    internal GalaxyBrowseSession(GalaxyRepositoryClient client)
    {
        _client = client ?? throw new ArgumentNullException(nameof(client));
    }

    /// <inheritdoc />
    public async Task<IReadOnlyList<BrowseNode>> RootAsync(CancellationToken cancellationToken)
    {
        ObjectDisposedException.ThrowIf(_disposed, this);
        await _rootGate.WaitAsync(cancellationToken).ConfigureAwait(false);
        try
        {
            _roots ??= await _client.BrowseAsync(new BrowseChildrenOptions(), cancellationToken)
                .ConfigureAwait(false);

            LastUsedUtc = DateTime.UtcNow;
            return Project(_roots);
        }
        finally
        {
            _rootGate.Release();
        }
    }

    /// <inheritdoc />
    public async Task<IReadOnlyList<BrowseNode>> ExpandAsync(string nodeId, CancellationToken cancellationToken)
    {
        ObjectDisposedException.ThrowIf(_disposed, this);
        if (!_byTagName.TryGetValue(nodeId, out var node))
        {
            throw new ArgumentException(
                $"Galaxy object '{nodeId}' is not in the current browse-session cache. " +
                "Re-open the browser or expand its parent first.", nameof(nodeId));
        }

        await node.ExpandAsync(cancellationToken).ConfigureAwait(false);
        LastUsedUtc = DateTime.UtcNow;
        return Project(node.Children);
    }

    /// <inheritdoc />
    public async Task<IReadOnlyList<AttributeInfo>> AttributesAsync(string nodeId, CancellationToken cancellationToken)
    {
        ObjectDisposedException.ThrowIf(_disposed, this);
        var rows = await _client.DiscoverHierarchyAsync(
            new DiscoverHierarchyOptions
            {
                RootTagName = nodeId,
                MaxDepth = 0,
                IncludeAttributes = true,
            }, cancellationToken).ConfigureAwait(false);

        LastUsedUtc = DateTime.UtcNow;
        var obj = rows.FirstOrDefault();
        if (obj is null) return Array.Empty<AttributeInfo>();

        var result = new List<AttributeInfo>(obj.Attributes.Count);
        foreach (var attr in obj.Attributes)
        {
            var driverType = !string.IsNullOrEmpty(attr.DataTypeName)
                ? attr.DataTypeName
                : attr.MxDataType.ToString(System.Globalization.CultureInfo.InvariantCulture);
            result.Add(new AttributeInfo(
                Name: attr.AttributeName,
                DriverDataType: driverType,
                IsArray: attr.IsArray,
                SecurityClass: MapSecurityClass(attr.SecurityClassification),
                IsAlarm: attr.IsAlarm));
        }
        return result;
    }

    /// <summary>
    ///     Projects <see cref="LazyBrowseNode"/>s to <see cref="BrowseNode"/>s, caching
    ///     each by <c>TagName</c> so a subsequent <see cref="ExpandAsync"/> can locate
    ///     it. Galaxy nodes are always <see cref="BrowseNodeKind.Folder"/> — leaves only
    ///     appear in the attribute side-panel.
    /// </summary>
    private IReadOnlyList<BrowseNode> Project(IReadOnlyList<LazyBrowseNode> nodes)
    {
        var result = new List<BrowseNode>(nodes.Count);
        foreach (var n in nodes)
        {
            _byTagName[n.Object.TagName] = n;
            var displayName = !string.IsNullOrEmpty(n.Object.ContainedName)
                ? n.Object.ContainedName
                : n.Object.TagName;
            result.Add(new BrowseNode(
                NodeId: n.Object.TagName,
                DisplayName: displayName,
                Kind: BrowseNodeKind.Folder,
                HasChildrenHint: n.HasChildrenHint));
        }
        return result;
    }

    /// <summary>
    ///     Maps the Galaxy raw security-classification integer to a display string.
    ///     Matches the <c>SecurityClassification</c> enum ordinals and the runtime
    ///     <c>SecurityMap</c> in Driver.Galaxy:
    ///     0=FreeAccess, 1=Operate, 2=SecuredWrite, 3=VerifiedWrite,
    ///     4=Tune, 5=Configure, 6=ViewOnly; anything else surfaces as <c>Unknown(N)</c>.
    /// </summary>
    /// <param name="raw">The raw Galaxy security-classification integer.</param>
    /// <returns>The display string for the classification, or <c>Unknown(N)</c> if unrecognized.</returns>
    // internal for unit-test access (InternalsVisibleTo on the src project).
    internal static string MapSecurityClass(int raw) => raw switch
    {
        0 => "FreeAccess",
        1 => "Operate",
        2 => "SecuredWrite",
        3 => "VerifiedWrite",
        4 => "Tune",
        5 => "Configure",
        6 => "ViewOnly",
        _ => $"Unknown({raw})",
    };

    /// <summary>
    ///     Idempotently tears down the underlying repository client. Swallows exceptions
    ///     on shutdown — the registry's reaper may be racing a client-initiated close.
    ///     Both <see cref="SemaphoreSlim.Dispose"/> and the client dispose are wrapped
    ///     in try/catch so a concurrent second dispose call never propagates.
    /// </summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    public async ValueTask DisposeAsync()
    {
        if (_disposed) return;
        _disposed = true;
        try
        {
            _rootGate.Dispose();
        }
        catch (ObjectDisposedException)
        {
            // Concurrent DisposeAsync caller already disposed the gate — safe to ignore.
        }
        try
        {
            await _client.DisposeAsync().ConfigureAwait(false);
        }
        catch
        {
            // Best-effort: a gateway-side close that hits a torn-down channel is normal.
        }
    }
}
