using Microsoft.Extensions.Logging;
using ScadaLink.Commons.Interfaces.Protocol;
using ScadaLink.Commons.Serialization;
using ScadaLink.Commons.Types;
using ScadaLink.Commons.Types.DataConnections;
using ScadaLink.Commons.Types.Enums;

namespace ScadaLink.DataConnectionLayer.Adapters;

/// <summary>
/// WP-7: OPC UA adapter implementing IDataConnection.
/// Maps IDataConnection methods to OPC UA concepts via IOpcUaClient abstraction.
///
/// OPC UA mapping:
/// - TagPath → NodeId (e.g., "ns=2;s=MyDevice.Temperature")
/// - Subscribe → MonitoredItem with DataChangeNotification
/// - Read/Write → Read/Write service calls
/// - Quality → OPC UA StatusCode mapping
/// </summary>
public class OpcUaDataConnection : IDataConnection
{
    private readonly IOpcUaClientFactory _clientFactory;
    private readonly ILogger<OpcUaDataConnection> _logger;
    private IOpcUaClient? _client;
    private string _endpointUrl = string.Empty;
    private ConnectionHealth _status = ConnectionHealth.Disconnected;

    /// <summary>
    /// Maps subscription IDs to their tag paths for cleanup.
    /// </summary>
    private readonly Dictionary<string, string> _subscriptionHandles = new();
    private StaleTagMonitor? _staleMonitor;
    private string? _heartbeatSubscriptionId;

    public OpcUaDataConnection(IOpcUaClientFactory clientFactory, ILogger<OpcUaDataConnection> logger)
    {
        _clientFactory = clientFactory;
        _logger = logger;
    }

    private volatile bool _disconnectFired;

    public ConnectionHealth Status => _status;
    public event Action? Disconnected;

    public async Task ConnectAsync(IDictionary<string, string> connectionDetails, CancellationToken cancellationToken = default)
    {
        var config = OpcUaEndpointConfigSerializer.FromFlatDict(connectionDetails);

        _endpointUrl = string.IsNullOrWhiteSpace(config.EndpointUrl)
            ? "opc.tcp://localhost:4840"
            : config.EndpointUrl;

        var options = new OpcUaConnectionOptions(
            SessionTimeoutMs: config.SessionTimeoutMs,
            OperationTimeoutMs: config.OperationTimeoutMs,
            PublishingIntervalMs: config.PublishingIntervalMs,
            KeepAliveCount: config.KeepAliveCount,
            LifetimeCount: config.LifetimeCount,
            MaxNotificationsPerPublish: config.MaxNotificationsPerPublish,
            SamplingIntervalMs: config.SamplingIntervalMs,
            QueueSize: config.QueueSize,
            SecurityMode: config.SecurityMode.ToString(),
            AutoAcceptUntrustedCerts: config.AutoAcceptUntrustedCerts,
            DiscardOldest: config.DiscardOldest,
            SubscriptionPriority: config.SubscriptionPriority,
            SubscriptionDisplayName: config.SubscriptionDisplayName,
            TimestampsToReturn: config.TimestampsToReturn.ToString(),
            Deadband: config.Deadband is { } db
                ? new OpcUaDeadbandOptions(db.Type.ToString(), db.Value)
                : null,
            UserIdentity: config.UserIdentity is { } ui
                ? new OpcUaUserIdentityOptions(
                    ui.TokenType.ToString(), ui.Username, ui.Password,
                    ui.CertificatePath, ui.CertificatePassword)
                : null);

        _status = ConnectionHealth.Connecting;

        _client = _clientFactory.Create();
        _client.ConnectionLost += OnClientConnectionLost;
        await _client.ConnectAsync(_endpointUrl, options, cancellationToken);

        _status = ConnectionHealth.Connected;
        _disconnectFired = false;
        _logger.LogInformation("OPC UA connected to {Endpoint}", _endpointUrl);

        await StartHeartbeatMonitorAsync(config.Heartbeat, cancellationToken);
    }

