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

/// <summary>
///     Abstraction over the OPC UA SDK's address space. <c>OpcUaPublishActor</c> consumes this
///     so the Runtime project doesn't reference <c>Opc.Ua.Server</c> directly — production
///     binds a real SDK-backed sink in the fused Host's wiring, dev/Mac binds the
///     <see cref="NullOpcUaAddressSpaceSink"/> no-op.
/// </summary>
public interface IOpcUaAddressSpaceSink
{
    /// <summary>Write a Variable node's current value + quality + source timestamp.</summary>
    /// <param name="nodeId">The OPC UA node ID of the variable.</param>
    /// <param name="value">The value to write.</param>
    /// <param name="quality">The quality status of the value.</param>
    /// <param name="sourceTimestampUtc">The source timestamp in UTC.</param>
    /// <param name="realm">Which of the two v3 namespaces (<see cref="AddressSpaceRealm.Raw"/> device tree or
    /// <see cref="AddressSpaceRealm.Uns"/> equipment tree) the <paramref name="nodeId"/> lives in — the sink
    /// resolves the namespace index from the realm rather than parsing it out of the id string. WP3's fan-out
    /// calls this once per NodeId (raw + every referencing UNS node) with the matching realm.</param>
    void WriteValue(string nodeId, object? value, OpcUaQuality quality, DateTime sourceTimestampUtc, AddressSpaceRealm realm);

    /// <summary>Write an alarm-condition's full Part 9 state. When a real condition node has been
    /// materialised for <paramref name="alarmNodeId"/> via <see cref="MaterialiseAlarmCondition"/>,
    /// this projects the whole <see cref="AlarmConditionSnapshot"/>
    /// (Enabled/Active/Acked/Confirmed/Shelving/Severity/Message) onto it and recomputes Retain;
    /// otherwise it falls back to the legacy two-element <c>[Active, Acknowledged]</c> placeholder
    /// variable. A materialised condition also fires a Part 9 condition event on each transition (T16)
    /// so subscribed clients receive the alarm event, not just the changed attributes.</summary>
    /// <param name="alarmNodeId">The OPC UA node ID of the alarm (== ScriptedAlarmId for materialised conditions).</param>
    /// <param name="state">The full condition state to project onto the node.</param>
    /// <param name="sourceTimestampUtc">The source timestamp in UTC.</param>
    /// <param name="realm">The namespace realm <paramref name="alarmNodeId"/> lives in (must match the realm
    /// the condition was materialised under).</param>
    void WriteAlarmCondition(string alarmNodeId, AlarmConditionSnapshot state, DateTime sourceTimestampUtc, AddressSpaceRealm realm);

    /// <summary>#477 — annotate a materialised condition's source-data quality OUT OF BAND from any alarm
    /// transition (used by the driver-connectivity path: comms lost → <see cref="OpcUaQuality.Bad"/>,
    /// restored → <see cref="OpcUaQuality.Good"/>). Sets ONLY the condition's Quality — never
    /// Active/Acked/Severity/Retain (a comms-lost active alarm stays active) — and fires one Part 9 event
    /// only on a quality-bucket change. A no-op for an unmaterialised / non-condition node.</summary>
    /// <param name="alarmNodeId">The condition node id (RawPath for a native alarm).</param>
    /// <param name="quality">The source-data quality to annotate.</param>
    /// <param name="sourceTimestampUtc">The connectivity transition timestamp in UTC.</param>
    /// <param name="realm">The namespace realm the condition was materialised under.</param>
    void WriteAlarmQuality(string alarmNodeId, OpcUaQuality quality, DateTime sourceTimestampUtc, AddressSpaceRealm realm);

