using ZB.MOM.WW.MxGateway.Contracts.Proto;

namespace ZB.MOM.WW.MxGateway.Client;

/// <summary>
/// Scrubs exact secret substrings out of diagnostic text before it leaves the client on an
/// exception path. MXAccess can echo a submitted credential or secured value back inside a
/// failure diagnostic (protocol message, MXSTATUS_PROXY diagnostic text, HRESULT description);
/// this helper replaces any such verbatim occurrence with <c>&lt;redacted&gt;</c> so the raw
/// request payload never reaches a caught exception's message. The marker matches the Go, Rust,
/// and Java clients.
/// </summary>
internal static class MxGatewaySecretRedaction
{
    private const string Marker = "<redacted>";

    /// <summary>
    /// Replaces every usable secret in <paramref name="secrets"/> with the redaction marker
    /// (ordinal comparison). Returns the message unchanged when it is null or empty, or when no
    /// usable secret is supplied. A secret that is null, empty, or whitespace-only is ignored so
    /// it cannot over-redact ordinary separator characters in the message.
    /// </summary>
    /// <param name="message">The diagnostic message to scrub.</param>
    /// <param name="secrets">The secret values to remove from the message.</param>
    /// <returns>The scrubbed message.</returns>
    internal static string Redact(string message, params string?[] secrets)
    {
        if (string.IsNullOrEmpty(message) || secrets is null)
        {
            return message;
        }

        string result = message;
        foreach (string? secret in secrets)
        {
            if (!string.IsNullOrWhiteSpace(secret))
            {
                result = result.Replace(secret, Marker, StringComparison.Ordinal);
            }
        }

        return result;
    }

    /// <summary>
    /// Returns a scrubbed clone of <paramref name="reply"/>: the protocol-status message, the
    /// reply-level diagnostic message, and each MXSTATUS_PROXY diagnostic text have every verbatim
    /// secret replaced with the redaction marker. The original is left untouched. MXAccess can echo
    /// a submitted credential into any of these fields, so a redacted exception must carry the
    /// scrubbed reply rather than the secret-bearing original.
    /// </summary>
    /// <param name="reply">The reply to clone and scrub.</param>
    /// <param name="secrets">The secret values to remove.</param>
    /// <returns>A scrubbed clone of the reply.</returns>
    internal static MxCommandReply RedactReply(MxCommandReply reply, params string?[] secrets)
    {
        ArgumentNullException.ThrowIfNull(reply);

        MxCommandReply clone = reply.Clone();
        if (clone.ProtocolStatus is not null)
        {
            clone.ProtocolStatus.Message = Redact(clone.ProtocolStatus.Message, secrets);
        }

        clone.DiagnosticMessage = Redact(clone.DiagnosticMessage, secrets);
        foreach (MxStatusProxy status in clone.Statuses)
        {
            status.DiagnosticText = Redact(status.DiagnosticText, secrets);
        }

        return clone;
    }

    /// <summary>
    /// Returns a scrubbed clone of <paramref name="status"/> (its message with every verbatim
    /// secret removed), or <see langword="null"/> when the input is null.
    /// </summary>
    /// <param name="status">The protocol status to clone and scrub.</param>
    /// <param name="secrets">The secret values to remove.</param>
    /// <returns>A scrubbed clone, or <see langword="null"/>.</returns>
    internal static ProtocolStatus? RedactStatus(ProtocolStatus? status, params string?[] secrets)
    {
        if (status is null)
        {
            return null;
        }

        ProtocolStatus clone = status.Clone();
        clone.Message = Redact(clone.Message, secrets);
        return clone;
    }

    /// <summary>
    /// Returns a list of scrubbed clones of <paramref name="statuses"/> — each MXSTATUS_PROXY's
    /// diagnostic text has every verbatim secret removed. The originals are left untouched.
    /// </summary>
    /// <param name="statuses">The statuses to clone and scrub.</param>
    /// <param name="secrets">The secret values to remove.</param>
    /// <returns>A list of scrubbed clones.</returns>
    internal static IReadOnlyList<MxStatusProxy> RedactStatuses(
        IReadOnlyList<MxStatusProxy> statuses,
        params string?[] secrets)
    {
        if (statuses is null || statuses.Count is 0)
        {
            return statuses ?? [];
        }

        MxStatusProxy[] result = new MxStatusProxy[statuses.Count];
        for (int i = 0; i < statuses.Count; i++)
        {
            MxStatusProxy clone = statuses[i].Clone();
            clone.DiagnosticText = Redact(clone.DiagnosticText, secrets);
            result[i] = clone;
        }

        return result;
    }

