namespace ZB.MOM.WW.ScadaBridge.NotificationService;

/// <summary>
/// Scrubs SMTP credential secrets out of free text (typically exception
/// messages echoed back by an SMTP server) before that text is written to a log.
/// MailKit authentication exceptions can contain server responses that quote the
/// supplied credentials; this prevents a password, client secret, or OAuth2 token
/// from leaking into the operational logs.
/// <para>
/// Public so the central Notification Outbox's <c>EmailNotificationDeliveryAdapter</c>
/// can share this exact redaction logic rather than carry a divergent copy.
/// </para>
/// </summary>
public static class CredentialRedactor
{
    private const string Mask = "***REDACTED***";

    /// <summary>
    /// Returns <paramref name="text"/> with every secret component of the supplied
    /// colon-delimited credential string masked.
    /// </summary>
    /// <param name="text">The text to scrub (e.g. an exception message).</param>
    /// <param name="credentials">
    /// The credential string in use — Basic Auth <c>user:pass</c> or OAuth2
    /// <c>tenantId:clientId:clientSecret</c>. May be null.
    /// </param>
    /// <summary>
    /// Minimum length for a colon-separated SECRET component to be
    /// considered worth masking. Twelve characters is the standard heuristic
    /// for "long enough to be a password / client secret"; shorter components
    /// (e.g. a 4-char user name like <c>root</c>, or a 7-char "from" alias)
    /// would mask too much unrelated diagnostic text if treated as secrets.
    /// </summary>
    private const int MinSecretLength = 12;

    /// <summary>
    /// Returns <paramref name="text"/> with every secret component of the supplied
    /// colon-delimited credential string masked.
    /// </summary>
    /// <param name="text">The text to scrub (e.g. an exception message). Returns empty string if null.</param>
    /// <param name="credentials">
    /// The credential string in use — Basic Auth <c>user:pass</c> or OAuth2
    /// <c>tenantId:clientId:clientSecret</c>. May be null; returns <paramref name="text"/> unmodified when null.
    /// </param>
    /// <returns>The scrubbed text with secret components replaced by <c>***REDACTED***</c>.</returns>
    public static string Scrub(string? text, string? credentials)
    {
        if (string.IsNullOrEmpty(text) || string.IsNullOrEmpty(credentials))
        {
            return text ?? string.Empty;
        }

        var result = text;

        // Redact only the obviously-secret slots — the LAST
        // colon-separated component (the password in Basic, the client
        // secret in OAuth2) and the whole packed string — not the user
        // name / tenant id / client id. A short user name like "root" or
        // a sender alias like "smtp" no longer becomes a global redaction
        // token that eats unrelated path / error text.
        var secretsToRedact = new List<string>();

        // The full packed credential is always the most-sensitive shape.
        secretsToRedact.Add(credentials);

        // The trailing colon-component is the password / clientSecret slot.
        // Only redact it if it's plausibly secret-shaped (>= MinSecretLength).
        var parts = credentials.Split(':');
        if (parts.Length >= 2)
        {
            var lastComponent = parts[^1];
            if (lastComponent.Length >= MinSecretLength)
            {
                secretsToRedact.Add(lastComponent);
            }
        }

        // Order longest first so a secret that is a substring of the packed
        // string is still fully masked.
        var ordered = secretsToRedact
            .Distinct()
            .OrderByDescending(s => s.Length);

        foreach (var part in ordered)
        {
            result = result.Replace(part, Mask, StringComparison.Ordinal);
        }

        return result;
    }
}
