using System.Collections;
using System.Globalization;
using System.Text.Json;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;

namespace ZB.MOM.WW.ScadaBridge.Commons.Types;

/// <summary>
/// Canonical, round-trippable codec for attribute values. Scalars encode to an
/// invariant-culture string (identical to the historical representation); List
/// attributes encode to a JSON array. Used wherever a value is stored or
/// transmitted (DB Value column, site SQLite, gRPC wire). <see cref="ValueFormatter"/>
/// remains a separate, display-only (comma-joined) formatter.
/// </summary>
public static class AttributeValueCodec
{
    private static readonly JsonSerializerOptions JsonOpts = new() { WriteIndented = false };

    /// <summary>Encodes a value to its canonical string form.</summary>
    /// <param name="value">The value to encode; scalars use invariant-culture formatting, enumerables encode as a JSON array.</param>
    /// <returns>The canonical string representation of <paramref name="value"/>, or <c>null</c> if <paramref name="value"/> is <c>null</c>.</returns>
    public static string? Encode(object? value)
    {
        switch (value)
        {
            case null: return null;
            case string s: return s;                       // already canonical
            case IFormattable f: return f.ToString(null, CultureInfo.InvariantCulture);
            case IEnumerable e:
                // Native-typed JSON: serialize the runtime collection so System.Text.Json emits
                // numbers/bools unquoted, strings quoted, and DateTime as ISO-8601. Boxed as object
                // so STJ uses the runtime element type. STJ numbers/dates are culture-invariant.
                return JsonSerializer.Serialize<object>(e, JsonOpts);
            default: return value.ToString();
        }
    }

    /// <summary>
    /// Decodes a canonical string. For <see cref="DataType.List"/> returns a typed
    /// <c>List&lt;T&gt;</c>; for scalars returns the string unchanged. Throws
    /// <see cref="FormatException"/> on malformed list JSON or an un-parseable element.
    /// </summary>
    /// <param name="value">The canonical string value to decode, or <c>null</c>/empty for a null result.</param>
    /// <param name="dataType">The attribute's data type; only <see cref="DataType.List"/> triggers list decoding.</param>
    /// <param name="elementType">The list element scalar type, required when <paramref name="dataType"/> is <see cref="DataType.List"/>.</param>
    /// <returns>The decoded value: a typed <c>List&lt;T&gt;</c> for list attributes, or the string unchanged for scalars.</returns>
    public static object? Decode(string? value, DataType dataType, DataType? elementType)
    {
        if (dataType != DataType.List) return value;        // scalar: unchanged
        if (string.IsNullOrEmpty(value)) return null;
        if (elementType is null)
            throw new FormatException("List attribute requires an element type.");

        JsonElement[] raw;
        try { raw = JsonSerializer.Deserialize<JsonElement[]>(value) ?? []; }
        catch (JsonException ex) { throw new FormatException("Malformed list JSON.", ex); }

        var clrType = ElementClrType(elementType.Value);
        var listType = typeof(List<>).MakeGenericType(clrType);
        var result = (IList)Activator.CreateInstance(listType)!;
        foreach (var el in raw)
            result.Add(ParseScalar(JsonElementToString(el), elementType.Value));
        return result;
    }

    private static string? JsonElementToString(JsonElement el) => el.ValueKind switch
    {
        JsonValueKind.String => el.GetString(),   // old form, or string-typed lists
        JsonValueKind.Null => null,               // ParseScalar throws "may not be null"
        _ => el.GetRawText()                       // number/bool → "10" / "1.5" / "true"
    };

    /// <summary>
    /// The CLR element type backing a <see cref="DataType.List"/> of the given
    /// element scalar — the single source of truth for the List element CLR
    /// mapping. Throws <see cref="FormatException"/> for an unsupported element
    /// type (see <see cref="IsValidElementType"/>).
    /// </summary>
    /// <param name="t">The List element scalar type.</param>
    /// <returns>The CLR <see cref="Type"/> backing the given element scalar type.</returns>
    public static Type ElementClrType(DataType t) => t switch
    {
        DataType.String => typeof(string),
        DataType.Int32 => typeof(int),
        DataType.Float => typeof(float),
        DataType.Double => typeof(double),
        DataType.Boolean => typeof(bool),
        DataType.DateTime => typeof(DateTime),
        _ => throw new FormatException($"Unsupported list element type '{t}'.")
    };

