using System.Net;

namespace ZB.MOM.WW.ScadaBridge.NotificationOutbox.Delivery;

/// <summary>
/// Classification of a Twilio-REST SMS delivery failure. This decides whether a
/// failure is retried (the whole notification is re-queued) or surfaced as permanent
/// (the notification is parked), so it is part of the system's correctness-relevant
/// behaviour.
/// </summary>
public enum SmsErrorClass
{
    /// <summary>Cancellation or an unrecognised exception — caller decides.</summary>
    Unknown,

    /// <summary>Retryable failure (HTTP 5xx / 408 / 429, transport/timeout error).</summary>
    Transient,

    /// <summary>Non-retryable failure (other 4xx — bad creds, invalid number) — must not be retried.</summary>
    Permanent,
}

/// <summary>
/// Classifies a Twilio-REST SMS delivery failure. Mirrors
/// <see cref="ZB.MOM.WW.ScadaBridge.NotificationService.SmtpErrorClassifier"/>'s
/// enum + static-method shape so the SMS adapter routes every failure through one
/// local policy (no cross-project reference to the External System Gateway). The
/// classification is keyed off the numeric HTTP status code (Twilio returns rich
/// status codes) and the typed transport exceptions rather than locale-dependent
/// substring matching.
/// <para>
/// Transient: HTTP 5xx, 408, 429; <see cref="HttpRequestException"/>,
/// <see cref="TimeoutException"/> and <see cref="TaskCanceledException"/> that are
/// NOT a caller-requested cancellation. Everything else (other 4xx — 400 invalid /
/// unsubscribed number, 401/403 bad credentials) is permanent.
/// </para>
/// </summary>
public static class SmsErrorClassifier
{
    /// <summary>
    /// Classifies an HTTP response status code. Twilio accepts a message with a 2xx;
    /// 5xx / 408 / 429 are transient (provider-side / rate-limited); any other 4xx is
    /// permanent (the request itself is wrong and retrying cannot fix it).
    /// </summary>
    /// <param name="statusCode">The HTTP status code returned by the Twilio Messages endpoint.</param>
    /// <returns>The <see cref="SmsErrorClass"/> describing the failure (never <see cref="SmsErrorClass.Unknown"/> for a status code).</returns>
    public static SmsErrorClass Classify(HttpStatusCode statusCode)
    {
        var code = (int)statusCode;

        if (code == 408 || code == 429 || (code >= 500 && code < 600))
        {
            return SmsErrorClass.Transient;
        }

        // Any non-2xx status that is not 408/429/5xx is a permanent client error.
        return SmsErrorClass.Permanent;
    }

    /// <summary>
    /// Classifies a transport-layer exception thrown while issuing the Twilio request.
    /// A caller-requested cancellation is never treated as a transient SMS error.
    /// </summary>
    /// <param name="ex">The exception thrown by the HTTP send.</param>
    /// <param name="cancellationToken">
    /// The token governing the send; a requested cancellation classifies as
    /// <see cref="SmsErrorClass.Unknown"/> so the caller can re-throw it.
    /// </param>
    /// <returns>The <see cref="SmsErrorClass"/> describing whether the failure is transient or unknown.</returns>
    public static SmsErrorClass Classify(Exception ex, CancellationToken cancellationToken)
    {
        ArgumentNullException.ThrowIfNull(ex);

        // A deliberate cancellation is not an SMS error at all — TaskCanceledException
        // derives from OperationCanceledException, so this guard catches both an
        // HttpClient request-timeout cancel and a caller cancel; only the latter is
        // honoured here so the caller can re-throw.
        if (ex is OperationCanceledException && cancellationToken.IsCancellationRequested)
        {
            return SmsErrorClass.Unknown;
        }

        // A connection failure, a request-level timeout (HttpClient surfaces its own
        // timeout as a TaskCanceledException with no caller cancel) and an explicit
        // TimeoutException are all retryable — the message has not been rejected.
        if (ex is HttpRequestException or TimeoutException or TaskCanceledException)
        {
            return SmsErrorClass.Transient;
        }

        return SmsErrorClass.Unknown;
    }

    /// <summary>
    /// Convenience predicate: true when the HTTP status code classifies as
    /// <see cref="SmsErrorClass.Transient"/>.
    /// </summary>
    /// <param name="statusCode">The HTTP status code to classify.</param>
    /// <returns><c>true</c> when <see cref="Classify(HttpStatusCode)"/> returns <see cref="SmsErrorClass.Transient"/>.</returns>
    public static bool IsTransient(HttpStatusCode statusCode)
        => Classify(statusCode) == SmsErrorClass.Transient;

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