using System.Collections.Concurrent;
using System.Diagnostics.Metrics;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using ZB.MOM.WW.MxGateway.Client;
using ZB.MOM.WW.MxGateway.Contracts.Proto;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;

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

/// <summary>
///     Production <see cref="IGalaxyDataWriter"/> over <see cref="GalaxyMxSession"/>.
///     For each batch entry: lazy-AddItem to obtain the MXAccess item handle, encode
///     the value via <see cref="MxValueEncoder"/>, route through Write or WriteSecured
///     based on the per-tag <see cref="SecurityClassification"/>, and translate the
///     reply's <c>MxStatusProxy</c> into an OPC UA <see cref="WriteResult"/>.
/// </summary>
/// <remarks>
///     Item handle cache survives across writes — repeated writes to the same tag avoid
///     re-AddItem. Per-tag failures are isolated: one bad write doesn't fail the batch.
///     When a <c>subscribedHandleSource</c> delegate is supplied, the first write to an
///     already-subscribed tag borrows the live handle from the subscription registry and
///     skips the AddItem round-trip entirely. Borrowed handles are intentionally NOT stored
///     in <c>_itemHandles</c> — the subscription registry owns their lifecycle (including
///     reconnect rebind), so each write re-borrows the fresh handle and no stale-cache
///     window is introduced.
/// </remarks>
public sealed class GatewayGalaxyDataWriter : IGalaxyDataWriter
{
    private readonly GalaxyMxSession _session;
    private readonly int _writeUserId;
    private readonly ILogger _logger;
    private readonly Func<string, int?>? _subscribedHandleSource;
    private readonly ConcurrentDictionary<string, int> _itemHandles =
        new(StringComparer.OrdinalIgnoreCase);
    // Item handles we've already AdviseSupervisory'd this session — supervisory advise is
    // idempotent but the round-trip isn't free, so do it once per handle (see EnsureSupervisoryAdvisedAsync).
    private readonly ConcurrentDictionary<int, byte> _supervisedHandles = new();
    private int _addItemCallCount;

    // Write failure-visibility meters (archreview 06/S-1). Reuse the pump's meter name so the single host
    // listener catches both. advise_failed = writes short-circuited to Bad because AdviseSupervisory failed
    // (the value could not have committed); unconfirmed = writes reported Good off an EMPTY gateway statuses
    // array (command accepted, COM commit unconfirmed pending the gateway WriteComplete correlation follow-up).
    private static readonly Meter WriterMeter = new(EventPump.MeterName);
    private static readonly Counter<long> WriteAdviseFailed =
        WriterMeter.CreateCounter<long>("galaxy.writes.advise_failed", unit: "{write}",
            description: "Writes short-circuited to Bad because AdviseSupervisory failed (value could not have committed).");
    private static readonly Counter<long> WriteUnconfirmed =
        WriterMeter.CreateCounter<long>("galaxy.writes.unconfirmed", unit: "{write}",
            description: "Writes reported Good off an EMPTY gateway statuses array (command accepted; commit unconfirmed).");

    /// <summary>Initializes a new Galaxy data writer.</summary>
    /// <param name="session">The MXAccess gateway session.</param>
    /// <param name="writeUserId">The user ID for write operations.</param>
    /// <param name="logger">Optional logger for tracing.</param>
    /// <param name="subscribedHandleSource">
    ///     Optional delegate that resolves a live MXAccess item handle from the subscription
    ///     registry for a given tag full reference. When the delegate returns a positive handle
    ///     the writer borrows it and skips the AddItem gateway round-trip. A return value of
    ///     <c>null</c>, 0, or negative is treated as "not available" and the writer falls back
    ///     to its own AddItem. Borrowed handles are NOT cached in this writer — the subscription
    ///     registry owns their lifecycle.
    /// </param>
    public GatewayGalaxyDataWriter(GalaxyMxSession session, int writeUserId, ILogger? logger = null,
        Func<string, int?>? subscribedHandleSource = null)
    {
        _session = session ?? throw new ArgumentNullException(nameof(session));
        _writeUserId = writeUserId;
        _logger = logger ?? NullLogger.Instance;
        _subscribedHandleSource = subscribedHandleSource;
    }

    /// <inheritdoc />
    public void InvalidateHandleCaches()
    {
        _itemHandles.Clear();
        _supervisedHandles.Clear();
    }