    /// <summary>
    ///     Materialise a real OPC UA Part 9 alarm-condition node under its equipment folder so clients
    ///     can browse it as a proper condition (with basic Active/Ack state). The node id equals the
    ///     alarm node id (the ScriptedAlarmId) so subsequent <see cref="WriteAlarmCondition"/> calls update
    ///     it. Used by <c>AddressSpaceApplier.MaterialiseScriptedAlarms</c>. Idempotent.
    /// </summary>
    /// <param name="alarmNodeId">The alarm node ID (== ScriptedAlarmId); becomes the condition's NodeId.</param>
    /// <param name="equipmentNodeId">The equipment folder node ID the condition parents under.</param>
    /// <param name="displayName">Human-readable condition name (BrowseName / DisplayName / Message).</param>
    /// <param name="alarmType">Domain alarm type — mapped to the SDK condition subtype by the sink.</param>
    /// <param name="severity">Domain severity (OPC UA 1..1000 scale); mapped to the SDK severity buckets.</param>
    /// <param name="isNative">When true this is a driver-fed (native, e.g. Galaxy) equipment-tag alarm; when
    /// false (default) it is a scripted alarm. The node manager tracks native condition node ids so a later
    /// task can route a native condition's inbound Acknowledge to the driver instead of the scripted engine.</param>
    /// <param name="realm">The namespace realm the condition (and its parent folder <paramref name="equipmentNodeId"/>)
    /// live in.</param>
    void MaterialiseAlarmCondition(string alarmNodeId, string equipmentNodeId, string displayName, string alarmType, int severity, AddressSpaceRealm realm, bool isNative = false);

    /// <summary>
    ///     v3 Batch 4 (multi-notifier native alarms) — wire an already-materialised native alarm condition
    ///     (<paramref name="alarmNodeId"/>, materialised at the raw tag via <see cref="MaterialiseAlarmCondition"/>
    ///     with ConditionId = the RawPath) as an event notifier of EACH referencing equipment's UNS folder, so
    ///     the condition's <b>single</b> <c>ReportEvent</c> fans one event to every referencing equipment root
    ///     WITHOUT re-reporting per root (distinct EventIds would break Server-object dedup + Part 9 ack
    ///     correlation). Per folder the sink wires the SDK's bidirectional notifier pattern
    ///     (<c>alarm.AddNotifier(isInverse:true, folder)</c> + <c>folder.AddNotifier(isInverse:false, alarm)</c>)
    ///     and promotes the folder to an event notifier. <b>Idempotent</b> (a re-wire of the same pair updates,
    ///     never duplicates); a <b>missing endpoint is a no-op</b> (logged, never thrown) so a mid-rebuild race
    ///     can't fault a deploy. The sink tracks each wired pair so a later rebuild / condition-removal /
    ///     equipment-subtree-removal tears the notifier down bidirectionally (no inverse-notifier entry leaks
    ///     across redeploys).
    /// </summary>
    /// <param name="alarmNodeId">The native alarm condition's node id (== the backing tag's RawPath).</param>
    /// <param name="alarmRealm">The namespace realm the condition lives in (Raw for native alarms).</param>
    /// <param name="notifierFolderNodeIds">The equipment folder node ids (their <c>s=</c> ids) to wire as
    /// extra event-notifier roots for this condition. An unknown / not-yet-materialised folder id is skipped.</param>
    /// <param name="notifierFolderRealm">The namespace realm the notifier folders live in (Uns for equipment folders).</param>
    void WireAlarmNotifiers(string alarmNodeId, AddressSpaceRealm alarmRealm,
                            IReadOnlyList<string> notifierFolderNodeIds, AddressSpaceRealm notifierFolderRealm);

    /// <summary>
    ///     Ensure a folder node exists under the given parent. Used by <c>AddressSpaceApplier</c> to
    ///     materialise the UNS Area/Line/Equipment hierarchy in the address space. When
    ///     <paramref name="parentNodeId"/> is null the folder is parented under the namespace
    ///     root. Idempotent: calling twice with the same id is safe.
    /// </summary>
    /// <param name="folderNodeId">The OPC UA node ID for the folder.</param>
    /// <param name="parentNodeId">The parent folder node ID, or null for namespace root.</param>
    /// <param name="displayName">The display name for the folder.</param>
    /// <param name="realm">The namespace realm the folder (and its <paramref name="parentNodeId"/>) live in.</param>
    void EnsureFolder(string folderNodeId, string? parentNodeId, string displayName, AddressSpaceRealm realm);

