using Microsoft.Extensions.DependencyInjection;
using Serilog.Core;
using Serilog.Events;

namespace ZB.MOM.WW.Telemetry.Serilog;

/// <summary>
/// Applies a registered <see cref="ILogRedactor"/> to every Serilog log event. Registered
/// automatically by <see cref="ZbSerilogExtensions.AddZbSerilog"/>. The enricher resolves
/// <see cref="ILogRedactor"/> from DI on first use (lazy, to avoid a circular-DI problem during
/// Serilog's bootstrap); if none is registered it is permanently inert — no DI call per event.
/// Resolution is thread-safe: <see cref="LazyThreadSafetyMode.ExecutionAndPublication"/> ensures
/// exactly one DI lookup regardless of how many logging threads race to the first event.
/// </summary>
public sealed class RedactionEnricher : ILogEventEnricher
{
    private readonly Lazy<ILogRedactor?> _redactor;

    /// <summary>
    /// Creates the enricher bound to a service provider from which the project-supplied
    /// <see cref="ILogRedactor"/> is resolved lazily on first use (thread-safe).
    /// </summary>
    /// <param name="serviceProvider">Provider used to resolve a registered <see cref="ILogRedactor"/>.</param>
    public RedactionEnricher(IServiceProvider serviceProvider)
    {
        ArgumentNullException.ThrowIfNull(serviceProvider);
        _redactor = new Lazy<ILogRedactor?>(
            () => serviceProvider.GetService<ILogRedactor>(),
            LazyThreadSafetyMode.ExecutionAndPublication);
    }

    /// <summary>
    /// Hands the log event's properties to the registered <see cref="ILogRedactor"/> and reconciles
    /// the result back onto the event. No-op when no redactor is registered or the event carries no
    /// properties.
    /// <para>
    /// Each property is projected into a mutable view the redactor can edit: a scalar is its
    /// unwrapped value, and a structured value (<see cref="StructureValue"/> from <c>{@Object}</c>,
    /// <see cref="SequenceValue"/>, <see cref="DictionaryValue"/>) becomes a nested
    /// <see cref="IDictionary{TKey,TValue}"/> / <see cref="IList{T}"/> the redactor can descend into —
    /// recursively — so a field nested inside a destructured object can be masked or removed. After
    /// redaction each property is rebuilt and written back only when it actually changed; the
    /// structure's type tag and a dictionary's original keys are preserved on rebuild, and keys the
    /// redactor removed are deleted via <c>RemovePropertyIfPresent</c>. Properties the redactor does
    /// not touch are left intact and are not reallocated.
    /// </para>
    /// </summary>
    /// <param name="logEvent">The log event to redact.</param>
    /// <param name="propertyFactory">Unused; structured values are rebuilt directly into Serilog values.</param>
    public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
    {
        ArgumentNullException.ThrowIfNull(logEvent);
        ArgumentNullException.ThrowIfNull(propertyFactory);

        var redactor = _redactor.Value;
        if (redactor is null)
        {
            return;
        }

        // Hot path: an event with no properties has nothing to redact — skip the snapshot copy.
        if (logEvent.Properties.Count == 0)
        {
            return;
        }

        // Project every property into a mutable view. Scalars stay as their CLR value (zero extra
        // allocation); structured values become nested dictionaries/lists carrying enough metadata
        // (node kind, type tag, original dictionary keys) to be rebuilt faithfully.
        var snapshot = new Dictionary<string, object?>(logEvent.Properties.Count, StringComparer.Ordinal);
        foreach (var property in logEvent.Properties)
        {
            snapshot[property.Key] = Project(property.Value);
        }

        // Capture the original key set so we can honour deletions: any key the redactor drops from
        // the snapshot must be removed from the event (not silently retained).
        var originalKeys = new HashSet<string>(snapshot.Keys, StringComparer.Ordinal);

        redactor.Redact(snapshot);

        foreach (var entry in snapshot)
        {
            // Rebuild the (possibly redacted) value and write it back only when it differs from what
            // the event already holds, so an untouched property is never needlessly reallocated.
            var rebuilt = Rebuild(entry.Value);
            if (!logEvent.Properties.TryGetValue(entry.Key, out var existing) || !ValueEquals(existing, rebuilt))
            {
                logEvent.AddOrUpdateProperty(new LogEventProperty(entry.Key, rebuilt));
            }
        }

        // Reconcile removals: a redactor that deleted a key from the snapshot (e.g.
        // properties.Remove("apiKey")) means that property must not reach any sink.
        foreach (var key in originalKeys)
        {
            if (!snapshot.ContainsKey(key))
            {
                logEvent.RemovePropertyIfPresent(key);
            }
        }
    }