    // ===== Test seams (internal — not part of the public contract) =====

    /// <summary>
    ///     Count of item-handle cache entries. Zero on a fresh instance or immediately after
    ///     <see cref="InvalidateHandleCaches"/>. Used by unit tests to verify cache state
    ///     without running a real gRPC round-trip.
    /// </summary>
    internal int CachedItemHandleCount => _itemHandles.Count;

    /// <summary>
    ///     Count of supervisory-advised handle entries. Zero on a fresh instance or immediately
    ///     after <see cref="InvalidateHandleCaches"/>. Used by unit tests to verify cache state.
    /// </summary>
    internal int CachedSupervisedHandleCount => _supervisedHandles.Count;

    /// <summary>Count of real gateway <c>AddItem</c> round-trips this writer has issued. Stays zero
    /// when every handle was served from cache or borrowed from the subscription registry. Test seam.</summary>
    internal int AddItemCallCount => Volatile.Read(ref _addItemCallCount);

    /// <summary>
    ///     Pre-populate both caches as if a write had already occurred. Used by unit tests to
    ///     simulate the "post-write" state without running a real gRPC gateway session (the SDK
    ///     session types are sealed + internal-ctor and cannot be faked).
    /// </summary>
    /// <param name="fullRef">The tag full reference to add to the item-handle cache.</param>
    /// <param name="itemHandle">The item handle to cache for that reference.</param>
    /// <param name="supervised">When true, also records the handle in the supervised-handle cache.</param>
    internal void SeedHandleCachesForTest(string fullRef, int itemHandle, bool supervised)
    {
        _itemHandles[fullRef] = itemHandle;
        if (supervised) _supervisedHandles.TryAdd(itemHandle, 0);
    }

    /// <summary>
    ///     Resolve an item handle WITHOUT touching the gateway: a handle this writer already AddItem'd
    ///     (<c>_itemHandles</c>), else a live subscription handle borrowed via
    ///     <c>_subscribedHandleSource</c>. Returns null when neither is available (the caller must
    ///     AddItem). A BORROWED handle is intentionally NOT stored in <c>_itemHandles</c> — the
    ///     subscription registry owns the borrowed handle's lifecycle (including reconnect rebind),
    ///     so the next write re-borrows the fresh handle and no stale-cache window is introduced.
    /// </summary>
    /// <param name="fullRef">The dotted tag full reference.</param>
    /// <returns>A usable item handle, or null when an AddItem is required.</returns>
    internal int? TryResolveCachedOrBorrowed(string fullRef)
    {
        if (_itemHandles.TryGetValue(fullRef, out var existing)) return existing;
        if (_subscribedHandleSource?.Invoke(fullRef) is int borrowed && borrowed > 0) return borrowed;
        return null;
    }

    /// <inheritdoc />
    public async Task<IReadOnlyList<WriteResult>> WriteAsync(
        IReadOnlyList<WriteRequest> writes,
        Func<string, SecurityClassification> securityResolver,
        CancellationToken cancellationToken)
    {
        ArgumentNullException.ThrowIfNull(writes);
        ArgumentNullException.ThrowIfNull(securityResolver);

        var session = _session.Session
            ?? throw new InvalidOperationException(
                "GalaxyMxSession is not connected. Call ConnectAsync before issuing writes.");
        var serverHandle = _session.ServerHandle;

        // Bind the per-batch MX write operations behind the internal IMxWriteOps seam (see below) so the
        // whole write pipeline is unit-testable without the sealed SDK session (archreview 06/S-1).
        var ops = new SessionMxWriteOps(session, serverHandle);

        var results = new WriteResult[writes.Count];
        for (var i = 0; i < writes.Count; i++)
        {
            results[i] = await WriteOneAsync(ops, writes[i],
                securityResolver(writes[i].FullReference), cancellationToken)
                .ConfigureAwait(false);
        }
        return results;
    }

    /// <summary>Test seam: drive the real single-write pipeline against a fake <see cref="IMxWriteOps"/>
    /// so the fail-closed / unconfirmed behaviour (archreview 06/S-1) is covered offline. Behaviourally
    /// identical to the per-entry path <see cref="WriteAsync"/> runs.</summary>
    /// <param name="ops">The (fake) MX write operations.</param>
    /// <param name="request">The write request.</param>
    /// <param name="classification">The tag's security classification.</param>
    /// <param name="ct">The cancellation token.</param>
    /// <returns>The translated write result.</returns>
    internal Task<WriteResult> WriteOneForTestAsync(
        IMxWriteOps ops, WriteRequest request, SecurityClassification classification, CancellationToken ct)
        => WriteOneAsync(ops, request, classification, ct);

