using System.Collections.Concurrent;
using System.Diagnostics.CodeAnalysis;
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using AyCode.Core.Serializers.Expressions;
using LExpression = System.Linq.Expressions.Expression;
using LExpressionType = System.Linq.Expressions.ExpressionType;

namespace AyCode.Core.Serializers;

/// <summary>
/// Common utilities shared across all serializers (JSON, Binary).
/// Centralizes expression compilation helpers to eliminate code duplication.
/// Type references and caches remain in JsonUtilities for backward compatibility.
/// </summary>
public static class AcSerializerCommon
{
    #region Type References
    
    /// <summary>
    /// Expression base type for type checking.
    /// </summary>
    public static readonly Type ExpressionType = typeof(LExpression);
    
    /// <summary>
    /// Generic Expression&lt;TDelegate&gt; type definition.
    /// </summary>
    public static readonly Type ExpressionGenericType = typeof(Expression<>);
    
    /// <summary>
    /// Generic IQueryable&lt;T&gt; type definition.
    /// </summary>
    public static readonly Type QueryableGenericType = typeof(IQueryable<>);
    
    /// <summary>
    /// Non-generic IQueryable type.
    /// </summary>
    public static readonly Type QueryableType = typeof(IQueryable);
    
    #endregion

    #region ThreadLocal Cache Helper
    
    /// <summary>
    /// Maximum local cache size before clearing.
    /// Clear() is preferred over new Dictionary() because:
    /// 1. Reuses already allocated internal array (no new allocation)
    /// 2. Reduces GC pressure
    /// 3. Adaptive: if cache grew to 64 once, it stays at that capacity
    /// </summary>
    public const int MaxLocalCacheSize = 64;

    /// <summary>
    /// Helper class for ThreadLocal caching pattern used across serializers.
    /// Provides a two-level cache: ThreadLocal (fast) + ConcurrentDictionary (shared).
    /// </summary>
    /// <typeparam name="TKey">Cache key type (usually Type)</typeparam>
    /// <typeparam name="TValue">Cached value type</typeparam>
    public sealed class ThreadLocalCache<TKey, TValue>(Func<TKey, TValue> factory)
        where TKey : notnull
    {
        private readonly ConcurrentDictionary<TKey, TValue> _globalCache = new();

        [ThreadStatic]
        private static Dictionary<TKey, TValue>? t_localCache;

        /// <summary>
        /// Gets a value from cache, creating it if necessary.
        /// Uses ThreadLocal cache for hot path, falls back to ConcurrentDictionary.
        /// </summary>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public TValue Get(TKey key)
        {
            // Fast path: check ThreadLocal cache first
            var localCache = t_localCache;
            if (localCache != null && localCache.TryGetValue(key, out var cached))
            {
                return cached;
            }
            
            // Slow path
            return GetSlow(key);
        }

        [MethodImpl(MethodImplOptions.NoInlining)]
        private TValue GetSlow(TKey key)
        {
            var value = _globalCache.GetOrAdd(key, factory);
            
            // Populate ThreadLocal cache
            var localCache = t_localCache ??= new Dictionary<TKey, TValue>();
            
            // Clear when full - reuses internal array
            if (localCache.Count >= MaxLocalCacheSize)
            {
                localCache.Clear();
            }
            
            localCache[key] = value;
            return value;
        }

        /// <summary>
        /// Clears the ThreadLocal cache for the current thread.
        /// </summary>
        public static void ClearLocalCache()
        {
            t_localCache?.Clear();
        }
    }

    #endregion

    #region Type Checking
    
    /// <summary>
    /// Checks if a type is an Expression type.
    /// Shared across JSON and Binary serializers.
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static bool IsExpressionType(Type type)
    {
        return ExpressionType.IsAssignableFrom(type);
    }
    
    /// <summary>
    /// Checks if a type is Expression&lt;TDelegate&gt; (e.g., Expression&lt;Func&lt;T, bool&gt;&gt;).
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static bool IsGenericExpressionType(Type type)
    {
        return type.IsGenericType && type.GetGenericTypeDefinition() == ExpressionGenericType;
    }
    
    /// <summary>
    /// Checks if a type is IQueryable or IQueryable&lt;T&gt;.
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static bool IsQueryableType(Type type)
    {
        if (QueryableType.IsAssignableFrom(type))
            return true;
        if (type.IsGenericType && type.GetGenericTypeDefinition() == QueryableGenericType)
            return true;
        return false;
    }
    
    /// <summary>
    /// Gets the entity type from IQueryable&lt;TEntity&gt;.
    /// Returns null if not a valid queryable type.
    /// </summary>
    public static Type? GetQueryableEntityType(Type queryableType)
    {
        if (queryableType.IsGenericType && queryableType.GetGenericTypeDefinition() == QueryableGenericType)
            return queryableType.GetGenericArguments()[0];
            
        // Check interfaces for IQueryable<T>
        foreach (var iface in queryableType.GetInterfaces())
        {
            if (iface.IsGenericType && iface.GetGenericTypeDefinition() == QueryableGenericType)
                return iface.GetGenericArguments()[0];
        }
        
        return null;
    }
    
