using Microsoft.Extensions.Logging;
using ZB.MOM.WW.Secrets.Abstractions;

namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Config;

/// <summary>
///     Resolves <c>Gateway.ApiKeySecretRef</c> to the actual API-key string. Five
///     forms supported, evaluated in order:
///     <list type="number">
///       <item><c>env:NAME</c> — reads <c>Environment.GetEnvironmentVariable(NAME)</c>.
///         Throws when the variable is unset, so a misconfigured deployment fails
///         fast rather than silently sending an empty key.</item>
///       <item><c>file:PATH</c> — reads UTF-8 text from <c>PATH</c>, trimming
///         whitespace. Lets operators stash the key in an ACL'd file outside the
///         repo (the same pattern as the legacy <c>.local/galaxy-host-secret.txt</c>).</item>
///       <item><c>dev:KEY</c> — explicit cleartext literal. The <c>dev:</c> prefix
///         is a deliberate opt-in signal (dev box, parity rig) so the resolver
///         doesn't emit a warning; production should never use this arm.</item>
///       <item><c>secret:NAME</c> — resolves NAME through the shared
///         <c>ZB.MOM.WW.Secrets</c> <see cref="ISecretResolver"/> (the encrypted-at-rest
///         store). Fail-closed: a <c>secret:</c> ref whose secret is absent/tombstoned
///         throws rather than falling through to the literal arm — the production path
///         that retires the cleartext <c>dev:</c>/literal-in-DB model.</item>
///       <item>Anything else — used as the literal API key for back-compat with
///         configs that pre-date this resolver. When a logger is supplied the
///         resolver emits a startup warning so an operator who accidentally
///         committed a cleartext key sees it.</item>
///     </list>
///     A future PR can swap any of these arms for a different backing store without
///     changing the call site.
/// </summary>
/// <remarks>
///     Lives in the Contracts project so both the runtime <c>GalaxyDriver</c> and the
///     AdminUI <c>GalaxyDriverBrowser</c> (which intentionally don't reference each
///     other) share a single resolver rather than each maintaining a copy.
/// </remarks>
public static class GalaxySecretRef
{
    /// <summary>
    ///     Resolves the supplied secret reference. The <c>secret:NAME</c> arm resolves
    ///     through <paramref name="resolver"/> and is fail-closed (throws when the secret
    ///     is absent). When the ref falls through to the back-compat literal arm (an
    ///     unprefixed cleartext API key in <c>DriverConfig</c> JSON) and a
    ///     <paramref name="logger"/> is supplied, emits a <see cref="LogLevel.Warning"/>.
    ///     The <c>dev:</c> prefix is the explicit opt-in path that doesn't warn.
    /// </summary>
    /// <param name="secretRef">The secret reference string to resolve.</param>
    /// <param name="resolver">The shared secret resolver used by the <c>secret:</c> arm.</param>
    /// <param name="logger">Optional logger for warning on cleartext keys.</param>
    /// <param name="ct">Cancellation token for the async <c>secret:</c> resolution.</param>
    /// <returns>The resolved API-key string.</returns>
    public static async Task<string> ResolveApiKeyAsync(
        string secretRef,
        ISecretResolver resolver,
        ILogger? logger = null,
        CancellationToken ct = default)
    {
        ArgumentException.ThrowIfNullOrEmpty(secretRef);
        ArgumentNullException.ThrowIfNull(resolver);

        if (secretRef.StartsWith("env:", StringComparison.OrdinalIgnoreCase))
        {
            var name = secretRef[4..];
            var value = Environment.GetEnvironmentVariable(name);
            return !string.IsNullOrEmpty(value)
                ? value
                : throw new InvalidOperationException(
                    $"Galaxy.Gateway.ApiKeySecretRef='{secretRef}' resolves to env var '{name}', but it is unset.");
        }

        if (secretRef.StartsWith("file:", StringComparison.OrdinalIgnoreCase))
        {
            var path = secretRef[5..];
            if (!File.Exists(path))
            {
                throw new InvalidOperationException(
                    $"Galaxy.Gateway.ApiKeySecretRef='{secretRef}' points at '{path}', which doesn't exist.");
            }
            var contents = File.ReadAllText(path).Trim();
            return !string.IsNullOrEmpty(contents)
                ? contents
                : throw new InvalidOperationException(
                    $"Galaxy.Gateway.ApiKeySecretRef='{secretRef}' file '{path}' is empty.");
        }

        if (secretRef.StartsWith("dev:", StringComparison.OrdinalIgnoreCase))
        {
            // Explicit dev opt-in — no warning, the operator deliberately chose a
            // cleartext literal (dev box, parity rig).
            return secretRef[4..];
        }

        if (secretRef.StartsWith("secret:", StringComparison.OrdinalIgnoreCase))
        {
            // Production path: resolve the name through the shared encrypted secret store.
            // Fail-closed — an absent/tombstoned secret throws rather than falling through
            // to the literal arm (which would silently treat the ref string as the key).
            var name = secretRef["secret:".Length..];
            var value = await resolver.GetAsync(new SecretName(name), ct).ConfigureAwait(false);
            return !string.IsNullOrEmpty(value)
                ? value
                : throw new InvalidOperationException(
                    $"Galaxy.Gateway.ApiKeySecretRef='{secretRef}' resolves secret '{name}', but it is absent from the store (fail-closed).");
        }

        // Back-compat literal arm. An unprefixed string is treated as the literal
        // API key — but emit a warning so an operator who accidentally committed a
        // cleartext key into DriverConfig sees it. Use the dev: prefix to suppress
        // this warning when the literal is intentional.
        logger?.LogWarning(
            "Galaxy.Gateway.ApiKeySecretRef is being treated as a literal cleartext API key. " +
            "Prefer env:NAME, file:PATH, or the explicit dev:KEY prefix for dev rigs — " +
            "a literal key in DriverConfig JSON is stored in cleartext in the central config DB.");
        return secretRef;
    }
}
