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

namespace ClawdDotNet.Core.Security;

/// <summary>
/// Schreibt Zugangsdaten in einer JSON-Datei um, ohne sonst etwas zu verändern.
///
/// Bewusst auf dem JSON-Baum statt über die typisierten Konfigurationsklassen:
/// Beim Deserialisieren und erneuten Serialisieren gingen unbekannte Felder verloren.
/// Eine Sicherung darf aber nichts wegwerfen, nur weil eine ältere Programmfassung
/// ein Feld nicht kennt.
///
/// Welche Felder betroffen sind, entscheidet <see cref="ConfigSecrets.IsSecretKey"/> —
/// dieselbe Liste wie im laufenden Betrieb.
/// </summary>
public static class JsonSecretRewriter
{
    private static readonly JsonSerializerOptions WriteOptions = new() { WriteIndented = true };

    private static readonly JsonNodeOptions NodeOptions = new() { PropertyNameCaseInsensitive = false };

    private static readonly JsonDocumentOptions DocumentOptions = new()
    {
        CommentHandling = JsonCommentHandling.Skip,
        AllowTrailingCommas = true
    };

    /// <summary>
    /// Wendet <paramref name="transform"/> auf alle Werte an, deren Feldname als
    /// Zugangsdatum gilt. Gibt das neue JSON zurück.
    /// </summary>
    public static string Rewrite(string json, Func<string, string?> transform)
    {
        var root = JsonNode.Parse(json, NodeOptions, DocumentOptions);
        if (root is null)
            return json;

        Walk(root, transform);

        return root.ToJsonString(WriteOptions);
    }

    /// <summary>Zählt, wie viele Zugangsdaten in der Datei stecken — für das Manifest.</summary>
    public static int CountSecrets(string json)
    {
        var count = 0;
        Rewrite(json, value => { count++; return value; });
        return count;
    }

    private static void Walk(JsonNode node, Func<string, string?> transform)
    {
        switch (node)
        {
            case JsonObject obj:
                // Über eine Kopie laufen, weil Werte im Zuge ersetzt werden.
                foreach (var (name, child) in obj.ToList())
                {
                    if (child is null)
                        continue;

                    if (ConfigSecrets.IsSecretKey(name) &&
                        child is JsonValue value &&
                        value.TryGetValue<string>(out var text) &&
                        !string.IsNullOrEmpty(text))
                    {
                        obj[name] = transform(text);
                        continue;
                    }

                    Walk(child, transform);
                }
                break;

            case JsonArray array:
                foreach (var child in array)
                {
                    if (child is not null)
                        Walk(child, transform);
                }
                break;
        }
    }
}