    /// <summary>
    /// Gets the entity type from Expression&lt;Func&lt;TEntity, TResult&gt;&gt;.
    /// Returns null if not a valid expression type.
    /// </summary>
    public static Type? GetExpressionEntityType(Type expressionType)
    {
        if (!IsGenericExpressionType(expressionType))
            return null;
            
        var delegateType = expressionType.GetGenericArguments()[0];
        if (!delegateType.IsGenericType)
            return null;
            
        var delegateArgs = delegateType.GetGenericArguments();
        return delegateArgs.Length > 0 ? delegateArgs[0] : null;
    }
    
    #endregion

    #region IQueryable Serialization
    
    /// <summary>
    /// Converts an IQueryable to AcExpressionNode for serialization.
    /// The query's expression tree is serialized.
    /// </summary>
    public static AcExpressionNode QueryableToNode(IQueryable queryable)
    {
        return AcExpressionConverter.ToNode(queryable.Expression);
    }
    
    /// <summary>
    /// Applies an AcExpressionNode to an IQueryable source.
    /// Used to rebuild the query on the server side.
    /// </summary>
    public static IQueryable<T> ApplyQueryFromNode<T>(IQueryable<T> source, AcExpressionNode node)
    {
        var expression = RebuildQueryExpression(source.Expression, node, typeof(T));
        return source.Provider.CreateQuery<T>(expression);
    }
    
    /// <summary>
    /// Executes an aggregate query (Count, Sum, Any, etc.) from an AcExpressionNode.
    /// Returns the scalar result of the query.
    /// </summary>
    public static object? ExecuteQueryFromNode<T>(IQueryable<T> source, AcExpressionNode node)
    {
        var expression = RebuildQueryExpression(source.Expression, node, typeof(T));
        return source.Provider.Execute(expression);
    }
    
    /// <summary>
    /// Rebuilds a query expression, replacing the source with the provided expression.
    /// </summary>
    private static LExpression RebuildQueryExpression(LExpression sourceExpression, AcExpressionNode node, Type entityType)
    {
        if (node is { NodeType: LExpressionType.Call, MethodName: not null })
        {
            return RebuildMethodCallChain(sourceExpression, node, entityType);
        }

        // If it's just a lambda (filter expression), wrap it in a Where call
        if (node.NodeType == LExpressionType.Lambda)
        {
            var lambda = AcExpressionRebuilder.FromNode(node, entityType);
            
            var whereMethod = typeof(Queryable).GetMethods()
                .First(m => m.Name == "Where" && m.GetParameters().Length == 2)
                .MakeGenericMethod(entityType);

            return LExpression.Call(whereMethod, sourceExpression, lambda);
        }

        throw new NotSupportedException($"Cannot apply expression of type '{node.NodeType}' to IQueryable.");
    }

    /// <summary>
    /// Rebuilds a chain of method calls (Where, OrderBy, Skip, Take, etc.)
    /// Automatically replaces the original IQueryable source with the server's source.
    /// </summary>
    private static LExpression RebuildMethodCallChain(LExpression sourceExpression, AcExpressionNode node, Type entityType)
    {
        // First, process the inner expression (the source of this method call)
        LExpression currentSource;
        
        if (node.Arguments?.Count > 0)
        {
            var firstArg = node.Arguments[0];
            
            // Check if first argument is another method call (recursive chain)
            if (firstArg.NodeType == LExpressionType.Call)
            {
                currentSource = RebuildMethodCallChain(sourceExpression, firstArg, entityType);
            }
            // Check if first argument is a Constant (this is the original IQueryable source - replace it)
            else if (firstArg.NodeType == LExpressionType.Constant)
            {
                // The original source (e.g., empty list from client) - replace with server source
                currentSource = sourceExpression;
            }
            else
            {
                // Other cases - use the provided source
                currentSource = sourceExpression;
            }
        }
        else
        {
            currentSource = sourceExpression;
        }

        // Now apply this method call
        var methodName = node.MethodName!;
        var declaringType = ResolveDeclaringType(node.DeclaringType);

        // Find the method
        var method = FindQueryableMethod(declaringType, methodName, node.GenericArguments, node.Arguments?.Count ?? 1);
        
        if (method == null)
            throw new InvalidOperationException($"Method '{methodName}' not found on type '{declaringType.FullName}'.");

        // Apply generic type arguments
        if (method.IsGenericMethodDefinition && node.GenericArguments?.Count > 0)
        {
            var genericTypes = node.GenericArguments.Select(t => ResolveTypeName(t) ?? entityType).ToArray();
            method = method.MakeGenericMethod(genericTypes);
        }

        // Build arguments - first argument is always the source
        var arguments = new List<LExpression> { currentSource };

        // Process remaining arguments (skip first - it's the source we already handled)
        if (node.Arguments?.Count > 1)
        {
            for (var i = 1; i < node.Arguments.Count; i++)
            {
                var argNode = node.Arguments[i];
                
                if (argNode.NodeType == LExpressionType.Quote && argNode.Operand != null)
                {
                    // Quoted lambda - unquote and deserialize
                    var lambda = AcExpressionRebuilder.FromNode(argNode.Operand, entityType);
                    arguments.Add(LExpression.Quote(lambda));
                }
                else if (argNode.NodeType == LExpressionType.Lambda)
                {
                    // Lambda needs to be quoted for Queryable methods
                    var lambda = AcExpressionRebuilder.FromNode(argNode, entityType);
                    arguments.Add(LExpression.Quote(lambda));
                }
                else
                {
                    // Other arguments (e.g., int for Skip/Take)
                    arguments.Add(AcExpressionRebuilder.FromNode(argNode, entityType));
                }
            }
        }

        return LExpression.Call(method, arguments);
    }

