using System.Collections;
using System.Globalization;
using System.Runtime.CompilerServices;
using System.Text.Json;
using AyCode.Core.Helpers;
using static AyCode.Core.Helpers.JsonUtilities;

namespace AyCode.Core.Serializers.Jsons;

public static partial class AcJsonDeserializer
{
    #region With Reference Handling (JsonElement Path)

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private static object? ReadValue(in JsonElement element, in Type targetType, DeserializationContext context)
    {
        var kind = element.ValueKind;
        if (kind == JsonValueKind.Object) return ReadObject(element, targetType, context);
        if (kind == JsonValueKind.Array) return ReadArray(element, targetType, context);
        if (kind == JsonValueKind.Null || kind == JsonValueKind.Undefined) return null;
        return ReadPrimitive(element, targetType, kind);
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private static object? ReadObject(in JsonElement element, in Type targetType, DeserializationContext context)
    {
        // Check for $ref first - support both string (Newtonsoft) and int formats
        if (element.TryGetProperty(RefPropertyUtf8, out var refElement))
        {
            var refId = ParseRefId(refElement);
            return context.TryGetReferencedObject(refId, out var refObj) ? refObj : new DeferredReference(refId, targetType);
        }

        if (IsDictionaryType(targetType, out var keyType, out var valueType))
            return ReadDictionary(element, keyType!, valueType!, context);

        var metadata = GetTypeMetadata(targetType);
        
        var instance = metadata.CompiledConstructor?.Invoke();
        if (instance == null)
        {
            try { instance = Activator.CreateInstance(targetType); }
            catch (MissingMethodException ex)
            {
                throw new AcJsonDeserializationException(
                    $"Cannot deserialize type '{targetType.FullName}' because it does not have a parameterless constructor.",
                    null, targetType, ex);
            }
        }
        
        if (instance == null) return null;

        // Check for $id and register - support both string (Newtonsoft) and int formats
        if (element.TryGetProperty(IdPropertyUtf8, out var idElement))
            context.RegisterObject(ParseRefId(idElement), instance);

        PopulateObjectInternal(element, instance, metadata, context);
        
        // ChainMode: Use cached IId info from metadata (no runtime reflection!)
        if (context.IsChainMode && metadata.IsIId && metadata.IdGetter != null && metadata.IdType != null)
        {
            var id = metadata.IdGetter(instance);
            if (id != null && !IsDefaultValue(id, metadata.IdType))
            {
                // Check if we already have this object
                if (context.ChainTracker!.TryGetObject(targetType, id, out var existingObj))
                {
                    // Update existing object's properties and return it
                    CopyPropertiesJson(instance, existingObj!, metadata);
                    return existingObj;
                }
                
                // Register this new object
                context.ChainTracker.TryRegisterIIdObject(instance);
            }
        }
        
        return instance;
    }
    
    /// <summary>
    /// Parse $id/$ref value - supports both string (Newtonsoft format) and int formats.
    /// Only numeric values are supported. Non-numeric string references (e.g., "tag1") are not supported.
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private static int ParseRefId(in JsonElement element)
    {
        if (element.ValueKind == JsonValueKind.Number)
            return element.GetInt32();
        
        if (element.ValueKind == JsonValueKind.String)
        {
            var str = element.GetString();
            if (str == null) return 0;
            
            if (int.TryParse(str, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result))
                return result;
            
            throw new AcJsonDeserializationException(
                $"Non-numeric $id/$ref value '{str}' is not supported. Only numeric reference IDs are supported. " +
                $"If you need string-based references, please use a different serializer or convert the JSON to use numeric IDs.",
                str, typeof(int));
        }
        
        return 0;
    }

    /// <summary>
    /// Copies properties from source to target using JSON metadata.
    /// </summary>
    private static void CopyPropertiesJson(object source, object target, JsonDeserializeTypeMetadata metadata)
    {
        var props = metadata.PropertiesArray;
        for (var i = 0; i < props.Length; i++)
        {
            var prop = props[i];
            var value = prop.GetValue(source);
            if (value != null)
                prop.SetValue(target, value);
        }
    }

    private static void PopulateObjectInternal(in JsonElement element, object target, JsonDeserializeTypeMetadata metadata, DeserializationContext context)
    {
        var propsDict = metadata.PropertySettersFrozen;
        
        foreach (var jsonProp in element.EnumerateObject())
        {
            var propName = jsonProp.Name;
            
            // Skip $ properties
            if (propName.Length > 0 && propName[0] == '$') continue;

            if (!propsDict.TryGetValue(propName, out var propInfo)) continue;
            
            var value = ReadValue(jsonProp.Value, propInfo.PropertyType, context);
            
            if (value is DeferredReference deferred)
                context.AddPropertyToResolve(target, propInfo, deferred.RefId);
            else
                propInfo.SetValue(target, value);
        }
    }

    private static void PopulateObjectInternalMerge(in JsonElement element, object target, JsonDeserializeTypeMetadata metadata, DeserializationContext context)
    {
        var propsDict = metadata.PropertySettersFrozen;
        
        foreach (var jsonProp in element.EnumerateObject())
        {
            var propName = jsonProp.Name;
            
            // Skip $ properties
            if (propName.Length > 0 && propName[0] == '$') continue;

            if (!propsDict.TryGetValue(propName, out var propInfo)) continue;
            
            var propValue = jsonProp.Value;
            var propValueKind = propValue.ValueKind;
            
            // Handle IId collection merge
            if (propInfo.IsIIdCollection && propValueKind == JsonValueKind.Array)
            {
                var existingCollection = propInfo.GetValue(target);
                if (existingCollection != null)
                {
                    MergeIIdCollection(propValue, existingCollection, propInfo, context);
                    continue;
                }
            }
            
            // Handle nested objects
            if (propValueKind == JsonValueKind.Object)
            {
                // Check for $ref
                if (propValue.TryGetProperty(RefPropertyUtf8, out _))
                {
                    var value = ReadValue(propValue, propInfo.PropertyType, context);
                    if (value is DeferredReference deferred)
                        context.AddPropertyToResolve(target, propInfo, deferred.RefId);
                    else
                        propInfo.SetValue(target, value);
                    continue;
                }
                
                // Merge into existing object
                if (!propInfo.PropertyType.IsPrimitive && !ReferenceEquals(propInfo.PropertyType, StringType))
                {
                    var existingObj = propInfo.GetValue(target);
                    if (existingObj != null)
                    {
                        var nestedMetadata = GetTypeMetadata(propInfo.PropertyType);
                        PopulateObjectInternalMerge(propValue, existingObj, nestedMetadata, context);
                        continue;
                    }
                }
            }

            var value2 = ReadValue(propValue, propInfo.PropertyType, context);
            
            if (value2 is DeferredReference deferred2)
                context.AddPropertyToResolve(target, propInfo, deferred2.RefId);
            else
                propInfo.SetValue(target, value2);
        }
    }

    private static void PopulateList(in JsonElement arrayElement, IList targetList, in Type listType, DeserializationContext context)
    {
        var elementType = GetCollectionElementType(listType);
        if (elementType == null) return;
        
        var acObservable = targetList as IAcObservableCollection;
        acObservable?.BeginUpdate();

        try
        {
            targetList.Clear();

            // ChainMode: Use cached IId info from element type metadata (no runtime reflection!)
            JsonDeserializeTypeMetadata? elementMetadata = null;
            if (context.IsChainMode && !IsPrimitiveOrStringFast(elementType))
            {
                elementMetadata = GetTypeMetadata(elementType);
            }
            
            foreach (var item in arrayElement.EnumerateArray())
            {
                var value = ReadValue(item, elementType, context);
                
                // ChainMode: Check if we already have this IId object using cached metadata
                if (context.IsChainMode && value != null && elementMetadata != null &&
                    elementMetadata.IsIId && elementMetadata.IdGetter != null && elementMetadata.IdType != null)
                {
                    var id = elementMetadata.IdGetter(value);
                    if (id != null && !IsDefaultValue(id, elementMetadata.IdType))
                    {
                        if (context.ChainTracker!.TryGetObject(elementType, id, out var existingObj))
                        {
                            // Use existing object instead of new one
                            targetList.Add(existingObj);
                            continue;
                        }
                    }
                }
                
                if (value != null)
                    targetList.Add(value);
            }
        }
        finally { acObservable?.EndUpdate(); }
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private static object? ReadArray(in JsonElement element, in Type targetType, DeserializationContext context)
    {
        var elementType = GetCollectionElementType(targetType);
        if (elementType == null) return null;

        if (targetType.IsArray)
        {
            var list = GetOrCreateListFactory(elementType)(0);
            foreach (var item in element.EnumerateArray())
                list.Add(ReadValue(item, elementType, context));
            
            var array = Array.CreateInstance(elementType, list.Count);
            list.CopyTo(array, 0);
            return array;
        }

        IList? targetList = null;
        try
        {
            var instance = Activator.CreateInstance(targetType);
            if (instance is IList list) targetList = list;
        }
        catch { /* Fallback to List<T> */ }
        
        targetList ??= GetOrCreateListFactory(elementType)(0);

        var acObservable = targetList as IAcObservableCollection;
        acObservable?.BeginUpdate();

        try
        {
            foreach (var item in element.EnumerateArray())
                targetList.Add(ReadValue(item, elementType, context));
        }
        finally { acObservable?.EndUpdate(); }

        return targetList;
    }

    private static void MergeIIdCollection(in JsonElement arrayElement, object existingCollection, PropertySetterInfo propInfo, DeserializationContext context)
    {
        var elementType = propInfo.ElementType!;
        var idGetter = propInfo.ElementIdGetter!;
        var idType = propInfo.ElementIdType!;

        var existingList = (IList)existingCollection;
        var count = existingList.Count;
        
        var acObservable = existingList as IAcObservableCollection;
        acObservable?.BeginUpdate();

        try
        {
            Dictionary<object, object>? existingById = null;
            if (count > 0)
            {
                existingById = new Dictionary<object, object>(count);
                for (var i = 0; i < count; i++)
                {
                    var item = existingList[i];
                    if (item != null)
                    {
                        var id = idGetter(item);
                        if (id != null && !IsDefaultValue(id, idType))
                            existingById[id] = item;
                    }
                }
            }

            foreach (var jsonItem in arrayElement.EnumerateArray())
            {
                if (jsonItem.ValueKind != JsonValueKind.Object) continue;

                object? itemId = null;
                if (jsonItem.TryGetProperty("Id", out var idProp))
                    itemId = ReadPrimitive(idProp, idType, idProp.ValueKind);

                if (itemId != null && !IsDefaultValue(itemId, idType) && existingById != null)
                {
                    if (existingById.TryGetValue(itemId, out var existingItem))
                    {
                        var itemMetadata = GetTypeMetadata(elementType);
                        PopulateObjectInternalMerge(jsonItem, existingItem, itemMetadata, context);
                        continue;
                    }
                }
                
                var newItem = ReadValue(jsonItem, elementType, context);
                if (newItem != null) existingList.Add(newItem);
            }
        }
        finally { acObservable?.EndUpdate(); }
    }

    private static object ReadDictionary(in JsonElement element, in Type keyType, in Type valueType, DeserializationContext context)
    {
        var dictType = DictionaryGenericType.MakeGenericType(keyType, valueType);
        var dict = (IDictionary)Activator.CreateInstance(dictType)!;

        foreach (var prop in element.EnumerateObject())
        {
            var name = prop.Name;
            if (name.Length > 0 && name[0] == '$') continue;
            
            object key;
            if (ReferenceEquals(keyType, StringType)) key = name;
            else if (ReferenceEquals(keyType, IntType)) key = int.Parse(name, CultureInfo.InvariantCulture);
            else if (ReferenceEquals(keyType, LongType)) key = long.Parse(name, CultureInfo.InvariantCulture);
            else if (ReferenceEquals(keyType, GuidType)) key = Guid.Parse(name);
            else if (keyType.IsEnum) key = Enum.Parse(keyType, name);
            else key = Convert.ChangeType(name, keyType, CultureInfo.InvariantCulture);
            
            dict.Add(key, ReadValue(prop.Value, valueType, context));
        }
        
        return dict;
    }

    #endregion
}
