using System.Text.Json;

namespace ZB.MOM.WW.OtOpcUa.Commons.Types;

/// <summary>
///     The cross-driver platform intents parsed from a Tag's schemaless <c>TagConfig</c> JSON.
///     SINGLE SOURCE OF TRUTH for the byte-parity contract shared by the live-compose seam
///     (<c>AddressSpaceComposer</c>), the artifact-decode seam (<c>DeploymentArtifact</c>), the draft
///     gate (<c>DraftValidator</c>), and the walker (<c>EquipmentNodeWalker</c>). One
///     <see cref="JsonDocument.Parse(string)"/> per blob. Never throws.
///     <para>Under the v3 identity contract, <c>TagConfig</c> no longer carries identity — the
///     RawPath (computed elsewhere) is the single identity string at every seam. This type parses
///     only the cross-driver PLATFORM intents (alarm / historize / array); the driver-specific
///     address fields stay opaque inside the blob and are read by the per-driver resolvers.</para>
/// </summary>
/// <param name="Alarm">The optional native-alarm intent parsed from the <c>alarm</c> object; <see langword="null"/> ⇒ plain value variable.</param>
/// <param name="IsHistorized"><c>isHistorized</c> — true only when the value is a bool <c>true</c>.</param>
/// <param name="HistorianTagname">Non-whitespace <c>historianTagname</c> string override, else <see langword="null"/> (not trimmed).</param>
/// <param name="IsArray"><c>isArray</c> bool.</param>
/// <param name="ArrayLength"><c>arrayLength</c> honoured ONLY when <see cref="IsArray"/> and a JSON number fitting <see cref="uint"/>; else <see langword="null"/>.</param>
public sealed record TagConfigIntent(
    TagAlarmIntent? Alarm,
    bool IsHistorized,
    string? HistorianTagname,
    bool IsArray,
    uint? ArrayLength)
{
    /// <summary>
    ///     Parse a Tag's schemaless <c>TagConfig</c> JSON into the cross-driver platform intents.
    ///     Never throws: malformed JSON / non-object root / blank / null ⇒ all intents default.
    ///     Semantics are ported verbatim from the historical <c>AddressSpaceComposer.Extract*</c>
    ///     statics (the canonical copy).
    /// </summary>
    /// <param name="tagConfig">The tag's raw <c>TagConfig</c> JSON (nullable/blank tolerated).</param>
    /// <returns>The parsed intents; never <see langword="null"/>.</returns>
    public static TagConfigIntent Parse(string? tagConfig)
    {
        if (string.IsNullOrWhiteSpace(tagConfig))
            return new TagConfigIntent(null, false, null, false, null);

        try
        {
            using var doc = JsonDocument.Parse(tagConfig);
            var root = doc.RootElement;
            if (root.ValueKind != JsonValueKind.Object)
                return new TagConfigIntent(null, false, null, false, null);

            var (isHistorized, historianTagname) = ParseHistorize(root);
            var (isArray, arrayLength) = ParseArray(root);

            return new TagConfigIntent(
                ParseAlarm(root), isHistorized, historianTagname, isArray, arrayLength);
        }
        catch (JsonException)
        {
            return new TagConfigIntent(null, false, null, false, null);
        }
    }

    private static TagAlarmIntent? ParseAlarm(JsonElement root)
    {
        if (!root.TryGetProperty("alarm", out var a) || a.ValueKind != JsonValueKind.Object) return null;
        var type = a.TryGetProperty("alarmType", out var tEl) && tEl.ValueKind == JsonValueKind.String
            ? (tEl.GetString() ?? "AlarmCondition") : "AlarmCondition";
        var sev = a.TryGetProperty("severity", out var sEl) && sEl.ValueKind == JsonValueKind.Number
            && sEl.TryGetInt32(out var sv) ? sv : 500;
        // historizeToAveva (bool?, absent ⇒ null ⇒ historize): only an explicit false suppresses the
        // durable AVEVA write at the HistorianAdapterActor gate; a non-bool node ⇒ null (default-on).
        bool? historize = a.TryGetProperty("historizeToAveva", out var hEl)
                && hEl.ValueKind is JsonValueKind.True or JsonValueKind.False
            ? hEl.GetBoolean()
            : null;
        return new TagAlarmIntent(type, sev, historize);
    }

    private static (bool IsHistorized, string? HistorianTagname) ParseHistorize(JsonElement root)
    {
        var isHistorized = root.TryGetProperty("isHistorized", out var hEl)
            && (hEl.ValueKind == JsonValueKind.True || hEl.ValueKind == JsonValueKind.False)
            && hEl.GetBoolean();
        string? tagname = null;
        if (root.TryGetProperty("historianTagname", out var nEl) && nEl.ValueKind == JsonValueKind.String)
        {
            var raw = nEl.GetString();
            if (!string.IsNullOrWhiteSpace(raw)) tagname = raw; // not trimmed
        }
        return (isHistorized, tagname);
    }

    private static (bool IsArray, uint? ArrayLength) ParseArray(JsonElement root)
    {
        var isArray = root.TryGetProperty("isArray", out var aEl)
            && (aEl.ValueKind == JsonValueKind.True || aEl.ValueKind == JsonValueKind.False)
            && aEl.GetBoolean();
        uint? arrayLength = null;
        if (isArray
            && root.TryGetProperty("arrayLength", out var lEl)
            && lEl.ValueKind == JsonValueKind.Number
            && lEl.TryGetUInt32(out var len))
        {
            arrayLength = len;
        }
        return (isArray, arrayLength);
    }
}

/// <summary>The optional native-alarm intent parsed from a tag's <c>TagConfig.alarm</c> object.</summary>
/// <param name="AlarmType">OPC UA Part 9 subtype string; default <c>"AlarmCondition"</c>.</param>
/// <param name="Severity">1..1000 severity; default <c>500</c>.</param>
/// <param name="HistorizeToAveva">Per-condition durable-write opt-out (<c>bool?</c>; absent ⇒ <see langword="null"/> ⇒ historize).</param>
public sealed record TagAlarmIntent(string AlarmType, int Severity, bool? HistorizeToAveva);