    /// <summary>
    /// Resolves the declaring type from the type name.
    /// Supports AssemblyQualifiedName, FullName, and searches all loaded assemblies as fallback.
    /// Works with any ORM extension methods (EF Core Include, linq2db LoadWith, etc.)
    /// </summary>
    private static Type ResolveDeclaringType(string? typeName)
    {
        if (string.IsNullOrEmpty(typeName))
            return typeof(Queryable);
        
        // Try direct resolution first (works for AssemblyQualifiedName)
        var type = Type.GetType(typeName);
        if (type != null) 
            return type;
        
        // Extract the type name without assembly info for searching
        var typeNameOnly = typeName.Contains(',') 
            ? typeName.Substring(0, typeName.IndexOf(',')) 
            : typeName;
        
        // Fast path for common LINQ types
        if (typeNameOnly is "System.Linq.Queryable") return typeof(Queryable);
        if (typeNameOnly is "System.Linq.Enumerable") return typeof(Enumerable);
        
        // Search all loaded assemblies (works for FullName when assembly is loaded)
        foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
        {
            type = assembly.GetType(typeNameOnly);
            if (type != null) 
                return type;
        }
        
        // Fallback to Queryable
        return typeof(Queryable);
    }

    /// <summary>
    /// Resolves a type from its name.
    /// Supports AssemblyQualifiedName, FullName, and searches all loaded assemblies as fallback.
    /// </summary>
    internal static Type? ResolveTypeName(string typeName)
    {
        // Try direct resolution first (works for AssemblyQualifiedName)
        var type = Type.GetType(typeName);
        if (type != null) 
            return type;
        
        // Extract the type name without assembly info
        var typeNameOnly = typeName.Contains(',') 
            ? typeName.Substring(0, typeName.IndexOf(',')) 
            : typeName;
        
        // Fast path for common types
        type = typeNameOnly switch
        {
            "System.String" or "string" => typeof(string),
            "System.Int32" or "int" => typeof(int),
            "System.Int64" or "long" => typeof(long),
            "System.Int16" or "short" => typeof(short),
            "System.Byte" or "byte" => typeof(byte),
            "System.SByte" or "sbyte" => typeof(sbyte),
            "System.Boolean" or "bool" => typeof(bool),
            "System.Double" or "double" => typeof(double),
            "System.Single" or "float" => typeof(float),
            "System.Decimal" or "decimal" => typeof(decimal),
            "System.DateTime" => typeof(DateTime),
            "System.DateTimeOffset" => typeof(DateTimeOffset),
            "System.TimeSpan" => typeof(TimeSpan),
            "System.DateOnly" => typeof(DateOnly),
            "System.TimeOnly" => typeof(TimeOnly),
            "System.Guid" => typeof(Guid),
            "System.Object" or "object" => typeof(object),
            _ => null
        };
        
        if (type != null) 
            return type;
        
        // Search all loaded assemblies
        foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
        {
            type = assembly.GetType(typeNameOnly);
            if (type != null) 
                return type;
        }
        
        return null;
    }

    private static MethodInfo? FindQueryableMethod(Type declaringType, string methodName, List<string>? genericArgs, int argCount)
    {
        return declaringType.GetMethods()
            .Where(m => m.Name == methodName)
            .FirstOrDefault(m => 
            {
                var parameters = m.GetParameters();
                if (parameters.Length != argCount) return false;
                
                // Check if generic argument count matches
                if (m.IsGenericMethodDefinition)
                {
                    var genericCount = genericArgs?.Count ?? 1;
                    if (m.GetGenericArguments().Length != genericCount) return false;
                }
                
                return true;
            });
    }
    
    #endregion

    #region Expression Rebuilding
    
    /// <summary>
    /// Rebuilds an Expression from AcExpressionNode for the target Expression type.
    /// Used by both JSON and Binary deserializers.
    /// </summary>
    /// <param name="node">The serialized expression node</param>
    /// <param name="targetExpressionType">The target Expression&lt;TDelegate&gt; type</param>
    /// <returns>The rebuilt Expression, or null if conversion fails</returns>
    public static object? RebuildExpression(AcExpressionNode node, Type targetExpressionType)
    {
        if (node == null)
            return null;
            
        var entityType = GetExpressionEntityType(targetExpressionType);
        return AcExpressionRebuilder.FromNode(node, entityType);
    }
    
    #endregion

    #region Expression Compilation Helpers
    
    /// <summary>
    /// Creates a compiled getter for a property using expression trees.
    /// Shared across all TypeMetadata implementations.
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static Func<object, object?> CreateCompiledGetter(
        [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] Type declaringType,
        PropertyInfo prop)
    {
        // NativeAOT (and other no-dynamic-code targets): fall back to plain reflection.
        // The returned delegate is cached per-property by the caller, so the indirection cost is paid
        // only at cache-population time — hot path is a direct delegate invocation either way.
        if (!RuntimeFeature.IsDynamicCodeSupported)
            return prop.GetValue;

        var objParam = LExpression.Parameter(typeof(object), "obj");
        var castExpr = LExpression.Convert(objParam, declaringType);
        var propAccess = LExpression.Property(castExpr, prop);
        var boxed = LExpression.Convert(propAccess, typeof(object));
        return LExpression.Lambda<Func<object, object?>>(boxed, objParam).Compile();
    }
    
