using System.Diagnostics;
using Grpc.Core;
using Grpc.Net.Client;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Polly;
using Polly.Retry;
using ZB.MOM.WW.LmxProxy.Client.Domain;

namespace ZB.MOM.WW.LmxProxy.Client;

/// <summary>
/// gRPC client for the LmxProxy SCADA proxy service. Uses v2 protocol with native TypedValue.
/// </summary>
public partial class LmxProxyClient : ILmxProxyClient
{
    private readonly ILogger<LmxProxyClient> _logger;
    private readonly string _host;
    private readonly int _port;
    private readonly string? _apiKey;
    private readonly ClientTlsConfiguration? _tlsConfiguration;
    private readonly ClientMetrics _metrics = new();
    private readonly SemaphoreSlim _connectionLock = new(1, 1);
    private readonly List<ISubscription> _activeSubscriptions = [];
    private readonly Lock _subscriptionLock = new();

    private GrpcChannel? _channel;
    private IScadaService? _client;
    private string _sessionId = string.Empty;
    private bool _disposed;
    private bool _isConnected;
    private TimeSpan _defaultTimeout = TimeSpan.FromSeconds(30);
    private ClientConfiguration? _configuration;
    private ResiliencePipeline? _resiliencePipeline;
    private Timer? _keepAliveTimer;
    private readonly TimeSpan _keepAliveInterval = TimeSpan.FromSeconds(30);

    /// <summary>Returns true if the client has an active session and is not disposed.</summary>
    public bool IsConnected => !_disposed && _isConnected && !string.IsNullOrEmpty(_sessionId);

    /// <inheritdoc />
    public TimeSpan DefaultTimeout
    {
        get => _defaultTimeout;
        set
        {
            if (value < TimeSpan.FromSeconds(1) || value > TimeSpan.FromMinutes(10))
                throw new ArgumentOutOfRangeException(nameof(value), "DefaultTimeout must be between 1 second and 10 minutes.");
            _defaultTimeout = value;
        }
    }

    /// <summary>
    /// Creates a new LmxProxyClient instance.
    /// </summary>
    public LmxProxyClient(
        string host, int port, string? apiKey,
        ClientTlsConfiguration? tlsConfiguration,
        ILogger<LmxProxyClient>? logger = null)
    {
        _host = host ?? throw new ArgumentNullException(nameof(host));
        _port = port;
        _apiKey = apiKey;
        _tlsConfiguration = tlsConfiguration;
        _logger = logger ?? NullLogger<LmxProxyClient>.Instance;
    }

    /// <summary>
    /// Sets builder configuration including retry policies. Called internally by the builder.
    /// </summary>
    internal void SetBuilderConfiguration(ClientConfiguration config)
    {
        _configuration = config;
        if (config.MaxRetryAttempts > 0)
        {
            _resiliencePipeline = new ResiliencePipelineBuilder()
                .AddRetry(new RetryStrategyOptions
                {
                    MaxRetryAttempts = config.MaxRetryAttempts,
                    Delay = config.RetryDelay,
                    BackoffType = DelayBackoffType.Exponential,
                    ShouldHandle = new PredicateBuilder()
                        .Handle<RpcException>(ex =>
                            ex.StatusCode == StatusCode.Unavailable ||
                            ex.StatusCode == StatusCode.DeadlineExceeded ||
                            ex.StatusCode == StatusCode.ResourceExhausted ||
                            ex.StatusCode == StatusCode.Aborted),
                    OnRetry = args =>
                    {
                        _logger.LogWarning("Retry {Attempt} after {Delay} for {Exception}",
                            args.AttemptNumber, args.RetryDelay, args.Outcome.Exception?.Message);
                        return ValueTask.CompletedTask;
                    }
                })
                .Build();
        }
    }

