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

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

/// <summary>
///     v3 pure mapper: turns an authored raw tag's <c>TagConfig</c> JSON (the shape produced by the
///     AdminUI <c>FocasTagConfigModel</c>) into a <see cref="FocasTagDefinition"/>. 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="FocasTagDefinition.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 FocasTagDefinitionFactory
{
    /// <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. The FOCAS TagConfig carries <b>no device host</b> — the
    ///     driver resolves the device from the tag's <see cref="RawTagEntry.DeviceName"/>, so the produced
    ///     definition's <see cref="FocasTagDefinition.DeviceHostAddress"/> is left empty here and filled in by
    ///     the driver at table-build time. Writability lives on the Tag entity / node ACL (the authored FOCAS
    ///     TagConfig has no <c>writable</c> key), so an absent key ⇒ read-only; an explicit <c>writable:true</c>
    ///     is honoured for the driver's internal write gate (the wire itself is still read-only).
    ///     <see cref="FocasTagDefinition.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 FOCAS address object.</returns>
    public static bool FromTagConfig(string tagConfig, string rawPath, out FocasTagDefinition def)
    {
        def = null!;
        if (string.IsNullOrWhiteSpace(tagConfig)) return false;
        try
        {
            using var doc = JsonDocument.Parse(tagConfig);
            var root = doc.RootElement;
            // The address string is FOCAS's sole mandatory address field (FocasTagConfigModel.Validate).
            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", FocasDataType.Int32, out var dataType)) return false;
            // FOCAS authored tags carry no `writable` key (writability lives on the Tag entity / node ACL):
            // absent ⇒ read-only. An explicit writable:true is honoured for the driver's internal write gate
            // (default-false, so a missing key stays read-only — the note is preserved).
            var writable = root.TryGetProperty("writable", out var wr) && wr.ValueKind == JsonValueKind.True;
            def = new FocasTagDefinition(
                Name: rawPath,
                // The device is resolved by the driver from RawTagEntry.DeviceName; empty here.
                DeviceHostAddress: "",
                Address: address,
                DataType: dataType,
                Writable: writable,
                WriteIdempotent: false);
            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="FocasTagDefinition"/> from operator flags and need the <c>TagConfig</c> string to hand
    ///     the driver as a <see cref="RawTagEntry"/>. The enum is written as its name string. Neither the
    ///     device host (which travels on <see cref="RawTagEntry.DeviceName"/>) nor <c>WriteIdempotent</c>
    ///     (which travels on the <see cref="RawTagEntry"/>) is emitted.
    /// </summary>
    /// <param name="def">The definition to serialise.</param>
    /// <returns>The camelCase TagConfig JSON string.</returns>
    public static string ToTagConfig(FocasTagDefinition def)
    {
        ArgumentNullException.ThrowIfNull(def);
        var o = new JsonObject
        {
            ["address"] = def.Address,
            ["dataType"] = def.DataType.ToString(),
            ["writable"] = def.Writable,
        };
        return o.ToJsonString();
    }

    /// <summary>
    ///     Deploy-time inspection (05/CONV-2, 05/UNDER-1): warns on a present-but-invalid <c>dataType</c>
    ///     (silently defaulted by the lenient runtime), on an authored <c>writable:true</c> (the FOCAS wire
    ///     is read-only — <c>WriteAsync</c> always returns <c>BadNotWritable</c>), 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("FOCAS TagConfig root is not a JSON object — the tag will not resolve (BadNodeIdUnknown).");
                return warnings;
            }
            var w = TagConfigJson.DescribeInvalidEnum<FocasDataType>(root, "dataType");
            if (w is not null) warnings.Add(w);
            if (root.TryGetProperty("writable", out var wr) && wr.ValueKind == JsonValueKind.True)
                warnings.Add("FOCAS wire is read-only — an authored 'writable:true' still fails at the wire with BadNotWritable.");
        }
        catch (JsonException)
        {
            warnings.Add("FOCAS TagConfig is not valid JSON — the tag will not resolve (BadNodeIdUnknown).");
        }
        return warnings;
    }
}