    /// <summary>
    /// Creates a compiled setter for a property using expression trees.
    /// Handles nullable value types correctly, including null values.
    /// </summary>
    public static Action<object, object?> CreateCompiledSetter(
        [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] Type declaringType,
        PropertyInfo prop)
    {
        if (!RuntimeFeature.IsDynamicCodeSupported)
            return prop.SetValue;

        var targetParam = LExpression.Parameter(typeof(object), "target");
        var valueParam = LExpression.Parameter(typeof(object), "value");

        var castTarget = LExpression.Convert(targetParam, declaringType);
        var propertyAccess = LExpression.Property(castTarget, prop);
        
        Expression castValue;
        var propertyType = prop.PropertyType;
        var underlyingType = Nullable.GetUnderlyingType(propertyType);
        
        if (underlyingType != null)
        {
            // Nullable value type: handle null case with conditional
            // if (value == null) property = default; else property = (T?)Unbox(value)
            var nullCheck = LExpression.Equal(valueParam, LExpression.Constant(null, typeof(object)));
            var nullValue = LExpression.Default(propertyType);
            var unboxed = LExpression.Unbox(valueParam, underlyingType);
            var converted = LExpression.Convert(unboxed, propertyType);
            castValue = LExpression.Condition(nullCheck, nullValue, converted);
        }
        else if (propertyType.IsValueType)
        {
            // Non-nullable value type: use Unbox for proper unboxing
            castValue = LExpression.Unbox(valueParam, propertyType);
        }
        else
        {
            // Reference type: use TypeAs for safe casting
            castValue = LExpression.TypeAs(valueParam, propertyType);
        }

        var assign = LExpression.Assign(propertyAccess, castValue);
        return LExpression.Lambda<Action<object, object?>>(assign, targetParam, valueParam).Compile();
    }
    
    /// <summary>
    /// Creates a compiled parameterless constructor for a type.
    /// Returns null if type is abstract or has no parameterless constructor.
    /// </summary>
    /// <remarks>
    /// NativeAOT: <see cref="DynamicallyAccessedMembersAttribute"/> with
    /// <see cref="DynamicallyAccessedMemberTypes.PublicParameterlessConstructor"/> ensures the trimmer
    /// preserves the public parameterless ctor's reflection metadata on the supplied type — required
    /// for <see cref="Type.GetConstructor(BindingFlags, Binder?, Type[], ParameterModifier[]?)"/> to
    /// return non-null AND for the reflection-fallback <c>ctor.Invoke(null)</c> path (when
    /// <see cref="RuntimeFeature.IsDynamicCodeSupported"/> is false). Without this annotation the
    /// trimmer drops the metadata and both paths fail with <see cref="MissingMethodException"/>.
    /// </remarks>
    public static Func<object>? CreateCompiledConstructor(
        [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] Type type)
    {
        if (type.IsAbstract) return null;

        var ctor = type.GetConstructor(BindingFlags.Instance | BindingFlags.Public, null, Type.EmptyTypes, null);
        if (ctor == null) return null;

        if (!RuntimeFeature.IsDynamicCodeSupported)
            return () => ctor.Invoke(null);

        var newExpr = LExpression.New(ctor);
        var convert = LExpression.Convert(newExpr, typeof(object));
        return LExpression.Lambda<Func<object>>(convert).Compile();
    }
    
    /// <summary>
    /// Creates a typed getter delegate to avoid boxing for value types.
    /// </summary>
    public static Func<object, TProperty> CreateTypedGetter<TProperty>(
        [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] Type declaringType,
        PropertyInfo prop)
    {
        if (!RuntimeFeature.IsDynamicCodeSupported)
            return obj => (TProperty)prop.GetValue(obj)!;

        var objParam = LExpression.Parameter(typeof(object), "obj");
        var castExpr = LExpression.Convert(objParam, declaringType);
        var propAccess = LExpression.Property(castExpr, prop);

        // Only convert if property type differs from TProperty (avoids unnecessary boxing)
        Expression resultExpr = prop.PropertyType == typeof(TProperty)
            ? propAccess
            : LExpression.Convert(propAccess, typeof(TProperty));

        return LExpression.Lambda<Func<object, TProperty>>(resultExpr, objParam).Compile();
    }
    
    /// <summary>
    /// Creates an enum getter that returns int to avoid boxing.
    /// </summary>
    public static Func<object, int> CreateEnumGetter(
        [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] Type declaringType,
        PropertyInfo prop)
    {
        if (!RuntimeFeature.IsDynamicCodeSupported)
            return obj => Convert.ToInt32(prop.GetValue(obj));

        var objParam = LExpression.Parameter(typeof(object), "obj");
        var castExpr = LExpression.Convert(objParam, declaringType);
        var propAccess = LExpression.Property(castExpr, prop);
        var convertToInt = LExpression.Convert(propAccess, typeof(int));
        return LExpression.Lambda<Func<object, int>>(convertToInt, objParam).Compile();
    }

    /// <summary>
    /// Creates a typed setter delegate to avoid boxing for value types.
    /// </summary>
    public static Action<object, TProperty> CreateTypedSetter<TProperty>(
        [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] Type declaringType,
        PropertyInfo prop)
    {
        if (!RuntimeFeature.IsDynamicCodeSupported)
            return (obj, value) => prop.SetValue(obj, value);

        var objParam = LExpression.Parameter(typeof(object), "obj");
        var valueParam = LExpression.Parameter(typeof(TProperty), "value");
        var castExpr = LExpression.Convert(objParam, declaringType);
        var propAccess = LExpression.Property(castExpr, prop);
        var assign = LExpression.Assign(propAccess, valueParam);
        return LExpression.Lambda<Action<object, TProperty>>(assign, objParam, valueParam).Compile();
    }

