using ZB.MOM.WW.OtOpcUa.Commons.Browsing;

namespace ZB.MOM.WW.OtOpcUa.AdminUI.Browsing;

/// <summary>
///     Outcome of <see cref="IBrowserSessionService.OpenAsync"/>. On success
///     <paramref name="Ok"/> is <see langword="true"/> and <paramref name="Token"/> is the
///     registry handle; on failure <paramref name="Message"/> carries a human-readable
///     diagnostic for the UI's error chip.
/// </summary>
/// <param name="Ok">True iff the browse session was opened and registered.</param>
/// <param name="Message">Failure diagnostic, or <see langword="null"/> on success.</param>
/// <param name="Token">Registry handle on success; <see cref="Guid.Empty"/> on failure.</param>
public sealed record BrowseOpenResult(bool Ok, string? Message, Guid Token);

/// <summary>
///     Scoped Razor-page facade over the in-process browse-session machinery. Owns
///     driver-type dispatch on open and per-call timeout enforcement on expand/attributes.
/// </summary>
public interface IBrowserSessionService
{
    /// <summary>Opens a session against the named driver type using the given JSON config.
    /// Never throws — all errors are surfaced via <see cref="BrowseOpenResult"/>.</summary>
    /// <param name="driverType">The driver type to open a browse session against.</param>
    /// <param name="configJson">The driver configuration as JSON.</param>
    /// <param name="ct">Cancellation token for the operation.</param>
    /// <returns>The result of opening the session, including the registry token on success.</returns>
    Task<BrowseOpenResult> OpenAsync(string driverType, string configJson, CancellationToken ct);

    /// <summary>Returns the root nodes of an open session. Throws
    /// <see cref="BrowseSessionNotFoundException"/> if the token is unknown.</summary>
    /// <param name="token">Registry handle for the open browse session.</param>
    /// <param name="ct">Cancellation token for the operation.</param>
    /// <returns>The top-level browse nodes.</returns>
    Task<IReadOnlyList<BrowseNode>> RootAsync(Guid token, CancellationToken ct);

    /// <summary>Returns the direct children of <paramref name="nodeId"/> in an open session.
    /// Throws <see cref="BrowseSessionNotFoundException"/> if the token is unknown.</summary>
    /// <param name="token">Registry handle for the open browse session.</param>
    /// <param name="nodeId">The node whose direct children are returned.</param>
    /// <param name="ct">Cancellation token for the operation.</param>
    /// <returns>The direct children of the node.</returns>
    Task<IReadOnlyList<BrowseNode>> ExpandAsync(Guid token, string nodeId, CancellationToken ct);

    /// <summary>Returns the attributes of <paramref name="nodeId"/> in an open session. Throws
    /// <see cref="BrowseSessionNotFoundException"/> if the token is unknown.</summary>
    /// <param name="token">Registry handle for the open browse session.</param>
    /// <param name="nodeId">The node whose attributes are returned.</param>
    /// <param name="ct">Cancellation token for the operation.</param>
    /// <returns>The attributes of the node.</returns>
    Task<IReadOnlyList<AttributeInfo>> AttributesAsync(Guid token, string nodeId, CancellationToken ct);

    /// <summary>
    ///     Asks the remote peer(s) addressed by <paramref name="scope"/> to re-announce themselves, on
    ///     a session whose driver offers that action (today: MQTT in Sparkplug B mode, where it is a
    ///     Sparkplug rebirth-request NCMD).
    /// </summary>
    /// <param name="token">Registry handle for the open browse session.</param>
    /// <param name="scope">The driver-specific target — for Sparkplug, a browse node id from the
    /// session's own tree, or a bare <c>{group}/{edgeNode}</c>.</param>
    /// <param name="ct">Cancellation token for the operation.</param>
    /// <returns>The number of request messages published.</returns>
    /// <remarks>
    ///     <b>The only browse operation that writes to the device network</b>, and therefore the only
    ///     one carrying an authorization check: the caller must satisfy the same
    ///     <c>DriverOperator</c> policy that gates the picker's Browse affordance. Everything else on
    ///     this facade is read-only.
    /// </remarks>
    /// <exception cref="BrowseSessionNotFoundException">The token is unknown (or was reaped).</exception>
    /// <exception cref="UnauthorizedAccessException">The caller is not a DriverOperator.</exception>
    /// <exception cref="NotSupportedException">This session's driver has no re-announce action.</exception>
    Task<int> RequestRebirthAsync(Guid token, string scope, CancellationToken ct);

    /// <summary>
    ///     True when the open session behind <paramref name="token"/> can actually re-announce — i.e.
    ///     <see cref="RequestRebirthAsync"/> is worth offering. Cheap (no I/O); never throws; false for
    ///     an unknown/reaped token.
    /// </summary>
    /// <param name="token">Registry handle for the open browse session.</param>
    /// <returns>True when the picker should draw a Request-rebirth affordance.</returns>
    /// <remarks>
    ///     A capability probe, <b>not</b> an authorization check — the DriverOperator gate lives on
    ///     <see cref="RequestRebirthAsync"/> and fails closed there. The picker gates on both: this
    ///     decides whether the action exists at all (a plain-MQTT window has none), the policy decides
    ///     whether this operator may fire it.
    /// </remarks>
    bool CanRequestRebirth(Guid token);

    /// <summary>Removes the session from the registry and disposes it. No-op for unknown tokens.</summary>
    /// <param name="token">Registry handle for the browse session to close.</param>
    /// <returns>A task that represents the asynchronous operation.</returns>
    Task CloseAsync(Guid token);

    /// <summary>True when a browse affordance exists for this driver type: a bespoke browser
    /// is registered, or the universal browser can serve it. Cheap (no connect); never throws.
    /// Drives the picker's Browse-button visibility.</summary>
    /// <param name="driverType">The driver type name.</param>
    /// <param name="configJson">The authoring DriverConfig JSON (may be mid-edit).</param>
    /// <returns>True when the picker should offer a Browse affordance for this driver.</returns>
    bool CanBrowse(string driverType, string configJson);
}

/// <summary>
///     Raised by the service layer when a caller references a token that is not
///     (or no longer) in the registry — typically because the reaper evicted it
///     between calls.
/// </summary>
public sealed class BrowseSessionNotFoundException(Guid token)
    : InvalidOperationException($"Browse session {token} not found (may have been reaped).");
