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

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

/// <summary>
///     v3 pure mapper: turns an authored raw tag's <c>TagConfig</c> JSON (the shape produced by the
///     AdminUI <c>S7TagConfigModel</c> — <c>address</c> / <c>dataType</c> / <c>stringLength</c>) into a
///     <see cref="S7TagDefinition"/>. 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="S7TagDefinition.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>
public static class S7TagDefinitionFactory
{
    /// <summary>S7 string max length (DBSTRING header reserves 2 bytes; 254 chars max).</summary>
    private const int MaxStringLength = 254;

    /// <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="S7TagDefinition.WriteIdempotent"/> is NOT read
    ///     from the blob — it is a platform flag the caller threads in from the tag's
    ///     <see cref="RawTagEntry.WriteIdempotent"/>.
    /// </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 S7 address object.</returns>
    public static bool FromTagConfig(string tagConfig, string rawPath, out S7TagDefinition 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 addrEl)
                || addrEl.ValueKind != JsonValueKind.String
                || !root.TryGetProperty("dataType", out _))
                return false;
            var address = addrEl.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", S7DataType.Int16, out var dataType)) return false;
            var stringLength = ReadInt(root, "stringLength");
            // Range-guard applies only to String tags: an S7 string can't exceed 254 chars, and a
            // negative length is meaningless. For non-String types stringLength is irrelevant and any
            // authored value (including an out-of-range remnant from copy-paste) must not block parsing.
            if (dataType == S7DataType.String && (stringLength < 0 || stringLength > MaxStringLength)) return false;
            // Array intent: the canonical sink-side parse (DeploymentArtifact.ExtractTagArray)
            // honours arrayLength ONLY when isArray is true AND the prop is a JSON number — mirror
            // that here so the driver's def agrees byte-for-byte with the materialised OPC UA node's
            // ValueRank/ArrayDimensions. Absent / isArray=false ⇒ null (scalar).
            var arrayCount = ReadArrayCount(root);
            // "writable" defaults to true when absent (today's value); node-level authz still governs
            // writes. Honouring the key makes read-only equipment tags authorable (UNDER-6).
            var writable = TagConfigJson.ReadWritable(root);
            def = new S7TagDefinition(
                Name: rawPath,
                Address: address,
                DataType: dataType,
                Writable: writable,
                StringLength: stringLength == 0 ? MaxStringLength : stringLength,
                WriteIdempotent: false,
                ArrayCount: arrayCount);
            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="S7TagDefinition"/> 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;
    ///     <c>stringLength</c> is emitted only when it diverges from the 254 default so the blob stays minimal.
    ///     <c>Name</c> is NOT emitted — it becomes the <see cref="RawTagEntry.RawPath"/>. <c>WriteIdempotent</c>
    ///     is NOT emitted — it travels 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(S7TagDefinition def)
    {
        ArgumentNullException.ThrowIfNull(def);
        var o = new JsonObject
        {
            ["address"] = def.Address,
            ["dataType"] = def.DataType.ToString(),
            ["writable"] = def.Writable,
        };
        // FromTagConfig maps an absent / zero stringLength to MaxStringLength (254), so a def carrying
        // the 254 default round-trips whether or not we emit it — emit only when it diverges.
        if (def.StringLength != MaxStringLength) o["stringLength"] = def.StringLength;
        if (def.ArrayCount is { } count)
        {
            o["isArray"] = true;
            o["arrayLength"] = count;
        }
        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 (a silent
    ///     <c>BadNodeIdUnknown</c> at runtime). 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("S7 TagConfig root is not a JSON object — the tag will not resolve (BadNodeIdUnknown).");
                return warnings;
            }
            var w = TagConfigJson.DescribeInvalidEnum<S7DataType>(root, "dataType");
            if (w is not null) warnings.Add(w);
        }
        catch (JsonException)
        {
            warnings.Add("S7 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;

    /// <summary>
    ///     Reads the optional 1-D array element count from the TagConfig blob. Returns the
    ///     <c>arrayLength</c> int ONLY when <c>isArray</c> is <c>true</c> AND <c>arrayLength</c>
    ///     is a positive JSON integer; <c>null</c> otherwise (scalar). Mirrors the byte-parity
    ///     contract of <c>DeploymentArtifact.ExtractTagArray</c> on the sink side.
    /// </summary>
    private static int? ReadArrayCount(JsonElement root)
    {
        var isArray = root.TryGetProperty("isArray", out var aEl)
            && (aEl.ValueKind == JsonValueKind.True || aEl.ValueKind == JsonValueKind.False)
            && aEl.GetBoolean();
        if (!isArray) return null;
        if (root.TryGetProperty("arrayLength", out var lEl)
            && lEl.ValueKind == JsonValueKind.Number
            && lEl.TryGetInt32(out var len)
            && len > 0)
        {
            return len;
        }
        return null;
    }
}
