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

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

/// <summary>
///     v3 pure mapper: turns an authored raw tag's <c>TagConfig</c> JSON (the shape produced by the
///     AdminUI <c>TwinCATTagConfigModel</c>) into a <see cref="TwinCATTagDefinition"/>. 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="TwinCATTagDefinition.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>
///     <b>Device routing is NOT read from the blob under v3.</b> The pre-v3 <c>deviceHostAddress</c> key is
///     dropped: which device a tag lives under is carried on the <see cref="RawTagEntry.DeviceName"/> the
///     deploy artifact populates (this driver is multi-device), threaded onto
///     <see cref="TwinCATTagDefinition.DeviceHostAddress"/> by the driver at table-build time. The mapper
///     therefore always produces a definition with an empty <see cref="TwinCATTagDefinition.DeviceHostAddress"/>.
/// </remarks>
public static class TwinCATTagDefinitionFactory
{
    /// <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="TwinCATTagDefinition.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="TwinCATTagDefinition.DeviceHostAddress"/> is
    ///     likewise threaded from the tag's <see cref="RawTagEntry.DeviceName"/> (multi-device routing), not
    ///     the blob.
    /// </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 TwinCAT symbol object.</returns>
    public static bool FromTagConfig(string tagConfig, string rawPath, out TwinCATTagDefinition 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("symbolPath", out var symbol)
                || symbol.ValueKind != JsonValueKind.String)
                return false;
            var symbolPath = symbol.GetString();
            if (string.IsNullOrWhiteSpace(symbolPath)) 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", TwinCATDataType.DInt, out var dataType)) return false;
            // Guard (Driver.TwinCAT-003): the driver does not support UDT/FB-instance tags — a
            // Structure-typed tag would read as a garbage int blob or fail late on a write. Reject it
            // here so it never materialises (→ BadNodeIdUnknown); declare atomic-typed members instead,
            // or use EnableControllerBrowse to discover UDT members individually.
            if (dataType == TwinCATDataType.Structure) return false;
            // Array intent — same shape the runtime/OPC-UA foundation parses (camelCase
            // `isArray` bool + `arrayLength` uint). arrayLength is honoured ONLY when isArray
            // is true AND it is a positive JSON number, so a stale length behind a cleared
            // isArray never produces an orphan array tag (Phase 4c).
            var arrayLength = ReadArrayLength(root);
            // "writable" defaults to true when absent (today's value) — makes read-only equipment tags
            // authorable (UNDER-6).
            var writable = TagConfigJson.ReadWritable(root);
            def = new TwinCATTagDefinition(
                Name: rawPath, DeviceHostAddress: "", SymbolPath: symbolPath!,
                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 a
    ///     <see cref="TwinCATTagDefinition"/> from operator flags and need the <c>TagConfig</c> string to hand
    ///     the driver as a <see cref="RawTagEntry"/>. The <c>dataType</c> enum is written as its name string;
    ///     the array keys are emitted only for an array tag so the blob stays minimal. Neither
    ///     <c>WriteIdempotent</c> nor <c>DeviceHostAddress</c> is 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(TwinCATTagDefinition def)
    {
        ArgumentNullException.ThrowIfNull(def);
        var o = new JsonObject
        {
            ["symbolPath"] = def.SymbolPath,
            ["dataType"] = def.DataType.ToString(),
            ["writable"] = def.Writable,
        };
        if (def.ArrayLength is > 0)
        {
            o["isArray"] = true;
            o["arrayLength"] = def.ArrayLength.Value;
        }
        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("TwinCAT TagConfig root is not a JSON object — the tag will not resolve (BadNodeIdUnknown).");
                return warnings;
            }
            var w = TagConfigJson.DescribeInvalidEnum<TwinCATDataType>(root, "dataType");
            if (w is not null) warnings.Add(w);
        }
        catch (JsonException)
        {
            warnings.Add("TwinCAT TagConfig is not valid JSON — the tag will not resolve (BadNodeIdUnknown).");
        }
        return warnings;
    }

    /// <summary>
    ///     Reads the optional 1-D array length: <c>arrayLength</c> (a positive int32; values above
    ///     <see cref="int.MaxValue"/> are treated as absent/scalar) honoured ONLY when <c>isArray</c>
    ///     is the JSON literal <c>true</c>. Returns <c>null</c> (scalar) when isArray is absent/false,
    ///     when arrayLength is absent / non-numeric / zero / negative / greater than
    ///     <see cref="int.MaxValue"/>.
    /// </summary>
    private static int? ReadArrayLength(JsonElement o)
    {
        if (!o.TryGetProperty("isArray", out var aEl) || aEl.ValueKind != JsonValueKind.True)
            return null;
        if (!o.TryGetProperty("arrayLength", out var lEl) || lEl.ValueKind != JsonValueKind.Number)
            return null;
        return lEl.TryGetInt32(out var len) && len > 0 ? len : null;
    }
}
