using System.Collections.Concurrent;
using System.Security.Cryptography;
using System.Text;
namespace ZB.MOM.WW.ScadaBridge.CLI;
///
/// In-memory, process-lifetime store of the bearer tokens the CLI obtains from
/// POST /auth/token (review 2026-10-21 05/U1, 05/P1). One entry per
/// (management URL, username, password) so every a
/// single CLI invocation creates shares one credential exchange — one LDAP bind per invocation
/// instead of one per management call.
///
///
/// Tokens are never written to disk, logged or echoed; the store dies with the process. The key
/// includes a SHA-256 digest of the password rather than the password itself, so a different
/// password for the same user never reuses a token minted for the old one.
///
internal sealed class CliTokenStore
{
/// The process-wide store used by the public constructor.
public static CliTokenStore Shared { get; } = new();
private readonly ConcurrentDictionary _sessions = new(StringComparer.Ordinal);
/// Returns the (shared) session for a management URL and credential pair.
/// The management base URL.
/// The CLI username.
/// The CLI password.
/// The session carrying the cached token, if any.
public Session For(string baseUrl, string username, string password)
{
var digest = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(password)));
var key = $"{baseUrl.TrimEnd('/').ToLowerInvariant()}\n{username}\n{digest}";
return _sessions.GetOrAdd(key, _ => new Session());
}
/// Token state for one credential pair against one server.
internal sealed class Session
{
/// Serialises token exchanges so concurrent calls share one bind.
public SemaphoreSlim Gate { get; } = new(1, 1);
/// The current bearer token, or null when none is held.
public string? Token { get; set; }
/// When should be treated as stale (expiry minus a safety margin).
public DateTimeOffset RefreshAfter { get; set; }
///
/// True once the server has shown it does not take bearer tokens on the management
/// surfaces (no /auth/token, an older server whose tokens nothing accepts, or a
/// login-disabled deployment): every later call goes straight to HTTP Basic.
///
public bool BasicOnly { get; set; }
/// True while a non-stale token is held.
/// The current instant.
/// Whether can be sent.
public bool HasUsableToken(DateTimeOffset now) => Token is not null && now < RefreshAfter;
}
}