using Microsoft.Extensions.Logging;
using ScadaLink.Commons.Interfaces.Protocol;
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();

    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)
    {
        _endpointUrl = connectionDetails.TryGetValue("endpoint", out var url)
            ? url
            : connectionDetails.TryGetValue("EndpointUrl", out var url2)
                ? url2
                : "opc.tcp://localhost:4840";

        var options = new OpcUaConnectionOptions(
            SessionTimeoutMs: ParseInt(connectionDetails, "SessionTimeoutMs", 60000),
            OperationTimeoutMs: ParseInt(connectionDetails, "OperationTimeoutMs", 15000),
            PublishingIntervalMs: ParseInt(connectionDetails, "PublishingIntervalMs", 1000),
            KeepAliveCount: ParseInt(connectionDetails, "KeepAliveCount", 10),
            LifetimeCount: ParseInt(connectionDetails, "LifetimeCount", 30),
            MaxNotificationsPerPublish: ParseInt(connectionDetails, "MaxNotificationsPerPublish", 100),
            SamplingIntervalMs: ParseInt(connectionDetails, "SamplingIntervalMs", 1000),
            QueueSize: ParseInt(connectionDetails, "QueueSize", 10),
            SecurityMode: connectionDetails.TryGetValue("SecurityMode", out var secMode) ? secMode : "None",
            AutoAcceptUntrustedCerts: ParseBool(connectionDetails, "AutoAcceptUntrustedCerts", true));

        _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);
    }

    internal static int ParseInt(IDictionary<string, string> d, string key, int defaultValue)
    {
        return d.TryGetValue(key, out var str) && int.TryParse(str, out var val) ? val : defaultValue;
    }

    internal static bool ParseBool(IDictionary<string, string> d, string key, bool defaultValue)
    {
        return d.TryGetValue(key, out var str) && bool.TryParse(str, out var val) ? val : defaultValue;
    }

    private void OnClientConnectionLost()
    {
        RaiseDisconnected();
    }

    public async Task DisconnectAsync(CancellationToken cancellationToken = default)
    {
        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)
    {
        var results = new Dictionary<string, ReadResult>();
        foreach (var tagPath in tagPaths)
        {
            results[tagPath] = await ReadAsync(tagPath, cancellationToken);
        }
        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;
    }

    public async ValueTask DisposeAsync()
    {
        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;
    }
}