    /// <summary>
    /// Creates an enum setter that accepts int to avoid boxing.
    /// </summary>
    public static Action<object, int> CreateEnumSetter(
        [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] Type declaringType,
        PropertyInfo prop)
    {
        if (!RuntimeFeature.IsDynamicCodeSupported)
        {
            var enumType = prop.PropertyType;
            return (obj, value) => prop.SetValue(obj, Enum.ToObject(enumType, value));
        }

        var objParam = LExpression.Parameter(typeof(object), "obj");
        var valueParam = LExpression.Parameter(typeof(int), "value");
        var castExpr = LExpression.Convert(objParam, declaringType);
        var propAccess = LExpression.Property(castExpr, prop);
        var convertToEnum = LExpression.Convert(valueParam, prop.PropertyType);
        var assign = LExpression.Assign(propAccess, convertToEnum);
        return LExpression.Lambda<Action<object, int>>(assign, objParam, valueParam).Compile();
    }
    
    
    #endregion

    #region Chain Reference Tracking
    
    /// <summary>
    /// Tracks IId objects across chain deserializations to maintain reference identity.
    /// Used internally by IBinaryDeserializeChain.ThenPopulate to ensure same object references.
    /// </summary>
    public sealed class ChainReferenceTracker
    {
        private readonly Dictionary<(Type, object), object> _idToObject = new();
        
        /// <summary>
        /// Registers an IId object for later retrieval.
        /// </summary>
        public bool TryRegisterIIdObject(object obj)
        {
            if (obj == null) return false;
            
            var type = obj.GetType();
            var idProp = type.GetProperty("Id");
            if (idProp == null) return false;
            
            var id = idProp.GetValue(obj);
            if (id == null) return false;
            
            // Create a normalized key
            var key = (type, NormalizeId(id));
            _idToObject[key] = obj;
            return true;
        }
        
        /// <summary>
        /// Tries to get a previously registered object by type and ID.
        /// </summary>
        public bool TryGetObject(object id, out object? obj)
        {
            obj = null;
            if (id == null) return false;
            
            var normalizedId = NormalizeId(id);
            
            // Search by normalized ID (ignoring type for simplicity in lookup)
            foreach (var kvp in _idToObject)
            {
                if (Equals(kvp.Key.Item2, normalizedId))
                {
                    obj = kvp.Value;
                    return true;
                }
            }
            
            return false;
        }
        
        /// <summary>
        /// Tries to get a previously registered object by exact type and ID.
        /// </summary>
        public bool TryGetObject(Type type, object id, out object? obj)
        {
            var key = (type, NormalizeId(id));
            return _idToObject.TryGetValue(key, out obj);
        }
        
        /// <summary>
        /// Clears all tracked references.
        /// </summary>
        public void Clear() => _idToObject.Clear();
        
        /// <summary>
        /// Normalizes the ID value for consistent dictionary lookups.
        /// Handles boxed value type comparisons.
        /// </summary>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private static object NormalizeId(object id)
        {
            // Convert common ID types to a consistent representation
            return id switch
            {
                int i => i,
                long l => l,
                Guid g => g,
                string s => s,
                _ => id
            };
        }
    }
    
    #endregion


    #region Serialization Reference Tracking
    
    /// <summary>
    /// Common reference tracking for serialization.
    /// Uses HashSet for tracking seen objects.
    /// - IId objects: hash = Id (positive, DB guarantees uniqueness per entity type)
    /// - Non-IId objects: hash = RuntimeHelpers.GetHashCode | 0x80000000 (negative)
    /// </summary>
    public sealed class SerializationReferenceTracker
    {
        private const int InitialCapacity = 128;
        
        // Unified HashSet for seen hashes (both IId and Reference)
        private HashSet<int>? _seenHashes;
        
        // Multi-referenced hashes
        private HashSet<int>? _multiRefHashes;
        
        // Written refs: hash → refId
        private Dictionary<int, int>? _writtenRefs;
        
        private int _nextRefId = 1;

        /// <summary>
        /// Resets the tracker for reuse.
        /// </summary>
        public void Reset()
        {
            _nextRefId = 1;
            _seenHashes?.Clear();
            _multiRefHashes?.Clear();
            _writtenRefs?.Clear();
        }

        /// <summary>
        /// Ensures internal collections are initialized.
        /// </summary>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public void EnsureInitialized()
        {
            _seenHashes ??= new HashSet<int>(InitialCapacity);
            _multiRefHashes ??= new HashSet<int>();
            _writtenRefs ??= new Dictionary<int, int>(InitialCapacity);
        }

