using AyCode.Core.Helpers;
using AyCode.Core.Interfaces;
using AyCode.Core.Serializers.Attributes;
using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using static AyCode.Core.Helpers.JsonUtilities;

namespace AyCode.Core.Serializers;

/// <summary>
/// Base class for type metadata used by all serializers.
/// Contains common functionality for type analysis, constructor compilation, and IId detection.
/// </summary>
public abstract class TypeMetadataBase
{
    /// <summary>
    /// Global shared cache for all metadata types.
    /// Key: (Type sourceType, Type metadataType) - ensures each type can have multiple metadata representations
    /// Value: TypeMetadataBase instance (BinarySerializeTypeMetadata, JsonTypeMetadata, etc.)
    /// This single cache is shared across Binary/JSON Serializers/Deserializers.
    /// </summary>
    //protected static readonly ConcurrentDictionary<(Type, Type), TypeMetadataBase> GlobalMetadataCache = new();

    /// <summary>
    /// Maximum ThreadLocal cache size before clearing.
    /// Set to 256 because if a system uses 256+ different types, they're likely all frequently accessed.
    /// Minimal memory overhead (256 dictionary entries ≈ 8KB) vs significant lookup performance benefit.
    /// Clear() reuses internal array - no new allocation, just resets count.
    /// </summary>
    internal const int MaxLocalCacheSize = 1024;

    /// <summary>
    /// Cache for serializable properties per type.
    /// Key: (Type, requiresWrite) - since requiresRead is always true in practice.
    /// Value: PropertyInfo[] ordered alphabetically by name for deterministic serialization.
    /// </summary>
    private static readonly ConcurrentDictionary<(Type, bool), List<PropertyInfo>> UnfilteredPropertiesGlobalCache = new();

    private readonly Func<PropertyInfo, bool> _ignorePropertyFilter;
    
    /// <summary>
    /// The type this metadata is for. Stored to avoid repeated reflection.
    /// </summary>
    public Type SourceType { get; }
    
    /// <summary>
    /// Readable properties (CanRead=true) for this type. Used by serializers.
    /// Pre-computed in constructor, no dictionary lookup needed.
    /// </summary>
    protected PropertyInfo[] ReadableProperties { get; }
    
    /// <summary>
    /// Writable properties (CanRead=true AND CanWrite=true) for this type. Used by deserializers.
    /// Pre-computed in constructor, no dictionary lookup needed.
    /// </summary>
    protected PropertyInfo[] WritableProperties { get; }
    
    /// <summary>
    /// Compiled parameterless constructor for the type.
    /// Null if the type is abstract or has no parameterless constructor.
    /// </summary>
    public Func<object>? CompiledConstructor { get; }

    /// <summary>
    /// Whether this type implements IId interface.
    /// Cached at metadata creation time to avoid runtime reflection.
    /// Used by both serializers (for IId-based reference deduplication) and deserializers.
    /// </summary>
    public bool IsIId { get; }

    /// <summary>
    /// The Id property type if IsIId is true, null otherwise.
    /// </summary>
    public Type? IdType { get; }

    /// <summary>
    /// Compiled getter for the Id property (if IsIId is true).
    /// Pre-compiled delegate avoids reflection overhead during serialization/deserialization.
    /// Note: Use typed getters (GetIdInt32, GetIdInt64, GetIdGuid) for zero-boxing access.
    /// </summary>
    public Func<object, object?>? IdGetter { get; }
    
    /// <summary>
    /// The accessor type for IId.Id property.
    /// Used for fast typed getter dispatch without boxing.
    /// </summary>
    public IdAccessorType IdAccessorType { get; }
    
    /// <summary>
    /// The Id property info if IsIId is true, null otherwise.
    /// Used by TypeMetadataWrapper to create typed RefIdGetter.
    /// </summary>
    public PropertyInfo? IdPropertyInfo { get; }
    
    /// <summary>
    /// Public accessor for the type. Used by wrapper for typed getter creation.
    /// </summary>
    public Type MetadataType => SourceType;
    
    /// <summary>
    /// Typed getter delegate for IId.Id property.
    /// Type depends on IdAccessorType (Func&lt;object, int&gt;, Func&lt;object, long&gt;, or Func&lt;object, Guid&gt;).
    /// Cached here to avoid creating new delegates per wrapper.
    /// </summary>
    public Delegate? TypedIdGetter { get; }
    
    #region Scan Optimization Flags
    
    /// <summary>
    /// True if this type has any complex (non-primitive) properties that need recursive scanning.
    /// If false, ScanReferences can skip child property scanning entirely.
    /// </summary>
    public bool HasComplexProperties { get; protected set; }

