using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace ZB.MOM.WW.ScadaBridge.Commons.Types.Audit;

/// <summary>
/// Serializes and deserializes <see cref="AuditDetails"/> to/from the JSON string that
/// occupies <c>ZB.MOM.WW.Audit.AuditEvent.DetailsJson</c>.
/// </summary>
/// <remarks>
/// <para>
/// The options are DETERMINISTIC by design — this is a load-bearing contract:
/// canonical <c>AuditEvent</c> uses value-equality (record) and consumers dedup on it, so
/// two independent emitters must produce byte-identical <c>DetailsJson</c> for equal inputs.
/// The options that make this deterministic:
/// <list type="bullet">
///   <item><see cref="JsonNamingPolicy.CamelCase"/> — stable camelCase key names.</item>
///   <item><c>WriteIndented = false</c> — no whitespace variation.</item>
///   <item><see cref="JsonIgnoreCondition.WhenWritingNull"/> — null fields absent; non-nullable
///     <c>bool</c> fields (like <see cref="AuditDetails.PayloadTruncated"/>) always present.</item>
///   <item><see cref="JavaScriptEncoder.UnsafeRelaxedJsonEscaping"/> — angle brackets and other
///     extended characters (including <c>&lt;redacted&gt;</c> markers) are NOT percent-escaped,
///     preserving the exact byte value of any existing redaction strings.</item>
///   <item>Property declaration order on <see cref="AuditDetails"/> fixes key order —
///     System.Text.Json honours declared order, so serialization is stable across calls.</item>
/// </list>
/// </para>
/// </remarks>
public static class AuditDetailsCodec
{
    /// <summary>
    /// Single, cached, immutable options instance. Re-using one instance avoids repeated
    /// reflection overhead and guarantees the same encoder is used across all serialization calls.
    /// </summary>
    private static readonly JsonSerializerOptions Options = new()
    {
        PropertyNamingPolicy        = JsonNamingPolicy.CamelCase,
        WriteIndented               = false,
        DefaultIgnoreCondition      = JsonIgnoreCondition.WhenWritingNull,
        Encoder                     = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
    };

    /// <summary>
    /// Serializes <paramref name="details"/> to a compact, deterministic JSON string
    /// suitable for storage in <c>AuditEvent.DetailsJson</c>.
    /// </summary>
    /// <param name="details">The audit details instance to serialize.</param>
    /// <returns>A compact, deterministic JSON string representing the audit details.</returns>
    public static string Serialize(AuditDetails details)
        => JsonSerializer.Serialize(details, Options);

    /// <summary>
    /// Deserializes a <see cref="AuditDetails"/> from <paramref name="json"/>.
    /// Returns an empty (all-null) <see cref="AuditDetails"/> when <paramref name="json"/>
    /// is <c>null</c>, empty, or whitespace — never throws.
    /// </summary>
    /// <param name="json">The JSON string to deserialize; null or whitespace returns an empty instance.</param>
    /// <returns>The deserialized <see cref="AuditDetails"/>, or an empty instance on null/invalid input.</returns>
    public static AuditDetails Deserialize(string? json)
    {
        if (string.IsNullOrWhiteSpace(json))
            return new AuditDetails();

        try
        {
            return JsonSerializer.Deserialize<AuditDetails>(json, Options)
                   ?? new AuditDetails();
        }
        catch (JsonException)
        {
            return new AuditDetails();
        }
    }
}