    /// <inheritdoc />
    public async Task<Vtq> ReadAsync(string address, CancellationToken cancellationToken = default)
    {
        EnsureConnected();
        _metrics.IncrementOperationCount("Read");
        var sw = Stopwatch.StartNew();
        try
        {
            var request = new ReadRequest { SessionId = _sessionId, Tag = address };
            ReadResponse response = await ExecuteWithRetry(
                () => _client!.ReadAsync(request).AsTask(), cancellationToken);
            if (!response.Success)
                throw new InvalidOperationException($"Read failed: {response.Message}");
            return ConvertVtqMessage(response.Vtq);
        }
        catch
        {
            _metrics.IncrementErrorCount("Read");
            throw;
        }
        finally
        {
            sw.Stop();
            _metrics.RecordLatency("Read", sw.ElapsedMilliseconds);
        }
    }

    /// <inheritdoc />
    public async Task<IDictionary<string, Vtq>> ReadBatchAsync(
        IEnumerable<string> addresses, CancellationToken cancellationToken = default)
    {
        EnsureConnected();
        _metrics.IncrementOperationCount("ReadBatch");
        var sw = Stopwatch.StartNew();
        try
        {
            var request = new ReadBatchRequest { SessionId = _sessionId, Tags = addresses.ToList() };
            ReadBatchResponse response = await ExecuteWithRetry(
                () => _client!.ReadBatchAsync(request).AsTask(), cancellationToken);
            var result = new Dictionary<string, Vtq>();
            foreach (var vtqMsg in response.Vtqs)
            {
                result[vtqMsg.Tag] = ConvertVtqMessage(vtqMsg);
            }
            return result;
        }
        catch
        {
            _metrics.IncrementErrorCount("ReadBatch");
            throw;
        }
        finally
        {
            sw.Stop();
            _metrics.RecordLatency("ReadBatch", sw.ElapsedMilliseconds);
        }
    }

    /// <inheritdoc />
    public async Task WriteAsync(string address, TypedValue value, CancellationToken cancellationToken = default)
    {
        EnsureConnected();
        _metrics.IncrementOperationCount("Write");
        var sw = Stopwatch.StartNew();
        try
        {
            var request = new WriteRequest { SessionId = _sessionId, Tag = address, Value = value };
            WriteResponse response = await ExecuteWithRetry(
                () => _client!.WriteAsync(request).AsTask(), cancellationToken);
            if (!response.Success)
                throw new InvalidOperationException($"Write failed: {response.Message}");
        }
        catch
        {
            _metrics.IncrementErrorCount("Write");
            throw;
        }
        finally
        {
            sw.Stop();
            _metrics.RecordLatency("Write", sw.ElapsedMilliseconds);
        }
    }

    /// <inheritdoc />
    public async Task WriteBatchAsync(IDictionary<string, TypedValue> values, CancellationToken cancellationToken = default)
    {
        EnsureConnected();
        _metrics.IncrementOperationCount("WriteBatch");
        var sw = Stopwatch.StartNew();
        try
        {
            var request = new WriteBatchRequest
            {
                SessionId = _sessionId,
                Items = values.Select(kv => new WriteItem { Tag = kv.Key, Value = kv.Value }).ToList()
            };
            WriteBatchResponse response = await ExecuteWithRetry(
                () => _client!.WriteBatchAsync(request).AsTask(), cancellationToken);
            if (!response.Success)
                throw new InvalidOperationException($"WriteBatch failed: {response.Message}");
        }
        catch
        {
            _metrics.IncrementErrorCount("WriteBatch");
            throw;
        }
        finally
        {
            sw.Stop();
            _metrics.RecordLatency("WriteBatch", sw.ElapsedMilliseconds);
        }
    }

    /// <inheritdoc />
    public async Task<WriteBatchAndWaitResponse> WriteBatchAndWaitAsync(
        IDictionary<string, TypedValue> values, string flagTag, TypedValue flagValue,
        int timeoutMs = 5000, int pollIntervalMs = 100, CancellationToken cancellationToken = default)
    {
        EnsureConnected();
        var request = new WriteBatchAndWaitRequest
        {
            SessionId = _sessionId,
            Items = values.Select(kv => new WriteItem { Tag = kv.Key, Value = kv.Value }).ToList(),
            FlagTag = flagTag,
            FlagValue = flagValue,
            TimeoutMs = timeoutMs,
            PollIntervalMs = pollIntervalMs
        };
        return await ExecuteWithRetry(
            () => _client!.WriteBatchAndWaitAsync(request).AsTask(), cancellationToken);
    }

