using ZB.MOM.WW.Secrets.Abstractions;

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

/// <summary>
///     Layer-B (G-2b) secret resolution for the OPC UA Client driver's credential fields. Resolves
///     <c>secret:</c>-prefixed <see cref="OpcUaClientDriverOptions.Password"/> and
///     <see cref="OpcUaClientDriverOptions.UserCertificatePassword"/> through the shared
///     <see cref="ISecretResolver"/> (the encrypted-at-rest store) just before the async
///     session-open path consumes them — retiring the cleartext password-in-DB model for
///     production.
/// </summary>
/// <remarks>
///     Resolution is lazy (called from the connect flow, not at config deserialization) so a
///     full reconnect via <c>InitializeAsync</c> re-resolves and picks up secret rotations,
///     mirroring the Galaxy driver's re-resolve-on-reconnect behaviour. Only <c>secret:</c>-prefixed
///     values are resolved; a null/empty or non-<c>secret:</c> value passes through verbatim so the
///     literal-password back-compat path is preserved. The <c>secret:</c> arm is <b>fail-closed</b>:
///     an absent/tombstoned secret throws rather than leaving the <c>secret:</c> literal in place,
///     which would otherwise be sent verbatim to the remote server as the password.
/// </remarks>
internal static class OpcUaClientSecretResolution
{
    private const string SecretPrefix = "secret:";

    /// <summary>
    ///     Return a copy of <paramref name="options"/> with any <c>secret:</c>-prefixed
    ///     <see cref="OpcUaClientDriverOptions.Password"/> /
    ///     <see cref="OpcUaClientDriverOptions.UserCertificatePassword"/> resolved to their
    ///     plaintext via <paramref name="resolver"/>. Non-secret / null / empty fields are
    ///     returned unchanged.
    /// </summary>
    /// <param name="options">The raw driver options (credential fields may carry <c>secret:</c> refs).</param>
    /// <param name="resolver">The shared secret resolver used by the <c>secret:</c> arm.</param>
    /// <param name="ct">Cancellation token for the async secret resolution.</param>
    /// <returns>An options copy whose credential fields carry resolved plaintext.</returns>
    /// <exception cref="InvalidOperationException">
    ///     A <c>secret:</c> ref names a secret that is absent/tombstoned in the store (fail-closed).
    /// </exception>
    internal static async Task<OpcUaClientDriverOptions> ResolveSecretRefsAsync(
        OpcUaClientDriverOptions options, ISecretResolver resolver, CancellationToken ct)
    {
        ArgumentNullException.ThrowIfNull(options);
        ArgumentNullException.ThrowIfNull(resolver);

        var password = await ResolveFieldAsync(options.Password, nameof(options.Password), resolver, ct)
            .ConfigureAwait(false);
        var certPassword = await ResolveFieldAsync(
            options.UserCertificatePassword, nameof(options.UserCertificatePassword), resolver, ct)
            .ConfigureAwait(false);

        // Only re-materialize when something actually changed — a `with` on the reference-equal
        // strings is harmless, but skipping it keeps the common (no-secret) case allocation-free.
        if (ReferenceEquals(password, options.Password)
            && ReferenceEquals(certPassword, options.UserCertificatePassword))
        {
            return options;
        }

        return options with { Password = password, UserCertificatePassword = certPassword };
    }

    /// <summary>
    ///     Resolve a single credential field. Null/empty or non-<c>secret:</c> values pass through
    ///     unchanged (the reference-equal original is returned). A <c>secret:NAME</c> value is
    ///     resolved through <paramref name="resolver"/> and is fail-closed when the secret is absent.
    /// </summary>
    /// <param name="value">The raw field value (may be a <c>secret:</c> ref).</param>
    /// <param name="fieldName">The field name, used in the fail-closed exception message.</param>
    /// <param name="resolver">The shared secret resolver.</param>
    /// <param name="ct">Cancellation token for the async resolution.</param>
    /// <returns>The resolved plaintext, or the original value when it is not a <c>secret:</c> ref.</returns>
    private static async Task<string?> ResolveFieldAsync(
        string? value, string fieldName, ISecretResolver resolver, CancellationToken ct)
    {
        if (string.IsNullOrEmpty(value)
            || !value.StartsWith(SecretPrefix, StringComparison.OrdinalIgnoreCase))
        {
            return value;
        }

        var name = value[SecretPrefix.Length..];
        var resolved = await resolver.GetAsync(new SecretName(name), ct).ConfigureAwait(false);
        return !string.IsNullOrEmpty(resolved)
            ? resolved
            : throw new InvalidOperationException(
                $"OpcUaClientDriverOptions.{fieldName}='{value}' resolves secret '{name}', but it is " +
                "absent from the store (fail-closed).");
    }
}
