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

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

/// <summary>
///     v3 pure mapper: turns an authored raw tag's <c>TagConfig</c> JSON (the shape produced by the
///     AdminUI <c>AbCipTagConfigModel</c>) into an <see cref="AbCipTagDefinition"/>. 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="AbCipTagDefinition.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 + the driver-test <c>AbCipRawTags</c> helper) that hold a typed definition and need to
///     synthesise the <c>TagConfig</c> blob for a <see cref="RawTagEntry"/>.
/// </summary>
/// <remarks>
///     <para>
///         <b>Device placement is NOT in the blob.</b> The pre-v3 parser read a <c>deviceHostAddress</c>
///         key off the TagConfig; v3 drops it — a tag's device is the RawPath's device segment, delivered
///         out-of-band on <see cref="RawTagEntry.DeviceName"/> and threaded onto the definition's
///         <see cref="AbCipTagDefinition.DeviceHostAddress"/> (the device routing key) by the driver at
///         table-build time. <see cref="FromTagConfig"/> therefore leaves that field empty.
///     </para>
///     <para>
///         <b>Array / member / safety round-trip.</b> Today's production authoring surface
///         (<c>AbCipTagConfigModel</c>) emits only <c>tagPath</c> / <c>dataType</c> / <c>writable</c>, so a
///         real equipment tag maps to a scalar atomic definition. The additional <c>isArray</c> /
///         <c>arrayLength</c> / <c>safetyTag</c> / <c>members</c> keys are read (and emitted by
///         <see cref="ToTagConfig"/>) so the retired pre-declared-tag coverage — arrays and declared UDT
///         member fan-out — survives the migration to the RawPath seam. They are additive: absent keys map
///         to the scalar / no-member / non-safety defaults.
///     </para>
/// </remarks>
public static class AbCipTagDefinitionFactory
{
    /// <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 mandatory addressing field is the Logix <c>tagPath</c>; <c>dataType</c> 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="AbCipTagDefinition.DeviceHostAddress"/> and <see cref="AbCipTagDefinition.WriteIdempotent"/>
    ///     are NOT read from the blob — the driver threads them in from the tag's <see cref="RawTagEntry"/>.
    /// </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 AbCip address object.</returns>
    /// <remarks>
    ///     <see cref="AbCipTagDefinition.Writable"/> is read from the optional <c>"writable"</c> boolean;
    ///     it defaults to <c>true</c> when absent, matching the record's documented default. Operators who
    ///     need a read-only OPC UA surface can author <c>"writable":false</c>; the PLC's ExternalAccess
    ///     attribute remains the effective write gate at the wire level.
    /// </remarks>
    public static bool FromTagConfig(string tagConfig, string rawPath, out AbCipTagDefinition def)
    {
        def = null!;
        if (string.IsNullOrWhiteSpace(tagConfig)) return false;
        try
        {
            using var doc = JsonDocument.Parse(tagConfig);
            var root = doc.RootElement;
            // AbCip is a symbolic driver: the mandatory addressing field is the Logix tag path.
            if (root.ValueKind != JsonValueKind.Object
                || !root.TryGetProperty("tagPath", out var tagPathEl)
                || tagPathEl.ValueKind != JsonValueKind.String)
                return false;
            var tagPath = tagPathEl.GetString();
            if (string.IsNullOrWhiteSpace(tagPath)) 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", AbCipDataType.DInt, out var dataType)) return false;
            // Review I-1 — an equipment tag is an ARRAY ⟺ isArray:true AND arrayLength >= 1. A 1-element
            // array is a VALID 1-element array (the foundation materialises a [1] OPC UA array node).
            var (isArray, elementCount) = ReadArrayShape(root);
            // "writable" defaults to true when absent — matches AbCipTagDefinition.Writable default.
            var writable = TagConfigJson.ReadWritable(root);
            var safetyTag = ReadBool(root, "safetyTag");

            // Optional declared UDT member fan-out (round-trips the retired pre-declared coverage). A
            // malformed member (missing name / typo'd dataType) rejects the whole tag, mirroring the
            // strict top-level contract.
            IReadOnlyList<AbCipStructureMember>? members = null;
            if (root.TryGetProperty("members", out var membersEl) && membersEl.ValueKind == JsonValueKind.Array)
            {
                var list = new List<AbCipStructureMember>();
                foreach (var m in membersEl.EnumerateArray())
                {
                    if (m.ValueKind != JsonValueKind.Object
                        || !m.TryGetProperty("name", out var nameEl)
                        || nameEl.ValueKind != JsonValueKind.String
                        || string.IsNullOrWhiteSpace(nameEl.GetString()))
                        return false;
                    if (!TagConfigJson.TryReadEnumStrict(m, "dataType", AbCipDataType.DInt, out var memberType)) return false;
                    var (memberIsArray, memberCount) = ReadArrayShape(m);
                    list.Add(new AbCipStructureMember(
                        Name: nameEl.GetString()!,
                        DataType: memberType,
                        Writable: ReadBoolDefaultTrue(m, "writable"),
                        WriteIdempotent: ReadBool(m, "writeIdempotent"),
                        ElementCount: memberCount,
                        IsArray: memberIsArray));
                }
                if (list.Count > 0) members = list;
            }

            def = new AbCipTagDefinition(
                Name: rawPath, DeviceHostAddress: "", TagPath: tagPath!,
                DataType: dataType, Writable: writable, WriteIdempotent: false,
                Members: members, SafetyTag: safetyTag, ElementCount: elementCount, IsArray: isArray);
            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, the driver-test
    ///     <c>AbCipRawTags</c> helper) that construct an <see cref="AbCipTagDefinition"/> and need the
    ///     <c>TagConfig</c> string to hand the driver as a <see cref="RawTagEntry"/>. Enum values are written
    ///     as their name strings; optional fields are emitted only when non-default so the blob stays minimal.
    ///     <see cref="AbCipTagDefinition.DeviceHostAddress"/> (device routing key) and
    ///     <see cref="AbCipTagDefinition.WriteIdempotent"/> are NOT emitted — they travel on the
    ///     <see cref="RawTagEntry"/>, not inside the blob.
    /// </summary>
    /// <param name="def">The definition to serialise.</param>
    /// <returns>The camelCase TagConfig JSON string.</returns>
    public static string ToTagConfig(AbCipTagDefinition def)
    {
        ArgumentNullException.ThrowIfNull(def);
        var o = new JsonObject
        {
            ["tagPath"] = def.TagPath,
            ["dataType"] = def.DataType.ToString(),
            ["writable"] = def.Writable,
        };
        if (def.IsArray)
        {
            o["isArray"] = true;
            o["arrayLength"] = Math.Max(1, def.ElementCount);
        }
        if (def.SafetyTag) o["safetyTag"] = true;
        if (def.Members is { Count: > 0 } members)
        {
            var arr = new JsonArray();
            foreach (var m in members)
            {
                var mo = new JsonObject
                {
                    ["name"] = m.Name,
                    ["dataType"] = m.DataType.ToString(),
                    ["writable"] = m.Writable,
                };
                if (m.WriteIdempotent) mo["writeIdempotent"] = true;
                if (m.IsArray)
                {
                    mo["isArray"] = true;
                    mo["arrayLength"] = Math.Max(1, m.ElementCount);
                }
                arr.Add(mo);
            }
            o["members"] = arr;
        }
        return o.ToJsonString();
    }

    /// <summary>
    ///     Resolve the 1-D array shape from an <c>isArray</c> / <c>arrayLength</c> pair (the foundation
    ///     contract carrier). The canonical rule: the tag is an ARRAY ⟺ <c>isArray</c> is truthy AND
    ///     <c>arrayLength</c> is a number <c>&gt;= 1</c>. Any other combination is a SCALAR and returns
    ///     <c>(IsArray: false, ElementCount: 1)</c> — matching every other driver.
    /// </summary>
    private static (bool IsArray, int ElementCount) ReadArrayShape(JsonElement o)
    {
        var isArray = o.TryGetProperty("isArray", out var a) && a.ValueKind == JsonValueKind.True;
        if (!isArray) return (false, 1);
        if (o.TryGetProperty("arrayLength", out var len)
            && len.ValueKind == JsonValueKind.Number
            && len.TryGetInt32(out var n)
            && n >= 1)
            return (true, n);
        // isArray:true but arrayLength missing/invalid/<1 — canonical rule: scalar (matches all other drivers).
        return (false, 1);
    }

    /// <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("AbCip TagConfig root is not a JSON object — the tag will not resolve (BadNodeIdUnknown).");
                return warnings;
            }
            var w = TagConfigJson.DescribeInvalidEnum<AbCipDataType>(root, "dataType");
            if (w is not null) warnings.Add(w);
        }
        catch (JsonException)
        {
            warnings.Add("AbCip TagConfig is not valid JSON — the tag will not resolve (BadNodeIdUnknown).");
        }
        return warnings;
    }

    private static bool ReadBool(JsonElement o, string name)
        => o.TryGetProperty(name, out var e) && e.ValueKind == JsonValueKind.True;

    private static bool ReadBoolDefaultTrue(JsonElement o, string name)
        => !o.TryGetProperty(name, out var e) || e.ValueKind != JsonValueKind.False;
}
