using System.Text.Json;
using ZB.MOM.WW.OtOpcUa.Commons.Types;

namespace ZB.MOM.WW.OtOpcUa.Driver.Calculation;


/// <summary>
///     A single authored calc tag as the driver runs it: its <see cref="RawPath"/> identity, the script
///     that computes it (<see cref="ScriptId"/> for attribution, <see cref="ScriptSource"/> for
///     evaluation), its trigger config (<see cref="ChangeTriggered"/> / <see cref="TimerInterval"/>), and
///     the static <see cref="DependencyRefs"/> extracted from the script's literal
///     <c>ctx.GetTag("…")</c> reads.
/// </summary>
/// <param name="RawPath">The tag's cluster-scoped RawPath — its identity + the calc-tag id passed to the evaluator.</param>
/// <param name="ScriptId">Logical FK to the shared <c>Script</c> entity — bound to the failure script-log for attribution.</param>
/// <param name="ScriptSource">The resolved C# script source that produces the tag's value; empty when unresolved.</param>
/// <param name="ChangeTriggered">Re-evaluate whenever a declared dependency changes.</param>
/// <param name="TimerInterval">Optional periodic re-evaluation cadence; null ⇒ change-trigger only.</param>
/// <param name="DependencyRefs">Distinct literal <c>ctx.GetTag</c> RawPaths this tag reads (first-seen order).</param>
public sealed record CalculationTagDefinition(
    string RawPath,
    string ScriptId,
    string ScriptSource,
    bool ChangeTriggered,
    TimeSpan? TimerInterval,
    IReadOnlyList<string> DependencyRefs)
{
    /// <summary>
    ///     Map a calc tag's <c>TagConfig</c> blob (<c>{ "scriptId", "scriptSource", "changeTriggered",
    ///     "timerIntervalMs" }</c>) + its RawPath into a <see cref="CalculationTagDefinition"/>. Returns
    ///     <see langword="false"/> (and leaves <paramref name="def"/> null) only when the blob has no
    ///     <c>scriptId</c> — a tag with no identifiable script cannot be a calc tag. A present
    ///     <c>scriptId</c> with an empty/absent <c>scriptSource</c> still maps (the driver logs it and the
    ///     tag never computes). Never throws — malformed JSON yields a miss.
    /// </summary>
    /// <param name="tagConfig">The calc tag's schemaless <c>TagConfig</c> JSON.</param>
    /// <param name="rawPath">The tag's RawPath identity.</param>
    /// <param name="def">The mapped definition when this returns <see langword="true"/>.</param>
    /// <returns><see langword="true"/> when a definition (with a scriptId) was mapped.</returns>
    public static bool TryFromTagConfig(string tagConfig, string rawPath, out CalculationTagDefinition def)
    {
        def = null!;
        if (string.IsNullOrWhiteSpace(tagConfig)) return false;

        string? scriptId;
        string scriptSource;
        bool changeTriggered;
        int? timerMs;
        try
        {
            using var doc = JsonDocument.Parse(tagConfig);
            var root = doc.RootElement;
            if (root.ValueKind != JsonValueKind.Object) return false;

            scriptId = root.TryGetProperty("scriptId", out var sidEl) && sidEl.ValueKind == JsonValueKind.String
                ? sidEl.GetString() : null;
            if (string.IsNullOrWhiteSpace(scriptId)) return false;

            scriptSource = root.TryGetProperty("scriptSource", out var srcEl) && srcEl.ValueKind == JsonValueKind.String
                ? srcEl.GetString() ?? string.Empty : string.Empty;

            // changeTriggered defaults to true (mini-design §2) — only an explicit false disables it.
            changeTriggered = !(root.TryGetProperty("changeTriggered", out var ctEl)
                && ctEl.ValueKind == JsonValueKind.False);

            timerMs = root.TryGetProperty("timerIntervalMs", out var tEl)
                && tEl.ValueKind == JsonValueKind.Number && tEl.TryGetInt32(out var ms) && ms > 0
                ? ms : null;
        }
        catch (JsonException)
        {
            return false;
        }

        var deps = EquipmentScriptPaths.ExtractDependencyRefs(scriptSource);
        def = new CalculationTagDefinition(
            RawPath: rawPath,
            ScriptId: scriptId!,
            ScriptSource: scriptSource,
            ChangeTriggered: changeTriggered,
            TimerInterval: timerMs is { } m ? TimeSpan.FromMilliseconds(m) : null,
            DependencyRefs: deps);
        return true;
    }
}
