using System.Text.Json;

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

/// <summary>The outcome of reading an enum-valued field from a TagConfig JSON object.</summary>
public enum JsonEnumRead
{
    /// <summary>The property is absent, or present but not a JSON string (nothing to validate).</summary>
    Absent,

    /// <summary>The property is a JSON string that parses (case-insensitively) to the enum.</summary>
    Valid,

    /// <summary>The property is a JSON string that does NOT parse to the enum (a typo'd value).</summary>
    Invalid,
}

/// <summary>
///     Shared TagConfig JSON field readers for the six equipment-tag parsers (05/CONV-2). Replaces the
///     six byte-identical private <c>ReadEnum</c> copies with one strict-capable home beside
///     <see cref="EquipmentTagRefResolver{TDef}"/>.
///     <para><b>Unknown-key handling is a deliberate non-goal.</b> The TagConfig blob is a SHARED
///     namespace — platform intent keys (<c>FullName</c>, <c>alarm</c>, <c>isHistorized</c>, …) coexist
///     with per-driver address keys (<c>region</c>, <c>dataType</c>, …) and forward-compat keys the typed
///     editors preserve. There is no single canonical schema, so these readers read the known fields and
///     ignore the rest; they NEVER warn on unknown keys.</para>
/// </summary>
public static class TagConfigJson
{
    /// <summary>
    ///     Reads an enum-valued string field, distinguishing absent / valid / present-but-invalid so callers
    ///     can be lenient (fall back) while still <em>reporting</em> the invalid case. A property that is
    ///     absent or present-but-non-string yields <see cref="JsonEnumRead.Absent"/> (there is no enum
    ///     string to validate); a present JSON string that parses case-insensitively yields
    ///     <see cref="JsonEnumRead.Valid"/>; a present JSON string that does not parse yields
    ///     <see cref="JsonEnumRead.Invalid"/>.
    /// </summary>
    /// <typeparam name="TEnum">The enum type to parse.</typeparam>
    /// <param name="o">The TagConfig root object.</param>
    /// <param name="name">The property name to read.</param>
    /// <param name="value">The parsed enum on <see cref="JsonEnumRead.Valid"/>; otherwise <c>default</c>.</param>
    /// <returns>The read outcome.</returns>
    public static JsonEnumRead TryReadEnum<TEnum>(JsonElement o, string name, out TEnum value)
        where TEnum : struct, Enum
    {
        if (!o.TryGetProperty(name, out var e) || e.ValueKind != JsonValueKind.String)
        {
            value = default;
            return JsonEnumRead.Absent;
        }
        if (Enum.TryParse(e.GetString(), ignoreCase: true, out value))
            return JsonEnumRead.Valid;
        value = default;
        return JsonEnumRead.Invalid;
    }

    /// <summary>
    ///     Lenient enum read with today's exact semantics: absent OR invalid ⇒ <paramref name="fallback"/>;
    ///     only a present, parseable string yields the parsed value. Bit-for-bit equivalent to the six
    ///     retired private <c>ReadEnum</c> copies.
    /// </summary>
    /// <typeparam name="TEnum">The enum type to parse.</typeparam>
    /// <param name="o">The TagConfig root object.</param>
    /// <param name="name">The property name to read.</param>
    /// <param name="fallback">The value returned when the field is absent or invalid.</param>
    /// <returns>The parsed enum, or <paramref name="fallback"/>.</returns>
    public static TEnum ReadEnumOrDefault<TEnum>(JsonElement o, string name, TEnum fallback)
        where TEnum : struct, Enum
        => TryReadEnum<TEnum>(o, name, out var v) == JsonEnumRead.Valid ? v : fallback;

    /// <summary>
    ///     Strict enum read (R2-11 Phase C — the runtime flip). Absent ⇒ <paramref name="fallback"/>;
    ///     a present, case-insensitively-parseable string ⇒ the parsed value; a present-but-invalid
    ///     string (a typo) ⇒ the read FAILS (returns <see langword="false"/>) so the runtime parser can
    ///     reject the whole tag and the driver surfaces <c>BadNodeIdUnknown</c> — instead of silently
    ///     substituting a wrong-width default and publishing a misleading <c>Good</c>. This is the runtime
    ///     counterpart of <see cref="ReadEnumOrDefault{TEnum}"/>: identical absent/valid handling, but
    ///     <see cref="JsonEnumRead.Invalid"/> is fatal rather than defaulted. The deploy-time gate
    ///     (<c>TagConfigValidationMode.Error</c>) is expected to have already surfaced the same typo via
    ///     <see cref="DescribeInvalidEnum{TEnum}"/>; a typo reaching this fatal path means it was authored
    ///     after the gate, or the gate ran in Warn mode.
    /// </summary>
    /// <typeparam name="TEnum">The enum type to parse.</typeparam>
    /// <param name="o">The TagConfig root object.</param>
    /// <param name="name">The property name to read.</param>
    /// <param name="fallback">The value assigned when the field is absent.</param>
    /// <param name="value">The parsed enum (valid), the fallback (absent), or <c>default</c> (invalid).</param>
    /// <returns><see langword="false"/> only when the field is a present-but-invalid string; otherwise <see langword="true"/>.</returns>
    public static bool TryReadEnumStrict<TEnum>(JsonElement o, string name, TEnum fallback, out TEnum value)
        where TEnum : struct, Enum
    {
        switch (TryReadEnum<TEnum>(o, name, out value))
        {
            case JsonEnumRead.Valid: return true;
            case JsonEnumRead.Absent: value = fallback; return true;
            default: value = default; return false; // Invalid (typo) ⇒ reject the tag
        }
    }

    /// <summary>
    ///     Reads the optional <c>"writable"</c> flag with AbCip semantics (the model): an explicit JSON
    ///     <c>false</c> ⇒ <see langword="false"/>; anything else (absent, <c>true</c>, or a non-bool token)
    ///     ⇒ <paramref name="defaultValue"/>.
    /// </summary>
    /// <param name="o">The TagConfig root object.</param>
    /// <param name="defaultValue">The value returned unless the field is an explicit JSON <c>false</c>.</param>
    /// <returns><see langword="false"/> only for an explicit <c>false</c>; otherwise <paramref name="defaultValue"/>.</returns>
    public static bool ReadWritable(JsonElement o, bool defaultValue = true)
        => o.TryGetProperty("writable", out var e) && e.ValueKind == JsonValueKind.False
            ? false
            : defaultValue;

    /// <summary>
    ///     A human-readable warning for a present-but-invalid enum field (naming the field, the offending
    ///     value, and the valid enum members), or <see langword="null"/> when the field is absent or valid.
    /// </summary>
    /// <typeparam name="TEnum">The enum type expected.</typeparam>
    /// <param name="o">The TagConfig root object.</param>
    /// <param name="name">The property name to describe.</param>
    /// <returns>The warning text, or <see langword="null"/>.</returns>
    public static string? DescribeInvalidEnum<TEnum>(JsonElement o, string name)
        where TEnum : struct, Enum
    {
        if (TryReadEnum<TEnum>(o, name, out _) != JsonEnumRead.Invalid) return null;
        var raw = o.TryGetProperty(name, out var e) ? e.GetString() : null;
        var valid = string.Join(", ", Enum.GetNames<TEnum>());
        return $"value '{raw}' for '{name}' is not a valid {typeof(TEnum).Name}; valid: {valid}";
    }
}
