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

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

/// <summary>
///     v3 pure mapper: turns an authored raw tag's <c>TagConfig</c> JSON (the shape produced by the
///     AdminUI <c>ModbusTagConfigModel</c>) into a <see cref="ModbusTagDefinition"/>. 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="ModbusTagDefinition.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 ModbusTagDefinitionFactory
{
    /// <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).
    ///     Enum fields (<c>region</c> / <c>dataType</c> / <c>byteOrder</c> / <c>stringByteOrder</c>) are 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="ModbusTagDefinition.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 Modbus address object.</returns>
    public static bool FromTagConfig(string tagConfig, string rawPath, out ModbusTagDefinition 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("region", out _)
                || !root.TryGetProperty("address", out var addr))
                return false;
            if (addr.ValueKind != JsonValueKind.Number
                || !addr.TryGetInt32(out var address)
                || address < 0 || address > ushort.MaxValue)
                return false;
            // Strict enum reads (R2-11 Phase C): a present-but-invalid (typo'd) enum rejects the tag
            // (→ BadNodeIdUnknown) instead of silently defaulting to a wrong-width Good.
            if (!TagConfigJson.TryReadEnumStrict(root, "region", ModbusRegion.HoldingRegisters, out var region)) return false;
            if (!TagConfigJson.TryReadEnumStrict(root, "dataType", ModbusDataType.Int16, out var dataType)) return false;
            if (!TagConfigJson.TryReadEnumStrict(root, "byteOrder", ModbusByteOrder.BigEndian, out var byteOrder)) return false;
            if (!TagConfigJson.TryReadEnumStrict(root, "stringByteOrder", ModbusStringByteOrder.HighByteFirst, out var stringByteOrder)) return false;
            var bitIndex = (byte)ReadInt(root, "bitIndex");
            var stringLength = (ushort)ReadInt(root, "stringLength");
            // Guard: String tags require StringLength >= 1. RegisterCount = (StringLength+1)/2,
            // so StringLength=0 → 0 registers → spec-illegal FC03/FC04 with quantity=0 → PLC
            // returns exception 03. Reject here so the driver surfaces BadNodeIdUnknown rather
            // than a misleading BadCommunicationError.
            if (dataType == ModbusDataType.String && stringLength < 1) return false;
            // Guard: BitInRegister tags require BitIndex 0–15. Shift by ≥ 16 on an int overflows
            // a ushort mask silently — reads always return false, writes have no effect.
            if (dataType == ModbusDataType.BitInRegister && bitIndex > 15) return false;
            // isArray / arrayLength — optional keys authored by the typed Modbus tag editor.
            // Canonical rule: a tag is an array iff isArray:true AND arrayLength >= 1.
            // isArray:false (with any arrayLength) is scalar — the foundation materialises a
            // scalar OPC UA node, so returning an array value would cause a shape mismatch.
            // A 1-element array (isArray:true, arrayLength:1) IS valid and reads as short[1].
            var isArray = ReadBool(root, "isArray");
            var arrayLength = ReadInt(root, "arrayLength");
            int? arrayCount = (isArray && arrayLength >= 1) ? arrayLength : null;
            // "writable" defaults to true when absent (today's value) — makes read-only equipment tags
            // authorable (UNDER-6). Node-level authz remains the effective write gate.
            var writable = TagConfigJson.ReadWritable(root);
            // CONV-4: optional per-tag unitId (0–255). Absent ⇒ null ⇒ the driver-level UnitId via
            // ModbusDriver.ResolveUnitId, so multi-unit equipment tags key their own per-host breaker.
            var unitId = ReadOptionalByte(root, "unitId");
            // Optional per-tag encoding/planner knobs — carried on the def so a RawTagEntry round-trips
            // the full authored definition (these have no AdminUI editor field yet; absent ⇒ default).
            var deadband = ReadOptionalDouble(root, "deadband");
            var coalesceProhibited = ReadBool(root, "coalesceProhibited");
            def = new ModbusTagDefinition(
                Name: rawPath, Region: region, Address: (ushort)address, DataType: dataType,
                Writable: writable, ByteOrder: byteOrder, BitIndex: bitIndex, StringLength: stringLength,
                StringByteOrder: stringByteOrder, WriteIdempotent: false,
                ArrayCount: arrayCount, Deadband: deadband, UnitId: unitId,
                CoalesceProhibited: coalesceProhibited);
            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="ModbusTagDefinition"/> from operator flags 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. <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(ModbusTagDefinition def)
    {
        ArgumentNullException.ThrowIfNull(def);
        var o = new JsonObject
        {
            ["region"] = def.Region.ToString(),
            ["address"] = def.Address,
            ["dataType"] = def.DataType.ToString(),
            ["byteOrder"] = def.ByteOrder.ToString(),
            ["bitIndex"] = def.BitIndex,
            ["stringLength"] = def.StringLength,
            ["stringByteOrder"] = def.StringByteOrder.ToString(),
            ["writable"] = def.Writable,
        };
        if (def.ArrayCount is { } count)
        {
            o["isArray"] = true;
            o["arrayLength"] = count;
        }
        if (def.Deadband is { } deadband) o["deadband"] = deadband;
        if (def.UnitId is { } unitId) o["unitId"] = unitId;
        if (def.CoalesceProhibited) o["coalesceProhibited"] = true;
        return o.ToJsonString();
    }

    /// <summary>
    ///     Deploy-time inspection of an equipment-tag <c>TagConfig</c> blob (05/CONV-2). Returns
    ///     human-readable warnings for present-but-invalid enum values (<c>region</c> / <c>dataType</c> /
    ///     <c>byteOrder</c> — which the lenient runtime silently defaults) and for a structurally
    ///     unparseable TagConfig (which the runtime turns into a silent <c>BadNodeIdUnknown</c>). Empty when
    ///     the blob is clean or is not an equipment-tag TagConfig 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("Modbus TagConfig root is not a JSON object — the tag will not resolve (BadNodeIdUnknown).");
                return warnings;
            }
            foreach (var w in new[]
            {
                TagConfigJson.DescribeInvalidEnum<ModbusRegion>(root, "region"),
                TagConfigJson.DescribeInvalidEnum<ModbusDataType>(root, "dataType"),
                TagConfigJson.DescribeInvalidEnum<ModbusByteOrder>(root, "byteOrder"),
            })
            {
                if (w is not null) warnings.Add(w);
            }
        }
        catch (JsonException)
        {
            warnings.Add("Modbus 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;

    private static byte? ReadOptionalByte(JsonElement o, string name)
        => o.TryGetProperty(name, out var e) && e.ValueKind == JsonValueKind.Number
           && e.TryGetInt32(out var v) && v is >= 0 and <= 255 ? (byte)v : null;

    private static double? ReadOptionalDouble(JsonElement o, string name)
        => o.TryGetProperty(name, out var e) && e.ValueKind == JsonValueKind.Number
           && e.TryGetDouble(out var v) ? v : null;

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