using Grpc.Net.Client;
using Microsoft.Extensions.Logging;
using ProtoBuf.Grpc.Client;
using ZB.MOM.WW.LmxProxy.Client.Domain;
using ZB.MOM.WW.LmxProxy.Client.Security;

namespace ZB.MOM.WW.LmxProxy.Client;

public partial class LmxProxyClient
{
    /// <inheritdoc />
    public async Task ConnectAsync(CancellationToken cancellationToken = default)
    {
        await _connectionLock.WaitAsync(cancellationToken);
        try
        {
            ObjectDisposedException.ThrowIf(_disposed, this);

            if (IsConnected)
                return;

            var endpoint = BuildEndpointUri();
            _logger.LogInformation("Connecting to LmxProxy at {Endpoint}", endpoint);

            GrpcChannel channel = GrpcChannelFactory.CreateChannel(endpoint, _tlsConfiguration, _logger, _apiKey);
            IScadaService client;
            try
            {
                client = channel.CreateGrpcService<IScadaService>();
            }
            catch
            {
                channel.Dispose();
                throw;
            }

            ConnectResponse response;
            try
            {
                var request = new ConnectRequest
                {
                    ClientId = $"ScadaBridge-{Guid.NewGuid():N}",
                    ApiKey = _apiKey ?? string.Empty
                };
                response = await client.ConnectAsync(request);
            }
            catch
            {
                channel.Dispose();
                throw;
            }

            if (!response.Success)
            {
                channel.Dispose();
                throw new InvalidOperationException($"Connect failed: {response.Message}");
            }

            _channel = channel;
            _client = client;
            _sessionId = response.SessionId;
            _isConnected = true;

            StartKeepAlive();

            _logger.LogInformation("Connected to LmxProxy, session={SessionId}", _sessionId);
        }
        catch (Exception ex)
        {
            _channel = null;
            _client = null;
            _sessionId = string.Empty;
            _isConnected = false;
            _logger.LogError(ex, "Failed to connect to LmxProxy");
            throw;
        }
        finally
        {
            _connectionLock.Release();
        }
    }

    /// <inheritdoc />
    public async Task DisconnectAsync()
    {
        await _connectionLock.WaitAsync();
        try
        {
            StopKeepAlive();

            if (_client is not null && !string.IsNullOrEmpty(_sessionId))
            {
                try
                {
                    await _client.DisconnectAsync(new DisconnectRequest { SessionId = _sessionId });
                }
                catch (Exception ex)
                {
                    _logger.LogWarning(ex, "Error sending disconnect request");
                }
            }

            _client = null;
            _sessionId = string.Empty;
            _isConnected = false;
            _channel?.Dispose();
            _channel = null;
        }
        finally
        {
            _connectionLock.Release();
        }
    }

    /// <inheritdoc cref="SubscribeAsync"/>
    public async Task<ISubscription> SubscribeAsync(
        IEnumerable<string> addresses,
        Action<string, Vtq> onUpdate,
        Action<Exception>? onStreamError = null,
        CancellationToken cancellationToken = default)
    {
        EnsureConnected();

        var subscription = new CodeFirstSubscription(
            _client!,
            _sessionId,
            addresses.ToList(),
            onUpdate,
            onStreamError,
            _logger,
            sub =>
            {
                lock (_subscriptionLock)
                {
                    _activeSubscriptions.Remove(sub);
                }
            });

        lock (_subscriptionLock)
        {
            _activeSubscriptions.Add(subscription);
        }

        await subscription.StartAsync(cancellationToken);
        return subscription;
    }

    private void StartKeepAlive()
    {
        _keepAliveTimer = new Timer(
            async _ => await KeepAliveCallback(),
            null,
            _keepAliveInterval,
            _keepAliveInterval);
    }

    private async Task KeepAliveCallback()
    {
        try
        {
            if (_client is null || string.IsNullOrEmpty(_sessionId))
                return;

            await _client.GetConnectionStateAsync(new GetConnectionStateRequest { SessionId = _sessionId });
        }
        catch (Exception ex)
        {
            _logger.LogWarning(ex, "Keep-alive failed, marking disconnected");
            StopKeepAlive();
            await MarkDisconnectedAsync(ex);
        }
    }

    private void StopKeepAlive()
    {
        _keepAliveTimer?.Dispose();
        _keepAliveTimer = null;
    }

    internal async Task MarkDisconnectedAsync(Exception ex)
    {
        if (_disposed) return;

        await _connectionLock.WaitAsync();
        try
        {
            _isConnected = false;
            _client = null;
            _sessionId = string.Empty;
            _channel?.Dispose();
            _channel = null;
        }
        finally
        {
            _connectionLock.Release();
        }

        List<ISubscription> subscriptions;
        lock (_subscriptionLock)
        {
            subscriptions = [.. _activeSubscriptions];
            _activeSubscriptions.Clear();
        }

        foreach (var sub in subscriptions)
        {
            try { sub.Dispose(); }
            catch { /* swallow */ }
        }

        _logger.LogWarning(ex, "Client marked as disconnected");
    }

    private Uri BuildEndpointUri()
    {
        string scheme = _tlsConfiguration?.UseTls == true ? Uri.UriSchemeHttps : Uri.UriSchemeHttp;
        return new UriBuilder { Scheme = scheme, Host = _host, Port = _port }.Uri;
    }
}
