// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Diagnostics;
using System.IO.Pipelines;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Connections;
using Microsoft.AspNetCore.Shared;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;

namespace Microsoft.AspNetCore.Http.Connections.Client.Internal;

internal sealed partial class ServerSentEventsTransport : ITransport
{
    private readonly HttpClient _httpClient;
    private readonly ILogger _logger;
    private readonly HttpConnectionOptions _httpConnectionOptions;
    // Volatile so that the SSE loop sees the updated value set from a different thread
    private volatile Exception? _error;
    private readonly CancellationTokenSource _transportCts = new CancellationTokenSource();
    private readonly CancellationTokenSource _inputCts = new CancellationTokenSource();
    private readonly ServerSentEventsMessageParser _parser = new ServerSentEventsMessageParser();
    private IDuplexPipe? _transport;
    private IDuplexPipe? _application;

    internal Task Running { get; private set; } = Task.CompletedTask;

    public PipeReader Input => _transport!.Input;

    public PipeWriter Output => _transport!.Output;

    public ServerSentEventsTransport(HttpClient httpClient, HttpConnectionOptions? httpConnectionOptions = null, ILoggerFactory? loggerFactory = null)
    {
        ArgumentNullThrowHelper.ThrowIfNull(httpClient);

        _httpClient = httpClient;
        _logger = (loggerFactory ?? NullLoggerFactory.Instance).CreateLogger<ServerSentEventsTransport>();
        _httpConnectionOptions = httpConnectionOptions ?? new();
    }

    public async Task StartAsync(Uri url, TransferFormat transferFormat, CancellationToken cancellationToken = default)
    {
        if (transferFormat != TransferFormat.Text)
        {
            throw new ArgumentException($"The '{transferFormat}' transfer format is not supported by this transport.", nameof(transferFormat));
        }

        Log.StartTransport(_logger, transferFormat);

        var request = new HttpRequestMessage(HttpMethod.Get, url);
        request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("text/event-stream"));

        HttpResponseMessage? response = null;

        try
        {
            response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
            response.EnsureSuccessStatusCode();
        }
        catch
        {
            response?.Dispose();

            Log.TransportStopping(_logger);

            throw;
        }

        // Create the pipe pair (Application's writer is connected to Transport's reader, and vice versa)
        var pair = DuplexPipe.CreateConnectionPair(_httpConnectionOptions.TransportPipeOptions, _httpConnectionOptions.AppPipeOptions);

        _transport = pair.Transport;
        _application = pair.Application;

        // Cancellation token will be triggered when the pipe is stopped on the client.
        // This is to avoid the client throwing from a 404 response caused by the
        // server stopping the connection while the send message request is in progress.
        // _application.Input.OnWriterCompleted((exception, state) => ((CancellationTokenSource)state).Cancel(), inputCts);

        Running = ProcessAsync(url, response);
    }

    private async Task ProcessAsync(Uri url, HttpResponseMessage response)
    {
        Debug.Assert(_application != null);

        // Start sending and polling (ask for binary if the server supports it)
        var receiving = ProcessEventStream(response, _transportCts.Token);
        var sending = SendUtils.SendMessages(url, _application, _httpClient, _logger, _inputCts.Token);

        // Wait for send or receive to complete
        var trigger = await Task.WhenAny(receiving, sending).ConfigureAwait(false);

        if (trigger == receiving)
        {
            // We're waiting for the application to finish and there are 2 things it could be doing
            // 1. Waiting for application data
            // 2. Waiting for an outgoing send (this should be instantaneous)

            _inputCts.Cancel();

            // Cancel the application so that ReadAsync yields
            _application.Input.CancelPendingRead();

            await sending.ConfigureAwait(false);
        }
        else
        {
            // Set the sending error so we communicate that to the application
            _error = sending.IsFaulted ? sending.Exception!.InnerException : null;

            _transportCts.Cancel();

            // Cancel any pending flush so that we can quit
            _application.Output.CancelPendingFlush();

            await receiving.ConfigureAwait(false);
        }
    }

    private async Task ProcessEventStream(HttpResponseMessage response, CancellationToken cancellationToken)
    {
        Debug.Assert(_application != null);

        Log.StartReceive(_logger);

        static void CancelReader(object? state) => ((PipeReader)state!).CancelPendingRead();

        using (response)
#pragma warning disable CA2016 // Forward the 'CancellationToken' parameter to methods
        using (var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false))
#pragma warning restore CA2016 // Forward the 'CancellationToken' parameter to methods
        {
            var reader = PipeReader.Create(stream);

            using var registration = cancellationToken.Register(CancelReader, reader);

            try
            {
                while (true)
                {
                    // We rely on the CancelReader callback to cancel pending reads. Do not pass the token to ReadAsync since that would result in an exception on cancelation.
                    var result = await reader.ReadAsync(default).ConfigureAwait(false);
                    var buffer = result.Buffer;
                    var consumed = buffer.Start;
                    var examined = buffer.End;

                    try
                    {
                        if (result.IsCanceled)
                        {
                            Log.ReceiveCanceled(_logger);
                            break;
                        }

                        if (!buffer.IsEmpty)
                        {
                            Log.ParsingSSE(_logger, buffer.Length);

                            var parseResult = _parser.ParseMessage(buffer, out consumed, out examined, out var message);
                            FlushResult flushResult = default;

                            switch (parseResult)
                            {
                                case ServerSentEventsMessageParser.ParseResult.Completed:
                                    Log.MessageToApplication(_logger, message!.Length);

                                    // When cancellationToken is canceled the next line will cancel pending flushes on the pipe unblocking the await.
                                    // Avoid passing the passed in context.
                                    flushResult = await _application.Output.WriteAsync(message, default).ConfigureAwait(false);

                                    _parser.Reset();
                                    break;
                                case ServerSentEventsMessageParser.ParseResult.Incomplete:
                                    if (result.IsCompleted)
                                    {
                                        throw new FormatException("Incomplete message.");
                                    }
                                    break;
                            }

                            // We canceled in the middle of applying back pressure
                            // or if the consumer is done
                            if (flushResult.IsCanceled || flushResult.IsCompleted)
                            {
                                Log.EventStreamEnded(_logger);
                                break;
                            }
                        }
                        else if (result.IsCompleted)
                        {
                            break;
                        }
                    }
                    finally
                    {
                        reader.AdvanceTo(consumed, examined);
                    }
                }
            }
            catch (Exception ex)
            {
                _error = ex;
            }
            finally
            {
                _application.Output.Complete(_error);

                Log.ReceiveStopped(_logger);

                reader.Complete();
            }
        }
    }

    public async Task StopAsync()
    {
        Log.TransportStopping(_logger);

        if (_application == null)
        {
            // We never started
            return;
        }

        _transport!.Output.Complete();
        _transport!.Input.Complete();

        _application.Input.CancelPendingRead();

        try
        {
            await Running.ConfigureAwait(false);
        }
        catch (Exception ex)
        {
            Log.TransportStopped(_logger, ex);
            throw;
        }

        Log.TransportStopped(_logger, null);
    }
}