    private async Task StartHeartbeatMonitorAsync(OpcUaHeartbeatConfig? heartbeat, CancellationToken cancellationToken)
    {
        if (heartbeat is null || string.IsNullOrWhiteSpace(heartbeat.TagPath))
            return;

        _staleMonitor?.Dispose();
        _staleMonitor = new StaleTagMonitor(TimeSpan.FromSeconds(heartbeat.MaxSilenceSeconds));
        _staleMonitor.Stale += () =>
        {
            _logger.LogWarning("OPC UA heartbeat tag '{Tag}' stale — no update in {Seconds}s",
                heartbeat.TagPath, heartbeat.MaxSilenceSeconds);
            RaiseDisconnected();
        };

        try
        {
            _heartbeatSubscriptionId = await SubscribeAsync(heartbeat.TagPath,
                (_, _) => _staleMonitor.OnValueReceived(), cancellationToken);
            _staleMonitor.Start();
            _logger.LogInformation("OPC UA heartbeat monitor started for '{Tag}' with {Seconds}s max silence",
                heartbeat.TagPath, heartbeat.MaxSilenceSeconds);
        }
        catch (Exception ex)
        {
            _logger.LogWarning(ex, "Failed to subscribe to heartbeat tag '{Tag}' — stale monitor not active",
                heartbeat.TagPath);
            _staleMonitor.Dispose();
            _staleMonitor = null;
        }
    }

    private void OnClientConnectionLost()
    {
        RaiseDisconnected();
    }

    public async Task DisconnectAsync(CancellationToken cancellationToken = default)
    {
        StopHeartbeatMonitor();
        if (_client != null)
        {
            _client.ConnectionLost -= OnClientConnectionLost;
            await _client.DisconnectAsync(cancellationToken);
            _status = ConnectionHealth.Disconnected;
            _logger.LogInformation("OPC UA disconnected from {Endpoint}", _endpointUrl);
        }
    }

    public async Task<string> SubscribeAsync(string tagPath, SubscriptionCallback callback, CancellationToken cancellationToken = default)
    {
        EnsureConnected();

        var subscriptionId = await _client!.CreateSubscriptionAsync(
            tagPath,
            (nodeId, value, timestamp, statusCode) =>
            {
                var quality = MapStatusCode(statusCode);
                callback(tagPath, new TagValue(value, quality, new DateTimeOffset(timestamp, TimeSpan.Zero)));
            },
            cancellationToken);

        _subscriptionHandles[subscriptionId] = tagPath;
        return subscriptionId;
    }

    public async Task UnsubscribeAsync(string subscriptionId, CancellationToken cancellationToken = default)
    {
        if (_client != null)
        {
            await _client.RemoveSubscriptionAsync(subscriptionId, cancellationToken);
            _subscriptionHandles.Remove(subscriptionId);
        }
    }

    public async Task<ReadResult> ReadAsync(string tagPath, CancellationToken cancellationToken = default)
    {
        EnsureConnected();

        try
        {
            var (value, timestamp, statusCode) = await _client!.ReadValueAsync(tagPath, cancellationToken);
            var quality = MapStatusCode(statusCode);

            if (quality == QualityCode.Bad)
                return new ReadResult(false, null, $"OPC UA read returned bad status: 0x{statusCode:X8}");

            return new ReadResult(true, new TagValue(value, quality, new DateTimeOffset(timestamp, TimeSpan.Zero)), null);
        }
        catch (Exception ex) when (ex is not OperationCanceledException)
        {
            _logger.LogWarning(ex, "OPC UA read failed for {TagPath} — connection may be lost", tagPath);
            RaiseDisconnected();
            throw;
        }
    }

