using System.Net.Sockets;
using Mbproxy.Options;
using Polly;
using Polly.Retry;

namespace Mbproxy.Proxy.Supervision;

/// <summary>
/// Builds Polly v8 <see cref="ResiliencePipeline"/> instances from the typed resilience
/// configuration (<see cref="RetryProfile"/> and <see cref="RecoveryProfile"/>).
///
/// <para>Pipelines are built once at startup and reused across all operations. They are
/// thread-safe and allocation-free on the happy path.</para>
/// </summary>
internal static class PolicyFactory
{
    // ── Network errors that are safe to retry on backend connect ────────────────────────
    // Only these SocketError values are transient; everything else is a programming error
    // or a configuration mistake and should not be retried.
    private static readonly HashSet<SocketError> RetryableSocketErrors =
    [
        SocketError.ConnectionRefused,
        SocketError.TimedOut,
        SocketError.HostUnreachable,
        SocketError.NetworkUnreachable,
    ];

    /// <summary>
    /// Builds a retry pipeline for backend (PLC) TCP connect attempts.
    ///
    /// <para>Retries only on <see cref="SocketException"/> with a
    /// <see cref="SocketError"/> in <see cref="RetryableSocketErrors"/>. Does NOT retry
    /// <see cref="ArgumentException"/>, <see cref="OperationCanceledException"/>, or any
    /// non-network exception.</para>
    ///
    /// <para>The delay sequence is taken directly from <see cref="RetryProfile.BackoffMs"/>;
    /// element [i] is the delay before attempt i+1 (0-based). If the attempt index
    /// exceeds the array, the last element is used.</para>
    ///
    /// <para>After all attempts are exhausted, the pipeline re-throws the last exception
    /// so the caller can log <c>mbproxy.backend.failed</c> and close the upstream socket.</para>
    /// </summary>
    public static ResiliencePipeline BuildBackendConnect(RetryProfile profile, ILogger logger)
    {
        // MaxAttempts in Polly v8 includes the first attempt.
        int maxAttempts = Math.Max(1, profile.MaxAttempts);
        var backoffMs   = profile.BackoffMs;

        return new ResiliencePipelineBuilder()
            .AddRetry(new RetryStrategyOptions
            {
                MaxRetryAttempts = maxAttempts - 1, // retries = total - 1 (first attempt is free)
                ShouldHandle = new PredicateBuilder()
                    .Handle<SocketException>(ex => RetryableSocketErrors.Contains(ex.SocketErrorCode)),
                DelayGenerator = args =>
                {
                    int idx = args.AttemptNumber; // 0 = first retry, i.e. after attempt 0
                    // Clamp to the last element if we exceed the array.
                    int ms = backoffMs.Count > 0
                        ? backoffMs[Math.Min(idx, backoffMs.Count - 1)]
                        : 0;
                    return new ValueTask<TimeSpan?>(TimeSpan.FromMilliseconds(ms));
                },
                OnRetry = args =>
                {
                    logger.LogDebug(
                        "Backend connect retry {Attempt}/{Max}: {Error}",
                        args.AttemptNumber + 1,
                        maxAttempts - 1,
                        args.Outcome.Exception?.Message);
                    return ValueTask.CompletedTask;
                },
            })
            .Build();
    }

    /// <summary>
    /// Builds an infinite-retry pipeline for listener bind recovery.
    ///
    /// <para>The delay sequence is:
    /// <list type="bullet">
    ///   <item><description>Attempts 0 .. (InitialBackoffMs.Length-1) use the initial backoff array.</description></item>
    ///   <item><description>All subsequent attempts use <see cref="RecoveryProfile.SteadyStateMs"/>.</description></item>
    /// </list>
    /// The pipeline never exhausts — it retries until the supervisor's cancellation token
    /// fires (on <see cref="PlcListenerSupervisor.StopAsync"/>).</para>
    ///
    /// <para>Polly's <c>ExecuteAsync(ct)</c> propagates <see cref="OperationCanceledException"/>
    /// when <paramref name="ct"/> fires, so the supervisor exits the loop cleanly.</para>
    /// </summary>
    public static ResiliencePipeline BuildListenerRecovery(RecoveryProfile profile, ILogger logger)
    {
        var initialMs   = profile.InitialBackoffMs;
        int steadyMs    = profile.SteadyStateMs;

        return new ResiliencePipelineBuilder()
            .AddRetry(new RetryStrategyOptions
            {
                // int.MaxValue makes the pipeline retry indefinitely; cancellation
                // is the only exit path (besides the supervisor calling StopAsync).
                MaxRetryAttempts = int.MaxValue,
                ShouldHandle     = new PredicateBuilder().Handle<Exception>(
                    ex => ex is not OperationCanceledException),
                DelayGenerator = args =>
                {
                    // args.AttemptNumber is the zero-based index of the retry
                    // (0 = first retry, after the first failed attempt).
                    int idx = args.AttemptNumber;
                    int ms = idx < initialMs.Count
                        ? initialMs[idx]
                        : steadyMs;
                    return new ValueTask<TimeSpan?>(TimeSpan.FromMilliseconds(ms));
                },
                OnRetry = args =>
                {
                    logger.LogDebug(
                        "Listener recovery attempt {Attempt}: {Error}",
                        args.AttemptNumber + 1,
                        args.Outcome.Exception?.Message);
                    return ValueTask.CompletedTask;
                },
            })
            .Build();
    }
}
