namespace ZB.MOM.WW.MxGateway.Server.Diagnostics;

/// <summary>
/// Redacts sensitive information from log entries.
/// </summary>
public static class GatewayLogRedactor
{
    /// <summary>Placeholder for redacted values.</summary>
    public const string RedactedValue = "[redacted]";

    private static readonly HashSet<string> SensitiveCommandMethods = new(StringComparer.OrdinalIgnoreCase)
    {
        "AuthenticateUser",
        "WriteSecured",
        "WriteSecured2"
    };

    /// <summary>
    /// Authorization schemes whose name may survive redaction. Anything outside this list is
    /// dropped whole: an unrecognized leading word is as likely to be credential material as it
    /// is to be a scheme, so it is not worth the leak.
    /// </summary>
    private static readonly string[] KnownAuthorizationSchemes =
    [
        "Bearer",
        "Basic",
        "Digest",
        "Negotiate",
        "NTLM",
        "ApiKey",
        "Token",
    ];

    /// <summary>Prefix identifying a gateway-issued API key.</summary>
    private const string GatewayKeyPrefix = "mxgw_";

    /// <summary>Upper bound on a key id kept in the clear; a longer run is treated as secret material.</summary>
    private const int MaxKeyIdLength = 64;

    /// <summary>
    /// Determines whether a command method bears credentials.
    /// </summary>
    /// <param name="commandMethod">The command method name to check.</param>
    /// <returns><see langword="true"/> if the command method carries credentials; otherwise <see langword="false"/>.</returns>
    public static bool IsCredentialBearingCommand(string? commandMethod)
    {
        return commandMethod is not null
            && SensitiveCommandMethods.Contains(commandMethod);
    }

    /// <summary>
    /// Redacts the credential portion of an authorization header value.
    /// </summary>
    /// <param name="authorizationHeader">The authorization header value to redact.</param>
    /// <returns>The header with the credential redacted, or the original value when it is null or blank.</returns>
    public static string? RedactApiKey(string? authorizationHeader)
    {
        return RedactClientIdentity(authorizationHeader);
    }

    /// <summary>
    /// Redacts the credential carried by a client identity. Redaction fails closed: only a
    /// gateway-issued API key keeps its <c>mxgw_&lt;key-id&gt;_</c> shape (so operators can tell keys
    /// apart in logs), and only a recognized scheme keeps its name. Every other value — a foreign
    /// bearer token, a scheme-less string, junk — is replaced whole, because nothing that reaches
    /// this method is known to be safe to log.
    /// </summary>
    /// <param name="clientIdentity">The client identity string to redact.</param>
    /// <returns>The redacted client identity, or the original value when it is null or blank.</returns>
    public static string? RedactClientIdentity(string? clientIdentity)
    {
        if (string.IsNullOrWhiteSpace(clientIdentity))
        {
            return clientIdentity;
        }

        ReadOnlySpan<char> value = clientIdentity.AsSpan().Trim();
        int separatorIndex = value.IndexOf(' ');

        if (separatorIndex < 0)
        {
            // A single token carries no scheme, so the token itself is the credential.
            return RedactedValue;
        }

        ReadOnlySpan<char> scheme = value[..separatorIndex];
        ReadOnlySpan<char> credential = value[(separatorIndex + 1)..].Trim();

        if (credential.IsEmpty || !IsKnownAuthorizationScheme(scheme))
        {
            return RedactedValue;
        }

        return credential.StartsWith(GatewayKeyPrefix, StringComparison.OrdinalIgnoreCase)
            ? $"{scheme} {GatewayKeyPrefix}{RedactKeyId(credential)}"
            : $"{scheme} {RedactedValue}";
    }

    /// <summary>
    /// Renders the trailing portion of a gateway API key: the key id when the key is well formed,
    /// otherwise nothing but the placeholder.
    /// </summary>
    /// <param name="credential">The credential, known to start with the gateway key prefix.</param>
    /// <returns>The <c>&lt;key-id&gt;_[redacted]</c> tail, or just the placeholder.</returns>
    private static string RedactKeyId(ReadOnlySpan<char> credential)
    {
        ReadOnlySpan<char> remainder = credential[GatewayKeyPrefix.Length..];
        int secretIndex = remainder.IndexOf('_');

        // No separator means no secret boundary to trust, so the whole remainder is treated as secret.
        return secretIndex is <= 0 or > MaxKeyIdLength
            ? RedactedValue
            : $"{remainder[..secretIndex]}_{RedactedValue}";
    }

    /// <summary>
    /// Determines whether a leading word is a recognized authorization scheme.
    /// </summary>
    /// <param name="scheme">The candidate scheme word.</param>
    /// <returns><see langword="true"/> when the word may survive redaction; otherwise <see langword="false"/>.</returns>
    private static bool IsKnownAuthorizationScheme(ReadOnlySpan<char> scheme)
    {
        foreach (string knownScheme in KnownAuthorizationSchemes)
        {
            if (scheme.Equals(knownScheme, StringComparison.OrdinalIgnoreCase))
            {
                return true;
            }
        }

        return false;
    }

    /// <summary>
    /// Redacts a command value if it contains credentials or value logging is disabled.
    /// </summary>
    /// <param name="commandMethod">The command method name to check for credentials.</param>
    /// <param name="value">The command value to redact.</param>
    /// <param name="valueLoggingEnabled">Whether value logging is enabled.</param>
    /// <returns>The redacted placeholder, the original value, or <see langword="null"/> when <paramref name="value"/> is null.</returns>
    public static object? RedactCommandValue(
        string? commandMethod,
        object? value,
        bool valueLoggingEnabled = false)
    {
        if (value is null)
        {
            return null;
        }

        if (!valueLoggingEnabled || IsCredentialBearingCommand(commandMethod))
        {
            return RedactedValue;
        }

        return value;
    }
}
