using Opc.Ua;
using Opc.Ua.Client;

namespace ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient;

/// <summary>
///     Bidirectional namespace map for the OPC UA Client (gateway) driver, built at connect
///     time from <c>session.NamespaceUris</c> per <c>docs/v2/driver-specs.md</c> §8
///     "Namespace Remapping".
/// </summary>
/// <remarks>
///     <para>
///         The session-relative namespace index embedded in an <c>ns=N;…</c> NodeId string is
///         <b>not</b> stable: the OPC UA spec permits a server to reorder its namespace table
///         across a restart. A driver that stores raw <c>ns=N</c> references and re-parses
///         them verbatim for reads/writes will, after such a reorder, silently address the
///         wrong namespace.
///     </para>
///     <para>
///         This map captures the upstream namespace table as it was at connect time and lets
///         the driver persist NodeIds in a <b>server-stable</b> form — the namespace
///         <i>URI</i> plus the identifier — via <see cref="ToStableReference"/>. At
///         read/write time <see cref="TryResolve"/> re-binds that stable form against the
///         <i>current</i> session's namespace table, so a table reorder is transparently
///         corrected instead of silently misaddressing nodes.
///     </para>
///     <para>
///         Stable references use the form <c>nsu=&lt;uri&gt;;&lt;idType&gt;=&lt;identifier&gt;</c>,
///         which is the standard OPC UA namespace-URI NodeId encoding the SDK already
///         understands. References that are already in plain <c>ns=N;…</c> form (e.g. a
///         hand-entered config tag) still resolve — <see cref="TryResolve"/> falls back to a
///         direct parse against the current session.
///     </para>
/// </remarks>
public sealed class NamespaceMap
{
    // index -> URI and URI -> index, as the upstream server published them at connect time.
    private readonly string[] _uris;
    private readonly Dictionary<string, ushort> _uriToIndex;

    private NamespaceMap(string[] uris)
    {
        _uris = uris;
        _uriToIndex = new Dictionary<string, ushort>(uris.Length, StringComparer.Ordinal);
        for (var i = 0; i < uris.Length; i++)
            _uriToIndex[uris[i]] = (ushort)i;
    }

    /// <summary>Snapshot the namespace table from a live session.</summary>
    /// <param name="session">The OPC UA session to snapshot.</param>
    public static NamespaceMap FromSession(ISession session)
    {
        ArgumentNullException.ThrowIfNull(session);
        return FromTable(session.NamespaceUris);
    }

    /// <summary>
    ///     Snapshot a <see cref="NamespaceTable"/> directly. Separated from
    ///     <see cref="FromSession"/> so the URI encoding can be exercised without standing up
    ///     a live <see cref="ISession"/>.
    /// </summary>
    /// <param name="namespaceUris">The namespace table to snapshot.</param>
    public static NamespaceMap FromTable(NamespaceTable namespaceUris)
    {
        ArgumentNullException.ThrowIfNull(namespaceUris);
        return new NamespaceMap(namespaceUris.ToArray());
    }

    /// <summary>Number of namespaces captured. Index 0 is always the OPC UA core namespace.</summary>
    public int Count => _uris.Length;

    /// <summary>The namespace URI at the given index, or null if out of range.</summary>
    /// <param name="index">The zero-based index in the namespace table.</param>
    public string? UriForIndex(int index) =>
        index >= 0 && index < _uris.Length ? _uris[index] : null;

    /// <summary>The index for a namespace URI, or null if the URI is not in the table.</summary>
    /// <param name="uri">The namespace URI to look up.</param>
    public ushort? IndexForUri(string uri) =>
        _uriToIndex.TryGetValue(uri, out var idx) ? idx : null;

    /// <summary>
    ///     Render a NodeId resolved against this map's session as a <b>server-stable</b>
    ///     reference string — namespace URI plus identifier, in the SDK's <c>nsu=…</c>
    ///     encoding. The OPC UA core namespace (index 0) keeps the compact <c>i=…</c>/<c>ns=0</c>
    ///     form since URI 0 never moves. Used to persist a discovered NodeId into the local
    ///     address space so it survives a remote namespace-table reorder.
    /// </summary>
    /// <param name="nodeId">The NodeId to render as a stable reference.</param>
    public string ToStableReference(NodeId nodeId)
    {
        ArgumentNullException.ThrowIfNull(nodeId);
        // Namespace 0 is fixed by the spec — the compact form is already stable.
        if (nodeId.NamespaceIndex == 0)
            return nodeId.ToString() ?? string.Empty;

        var uri = UriForIndex(nodeId.NamespaceIndex);
        if (uri is null)
            // Namespace index not in the captured table — fall back to the raw form rather
            // than throwing; the read/write path will surface BadNodeIdInvalid if it truly
            // can't resolve.
            return nodeId.ToString() ?? string.Empty;

        // nsu=<uri>;<idType>=<identifier> is the standard namespace-URI NodeId encoding.
        return $"nsu={uri};{IdentifierPart(nodeId)}";
    }

    /// <summary>
    ///     Resolve a reference string — either a stable <c>nsu=…</c> reference produced by
    ///     <see cref="ToStableReference"/> or a plain <c>ns=N;…</c> NodeId — against the
    ///     <paramref name="currentSession"/>'s namespace table. A <c>nsu=…</c> reference is
    ///     re-bound through the current session's URI table so a remote reorder since connect
    ///     time is corrected. Returns false for empty/malformed input or an unknown URI.
    /// </summary>
    /// <param name="currentSession">The OPC UA session whose namespace table to resolve against.</param>
    /// <param name="reference">The reference string to resolve (either <c>nsu=…</c> or <c>ns=N;…</c> format).</param>
    /// <param name="nodeId">On success, the resolved NodeId; otherwise NodeId.Null.</param>
    public static bool TryResolve(ISession currentSession, string reference, out NodeId nodeId)
    {
        nodeId = NodeId.Null;
        if (currentSession is null || string.IsNullOrWhiteSpace(reference)) return false;
        try
        {
            // NodeId.Parse with a NamespaceUriString form (nsu=…) re-maps the URI against
            // the supplied context's NamespaceUris — i.e. the *current* session table — so a
            // reorder since the reference was captured resolves correctly. Plain ns=N forms
            // resolve directly.
            nodeId = NodeId.Parse(currentSession.MessageContext, reference);
            return !NodeId.IsNull(nodeId);
        }
        catch
        {
            return false;
        }
    }

    /// <summary>Render just the identifier portion (<c>s=…</c>, <c>i=…</c>, <c>g=…</c>, <c>b=…</c>) of a NodeId.</summary>
    private static string IdentifierPart(NodeId nodeId) => nodeId.IdType switch
    {
        IdType.Numeric => $"i={nodeId.Identifier}",
        IdType.String => $"s={nodeId.Identifier}",
        IdType.Guid => $"g={nodeId.Identifier}",
        IdType.Opaque => $"b={Convert.ToBase64String((byte[])nodeId.Identifier)}",
        _ => $"s={nodeId.Identifier}",
    };
}