    /// <inheritdoc />
    public async Task<ApiKeyInfo> CheckApiKeyAsync(string apiKey, CancellationToken cancellationToken = default)
    {
        EnsureConnected();
        var request = new CheckApiKeyRequest { ApiKey = apiKey };
        CheckApiKeyResponse response = await _client!.CheckApiKeyAsync(request);
        return new ApiKeyInfo { IsValid = response.IsValid, Description = response.Message };
    }

    /// <inheritdoc />
    public Task<bool> IsConnectedAsync() => Task.FromResult(IsConnected);

    /// <inheritdoc />
    public Dictionary<string, object> GetMetrics() => _metrics.GetSnapshot();

    internal static Vtq ConvertVtqMessage(VtqMessage? msg)
    {
        if (msg is null)
            return new Vtq(null, DateTime.UtcNow, Quality.Bad);

        object? value = ExtractTypedValue(msg.Value);
        DateTime timestamp = msg.TimestampUtcTicks > 0
            ? new DateTime(msg.TimestampUtcTicks, DateTimeKind.Utc)
            : DateTime.UtcNow;
        Quality quality = QualityExtensions.FromStatusCode(msg.Quality?.StatusCode ?? 0x80000000u);
        return new Vtq(value, timestamp, quality);
    }

    internal static object? ExtractTypedValue(TypedValue? tv)
    {
        if (tv is null) return null;

        return tv.GetValueCase() switch
        {
            TypedValueCase.BoolValue => tv.BoolValue,
            TypedValueCase.Int32Value => tv.Int32Value,
            TypedValueCase.Int64Value => tv.Int64Value,
            TypedValueCase.FloatValue => tv.FloatValue,
            TypedValueCase.DoubleValue => tv.DoubleValue,
            TypedValueCase.StringValue => tv.StringValue,
            TypedValueCase.BytesValue => tv.BytesValue,
            TypedValueCase.DatetimeValue => new DateTime(tv.DatetimeValue, DateTimeKind.Utc),
            TypedValueCase.ArrayValue => ExtractArrayValue(tv.ArrayValue),
            TypedValueCase.None => null,
            _ => null
        };
    }

    internal static object? ExtractArrayValue(ArrayValue? av)
    {
        if (av is null) return null;
        if (av.BoolValues is not null) return av.BoolValues.Values.ToArray();
        if (av.Int32Values is not null) return av.Int32Values.Values.ToArray();
        if (av.Int64Values is not null) return av.Int64Values.Values.ToArray();
        if (av.FloatValues is not null) return av.FloatValues.Values.ToArray();
        if (av.DoubleValues is not null) return av.DoubleValues.Values.ToArray();
        if (av.StringValues is not null) return av.StringValues.Values.ToArray();
        if (av.DatetimeValues is not null)
        {
            return av.DatetimeValues.Values
                .Select(ticks => new DateTime(ticks, DateTimeKind.Utc))
                .ToArray();
        }
        return null;
    }

    private async Task<T> ExecuteWithRetry<T>(Func<Task<T>> operation, CancellationToken ct)
    {
        if (_resiliencePipeline is not null)
        {
            return await _resiliencePipeline.ExecuteAsync(
                async token => await operation(), ct);
        }
        return await operation();
    }

    private void EnsureConnected()
    {
        ObjectDisposedException.ThrowIf(_disposed, this);
        if (!IsConnected)
            throw new InvalidOperationException("Client is not connected. Call ConnectAsync first.");
    }

    /// <inheritdoc />
    public void Dispose()
    {
        if (_disposed) return;
        _disposed = true;
        _keepAliveTimer?.Dispose();
        _channel?.Dispose();
        _connectionLock.Dispose();
    }

    /// <inheritdoc />
    public async ValueTask DisposeAsync()
    {
        if (_disposed) return;
        try { await DisconnectAsync(); } catch { /* swallow */ }
        Dispose();
    }
}
