using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using ZB.MOM.WW.OtOpcUa.Commons.OpcUa;
using ZB.MOM.WW.OtOpcUa.OpcUaServer;
using ZB.MOM.WW.OtOpcUa.OpcUaServer.Security;

namespace ZB.MOM.WW.OtOpcUa.Host.OpcUa;

/// <summary>
///     Owns the OPC UA SDK lifecycle on driver-role hosts. Reads
///     <see cref="OpcUaApplicationHostOptions"/> from the <c>OpcUa</c> config section, boots
///     an <see cref="OtOpcUaSdkServer"/> through <see cref="OpcUaApplicationHost"/>, then
///     swaps a real <see cref="SdkAddressSpaceSink"/> into the
///     <see cref="DeferredAddressSpaceSink"/> singleton so <c>OpcUaPublishActor</c>'s writes
///     start landing in the real address space.
///
///     Tests boot the OPC UA server directly via <see cref="OpcUaApplicationHost"/>; this
///     hosted service is the production wiring.
/// </summary>
public sealed class OtOpcUaServerHostedService : IHostedService, IAsyncDisposable
{
    private readonly OpcUaApplicationHostOptions _options;
    private readonly DeferredAddressSpaceSink _deferredSink;
    private readonly DeferredServiceLevelPublisher _deferredServiceLevel;
    private readonly IOpcUaUserAuthenticator _userAuthenticator;
    private readonly ILoggerFactory _loggerFactory;
    private readonly ILogger<OtOpcUaServerHostedService> _logger;

    private OpcUaApplicationHost? _appHost;
    private OtOpcUaSdkServer? _server;

    /// <summary>
    /// Initializes a new instance of the OtOpcUaServerHostedService class.
    /// </summary>
    /// <param name="options">The validated OPC UA host options (bound from the <c>OpcUa</c> section and validated at startup via <c>ValidateOnStart</c>).</param>
    /// <param name="deferredSink">The deferred address space sink that receives the real sink once the server is ready.</param>
    /// <param name="deferredServiceLevel">The deferred service level publisher that receives the real publisher once the server is ready.</param>
    /// <param name="userAuthenticator">The OPC UA user authenticator.</param>
    /// <param name="loggerFactory">The logger factory for creating loggers.</param>
    public OtOpcUaServerHostedService(
        IOptions<OpcUaApplicationHostOptions> options,
        DeferredAddressSpaceSink deferredSink,
        DeferredServiceLevelPublisher deferredServiceLevel,
        IOpcUaUserAuthenticator userAuthenticator,
        ILoggerFactory loggerFactory)
    {
        _options = options.Value;
        _deferredSink = deferredSink;
        _deferredServiceLevel = deferredServiceLevel;
        _userAuthenticator = userAuthenticator;
        _loggerFactory = loggerFactory;
        _logger = loggerFactory.CreateLogger<OtOpcUaServerHostedService>();
    }

    /// <summary>
    /// Starts the OPC UA server asynchronously.
    /// </summary>
    /// <param name="cancellationToken">Cancellation token.</param>
    public async Task StartAsync(CancellationToken cancellationToken)
    {
        _server = new OtOpcUaSdkServer();
        _appHost = new OpcUaApplicationHost(
            _options,
            _loggerFactory.CreateLogger<OpcUaApplicationHost>(),
            _userAuthenticator);

        try
        {
            await _appHost.StartAsync(_server, cancellationToken).ConfigureAwait(false);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex,
                "OtOpcUaServerHostedService: SDK start failed; OpcUaPublishActor writes will continue to no-op");
            // Don't rethrow — the rest of the host (admin UI, driver actors, etc.) can still boot.
            // Operators see the failure via the logs + can correct config without a process bounce
            // of the whole binary.
            return;
        }

        if (_server.NodeManager is null)
        {
            _logger.LogWarning(
                "OtOpcUaServerHostedService: SDK reported started but NodeManager is null; sink stays Null");
            return;
        }

        _deferredSink.SetSink(new SdkAddressSpaceSink(_server.NodeManager));

        // ServiceLevel publisher needs IServerInternal — only available after Start.
        if (_server.CurrentInstance is { } serverInternal)
        {
            _deferredServiceLevel.SetInner(new SdkServiceLevelPublisher(
                serverInternal,
                _loggerFactory.CreateLogger<SdkServiceLevelPublisher>()));
        }

        _logger.LogInformation("OtOpcUaServerHostedService: SDK started, address-space + ServiceLevel sinks bound");
    }

    /// <summary>
    /// Stops the OPC UA server asynchronously.
    /// </summary>
    /// <param name="cancellationToken">Cancellation token.</param>
    public Task StopAsync(CancellationToken cancellationToken)
    {
        // Revert to Null adapters so any in-flight writes from a poison-pilled actor don't hit a
        // half-disposed NodeManager.
        _deferredSink.SetSink(null);
        _deferredServiceLevel.SetInner(null);
        return Task.CompletedTask;
    }

    /// <summary>
    /// Disposes the hosted service and its resources asynchronously.
    /// </summary>
    public async ValueTask DisposeAsync()
    {
        if (_appHost is not null) await _appHost.DisposeAsync();
    }
}