        /// <summary>
        /// Computes the tracking hash for an object.
        /// IId objects: positive hash from Id
        /// Non-IId objects: negative hash from RuntimeHelpers.GetHashCode
        /// </summary>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static int ComputeHash<TMetadata>(object obj, TMetadata metadata) where TMetadata : TypeMetadataBase
        {
            if (metadata.IsIId)
            {
                return metadata.IdAccessorType switch
                {
                    IdAccessorType.Int32 => metadata.GetIdInt32(obj),
                    IdAccessorType.Int64 => (int)(metadata.GetIdInt64(obj) ^ (metadata.GetIdInt64(obj) >> 32)),
                    IdAccessorType.Guid => metadata.GetIdGuid(obj).GetHashCode() & 0x7FFFFFFF,
                    _ => metadata.IdGetter?.Invoke(obj)?.GetHashCode() ?? 0
                };
            }
            
            // Non-IId: use RuntimeHelpers identity hash with high bit set (negative)
            return RuntimeHelpers.GetHashCode(obj) | unchecked((int)0x80000000);
        }

        /// <summary>
        /// Tracks an object during reference scanning phase (non-IId version).
        /// </summary>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public bool TrackForScanning(object obj)
        {
            var hash = RuntimeHelpers.GetHashCode(obj) | unchecked((int)0x80000000);
            return TrackHash(hash);
        }
        
        /// <summary>
        /// Tracks an object during reference scanning phase with IId support.
        /// Returns true if first occurrence (continue scanning).
        /// Returns false if already seen (multi-referenced, stop scanning).
        /// </summary>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public bool TrackForScanningWithIId<TMetadata>(object obj, TMetadata metadata, out int existingRefId) 
            where TMetadata : TypeMetadataBase
        {
            existingRefId = 0;
            var hash = ComputeHash(obj, metadata);
            
            // Skip default IId values (Id = 0)
            if (metadata.IsIId && hash == 0) return true;
            
            return TrackHash(hash);
        }
        
        /// <summary>
        /// Core tracking logic using HashSet.Add() which returns false if already exists.
        /// </summary>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private bool TrackHash(int hash)
        {
            _seenHashes ??= new HashSet<int>(InitialCapacity);
            
            // HashSet.Add returns false if element already exists
            if (!_seenHashes.Add(hash))
            {
                // Already seen - multi-referenced!
                _multiRefHashes ??= new HashSet<int>();
                _multiRefHashes.Add(hash);
                return false;
            }
            
            return true;
        }

        /// <summary>
        /// Checks if object needs a reference ID during serialization.
        /// </summary>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public bool ShouldWriteId(object obj, out int refId)
        {
            var hash = RuntimeHelpers.GetHashCode(obj) | unchecked((int)0x80000000);
            return ShouldWriteIdForHash(hash, out refId);
        }
        
        /// <summary>
        /// Checks if object needs a reference ID (IId-aware version).
        /// </summary>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public bool ShouldWriteIdForIId<TMetadata>(object obj, TMetadata metadata, out int refId)
            where TMetadata : TypeMetadataBase
        {
            var hash = ComputeHash(obj, metadata);
            return ShouldWriteIdForHash(hash, out refId);
        }
        
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private bool ShouldWriteIdForHash(int hash, out int refId)
        {
            if (_multiRefHashes != null && _multiRefHashes.Contains(hash))
            {
                _writtenRefs ??= new Dictionary<int, int>(InitialCapacity);
                if (!_writtenRefs.ContainsKey(hash))
                {
                    refId = _nextRefId++;
                    return true;
                }
            }
            refId = 0;
            return false;
        }

        /// <summary>
        /// Marks object as written with its reference ID.
        /// </summary>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public void MarkAsWritten(object obj, int refId)
        {
            var hash = RuntimeHelpers.GetHashCode(obj) | unchecked((int)0x80000000);
            _writtenRefs![hash] = refId;
        }
        
        /// <summary>
        /// Marks object as written (IId-aware version).
        /// </summary>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public void MarkAsWrittenForIId<TMetadata>(object obj, TMetadata metadata, int refId)
            where TMetadata : TypeMetadataBase
        {
            var hash = ComputeHash(obj, metadata);
            _writtenRefs ??= new Dictionary<int, int>(InitialCapacity);
            _writtenRefs[hash] = refId;
        }

        /// <summary>
        /// Tries to get existing reference ID for an object.
        /// </summary>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public bool TryGetExistingRef(object obj, out int refId)
        {
            var hash = RuntimeHelpers.GetHashCode(obj) | unchecked((int)0x80000000);
            return TryGetExistingRefForHash(hash, out refId);
        }
        
        /// <summary>
        /// Tries to get existing reference ID (IId-aware version).
        /// </summary>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public bool TryGetExistingRefForIId<TMetadata>(object obj, TMetadata metadata, out int refId)
            where TMetadata : TypeMetadataBase
        {
            var hash = ComputeHash(obj, metadata);
            return TryGetExistingRefForHash(hash, out refId);
        }
        
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private bool TryGetExistingRefForHash(int hash, out int refId)
        {
            if (_writtenRefs != null && _writtenRefs.TryGetValue(hash, out refId))
            {
                return true;
            }
            refId = 0;
            return false;
        }
    }

    #endregion

    #region Property Mapping for Cross-Type Operations

    /// <summary>
    /// Maps property indices or names from source type to destination type.
    /// Supports both name-based matching and custom PropertyMapper callbacks.
    /// Used by cross-type deserialization (Deserialize&lt;TSource, TDest&gt;).
    /// Thread-safe and cached for performance.
    /// </summary>
    //public sealed class PropertyMappingCache
    //{
    //    private readonly ConcurrentDictionary<(Type Source, Type Dest), PropertyMappingInfo> _cache = new();

