using System.Collections.Concurrent;
using System.Security.Cryptography.X509Certificates;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Opc.Ua;
using Opc.Ua.Client;
using Opc.Ua.Configuration;

namespace ScadaLink.DataConnectionLayer.Adapters;

/// <summary>
/// Real OPC UA client implementation using the OPC Foundation .NET Standard Library.
/// Wraps Session, Subscription, and MonitoredItem for tag subscriptions.
/// </summary>
public class RealOpcUaClient : IOpcUaClient
{
    private ISession? _session;
    private Subscription? _subscription;

    // DataConnectionLayer-003: these maps are read from the OPC Foundation SDK's
    // internal publish threads (the MonitoredItem.Notification handler reads
    // _callbacks) concurrently with subscribe/disconnect mutations that run on
    // thread-pool threads. Plain Dictionary access during a concurrent resize or
    // Clear() is undefined behaviour, so they must be ConcurrentDictionary.
    private readonly ConcurrentDictionary<string, MonitoredItem> _monitoredItems = new();
    private readonly ConcurrentDictionary<string, Action<string, object?, DateTime, uint>> _callbacks = new();
    private volatile bool _connectionLostFired;
    private OpcUaConnectionOptions _options = new();
    private readonly OpcUaGlobalOptions _globalOptions;
    private readonly ILogger<RealOpcUaClient> _logger;

    public RealOpcUaClient(OpcUaGlobalOptions? globalOptions = null, ILogger<RealOpcUaClient>? logger = null)
    {
        _globalOptions = globalOptions ?? new OpcUaGlobalOptions();
        _logger = logger ?? NullLogger<RealOpcUaClient>.Instance;
    }

    public bool IsConnected => _session?.Connected ?? false;
    public event Action? ConnectionLost;

    public async Task ConnectAsync(string endpointUrl, OpcUaConnectionOptions? options = null, CancellationToken cancellationToken = default)
    {
        var opts = options ?? new OpcUaConnectionOptions();

        var preferredSecurityMode = opts.SecurityMode?.ToUpperInvariant() switch
        {
            "SIGN" => MessageSecurityMode.Sign,
            "SIGNANDENCRYPT" => MessageSecurityMode.SignAndEncrypt,
            _ => MessageSecurityMode.None
        };

        var appConfig = new ApplicationConfiguration
        {
            ApplicationName = string.IsNullOrWhiteSpace(_globalOptions.ApplicationName)
                ? "ScadaLink-DCL"
                : _globalOptions.ApplicationName,
            ApplicationType = ApplicationType.Client,
            SecurityConfiguration = new SecurityConfiguration
            {
                AutoAcceptUntrustedCertificates = opts.AutoAcceptUntrustedCerts,
                ApplicationCertificate = new CertificateIdentifier(),
                TrustedIssuerCertificates = new CertificateTrustList { StorePath = ResolveStorePath(_globalOptions.TrustedIssuerStorePath, "issuers") },
                TrustedPeerCertificates = new CertificateTrustList { StorePath = ResolveStorePath(_globalOptions.TrustedPeerStorePath, "trusted") },
                RejectedCertificateStore = new CertificateTrustList { StorePath = ResolveStorePath(_globalOptions.RejectedCertificateStorePath, "rejected") }
            },
            ClientConfiguration = new ClientConfiguration { DefaultSessionTimeout = opts.SessionTimeoutMs },
            TransportQuotas = new TransportQuotas { OperationTimeout = opts.OperationTimeoutMs }
        };

        await appConfig.ValidateAsync(ApplicationType.Client);
        if (opts.AutoAcceptUntrustedCerts)
        {
            // DataConnectionLayer-012: this accepts ANY server certificate, defeating
            // certificate trust enforcement. Surface a prominent warning so an operator
            // who has opted in is aware of the man-in-the-middle exposure on the link.
            _logger.LogWarning(
                "OPC UA connection to {Endpoint} has AutoAcceptUntrustedCerts enabled — every " +
                "server certificate is accepted unconditionally. This defeats Sign / " +
                "SignAndEncrypt protection against a man-in-the-middle.", endpointUrl);
            appConfig.CertificateValidator.CertificateValidation += (_, e) => e.Accept = true;
        }

        // Discover endpoints from the server, pick the preferred security mode
        EndpointDescription? endpoint;
        try
        {
#pragma warning disable CS0618
            using var discoveryClient = DiscoveryClient.Create(new Uri(endpointUrl));
#pragma warning restore CS0618
#pragma warning disable CS0618
            var endpoints = discoveryClient.GetEndpoints(null);
#pragma warning restore CS0618
            endpoint = endpoints
                .Where(e => e.SecurityMode == preferredSecurityMode)
                .FirstOrDefault() ?? endpoints.FirstOrDefault();
        }
        catch
        {
            // Fallback: construct endpoint description manually
            endpoint = new EndpointDescription(endpointUrl);
        }

        var endpointConfig = EndpointConfiguration.Create(appConfig);
        var configuredEndpoint = new ConfiguredEndpoint(null, endpoint, endpointConfig);

#pragma warning disable CS0618 // Allow obsolete DefaultSessionFactory constructor for compatibility
        var sessionFactory = new DefaultSessionFactory();
#pragma warning restore CS0618
        var userIdentity = BuildUserIdentity(opts.UserIdentity);
        _session = await sessionFactory.CreateAsync(
            appConfig, configuredEndpoint, false,
            "ScadaLink-DCL-Session", (uint)opts.SessionTimeoutMs, userIdentity, null, cancellationToken);

        // Detect server going offline via keep-alive failures
        _connectionLostFired = false;
        _session.KeepAlive += OnSessionKeepAlive;

        // Store options for monitored item creation
        _options = opts;

        // Create a default subscription for all monitored items
        _subscription = new Subscription(_session.DefaultSubscription)
        {
            DisplayName = opts.SubscriptionDisplayName,
            Priority = opts.SubscriptionPriority,
            PublishingEnabled = true,
            PublishingInterval = opts.PublishingIntervalMs,
            KeepAliveCount = (uint)opts.KeepAliveCount,
            LifetimeCount = (uint)opts.LifetimeCount,
            MaxNotificationsPerPublish = (uint)opts.MaxNotificationsPerPublish
        };

        _session.AddSubscription(_subscription);
        await _subscription.CreateAsync(cancellationToken);
    }

