using System.Net;

namespace ZB.MOM.WW.ScadaBridge.ExternalSystemGateway;

/// <summary>
/// Classifies HTTP errors as transient or permanent.
/// Transient: connection refused, timeout, HTTP 408/429/5xx.
/// Permanent: HTTP 4xx (except 408/429).
/// </summary>
public static class ErrorClassifier
{
    /// <summary>
    /// Determines whether an HTTP status code represents a transient failure.
    /// Transient: HTTP 5xx, 408 (Request Timeout) and 429 (Too Many Requests).
    /// Every other non-success status (the remaining 4xx) defaults to permanent —
    /// a permanent failure is the safe default because retrying a 4xx is unlikely to
    /// succeed and risks duplicate side effects.
    /// </summary>
    /// <param name="statusCode">The HTTP status code to classify.</param>
    /// <returns><see langword="true"/> for 5xx, 408, or 429; <see langword="false"/> for all other status codes.</returns>
    public static bool IsTransient(HttpStatusCode statusCode)
    {
        var code = (int)statusCode;
        return code >= 500 || code == 408 || code == 429;
    }

    /// <summary>
    /// Determines whether an exception represents a transient failure.
    /// Prefer the cancellation-aware <see cref="IsTransient(Exception, CancellationToken)"/>
    /// overload at call sites that hold the caller's token — this token-less overload
    /// classifies every <see cref="OperationCanceledException"/> as transient, including
    /// caller-requested cancellations that must never be buffered for S&amp;F retry.
    /// </summary>
    /// <param name="exception">The exception to classify.</param>
    /// <returns><see langword="true"/> for connection/timeout/cancellation exceptions; <see langword="false"/> otherwise.</returns>
    public static bool IsTransient(Exception exception)
    {
        return exception is HttpRequestException
            or TaskCanceledException
            or TimeoutException
            or OperationCanceledException;
    }

    /// <summary>
    /// Cancellation-aware overload: a caller-requested cancellation is NOT transient
    /// (buffering caller-abandoned work into S&amp;F would be wrong). Prefer this overload
    /// at call sites that hold the caller's token.
    /// </summary>
    /// <param name="exception">The exception to classify.</param>
    /// <param name="cancellationToken">The caller's cancellation token, used to distinguish caller-requested cancellation from a transient timeout.</param>
    /// <returns><see langword="true"/> for connection/timeout exceptions and cancellations not requested by the caller's token; <see langword="false"/> otherwise.</returns>
    public static bool IsTransient(Exception exception, CancellationToken cancellationToken)
    {
        if (exception is OperationCanceledException && cancellationToken.IsCancellationRequested)
            return false;
        return IsTransient(exception);
    }

    /// <summary>
    /// Creates a TransientException for S&amp;F buffering.
    /// </summary>
    /// <param name="message">Human-readable failure description.</param>
    /// <param name="inner">Optional inner exception that caused the transient failure.</param>
    /// <returns>A new <see cref="TransientExternalSystemException"/> wrapping the given details.</returns>
    public static TransientExternalSystemException AsTransient(string message, Exception? inner = null)
    {
        return new TransientExternalSystemException(message, inner);
    }
}

/// <summary>
/// Exception type that signals a transient failure suitable for store-and-forward retry.
/// </summary>
public class TransientExternalSystemException : Exception
{
    /// <summary>Initializes a new <see cref="TransientExternalSystemException"/> with a message and optional inner exception.</summary>
    /// <param name="message">The error message.</param>
    /// <param name="innerException">Optional inner exception.</param>
    public TransientExternalSystemException(string message, Exception? innerException = null)
        : base(message, innerException) { }
}

/// <summary>
/// Exception type that signals a permanent failure (should not be retried).
/// </summary>
public class PermanentExternalSystemException : Exception
{
    /// <summary>Gets the HTTP status code that caused the permanent failure, if applicable.</summary>
    public int? HttpStatusCode { get; }

    /// <summary>Initializes a new <see cref="PermanentExternalSystemException"/> with a message, optional HTTP status code, and optional inner exception.</summary>
    /// <param name="message">The error message.</param>
    /// <param name="httpStatusCode">The HTTP status code that triggered the failure, if available.</param>
    /// <param name="innerException">Optional inner exception.</param>
    public PermanentExternalSystemException(string message, int? httpStatusCode = null, Exception? innerException = null)
        : base(message, innerException)
    {
        HttpStatusCode = httpStatusCode;
    }
}
