using System.Net.Sockets;
using MailKit;
using MailKit.Net.Smtp;

namespace ZB.MOM.WW.ScadaBridge.NotificationService;

/// <summary>
/// The classification of an SMTP delivery failure. This decides
/// whether a failure is retried or surfaced to the caller, so it is part of the
/// system's correctness-relevant behaviour.
/// </summary>
public enum SmtpErrorClass
{
    /// <summary>Cancellation or an unrecognised exception — caller decides.</summary>
    Unknown,

    /// <summary>Retryable failure (4xx, connection/socket/protocol error, timeout).</summary>
    Transient,

    /// <summary>Non-retryable failure (5xx) — must not be retried.</summary>
    Permanent,
}

/// <summary>
/// Classifies an SMTP failure using MailKit's typed exceptions and
/// the numeric <see cref="SmtpStatusCode"/> rather than locale-dependent substring
/// matching on the exception message.
/// <para>
/// Public and shared: the central Notification Outbox's <c>EmailNotificationDeliveryAdapter</c>
/// routes every SMTP failure through this single policy. (The orphaned site-side
/// <c>NotificationDeliveryService</c> that previously co-used this classifier was removed
/// when sites stopped delivering notifications.)
/// </para>
/// </summary>
public static class SmtpErrorClassifier
{
    /// <summary>
    /// Classifies an SMTP failure. A cancellation requested by the caller is never
    /// treated as a transient SMTP error.
    /// </summary>
    /// <param name="ex">The exception thrown by the SMTP send sequence.</param>
    /// <param name="cancellationToken">
    /// The token governing the send; a requested cancellation classifies as
    /// <see cref="SmtpErrorClass.Unknown"/> so the caller can re-throw it.
    /// </param>
    /// <returns>The <see cref="SmtpErrorClass"/> describing whether the failure is transient, permanent, or unknown.</returns>
    public static SmtpErrorClass Classify(Exception ex, CancellationToken cancellationToken)
    {
        ArgumentNullException.ThrowIfNull(ex);

        // A deliberate cancellation is not an SMTP error at all.
        if (ex is OperationCanceledException && cancellationToken.IsCancellationRequested)
        {
            return SmtpErrorClass.Unknown;
        }

        // MailKit reports SMTP command failures with the real status code; the
        // SmtpStatusCode enum's underlying value is the numeric SMTP reply code.
        if (ex is SmtpCommandException command)
        {
            var code = (int)command.StatusCode;
            if (code >= 400 && code < 500)
            {
                return SmtpErrorClass.Transient;
            }

            if (code >= 500 && code < 600)
            {
                return SmtpErrorClass.Permanent;
            }

            return SmtpErrorClass.Unknown;
        }

        // Protocol errors, a dropped/unavailable service, socket failures and
        // timeouts are all retryable — the message has not been rejected.
        if (ex is SmtpProtocolException
            or ServiceNotConnectedException
            or SocketException
            or TimeoutException)
        {
            return SmtpErrorClass.Transient;
        }

        return SmtpErrorClass.Unknown;
    }

    /// <summary>
    /// Convenience predicate: true when <see cref="Classify"/> returns
    /// <see cref="SmtpErrorClass.Transient"/>.
    /// </summary>
    /// <param name="ex">The exception to classify.</param>
    /// <param name="cancellationToken">Cancellation token passed to <see cref="Classify"/>.</param>
    /// <returns><c>true</c> when <see cref="Classify"/> returns <see cref="SmtpErrorClass.Transient"/>.</returns>
    public static bool IsTransient(Exception ex, CancellationToken cancellationToken)
        => Classify(ex, cancellationToken) == SmtpErrorClass.Transient;
}