    /// <summary>
    /// Number of complex (non-primitive, non-string) properties.
    /// Used to allocate PropertyTypeWrappers array on TypeMetadataWrapper.
    /// </summary>
    public int ComplexPropertyCount { get; protected set; }

    public bool IsComplexType { get; init; }

    /// <summary>
    /// True if this type or any of its properties could potentially be shared (multi-referenced).
    /// False for sealed value-like types with only primitives - these never need reference tracking.
    /// </summary>
    public bool NeedsReferenceTracking { get; protected set; }

    /// <summary>
    /// When false, this type skips inline metadata and uses markerless property write/read
    /// — even when global UseMetadata=true. Read from [AcBinarySerializable] attribute.
    /// </summary>
    public bool EnableMetadataFeature { get; }

    /// <summary>
    /// When false, this type's runtime write loop skips the per-property
    /// <c>PropertyFilter</c> callback check entirely — runtime-set filters on
    /// <see cref="AyCode.Core.Serializers.Binaries.AcBinarySerializerOptions.PropertyFilter"/>
    /// are silently ignored for this type.
    /// <para>Read from [AcBinarySerializable(EnablePropertyFilterFeature = ...)]; defaults to true.
    /// Mirrors the SGen-side gate in <c>AcBinarySourceGenerator.EmitProp</c>: when false, the SGen
    /// emit omits the filter-check block entirely. Runtime-side this flag combines with
    /// <c>context.HasPropertyFilter</c> via short-circuit AND to drop the per-property branch
    /// for filter-disabled types.</para>
    /// </summary>
    public bool EnablePropertyFilterFeature { get; }

    /// <summary>
    /// True if this type is a primitive, string, enum, Guid, DateTime, etc.
    /// Pre-computed to avoid IsPrimitiveOrStringFast() calls in hot path.
    /// </summary>
    public bool IsPrimitiveType { get; }
    
    /// <summary>
    /// True if this type implements IEnumerable (excluding string and Dictionary).
    /// Pre-computed to replace IsCollectionCache and IsGenericCollectionType() lookups.
    /// </summary>
    public bool IsCollection { get; }
    
    /// <summary>
    /// The element type if IsCollection is true, null otherwise.
    /// Pre-computed to replace CollectionElementCache lookups.
    /// </summary>
    public Type? CollectionElementType { get; }
    
    /// <summary>
    /// True if collection elements need scanning (are complex types or strings).
    /// Pre-computed: !IsPrimitiveOrStringFast(elementType) || elementType == typeof(string).
    /// Only meaningful when IsCollection is true.
    /// </summary>
    public bool ElementNeedsScan { get; }
    
    /// <summary>
    /// True if collection elements are all primitive types (no scanning needed at all).
    /// Pre-computed to replace IsPrimitiveCollectionCache.
    /// </summary>
    public bool IsPrimitiveElementCollection { get; }
    
    #endregion
    
    /// <summary>
    /// Gets the Id as int without boxing. Only valid when IdAccessorType == Int32.
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public int GetIdInt32(object obj) => ((Func<object, int>)TypedIdGetter!)(obj);
    
    /// <summary>
    /// Gets the Id as long without boxing. Only valid when IdAccessorType == Int64.
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public long GetIdInt64(object obj) => ((Func<object, long>)TypedIdGetter!)(obj);
    
    /// <summary>
    /// Gets the Id as Guid without boxing. Only valid when IdAccessorType == Guid.
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public Guid GetIdGuid(object obj) => ((Func<object, Guid>)TypedIdGetter!)(obj);

    /// <summary>
    /// Combined DAMs requirement for any Type that flows into <see cref="TypeMetadataBase"/> construction.
    /// <see cref="DynamicallyAccessedMemberTypes.PublicParameterlessConstructor"/> — required by the
    /// <c>CreateCompiledConstructor</c> path (deserialize side) so <c>Type.GetConstructor</c> /
    /// <c>Activator.CreateInstance</c> survive AOT trimming.
    /// <see cref="DynamicallyAccessedMemberTypes.PublicProperties"/> — required by
    /// <c>GetUnfilteredProperties</c> and the typed/dynamic property-accessor factories so
    /// <c>Type.GetProperties</c> / <c>PropertyInfo.GetValue</c> / <c>SetValue</c> survive AOT trimming.
    /// Public so derived metadata classes (BinarySerializeTypeMetadata, JsonSerializeTypeMetadata, etc.)
    /// can reference the same constant in their own DAMs annotations and stay in sync.
    /// </summary>
    public const DynamicallyAccessedMemberTypes RequiredMembers =
        DynamicallyAccessedMemberTypes.PublicParameterlessConstructor |
        DynamicallyAccessedMemberTypes.PublicProperties;

