using System;
using System.Collections.Generic;
using Serilog;
using Serilog.Configuration;
using Serilog.Sinks.OpenTelemetry;
using ZB.MOM.WW.Telemetry;

namespace ZB.MOM.WW.Telemetry.Serilog;

/// <summary>
/// Reusable seam that applies the shared ZB.MOM.WW logging configuration (identity enrichers,
/// trace-context correlation, redaction, and OTel log export) to a
/// <see cref="LoggerConfiguration"/>. Shared by <see cref="ZbSerilogExtensions.AddZbSerilog"/>
/// and unit tests so both exercise an identical enricher/sink set.
/// Internal to keep the public NuGet surface minimal; exposed to the test assembly via
/// <c>[assembly: InternalsVisibleTo("ZB.MOM.WW.Telemetry.Serilog.Tests")]</c>.
/// </summary>
internal static class ZbSerilogConfig
{
    /// <summary>
    /// Applies the shared identity enrichers — <see cref="ZbLogEnricherNames.SiteId"/> and
    /// <see cref="ZbLogEnricherNames.NodeRole"/> from <paramref name="options"/>, and
    /// <see cref="ZbLogEnricherNames.NodeHostname"/> from
    /// <see cref="System.Environment.MachineName"/> (auto, never a caller-supplied option) — to
    /// <paramref name="loggerConfiguration"/>. <c>SiteId</c>/<c>NodeRole</c> are stamped only when
    /// the option is non-null/non-empty, mirroring the shared OTel Resource omission rules.
    /// </summary>
    /// <param name="loggerConfiguration">The Serilog configuration to enrich.</param>
    /// <param name="options">The telemetry options describing the service identity.</param>
    /// <returns>The same <paramref name="loggerConfiguration"/> for chaining.</returns>
    public static LoggerConfiguration Apply(
        LoggerConfiguration loggerConfiguration,
        ZbTelemetryOptions options) =>
        Apply(loggerConfiguration, options, serviceProvider: null);

    /// <summary>
    /// Overload of <see cref="Apply(LoggerConfiguration, ZbTelemetryOptions)"/> that additionally
    /// wires the service-provider-dependent stages — the redaction enricher (which lazily resolves
    /// a registered <c>ILogRedactor</c>). When <paramref name="serviceProvider"/> is null, only the
    /// provider-independent enrichers are applied.
    /// </summary>
    /// <param name="loggerConfiguration">The Serilog configuration to enrich.</param>
    /// <param name="options">The telemetry options describing the service identity.</param>
    /// <param name="serviceProvider">
    /// Provider used to lazily resolve project-supplied seams (e.g. <c>ILogRedactor</c>);
    /// may be null in tests or pipelines without DI.
    /// </param>
    /// <returns>The same <paramref name="loggerConfiguration"/> for chaining.</returns>
    public static LoggerConfiguration Apply(
        LoggerConfiguration loggerConfiguration,
        ZbTelemetryOptions options,
        IServiceProvider? serviceProvider)
    {
        ArgumentNullException.ThrowIfNull(loggerConfiguration);
        ArgumentNullException.ThrowIfNull(options);

        LoggerEnrichmentConfiguration enrich = loggerConfiguration.Enrich;

        if (!string.IsNullOrEmpty(options.SiteId))
        {
            enrich.WithProperty(ZbLogEnricherNames.SiteId, options.SiteId);
        }

        if (!string.IsNullOrEmpty(options.NodeRole))
        {
            enrich.WithProperty(ZbLogEnricherNames.NodeRole, options.NodeRole);
        }

        enrich.WithProperty(ZbLogEnricherNames.NodeHostname, Environment.MachineName);

        enrich.With(new TraceContextEnricher());

        if (serviceProvider is not null)
        {
            enrich.With(new RedactionEnricher(serviceProvider));
        }

        ApplyOpenTelemetryExport(loggerConfiguration, options);

        return loggerConfiguration;
    }

    /// <summary>
    /// Adds a <c>WriteTo.OpenTelemetry</c> log sink when an OTLP exporter is explicitly
    /// selected (<see cref="ZbTelemetryOptions.Exporter"/> = <see cref="ZbExporter.Otlp"/>).
    /// <see cref="ZbTelemetryOptions.OtlpEndpoint"/> is the address used when OTLP is selected
    /// — it is NOT an independent enable. This matches the core OTel path behaviour so that
    /// an endpoint-only config (without <c>Exporter=Otlp</c>) exports nothing to OTLP on any
    /// signal. The sink carries the same Resource attributes as <c>ZbResource</c>
    /// (<c>service.name</c>/<c>service.namespace</c>/<c>service.version</c>/
    /// <c>service.instance.id</c>/<c>site.id</c>/<c>node.role</c>/<c>host.name</c>) so logs
    /// correlate with metrics and traces in the backend.
    /// </summary>
    private static void ApplyOpenTelemetryExport(
        LoggerConfiguration loggerConfiguration,
        ZbTelemetryOptions options)
    {
        if (options.Exporter != ZbExporter.Otlp)
        {
            return;
        }

        var resourceAttributes = BuildResourceAttributes(options);

        loggerConfiguration.WriteTo.OpenTelemetry(sink =>
        {
            if (!string.IsNullOrEmpty(options.OtlpEndpoint))
            {
                sink.Endpoint = options.OtlpEndpoint;
            }

            sink.Protocol = OtlpProtocol.Grpc;
            sink.ResourceAttributes = resourceAttributes;
        });
    }

    /// <summary>
    /// Builds the OTLP log-sink Resource-attribute map. This is <em>not</em> a parallel
    /// implementation: it is derived directly from <see cref="ZbResource.BuildAttributes"/> — the
    /// single source of truth shared with the OTel SDK metrics/traces pipeline — so the log sink can
    /// never drift from metrics and traces. Returned as a fresh mutable copy because the
    /// Serilog OpenTelemetry sink takes ownership of the dictionary it is handed. Internal so it can
    /// be asserted by the test assembly without being part of the public NuGet API.
    /// </summary>
    internal static IDictionary<string, object> BuildResourceAttributes(ZbTelemetryOptions options) =>
        new Dictionary<string, object>(ZbResource.BuildAttributes(options), StringComparer.Ordinal);
}