    /// <summary>
    /// Coerces a live CLR enumerable (e.g. an OPC UA array) into a typed
    /// <c>List&lt;<see cref="ElementClrType"/>&gt;</c> for the given element type,
    /// converting each element with invariant culture (round-trip parse for
    /// DateTime). Strings are parsed; other CLR types are converted via
    /// <see cref="Convert"/>. This is the object-input counterpart to the
    /// string-input parsing in <see cref="Decode"/>: callers holding decoded JSON
    /// strings go through <see cref="Decode"/>; callers holding a runtime
    /// collection use this. Throws <see cref="FormatException"/> for an
    /// unsupported element type and may throw on an element that cannot be
    /// converted (the caller decides how to handle the failure).
    /// </summary>
    /// <param name="source">The live CLR enumerable to coerce (e.g. an OPC UA array).</param>
    /// <param name="elementType">The target List element scalar type.</param>
    /// <returns>A typed <c>List&lt;T&gt;</c> containing the coerced elements.</returns>
    public static IList CoerceEnumerable(IEnumerable source, DataType elementType)
    {
        ArgumentNullException.ThrowIfNull(source);
        var clrType = ElementClrType(elementType);
        var list = (IList)Activator.CreateInstance(typeof(List<>).MakeGenericType(clrType))!;
        foreach (var element in source)
            list.Add(CoerceElement(element, elementType));
        return list;
    }

    private static object CoerceElement(object? element, DataType t)
    {
        if (element is null)
            throw new FormatException("List elements may not be null.");

        var c = CultureInfo.InvariantCulture;
        return t switch
        {
            DataType.String => Convert.ToString(element, c)
                ?? throw new FormatException("Null string element."),
            DataType.Int32 => element is string si ? int.Parse(si, c) : Convert.ToInt32(element, c),
            DataType.Float => element is string sf ? float.Parse(sf, c) : Convert.ToSingle(element, c),
            DataType.Double => element is string sd ? double.Parse(sd, c) : Convert.ToDouble(element, c),
            DataType.Boolean => element is string sb ? bool.Parse(sb) : Convert.ToBoolean(element, c),
            DataType.DateTime => element is string sdt
                ? DateTime.Parse(sdt, c, DateTimeStyles.RoundtripKind)
                : Convert.ToDateTime(element, c),
            _ => throw new FormatException($"Unsupported list element type '{t}'.")
        };
    }

    private static object? ParseScalar(string? s, DataType t)
    {
        if (s is null) throw new FormatException("List elements may not be null.");
        if (!IsValidElementType(t))
            throw new FormatException($"Unsupported list element type '{t}'.");
        var c = CultureInfo.InvariantCulture;
        try
        {
            return t switch
            {
                DataType.String => s,
                DataType.Int32 => int.Parse(s, c),
                DataType.Float => float.Parse(s, c),
                DataType.Double => double.Parse(s, c),
                DataType.Boolean => bool.Parse(s),
                DataType.DateTime => DateTime.Parse(s, c, DateTimeStyles.RoundtripKind),
                _ => throw new FormatException($"Unsupported list element type '{t}'.")
            };
        }
        catch (Exception ex) when (ex is FormatException or OverflowException)
        {
            throw new FormatException($"List element '{s}' is not a valid {t}.", ex);
        }
    }

    /// <summary>True if the type may be a List element scalar.</summary>
    /// <param name="t">The data type to check.</param>
    /// <returns><c>true</c> if <paramref name="t"/> may be a List element scalar; otherwise <c>false</c>.</returns>
    public static bool IsValidElementType(DataType t) =>
        t is DataType.String or DataType.Int32 or DataType.Float
          or DataType.Double or DataType.Boolean or DataType.DateTime;
}
