using System.Text.Json;
using System.Text.Json.Nodes;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;

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

/// <summary>
///     v3 pure mapper: turns an authored raw tag's <c>TagConfig</c> JSON (the shape produced by the
///     AdminUI <c>AbLegacyTagConfigModel</c>) into an <see cref="AbLegacyTagDefinition"/>. Under v3 a
///     tag's identity is its <b>RawPath</b> (a cluster-scoped slash path), not the address blob — so the
///     produced definition's <see cref="AbLegacyTagDefinition.Name"/> is the RawPath the driver was
///     handed, which is exactly the wire reference the driver's <c>RawPath → def</c> resolver keys on.
///     The driver builds that table by mapping each <see cref="RawTagEntry"/> the deploy artifact
///     delivers through <see cref="FromTagConfig"/>; <see cref="ToTagConfig"/> is the inverse, used by
///     authoring paths (the Client CLI) that hold a typed definition and need to synthesise the
///     <c>TagConfig</c> blob for a <see cref="RawTagEntry"/>.
/// </summary>
/// <remarks>
///     <para>
///         <b>Device routing is out of band.</b> The AB Legacy driver is multi-device, but a tag's
///         device no longer travels inside the <c>TagConfig</c> blob — the <c>deviceHostAddress</c> key
///         is DROPPED here. The deploy artifact carries the tag's device on the
///         <see cref="RawTagEntry.DeviceName"/> segment instead; the driver threads that name onto the
///         definition (resolving it to a configured device) at table-build time.
///     </para>
/// </remarks>
public static class AbLegacyTagDefinitionFactory
{
    /// <summary>
    ///     Maps an authored <c>TagConfig</c> object to a typed definition keyed by <paramref name="rawPath"/>.
    ///     The input is always an authored TagConfig JSON object (there is no name-vs-blob heuristic in v3).
    ///     The <c>dataType</c> enum is read STRICTLY — a present-but-invalid (typo'd) value rejects the whole
    ///     tag (returns <see langword="false"/> ⇒ the driver surfaces <c>BadNodeIdUnknown</c>) rather than
    ///     silently defaulting to a wrong-width Good. <see cref="AbLegacyTagDefinition.WriteIdempotent"/> is
    ///     NOT read from the blob — it is a platform flag the caller threads in from the tag's
    ///     <see cref="RawTagEntry.WriteIdempotent"/>. <see cref="AbLegacyTagDefinition.DeviceHostAddress"/>
    ///     is left empty here — device routing travels on <see cref="RawTagEntry.DeviceName"/> and the driver
    ///     resolves it at table-build time.
    /// </summary>
    /// <param name="tagConfig">The authored equipment-tag TagConfig JSON.</param>
    /// <param name="rawPath">The tag's RawPath — becomes the definition's identity (<c>Name</c>).</param>
    /// <param name="def">The mapped definition when this returns <see langword="true"/>.</param>
    /// <returns><see langword="true"/> when <paramref name="tagConfig"/> is a valid AbLegacy address object.</returns>
    /// <remarks>
    /// <para>
    ///     When <c>isArray</c> is the JSON literal <see langword="true"/> but <c>arrayLength</c> is
    ///     absent, zero, or negative, the result is silently a <b>scalar</b>
    ///     (<see cref="AbLegacyTagDefinition.ArrayLength"/> is <see langword="null"/>).
    ///     A valid positive <c>arrayLength</c> is required to produce an array tag; <c>isArray:true</c>
    ///     alone is not sufficient.  This is intentional: a stale length behind a cleared or absent
    ///     <c>isArray</c> flag must never produce an orphan array tag that mismatches its scalar OPC UA
    ///     node (see in-source comment, review C-2).
    /// </para>
    /// </remarks>
    public static bool FromTagConfig(string tagConfig, string rawPath, out AbLegacyTagDefinition def)
    {
        def = null!;
        if (string.IsNullOrWhiteSpace(tagConfig)) return false;
        try
        {
            using var doc = JsonDocument.Parse(tagConfig);
            var root = doc.RootElement;
            if (root.ValueKind != JsonValueKind.Object
                || !root.TryGetProperty("address", out var addr)
                || addr.ValueKind != JsonValueKind.String)
                return false;
            var address = addr.GetString();
            if (string.IsNullOrWhiteSpace(address)) return false;
            // Strict enum read (R2-11 Phase C): a typo'd dataType rejects the tag (→ BadNodeIdUnknown)
            // instead of silently defaulting to a wrong-width Good.
            if (!TagConfigJson.TryReadEnumStrict(root, "dataType", AbLegacyDataType.Int, out var dataType)) return false;
            int? arrayLength = null;
            if (IsArrayFlag(root))
            {
                var rawLength = ReadInt(root, "arrayLength");
                if (rawLength >= 1)
                    arrayLength = Math.Min(rawLength, AbLegacyArray.MaxElements);
            }
            // "writable" defaults to true when absent (today's value) — makes read-only equipment tags
            // authorable (UNDER-6).
            var writable = TagConfigJson.ReadWritable(root);
            def = new AbLegacyTagDefinition(
                Name: rawPath, DeviceHostAddress: "", Address: address,
                DataType: dataType, Writable: writable, WriteIdempotent: false, ArrayLength: arrayLength);
            return true;
        }
        catch (JsonException) { return false; }
        catch (FormatException) { return false; }
        catch (InvalidOperationException) { return false; }
    }