    /// <summary>
    /// Projects an immutable Serilog value into a mutable view the redactor can edit. Scalars unwrap
    /// to their CLR value; structures/sequences/dictionaries become nested mutable wrappers that
    /// remember their kind (and a structure's type tag / a dictionary's original keys) for rebuild.
    /// </summary>
    private static object? Project(LogEventPropertyValue value)
    {
        switch (value)
        {
            case ScalarValue scalar:
                return scalar.Value;

            case StructureValue structure:
                var projectedStructure = new ProjectedStructure(structure.TypeTag, structure.Properties.Count);
                foreach (var property in structure.Properties)
                {
                    projectedStructure[property.Name] = Project(property.Value);
                }

                return projectedStructure;

            case SequenceValue sequence:
                var projectedSequence = new ProjectedSequence(sequence.Elements.Count);
                foreach (var element in sequence.Elements)
                {
                    projectedSequence.Add(Project(element));
                }

                return projectedSequence;

            case DictionaryValue dictionary:
                var projectedDictionary = new ProjectedDictionary(dictionary.Elements.Count);
                foreach (var pair in dictionary.Elements)
                {
                    var key = pair.Key.Value?.ToString() ?? NullKey;
                    projectedDictionary[key] = Project(pair.Value);
                    projectedDictionary.OriginalKeys[key] = pair.Key;
                }

                return projectedDictionary;

            default:
                // Unknown future LogEventPropertyValue subtype — pass the wrapper through untouched.
                return value;
        }
    }

    /// <summary>
    /// Rebuilds a (possibly redacted) projected value back into an immutable Serilog value. The
    /// inverse of <see cref="Project"/>; also accepts plain dictionaries/lists a redactor synthesised
    /// and leaf CLR values it substituted.
    /// </summary>
    private static LogEventPropertyValue Rebuild(object? projected)
    {
        switch (projected)
        {
            case ProjectedStructure structure:
                return new StructureValue(RebuildProperties(structure), structure.TypeTag);

            case ProjectedDictionary dictionary:
                var pairs = new List<KeyValuePair<ScalarValue, LogEventPropertyValue>>(dictionary.Count);
                foreach (var entry in dictionary)
                {
                    var key = dictionary.OriginalKeys.TryGetValue(entry.Key, out var original)
                        ? original
                        : new ScalarValue(entry.Key);
                    pairs.Add(new KeyValuePair<ScalarValue, LogEventPropertyValue>(key, Rebuild(entry.Value)));
                }

                return new DictionaryValue(pairs);

            case ProjectedSequence sequence:
                return new SequenceValue(RebuildElements(sequence));

            case IDictionary<string, object?> injected:
                // A redactor synthesised a new structure (plain dictionary) — rebuild as a StructureValue.
                return new StructureValue(RebuildProperties(injected));

            case IList<object?> injectedList:
                return new SequenceValue(RebuildElements(injectedList));

            case LogEventPropertyValue raw:
                // An unknown subtype passed through by Project, or a value the redactor set directly.
                return raw;

            default:
                return new ScalarValue(projected);
        }
    }

