using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using ZB.MOM.WW.MxGateway.Client;
using ZB.MOM.WW.MxGateway.Contracts.Proto;
// Use the generated nested status enum for the SetBufferedUpdateInterval reply check.

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

/// <summary>
///     Production <see cref="IGalaxySubscriber"/> over a connected
///     <see cref="GalaxyMxSession"/>. Forwards SubscribeBulk / UnsubscribeBulk to the
///     gateway and streams MxEvents via the gw's bidirectional events RPC.
/// </summary>
/// <remarks>
///     PR 6.3 wired the per-call <c>buffered_update_interval_ms</c> through
///     <see cref="SubscribeBulkAsync"/>. The gw's contract is session-level
///     (<c>SetBufferedUpdateInterval</c> applies to all buffered subscriptions on the
///     server handle), so we cache the last-applied value and skip redundant calls.
/// </remarks>
public sealed class GatewayGalaxySubscriber : IGalaxySubscriber
{
    private readonly GalaxyMxSession _session;
    private readonly ILogger _logger;
    private readonly Lock _intervalLock = new();
    private int _lastAppliedIntervalMs = -1; // -1 = never applied; 0 = explicit "use gw default"

    /// <summary>Initializes a new instance of GatewayGalaxySubscriber.</summary>
    /// <param name="session">The Galaxy MX session to use for subscription operations.</param>
    /// <param name="logger">
    ///     Optional logger; surfaces a warning when <c>SetBufferedUpdateInterval</c>
    ///     soft-fails so the cadence-not-applied condition isn't silent. Null is allowed
    ///     for unit-test construction and falls back to <see cref="NullLogger.Instance"/>.
    /// </param>
    public GatewayGalaxySubscriber(GalaxyMxSession session, ILogger? logger = null)
    {
        _session = session ?? throw new ArgumentNullException(nameof(session));
        _logger = logger ?? NullLogger.Instance;
    }

    /// <inheritdoc />
    public async Task<IReadOnlyList<SubscribeResult>> SubscribeBulkAsync(
        IReadOnlyList<string> fullReferences, int bufferedUpdateIntervalMs, CancellationToken cancellationToken)
    {
        var session = _session.Session
            ?? throw new InvalidOperationException(
                "GalaxyMxSession is not connected. Call ConnectAsync before subscribing.");
        var serverHandle = _session.ServerHandle;

        // The gw's SubscribeBulk RPC doesn't carry a per-call interval — buffered cadence
        // is session-level, set via SetBufferedUpdateInterval. Apply it before the
        // SubscribeBulk so the very first events on the new handles publish at the
        // requested cadence. Skip when the last-applied value already matches.
        if (bufferedUpdateIntervalMs > 0)
        {
            await EnsureSessionIntervalAsync(session, serverHandle, bufferedUpdateIntervalMs, cancellationToken)
                .ConfigureAwait(false);
        }

        return await session.SubscribeBulkAsync(serverHandle, fullReferences, cancellationToken)
            .ConfigureAwait(false);
    }

    /// <summary>
    ///     Apply the gateway's session-level <c>SetBufferedUpdateInterval</c> command. The
    ///     gw's contract is "for this server handle, every buffered subscription publishes
    ///     at this cadence" — there's no per-handle granularity, so we cache the last
    ///     applied value and skip redundant calls.
    /// </summary>
    private async Task EnsureSessionIntervalAsync(
        ZB.MOM.WW.MxGateway.Client.MxGatewaySession session, int serverHandle, int intervalMs, CancellationToken cancellationToken)
    {
        lock (_intervalLock)
        {
            if (_lastAppliedIntervalMs == intervalMs) return;
        }

        var reply = await session.InvokeAsync(
            new MxCommandRequest
            {
                SessionId = session.SessionId,
                ClientCorrelationId = Guid.NewGuid().ToString("N"),
                Command = new MxCommand
                {
                    Kind = MxCommandKind.SetBufferedUpdateInterval,
                    SetBufferedUpdateInterval = new SetBufferedUpdateIntervalCommand
                    {
                        ServerHandle = serverHandle,
                        UpdateIntervalMilliseconds = intervalMs,
                    },
                },
            },
            cancellationToken).ConfigureAwait(false);

        var code = reply.ProtocolStatus?.Code;

        // MxaccessFailure means the COM-side SetBufferedUpdateInterval did NOT apply, so
        // we must NOT cache the requested value — caching it would suppress the retry on
        // the next subscribe at this interval. Only Ok records the value as applied.
        if (!ClassifyIntervalReply(code))
        {
            // Don't throw on a soft failure — the SubscribeBulk will still succeed at the
            // gw's default cadence, which is functional just not the requested cadence.
            // The trace span (PR 6.1) plus this warning gives ops the signal, and leaving
            // _lastAppliedIntervalMs unchanged lets the next subscribe re-attempt the set.
            if (code == ProtocolStatusCode.MxaccessFailure)
            {
                _logger.LogWarning(
                    "Galaxy SetBufferedUpdateInterval({IntervalMs}ms) soft-failed (MxaccessFailure); " +
                    "buffered subscriptions on server handle {ServerHandle} will publish at the gateway's " +
                    "default cadence. The requested interval was not cached, so a later subscribe will retry it.",
                    intervalMs, serverHandle);
            }
            else
            {
                _logger.LogWarning(
                    "Galaxy SetBufferedUpdateInterval({IntervalMs}ms) returned an unexpected protocol status " +
                    "{Code} on server handle {ServerHandle}; treating it as not-applied and leaving the " +
                    "requested interval uncached so a later subscribe retries it.",
                    intervalMs, code, serverHandle);
            }
            return;
        }

        lock (_intervalLock)
        {
            _lastAppliedIntervalMs = intervalMs;
        }
    }

    /// <summary>
    ///     Classifies a <c>SetBufferedUpdateInterval</c> reply: returns <c>true</c> only when
    ///     the requested interval was actually applied and may be cached as last-applied.
    ///     This is <see cref="ProtocolStatusCode.Ok"/> alone — <see cref="ProtocolStatusCode.MxaccessFailure"/>
    ///     means the COM-side set did NOT take effect (so caching it would prevent a retry),
    ///     and a <c>null</c> or any other unexpected code is treated as not-applied.
    /// </summary>
    /// <param name="code">The protocol status code from the gateway reply, or <c>null</c> when absent.</param>
    /// <returns><c>true</c> if the interval should be recorded as applied; otherwise <c>false</c>.</returns>
    internal static bool ClassifyIntervalReply(ProtocolStatusCode? code) => code == ProtocolStatusCode.Ok;

    /// <inheritdoc />
    public async Task UnsubscribeBulkAsync(IReadOnlyList<int> itemHandles, CancellationToken cancellationToken)
    {
        if (itemHandles.Count == 0) return;

        var session = _session.Session
            ?? throw new InvalidOperationException(
                "GalaxyMxSession is not connected. UnsubscribeBulk called after disconnect.");
        var serverHandle = _session.ServerHandle;

        await session.UnsubscribeBulkAsync(serverHandle, itemHandles, cancellationToken)
            .ConfigureAwait(false);
    }

    /// <inheritdoc />
    public IAsyncEnumerable<MxEvent> StreamEventsAsync(CancellationToken cancellationToken)
    {
        var session = _session.Session
            ?? throw new InvalidOperationException(
                "GalaxyMxSession is not connected. StreamEventsAsync called before ConnectAsync.");
        return session.StreamEventsAsync(afterWorkerSequence: 0, cancellationToken);
    }
}
