using System.Text.Json.Nodes;

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

/// <summary>Typed working model for a <c>Calculation</c> tag's TagConfig JSON — the calc-driver binding
/// (<c>scriptId</c> + trigger fields). Mirrors the <c>&lt;Driver&gt;TagConfigModel</c> template: camelCase
/// JSON keys, enum-name serialisation, and unknown-key preservation across a load→save. Shape per the
/// Calculation mini-design: <c>{ "scriptId": "…", "changeTriggered": true, "timerIntervalMs": 5000 }</c>.</summary>
public sealed class CalculationTagConfigModel
{
    /// <summary>Logical FK to the shared <c>Script</c> entity that computes this tag. Required.</summary>
    public string ScriptId { get; set; } = "";

    /// <summary>Re-evaluate whenever a declared dependency value changes. Defaults to <c>true</c>.</summary>
    public bool ChangeTriggered { get; set; } = true;

    /// <summary>Optional periodic re-evaluation interval in milliseconds; <c>null</c> ⇒ change-trigger only.
    /// When set it must be ≥ 50 ms (enforced by <see cref="Validate"/>). An absent key stays absent across
    /// a load→save (so it never leaks a phantom timer).</summary>
    public int? TimerIntervalMs { get; set; }

    private JsonObject _bag = new();

    /// <summary>Loads a model from a TagConfig JSON string, defaulting any absent field and retaining every
    /// original key (so fields this editor doesn't expose survive a load→save).</summary>
    /// <param name="json">The raw TagConfig JSON string, or <c>null</c> for a new/empty config.</param>
    /// <returns>The populated <see cref="CalculationTagConfigModel"/>.</returns>
    public static CalculationTagConfigModel FromJson(string? json)
    {
        var o = TagConfigJson.ParseOrNew(json);
        int? timer = null;
        if (o.TryGetPropertyValue("timerIntervalMs", out var tn) && tn is JsonValue tv && tv.TryGetValue<int>(out var ti))
        {
            timer = ti;
        }
        return new CalculationTagConfigModel
        {
            ScriptId = TagConfigJson.GetString(o, "scriptId") ?? "",
            ChangeTriggered = TagConfigJson.GetBool(o, "changeTriggered", true),
            TimerIntervalMs = timer,
            _bag = o,
        };
    }

    /// <summary>Serialises this model back to a TagConfig JSON string over the preserved key bag. A blank
    /// <see cref="ScriptId"/> and a null <see cref="TimerIntervalMs"/> remove their keys (never persist an
    /// empty/phantom value).</summary>
    /// <returns>The serialised TagConfig JSON string.</returns>
    public string ToJson()
    {
        TagConfigJson.Set(_bag, "scriptId", string.IsNullOrWhiteSpace(ScriptId) ? null : ScriptId);
        TagConfigJson.Set(_bag, "changeTriggered", ChangeTriggered);
        TagConfigJson.Set(_bag, "timerIntervalMs", TimerIntervalMs);
        return TagConfigJson.Serialize(_bag);
    }

    /// <summary>Validates the calc binding: a script is required, at least one trigger (change or timer)
    /// must be set, and a set timer must be ≥ 50 ms.</summary>
    /// <returns>An error message describing the validation failure, or <c>null</c> when the model is valid.</returns>
    public string? Validate()
    {
        if (string.IsNullOrWhiteSpace(ScriptId))
        {
            return "A Calculation tag requires a script.";
        }
        if (!ChangeTriggered && TimerIntervalMs is null)
        {
            return "A Calculation tag needs at least one trigger: change-triggered and/or a timer interval.";
        }
        if (TimerIntervalMs is { } ms && ms < 50)
        {
            return "Timer interval must be at least 50 ms.";
        }
        return null;
    }
}