    public async Task DisconnectAsync(CancellationToken cancellationToken = default)
    {
        if (_subscription != null)
        {
            await _subscription.DeleteAsync(true);
            _subscription = null;
        }
        if (_session != null)
        {
            _session.KeepAlive -= OnSessionKeepAlive;
            await _session.CloseAsync(cancellationToken);
            _session = null;
        }
        _monitoredItems.Clear();
        _callbacks.Clear();
    }

    public async Task<string> CreateSubscriptionAsync(
        string nodeId, Action<string, object?, DateTime, uint> onValueChanged,
        CancellationToken cancellationToken = default)
    {
        if (_subscription == null || _session == null)
            throw new InvalidOperationException("Not connected.");

        var handle = Guid.NewGuid().ToString();
        var monitoredItem = new MonitoredItem(_subscription.DefaultItem)
        {
            DisplayName = nodeId,
            StartNodeId = nodeId,
            AttributeId = Attributes.Value,
            SamplingInterval = _options.SamplingIntervalMs,
            QueueSize = (uint)_options.QueueSize,
            DiscardOldest = _options.DiscardOldest,
            Filter = BuildDataChangeFilter(_options.Deadband)
        };

        _callbacks[handle] = onValueChanged;

        monitoredItem.Notification += (item, e) =>
        {
            if (e.NotificationValue is MonitoredItemNotification notification)
            {
                var value = notification.Value?.Value;
                var timestamp = notification.Value?.SourceTimestamp ?? DateTime.UtcNow;
                var statusCode = notification.Value?.StatusCode.Code ?? 0;

                if (_callbacks.TryGetValue(handle, out var cb))
                {
                    cb(nodeId, value, timestamp, statusCode);
                }
            }
        };

        _subscription.AddItem(monitoredItem);
        await _subscription.ApplyChangesAsync(cancellationToken);

        _monitoredItems[handle] = monitoredItem;
        return handle;
    }