    /// <summary>
    ///     Ensure a Variable node exists at <paramref name="variableNodeId"/>, parented under
    ///     <paramref name="parentFolderNodeId"/> (or the namespace root when null). Created with
    ///     Bad quality + null value; subsequent <see cref="WriteValue"/> calls update both.
    ///     Used by <c>AddressSpaceApplier</c> to materialise equipment-namespace tags ahead of any
    ///     driver-side subscribe so OPC UA clients can browse them. Idempotent.
    /// </summary>
    /// <param name="variableNodeId">The OPC UA node ID for the variable.</param>
    /// <param name="parentFolderNodeId">The parent folder node ID, or null for namespace root.</param>
    /// <param name="displayName">The display name for the variable.</param>
    /// <param name="dataType">OPC UA built-in type name ("Boolean" / "Int32" / "Float" / etc.).</param>
    /// <param name="writable">When true the node is created <c>CurrentReadWrite</c> (an authored
    /// ReadWrite equipment tag); when false it stays <c>CurrentRead</c> (read-only). Non-equipment-tag
    /// variables (folders' children, alarm placeholders) always pass <c>false</c>.</param>
    /// <param name="historianTagname">null ⇒ the variable is not historized; non-null ⇒ create it
    /// Historizing with the HistoryRead access bit and register the (already default-resolved)
    /// historian tagname.</param>
    /// <param name="isArray">When true the node is created as a 1-D array (<c>ValueRank=OneDimension</c>
    /// with <c>ArrayDimensions=[arrayLength]</c>); when false (default) it stays scalar
    /// (<c>ValueRank=Scalar</c>). Array elements share the scalar's base <paramref name="dataType"/> —
    /// rank + dimensions carry the array-ness.</param>
    /// <param name="arrayLength">The declared length of the 1-D array when <paramref name="isArray"/> is
    /// true; ignored for scalars. Null ⇒ length 0 (unbounded).</param>
    /// <param name="realm">The namespace realm the variable (and its <paramref name="parentFolderNodeId"/>) live
    /// in. A historized UNS reference node registers the SAME <paramref name="historianTagname"/> as its backing
    /// raw node — both NodeIds map to one tagname — so this call carries the shared tagname per realm.</param>
    void EnsureVariable(string variableNodeId, string? parentFolderNodeId, string displayName, string dataType, bool writable, AddressSpaceRealm realm, string? historianTagname = null, bool isArray = false, uint? arrayLength = null);

    /// <summary>
    ///     Tear down + repopulate the address space. Called by <c>OpcUaPublishActor</c> after a
    ///     successful deployment apply so the node manager reflects the new config. Idempotent.
    /// </summary>
    void RebuildAddressSpace();

    /// <summary>
    ///     Announce that nodes were added at runtime (discovered-node injection) under
    ///     <paramref name="affectedNodeId"/> so subscribed clients refresh their browse
    ///     (Part 3 GeneralModelChangeEvent, verb NodeAdded).
    /// </summary>
    /// <param name="affectedNodeId">The node under which discovered nodes were added.</param>
    /// <param name="realm">The namespace realm <paramref name="affectedNodeId"/> lives in.</param>
    void RaiseNodesAddedModelChange(string affectedNodeId, AddressSpaceRealm realm);