    private async Task<WriteResult> WriteOneAsync(
        IMxWriteOps ops, WriteRequest request,
        SecurityClassification classification, CancellationToken ct)
    {
        try
        {
            var itemHandle = await EnsureItemHandleAsync(ops, request.FullReference, ct)
                .ConfigureAwait(false);
            var mxValue = MxValueEncoder.Encode(request.Value);

            MxCommandReply reply;
            if (NeedsSecuredWrite(classification))
            {
                // SecuredWrite/VerifiedWrite tags carry their own ArchestrA user identity
                // (current/verifier user), so they don't use the supervisory path.
                reply = await InvokeWriteSecuredAsync(ops, itemHandle, mxValue, ct)
                    .ConfigureAwait(false);
            }
            else
            {
                // A raw Write runs with NO user login (WriteUserId is typically 0), so MXAccess
                // only COMMITS the value when the item is advised in SUPERVISORY mode. Without it
                // the gateway's Write call doesn't throw (reply looks OK) but the value never
                // reaches the galaxy — a knowingly-lost write. FAIL-CLOSED (archreview 06/S-1): when
                // AdviseSupervisory fails we do NOT issue the raw Write; we return Bad so the
                // #5 write-outcome self-correction reverts the phantom-Good node instead of leaving it.
                // The real fix is a gateway-side WriteComplete correlation (mxaccessgw backlog) so the
                // unary reply's Statuses carry the actual COM commit outcome; until then advise-status +
                // the galaxy.writes.* meters are the honest signal.
                if (!await EnsureSupervisoryAdvisedAsync(ops, itemHandle, ct).ConfigureAwait(false))
                {
                    WriteAdviseFailed.Add(1);
                    return new WriteResult(StatusCodeMap.BadCommunicationError);
                }
                reply = await ops.WriteRawAsync(itemHandle, mxValue, _writeUserId, ct)
                    .ConfigureAwait(false);
            }

            return TranslateReply(reply, request.FullReference);
        }
        catch (ArgumentException ex)
        {
            // Bad value type — caller passed a CLR type the encoder can't render.
            _logger.LogWarning(ex,
                "GalaxyDriver write rejected — unsupported value type for {FullRef}", request.FullReference);
            return new WriteResult(StatusCodeMap.BadInternalError);
        }
        catch (OperationCanceledException) when (ct.IsCancellationRequested) { throw; }
        catch (Exception ex)
        {
            _logger.LogWarning(ex, "GalaxyDriver write failed for {FullRef}", request.FullReference);
            return new WriteResult(StatusCodeMap.BadCommunicationError);
        }
    }

    private static bool NeedsSecuredWrite(SecurityClassification classification) =>
        classification is SecurityClassification.SecuredWrite or SecurityClassification.VerifiedWrite;

    private async Task<int> EnsureItemHandleAsync(
        IMxWriteOps ops, string fullRef, CancellationToken ct)
    {
        if (TryResolveCachedOrBorrowed(fullRef) is int resolved) return resolved;
        var handle = await ops.AddItemAsync(fullRef, ct).ConfigureAwait(false);
        Interlocked.Increment(ref _addItemCallCount);
        _itemHandles[fullRef] = handle;
        return handle;
    }

