using System.Text.Json.Nodes;

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

/// <summary>
/// Typed working model for the optional native-alarm <c>alarm</c> sub-object inside a tag's
/// <c>TagConfig</c> JSON. A tag whose <c>TagConfig</c> carries an <c>alarm</c> object is materialised
/// as an OPC UA Part 9 condition (rather than a value variable); the fields here mirror what the
/// server's <c>AddressSpaceComposer.ExtractTagAlarm</c> / <c>DeploymentArtifact.ExtractTagAlarm</c> parse.
///
/// <para>
/// <see cref="HistorizeToAveva"/> is the per-condition opt-out of the DURABLE AVEVA historian write
/// (<c>bool?</c>; absent ⇒ <c>null</c> ⇒ historize). The server's <c>HistorianAdapterActor</c> gates
/// the sink write on <c>historizeToAveva is not false</c>, so <c>null</c> and <c>true</c> both
/// historize and only an explicit <c>false</c> suppresses the durable row — the live <c>/alerts</c>
/// fan-out is unaffected. This mirrors the scripted-alarm <c>HistorizeToAveva</c> opt-out posture.
/// </para>
///
/// <para>
/// <see cref="FromJson"/> / <see cref="ToJson"/> operate over the WHOLE <c>TagConfig</c> JSON (root +
/// nested <c>alarm</c>), preserving every unrecognised key at both levels so a load→save can't drop a
/// field this editor doesn't expose.
/// </para>
/// </summary>
public sealed class NativeAlarmModel
{
    /// <summary>True iff the TagConfig carried an <c>alarm</c> object (the tag is an alarm condition).</summary>
    public bool IsAlarm { get; set; }

    /// <summary>OPC UA Part 9 condition subtype (OffNormalAlarm/DiscreteAlarm/LimitAlarm/AlarmCondition).</summary>
    public string AlarmType { get; set; } = "AlarmCondition";

    /// <summary>1..1000 OPC UA severity.</summary>
    public int Severity { get; set; } = 500;

    /// <summary>Per-condition opt-out of the durable AVEVA historian write. <c>null</c> ⇒ absent ⇒
    /// historize (default-on at the server gate); <c>true</c> ⇒ historize; <c>false</c> ⇒ suppress the
    /// durable row (live alerts fan-out unaffected).</summary>
    public bool? HistorizeToAveva { get; set; }

    private JsonObject _root = new();

    /// <summary>Loads a model from a TagConfig JSON string. When no <c>alarm</c> object is present the
    /// model is flagged non-alarm and the alarm fields keep their defaults. Retains every original key
    /// at the root and inside <c>alarm</c> so a load→save preserves fields this editor doesn't expose.</summary>
    /// <param name="json">The tag's raw <c>TagConfig</c> JSON, or null/empty for a fresh config.</param>
    /// <returns>The populated model.</returns>
    public static NativeAlarmModel FromJson(string? json)
    {
        var root = TagConfigJson.ParseOrNew(json);
        var model = new NativeAlarmModel { _root = root };

        if (root.TryGetPropertyValue("alarm", out var aNode) && aNode is JsonObject alarm)
        {
            model.IsAlarm = true;
            model.AlarmType = TagConfigJson.GetString(alarm, "alarmType") is { } t && t.Length > 0
                ? t : "AlarmCondition";
            model.Severity = TagConfigJson.GetInt(alarm, "severity", 500);
            model.HistorizeToAveva = ReadNullableBool(alarm, "historizeToAveva");
        }

        return model;
    }

    /// <summary>Serialises this model back to a TagConfig JSON string. When <see cref="IsAlarm"/> is set,
    /// writes the alarm fields over the preserved <c>alarm</c> sub-object (creating it if absent); a
    /// <c>null</c> <see cref="HistorizeToAveva"/> REMOVES the key so the absent ⇒ historize default holds.</summary>
    /// <returns>The serialised TagConfig JSON string.</returns>
    public string ToJson()
    {
        if (IsAlarm)
        {
            var alarm = _root.TryGetPropertyValue("alarm", out var n) && n is JsonObject existing
                ? existing
                : new JsonObject();

            TagConfigJson.Set(alarm, "alarmType", AlarmType);
            TagConfigJson.Set(alarm, "severity", Severity);
            // null REMOVES the key (absent ⇒ historize default-on at the server gate); a concrete
            // true/false is written through. JsonValue.Create(bool) yields a JSON boolean literal.
            if (HistorizeToAveva is { } h)
            {
                alarm["historizeToAveva"] = JsonValue.Create(h);
            }
            else
            {
                alarm.Remove("historizeToAveva");
            }

            // Re-attach (no-op when it was already the same node; required when we created a fresh one).
            _root["alarm"] = alarm;
        }

        return TagConfigJson.Serialize(_root);
    }

    /// <summary>Validation hook; returns an error message or null when the model is valid.</summary>
    /// <returns>An error message describing the validation failure, or null when the model is valid.</returns>
    public string? Validate() => null;

    /// <summary>Default <c>alarmType</c> seeded when an alarm-bearing attribute is picked but the
    /// TagConfig has no <c>alarm</c> object yet. Galaxy's <c>AlarmExtension</c> is a boolean
    /// off-normal condition, so <c>OffNormalAlarm</c> is the natural Part 9 subtype.</summary>
    public const string DefaultSeedAlarmType = "OffNormalAlarm";

    /// <summary>Default OPC UA severity (1..1000) seeded for a freshly pre-filled native alarm.</summary>
    public const int DefaultSeedSeverity = 700;

    /// <summary>
    /// Pre-seeds a default native-alarm <c>alarm</c> object into <paramref name="json"/> when the
    /// selected driver attribute is itself an alarm and the TagConfig carries no <c>alarm</c> object
    /// yet. This is the Galaxy-picker convenience: picking an <c>IsAlarm</c> attribute materialises a
    /// ready-to-author Part 9 condition (<c>{"alarmType":"OffNormalAlarm","severity":700}</c>) so the
    /// operator doesn't hand-write the JSON. Every existing key (root + a pre-existing <c>alarm</c>) is
    /// preserved: when an <c>alarm</c> object already exists this returns the input unchanged (never
    /// overwrites an authored alarm). Returns the (possibly unchanged) TagConfig JSON string.
    /// </summary>
    /// <param name="json">The tag's raw <c>TagConfig</c> JSON, or null/empty for a fresh config.</param>
    /// <returns>The (possibly unchanged) TagConfig JSON string with the default alarm seeded when applicable.</returns>
    public static string SeedDefaultAlarm(string? json)
    {
        var root = TagConfigJson.ParseOrNew(json);

        // Never overwrite an existing alarm object (only seed when absent).
        if (root.TryGetPropertyValue("alarm", out var existing) && existing is JsonObject)
        {
            return TagConfigJson.Serialize(root);
        }

        var alarm = new JsonObject();
        TagConfigJson.Set(alarm, "alarmType", DefaultSeedAlarmType);
        TagConfigJson.Set(alarm, "severity", DefaultSeedSeverity);
        root["alarm"] = alarm;

        return TagConfigJson.Serialize(root);
    }

    /// <summary>Reads a nullable bool: <c>null</c> when absent/null/non-bool; otherwise the bool value.</summary>
    private static bool? ReadNullableBool(JsonObject o, string name)
        => o.TryGetPropertyValue(name, out var n) && n is JsonValue v && v.TryGetValue<bool>(out var b)
            ? b
            : null;
}
