using Opc.Ua;

namespace ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Adapters;

/// <summary>
/// Translates the node-reference strings ScadaBridge stores in configuration
/// (<c>TemplateAttribute.DataSourceReference</c> and friends) to session-local
/// <see cref="NodeId"/>s, and back again for the node browser.
/// </summary>
/// <remarks>
/// <para>
/// <b>Why this exists.</b> A stored <c>ns=2;s=Foo</c> reference hard-codes a namespace
/// <i>index</i>, and an index is only meaningful relative to one server's namespace
/// table. A server that adds, removes or reorders a namespace silently re-points every
/// stored binding: best case the read fails <c>BadNodeIdUnknown</c>, worst case the
/// index now names a different namespace whose table happens to contain a colliding
/// identifier and the binding resolves to the wrong node with Good quality. The
/// namespace <i>URI</i> is the stable identity — the index is just its position today.
/// </para>
/// <para>
/// <b>The durable form</b> is therefore <c>nsu=&lt;namespace-uri&gt;;s=&lt;identifier&gt;</c>,
/// resolved against the live namespace table at use time. <see cref="Resolve"/> accepts
/// both forms, so existing <c>ns=</c> bindings keep working unchanged; <see cref="ToDurable"/>
/// is what the browser emits so newly-authored bindings are index-proof from the start.
/// </para>
/// <para>
/// Namespace 0 is exempt: the OPC UA spec fixes it to <c>http://opcfoundation.org/UA/</c>,
/// so standard NodeIds like <c>i=85</c> are already stable and stay in their short form.
/// </para>
/// </remarks>
public static class OpcUaNodeReference
{
    /// <summary>
    /// Resolves a stored node reference — either <c>ns=&lt;index&gt;;…</c> or the durable
    /// <c>nsu=&lt;uri&gt;;…</c> form — to a <see cref="NodeId"/> valid for this session.
    /// </summary>
    /// <param name="reference">The stored reference string.</param>
    /// <param name="namespaceUris">The connected session's namespace table.</param>
    /// <returns>A session-local <see cref="NodeId"/>.</returns>
    /// <exception cref="ArgumentException">
    /// The reference is unparseable, names a namespace URI this server does not publish,
    /// or points at a different server via <c>svr=</c>.
    /// </exception>
    public static NodeId Resolve(string reference, NamespaceTable namespaceUris)
    {
        ArgumentException.ThrowIfNullOrEmpty(reference);
        ArgumentNullException.ThrowIfNull(namespaceUris);

        ExpandedNodeId expanded;
        try
        {
            expanded = ExpandedNodeId.Parse(reference);
        }
        catch (Exception ex)
        {
            throw new ArgumentException(
                $"'{reference}' is not a valid OPC UA node reference. Expected 'ns=<index>;s=<id>' " +
                $"or the namespace-URI-qualified 'nsu=<uri>;s=<id>'.",
                nameof(reference), ex);
        }

        // svr= addresses a node on a *different* server via this server's server table.
        // Nothing downstream follows that indirection, so fail loudly rather than hand
        // the SDK a reference it would resolve against the wrong address space.
        if (expanded.ServerIndex != 0)
        {
            throw new ArgumentException(
                $"'{reference}' targets another server (svr={expanded.ServerIndex}); " +
                "ScadaBridge binds only to nodes on the connected server.",
                nameof(reference));
        }

        // Maps nsu= -> the live index; leaves an ns= reference's index untouched.
        var nodeId = ExpandedNodeId.ToNodeId(expanded, namespaceUris);
        if (NodeId.IsNull(nodeId))
        {
            throw new ArgumentException(
                $"'{reference}' names namespace URI '{expanded.NamespaceUri}', which this server " +
                "does not publish in its NamespaceArray. The binding likely predates a server " +
                "upgrade that changed its namespaces.",
                nameof(reference));
        }

        return nodeId;
    }

    /// <summary>
    /// Renders a browse result as the durable, index-proof reference to store.
    /// </summary>
    /// <param name="nodeId">The <see cref="ExpandedNodeId"/> a browse returned.</param>
    /// <param name="namespaceUris">The connected session's namespace table.</param>
    /// <returns>
    /// <c>nsu=&lt;uri&gt;;s=&lt;id&gt;</c> for server-specific namespaces; the plain short form for
    /// namespace 0. Falls back to the raw string when the namespace cannot be named.
    /// </returns>
    public static string ToDurable(ExpandedNodeId nodeId, NamespaceTable namespaceUris)
    {
        ArgumentNullException.ThrowIfNull(nodeId);
        ArgumentNullException.ThrowIfNull(namespaceUris);

        // A browse can hand back a reference carrying a NamespaceUri or a server index
        // rather than a plain index. Resolving to session-local first collapses those
        // shapes; without it the raw ToString() can emit a string Resolve cannot re-read.
        var local = ExpandedNodeId.ToNodeId(nodeId, namespaceUris);
        if (NodeId.IsNull(local))
            return nodeId.ToString() ?? string.Empty;

        // ns=0 is spec-fixed, so its short form is already durable.
        if (local.NamespaceIndex == 0)
            return local.ToString() ?? string.Empty;

        var uri = namespaceUris.GetString(local.NamespaceIndex);
        if (string.IsNullOrEmpty(uri))
            return local.ToString() ?? string.Empty;

        return new ExpandedNodeId(local, uri).ToString() ?? string.Empty;
    }
}
