using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using OpenTelemetry.Metrics;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;
using Serilog;
using Serilog.Core;
using Serilog.Events;
using StellaOps.Feedser.Core.Jobs;
using StellaOps.Feedser.Source.Common.Telemetry;
using StellaOps.Feedser.WebService.Diagnostics;
using StellaOps.Feedser.WebService.Options;

namespace StellaOps.Feedser.WebService.Extensions;

public static class TelemetryExtensions
{
    public static void ConfigureFeedserTelemetry(this WebApplicationBuilder builder, FeedserOptions options)
    {
        ArgumentNullException.ThrowIfNull(builder);
        ArgumentNullException.ThrowIfNull(options);

        var telemetry = options.Telemetry ?? new FeedserOptions.TelemetryOptions();

        if (telemetry.EnableLogging)
        {
            builder.Host.UseSerilog((context, services, configuration) =>
            {
                ConfigureSerilog(configuration, telemetry, builder.Environment.EnvironmentName, builder.Environment.ApplicationName);
            });
        }

        if (!telemetry.Enabled || (!telemetry.EnableTracing && !telemetry.EnableMetrics))
        {
            return;
        }

        var openTelemetry = builder.Services.AddOpenTelemetry();

        openTelemetry.ConfigureResource(resource =>
        {
            var serviceName = telemetry.ServiceName ?? builder.Environment.ApplicationName;
            var version = Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? "unknown";

            resource.AddService(serviceName, serviceVersion: version, serviceInstanceId: Environment.MachineName);
            resource.AddAttributes(new[]
            {
                new KeyValuePair<string, object>("deployment.environment", builder.Environment.EnvironmentName),
            });

            foreach (var attribute in telemetry.ResourceAttributes)
            {
                if (string.IsNullOrWhiteSpace(attribute.Key) || attribute.Value is null)
                {
                    continue;
                }

                resource.AddAttributes(new[] { new KeyValuePair<string, object>(attribute.Key, attribute.Value) });
            }
        });

        if (telemetry.EnableTracing)
        {
            openTelemetry.WithTracing(tracing =>
            {
                tracing
                    .AddSource(JobDiagnostics.ActivitySourceName)
                    .AddSource(SourceDiagnostics.ActivitySourceName)
                    .AddAspNetCoreInstrumentation()
                    .AddHttpClientInstrumentation();

                ConfigureExporters(telemetry, tracing);
            });
        }

        if (telemetry.EnableMetrics)
        {
            openTelemetry.WithMetrics(metrics =>
            {
                metrics
                    .AddMeter(JobDiagnostics.MeterName)
                    .AddMeter(SourceDiagnostics.MeterName)
                    .AddMeter("StellaOps.Feedser.Source.CertBund")
                    .AddMeter("StellaOps.Feedser.Source.Nvd")
                    .AddMeter("StellaOps.Feedser.Source.Vndr.Chromium")
                    .AddMeter("StellaOps.Feedser.Source.Vndr.Apple")
                    .AddMeter("StellaOps.Feedser.Source.Vndr.Adobe")
                    .AddMeter(JobMetrics.MeterName)
                    .AddAspNetCoreInstrumentation()
                    .AddHttpClientInstrumentation()
                    .AddRuntimeInstrumentation();

                ConfigureExporters(telemetry, metrics);
            });
        }
    }

    private static void ConfigureSerilog(LoggerConfiguration configuration, FeedserOptions.TelemetryOptions telemetry, string environmentName, string applicationName)
    {
        if (!Enum.TryParse(telemetry.MinimumLogLevel, ignoreCase: true, out LogEventLevel level))
        {
            level = LogEventLevel.Information;
        }

        configuration
            .MinimumLevel.Is(level)
            .MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
            .MinimumLevel.Override("Microsoft.Hosting.Lifetime", LogEventLevel.Information)
            .Enrich.FromLogContext()
            .Enrich.With<ActivityEnricher>()
            .Enrich.WithProperty("service.name", telemetry.ServiceName ?? applicationName)
            .Enrich.WithProperty("deployment.environment", environmentName)
            .WriteTo.Console(outputTemplate: "[{Timestamp:O}] [{Level:u3}] {Message:lj} {Properties}{NewLine}{Exception}");
    }

    private static void ConfigureExporters(FeedserOptions.TelemetryOptions telemetry, TracerProviderBuilder tracing)
    {
        if (string.IsNullOrWhiteSpace(telemetry.OtlpEndpoint))
        {
            if (telemetry.ExportConsole)
            {
                tracing.AddConsoleExporter();
            }

            return;
        }

        tracing.AddOtlpExporter(options =>
        {
            options.Endpoint = new Uri(telemetry.OtlpEndpoint);
            var headers = BuildHeaders(telemetry);
            if (!string.IsNullOrEmpty(headers))
            {
                options.Headers = headers;
            }
        });

        if (telemetry.ExportConsole)
        {
            tracing.AddConsoleExporter();
        }
    }

    private static void ConfigureExporters(FeedserOptions.TelemetryOptions telemetry, MeterProviderBuilder metrics)
    {
        if (string.IsNullOrWhiteSpace(telemetry.OtlpEndpoint))
        {
            if (telemetry.ExportConsole)
            {
                metrics.AddConsoleExporter();
            }

            return;
        }

        metrics.AddOtlpExporter(options =>
        {
            options.Endpoint = new Uri(telemetry.OtlpEndpoint);
            var headers = BuildHeaders(telemetry);
            if (!string.IsNullOrEmpty(headers))
            {
                options.Headers = headers;
            }
        });

        if (telemetry.ExportConsole)
        {
            metrics.AddConsoleExporter();
        }
    }

    private static string? BuildHeaders(FeedserOptions.TelemetryOptions telemetry)
    {
        if (telemetry.OtlpHeaders.Count == 0)
        {
            return null;
        }

        return string.Join(",", telemetry.OtlpHeaders
            .Where(static kvp => !string.IsNullOrWhiteSpace(kvp.Key) && !string.IsNullOrWhiteSpace(kvp.Value))
            .Select(static kvp => $"{kvp.Key}={kvp.Value}"));
    }
}

internal sealed class ActivityEnricher : ILogEventEnricher
{
    public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
    {
        var activity = Activity.Current;
        if (activity is null)
        {
            return;
        }

        if (activity.TraceId != default)
        {
            logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty("trace_id", activity.TraceId.ToString()));
        }

        if (activity.SpanId != default)
        {
            logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty("span_id", activity.SpanId.ToString()));
        }

        if (activity.ParentSpanId != default)
        {
            logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty("parent_span_id", activity.ParentSpanId.ToString()));
        }

        if (!string.IsNullOrEmpty(activity.TraceStateString))
        {
            logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty("trace_state", activity.TraceStateString));
        }
    }
}