    /// <summary>
    ///     Advise an item in MXAccess <c>AdviseSupervisory</c> mode. A plain <c>Write</c> runs
    ///     under no user login (<see cref="_writeUserId"/> is typically 0); MXAccess only
    ///     COMMITS such a write when the item is supervisory-advised. (A logged-in identity —
    ///     i.e. a non-zero WriteUserId, as the ScadaBridge sister driver uses — would commit via
    ///     a regular Advise instead; we don't log in, so we use the supervisory context.)
    ///     The gateway client doesn't expose a typed method, so we build the <see cref="MxCommand"/>
    ///     and route through <c>InvokeAsync</c> (same pattern as <see cref="InvokeWriteSecuredAsync"/>).
    ///     Idempotent per handle for the session lifetime. The keyed-by-handle dedupe is reset on a
    ///     session reconnect via <see cref="InvalidateHandleCaches"/>, so a fresh session always
    ///     re-advises — the supervisory state never outlives the handle it was taken against.
    /// </summary>
    /// <summary>Ensure the item is supervisory-advised so a no-login raw Write can commit. Returns
    /// <c>true</c> when the item is advised (already-advised this session, or the advise round-trip
    /// succeeded), <c>false</c> when the advise failed — the caller then FAILS THE WRITE CLOSED
    /// (archreview 06/S-1) rather than issuing a raw Write that cannot commit.</summary>
    /// <returns><c>true</c> when advised; <c>false</c> when the advise failed (caller must not write).</returns>
    private async Task<bool> EnsureSupervisoryAdvisedAsync(
        IMxWriteOps ops, int itemHandle, CancellationToken ct)
    {
        // Already advised this session (a prior successful advise on this handle) — nothing to do.
        if (!_supervisedHandles.TryAdd(itemHandle, 0)) return true;

        var request = new MxCommandRequest
        {
            SessionId = ops.SessionId,
            ClientCorrelationId = Guid.NewGuid().ToString("N"),
            Command = new MxCommand
            {
                Kind = MxCommandKind.AdviseSupervisory,
                AdviseSupervisory = new AdviseSupervisoryCommand
                {
                    ServerHandle = ops.ServerHandle,
                    ItemHandle = itemHandle,
                },
            },
        };

        var reply = await ops.InvokeAsync(request, ct).ConfigureAwait(false);
        if (reply.ProtocolStatus is { } proto && proto.Code != ProtocolStatusCode.Ok)
        {
            // Supervisory advise failed — forget it so the next write retries the advise, and report
            // failure so the caller fails the write closed (the raw Write would not commit).
            _supervisedHandles.TryRemove(itemHandle, out _);
            _logger.LogWarning(
                "GalaxyDriver supervisory advise failed for item {ItemHandle}: {Code} {Message}",
                itemHandle, proto.Code, proto.Message);
            return false;
        }

        return true;
    }

    /// <summary>
    ///     Issue a WriteSecured command. The high-level session client doesn't expose
    ///     <c>WriteSecuredAsync</c> as a typed method — we build the <see cref="MxCommand"/>
    ///     directly and route through <c>InvokeAsync</c>. Verifier user is left at zero
    ///     for SecuredWrite; VerifiedWrite uses the same path because the gw's worker
    ///     interprets the underlying MXAccess command kind.
    /// </summary>
    private static Task<MxCommandReply> InvokeWriteSecuredAsync(
        IMxWriteOps ops, int itemHandle, MxValue value, CancellationToken ct)
    {
        var command = new MxCommand
        {
            Kind = MxCommandKind.WriteSecured,
            WriteSecured = new WriteSecuredCommand
            {
                ServerHandle = ops.ServerHandle,
                ItemHandle = itemHandle,
                Value = value,
                CurrentUserId = 0,
                VerifierUserId = 0,
            },
        };
        var request = new MxCommandRequest
        {
            SessionId = ops.SessionId,
            ClientCorrelationId = Guid.NewGuid().ToString("N"),
            Command = command,
        };
        return ops.InvokeAsync(request, ct);
    }

    /// <summary>
    ///     Translate a gateway <see cref="MxCommandReply"/> into an OPC UA
    ///     <see cref="WriteResult"/>. Honours the protocol-level Status field first
    ///     (transport / dispatch failures), then the first MXAccess status row.
    ///     <para>An EMPTY statuses array is treated as <c>Good</c> but is PROVISIONAL: the reply proves
    ///     worker-side command acceptance, not the COM-side commit (the gateway does not yet correlate a
    ///     WriteComplete row onto the unary reply — mxaccessgw backlog). Each such reply increments
    ///     <c>galaxy.writes.unconfirmed</c> so the unconfirmed-write rate is operator-visible today
    ///     (archreview 06/S-1).</para>
    /// </summary>
    private WriteResult TranslateReply(MxCommandReply reply, string fullRef)
    {
        // Protocol status — wraps transport / worker-side failures that happen before
        // MXAccess saw the command.
        if (reply.ProtocolStatus is { } proto && proto.Code != ProtocolStatusCode.Ok)
        {
            _logger.LogWarning(
                "GalaxyDriver write protocol failure {Code} for {FullRef}: {Message}",
                proto.Code, fullRef, proto.Message);
            return new WriteResult(StatusCodeMap.BadCommunicationError);
        }

        // MX-side status — the worker's WriteCompleteEvent rolls into the reply's
        // statuses array. Use the first row (single-write contract).
        if (reply.Statuses.Count > 0)
        {
            var status = reply.Statuses[0];
            return new WriteResult(StatusCodeMap.FromMxStatus(status, _logger));
        }

        // Empty statuses ⇒ provisional Good. Meter it so the unconfirmed-write rate is visible until the
        // gateway-side WriteComplete correlation lands (archreview 06/S-1 cross-repo follow-up).
        WriteUnconfirmed.Add(1);
        return new WriteResult(StatusCodeMap.Good);
    }
}

