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 Utf8JsonReader Fast Path (STJ-style streaming)

    /// <summary>
    /// Deserialize using Utf8JsonReader - streaming without DOM allocation.
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private static T? DeserializeWithUtf8Reader<T>(string json, byte maxDepth)
    {
        var (buffer, length) = GetUtf8Bytes(json);
        try
        {
            var reader = new Utf8JsonReader(buffer.AsSpan(0, length), new JsonReaderOptions { MaxDepth = maxDepth });
            if (!reader.Read()) return default;
            return (T?)ReadValueFromReader(ref reader, typeof(T), maxDepth, 0);
        }
        finally
        {
            ReturnUtf8Buffer(buffer);
        }
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private static object? DeserializeWithUtf8ReaderNonGeneric(string json, Type targetType, byte maxDepth)
    {
        var (buffer, length) = GetUtf8Bytes(json);
        try
        {
            var reader = new Utf8JsonReader(buffer.AsSpan(0, length), new JsonReaderOptions { MaxDepth = maxDepth });
            if (!reader.Read()) return null;
            return ReadValueFromReader(ref reader, targetType, maxDepth, 0);
        }
        finally
        {
            ReturnUtf8Buffer(buffer);
        }
    }

    private static object? ReadValueFromReader(ref Utf8JsonReader reader, Type targetType, byte maxDepth, int depth)
    {
        switch (reader.TokenType)
        {
            case JsonTokenType.Null:
                return null;
            case JsonTokenType.True:
                return true;
            case JsonTokenType.False:
                return false;
            case JsonTokenType.Number:
                return ReadNumberFromReader(ref reader, targetType);
            case JsonTokenType.String:
                return ReadStringFromReader(ref reader, targetType);
            case JsonTokenType.StartObject:
                return ReadObjectFromReader(ref reader, targetType, maxDepth, depth);
            case JsonTokenType.StartArray:
                return ReadArrayFromReader(ref reader, targetType, maxDepth, depth);
            default:
                return null;
        }
    }

    /// <summary>
    /// Read number value using cached PropertySetterInfo (avoids Nullable.GetUnderlyingType + Type.GetTypeCode calls).
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private static object? ReadNumberFromReaderCached(ref Utf8JsonReader reader, PropertySetterInfo propInfo)
    {
        return propInfo.PropertyTypeCode switch
        {
            TypeCode.Int32 => reader.TryGetInt32(out var i32) ? i32 : (int)reader.GetDouble(),
            TypeCode.Int64 => reader.TryGetInt64(out var i64) ? i64 : (long)reader.GetDouble(),
            TypeCode.Double => reader.GetDouble(),
            TypeCode.Decimal => reader.GetDecimal(),
            TypeCode.Single => reader.GetSingle(),
            TypeCode.Byte => reader.TryGetByte(out var b) ? b : (byte)reader.GetDouble(),
            TypeCode.Int16 => reader.TryGetInt16(out var i16) ? i16 : (short)reader.GetDouble(),
            TypeCode.UInt16 => reader.TryGetUInt16(out var ui16) ? ui16 : (ushort)reader.GetDouble(),
            TypeCode.UInt32 => reader.TryGetUInt32(out var ui32) ? ui32 : (uint)reader.GetDouble(),
            TypeCode.UInt64 => reader.TryGetUInt64(out var ui64) ? ui64 : (ulong)reader.GetDouble(),
            TypeCode.SByte => reader.TryGetSByte(out var sb) ? sb : (sbyte)reader.GetDouble(),
            _ => propInfo.UnderlyingType.IsEnum 
                ? Enum.ToObject(propInfo.UnderlyingType, reader.TryGetInt32(out var enumVal) ? enumVal : (int)reader.GetDouble()) 
                : reader.GetDouble()
        };
    }

    /// <summary>
    /// Read string value using cached PropertySetterInfo (avoids Nullable.GetUnderlyingType calls).
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private static object? ReadStringFromReaderCached(ref Utf8JsonReader reader, PropertySetterInfo propInfo)
    {
        var type = propInfo.UnderlyingType;
        
        if (ReferenceEquals(type, StringType)) return reader.GetString();
        if (ReferenceEquals(type, DateTimeType)) return reader.GetDateTime();
        if (ReferenceEquals(type, GuidType)) return reader.GetGuid();
        if (ReferenceEquals(type, DateTimeOffsetType)) return reader.GetDateTimeOffset();
        if (ReferenceEquals(type, TimeSpanType)) 
        {
            var str = reader.GetString();
            return str != null ? TimeSpan.Parse(str, CultureInfo.InvariantCulture) : default(TimeSpan);
        }
        if (type.IsEnum)
        {
            var str = reader.GetString();
            return str != null ? Enum.Parse(type, str) : null;
        }
        
        return reader.GetString();
    }

    /// <summary>
    /// Read value from reader using cached PropertySetterInfo for faster type resolution.
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private static object? ReadValueFromReaderCached(ref Utf8JsonReader reader, PropertySetterInfo propInfo, byte maxDepth, int depth)
    {
        switch (reader.TokenType)
        {
            case JsonTokenType.Null:
                return null;
            case JsonTokenType.True:
                return true;
            case JsonTokenType.False:
                return false;
            case JsonTokenType.Number:
                return ReadNumberFromReaderCached(ref reader, propInfo);
            case JsonTokenType.String:
                return ReadStringFromReaderCached(ref reader, propInfo);
            case JsonTokenType.StartObject:
                return ReadObjectFromReader(ref reader, propInfo.PropertyType, maxDepth, depth);
            case JsonTokenType.StartArray:
                return ReadArrayFromReader(ref reader, propInfo.PropertyType, maxDepth, depth);
            default:
                return null;
        }
    }

    private static object? ReadNumberFromReader(ref Utf8JsonReader reader, Type targetType)
    {
        var type = Nullable.GetUnderlyingType(targetType) ?? targetType;
        var typeCode = Type.GetTypeCode(type);
        
        return typeCode switch
        {
            TypeCode.Int32 => reader.TryGetInt32(out var i32) ? i32 : (int)reader.GetDouble(),
            TypeCode.Int64 => reader.TryGetInt64(out var i64) ? i64 : (long)reader.GetDouble(),
            TypeCode.Double => reader.GetDouble(),
            TypeCode.Decimal => reader.GetDecimal(),
            TypeCode.Single => reader.GetSingle(),
            TypeCode.Byte => reader.TryGetByte(out var b) ? b : (byte)reader.GetDouble(),
            TypeCode.Int16 => reader.TryGetInt16(out var i16) ? i16 : (short)reader.GetDouble(),
            TypeCode.UInt16 => reader.TryGetUInt16(out var ui16) ? ui16 : (ushort)reader.GetDouble(),
            TypeCode.UInt32 => reader.TryGetUInt32(out var ui32) ? ui32 : (uint)reader.GetDouble(),
            TypeCode.UInt64 => reader.TryGetUInt64(out var ui64) ? ui64 : (ulong)reader.GetDouble(),
            TypeCode.SByte => reader.TryGetSByte(out var sb) ? sb : (sbyte)reader.GetDouble(),
            _ => type.IsEnum ? Enum.ToObject(type, reader.TryGetInt32(out var enumVal) ? enumVal : (int)reader.GetDouble()) : reader.GetDouble()
        };
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private static object? ReadStringFromReader(ref Utf8JsonReader reader, Type targetType)
    {
        var type = Nullable.GetUnderlyingType(targetType) ?? targetType;
        
        if (ReferenceEquals(type, StringType)) return reader.GetString();
        if (ReferenceEquals(type, DateTimeType)) return reader.GetDateTime();
        if (ReferenceEquals(type, GuidType)) return reader.GetGuid();
        if (ReferenceEquals(type, DateTimeOffsetType)) return reader.GetDateTimeOffset();
        if (ReferenceEquals(type, TimeSpanType)) 
        {
            var str = reader.GetString();
            return str != null ? TimeSpan.Parse(str, CultureInfo.InvariantCulture) : default(TimeSpan);
        }
        if (type.IsEnum)
        {
            var str = reader.GetString();
            return str != null ? Enum.Parse(type, str) : null;
        }
        
        return reader.GetString();
    }

    private static object? ReadObjectFromReader(ref Utf8JsonReader reader, Type targetType, byte maxDepth, int depth)
    {
        if (depth > maxDepth) 
        {
            reader.Skip();
            return null;
        }

        if (IsDictionaryType(targetType, out var keyType, out var valueType))
            return ReadDictionaryFromReader(ref reader, keyType!, valueType!, maxDepth, depth);

        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;

        var nextDepth = depth + 1;
        
        while (reader.Read())
        {
            if (reader.TokenType == JsonTokenType.EndObject)
                break;
            
            if (reader.TokenType != JsonTokenType.PropertyName)
                continue;
            
            // Use UTF8 lookup to avoid string allocation
            if (!metadata.TryGetPropertyUtf8(ref reader, out var propInfo) || propInfo == null)
            {
                if (!reader.Read()) break;
                reader.Skip();
                continue;
            }
            
            if (!reader.Read())
                break;
            
            // Try direct set for primitives (no boxing)
            if (propInfo.TrySetValueDirect(instance, ref reader))
                continue;
            
            // Fallback to boxed path for complex types
            var value = ReadValueFromReaderCached(ref reader, propInfo, maxDepth, nextDepth);
            propInfo.SetValue(instance, value);
        }
        
        return instance;
    }

    /// <summary>
    /// Read object from reader when we're already at StartObject.
    /// </summary>
    private static object? ReadObjectFromReaderAtStart(ref Utf8JsonReader reader, Type targetType, JsonDeserializeTypeMetadata metadata, byte maxDepth, int depth)
    {
        if (depth > maxDepth) 
        {
            reader.Skip();
            return null;
        }

        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;

        var nextDepth = depth + 1;
        
        while (reader.Read())
        {
            if (reader.TokenType == JsonTokenType.EndObject)
                break;
            
            if (reader.TokenType != JsonTokenType.PropertyName)
                continue;
            
            // Use UTF8 lookup to avoid string allocation
            if (!metadata.TryGetPropertyUtf8(ref reader, out var propInfo) || propInfo == null)
            {
                if (!reader.Read()) break;
                reader.Skip();
                continue;
            }
            
            if (!reader.Read())
                break;
            
            // Try direct set for primitives (no boxing)
            if (propInfo.TrySetValueDirect(instance, ref reader))
                continue;
            
            // Fallback to boxed path for complex types
            var value = ReadValueFromReaderCached(ref reader, propInfo, maxDepth, nextDepth);
            propInfo.SetValue(instance, value);
        }
        
        return instance;
    }

    private static object? ReadArrayFromReader(ref Utf8JsonReader reader, Type targetType, byte maxDepth, int depth)
    {
        if (depth > maxDepth)
        {
            reader.Skip();
            return null;
        }
        
        var elementType = GetCollectionElementType(targetType);
        if (elementType == null) return null;

        var nextDepth = depth + 1;
        var list = GetOrCreateListFactory(elementType)(0);

        while (reader.Read())
        {
            if (reader.TokenType == JsonTokenType.EndArray)
                break;
            
            var value = ReadValueFromReader(ref reader, elementType, maxDepth, nextDepth);
            if (value != null)
                list.Add(value);
        }

        if (targetType.IsArray)
        {
            var array = Array.CreateInstance(elementType, list.Count);
            list.CopyTo(array, 0);
            return array;
        }

        return list;
    }

    private static object ReadDictionaryFromReader(ref Utf8JsonReader reader, Type keyType, Type valueType, byte maxDepth, int depth)
    {
        var dictType = DictionaryGenericType.MakeGenericType(keyType, valueType);
        var dict = (IDictionary)Activator.CreateInstance(dictType)!;
        var nextDepth = depth + 1;
        
        while (reader.Read())
        {
            if (reader.TokenType == JsonTokenType.EndObject)
                break;
            
            if (reader.TokenType != JsonTokenType.PropertyName)
                continue;
            
            var keyStr = reader.GetString();
            if (keyStr == null || !reader.Read())
                continue;
            
            object key;
            if (ReferenceEquals(keyType, StringType)) key = keyStr;
            else if (ReferenceEquals(keyType, IntType)) key = int.Parse(keyStr, CultureInfo.InvariantCulture);
            else if (ReferenceEquals(keyType, LongType)) key = long.Parse(keyStr, CultureInfo.InvariantCulture);
            else if (ReferenceEquals(keyType, GuidType)) key = Guid.Parse(keyStr);
            else if (keyType.IsEnum) key = Enum.Parse(keyType, keyStr);
            else key = Convert.ChangeType(keyStr, keyType, CultureInfo.InvariantCulture);
            
            var value = ReadValueFromReader(ref reader, valueType, maxDepth, nextDepth);
            dict.Add(key, value);
        }
        
        return dict;
    }

    /// <summary>
    /// Populate object using Utf8JsonReader streaming (no DOM allocation).
    /// </summary>
    private static void PopulateObjectWithUtf8Reader(string json, object target, JsonDeserializeTypeMetadata metadata, byte maxDepth)
    {
        var (buffer, length) = GetUtf8Bytes(json);
        try
        {
            var reader = new Utf8JsonReader(buffer.AsSpan(0, length), new JsonReaderOptions { MaxDepth = maxDepth });
            
            if (!reader.Read() || reader.TokenType != JsonTokenType.StartObject)
                return;
            
            PopulateObjectMergeFromReader(ref reader, target, metadata, maxDepth, 0);
        }
        finally
        {
            ReturnUtf8Buffer(buffer);
        }
    }

    /// <summary>
    /// Populate list using Utf8JsonReader streaming (no DOM allocation).
    /// </summary>
    private static void PopulateListWithUtf8Reader(string json, IList targetList, in Type listType, byte maxDepth)
    {
        var (buffer, length) = GetUtf8Bytes(json);
        try
        {
            var reader = new Utf8JsonReader(buffer.AsSpan(0, length), new JsonReaderOptions { MaxDepth = maxDepth });
            
            if (!reader.Read() || reader.TokenType != JsonTokenType.StartArray)
                return;
            
            var elementType = GetCollectionElementType(listType);
            if (elementType == null) return;
            
            var acObservable = targetList as IAcObservableCollection;
            acObservable?.BeginUpdate();

            try
            {
                targetList.Clear();
                
                while (reader.Read())
                {
                    if (reader.TokenType == JsonTokenType.EndArray)
                        break;
                    
                    var value = ReadValueFromReader(ref reader, elementType, maxDepth, 1);
                    if (value != null)
                        targetList.Add(value);
                }
            }
            finally { acObservable?.EndUpdate(); }
        }
        finally
        {
            ReturnUtf8Buffer(buffer);
        }
    }

    /// <summary>
    /// Populate object with merge semantics from Utf8JsonReader.
    /// </summary>
    private static void PopulateObjectMergeFromReader(ref Utf8JsonReader reader, object target, JsonDeserializeTypeMetadata metadata, byte maxDepth, int depth)
    {
        var nextDepth = depth + 1;
        var maxDepthReached = nextDepth > maxDepth;
        
        while (reader.Read())
        {
            if (reader.TokenType == JsonTokenType.EndObject)
                break;
            
            if (reader.TokenType != JsonTokenType.PropertyName)
                continue;
            
            // Use UTF8 lookup to avoid string allocation
            if (!metadata.TryGetPropertyUtf8(ref reader, out var propInfo) || propInfo == null)
            {
                if (!reader.Read()) break;
                reader.Skip();
                continue;
            }
            
            if (!reader.Read())
                break;
            
            var tokenType = reader.TokenType;
            
            if (maxDepthReached)
            {
                if (tokenType != JsonTokenType.StartObject && tokenType != JsonTokenType.StartArray)
                {
                    // Try direct set for primitives (no boxing)
                    if (!propInfo.TrySetValueDirect(target, ref reader))
                    {
                        var primitiveValue = ReadPrimitiveFromReaderCached(ref reader, propInfo);
                        propInfo.SetValue(target, primitiveValue);
                    }
                }
                else
                {
                    reader.Skip();
                }
                continue;
            }
            
            // Handle IId collection merge
            if (propInfo.IsIIdCollection && tokenType == JsonTokenType.StartArray)
            {
                var existingCollection = propInfo.GetValue(target);
                if (existingCollection != null)
                {
                    MergeIIdCollectionFromReader(ref reader, existingCollection, propInfo, maxDepth, depth);
                    continue;
                }
            }
            
            // Handle nested objects - merge into existing
            if (tokenType == JsonTokenType.StartObject && !propInfo.PropertyType.IsPrimitive && !ReferenceEquals(propInfo.PropertyType, StringType))
            {
                var existingObj = propInfo.GetValue(target);
                if (existingObj != null)
                {
                    var nestedMetadata = GetTypeMetadata(propInfo.PropertyType);
                    PopulateObjectMergeFromReader(ref reader, existingObj, nestedMetadata, maxDepth, nextDepth);
                    continue;
                }
            }

            // Try direct set for primitives (no boxing)
            if (propInfo.TrySetValueDirect(target, ref reader))
                continue;

            // Fallback to boxed path for complex types
            var value = ReadValueFromReaderCached(ref reader, propInfo, maxDepth, nextDepth);
            propInfo.SetValue(target, value);
        }
    }

    /// <summary>
    /// Read primitive value from Utf8JsonReader using cached PropertySetterInfo.
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private static object? ReadPrimitiveFromReaderCached(ref Utf8JsonReader reader, PropertySetterInfo propInfo)
    {
        var tokenType = reader.TokenType;
        if (tokenType == JsonTokenType.Null) return null;
        if (tokenType == JsonTokenType.True) return true;
        if (tokenType == JsonTokenType.False) return false;
        if (tokenType == JsonTokenType.Number) return ReadNumberFromReaderCached(ref reader, propInfo);
        if (tokenType == JsonTokenType.String) return ReadStringFromReaderCached(ref reader, propInfo);
        return null;
    }

    /// <summary>
    /// Merge IId collection from Utf8JsonReader streaming.
    /// </summary>
    private static void MergeIIdCollectionFromReader(ref Utf8JsonReader reader, object existingCollection, PropertySetterInfo propInfo, byte maxDepth, int depth)
    {
        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;
                    }
                }
            }

            var nextDepth = depth + 1;
            var elementMetadata = GetTypeMetadata(elementType);
            
            while (reader.Read())
            {
                if (reader.TokenType == JsonTokenType.EndArray)
                    break;
                
                if (reader.TokenType != JsonTokenType.StartObject)
                {
                    var primitiveValue = ReadValueFromReader(ref reader, elementType, maxDepth, nextDepth);
                    if (primitiveValue != null) existingList.Add(primitiveValue);
                    continue;
                }
                
                // For objects, we need to read the full object to check the Id
                var newItem = ReadObjectFromReaderAtStart(ref reader, elementType, elementMetadata, maxDepth, nextDepth);
                if (newItem == null) continue;
                
                // Check if this item already exists by Id
                var itemId = propInfo.ElementIdGetter?.Invoke(newItem);
                if (itemId != null && !IsDefaultValue(itemId, idType) && existingById != null)
                {
                    if (existingById.TryGetValue(itemId, out var existingItem))
                    {
                        // Copy properties from newItem to existingItem
                        CopyProperties(newItem, existingItem, elementMetadata);
                        continue;
                    }
                }
                
                existingList.Add(newItem);
            }
        }
        finally { acObservable?.EndUpdate(); }
    }

    /// <summary>
    /// Copy properties from source to target using metadata.
    /// </summary>
    private static void CopyProperties(object source, object target, JsonDeserializeTypeMetadata metadata)
    {
        foreach (var prop in metadata.PropertySettersFrozen.Values)
        {
            var value = prop.GetValue(source);
            if (value != null)
                prop.SetValue(target, value);
        }
    }

    /// <summary>
    /// Read primitive value from Utf8JsonReader.
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private static object? ReadPrimitiveFromReader(ref Utf8JsonReader reader, Type targetType)
    {
        var tokenType = reader.TokenType;
        if (tokenType == JsonTokenType.Null) return null;
        if (tokenType == JsonTokenType.True) return true;
        if (tokenType == JsonTokenType.False) return false;
        if (tokenType == JsonTokenType.Number) return ReadNumberFromReader(ref reader, targetType);
        if (tokenType == JsonTokenType.String) return ReadStringFromReader(ref reader, targetType);
        return null;
    }

    #endregion
}