    //    /// <summary>
    //    /// Gets or builds property mapping between two types.
    //    /// Result is cached for subsequent calls.
    //    /// </summary>
    //    public PropertyMappingInfo GetOrBuild(Type sourceType, Type destType, PropertyMapperDelegate? customMapper, Func<Type, object> getMetadata)
    //    {
    //        var key = (sourceType, destType);
    //        return _cache.GetOrAdd(key, _ => BuildMapping(sourceType, destType, customMapper, getMetadata));
    //    }

    //    private static PropertyMappingInfo BuildMapping(Type sourceType, Type destType, PropertyMapperDelegate? customMapper, Func<Type, object> getMetadata)
    //    {
    //        var sourceProps = sourceType.GetProperties(BindingFlags.Public | BindingFlags.Instance)
    //            .Where(p => p.CanRead)
    //            .ToArray();

    //        var destProps = destType.GetProperties(BindingFlags.Public | BindingFlags.Instance)
    //            .Where(p => p.CanWrite)
    //            .ToDictionary(p => p.Name, StringComparer.Ordinal);

    //        var mappings = new List<(PropertyInfo Source, PropertyInfo Dest)>();

    //        foreach (var sourceProp in sourceProps)
    //        {
    //            PropertyInfo? destProp = null;

    //            // Use custom mapper if provided
    //            if (customMapper != null)
    //            {
    //                destProp = customMapper(sourceProp, destType);
    //            }
    //            else
    //            {
    //                // Default: match by name
    //                destProps.TryGetValue(sourceProp.Name, out destProp);
    //            }

    //            if (destProp != null && AreTypesCompatible(sourceProp.PropertyType, destProp.PropertyType))
    //            {
    //                mappings.Add((sourceProp, destProp));
    //            }
    //        }

    //        return new PropertyMappingInfo(mappings.ToArray());
    //    }

    //    /// <summary>
    //    /// Checks if two property types are compatible for mapping.
    //    /// Handles exact match, inheritance, nullable unwrapping, and numeric conversions.
    //    /// </summary>
    //    private static bool AreTypesCompatible(Type sourceType, Type destType)
    //    {
    //        // Exact match
    //        if (sourceType == destType) return true;

    //        // Assignable (inheritance, interfaces)
    //        if (destType.IsAssignableFrom(sourceType)) return true;

    //        // Unwrap nullable types
    //        var sourceUnderlying = Nullable.GetUnderlyingType(sourceType) ?? sourceType;
    //        var destUnderlying = Nullable.GetUnderlyingType(destType) ?? destType;

    //        if (sourceUnderlying == destUnderlying) return true;

    //        // Numeric conversions (int -> long, float -> double, etc.)
    //        if (IsNumericType(sourceUnderlying) && IsNumericType(destUnderlying))
    //            return true;

    //        return false;
    //    }

    //    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    //    private static bool IsNumericType(Type type)
    //    {
    //        return type == typeof(byte) || type == typeof(sbyte) ||
    //               type == typeof(short) || type == typeof(ushort) ||
    //               type == typeof(int) || type == typeof(uint) ||
    //               type == typeof(long) || type == typeof(ulong) ||
    //               type == typeof(float) || type == typeof(double) ||
    //               type == typeof(decimal);
    //    }
    //}

    /// <summary>
    /// Contains property mapping information for a source->destination type pair.
    /// Immutable and thread-safe.
    /// </summary>
    //public sealed record PropertyMappingInfo(IReadOnlyList<(PropertyInfo Source, PropertyInfo Dest)> Mappings)
    //{
    //    /// <summary>
    //    /// List of source->destination property pairs.
    //    /// </summary>
    //    public IReadOnlyList<(PropertyInfo Source, PropertyInfo Dest)> Mappings { get; } = Mappings;
    //}

    /// <summary>
    /// Binary-specific index-to-index mapping for cross-type deserialization.
    /// Maps source PropertyIndex to destination PropertyIndex.
    /// Thread-safe and cached.
    /// </summary>
    public sealed class BinaryIndexMappingCache
    {
        private readonly ConcurrentDictionary<(Type Source, Type Dest), int[]> _cache = new();

        /// <summary>
        /// Gets or builds index mapping from source type to destination type.
        /// Returns array where index is source PropertyIndex, value is destination PropertyIndex (-1 if no mapping).
        /// </summary>
        public int[] GetOrBuild(Type sourceType, Type destType, PropertyMapperDelegate? customMapper, Func<Type, PropertyInfo[]> getOrderedProperties)
        {
            var key = (sourceType, destType);
            return _cache.GetOrAdd(key, _ => BuildIndexMapping(sourceType, destType, customMapper, getOrderedProperties));
        }

        private static int[] BuildIndexMapping(Type sourceType, Type destType, PropertyMapperDelegate? customMapper, Func<Type, PropertyInfo[]> getOrderedProperties)
        {
            var sourceProps = getOrderedProperties(sourceType);
            var destProps = getOrderedProperties(destType);

            // Build lookup: property name -> destination index
            var destIndexByName = new Dictionary<string, int>(destProps.Length, StringComparer.Ordinal);
            for (int i = 0; i < destProps.Length; i++)
            {
                destIndexByName[destProps[i].Name] = i;
            }

            // Map source index -> destination index
            var mapping = new int[sourceProps.Length];

            for (int srcIdx = 0; srcIdx < sourceProps.Length; srcIdx++)
            {
                var sourceProp = sourceProps[srcIdx];
                PropertyInfo? destProp = null;

                // Use custom mapper if provided
                if (customMapper != null)
                {
                    destProp = customMapper(sourceProp, destType);
                }
                else
                {
                    // Default: match by name
                    if (destIndexByName.TryGetValue(sourceProp.Name, out var destIdx))
                    {
                        destProp = destProps[destIdx];
                    }
                }

                if (destProp != null && destIndexByName.TryGetValue(destProp.Name, out var mappedIdx))
                {
                    mapping[srcIdx] = mappedIdx;
                }
                else
                {
                    mapping[srcIdx] = -1; // Skip this property
                }
            }

            return mapping;
        }
    }