    /// <summary>
    ///     Inverse of <see cref="FromTagConfig"/>: serialises a typed definition back to the camelCase
    ///     <c>TagConfig</c> JSON the mapper reads. Used by authoring paths (the Client CLI) that construct
    ///     an <see cref="AbLegacyTagDefinition"/> from operator flags and need the <c>TagConfig</c> string
    ///     to hand the driver as a <see cref="RawTagEntry"/>. The enum value is written as its name string;
    ///     the optional array fields are emitted only for a real array tag. <c>WriteIdempotent</c> and
    ///     <c>deviceHostAddress</c> are NOT emitted — they travel on the <see cref="RawTagEntry"/>
    ///     (<see cref="RawTagEntry.WriteIdempotent"/> / <see cref="RawTagEntry.DeviceName"/>), not inside
    ///     the blob.
    /// </summary>
    /// <param name="def">The definition to serialise.</param>
    /// <returns>The camelCase TagConfig JSON string.</returns>
    public static string ToTagConfig(AbLegacyTagDefinition def)
    {
        ArgumentNullException.ThrowIfNull(def);
        var o = new JsonObject
        {
            ["address"] = def.Address,
            ["dataType"] = def.DataType.ToString(),
            ["writable"] = def.Writable,
        };
        if (def.ArrayLength is { } len)
        {
            o["isArray"] = true;
            o["arrayLength"] = len;
        }
        return o.ToJsonString();
    }

    /// <summary>
    ///     Deploy-time inspection (05/CONV-2): warns on a present-but-invalid <c>dataType</c> (silently
    ///     defaulted by the lenient runtime) and on a structurally unparseable TagConfig. Empty when clean
    ///     or not an equipment-tag object. Never throws.
    /// </summary>
    /// <param name="reference">The equipment tag's TagConfig JSON.</param>
    /// <returns>The warnings; empty when clean.</returns>
    public static IReadOnlyList<string> Inspect(string reference)
    {
        var warnings = new List<string>();
        if (string.IsNullOrWhiteSpace(reference) || reference[0] != '{') return warnings;
        try
        {
            using var doc = JsonDocument.Parse(reference);
            var root = doc.RootElement;
            if (root.ValueKind != JsonValueKind.Object)
            {
                warnings.Add("AbLegacy TagConfig root is not a JSON object — the tag will not resolve (BadNodeIdUnknown).");
                return warnings;
            }
            var w = TagConfigJson.DescribeInvalidEnum<AbLegacyDataType>(root, "dataType");
            if (w is not null) warnings.Add(w);
        }
        catch (JsonException)
        {
            warnings.Add("AbLegacy TagConfig is not valid JSON — the tag will not resolve (BadNodeIdUnknown).");
        }
        return warnings;
    }

    private static int ReadInt(JsonElement o, string name)
        => o.TryGetProperty(name, out var e) && e.ValueKind == JsonValueKind.Number
           && e.TryGetInt32(out var v) ? v : 0;

    // True only when the `isArray` property is the JSON literal true. Absent or false → scalar.
    private static bool IsArrayFlag(JsonElement o)
        => o.TryGetProperty("isArray", out var e) && e.ValueKind == JsonValueKind.True;
}