/// <summary>
///     The per-write MXAccess gateway operations the writer needs, extracted behind an internal seam so the
///     fail-closed write pipeline (archreview 06/S-1) is unit-testable — the SDK <c>MxGatewaySession</c>
///     types are sealed with internal ctors and cannot be faked. Production binds
///     <see cref="SessionMxWriteOps"/> over the real <c>(MxGatewaySession, serverHandle)</c>; tests inject a
///     fake that records calls and returns canned replies.
/// </summary>
internal interface IMxWriteOps
{
    /// <summary>The gateway session id stamped onto every <see cref="MxCommandRequest"/>.</summary>
    string SessionId { get; }

    /// <summary>The MXAccess server handle carried in each item/advise/write command.</summary>
    int ServerHandle { get; }

    /// <summary>Add an MXAccess item and return its item handle.</summary>
    /// <param name="fullRef">The dotted tag full reference.</param>
    /// <param name="ct">The cancellation token.</param>
    /// <returns>The MXAccess item handle.</returns>
    Task<int> AddItemAsync(string fullRef, CancellationToken ct);

    /// <summary>Invoke a raw <see cref="MxCommand"/> (AdviseSupervisory / WriteSecured) and return the reply.</summary>
    /// <param name="request">The command request.</param>
    /// <param name="ct">The cancellation token.</param>
    /// <returns>The gateway reply.</returns>
    Task<MxCommandReply> InvokeAsync(MxCommandRequest request, CancellationToken ct);

    /// <summary>Issue a raw (non-secured) Write for the given item handle.</summary>
    /// <param name="itemHandle">The MXAccess item handle to write.</param>
    /// <param name="value">The encoded MX value.</param>
    /// <param name="userId">The write user id (typically 0 — the supervisory path commits it).</param>
    /// <param name="ct">The cancellation token.</param>
    /// <returns>The gateway reply.</returns>
    Task<MxCommandReply> WriteRawAsync(int itemHandle, MxValue value, int userId, CancellationToken ct);
}

/// <summary>Production <see cref="IMxWriteOps"/> — a thin adapter over a live
/// <see cref="MxGatewaySession"/> and its server handle. Behaviour is bit-identical to the previous
/// inline <c>(session, serverHandle)</c> calls.</summary>
internal sealed class SessionMxWriteOps : IMxWriteOps
{
    private readonly MxGatewaySession _session;
    private readonly int _serverHandle;

    /// <summary>Initializes the adapter over a connected session + server handle.</summary>
    /// <param name="session">The connected gateway session.</param>
    /// <param name="serverHandle">The MXAccess server handle for this session.</param>
    public SessionMxWriteOps(MxGatewaySession session, int serverHandle)
    {
        _session = session;
        _serverHandle = serverHandle;
    }

    /// <inheritdoc />
    public string SessionId => _session.SessionId;

    /// <inheritdoc />
    public int ServerHandle => _serverHandle;

    /// <inheritdoc />
    public Task<int> AddItemAsync(string fullRef, CancellationToken ct)
        => _session.AddItemAsync(_serverHandle, fullRef, ct);

    /// <inheritdoc />
    public Task<MxCommandReply> InvokeAsync(MxCommandRequest request, CancellationToken ct)
        => _session.InvokeAsync(request, ct);

    /// <inheritdoc />
    public Task<MxCommandReply> WriteRawAsync(int itemHandle, MxValue value, int userId, CancellationToken ct)
        => _session.WriteRawAsync(_serverHandle, itemHandle, value, userId, ct);
}
