using System.Collections.Concurrent;
using System.Globalization;
using Grpc.Core;
using Microsoft.Extensions.Logging;
using ZB.MOM.WW.MxGateway.Client;
using ZB.MOM.WW.MxGateway.Contracts.Proto;
using ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Protocol;

namespace ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Adapters;

/// <summary>
/// Production <see cref="IMxGatewayClient"/> implementation over the
/// <c>ZB.MOM.WW.MxGateway.Client</c> NuGet package. This is the only type in the
/// Data Connection Layer that references the generated gRPC/protobuf contracts;
/// the adapter and its tests run entirely against the neutral seam.
/// </summary>
public sealed class RealMxGatewayClient : IMxGatewayClient
{
    private readonly ILogger<RealMxGatewayClient> _logger;
    private readonly ILoggerFactory? _loggerFactory;

    private MxGatewayClient? _client;
    private GalaxyRepositoryClient? _galaxy;
    private MxGatewaySession? _session;
    private int _serverHandle;
    private int _writeUserId;
    private int _readTimeoutMs;
    private ulong _lastSeq;

    // tag ↔ MXAccess item handle, maintained across subscribe/write.
    private readonly ConcurrentDictionary<string, int> _tagToHandle = new();
    private readonly ConcurrentDictionary<int, string> _handleToTag = new();

    /// <summary>Initializes a new instance of <see cref="RealMxGatewayClient"/>.</summary>
    /// <param name="loggerFactory">Logger factory shared with the gateway client.</param>
    public RealMxGatewayClient(ILoggerFactory? loggerFactory)
    {
        _loggerFactory = loggerFactory;
        _logger = (loggerFactory ?? Microsoft.Extensions.Logging.Abstractions.NullLoggerFactory.Instance)
            .CreateLogger<RealMxGatewayClient>();
    }

    /// <inheritdoc />
    public async Task ConnectAsync(MxGatewayConnectionOptions options, CancellationToken ct = default)
    {
        _writeUserId = options.WriteUserId;
        _readTimeoutMs = options.ReadTimeoutMs;

        var clientOptions = new MxGatewayClientOptions
        {
            Endpoint = new Uri(options.Endpoint),
            ApiKey = options.ApiKey,
            UseTls = options.UseTls,
            CaCertificatePath = options.CaFile,
            ServerNameOverride = options.ServerName,
            LoggerFactory = _loggerFactory,
        };

        _client = MxGatewayClient.Create(clientOptions);
        _galaxy = GalaxyRepositoryClient.Create(clientOptions);
        _session = await _client.OpenSessionAsync(cancellationToken: ct).ConfigureAwait(false);
        _serverHandle = await _session.RegisterAsync(options.ClientName, ct).ConfigureAwait(false);
    }

    /// <inheritdoc />
    public async Task DisconnectAsync(CancellationToken ct = default)
    {
        if (_session is not null)
            await _session.CloseAsync(ct).ConfigureAwait(false);
    }

    /// <inheritdoc />
    public async Task<string> SubscribeAsync(string tagPath, CancellationToken ct = default)
    {
        var handle = await GetOrAddItemHandleAsync(tagPath, ct).ConfigureAwait(false);
        await _session!.AdviseAsync(_serverHandle, handle, ct).ConfigureAwait(false);
        return handle.ToString(CultureInfo.InvariantCulture);
    }

    /// <inheritdoc />
    public async Task UnsubscribeAsync(string subscriptionId, CancellationToken ct = default)
    {
        if (!int.TryParse(subscriptionId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var handle))
            return;

        await _session!.UnAdviseAsync(_serverHandle, handle, ct).ConfigureAwait(false);
        await _session.RemoveItemAsync(_serverHandle, handle, ct).ConfigureAwait(false);

        if (_handleToTag.TryRemove(handle, out var tag))
            _tagToHandle.TryRemove(tag, out _);
    }

    /// <inheritdoc />
    public async Task<IReadOnlyList<MxReadOutcome>> ReadAsync(IReadOnlyList<string> tagPaths, CancellationToken ct = default)
    {
        var results = await _session!
            .ReadBulkAsync(_serverHandle, tagPaths, TimeSpan.FromMilliseconds(_readTimeoutMs), ct)
            .ConfigureAwait(false);

        return results.Select(r => new MxReadOutcome(
            r.TagAddress,
            r.WasSuccessful,
            r.WasSuccessful ? r.Value?.ToClrValue() : null,
            MapQuality(r.Quality, r.Statuses),
            r.SourceTimestamp?.ToDateTimeOffset() ?? DateTimeOffset.UtcNow,
            r.WasSuccessful ? null : r.ErrorMessage)).ToList();
    }