    /// <summary>
    ///     Add an OPC UA reference from an already-materialised source node to an already-materialised
    ///     target node, both realm-qualified. Used by <c>AddressSpaceApplier</c> to link each UNS reference
    ///     Variable (source, <see cref="AddressSpaceRealm.Uns"/>) to its backing Raw node (target,
    ///     <see cref="AddressSpaceRealm.Raw"/>) with an <c>Organizes</c> edge, so the UNS variable
    ///     browse-resolves to its raw source (and the raw node back via the inverse). The edge is wired
    ///     bidirectionally (forward on the source, inverse on the target) so browse resolves both ways.
    ///     <b>Idempotent</b> (an existing edge is not duplicated); a <b>missing endpoint is a no-op</b>
    ///     (logged, never thrown) so a mid-rebuild race can't fault a deploy.
    /// </summary>
    /// <param name="sourceNodeId">The source node's <c>s=</c> id (the referencing node — e.g. the UNS variable).</param>
    /// <param name="sourceRealm">The namespace realm <paramref name="sourceNodeId"/> lives in.</param>
    /// <param name="targetNodeId">The target node's <c>s=</c> id (the referenced node — e.g. the backing Raw node).</param>
    /// <param name="targetRealm">The namespace realm <paramref name="targetNodeId"/> lives in.</param>
    /// <param name="referenceType">The hierarchical reference type name — <c>Organizes</c> (default),
    /// <c>HasComponent</c>, or <c>HasProperty</c>; unknown names fall back to <c>Organizes</c>.</param>
    void AddReference(string sourceNodeId, AddressSpaceRealm sourceRealm,
                      string targetNodeId, AddressSpaceRealm targetRealm,
                      string referenceType = "Organizes");
}

/// <summary>OPC UA status code projection — Good / Uncertain / Bad. Real SDK has finer-grained
/// codes; the engine actors only need this 3-state classification.</summary>
public enum OpcUaQuality { Good, Uncertain, Bad }

/// <summary>No-op sink. Bound by default so the actors are safe to run in dev / Mac /
/// integration tests without a real SDK behind them.</summary>
public sealed class NullOpcUaAddressSpaceSink : IOpcUaAddressSpaceSink
{
    public static readonly NullOpcUaAddressSpaceSink Instance = new();
    private NullOpcUaAddressSpaceSink() { }

    /// <inheritdoc />
    public void WriteValue(string nodeId, object? value, OpcUaQuality quality, DateTime sourceTimestampUtc, AddressSpaceRealm realm) { }

    /// <inheritdoc />
    public void WriteAlarmCondition(string alarmNodeId, AlarmConditionSnapshot state, DateTime sourceTimestampUtc, AddressSpaceRealm realm) { }
    public void WriteAlarmQuality(string alarmNodeId, OpcUaQuality quality, DateTime sourceTimestampUtc, AddressSpaceRealm realm) { }

    /// <inheritdoc />
    public void MaterialiseAlarmCondition(string alarmNodeId, string equipmentNodeId, string displayName, string alarmType, int severity, AddressSpaceRealm realm, bool isNative = false) { }

    /// <inheritdoc />
    public void WireAlarmNotifiers(string alarmNodeId, AddressSpaceRealm alarmRealm, IReadOnlyList<string> notifierFolderNodeIds, AddressSpaceRealm notifierFolderRealm) { }

    /// <inheritdoc />
    public void EnsureFolder(string folderNodeId, string? parentNodeId, string displayName, AddressSpaceRealm realm) { }

    /// <inheritdoc />
    public void EnsureVariable(string variableNodeId, string? parentFolderNodeId, string displayName, string dataType, bool writable, AddressSpaceRealm realm, string? historianTagname = null, bool isArray = false, uint? arrayLength = null) { }

    /// <inheritdoc />
    public void RebuildAddressSpace() { }

    /// <inheritdoc />
    public void RaiseNodesAddedModelChange(string affectedNodeId, AddressSpaceRealm realm) { }

    /// <inheritdoc />
    public void AddReference(string sourceNodeId, AddressSpaceRealm sourceRealm, string targetNodeId, AddressSpaceRealm targetRealm, string referenceType = "Organizes") { }
}
