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

namespace ZB.MOM.WW.OtOpcUa.AdminUI.Uns.TagEditors;

/// <summary>
/// Helpers for per-driver tag-config editors: parse a TagConfig JSON string into a mutable
/// <see cref="JsonObject"/> (preserving every key, so fields the editor doesn't expose survive a
/// load→save), read typed scalars, and serialise back.
/// </summary>
public static class TagConfigJson
{
    /// <summary>Parses <paramref name="json"/> into a mutable object; returns a fresh empty object on null/blank/malformed/non-object input.</summary>
    /// <param name="json">The JSON string to parse.</param>
    /// <returns>The parsed <see cref="JsonObject"/>, or a fresh empty object on invalid input.</returns>
    public static JsonObject ParseOrNew(string? json)
    {
        if (string.IsNullOrWhiteSpace(json)) { return new JsonObject(); }
        try { return JsonNode.Parse(json) as JsonObject ?? new JsonObject(); }
        catch (JsonException) { return new JsonObject(); }
    }

    /// <summary>Serialises the object to compact JSON (JsonNode.ToJsonString() defaults to non-indented).</summary>
    /// <param name="obj">The JSON object to serialise.</param>
    /// <returns>The compact JSON string.</returns>
    public static string Serialize(JsonObject obj) => obj.ToJsonString();

    /// <summary>Reads a string value, or null if absent/null/non-string.</summary>
    /// <param name="o">The JSON object to read from.</param>
    /// <param name="name">The property name to read.</param>
    /// <returns>The string value, or null if absent, null, or non-string.</returns>
    public static string? GetString(JsonObject o, string name)
        => o.TryGetPropertyValue(name, out var n) && n is JsonValue v && v.TryGetValue<string>(out var s) ? s : null;

    /// <summary>Reads an int value, or <paramref name="fallback"/> if absent/null/non-numeric (incl. object/array nodes).</summary>
    /// <param name="o">The JSON object to read from.</param>
    /// <param name="name">The property name to read.</param>
    /// <param name="fallback">The value to return if the property is absent, null, or non-numeric.</param>
    /// <returns>The int value, or <paramref name="fallback"/>.</returns>
    public static int GetInt(JsonObject o, string name, int fallback = 0)
        => o.TryGetPropertyValue(name, out var n) && n is JsonValue v && v.TryGetValue<int>(out var i) ? i : fallback;

    /// <summary>Reads a bool value, or <paramref name="fallback"/> if absent/null/non-boolean (incl. object/array/number/string nodes).</summary>
    /// <param name="o">The JSON object to read from.</param>
    /// <param name="name">The property name to read.</param>
    /// <param name="fallback">The value to return if the property is absent, null, or non-boolean.</param>
    /// <returns>The bool value, or <paramref name="fallback"/>.</returns>
    public static bool GetBool(JsonObject o, string name, bool fallback = false)
        => o.TryGetPropertyValue(name, out var n) && n is JsonValue v && v.TryGetValue<bool>(out var b) ? b : fallback;

    /// <summary>Reads a bool value, or <c>null</c> when the property is absent/null/non-boolean — so an
    /// absent key stays absent through a load→save (distinguishes "not set" from an explicit false).</summary>
    /// <param name="o">The JSON object to read from.</param>
    /// <param name="name">The property name to read.</param>
    /// <returns>The bool value, or <c>null</c> when absent/null/non-boolean.</returns>
    public static bool? GetBoolNullable(JsonObject o, string name)
        => o.TryGetPropertyValue(name, out var n) && n is JsonValue v && v.TryGetValue<bool>(out var b) ? b : null;

    /// <summary>Reads an enum by its serialised name, or <paramref name="fallback"/> if absent/unparseable.</summary>
    /// <typeparam name="TEnum">The enum type to parse.</typeparam>
    /// <param name="o">The JSON object to read from.</param>
    /// <param name="name">The property name to read.</param>
    /// <param name="fallback">The value to return if the property is absent or unparseable.</param>
    /// <returns>The parsed enum value, or <paramref name="fallback"/>.</returns>
    public static TEnum GetEnum<TEnum>(JsonObject o, string name, TEnum fallback) where TEnum : struct, Enum
        => GetString(o, name) is { } s && Enum.TryParse<TEnum>(s, ignoreCase: true, out var v) ? v : fallback;

    /// <summary>Sets a string/number/enum-name value (enums via ToString()). A null value REMOVES the key, so it is omitted from the serialised JSON.</summary>
    /// <param name="o">The JSON object to mutate.</param>
    /// <param name="name">The property name to set.</param>
    /// <param name="value">The value to set, or null to remove the property.</param>
    public static void Set(JsonObject o, string name, object? value)
    {
        if (value is null) { o.Remove(name); return; }
        o[name] = JsonValue.Create(value is Enum e ? e.ToString() : value);
    }

    /// <summary>
    /// Merges a freshly-picked Galaxy reference (<c>tag_name.AttributeName</c>) into <paramref name="json"/>
    /// by setting ONLY the canonical PascalCase <c>FullName</c> key — every other key is preserved verbatim.
    /// This is the Galaxy address re-pick seam: re-picking an address must update the bound attribute WITHOUT
    /// discarding fields the operator already edited independently of the address — ESPECIALLY a hand-authored
    /// <c>alarm</c> object (and root history/array intent, scaling, etc.). Null/blank/malformed input starts
    /// from a bare <c>{"FullName":...}</c> object.
    /// </summary>
    /// <param name="json">The existing TagConfig JSON string, or null/blank/malformed to start fresh.</param>
    /// <param name="fullName">The newly-picked Galaxy <c>tag_name.AttributeName</c> reference.</param>
    /// <returns>The updated TagConfig serialised back to a JSON string.</returns>
    public static string SetFullName(string? json, string fullName)
    {
        // A blank reference is never a valid Galaxy bind — surface it loudly rather than persisting
        // a poisoned "FullName": null/"" that would silently fail to bind at deploy time.
        ArgumentException.ThrowIfNullOrWhiteSpace(fullName);
        var o = ParseOrNew(json);
        o["FullName"] = JsonValue.Create(fullName);
        return Serialize(o);
    }
}