    /// <inheritdoc />
    public async Task<IReadOnlyList<MxWriteOutcome>> WriteAsync(IReadOnlyList<(string TagPath, object? Value)> writes, CancellationToken ct = default)
    {
        // Build entries in request order; remember the tag for each handle so the
        // per-handle BulkWriteResult can be mapped back to its tag.
        var entries = new List<WriteBulkEntry>(writes.Count);
        var orderedTags = new List<string>(writes.Count);
        foreach (var (tag, value) in writes)
        {
            var handle = await GetOrAddItemHandleAsync(tag, ct).ConfigureAwait(false);
            entries.Add(new WriteBulkEntry
            {
                ItemHandle = handle,
                Value = ToMxValue(value),
                UserId = _writeUserId,
            });
            orderedTags.Add(tag);
        }

        var results = await _session!.WriteBulkAsync(_serverHandle, entries, ct).ConfigureAwait(false);

        // Results are returned in request order; pair by index back to the tags.
        return results.Select((r, i) => new MxWriteOutcome(
            i < orderedTags.Count ? orderedTags[i] : (_handleToTag.TryGetValue(r.ItemHandle, out var t) ? t : ""),
            r.WasSuccessful,
            r.WasSuccessful ? null : r.ErrorMessage)).ToList();
    }

    /// <inheritdoc />
    public async Task<(IReadOnlyList<MxBrowseChild> Children, bool Truncated)> BrowseChildrenAsync(string? parentNodeId, CancellationToken ct = default)
    {
        // Navigation browse returns child OBJECTS only (IncludeAttributes = false).
        // An object's own attributes are loaded lazily — via DiscoverHierarchy below,
        // scoped to that single object — only when the object is expanded. This keeps
        // each browse level's payload to a few KB. Inlining every child's full
        // attribute set (the previous behaviour) produced replies that exceeded
        // Akka's remote frame size (e.g. ~152 KB for one attribute-heavy area), and
        // remoting silently discarded the oversized reply, hanging the picker.
        var request = new BrowseChildrenRequest { IncludeAttributes = false };

        // Object NodeIds are the Galaxy gobject id (encoded as a string); attribute
        // NodeIds are FullTagReference leaves and never arrive here as a parent.
        var parentGobjectId = 0;
        var haveParentObject = !string.IsNullOrEmpty(parentNodeId)
            && int.TryParse(parentNodeId, NumberStyles.Integer, CultureInfo.InvariantCulture, out parentGobjectId);
        if (haveParentObject)
        {
            request.ParentGobjectId = parentGobjectId;
        }

        BrowseChildrenReply reply;
        GalaxyObject? parentObject = null;
        try
        {
            reply = await _galaxy!.BrowseChildrenRawAsync(request, ct).ConfigureAwait(false);

            // When expanding a concrete object, fetch just that object (MaxDepth = 0)
            // with its attributes so they can be listed as selectable leaves. The root
            // level has no parent object, hence no attributes of its own.
            if (haveParentObject)
            {
                var attrRequest = new DiscoverHierarchyRequest
                {
                    RootGobjectId = parentGobjectId,
                    MaxDepth = 0,
                    IncludeAttributes = true,
                };
                var attrReply = await _galaxy!.DiscoverHierarchyRawAsync(attrRequest, ct).ConfigureAwait(false);
                parentObject = attrReply.Objects.Count > 0 ? attrReply.Objects[0] : null;
            }
        }
        catch (RpcException ex) when (ex.StatusCode == StatusCode.Unavailable)
        {
            throw new ConnectionNotConnectedException($"MxGateway repository unavailable: {ex.Status.Detail}");
        }
        catch (RpcException ex) when (ex.StatusCode == StatusCode.Unimplemented)
        {
            // The data pipe (read/subscribe/write) works against every gateway
            // build, but Galaxy hierarchy browsing (the BrowseChildren RPC) was
            // added later. An older gateway answers Unimplemented. Surface a
            // clear, actionable reason instead of a raw gRPC fault — the actor
            // maps NotSupportedException to BrowseFailureKind.NotBrowsable and
            // carries this message through to the picker.
            throw new NotSupportedException(
                "The connected MxGateway build does not support hierarchy browsing. "
                + "Update the gateway to a build that implements BrowseChildren, "
                + "or enter the tag reference manually.");
        }

        var children = new List<MxBrowseChild>();

        // Navigable child objects, keyed by gobject id. Always marked expandable —
        // every Galaxy object carries attributes (and may host sub-objects), both
        // resolved on demand when the node is expanded.
        foreach (var obj in reply.Children)
        {
            children.Add(new MxBrowseChild(
                obj.GobjectId.ToString(CultureInfo.InvariantCulture),
                string.IsNullOrEmpty(obj.TagName) ? obj.ContainedName : obj.TagName,
                BrowseNodeClass.Object,
                true));
        }

        // The expanded object's own attributes, as selectable leaves keyed by their
        // full tag reference.
        if (parentObject is not null)
        {
            foreach (var attr in parentObject.Attributes)
            {
                children.Add(new MxBrowseChild(
                    attr.FullTagReference,
                    attr.AttributeName,
                    BrowseNodeClass.Variable,
                    false));
            }
        }

        return (children, !string.IsNullOrEmpty(reply.NextPageToken));
    }

