using System.Text.Json;
using System.Text.Json.Nodes;

namespace ZB.MOM.WW.ScadaBridge.ManagementService;

/// <summary>
/// Elides secret-bearing values inside opaque connection-config JSON blobs before
/// they leave the management boundary (responses AND audit afterState), mirroring
/// the SmtpConfigPublicShape/SmsConfigPublicShape pattern for structured entities.
/// A property is secret-bearing when its name contains one of the fragments below
/// (case-insensitive) and its value is a JSON string. MergeSentinels lets a client
/// round-trip a scrubbed config through update without wiping stored secrets.
/// </summary>
internal static class ConfigSecretScrubber
{
    public const string Sentinel = "***REDACTED***";

    private static readonly string[] SecretNameFragments =
        ["password", "secret", "token", "apikey", "credential", "passphrase"];

    internal static bool IsSecretName(string propertyName)
    {
        foreach (var fragment in SecretNameFragments)
        {
            if (propertyName.Contains(fragment, StringComparison.OrdinalIgnoreCase))
            {
                return true;
            }
        }

        return false;
    }

    /// <summary>
    /// Replaces every secret-bearing string value (at any depth) with <see cref="Sentinel"/>.
    /// Null/empty/parse-failure inputs are returned unchanged with hadSecrets=false.
    /// </summary>
    /// <param name="json">The connection-config JSON blob to scrub, or null/empty.</param>
    /// <returns>The scrubbed JSON (or the original input if unchanged) and whether any secrets were found.</returns>
    public static (string? scrubbed, bool hadSecrets) Scrub(string? json)
    {
        if (string.IsNullOrEmpty(json))
        {
            return (json, false);
        }

        JsonNode? root;
        try
        {
            root = JsonNode.Parse(json);
        }
        catch (JsonException)
        {
            return (json, false);
        }

        if (root is null)
        {
            return (json, false);
        }

        var hadSecrets = ScrubNode(root, propertyIsSecret: false);
        if (!hadSecrets)
        {
            return (json, false);
        }

        return (root.ToJsonString(), true);
    }

    private static bool ScrubNode(JsonNode? node, bool propertyIsSecret)
    {
        switch (node)
        {
            case JsonObject obj:
            {
                var had = false;
                // Snapshot keys so we can reassign values while iterating.
                foreach (var name in obj.Select(kvp => kvp.Key).ToList())
                {
                    var child = obj[name];
                    if (child is JsonValue && IsSecretName(name) && child.GetValueKind() == JsonValueKind.String)
                    {
                        obj[name] = Sentinel;
                        had = true;
                    }
                    else
                    {
                        had |= ScrubNode(child, IsSecretName(name));
                    }
                }

                return had;
            }

            case JsonArray arr:
            {
                var had = false;
                for (var i = 0; i < arr.Count; i++)
                {
                    var child = arr[i];
                    if (propertyIsSecret && child is JsonValue && child.GetValueKind() == JsonValueKind.String)
                    {
                        arr[i] = Sentinel;
                        had = true;
                    }
                    else
                    {
                        had |= ScrubNode(child, propertyIsSecret);
                    }
                }

                return had;
            }

            default:
                return false;
        }
    }

    /// <summary>
    /// Walks the incoming config; wherever a string value equals <see cref="Sentinel"/>,
    /// substitutes the value at the same path in <paramref name="stored"/> (if absent, the
    /// Sentinel is left in place — an update would store it verbatim, which is inert).
    /// Null/empty/parse-failure inputs pass through unchanged.
    /// </summary>
    /// <param name="incoming">The client-submitted config JSON, possibly containing sentinel values.</param>
    /// <param name="stored">The previously stored config JSON to source real secret values from.</param>
    /// <returns>The incoming JSON with sentinel values replaced by the corresponding stored values.</returns>
    /// <remarks>
    /// <b>Array limitation (arch-review R2 N9):</b> sentinel restoration inside arrays
    /// pairs incoming[i] with stored[i] BY INDEX. A client that reorders an array of
    /// credentialed objects while round-tripping sentinels grafts the wrong stored
    /// secret into each slot — a silent misconfiguration (never a disclosure). Clients
    /// must not reorder arrays that carry sentinels (the CLI round-trip does not);
    /// key-based matching is a possible future refinement if a config type ever nests
    /// credentialed arrays.
    /// </remarks>
    public static string? MergeSentinels(string? incoming, string? stored)
    {
        if (string.IsNullOrEmpty(incoming))
        {
            return incoming;
        }

        JsonNode? incomingRoot;
        try
        {
            incomingRoot = JsonNode.Parse(incoming);
        }
        catch (JsonException)
        {
            return incoming;
        }

        if (incomingRoot is null)
        {
            return incoming;
        }

        JsonNode? storedRoot = null;
        if (!string.IsNullOrEmpty(stored))
        {
            try
            {
                storedRoot = JsonNode.Parse(stored);
            }
            catch (JsonException)
            {
                storedRoot = null;
            }
        }

        MergeNode(incomingRoot, storedRoot);
        return incomingRoot.ToJsonString();
    }

    private static void MergeNode(JsonNode? incoming, JsonNode? stored)
    {
        switch (incoming)
        {
            case JsonObject obj:
            {
                var storedObj = stored as JsonObject;
                foreach (var name in obj.Select(kvp => kvp.Key).ToList())
                {
                    var child = obj[name];
                    JsonNode? storedChild = null;
                    storedObj?.TryGetPropertyValue(name, out storedChild);

                    if (child is JsonValue
                        && child.GetValueKind() == JsonValueKind.String
                        && child.GetValue<string>() == Sentinel)
                    {
                        if (storedChild is not null)
                        {
                            obj[name] = storedChild.DeepClone();
                        }
                    }
                    else
                    {
                        MergeNode(child, storedChild);
                    }
                }

                break;
            }

            case JsonArray arr:
            {
                var storedArr = stored as JsonArray;
                for (var i = 0; i < arr.Count; i++)
                {
                    var child = arr[i];
                    var storedChild = storedArr is not null && i < storedArr.Count ? storedArr[i] : null;

                    if (child is JsonValue
                        && child.GetValueKind() == JsonValueKind.String
                        && child.GetValue<string>() == Sentinel)
                    {
                        if (storedChild is not null)
                        {
                            arr[i] = storedChild.DeepClone();
                        }
                    }
                    else
                    {
                        MergeNode(child, storedChild);
                    }
                }

                break;
            }
        }
    }
}
