using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using OpenTelemetry.Metrics;
using OpenTelemetry.Trace;

namespace ZB.MOM.WW.Telemetry;

/// <summary>
/// Extension point for configuring the OpenTelemetry metrics + traces bootstrap on an
/// <see cref="IHostApplicationBuilder"/> (or directly on an <see cref="IServiceCollection"/>).
/// Wires the shared Resource, standard instrumentation, the app's own Meters and
/// ActivitySources, and the selected exporter. Does NOT configure Serilog.
/// </summary>
public static class ZbTelemetryExtensions
{
    /// <summary>
    /// Configures the OpenTelemetry MeterProvider and TracerProvider with the shared Resource,
    /// standard instrumentation (ASP.NET Core, HttpClient, gRPC client, runtime, process), the
    /// app's own Meters and ActivitySources, and the selected exporter.
    /// </summary>
    /// <param name="builder">The host application builder.</param>
    /// <param name="configure">Populates the <see cref="ZbTelemetryOptions"/>.</param>
    public static IHostApplicationBuilder AddZbTelemetry(
        this IHostApplicationBuilder builder,
        Action<ZbTelemetryOptions> configure)
    {
        ArgumentNullException.ThrowIfNull(builder);
        ArgumentNullException.ThrowIfNull(configure);
        builder.Services.AddZbTelemetry(BuildOptions(configure));
        return builder;
    }

    /// <summary>
    /// <see cref="IServiceCollection"/> overload for contexts where
    /// <see cref="IHostApplicationBuilder"/> is not available. Requires the caller to supply a
    /// pre-built <see cref="ZbTelemetryOptions"/>.
    /// </summary>
    /// <param name="services">The service collection.</param>
    /// <param name="options">The fully-populated telemetry options.</param>
    public static IServiceCollection AddZbTelemetry(
        this IServiceCollection services,
        ZbTelemetryOptions options)
    {
        ArgumentNullException.ThrowIfNull(services);
        ArgumentNullException.ThrowIfNull(options);

        services.AddOpenTelemetry()
            .ConfigureResource(rb => ZbResource.Configure(rb, options))
            .WithMetrics(metrics =>
            {
                foreach (var meter in options.Meters)
                {
                    metrics.AddMeter(meter);
                }

                metrics
                    .AddAspNetCoreInstrumentation()
                    .AddHttpClientInstrumentation()
                    .AddRuntimeInstrumentation()
                    .AddProcessInstrumentation();

                ApplyMetricsExporter(metrics, options);
            })
            .WithTracing(tracing =>
            {
                foreach (var source in options.ActivitySources)
                {
                    tracing.AddSource(source);
                }

                tracing
                    .AddAspNetCoreInstrumentation()
                    .AddHttpClientInstrumentation()
                    .AddGrpcClientInstrumentation();

                ApplyTracingExporter(tracing, options);
            });

        return services;
    }

    /// <summary>
    /// IServiceCollection overload that accepts a configure delegate (convenience for callers
    /// that only have an <see cref="IServiceCollection"/> but prefer the lambda form).
    /// </summary>
    /// <param name="services">The service collection.</param>
    /// <param name="configure">Populates the <see cref="ZbTelemetryOptions"/>.</param>
    public static IServiceCollection AddZbTelemetry(
        this IServiceCollection services,
        Action<ZbTelemetryOptions> configure) =>
        services.AddZbTelemetry(BuildOptions(configure));

    private static ZbTelemetryOptions BuildOptions(Action<ZbTelemetryOptions> configure)
    {
        ArgumentNullException.ThrowIfNull(configure);
        var options = new ZbTelemetryOptions();
        configure(options);
        if (string.IsNullOrWhiteSpace(options.ServiceName))
        {
            throw new ArgumentException(
                "ZbTelemetryOptions.ServiceName is required (e.g. \"otopcua\").",
                nameof(configure));
        }
        ZbTelemetryOptionsValidator.Validate(options, nameof(configure));
        return options;
    }

    private static void ApplyMetricsExporter(MeterProviderBuilder metrics, ZbTelemetryOptions options)
    {
        // Prometheus is always wired so that /metrics and MapZbMetrics() work regardless of
        // the exporter setting. OTLP is an additive overlay when explicitly requested.
        metrics.AddPrometheusExporter();
        if (options.Exporter == ZbExporter.Otlp)
        {
            metrics.AddOtlpExporter(o => ConfigureOtlp(o, options));
        }
    }

    private static void ApplyTracingExporter(TracerProviderBuilder tracing, ZbTelemetryOptions options)
    {
        // Prometheus is metrics-only; traces have no Prometheus path. Only OTLP exports traces.
        if (options.Exporter == ZbExporter.Otlp)
        {
            tracing.AddOtlpExporter(o => ConfigureOtlp(o, options));
        }
    }

    private static void ConfigureOtlp(
        OpenTelemetry.Exporter.OtlpExporterOptions otlp,
        ZbTelemetryOptions options)
    {
        if (!string.IsNullOrEmpty(options.OtlpEndpoint))
        {
            otlp.Endpoint = new Uri(options.OtlpEndpoint);
        }
    }
}
