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

namespace ScadaLink.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().
/// </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;

    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();
    }

    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;
    }

    public override bool TryGetIndex(GetIndexBinder binder, object[] indexes, out object? result)
    {
        if (_element.ValueKind == JsonValueKind.Array &&
            indexes.Length == 1 && indexes[0] is int index)
        {
            var arrayLength = _element.GetArrayLength();
            if (index >= 0 && index < arrayLength)
            {
                result = Wrap(_element[index]);
                return true;
            }
        }
        result = null;
        return false;
    }

    public override bool TryConvert(ConvertBinder binder, out object? result)
    {
        result = ConvertTo(binder.Type);
        return result != null || binder.Type == typeof(object);
    }

    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 => element.TryGetInt64(out var l) ? l : element.GetDouble(),
            JsonValueKind.True => true,
            JsonValueKind.False => false,
            JsonValueKind.Null => null,
            _ => element.GetRawText()
        };
    }
}