    /// <summary>
    /// Returns an exception equivalent to <paramref name="ex"/> but with any verbatim secret
    /// scrubbed from its message. When nothing changes, the original exception is returned
    /// unchanged; otherwise a new exception of the same concrete runtime type is built and the
    /// original reply/status context is preserved. The secret-bearing original is deliberately
    /// <b>not</b> chained as the inner exception — doing so would let its unredacted message
    /// re-surface through <see cref="Exception.ToString"/> (which logging frameworks call). The
    /// original's own inner cause (a transport error, never the request payload) is carried
    /// forward instead.
    /// </summary>
    /// <param name="ex">The exception to redact.</param>
    /// <param name="secrets">The secret values to remove from the message.</param>
    /// <returns>The redacted exception, or the original when no change was needed.</returns>
    internal static MxGatewayException Redacted(MxGatewayException ex, params string?[] secrets)
    {
        ArgumentNullException.ThrowIfNull(ex);

        string redacted = Redact(ex.Message, secrets);
        bool messageChanged = !string.Equals(redacted, ex.Message, StringComparison.Ordinal);
        Exception? cause = ex.InnerException;

        // MxAccessException derives its structured fields from the raw reply, so scrubbing must
        // clone and redact that reply — the message alone changing is not enough, because the reply
        // can carry the echoed secret even when the message does not.
        if (ex is MxAccessException access)
        {
            if (!messageChanged && !ReplyContainsSecret(access.Reply, secrets))
            {
                return ex;
            }

            return new MxAccessException(redacted, RedactReply(access.Reply, secrets), cause);
        }

        // Other subtypes carry the secret through ProtocolStatus.Message and Statuses[].DiagnosticText.
        if (!messageChanged
            && !ContainsSecret(ex.ProtocolStatus?.Message, secrets)
            && !StatusesContainSecret(ex.Statuses, secrets))
        {
            return ex;
        }

        ProtocolStatus? status = RedactStatus(ex.ProtocolStatus, secrets);
        IReadOnlyList<MxStatusProxy> statuses = RedactStatuses(ex.Statuses, secrets);
        return ex switch
        {
            MxGatewaySessionException => new MxGatewaySessionException(
                redacted, ex.SessionId, ex.CorrelationId, status, ex.HResultCode, statuses, cause),
            MxGatewayWorkerException => new MxGatewayWorkerException(
                redacted, ex.SessionId, ex.CorrelationId, status, ex.HResultCode, statuses, cause),
            MxGatewayAuthenticationException => new MxGatewayAuthenticationException(
                redacted, ex.SessionId, ex.CorrelationId, status, ex.HResultCode, statuses, cause),
            MxGatewayAuthorizationException => new MxGatewayAuthorizationException(
                redacted, ex.SessionId, ex.CorrelationId, status, ex.HResultCode, statuses, cause),
            MxGatewayMalformedReplyException => new MxGatewayMalformedReplyException(
                redacted, ex.SessionId, ex.CorrelationId, status, ex.HResultCode, statuses, cause),
            MxGatewayCommandException => new MxGatewayCommandException(
                redacted, ex.SessionId, ex.CorrelationId, status, ex.HResultCode, statuses, cause),
            _ => new MxGatewayException(redacted, cause),
        };
    }

    private static bool ContainsSecret(string? text, string?[] secrets)
    {
        if (string.IsNullOrEmpty(text) || secrets is null)
        {
            return false;
        }

        foreach (string? secret in secrets)
        {
            if (!string.IsNullOrWhiteSpace(secret) && text.Contains(secret, StringComparison.Ordinal))
            {
                return true;
            }
        }

        return false;
    }

    private static bool StatusesContainSecret(IReadOnlyList<MxStatusProxy> statuses, string?[] secrets)
    {
        if (statuses is null)
        {
            return false;
        }

        foreach (MxStatusProxy status in statuses)
        {
            if (ContainsSecret(status.DiagnosticText, secrets))
            {
                return true;
            }
        }

        return false;
    }

    private static bool ReplyContainsSecret(MxCommandReply reply, string?[] secrets)
    {
        if (reply is null)
        {
            return false;
        }

        return ContainsSecret(reply.ProtocolStatus?.Message, secrets)
            || ContainsSecret(reply.DiagnosticMessage, secrets)
            || StatusesContainSecret(reply.Statuses, secrets);
    }
}