    public async Task<IReadOnlyDictionary<string, ReadResult>> ReadBatchAsync(IEnumerable<string> tagPaths, CancellationToken cancellationToken = default)
    {
        // DataConnectionLayer-007: a single failing tag must not abort the whole batch.
        // ReadAsync re-throws non-cancellation exceptions; catch them per tag and record
        // a failed ReadResult so the caller receives a complete result map for every
        // requested tag (the ReadResult shape already carries per-tag Success/error).
        var results = new Dictionary<string, ReadResult>();
        foreach (var tagPath in tagPaths)
        {
            try
            {
                results[tagPath] = await ReadAsync(tagPath, cancellationToken);
            }
            catch (OperationCanceledException)
            {
                // Cancellation aborts the whole batch — propagate it.
                throw;
            }
            catch (Exception ex)
            {
                results[tagPath] = new ReadResult(false, null, ex.Message);
            }
        }
        return results;
    }

    public async Task<WriteResult> WriteAsync(string tagPath, object? value, CancellationToken cancellationToken = default)
    {
        EnsureConnected();

        var statusCode = await _client!.WriteValueAsync(tagPath, value, cancellationToken);
        if (statusCode != 0)
            return new WriteResult(false, $"OPC UA write failed with status: 0x{statusCode:X8}");

        return new WriteResult(true, null);
    }

    public async Task<IReadOnlyDictionary<string, WriteResult>> WriteBatchAsync(IDictionary<string, object?> values, CancellationToken cancellationToken = default)
    {
        var results = new Dictionary<string, WriteResult>();
        foreach (var (tagPath, value) in values)
        {
            results[tagPath] = await WriteAsync(tagPath, value, cancellationToken);
        }
        return results;
    }

    public async Task<bool> WriteBatchAndWaitAsync(
        IDictionary<string, object?> values, string flagPath, object? flagValue,
        string responsePath, object? responseValue, TimeSpan timeout,
        CancellationToken cancellationToken = default)
    {
        // Write all values including the flag
        var allValues = new Dictionary<string, object?>(values) { [flagPath] = flagValue };
        var writeResults = await WriteBatchAsync(allValues, cancellationToken);

        if (writeResults.Values.Any(r => !r.Success))
            return false;

        // Poll for response value within timeout
        var deadline = DateTimeOffset.UtcNow + timeout;
        while (DateTimeOffset.UtcNow < deadline)
        {
            cancellationToken.ThrowIfCancellationRequested();
            var readResult = await ReadAsync(responsePath, cancellationToken);
            if (readResult.Success && readResult.Value != null && Equals(readResult.Value.Value, responseValue))
                return true;

            await Task.Delay(100, cancellationToken);
        }

        return false;
    }

    private void StopHeartbeatMonitor()
    {
        _staleMonitor?.Dispose();
        _staleMonitor = null;
        _heartbeatSubscriptionId = null;
    }

    public async ValueTask DisposeAsync()
    {
        StopHeartbeatMonitor();
        if (_client != null)
        {
            _client.ConnectionLost -= OnClientConnectionLost;
            await _client.DisposeAsync();
            _client = null;
        }
        _status = ConnectionHealth.Disconnected;
    }

    private void EnsureConnected()
    {
        if (_client == null || !_client.IsConnected)
            throw new InvalidOperationException("OPC UA client is not connected.");
    }

    /// <summary>
    /// Marks the connection as disconnected and fires the Disconnected event once.
    /// Thread-safe: only the first caller triggers the event.
    /// </summary>
    private void RaiseDisconnected()
    {
        if (_disconnectFired) return;
        _disconnectFired = true;
        _status = ConnectionHealth.Disconnected;
        _logger.LogWarning("OPC UA connection to {Endpoint} lost", _endpointUrl);
        Disconnected?.Invoke();
    }

    /// <summary>
    /// Maps OPC UA StatusCode to QualityCode.
    /// StatusCode 0 = Good, high bit set = Bad, otherwise Uncertain.
    /// </summary>
    private static QualityCode MapStatusCode(uint statusCode)
    {
        if (statusCode == 0) return QualityCode.Good;
        if ((statusCode & 0x80000000) != 0) return QualityCode.Bad;
        return QualityCode.Uncertain;
    }
}
