using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Pipes;
using System.Threading;
using System.Threading.Tasks;
using ZB.MOM.WW.MxGateway.Worker.Bootstrap;
using Polly;
using Polly.Retry;

namespace ZB.MOM.WW.MxGateway.Worker.Ipc;

/// <summary>
/// Connects to the gateway via a named pipe and runs the worker frame protocol session.
/// </summary>
public sealed class WorkerPipeClient : IWorkerPipeClient
{
    /// <summary>Default overall connection timeout in milliseconds.</summary>
    public const int DefaultConnectTimeoutMilliseconds = 30000;

    /// <summary>Default per-attempt connection timeout in milliseconds.</summary>
    public const int DefaultConnectAttemptTimeoutMilliseconds = 2000;

    /// <summary>Environment variable for overriding the per-attempt connection timeout.</summary>
    public const string ConnectAttemptTimeoutEnvironmentVariableName =
        "MXGATEWAY_WORKER_PIPE_CONNECT_ATTEMPT_TIMEOUT_MS";

    private readonly int _connectTimeoutMilliseconds;
    private readonly int _connectAttemptTimeoutMilliseconds;
    private readonly Func<Stream, WorkerFrameProtocolOptions, IWorkerLogger?, WorkerPipeSession> _sessionFactory;
    private readonly IWorkerLogger? _logger;

    /// <summary>
    /// Initializes a worker pipe client with default timeouts.
    /// </summary>
    public WorkerPipeClient()
        : this(null, DefaultConnectTimeoutMilliseconds)
    {
    }

    /// <summary>
    /// Initializes a worker pipe client with a logger and default timeouts.
    /// </summary>
    /// <param name="logger">Optional logger for diagnostic output.</param>
    public WorkerPipeClient(IWorkerLogger? logger)
        : this(logger, DefaultConnectTimeoutMilliseconds)
    {
    }

    /// <summary>
    /// Initializes a worker pipe client with a custom overall connect timeout.
    /// </summary>
    /// <param name="connectTimeoutMilliseconds">Overall connection timeout in milliseconds.</param>
    public WorkerPipeClient(int connectTimeoutMilliseconds)
        : this(null, connectTimeoutMilliseconds)
    {
    }

    /// <summary>
    /// Initializes a worker pipe client with custom timeouts and a session factory.
    /// </summary>
    /// <param name="connectTimeoutMilliseconds">Overall connection timeout in milliseconds.</param>
    /// <param name="sessionFactory">Factory creating the worker pipe session.</param>
    public WorkerPipeClient(
        int connectTimeoutMilliseconds,
        Func<Stream, WorkerFrameProtocolOptions, WorkerPipeSession> sessionFactory)
        : this(
            null,
            connectTimeoutMilliseconds,
            ResolveDefaultConnectAttemptTimeoutMilliseconds(),
            (stream, frameOptions, _) => sessionFactory(stream, frameOptions))
    {
    }

    /// <summary>
    /// Initializes a worker pipe client with a logger and custom overall timeout.
    /// </summary>
    /// <param name="logger">Optional logger for diagnostic output.</param>
    /// <param name="connectTimeoutMilliseconds">Overall connection timeout in milliseconds.</param>
    public WorkerPipeClient(
        IWorkerLogger? logger,
        int connectTimeoutMilliseconds)
        : this(
            logger,
            connectTimeoutMilliseconds,
            ResolveDefaultConnectAttemptTimeoutMilliseconds(),
            (stream, frameOptions, workerLogger) => new WorkerPipeSession(stream, frameOptions, workerLogger))
    {
    }

    /// <summary>
    /// Initializes a worker pipe client with logger, timeouts, and a session factory.
    /// </summary>
    /// <param name="logger">Optional logger for diagnostic output.</param>
    /// <param name="connectTimeoutMilliseconds">Overall connection timeout in milliseconds.</param>
    /// <param name="sessionFactory">Factory creating the worker pipe session.</param>
    public WorkerPipeClient(
        IWorkerLogger? logger,
        int connectTimeoutMilliseconds,
        Func<Stream, WorkerFrameProtocolOptions, IWorkerLogger?, WorkerPipeSession> sessionFactory)
        : this(
            logger,
            connectTimeoutMilliseconds,
            ResolveDefaultConnectAttemptTimeoutMilliseconds(),
            sessionFactory)
    {
    }

    /// <summary>
    /// Initializes a worker pipe client with full configuration.
    /// </summary>
    /// <param name="logger">Optional logger for diagnostic output.</param>
    /// <param name="connectTimeoutMilliseconds">Overall connection timeout in milliseconds.</param>
    /// <param name="connectAttemptTimeoutMilliseconds">Per-attempt connection timeout in milliseconds.</param>
    /// <param name="sessionFactory">Factory creating the worker pipe session.</param>
    public WorkerPipeClient(
        IWorkerLogger? logger,
        int connectTimeoutMilliseconds,
        int connectAttemptTimeoutMilliseconds,
        Func<Stream, WorkerFrameProtocolOptions, IWorkerLogger?, WorkerPipeSession> sessionFactory)
    {
        if (connectTimeoutMilliseconds <= 0)
        {
            throw new ArgumentOutOfRangeException(
                nameof(connectTimeoutMilliseconds),
                "Worker pipe connect timeout must be greater than zero.");
        }

        if (connectAttemptTimeoutMilliseconds <= 0)
        {
            throw new ArgumentOutOfRangeException(
                nameof(connectAttemptTimeoutMilliseconds),
                "Worker pipe connect attempt timeout must be greater than zero.");
        }

        _logger = logger;
        _sessionFactory = sessionFactory ?? throw new ArgumentNullException(nameof(sessionFactory));
        _connectTimeoutMilliseconds = connectTimeoutMilliseconds;
        _connectAttemptTimeoutMilliseconds = connectAttemptTimeoutMilliseconds;
    }

