using Opc.Ua;
using Opc.Ua.Server;
using ZB.MOM.WW.OtOpcUa.Commons.OpcUa;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;

namespace ZB.MOM.WW.OtOpcUa.OpcUaServer;

/// <summary>
///     <see cref="StandardServer"/> subclass that wires in the v2 <see cref="OtOpcUaNodeManager"/>.
///     Exposes the live node manager after start so callers (<see cref="OpcUaApplicationHost"/>,
///     the fused Host's DI binding) can wrap it in a <see cref="SdkAddressSpaceSink"/> and hand
///     it to <c>OpcUaPublishActor</c>.
/// </summary>
public sealed class OtOpcUaSdkServer : StandardServer
{
    private OtOpcUaNodeManager? _otOpcUaNodeManager;

    /// <summary>The custom node manager once <c>StartAsync</c> has called
    /// <see cref="CreateMasterNodeManager"/>. Null until the SDK has bootstrapped.</summary>
    public OtOpcUaNodeManager? NodeManager => _otOpcUaNodeManager;

    /// <summary>
    ///     Wire the reverse-path sink for inbound Part 9 alarm method calls onto the created
    ///     <see cref="OtOpcUaNodeManager"/>. The host calls this after start with a non-blocking
    ///     <c>mediator.Tell</c> that publishes each <see cref="AlarmCommand"/> onto the
    ///     <c>alarm-commands</c> DistributedPubSub topic. No-op (returns <c>false</c>) when the node
    ///     manager has not been created yet, so the caller can detect a too-early call.
    /// </summary>
    /// <param name="router">The router invoked by the condition handlers once the <c>AlarmAck</c>
    /// gate passes; may be <c>null</c> to clear it.</param>
    /// <returns><c>true</c> when the router was set on a live node manager; <c>false</c> when no node
    /// manager exists yet.</returns>
    public bool SetAlarmCommandRouter(Action<AlarmCommand>? router)
    {
        if (_otOpcUaNodeManager is null) return false;
        _otOpcUaNodeManager.AlarmCommandRouter = router;
        return true;
    }

    /// <summary>
    ///     Wire the reverse-path router for inbound Part 9 Acknowledge of a NATIVE (driver-fed, e.g. Galaxy)
    ///     condition onto the created <see cref="OtOpcUaNodeManager"/>. The host calls this after start with a
    ///     non-blocking router that Tells a <c>DriverHostActor.RouteNativeAlarmAck</c> into the local
    ///     <c>DriverHostActor</c>, which (Primary-gated) routes the acknowledge to the owning driver child.
    ///     Native conditions are not owned by the scripted-alarm engine, so the node manager branches their
    ///     inbound Acknowledge to this seam instead of the scripted <see cref="SetAlarmCommandRouter"/> path.
    ///     Passing <c>null</c> clears it. No-op (returns <c>false</c>) when the node manager has not been
    ///     created yet, so the caller can detect a too-early call (mirrors <see cref="SetAlarmCommandRouter"/>).
    /// </summary>
    /// <param name="router">The router invoked by the native condition's Acknowledge handler once the
    /// <c>AlarmAck</c> gate passes; may be <c>null</c> to clear it.</param>
    /// <returns><c>true</c> when the router was set on a live node manager; <c>false</c> when no node
    /// manager exists yet.</returns>
    public bool SetNativeAlarmAckRouter(Action<NativeAlarmAck>? router)
    {
        if (_otOpcUaNodeManager is null) return false;
        _otOpcUaNodeManager.NativeAlarmAckRouter = router;
        return true;
    }

    /// <summary>
    ///     Wire the reverse-path gateway for inbound operator writes to writable equipment-tag nodes onto the
    ///     created <see cref="OtOpcUaNodeManager"/>. The host calls this after start with an
    ///     <c>ActorNodeWriteGateway</c> whose <c>WriteAsync</c> kicks off a bounded <c>Ask</c> of the local
    ///     <c>DriverHostActor</c> (<c>RouteNodeWrite</c>); the node manager calls it fire-and-forget and uses
    ///     the resolved <c>NodeWriteOutcome</c> to self-correct the node on a failed device write (mirrors
    ///     <see cref="SetAlarmCommandRouter"/>). Passing <c>null</c> restores the
    ///     <c>NullOpcUaNodeWriteGateway</c> default (writes unavailable). No-op (returns <c>false</c>) when the
    ///     node manager has not been created yet, so the caller can detect a too-early call.
    /// </summary>
    /// <param name="gateway">The gateway invoked by the writable node's <c>OnWriteValue</c> handler once the
    /// <c>WriteOperate</c> gate passes; may be <c>null</c> to restore the Null default.</param>
    /// <returns><c>true</c> when the gateway was set on a live node manager; <c>false</c> when no node
    /// manager exists yet.</returns>
    public bool SetNodeWriteGateway(IOpcUaNodeWriteGateway? gateway)
    {
        if (_otOpcUaNodeManager is null) return false;
        // The NodeWriteGateway setter null-coalesces to the Null default, so a null gateway is intentional
        // (restores "writes unavailable"); forgive the nullable-in here.
        _otOpcUaNodeManager.NodeWriteGateway = gateway!;
        return true;
    }

    /// <summary>
    ///     Wire the server-side HistoryRead backend (the <see cref="IHistorianDataSource"/> the node
    ///     manager's HistoryRead overrides block-bridge to) onto the created
    ///     <see cref="OtOpcUaNodeManager"/>. The host calls this after start with the DI-resolved source —
    ///     the <c>NullHistorianDataSource</c> default (GoodNoData-empty reads) or the configured HistorianGateway
    ///     read client. Passing <c>null</c> restores the Null default (the property setter null-coalesces),
    ///     i.e. "no historian". No-op (returns <c>false</c>) when the node manager has not been created yet,
    ///     so the caller can detect a too-early call (mirrors <see cref="SetNodeWriteGateway"/>).
    /// </summary>
    /// <param name="source">The read backend invoked by the node manager's HistoryRead overrides; may be
    /// <c>null</c> to restore the Null default (GoodNoData-empty reads).</param>
    /// <returns><c>true</c> when the source was set on a live node manager; <c>false</c> when no node
    /// manager exists yet.</returns>
    public bool SetHistorianDataSource(IHistorianDataSource? source)
    {
        if (_otOpcUaNodeManager is null) return false;
        // The HistorianDataSource setter null-coalesces to the Null default, so a null source is intentional
        // (restores GoodNoData-empty reads); forgive the nullable-in here.
        _otOpcUaNodeManager.HistorianDataSource = source!;
        return true;
    }

    /// <inheritdoc />
    protected override MasterNodeManager CreateMasterNodeManager(
        IServerInternal server, ApplicationConfiguration configuration)
    {
        _otOpcUaNodeManager = new OtOpcUaNodeManager(server, configuration);
        return new MasterNodeManager(server, configuration, dynamicNamespaceUri: null, _otOpcUaNodeManager);
    }
}
