// `SparkplugDataType` is an ALIAS for the vendored proto's generated `Org.Eclipse.Tahu.Protobuf.DataType`
// enum, not a second, hand-maintained enum. See the remarks on `SparkplugDataTypeExtensions` below for
// the reasoning; the short version is that a duplicate enum is a drift hazard this repo has a documented
// systemic bug class around (see CLAUDE.md "Driver enum-serialization bug"), and `Payload.Types.Metric`'s
// wire-level `Datatype` field is a raw `uint32` anyway — nothing structurally forces a second CLR enum to
// exist, so the lowest-risk shape is for `SparkplugDataType` to be the exact same type as the generated
// one, not a value-compatible lookalike that some cast has to bridge.
global using SparkplugDataType = Org.Eclipse.Tahu.Protobuf.DataType;

using ZB.MOM.WW.OtOpcUa.Core.Abstractions;

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

/// <summary>
///     Maps a Sparkplug B metric <see cref="SparkplugDataType"/> (the vendored Eclipse Tahu proto's
///     generated <c>Org.Eclipse.Tahu.Protobuf.DataType</c> enum — see the <c>global using</c> alias
///     above) to a driver-agnostic <see cref="DriverDataType"/>, per design doc §3.5.
/// </summary>
/// <remarks>
///     <para>
///         <b>Why an alias, not a duplicate enum.</b> The obvious "clean seam" shape is a fresh
///         <c>Driver.Mqtt.Contracts</c>-owned enum with its own <c>ToDriverDataType()</c> — but that
///         creates a second definition of the same 35-member vocabulary that has to be hand-kept in
///         sync with whatever Eclipse Tahu's <c>sparkplug_b.proto</c> defines, which is exactly the
///         enum-drift shape this repo already has a name for (CLAUDE.md's "Driver enum-serialization
///         bug (AdminUI authoring)" — a systemic mismatch between two enums meant to describe the same
///         thing). It also does not match how the decoder actually produces values: Sparkplug's
///         <c>Metric.datatype</c> wire field is a raw <c>uint32</c> (see <c>sparkplug_b.proto</c> line
///         230 — <c>optional uint32 datatype = 4</c>), not the <c>DataType</c> enum type itself, so
///         <c>SparkplugCodec</c> (Task 16) already casts the decoded value straight to the generated
///         <c>Org.Eclipse.Tahu.Protobuf.DataType</c> (locally aliased there as <c>TahuDataType</c>).
///         A second, hand-duplicated enum would force every downstream consumer (Tasks 18/19/20/21) to
///         cast between two value-compatible-but-nominally-different enums to call this extension
///         method — extra surface for exactly zero benefit, since both "enums" would need to enumerate
///         the identical 35 members in the identical order to stay castable.
///     </para>
///     <para>
///         Making <see cref="SparkplugDataType"/> a <c>global using</c> alias for the generated type
///         sidesteps all of that: there is no second definition to drift, by construction — the alias
///         and the generated enum are the exact same CLR type. <see cref="ToDriverDataType"/> below is
///         written as an extension on <see cref="SparkplugDataType"/> purely for call-site readability;
///         it is equally callable as <c>someDecodedDatatype.ToDriverDataType()</c> against a plain
///         <c>Org.Eclipse.Tahu.Protobuf.DataType</c> value with no cast, which is exactly the shape
///         <c>SparkplugCodec</c>'s decoded metrics hand back.
///     </para>
///     <para>
///         <b>Drift guard.</b> Because there is only one enum, <c>SparkplugDataTypeTests</c>'
///         completeness test (<c>ToDriverDataType_HandlesEveryGeneratedDataTypeMember_...</c>) can
///         enumerate <c>Enum.GetValues&lt;SparkplugDataType&gt;()</c> — which, being the alias, is
///         literally the live generated member set — and assert every member is either mapped or on
///         the explicit unsupported list below. If Eclipse Tahu's proto ever gains a member, that test
///         picks it up automatically and fails until this map makes an explicit decision about it,
///         rather than the new member silently falling through a duplicate-enum's stale default.
///     </para>
///     <para>
///         Per design §3.5: <c>Int8</c>/<c>UInt8</c> widen to <see cref="DriverDataType.Int16"/> /
///         <see cref="DriverDataType.UInt16"/> (no OPC UA signed-byte distinction is needed and
///         <see cref="DriverDataType"/> has no 8-bit members at all); <c>Float</c>/<c>Double</c> map to
///         <see cref="DriverDataType.Float32"/> / <see cref="DriverDataType.Float64"/> — note there is
///         no <c>DriverDataType.Double</c> member, only <c>Float64</c>; <c>Text</c>/<c>UUID</c>
///         (generated as <c>Uuid</c> — protoc mangles the wire spelling, see
///         <c>SparkplugProtoCodegenTests.DataTypeEnum_CSharpNamesAreProtocMangled_NotTheProtoSpelling</c>)
///         /<c>Bytes</c>/<c>File</c> all fall back to <see cref="DriverDataType.String"/> (v1 base64/raw
///         fallback for Bytes/File, per design); every <c>*Array</c> variant maps to its scalar element
///         type (<see cref="IsSparkplugArray"/> carries the "and it's an array" bit separately, since
///         <see cref="DriverDataType"/> itself has no array concept — that lives at the OPC UA
///         ValueRank/ArrayDimensions layer the address-space builder owns). <c>DataSet</c>/
///         <c>Template</c>/<c>PropertySet</c>/<c>PropertySetList</c>/<c>Unknown</c> are unsupported in
///         v1 and map to <see langword="null"/> — deliberately, not a guessed
///         <see cref="DriverDataType.String"/>, so a caller has to make an explicit skip-or-warn
///         decision (unlike Galaxy's <c>DataTypeMap</c>, which silently defaults unknown codes to
///         <c>String</c> for legacy wire-compatibility reasons that do not apply here).
///     </para>
/// </remarks>
public static class SparkplugDataTypeExtensions
{
    /// <summary>
    ///     Maps a Sparkplug metric datatype to the equivalent <see cref="DriverDataType"/>, or
    ///     <see langword="null"/> if the type is unsupported in v1 (<c>DataSet</c>, <c>Template</c>,
    ///     <c>PropertySet</c>, <c>PropertySetList</c>, <c>Unknown</c>). Callers must treat
    ///     <see langword="null"/> as an explicit "skip and warn", never coerce it to a guessed type.
    /// </summary>
    public static DriverDataType? ToDriverDataType(this SparkplugDataType dataType) => dataType switch
    {
        SparkplugDataType.Int8 => DriverDataType.Int16,
        SparkplugDataType.Int16 => DriverDataType.Int16,
        SparkplugDataType.Int32 => DriverDataType.Int32,
        SparkplugDataType.Int64 => DriverDataType.Int64,
        SparkplugDataType.Uint8 => DriverDataType.UInt16,
        SparkplugDataType.Uint16 => DriverDataType.UInt16,
        SparkplugDataType.Uint32 => DriverDataType.UInt32,
        SparkplugDataType.Uint64 => DriverDataType.UInt64,
        SparkplugDataType.Float => DriverDataType.Float32,
        SparkplugDataType.Double => DriverDataType.Float64,
        SparkplugDataType.Boolean => DriverDataType.Boolean,
        SparkplugDataType.String => DriverDataType.String,
        SparkplugDataType.DateTime => DriverDataType.DateTime,
        SparkplugDataType.Text => DriverDataType.String,
        SparkplugDataType.Uuid => DriverDataType.String,
        SparkplugDataType.Bytes => DriverDataType.String,
        SparkplugDataType.File => DriverDataType.String,

        SparkplugDataType.Int8Array => DriverDataType.Int16,
        SparkplugDataType.Int16Array => DriverDataType.Int16,
        SparkplugDataType.Int32Array => DriverDataType.Int32,
        SparkplugDataType.Int64Array => DriverDataType.Int64,
        SparkplugDataType.Uint8Array => DriverDataType.UInt16,
        SparkplugDataType.Uint16Array => DriverDataType.UInt16,
        SparkplugDataType.Uint32Array => DriverDataType.UInt32,
        SparkplugDataType.Uint64Array => DriverDataType.UInt64,
        SparkplugDataType.FloatArray => DriverDataType.Float32,
        SparkplugDataType.DoubleArray => DriverDataType.Float64,
        SparkplugDataType.BooleanArray => DriverDataType.Boolean,
        SparkplugDataType.StringArray => DriverDataType.String,
        SparkplugDataType.DateTimeArray => DriverDataType.DateTime,

        // Unsupported v1 (design §3.5): DataSet/Template are deferred scope; PropertySet/
        // PropertySetList are PropertyValue-only metadata types that never legitimately appear as a
        // Metric's own datatype; Unknown is the proto's explicit "placeholder for future expansion"
        // (index 0). All five fall through here deliberately rather than being listed with a fake
        // mapping.
        _ => null,
    };

    /// <summary>
    ///     Whether <paramref name="dataType"/> is one of the 13 <c>*Array</c> variants. Combine with
    ///     <see cref="ToDriverDataType"/> (which already returns the *element* type for an array
    ///     variant) to build an OPC UA ValueRank=1 array node per design §3.5.
    /// </summary>
    public static bool IsSparkplugArray(this SparkplugDataType dataType) => dataType switch
    {
        SparkplugDataType.Int8Array or SparkplugDataType.Int16Array or SparkplugDataType.Int32Array
            or SparkplugDataType.Int64Array or SparkplugDataType.Uint8Array or SparkplugDataType.Uint16Array
            or SparkplugDataType.Uint32Array or SparkplugDataType.Uint64Array or SparkplugDataType.FloatArray
            or SparkplugDataType.DoubleArray or SparkplugDataType.BooleanArray or SparkplugDataType.StringArray
            or SparkplugDataType.DateTimeArray => true,
        _ => false,
    };
}
