using System.Text.Json.Nodes;

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

/// <summary>
/// Pure, driver-agnostic merge helper for the two server-side array-shape intent keys at the ROOT of a
/// tag's <c>TagConfig</c> JSON: <c>isArray</c> (camelCase bool — omit/false default) and <c>arrayLength</c>
/// (camelCase JSON number — the 1-D array length; omit when not an array). These map to what the
/// EquipmentTagPlan reads to materialise a 1-D array OPC UA node (ValueRank=1, ArrayDimensions=[length]).
///
/// <para>
/// This is a TagModal-merge seam mirroring <see cref="TagHistorizeConfig"/>: the TagModal owns the
/// canonical TagConfig JSON; the driver's typed editor reads/writes its driver-specific fields on that same
/// JSON, and the modal's "This tag is an array" checkbox + "Array length" numeric input read/write these two
/// keys through this helper. They COMPOSE because both sides preserve every unrecognised key (delegated to
/// <see cref="TagConfigJson.ParseOrNew"/> / <see cref="TagConfigJson.Set"/>), so neither clobbers the
/// other's fields. These keys are intentionally NOT carried inside any per-driver typed model.
/// </para>
/// </summary>
public static class TagArrayConfig
{
    /// <summary>The two array-shape values parsed from a TagConfig JSON's root.</summary>
    /// <param name="IsArray">Whether the server materialises this tag as a 1-D array node.</param>
    /// <param name="ArrayLength">The 1-D array length; <c>null</c> when absent (incl. when not an array).</param>
    public readonly record struct ArrayState(bool IsArray, uint? ArrayLength);

    /// <summary>Reads the array-shape keys from a TagConfig JSON, defaulting any absent key
    /// (null/blank/malformed input ⇒ <c>(false, null)</c>). A negative or non-numeric <c>arrayLength</c>
    /// reads as <c>null</c>.</summary>
    /// <param name="json">The TagConfig JSON to read from, or <c>null</c>.</param>
    /// <returns>The parsed array-shape state.</returns>
    public static ArrayState Read(string? json)
    {
        var o = TagConfigJson.ParseOrNew(json);
        return new ArrayState(
            TagConfigJson.GetBool(o, "isArray"),
            ReadLength(o));
    }

    /// <summary>Merges the two array-shape keys into <paramref name="json"/>, preserving every other
    /// (driver-specific or unknown) key. <c>isArray</c> is dropped when false (absent ⇒ false at the
    /// materialiser); <c>arrayLength</c> is dropped when <paramref name="isArray"/> is false or
    /// <paramref name="arrayLength"/> is null (never leaves an orphan length behind a cleared isArray).</summary>
    /// <param name="json">The existing TagConfig JSON to merge into, or <c>null</c> for a new config.</param>
    /// <param name="isArray">Whether the tag should be materialised as a 1-D array.</param>
    /// <param name="arrayLength">The array length to store; ignored when <paramref name="isArray"/> is false.</param>
    /// <returns>The merged TagConfig JSON string.</returns>
    public static string Set(string? json, bool isArray, uint? arrayLength)
    {
        var o = TagConfigJson.ParseOrNew(json);
        TagConfigJson.Set(o, "isArray", isArray ? true : null);
        // Drop arrayLength whenever isArray is off OR no length is set, so a cleared array never
        // leaves an orphan length key behind.
        TagConfigJson.Set(o, "arrayLength", isArray && arrayLength is { } len ? len : null);
        return TagConfigJson.Serialize(o);
    }

    /// <summary>Validates the array-shape intent: when <paramref name="isArray"/> is set, a positive
    /// <paramref name="arrayLength"/> is required. Returns an error string when invalid, or <c>null</c>
    /// when valid (a non-array is always valid regardless of length).</summary>
    /// <param name="isArray">Whether the tag is configured as a 1-D array.</param>
    /// <param name="arrayLength">The configured array length, or <c>null</c> when unset.</param>
    /// <returns>An error message when the combination is invalid; otherwise <c>null</c>.</returns>
    public static string? Validate(bool isArray, uint? arrayLength)
        => isArray && (arrayLength is null or 0)
            ? "Array length must be a positive number when 'This tag is an array' is checked."
            : null;

    // Reads arrayLength as a non-negative uint, or null when absent/null/non-numeric/negative.
    private static uint? ReadLength(JsonObject o)
        => o.TryGetPropertyValue("arrayLength", out var n)
           && n is JsonValue v
           && v.TryGetValue<long>(out var l)
           && l is >= 0 and <= uint.MaxValue
            ? (uint)l
            : null;
}
