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

using System.Diagnostics;
using System.Diagnostics.Metrics;

namespace Microsoft.AspNetCore.Http.Connections.Internal;

internal readonly struct MetricsContext
{
    public MetricsContext(bool connectionDurationEnabled, bool currentConnectionsCounterEnabled)
    {
        ConnectionDurationEnabled = connectionDurationEnabled;
        CurrentConnectionsCounterEnabled = currentConnectionsCounterEnabled;
    }

    public bool ConnectionDurationEnabled { get; }
    public bool CurrentConnectionsCounterEnabled { get; }
}

internal sealed class HttpConnectionsMetrics : IDisposable
{
    public const string MeterName = "Microsoft.AspNetCore.Http.Connections";

    private readonly Meter _meter;
    private readonly Histogram<double> _connectionDuration;
    private readonly UpDownCounter<long> _currentConnectionsCounter;

    public HttpConnectionsMetrics(IMeterFactory meterFactory)
    {
        _meter = meterFactory.Create(MeterName);

        _connectionDuration = _meter.CreateHistogram<double>(
            "signalr.server.connection.duration",
            unit: "s",
            description: "The duration of connections on the server.");

        _currentConnectionsCounter = _meter.CreateUpDownCounter<long>(
            "signalr.server.active_connections",
            unit: "{connection}",
            description: "Number of connections that are currently active on the server.");
    }

    public void ConnectionStop(in MetricsContext metricsContext, HttpTransportType transportType, HttpConnectionStopStatus status, long startTimestamp, long currentTimestamp)
    {
        if (metricsContext.ConnectionDurationEnabled)
        {
            var duration = Stopwatch.GetElapsedTime(startTimestamp, currentTimestamp);
            _connectionDuration.Record(duration.TotalSeconds,
                new KeyValuePair<string, object?>("signalr.connection.status", ResolveStopStatus(status)),
                new KeyValuePair<string, object?>("signalr.transport", ResolveTransportType(transportType)));
        }
    }

    public void ConnectionTransportStart(in MetricsContext metricsContext, HttpTransportType transportType)
    {
        Debug.Assert(transportType != HttpTransportType.None);

        // Tags must match transport end.
        if (metricsContext.CurrentConnectionsCounterEnabled)
        {
            _currentConnectionsCounter.Add(1, new KeyValuePair<string, object?>("signalr.transport", ResolveTransportType(transportType)));
        }
    }

    public void TransportStop(in MetricsContext metricsContext, HttpTransportType transportType)
    {
        if (metricsContext.CurrentConnectionsCounterEnabled)
        {
            // Tags must match transport start.
            // If the transport type is none then the transport was never started for this connection.
            if (transportType != HttpTransportType.None)
            {
                _currentConnectionsCounter.Add(-1, new KeyValuePair<string, object?>("signalr.transport", ResolveTransportType(transportType)));
            }
        }
    }

    private static string ResolveTransportType(HttpTransportType transportType)
    {
        return transportType switch
        {
            HttpTransportType.ServerSentEvents => "server_sent_events",
            HttpTransportType.LongPolling => "long_polling",
            HttpTransportType.WebSockets => "web_sockets",
            _ => throw new InvalidOperationException("Unexpected value: " + transportType)
        };
    }

    private static string ResolveStopStatus(HttpConnectionStopStatus connectionStopStatus)
    {
        return connectionStopStatus switch
        {
            HttpConnectionStopStatus.NormalClosure => "normal_closure",
            HttpConnectionStopStatus.Timeout => "timeout",
            HttpConnectionStopStatus.AppShutdown => "app_shutdown",
            _ => throw new InvalidOperationException("Unexpected value: " + connectionStopStatus)
        };
    }

    public void Dispose()
    {
        _meter.Dispose();
    }

    public MetricsContext CreateContext()
    {
        return new MetricsContext(_connectionDuration.Enabled, _currentConnectionsCounter.Enabled);
    }
}