    #endregion

    #region Deserialization Reference Tracking
    
    /// <summary>
    /// Common reference tracking for deserialization.
    /// Used by both JSON and Binary deserializers to resolve $id/$ref references.
    /// Uses int IDs for efficiency.
    /// </summary>
    public sealed class DeserializationReferenceTracker
    {
        private Dictionary<int, object>? _idToObject;

        /// <summary>
        /// Resets the tracker for reuse.
        /// </summary>
        public void Reset()
        {
            _idToObject?.Clear();
        }

        /// <summary>
        /// Registers an object with its reference ID.
        /// </summary>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public void RegisterObject(int id, object obj)
        {
            _idToObject ??= new Dictionary<int, object>(64);
            _idToObject[id] = obj;
        }

        /// <summary>
        /// Tries to get a previously registered object by reference ID.
        /// </summary>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public bool TryGetReferencedObject(int id, out object? obj)
        {
            if (_idToObject != null && _idToObject.TryGetValue(id, out obj!))
            {
                return true;
            }
            obj = null;
            return false;
        }
    }

    #endregion

    #region Type Name Formatting

    /// <summary>
    /// Gets the C# display name for a type (e.g., "int", "List&lt;string&gt;", "Dictionary&lt;int, string&gt;").
    /// Handles primitives, nullables, generics, enums, collections, and dictionaries.
    /// Thread-safe with circular reference protection.
    /// </summary>
    /// <param name="type">The type to get the name for.</param>
    /// <param name="useShortNames">If true, uses C# keywords (int, string). If false, uses full names (Int32, String).</param>
    /// <returns>C# display name of the type.</returns>
    public static string GetCSharpTypeName(Type type, bool useShortNames = true)
    {
        // Handle nullable types
        var underlying = Nullable.GetUnderlyingType(type);
        if (underlying != null)
        {
            return GetCSharpTypeName(underlying, useShortNames) + "?";
        }

        // Handle arrays
        if (type.IsArray)
        {
            var elementType = type.GetElementType()!;
            return GetCSharpTypeName(elementType, useShortNames) + "[]";
        }

        // IMPORTANT: Check for enum BEFORE primitive type check!
        // Enum TypeCode returns the underlying type's code (e.g., Int32),
        // which would incorrectly return "int" instead of the enum name.
        if (type.IsEnum)
        {
            return type.Name;
        }

        // Handle primitive types with C# keywords
        if (useShortNames)
        {
            var typeCode = Type.GetTypeCode(type);
            var primitiveName = typeCode switch
            {
                TypeCode.Boolean => "bool",
                TypeCode.Byte => "byte",
                TypeCode.SByte => "sbyte",
                TypeCode.Char => "char",
                TypeCode.Int16 => "short",
                TypeCode.UInt16 => "ushort",
                TypeCode.Int32 => "int",
                TypeCode.UInt32 => "uint",
                TypeCode.Int64 => "long",
                TypeCode.UInt64 => "ulong",
                TypeCode.Single => "float",
                TypeCode.Double => "double",
                TypeCode.Decimal => "decimal",
                TypeCode.String => "string",
                _ => null
            };
            if (primitiveName != null) return primitiveName;
        }

        // Handle common BCL types
        if (type == typeof(object)) return "object";
        if (type == typeof(void)) return "void";

        // Handle generic types
        if (type.IsGenericType)
        {
            var genericDef = type.GetGenericTypeDefinition();
            var args = type.GetGenericArguments();

            // Special handling for common generic types
            if (genericDef == typeof(List<>))
                return $"List<{GetCSharpTypeName(args[0], useShortNames)}>";
            if (genericDef == typeof(Dictionary<,>))
                return $"Dictionary<{GetCSharpTypeName(args[0], useShortNames)}, {GetCSharpTypeName(args[1], useShortNames)}>";
            if (genericDef == typeof(IEnumerable<>))
                return $"IEnumerable<{GetCSharpTypeName(args[0], useShortNames)}>";
            if (genericDef == typeof(ICollection<>))
                return $"ICollection<{GetCSharpTypeName(args[0], useShortNames)}>";
            if (genericDef == typeof(IList<>))
                return $"IList<{GetCSharpTypeName(args[0], useShortNames)}>";

            // Generic type with name mangling (e.g., SomeType`2)
            var baseName = type.Name;
            var backtickIndex = baseName.IndexOf('`');
            if (backtickIndex > 0)
            {
                baseName = baseName.Substring(0, backtickIndex);
            }
            var argNames = string.Join(", ", args.Select(a => GetCSharpTypeName(a, useShortNames)));
            return $"{baseName}<{argNames}>";
        }

        // Simple type name (class, struct, enum, etc.)
        return type.Name;
    }

    #endregion
}