using System.Collections;
using System.Collections.Concurrent;
using System.Reflection;
using System.Runtime.CompilerServices;
using AyCode.Core.Extensions;
using AyCode.Core.Interfaces;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Serialization;
using static AyCode.Core.Helpers.JsonUtilities;

namespace AyCode.Core.Serializers.Jsons;

[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public sealed class JsonNoMergeCollectionAttribute : Attribute { }

/// <summary>
/// Cached property metadata for faster JSON processing.
/// </summary>
public sealed class CachedPropertyInfo
{
    public PropertyInfo Property { get; }
    public string Name { get; }
    public Type PropertyType { get; }
    public bool IsIId { get; }
    public Type? IdType { get; }
    public bool IsIIdCollection { get; }
    public Type? CollectionElementType { get; }
    public Type? CollectionElementIdType { get; }
    public bool ShouldSkip { get; }

    public CachedPropertyInfo(PropertyInfo prop)
    {
        Property = prop;
        Name = prop.Name;
        PropertyType = prop.PropertyType;
        
        ShouldSkip = !prop.CanRead || 
                     prop.GetIndexParameters().Length > 0 || 
                     HasJsonIgnoreAttribute(prop);
        
        if (!ShouldSkip)
        {
            var idInfo = GetIdInfo(PropertyType);
            IsIId = idInfo.IsId;
            IdType = idInfo.IdType;
            
            if (!IsIId && typeof(IEnumerable).IsAssignableFrom(PropertyType) && !ReferenceEquals(PropertyType, StringType))
            {
                CollectionElementType = GetCollectionElementType(PropertyType);
                if (CollectionElementType != null)
                {
                    var elemIdInfo = GetIdInfo(CollectionElementType);
                    IsIIdCollection = elemIdInfo.IsId;
                    CollectionElementIdType = elemIdInfo.IdType;
                }
            }
        }
    }
}

/// <summary>
/// Static type metadata cache for semantic ID generation.
/// </summary>
public static class TypeCache
{
    private static readonly ConcurrentDictionary<Type, string> TypeNameCache = new();
    private static readonly ConcurrentDictionary<Type, CachedPropertyInfo[]> CachedPropertyInfoCache = new();
    private static readonly ConcurrentDictionary<Type, int> TypeIdCache = new();
    private static int _typeIdCounter;

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static int GetTypeId(Type t) => TypeIdCache.GetOrAdd(t, _ => Interlocked.Increment(ref _typeIdCounter));

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static long CreateSemanticId(int typeId, long objectId) => ((long)typeId << 48) | (objectId & 0xFFFFFFFFFFFF);

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static string GetTypeName(Type t) => TypeNameCache.GetOrAdd(t, static type => type.Name);

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static CachedPropertyInfo[] GetCachedProperties(Type t)
    {
        return CachedPropertyInfoCache.GetOrAdd(t, static type =>
        {
            var props = type.GetProperties(BindingFlags.Public | BindingFlags.Instance);
            var cached = new CachedPropertyInfo[props.Length];
            for (var i = 0; i < props.Length; i++)
                cached[i] = new CachedPropertyInfo(props[i]);
            return cached;
        });
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static long IdToLong(object idValue)
    {
        return idValue switch
        {
            int i => i,
            long l => l,
            Guid g => GuidToLong(g),
            short s => s,
            byte b => b,
            uint ui => ui,
            ulong ul => (long)ul,
            ushort us => us,
            sbyte sb => sb,
            _ => throw new NotSupportedException($"ID type '{idValue.GetType().Name}' is not supported for semantic ID generation.")
        };
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static long IdToLong<TId>(TId id) where TId : struct
    {
        if (typeof(TId) == typeof(int)) return Unsafe.As<TId, int>(ref id);
        if (typeof(TId) == typeof(long)) return Unsafe.As<TId, long>(ref id);
        if (typeof(TId) == typeof(Guid)) return GuidToLong(Unsafe.As<TId, Guid>(ref id));
        if (typeof(TId) == typeof(short)) return Unsafe.As<TId, short>(ref id);
        if (typeof(TId) == typeof(byte)) return Unsafe.As<TId, byte>(ref id);
        if (typeof(TId) == typeof(uint)) return Unsafe.As<TId, uint>(ref id);
        if (typeof(TId) == typeof(ulong)) return (long)Unsafe.As<TId, ulong>(ref id);
        if (typeof(TId) == typeof(ushort)) return Unsafe.As<TId, ushort>(ref id);
        if (typeof(TId) == typeof(sbyte)) return Unsafe.As<TId, sbyte>(ref id);
        return IdToLong((object)id);
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static long GuidToLong(Guid guid)
    {
        Span<byte> bytes = stackalloc byte[16];
        guid.TryWriteBytes(bytes);
        return BitConverter.ToInt64(bytes) ^ BitConverter.ToInt64(bytes.Slice(8));
    }
}

/// <summary>
/// Converter for IId collections that supports merging by ID.
/// </summary>
public class IdAwareCollectionMergeConverter<TItem, TId> : JsonConverter
    where TItem : class, IId<TId>, new() where TId : struct
{
    private static readonly int CachedTypeId = TypeCache.GetTypeId(typeof(TItem));
    private static readonly EqualityComparer<TId> IdComparer = EqualityComparer<TId>.Default;

    public override bool CanRead => true;
    public override bool CanWrite => false;
    public override bool CanConvert(Type objectType) =>
        typeof(ICollection<TItem>).IsAssignableFrom(objectType) || typeof(IEnumerable<TItem>).IsAssignableFrom(objectType);

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private static long GetSemanticId(TId id) => TypeCache.CreateSemanticId(CachedTypeId, TypeCache.IdToLong(id));

    public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
    {
        if (reader.TokenType == JsonToken.Null) return existingValue;

        if (existingValue is not IList targetList)
        {
            var jsonArrayFallback = JArray.Load(reader);
            return jsonArrayFallback.ToObject(objectType, serializer);
        }

        var isFixedSize = targetList.IsFixedSize;
        var jsonArray = JArray.Load(reader);
        var jsonCount = jsonArray.Count;
        var existingCount = targetList.Count;

        var existingItemsMap = new Dictionary<long, TItem>(existingCount);
        for (var index = 0; index < existingCount; index++)
        {
            if (targetList[index] is TItem item && !IdComparer.Equals(item.Id, default))
                existingItemsMap[GetSemanticId(item.Id)] = item;
        }

        var finalItems = new List<TItem>(jsonCount + existingCount);
        var processedIds = new HashSet<long>(jsonCount);

        for (var i = 0; i < jsonCount; i++)
        {
            var itemToken = jsonArray[i];
            TItem? itemResult = null;

            if (itemToken is JObject jObj)
            {
                var incomingId = IdExtractor.GetIdFromJToken<TId>(jObj);
                var hasId = !IdComparer.Equals(incomingId, default);
                var semanticId = hasId ? GetSemanticId(incomingId) : 0L;

                TItem? existingItem = hasId && existingItemsMap.TryGetValue(semanticId, out var found) ? found : null;

                if (existingItem != null)
                {
                    using var subReader = jObj.CreateReader();
                    serializer.Populate(subReader, existingItem);
                    itemResult = existingItem;
                }
                else
                    itemResult = jObj.ToObject<TItem>(serializer);
            }
            else
                itemResult = itemToken.ToObject<TItem>(serializer);

            if (itemResult == null) continue;

            var currentId = itemResult.Id;
            if (!IdComparer.Equals(currentId, default))
            {
                if (processedIds.Add(GetSemanticId(currentId)))
                    finalItems.Add(itemResult);
            }
            else
                finalItems.Add(itemResult);
        }

        foreach (var kvp in existingItemsMap)
        {
            if (processedIds.Add(kvp.Key))
                finalItems.Add(kvp.Value);
        }

        if (isFixedSize)
        {
            var resultArray = new TItem[finalItems.Count];
            finalItems.CopyTo(resultArray);
            return resultArray;
        }

        targetList.Clear();
        if (targetList is List<TItem> typedList)
        {
            typedList.EnsureCapacity(finalItems.Count);
            typedList.AddRange(finalItems);
        }
        else
        {
            for (var i = 0; i < finalItems.Count; i++)
                targetList.Add(finalItems[i]);
        }

        return targetList;
    }

    public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) =>
        throw new InvalidOperationException("IdAwareCollectionMergeConverter is read-only.");
}

/// <summary>
/// Static class with inlined ID extraction methods.
/// </summary>
public static class IdExtractor
{
    private static readonly string IdPropertyName = nameof(IId<int>.Id);

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static TId GetIdFromJToken<TId>(JObject obj) where TId : struct
    {
        var idPropToken = obj.GetValue(IdPropertyName, StringComparison.OrdinalIgnoreCase);
        if (idPropToken == null || idPropToken.Type == JTokenType.Null) return default;

        if (typeof(TId) == typeof(int))
        {
            if (idPropToken.Type == JTokenType.Integer)
                return Unsafe.As<int, TId>(ref Unsafe.AsRef((int)idPropToken));
            return (TId)(object)idPropToken.Value<int>();
        }
        
        if (typeof(TId) == typeof(long))
        {
            if (idPropToken.Type == JTokenType.Integer)
                return Unsafe.As<long, TId>(ref Unsafe.AsRef((long)idPropToken));
            return (TId)(object)idPropToken.Value<long>();
        }
        
        if (typeof(TId) == typeof(Guid))
        {
            var stringValue = idPropToken.Type == JTokenType.String 
                ? (string?)idPropToken 
                : idPropToken.Value<string>();
                
            if (string.IsNullOrEmpty(stringValue)) return default;
            
            if (Guid.TryParse(stringValue, out var guidValue))
                return Unsafe.As<Guid, TId>(ref guidValue);
            return default;
        }

        try { return idPropToken.Value<TId>(); }
        catch { return default; }
    }
}

public class UnifiedMergeContractResolver : DefaultContractResolver
{
    private static readonly ConcurrentDictionary<(Type, Type), JsonConverter> CollectionConverterCache = new();
    private static readonly ConcurrentDictionary<MemberInfo, bool> NoMergeAttributeCache = new();
    private static readonly ConcurrentDictionary<MemberInfo, CachedPropertyConfig> PropertyConfigCache = new();

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private static bool HasNoMergeAttribute(MemberInfo member) =>
        NoMergeAttributeCache.GetOrAdd(member, static m => m.GetCustomAttribute<JsonNoMergeCollectionAttribute>() != null);

    protected override JsonArrayContract CreateArrayContract(Type objectType)
    {
        var contract = base.CreateArrayContract(objectType);
        if (IsPrimitiveElementCollection(objectType))
        {
            contract.ItemIsReference = false;
            contract.IsReference = false;
        }
        return contract;
    }

    protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
    {
        var property = base.CreateProperty(member, memberSerialization);
        var t = property.PropertyType;
        if (t == null) return property;

        var config = GetOrCreatePropertyConfigRef(member, t);

        if (config.IsPrimitiveElementCollection)
        {
            property.ItemIsReference = false;
            property.IsReference = false;
            return property;
        }

        if (config.IsCollection && (!config.IsIdCollection || config.IsExcludedFromMerge))
        {
            property.ObjectCreationHandling = ObjectCreationHandling.Replace;
            return property;
        }

        if (config.IsIdCollection && config.IdType != null && config.ElementType != null && !config.IsPrimitiveElement)
        {
            property.Converter = CollectionConverterCache.GetOrAdd((config.ElementType, config.IdType), static k =>
                (JsonConverter)Activator.CreateInstance(
                    typeof(IdAwareCollectionMergeConverter<,>).MakeGenericType(k.Item1, k.Item2))!);
            property.ObjectCreationHandling = ObjectCreationHandling.Reuse;
            return property;
        }

        return property;
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private static CachedPropertyConfig GetOrCreatePropertyConfigRef(MemberInfo member, Type propertyType)
        => PropertyConfigCache.GetOrAdd(member, _ => CreatePropertyConfig(member, propertyType));

    private static CachedPropertyConfig CreatePropertyConfig(MemberInfo member, Type propertyType)
    {
        var config = new CachedPropertyConfig
        {
            IsPrimitiveElementCollection = IsPrimitiveElementCollection(propertyType),
            IsExcludedFromMerge = HasNoMergeAttribute(member),
            IsCollection = IsGenericCollectionType(propertyType)
        };

        if (config.IsCollection)
        {
            config.ElementType = GetCollectionElementType(propertyType);
            if (config.ElementType != null)
            {
                var idInfo = GetIdInfo(config.ElementType);
                if (idInfo.IsId && idInfo.IdType != null)
                {
                    config.IsIdCollection = true;
                    config.IdType = idInfo.IdType;
                }
                config.IsPrimitiveElement = IsPrimitiveOrString(config.ElementType);
            }
        }

        return config;
    }

    private sealed class CachedPropertyConfig
    {
        public bool IsPrimitiveElementCollection { get; set; }
        public bool IsExcludedFromMerge { get; set; }
        public bool IsCollection { get; set; }
        public bool IsIdCollection { get; set; }
        public Type? ElementType { get; set; }
        public Type? IdType { get; set; }
        public bool IsPrimitiveElement { get; set; }
    }
}

public static class JsonPopulateExtensions
{
    private static readonly ConcurrentDictionary<(Type, Type), JsonConverter> RootConverterCache = new();
    private static readonly UnifiedMergeContractResolver SharedContractResolver = new();
    private static readonly JsonSerializer CachedMergeSerializer = CreateMergeSerializer();
    
    private static JsonSerializer CreateMergeSerializer()
    {
        return JsonSerializer.Create(new JsonSerializerSettings
        {
            ContractResolver = SharedContractResolver,
            PreserveReferencesHandling = PreserveReferencesHandling.Objects,
            ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
            NullValueHandling = NullValueHandling.Ignore,
            MetadataPropertyHandling = MetadataPropertyHandling.ReadAhead,
        });
    }

    public static void DeepPopulateWithMerge<T>(this T target, string json, JsonSerializerSettings? settings = null) where T : notnull
    {
        ArgumentNullException.ThrowIfNull(target);
        ArgumentNullException.ThrowIfNull(json);

        json = UnwrapJsonString(json);
        var resolver = new HybridReferenceResolver(isForMerge: true, estimatedObjectCount: 32);

        JsonSerializer serializer;
        if (settings == null)
            serializer = CachedMergeSerializer;
        else
        {
            settings.ContractResolver ??= SharedContractResolver;
            serializer = JsonSerializer.Create(new JsonSerializerSettings(settings) { ReferenceResolverProvider = () => resolver });
        }

        var originalResolver = serializer.ReferenceResolver;
        serializer.ReferenceResolver = resolver;
        
        try
        {
            if (target is IList targetList)
            {
                var type = target.GetType();
                var elemType = GetCollectionElementType(type);

                if (elemType != null)
                {
                    var (isId, idType) = GetIdInfo(elemType);
                    if (isId && idType != null)
                    {
                        var converterInstance = RootConverterCache.GetOrAdd((elemType, idType), static k =>
                            (JsonConverter)Activator.CreateInstance(
                                typeof(IdAwareCollectionMergeConverter<,>).MakeGenericType(k.Item1, k.Item2))!);

                        var token = JToken.Parse(json);
                        using var reader = token.CreateReader();
                        converterInstance.ReadJson(reader, target.GetType(), target, serializer);
                        return;
                    }
                }
            }

            using var stringReader = new StringReader(json);
            using var jsonReader = new JsonTextReader(stringReader);
            serializer.Populate(jsonReader, target);
        }
        finally
        {
            serializer.ReferenceResolver = originalResolver;
        }
    }
}