    private static List<LogEventProperty> RebuildProperties(IDictionary<string, object?> source)
    {
        var properties = new List<LogEventProperty>(source.Count);
        foreach (var entry in source)
        {
            properties.Add(new LogEventProperty(entry.Key, Rebuild(entry.Value)));
        }

        return properties;
    }

    private static List<LogEventPropertyValue> RebuildElements(IList<object?> source)
    {
        var elements = new List<LogEventPropertyValue>(source.Count);
        foreach (var element in source)
        {
            elements.Add(Rebuild(element));
        }

        return elements;
    }

    /// <summary>
    /// Structural equality between two Serilog values, used to skip writing back a property the
    /// redactor left unchanged. Compares scalars by value and structures/sequences/dictionaries by
    /// their contents (recursively); unknown kinds fall back to reference equality.
    /// </summary>
    private static bool ValueEquals(LogEventPropertyValue left, LogEventPropertyValue right)
    {
        switch (left)
        {
            case ScalarValue scalar when right is ScalarValue otherScalar:
                return Equals(scalar.Value, otherScalar.Value);

            case StructureValue structure when right is StructureValue otherStructure:
                if (structure.TypeTag != otherStructure.TypeTag
                    || structure.Properties.Count != otherStructure.Properties.Count)
                {
                    return false;
                }

                foreach (var property in structure.Properties)
                {
                    var match = FindProperty(otherStructure, property.Name);
                    if (match is null || !ValueEquals(property.Value, match.Value))
                    {
                        return false;
                    }
                }

                return true;

            case SequenceValue sequence when right is SequenceValue otherSequence:
                if (sequence.Elements.Count != otherSequence.Elements.Count)
                {
                    return false;
                }

                for (var i = 0; i < sequence.Elements.Count; i++)
                {
                    if (!ValueEquals(sequence.Elements[i], otherSequence.Elements[i]))
                    {
                        return false;
                    }
                }

                return true;

            case DictionaryValue dictionary when right is DictionaryValue otherDictionary:
                if (dictionary.Elements.Count != otherDictionary.Elements.Count)
                {
                    return false;
                }

                foreach (var pair in dictionary.Elements)
                {
                    var match = FindDictionaryValue(otherDictionary, pair.Key);
                    if (match is null || !ValueEquals(pair.Value, match))
                    {
                        return false;
                    }
                }

                return true;

            default:
                return ReferenceEquals(left, right);
        }
    }

    private static LogEventProperty? FindProperty(StructureValue structure, string name)
    {
        foreach (var property in structure.Properties)
        {
            if (property.Name == name)
            {
                return property;
            }
        }

        return null;
    }

    private static LogEventPropertyValue? FindDictionaryValue(DictionaryValue dictionary, ScalarValue key)
    {
        foreach (var pair in dictionary.Elements)
        {
            if (Equals(pair.Key.Value, key.Value))
            {
                return pair.Value;
            }
        }

        return null;
    }

    private const string NullKey = "";

    /// <summary>A destructured object projected to a mutable dictionary; preserves its type tag.</summary>
    private sealed class ProjectedStructure : Dictionary<string, object?>
    {
        public ProjectedStructure(string? typeTag, int capacity)
            : base(capacity, StringComparer.Ordinal) => TypeTag = typeTag;

        public string? TypeTag { get; }
    }

    /// <summary>A logged dictionary projected to a mutable dictionary; preserves the original keys.</summary>
    private sealed class ProjectedDictionary : Dictionary<string, object?>
    {
        public ProjectedDictionary(int capacity)
            : base(capacity, StringComparer.Ordinal) =>
            OriginalKeys = new Dictionary<string, ScalarValue>(capacity, StringComparer.Ordinal);

        public Dictionary<string, ScalarValue> OriginalKeys { get; }
    }

    /// <summary>A logged collection projected to a mutable list.</summary>
    private sealed class ProjectedSequence : List<object?>
    {
        public ProjectedSequence(int capacity) : base(capacity)
        {
        }
    }
}