    public async Task RemoveSubscriptionAsync(string subscriptionHandle, CancellationToken cancellationToken = default)
    {
        if (_subscription != null && _monitoredItems.TryGetValue(subscriptionHandle, out var item))
        {
            _subscription.RemoveItem(item);
            await _subscription.ApplyChangesAsync(cancellationToken);
            _monitoredItems.TryRemove(subscriptionHandle, out _);
            _callbacks.TryRemove(subscriptionHandle, out _);
        }
    }

    public async Task<(object? Value, DateTime SourceTimestamp, uint StatusCode)> ReadValueAsync(
        string nodeId, CancellationToken cancellationToken = default)
    {
        if (_session == null) throw new InvalidOperationException("Not connected.");

        var readValue = new ReadValueId
        {
            NodeId = nodeId,
            AttributeId = Attributes.Value
        };

        var response = await _session.ReadAsync(
            null, 0, MapTimestampsToReturn(_options.TimestampsToReturn),
            new ReadValueIdCollection { readValue }, cancellationToken);

        var result = response.Results[0];
        return (result.Value, result.SourceTimestamp, result.StatusCode.Code);
    }

    public async Task<uint> WriteValueAsync(string nodeId, object? value, CancellationToken cancellationToken = default)
    {
        if (_session == null) throw new InvalidOperationException("Not connected.");

        var writeValue = new WriteValue
        {
            NodeId = nodeId,
            AttributeId = Attributes.Value,
            Value = new DataValue(new Variant(value))
        };

        var response = await _session.WriteAsync(
            null, new WriteValueCollection { writeValue }, cancellationToken);

        return response.Results[0].Code;
    }

    /// <summary>
    /// Called by the OPC UA SDK when a keep-alive response arrives (or fails).
    /// When CurrentState is bad, the server is unreachable.
    /// </summary>
    private void OnSessionKeepAlive(ISession session, KeepAliveEventArgs e)
    {
        if (ServiceResult.IsBad(e.Status))
        {
            if (_connectionLostFired) return;
            _connectionLostFired = true;
            ConnectionLost?.Invoke();
        }
    }

    public async ValueTask DisposeAsync()
    {
        await DisconnectAsync();
    }

    private static UserIdentity? BuildUserIdentity(OpcUaUserIdentityOptions? options)
    {
        if (options is null) return null;
        return options.TokenType.ToUpperInvariant() switch
        {
            "USERNAMEPASSWORD" => new UserIdentity(
                options.Username,
                System.Text.Encoding.UTF8.GetBytes(options.Password ?? "")),
            "X509CERTIFICATE" => new UserIdentity(
                X509CertificateLoader.LoadPkcs12FromFile(
                    options.CertificatePath, options.CertificatePassword)),
            _ => null
        };
    }

    private static MonitoringFilter? BuildDataChangeFilter(OpcUaDeadbandOptions? deadband)
    {
        if (deadband is null) return null;
        var deadbandType = deadband.Type.ToUpperInvariant() switch
        {
            "PERCENT" => DeadbandType.Percent,
            _ => DeadbandType.Absolute
        };
        return new DataChangeFilter
        {
            Trigger = DataChangeTrigger.StatusValue,
            DeadbandType = (uint)deadbandType,
            DeadbandValue = deadband.Value
        };
    }

    private static TimestampsToReturn MapTimestampsToReturn(string mode) =>
        mode.ToUpperInvariant() switch
        {
            "SERVER" => TimestampsToReturn.Server,
            "BOTH" => TimestampsToReturn.Both,
            _ => TimestampsToReturn.Source
        };

    private static string ResolveStorePath(string configured, string fallbackLeaf) =>
        string.IsNullOrWhiteSpace(configured)
            ? Path.Combine(Path.GetTempPath(), "ScadaLink", "pki", fallbackLeaf)
            : configured;
}

/// <summary>
/// Factory that creates real OPC UA client instances using the OPC Foundation SDK.
/// </summary>
public class RealOpcUaClientFactory : IOpcUaClientFactory
{
    private readonly OpcUaGlobalOptions _globalOptions;

    public RealOpcUaClientFactory() : this(new OpcUaGlobalOptions()) { }
    public RealOpcUaClientFactory(OpcUaGlobalOptions globalOptions)
    {
        _globalOptions = globalOptions;
    }

    public IOpcUaClient Create() => new RealOpcUaClient(_globalOptions);
}