    /// <inheritdoc />
    public async Task RunEventLoopAsync(Action<MxValueUpdate> onUpdate, CancellationToken ct = default)
    {
        await foreach (var ev in _session!.StreamEventsAsync(_lastSeq, ct).ConfigureAwait(false))
        {
            _lastSeq = ev.WorkerSequence;
            if (ev.Family != MxEventFamily.OnDataChange)
                continue;
            if (!_handleToTag.TryGetValue(ev.ItemHandle, out var tag))
                continue;

            onUpdate(new MxValueUpdate(
                tag,
                ev.Value?.ToClrValue(),
                MapQuality(ev.Quality, ev.Statuses),
                ev.SourceTimestamp?.ToDateTimeOffset() ?? DateTimeOffset.UtcNow));
        }
    }

    /// <inheritdoc />
    public async Task RunAlarmStreamAsync(
        string? alarmFilterPrefix, Action<Commons.Types.Alarms.NativeAlarmTransition> onTransition,
        CancellationToken ct = default)
    {
        var reconnectDelay = TimeSpan.FromSeconds(5);
        while (!ct.IsCancellationRequested)
        {
            try
            {
                var request = new StreamAlarmsRequest
                {
                    ClientCorrelationId = Guid.NewGuid().ToString("N"),
                    AlarmFilterPrefix = alarmFilterPrefix ?? string.Empty,
                };

                await foreach (var message in _client!.StreamAlarmsAsync(request, ct)
                    .WithCancellation(ct).ConfigureAwait(false))
                {
                    if (ct.IsCancellationRequested) break;
                    switch (message.PayloadCase)
                    {
                        case AlarmFeedMessage.PayloadOneofCase.ActiveAlarm:
                            onTransition(MxGatewayAlarmMapper.MapSnapshot(message.ActiveAlarm));
                            break;
                        case AlarmFeedMessage.PayloadOneofCase.Transition:
                            onTransition(MxGatewayAlarmMapper.MapTransition(message.Transition));
                            break;
                        case AlarmFeedMessage.PayloadOneofCase.SnapshotComplete:
                            onTransition(MxGatewayAlarmMapper.SnapshotComplete());
                            break;
                    }
                }
            }
            catch (OperationCanceledException) when (ct.IsCancellationRequested)
            {
                return; // clean shutdown
            }
            catch (Exception ex)
            {
                _logger.LogWarning(ex,
                    "MxGateway alarm stream faulted; reopening in {DelaySeconds}s", reconnectDelay.TotalSeconds);
            }

            try
            {
                await Task.Delay(reconnectDelay, ct).ConfigureAwait(false);
            }
            catch (OperationCanceledException) when (ct.IsCancellationRequested)
            {
                return;
            }
        }
    }

    /// <summary>
    /// Asynchronously disposes the active MxAccess session, client, and galaxy resources.
    /// </summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    public async ValueTask DisposeAsync()
    {
        if (_session is not null) await _session.DisposeAsync().ConfigureAwait(false);
        if (_client is not null) await _client.DisposeAsync().ConfigureAwait(false);
        if (_galaxy is not null) await _galaxy.DisposeAsync().ConfigureAwait(false);
    }

    private async Task<int> GetOrAddItemHandleAsync(string tagPath, CancellationToken ct)
    {
        if (_tagToHandle.TryGetValue(tagPath, out var existing))
            return existing;

        var handle = await _session!.AddItemAsync(_serverHandle, tagPath, ct).ConfigureAwait(false);
        _tagToHandle[tagPath] = handle;
        _handleToTag[handle] = tagPath;
        return handle;
    }

    /// <summary>
    /// Maps MXAccess quality. A failing status proxy is authoritative bad; otherwise
    /// the OPC-style quality byte: ≥192 Good, ≥64 Uncertain, else Bad.
    /// </summary>
    private static QualityCode MapQuality(int quality, IEnumerable<MxStatusProxy> statuses)
    {
        if (statuses.Any(s => !s.IsSuccess()))
            return QualityCode.Bad;
        return quality switch
        {
            >= 192 => QualityCode.Good,
            >= 64 => QualityCode.Uncertain,
            _ => QualityCode.Bad,
        };
    }

    private static MxValue ToMxValue(object? value) => value switch
    {
        null => new MxValue { IsNull = true },
        bool b => b.ToMxValue(),
        int i => i.ToMxValue(),
        long l => l.ToMxValue(),
        float f => f.ToMxValue(),
        double d => d.ToMxValue(),
        string s => s.ToMxValue(),
        DateTimeOffset dto => dto.ToMxValue(),
        DateTime dt => dt.ToMxValue(),
        // Fall back to invariant string for any other CLR type.
        _ => Convert.ToString(value, CultureInfo.InvariantCulture)!.ToMxValue(),
    };
}

/// <summary>Builds <see cref="RealMxGatewayClient"/> instances.</summary>
public sealed class RealMxGatewayClientFactory : IMxGatewayClientFactory
{
    private readonly ILoggerFactory? _loggerFactory;

    /// <summary>Initializes a new factory.</summary>
    /// <param name="loggerFactory">Logger factory passed to each created client.</param>
    public RealMxGatewayClientFactory(ILoggerFactory? loggerFactory) => _loggerFactory = loggerFactory;

    /// <inheritdoc />
    public IMxGatewayClient Create() => new RealMxGatewayClient(_loggerFactory);
}