    protected TypeMetadataBase(
        [DynamicallyAccessedMembers(RequiredMembers)] Type type,
        Func<PropertyInfo, bool> ignorePropertyFilter)
    {
        SourceType = type;
        _ignorePropertyFilter = ignorePropertyFilter;

        CompiledConstructor = AcSerializerCommon.CreateCompiledConstructor(type);

        // Pre-compute property arrays - no dictionary lookup needed later!
        // Uses static global cache for unfiltered properties, then applies filter once
        var allReadable = GetUnfilteredProperties(type, requiresWrite: false)
            .Where(p => !ignorePropertyFilter(p))
            .ToArray();

        ReadableProperties = allReadable;
        WritableProperties = allReadable.Where(p => p.CanWrite).ToArray();

        IsComplexType = IsComplexType2(type);
        IsPrimitiveType = !IsComplexType;
        
        // Pre-compute collection info — replaces CollectionElementCache / IsCollectionCache lookups
        if (!IsPrimitiveType && !ReferenceEquals(type, StringType) && typeof(IEnumerable).IsAssignableFrom(type))
        {
            CollectionElementType = GetCollectionElementType(type);
            IsCollection = CollectionElementType != null;
            if (IsCollection)
            {
                var elemIsPrimitive = IsPrimitiveOrStringFast(CollectionElementType!);
                ElementNeedsScan = !elemIsPrimitive || ReferenceEquals(CollectionElementType, StringType);
                IsPrimitiveElementCollection = elemIsPrimitive || CollectionElementType!.IsEnum;
            }
        }

        var serializableAttr = type.GetCustomAttribute<AcBinarySerializableAttribute>(inherit: false);
        EnableMetadataFeature = serializableAttr == null || serializableAttr.EnableMetadataFeature;
        EnablePropertyFilterFeature = serializableAttr == null || serializableAttr.EnablePropertyFilterFeature;
        if (serializableAttr is { EnableIdTrackingFeature: false })
        {
            IsIId = false;
        }
        else
        {
            // Cache IId info at construction time - no runtime reflection needed later!
            var idInfo = GetIdInfo(type);
            IsIId = idInfo.IsId;
            IdType = idInfo.IdType;
        }

        if (IsIId)
        {
            var idProp = type.GetProperty("Id");
            IdPropertyInfo = idProp; // Store for TypeMetadataWrapper

            // Create typed getter for the three common Id types to avoid boxing
            if (ReferenceEquals(IdType, IntType))
            {
                IdAccessorType = IdAccessorType.Int32;
                TypedIdGetter = AcSerializerCommon.CreateTypedGetter<int>(type, idProp);
            }
            else if (ReferenceEquals(IdType, LongType))
            {
                IdAccessorType = IdAccessorType.Int64;
                TypedIdGetter = AcSerializerCommon.CreateTypedGetter<long>(type, idProp);
            }
            else if (ReferenceEquals(IdType, GuidType))
            {
                IdAccessorType = IdAccessorType.Guid;
                TypedIdGetter = AcSerializerCommon.CreateTypedGetter<Guid>(type, idProp);
            }
            else
            {
                throw new NotSupportedException($"Unsupported IId type: {IdType.Name}. Only int, long, and Guid are supported.");
            }
        }
        else
        {
            // Non-IId types: use RuntimeHelpers.GetHashCode (int)
            // RefIdGetter is created in TypeMetadataWrapper.CreateRefIdGetter()
            IdAccessorType = IdAccessorType.Int32;
            // TypedIdGetter remains null - wrapper uses GetHashCode directly
        }
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public bool UseReferenceHandling(ReferenceHandlingMode referenceHandling)
    {
        return referenceHandling != ReferenceHandlingMode.None && (IsIId || referenceHandling == ReferenceHandlingMode.All);
    }

    /// <summary>
    /// Gets serializable properties with stable property index ordering.
    /// Properties are ordered hierarchy-aware (base class first, then derived)
    /// and alphabetically within each level.
    /// This ensures PropertyIndex remains stable across inheritance hierarchies:
    /// - Base properties always get the same index in both base and derived types
    /// - Supports safe deserialization of derived types into base types
    /// - Enables cross-version compatibility when new derived properties are added
    /// 
    /// PERFORMANCE: Returns pre-computed array, no dictionary lookup!
    /// </summary>
    /// <param name="type">The type to analyze (must match SourceType).</param>
    /// <param name="requiresWrite">Whether the property must be writable (true for deserialization).</param>
    /// <returns>Array of properties with stable indices.</returns>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public PropertyInfo[] GetSerializableProperties(Type type, bool requiresWrite = false)
    {
        // Direct array access - no dictionary lookup!
        return requiresWrite ? WritableProperties : ReadableProperties;
    }

    // Id properties are sorted alphabetically like all other properties
    public const string IdPropertyName = nameof(IId<int>.Id);

    private static List<PropertyInfo> GetUnfilteredProperties(
        [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] Type type,
        bool requiresWrite)
    {
        // Cache lookup keyed by (Type, bool). The cache hit path returns without DAMs context — that's
        // fine because the cached PropertyInfo[] was built when DAMs was honored. Cache miss path
        // dispatches to BuildPropertyList which carries the DAMs annotation through the call chain.
        if (UnfilteredPropertiesGlobalCache.TryGetValue((type, requiresWrite), out var cached))
            return cached;

        var built = BuildPropertyList(type, requiresWrite);
        UnfilteredPropertiesGlobalCache.TryAdd((type, requiresWrite), built);
        return built;
    }

    /// <summary>
    /// Walks the inheritance chain to collect declared public instance properties.
    /// </summary>
    /// <remarks>
    /// The <c>BaseType</c> walk is a documented trimmer blind spot — DAMs annotations cannot follow
    /// <see cref="Type.BaseType"/>. The IL2070/IL2080 suppression below is bounded: when consumers
    /// preserve their root type T via DAMs, the runtime trimmer keeps T's full inheritance chain
    /// (a base type of a kept type is itself kept), so <c>GetProperties</c> on each level finds
    /// preserved metadata. The blind spot is in static analysis only; runtime correctness is sound
    /// for any type rooted via <c>Deserialize&lt;T&gt;</c>'s DAMs requirement.
    /// </remarks>
    [UnconditionalSuppressMessage("Trimming", "IL2070",
        Justification = "BaseType walk: trimmer cannot follow DAMs through Type.BaseType. Consumer-side "
                      + "DAMs on Deserialize<T>'s T preserves the full inheritance chain at runtime.")]
    [UnconditionalSuppressMessage("Trimming", "IL2080",
        Justification = "Same as IL2070 — currentType variable starts DAMs-annotated but loses annotation "
                      + "through the BaseType reassignment in the loop.")]
    private static List<PropertyInfo> BuildPropertyList([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] Type type, bool requiresWrite)
    {
        // Collect properties from inheritance hierarchy (derived -> base order)
        // Sorted alphabetically for deterministic property index ordering
        var allProperties = new List<PropertyInfo>();

        // Dedup by name across inheritance levels. An overridden (or 'new'-shadowed) property is
        // declared at EVERY level it appears in, so BindingFlags.DeclaredOnly returns it once per
        // level — without this guard the derived AND base PropertyInfo would both be collected
        // (duplicate wire field on the runtime path; duplicate @types entry in Toon). The derived->base
        // walk means the most-derived declaration is seen first and wins. Mirrors the SGen path
        // (AcBinarySourceGenerator.GetAllSerializablePropertySymbols) so the runtime and generated
        // property orders stay bit-identical.
        var seen = new HashSet<string>(StringComparer.Ordinal);

        for (var currentType = type; currentType != null && currentType != typeof(object); currentType = currentType.BaseType)
        {
            // Get properties declared at this level only
            var levelProperties = currentType
                .GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly)
                .Where(p => p.CanRead &&
                            (!requiresWrite || p.CanWrite) &&
                            p.GetIndexParameters().Length == 0 &&
                            !IsUnsupportedPropertyType(p.PropertyType) &&
                            seen.Add(p.Name))
                .OrderBy(static p => p.Name, StringComparer.Ordinal);

            allProperties.AddRange(levelProperties);
        }

        return allProperties;
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private static bool IsComplexType2(Type type)
    {
        if (type.IsPrimitive) return false;
        if (ReferenceEquals(type, StringType)) return false;
        if (type.IsEnum) return false;
        if (ReferenceEquals(type, GuidType)) return false;
        if (ReferenceEquals(type, DateTimeType)) return false;
        if (ReferenceEquals(type, DecimalType)) return false;
        if (ReferenceEquals(type, TimeSpanType)) return false;
        if (ReferenceEquals(type, DateTimeOffsetType)) return false;
        if (Nullable.GetUnderlyingType(type) != null) return false;
        return true;
    }
}
