using System.Dynamic;
using System.Text.Json;

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

/// <summary>
/// Wraps a JsonElement as a dynamic object for convenient property access in scripts.
/// Supports property access (obj.name), indexing (obj.items[0]), and ToString().
/// Array indexing accepts any integral index type (int, long, short, byte, ...), so an
/// index derived from another wrapped JSON number — which Wrap surfaces as long — works.
/// </summary>
/// <remarks>
/// The element passed to the constructor is <see cref="JsonElement.Clone()">cloned</see>
/// so the wrapper owns a self-contained copy. This decouples its lifetime from the
/// <see cref="JsonDocument"/> the element originated from: a wrapper built from an
/// element inside a <c>using</c> block remains valid for deferred (e.g. script-time)
/// access after that document has been disposed.
/// </remarks>
public class DynamicJsonElement : DynamicObject
{
    private readonly JsonElement _element;

    /// <summary>Initializes a new <see cref="DynamicJsonElement"/> wrapping a clone of the given <see cref="JsonElement"/>.</summary>
    /// <param name="element">The JSON element to wrap; it is cloned to decouple lifetime from the source document.</param>
    public DynamicJsonElement(JsonElement element)
    {
        // Clone detaches the element from its owning JsonDocument so accessing it
        // later cannot throw ObjectDisposedException once that document is disposed.
        _element = element.Clone();
    }

    /// <inheritdoc />
    public override bool TryGetMember(GetMemberBinder binder, out object? result)
    {
        if (_element.ValueKind == JsonValueKind.Object &&
            _element.TryGetProperty(binder.Name, out var prop))
        {
            result = Wrap(prop);
            return true;
        }
        result = null;
        return false;
    }

    /// <inheritdoc />
    public override bool TryGetIndex(GetIndexBinder binder, object[] indexes, out object? result)
    {
        // Accept any integral index, not just int. DynamicJsonElement surfaces JSON
        // numbers as long (see Wrap), so an index computed from another wrapped value
        // (e.g. obj.items[obj.count - 1]) arrives as a long; byte/short widen too.
        if (_element.ValueKind == JsonValueKind.Array &&
            indexes.Length == 1 && TryGetIntegralIndex(indexes[0], out var index))
        {
            var arrayLength = _element.GetArrayLength();
            if (index >= 0 && index < arrayLength)
            {
                result = Wrap(_element[(int)index]);
                return true;
            }
        }
        result = null;
        return false;
    }

    private static bool TryGetIntegralIndex(object? value, out long index)
    {
        switch (value)
        {
            case int i: index = i; return true;
            case long l: index = l; return true;
            case short s: index = s; return true;
            case byte b: index = b; return true;
            case sbyte sb: index = sb; return true;
            case ushort us: index = us; return true;
            case uint ui: index = ui; return true;
            case ulong ul when ul <= long.MaxValue: index = (long)ul; return true;
            // Whole-valued floating-point indices are accepted too: arithmetic on
            // wrapped JSON numbers can yield a double/decimal even for an integer result.
            case double d when d >= long.MinValue && d <= long.MaxValue && d == Math.Floor(d):
                index = (long)d; return true;
            case decimal m when m == Math.Floor(m):
                index = (long)m; return true;
            default: index = 0; return false;
        }
    }

    /// <inheritdoc />
    public override bool TryConvert(ConvertBinder binder, out object? result)
    {
        // Conversion to object (or dynamic): never null out a present value. Return the
        // unwrapped value for scalars, this wrapper for objects/arrays, and null only
        // when the element is genuinely JSON null.
        if (binder.Type == typeof(object))
        {
            result = _element.ValueKind == JsonValueKind.Null ? null : Wrap(_element);
            return true;
        }

        result = ConvertTo(binder.Type);
        // A non-object target with a null result means ConvertTo could not handle the
        // element/type pair — report failure so the binder surfaces a binding error.
        return result != null;
    }

    /// <inheritdoc />
    public override string ToString()
    {
        return _element.ValueKind switch
        {
            JsonValueKind.String => _element.GetString() ?? "",
            JsonValueKind.Number => _element.GetRawText(),
            JsonValueKind.True => "true",
            JsonValueKind.False => "false",
            JsonValueKind.Null => "",
            _ => _element.GetRawText()
        };
    }

    private object? ConvertTo(Type type)
    {
        if (type == typeof(string)) return ToString();
        if (type == typeof(int) && _element.ValueKind == JsonValueKind.Number) return _element.GetInt32();
        if (type == typeof(long) && _element.ValueKind == JsonValueKind.Number) return _element.GetInt64();
        if (type == typeof(double) && _element.ValueKind == JsonValueKind.Number) return _element.GetDouble();
        if (type == typeof(decimal) && _element.ValueKind == JsonValueKind.Number) return _element.GetDecimal();
        if (type == typeof(bool) && (_element.ValueKind == JsonValueKind.True || _element.ValueKind == JsonValueKind.False))
            return _element.GetBoolean();
        return null;
    }

    private static object? Wrap(JsonElement element)
    {
        return element.ValueKind switch
        {
            JsonValueKind.Object => new DynamicJsonElement(element),
            JsonValueKind.Array => new DynamicJsonElement(element),
            JsonValueKind.String => element.GetString(),
            JsonValueKind.Number => WrapNumber(element),
            JsonValueKind.True => true,
            JsonValueKind.False => false,
            JsonValueKind.Null => null,
            _ => element.GetRawText()
        };
    }

    private static object WrapNumber(JsonElement element)
    {
        // An integral JSON number must box as long, not double. A ternary
        // (TryGetInt64 ? long : double) would unify both branches to double and
        // silently widen the long, so an integral index read back out of the wrapper
        // (e.g. obj.items[obj.count - 1]) would arrive as a double. Box each case
        // with its own typed return so the runtime type is preserved.
        if (element.TryGetInt64(out var l))
            return l;
        return element.GetDouble();
    }
}