    /// <inheritdoc />
    public async Task RunAsync(
        WorkerOptions options,
        CancellationToken cancellationToken = default)
    {
        if (options is null)
        {
            throw new ArgumentNullException(nameof(options));
        }

        WorkerFrameProtocolOptions frameOptions = new(options);

        // The session disposes this pipe itself as its last teardown step — that disposal is what
        // unblocks a net48 pipe read the message loop abandoned, and it has to happen while the
        // session still holds the read task so the resulting fault is observed rather than orphaned
        // (see WorkerPipeSession.RunAsync). The `using` stays as the backstop for the paths the
        // session never reaches: a session factory that throws, or a RunAsync that never gets past
        // its own construction. Disposal is idempotent, so the second Dispose is a no-op — do not
        // "clean up" this `using` on the assumption that it is now redundant.
        using NamedPipeClientStream pipe = await ConnectWithRetryAsync(options.PipeName, cancellationToken)
            .ConfigureAwait(false);

        WorkerPipeSession session = _sessionFactory(pipe, frameOptions, _logger);
        await session.RunAsync(cancellationToken).ConfigureAwait(false);
    }

    private async Task<NamedPipeClientStream> ConnectWithRetryAsync(
        string pipeName,
        CancellationToken cancellationToken)
    {
        // The real bound on connection attempts is the connectDeadline token
        // below (CancelAfter(connectTimeout)): Polly stops retrying as soon as
        // that token is cancelled. Driving retries purely off the deadline —
        // rather than a fragile attempt-count formula that ignored the
        // exponential backoff between attempts — keeps the time budget the
        // single source of truth. MaxRetryAttempts is set to its maximum so it
        // never ends the retry loop before the deadline does.
        ResiliencePipeline<NamedPipeClientStream> pipeline = new ResiliencePipelineBuilder<NamedPipeClientStream>()
            .AddRetry(new RetryStrategyOptions<NamedPipeClientStream>
            {
                MaxRetryAttempts = int.MaxValue,
                BackoffType = DelayBackoffType.Exponential,
                UseJitter = true,
                Delay = TimeSpan.FromMilliseconds(250),
                MaxDelay = TimeSpan.FromSeconds(2),
                ShouldHandle = new PredicateBuilder<NamedPipeClientStream>()
                    .Handle<Exception>(exception => exception is TimeoutException or IOException),
                OnRetry = args =>
                {
                    args.Outcome.Result?.Dispose();
                    _logger?.Information(
                        "WorkerPipeConnectRetry",
                        new Dictionary<string, object?>
                        {
                            ["attempt"] = args.AttemptNumber + 1,
                            ["pipe_name"] = pipeName,
                        });
                    return default;
                },
            })
            .Build();

        using CancellationTokenSource connectDeadline =
            CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
        connectDeadline.CancelAfter(_connectTimeoutMilliseconds);

        try
        {
            return await pipeline.ExecuteAsync(
                    async token => await ConnectSingleAttemptAsync(pipeName, token).ConfigureAwait(false),
                    connectDeadline.Token)
                .ConfigureAwait(false);
        }
        catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
        {
            throw new TimeoutException(
                $"Worker pipe {pipeName} did not connect within {_connectTimeoutMilliseconds}ms.");
        }
    }

    private async Task<NamedPipeClientStream> ConnectSingleAttemptAsync(
        string pipeName,
        CancellationToken cancellationToken)
    {
        NamedPipeClientStream pipe = new(
            ".",
            pipeName,
            PipeDirection.InOut,
            PipeOptions.Asynchronous);

        try
        {
            using CancellationTokenSource attemptTimeout =
                CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
            attemptTimeout.CancelAfter(_connectAttemptTimeoutMilliseconds);

            await Task.Run(
                    () =>
                    {
                        attemptTimeout.Token.ThrowIfCancellationRequested();
                        pipe.Connect(_connectAttemptTimeoutMilliseconds);
                    },
                    attemptTimeout.Token)
                .ConfigureAwait(false);

            return pipe;
        }
        catch
        {
            pipe.Dispose();
            throw;
        }
    }

    private static int ResolveDefaultConnectAttemptTimeoutMilliseconds()
    {
        string? configuredValue = Environment.GetEnvironmentVariable(ConnectAttemptTimeoutEnvironmentVariableName);
        return int.TryParse(configuredValue, out int milliseconds) && milliseconds > 0
            ? milliseconds
            : DefaultConnectAttemptTimeoutMilliseconds;
    }
}
