using System.Buffers;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Frozen;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text;
using AyCode.Core.Interfaces;
using AyCode.Core.Serializers;
using AyCode.Core.Serializers.Jsons;
using Newtonsoft.Json;

namespace AyCode.Core.Helpers;

/// <summary>
/// Cached result for IId type info lookup.
/// </summary>
public readonly struct IdTypeInfo
{
    public readonly bool IsId;
    public readonly Type? IdType;
    
    public IdTypeInfo(bool isId, Type? idType)
    {
        IsId = isId;
        IdType = idType;
    }
    
    public void Deconstruct(out bool isId, out Type? idType)
    {
        isId = IsId;
        idType = IdType;
    }
}

/// <summary>
/// Central utilities for JSON serialization/deserialization.
/// Contains shared type caches, primitive type checks, and string utilities.
/// </summary>
public static class JsonUtilities
{
    #region Pre-computed Type Handles
    
    public static readonly Type IntType = typeof(int);
    public static readonly Type LongType = typeof(long);
    public static readonly Type DoubleType = typeof(double);
    public static readonly Type DecimalType = typeof(decimal);
    public static readonly Type FloatType = typeof(float);
    public static readonly Type StringType = typeof(string);
    public static readonly Type DateTimeType = typeof(DateTime);
    public static readonly Type GuidType = typeof(Guid);
    public static readonly Type BoolType = typeof(bool);
    public static readonly Type DateTimeOffsetType = typeof(DateTimeOffset);
    public static readonly Type TimeSpanType = typeof(TimeSpan);
    public static readonly Type ByteType = typeof(byte);
    public static readonly Type ShortType = typeof(short);
    public static readonly Type UShortType = typeof(ushort);
    public static readonly Type UIntType = typeof(uint);
    public static readonly Type ULongType = typeof(ulong);
    public static readonly Type SByteType = typeof(sbyte);
    public static readonly Type CharType = typeof(char);
    
    #endregion

    #region Cached Generic Type Definitions
    
    internal static readonly Type IEnumerableGenericType = typeof(IEnumerable<>);
    internal static readonly Type IIdGenericType = typeof(IId<>);
    internal static readonly Type NullableGenericType = typeof(Nullable<>);
    internal static readonly Type IListGenericType = typeof(IList<>);
    internal static readonly Type ListGenericType = typeof(List<>);
    internal static readonly Type DictionaryGenericType = typeof(Dictionary<,>);
    internal static readonly Type IDictionaryGenericType = typeof(IDictionary<,>);
    internal static readonly Type ObservableCollectionType = typeof(System.Collections.ObjectModel.ObservableCollection<>);
    internal static readonly Type CollectionType = typeof(System.Collections.ObjectModel.Collection<>);
    
    #endregion

    #region Primitive Type Set
    
    private static readonly FrozenSet<Type> PrimitiveTypes = new HashSet<Type>
    {
        typeof(string), typeof(decimal), typeof(DateTime),
        typeof(DateTimeOffset), typeof(Guid), typeof(TimeSpan),
        typeof(bool), typeof(byte), typeof(sbyte), typeof(short),
        typeof(ushort), typeof(int), typeof(uint), typeof(long),
        typeof(ulong), typeof(float), typeof(double), typeof(char)
    }.ToFrozenSet();

    #endregion

    #region Type Caches

    private static readonly ConcurrentDictionary<Type, Func<int, IList>> ListFactoryCache = new();

    #endregion

    #region UTF8 Buffer Pool

    /// <summary>
    /// Rents a UTF8 byte buffer from the shared pool.
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static byte[] RentUtf8Buffer(int minLength)
        => ArrayPool<byte>.Shared.Rent(minLength);

    /// <summary>
    /// Returns a UTF8 byte buffer to the shared pool.
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static void ReturnUtf8Buffer(byte[] buffer)
        => ArrayPool<byte>.Shared.Return(buffer);

    /// <summary>
    /// Converts a JSON string to UTF8 bytes using pooled buffer.
    /// Returns the actual byte count written.
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static (byte[] buffer, int length) GetUtf8Bytes(string json)
    {
        var maxByteCount = Encoding.UTF8.GetMaxByteCount(json.Length);
        var buffer = RentUtf8Buffer(maxByteCount);
        var actualLength = Encoding.UTF8.GetBytes(json, 0, json.Length, buffer, 0);
        return (buffer, actualLength);
    }

    #endregion

    #region String Utilities
    
    /// <summary>
    /// Unwraps a JSON string that may be double-quoted (e.g., "\"...\""  -> ...).
    /// Optimized to avoid Regex.Unescape allocation when no escape sequences exist.
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static string UnwrapJsonString(string json)
    {
        if (string.IsNullOrEmpty(json)) return json;

        var span = json.AsSpan();
        if (span.Length < 2 || span[0] != '"' || span[^1] != '"')
            return json;

        var inner = span[1..^1];

        if (!inner.Contains('\\'))
            return json.Substring(1, json.Length - 2);

        return UnescapeJsonString(inner);
    }

    private static string UnescapeJsonString(ReadOnlySpan<char> input)
    {
        var sb = new StringBuilder(input.Length);

        for (var i = 0; i < input.Length; i++)
        {
            var c = input[i];
            if (c != '\\' || i + 1 >= input.Length)
            {
                sb.Append(c);
                continue;
            }

            var next = input[i + 1];
            switch (next)
            {
                case '"': sb.Append('"'); i++; break;
                case '\\': sb.Append('\\'); i++; break;
                case '/': sb.Append('/'); i++; break;
                case 'b': sb.Append('\b'); i++; break;
                case 'f': sb.Append('\f'); i++; break;
                case 'n': sb.Append('\n'); i++; break;
                case 'r': sb.Append('\r'); i++; break;
                case 't': sb.Append('\t'); i++; break;
                case 'u' when i + 5 < input.Length:
                    var hex = input.Slice(i + 2, 4);
                    if (TryParseHex(hex, out var unicode))
                    {
                        sb.Append((char)unicode);
                        i += 5;
                    }
                    else sb.Append(c);
                    break;
                default: sb.Append(c); break;
            }
        }

        return sb.ToString();
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private static bool TryParseHex(ReadOnlySpan<char> hex, out int value)
    {
        value = 0;
        foreach (var c in hex)
        {
            value <<= 4;
            if (c >= '0' && c <= '9') value |= c - '0';
            else if (c >= 'a' && c <= 'f') value |= c - 'a' + 10;
            else if (c >= 'A' && c <= 'F') value |= c - 'A' + 10;
            else return false;
        }
        return true;
    }

    /// <summary>
    /// Checks if a string needs JSON escaping.
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static bool NeedsEscaping(string value)
    {
        foreach (var c in value)
        {
            if (c < 32 || c == '"' || c == '\\')
                return true;
        }
        return false;
    }

    /// <summary>
    /// Checks if a span needs JSON escaping.
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static bool NeedsEscaping(ReadOnlySpan<char> value)
    {
        foreach (var c in value)
        {
            if (c < 32 || c == '"' || c == '\\')
                return true;
        }
        return false;
    }

    /// <summary>
    /// Escapes a string for JSON output.
    /// </summary>
    public static void WriteEscapedString(StringBuilder sb, string value)
    {
        foreach (var c in value)
        {
            switch (c)
            {
                case '"': sb.Append("\\\""); break;
                case '\\': sb.Append("\\\\"); break;
                case '\b': sb.Append("\\b"); break;
                case '\f': sb.Append("\\f"); break;
                case '\n': sb.Append("\\n"); break;
                case '\r': sb.Append("\\r"); break;
                case '\t': sb.Append("\\t"); break;
                default:
                    if (c < 32)
                    {
                        sb.Append("\\u");
                        sb.Append(((int)c).ToString("X4"));
                    }
                    else sb.Append(c);
                    break;
            }
        }
    }

    /// <summary>
    /// Escapes a span for JSON output.
    /// </summary>
    public static void WriteEscapedString(StringBuilder sb, ReadOnlySpan<char> value)
    {
        foreach (var c in value)
        {
            switch (c)
            {
                case '"': sb.Append("\\\""); break;
                case '\\': sb.Append("\\\\"); break;
                case '\b': sb.Append("\\b"); break;
                case '\f': sb.Append("\\f"); break;
                case '\n': sb.Append("\\n"); break;
                case '\r': sb.Append("\\r"); break;
                case '\t': sb.Append("\\t"); break;
                default:
                    if (c < 32)
                    {
                        sb.Append("\\u");
                        sb.Append(((int)c).ToString("X4"));
                    }
                    else sb.Append(c);
                    break;
            }
        }
    }
    
    #endregion

    #region Type Checking Methods
    
    /// <summary>
    /// Detects the serializer type from the response data.
    /// Checks the first byte after MessagePack deserialization to determine if it's JSON or Binary format.
    /// </summary>
    /// <param name="responseData">The response data (string for JSON, byte[] for Binary)</param>
    /// <returns>The detected serializer type</returns>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static AcSerializerType DetectSerializerType(object? responseData)
    {
        return responseData switch
        {
            byte[] => AcSerializerType.Binary,
            string => AcSerializerType.Json,
            null => AcSerializerType.Json, // Default to JSON for null
            _ => AcSerializerType.Json // Default to JSON for unknown types
        };
    }

    /// <summary>
    /// Detects if byte array contains JSON or Binary serialized data.
    /// JSON typically starts with '{', '[', '"' or whitespace.
    /// Binary format starts with version byte (typically 1) followed by metadata flag.
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static AcSerializerType DetectSerializerTypeFromBytes(ReadOnlySpan<byte> data)
    {
        if (data.IsEmpty) return AcSerializerType.Json;
        
        var firstByte = data[0];
        
        // JSON typically starts with: '{' (123), '[' (91), '"' (34), or whitespace (32, 9, 10, 13)
        // Also numbers: '0'-'9' (48-57), '-' (45), 't' (116 for true), 'f' (102 for false), 'n' (110 for null)
        return firstByte switch
        {
            (byte)'{' or (byte)'[' or (byte)'"' => AcSerializerType.Json,
            (byte)' ' or (byte)'\t' or (byte)'\n' or (byte)'\r' => AcSerializerType.Json,
            >= (byte)'0' and <= (byte)'9' => AcSerializerType.Json,
            (byte)'-' or (byte)'t' or (byte)'f' or (byte)'n' => AcSerializerType.Json,
            // Binary format version 1 with:
            // - Legacy metadata header (32) or no-metadata header (33)
            // - New flag-based header (34+)
            1 when data.Length > 1 && data[1] >= 32 => AcSerializerType.Binary,
            _ => AcSerializerType.Binary // Default to Binary for unknown byte patterns
        };
    }

    /// <summary>
    /// Detects serializer type from a string (checks if it looks like JSON).
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static AcSerializerType DetectSerializerTypeFromString(string? data)
    {
        if (string.IsNullOrEmpty(data)) return AcSerializerType.Json;
        
        var trimmed = data.AsSpan().Trim();
        if (trimmed.IsEmpty) return AcSerializerType.Json;
        
        var firstChar = trimmed[0];
        
        // Valid JSON starts with: '{', '[', '"', number, 't', 'f', 'n'
        return firstChar switch
        {
            '{' or '[' or '"' => AcSerializerType.Json,
            >= '0' and <= '9' => AcSerializerType.Json,
            '-' or 't' or 'f' or 'n' => AcSerializerType.Json,
            _ => AcSerializerType.Binary // Likely Base64 encoded binary
        };
    }

    /// <summary>
    /// Checks if type is primitive, string, or nullable primitive.
    /// Delegates to IsPrimitiveOrStringFast with nullable unwrapping.
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static bool IsPrimitiveOrString(Type type)
    {
        if (IsPrimitiveOrStringFast(type)) return true;
        if (type.IsGenericType && type.GetGenericTypeDefinition() == NullableGenericType)
            return IsPrimitiveOrStringFast(type.GetGenericArguments()[0]);
        return false;
    }

    /// <summary>
    /// Faster primitive check using TypeCode for hot paths.
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static bool IsPrimitiveOrStringFast(in Type type)
    {
        var typeCode = Type.GetTypeCode(type);
        return typeCode switch
        {
            TypeCode.Boolean or TypeCode.Char or TypeCode.SByte or TypeCode.Byte or
            TypeCode.Int16 or TypeCode.UInt16 or TypeCode.Int32 or TypeCode.UInt32 or
            TypeCode.Int64 or TypeCode.UInt64 or TypeCode.Single or TypeCode.Double or
            TypeCode.Decimal or TypeCode.DateTime or TypeCode.String => true,
            _ => ReferenceEquals(type, GuidType) || ReferenceEquals(type, TimeSpanType) || ReferenceEquals(type, DateTimeOffsetType) || type.IsEnum
        };
    }

    /// <summary>
    /// Checks if type is a generic collection type (List, IList, ObservableCollection, etc.)
    /// Only called at metadata/config creation time — not in hot path.
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static bool IsGenericCollectionType(in Type type)
    {
        if (ReferenceEquals(type, StringType) || type.IsPrimitive) return false;
        if (type.IsArray) return true;
        
        if (type.IsGenericType)
        {
            var genericDef = type.GetGenericTypeDefinition();
            if (ReferenceEquals(genericDef, ListGenericType) || 
                ReferenceEquals(genericDef, IListGenericType) || 
                genericDef == typeof(ICollection<>) || 
                ReferenceEquals(genericDef, IEnumerableGenericType) ||
                ReferenceEquals(genericDef, ObservableCollectionType) ||
                ReferenceEquals(genericDef, CollectionType))
                return true;
        }
        
        foreach (var iface in type.GetInterfaces())
        {
            if (iface.IsGenericType && ReferenceEquals(iface.GetGenericTypeDefinition(), IListGenericType))
                return true;
        }
        
        return typeof(IEnumerable).IsAssignableFrom(type);
    }

    /// <summary>
    /// Checks if type is a dictionary type.
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static bool IsDictionaryType(in Type type, out Type? keyType, out Type? valueType)
    {
        keyType = null;
        valueType = null;
        
        if (!type.IsGenericType) return false;
        
        var genericDef = type.GetGenericTypeDefinition();
        if (ReferenceEquals(genericDef, DictionaryGenericType) || ReferenceEquals(genericDef, IDictionaryGenericType))
        {
            var args = type.GetGenericArguments();
            keyType = args[0];
            valueType = args[1];
            return true;
        }
        
        foreach (var iface in type.GetInterfaces())
        {
            if (iface.IsGenericType && ReferenceEquals(iface.GetGenericTypeDefinition(), IDictionaryGenericType))
            {
                var args = iface.GetGenericArguments();
                keyType = args[0];
                valueType = args[1];
                return true;
            }
        }
        
        return false;
    }

    /// <summary>
    /// Gets the element type of a collection.
    /// Only called at metadata/config creation time — not in hot path.
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static Type? GetCollectionElementType(in Type collectionType)
    {
        if (collectionType.IsArray)
            return collectionType.GetElementType();

        if (collectionType.IsGenericType)
        {
            var genericDef = collectionType.GetGenericTypeDefinition();
            if (ReferenceEquals(genericDef, ListGenericType) || ReferenceEquals(genericDef, IListGenericType) || 
                genericDef == typeof(ICollection<>) || ReferenceEquals(genericDef, IEnumerableGenericType))
                return collectionType.GetGenericArguments()[0];
        }
        
        foreach (var iface in collectionType.GetInterfaces())
        {
            if (iface.IsGenericType && ReferenceEquals(iface.GetGenericTypeDefinition(), IListGenericType))
                return iface.GetGenericArguments()[0];
        }

        return typeof(object);
    }

    /// <summary>
    /// Gets IId info for a type. Returns struct to avoid allocation.
    /// Only called at metadata creation time — not in hot path.
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static IdTypeInfo GetIdInfo(in Type type)
    {
        foreach (var iface in type.GetInterfaces())
        {
            if (!iface.IsGenericType) continue;
            if (!ReferenceEquals(iface.GetGenericTypeDefinition(), IIdGenericType)) continue;
            var idType = iface.GetGenericArguments()[0];
            return new IdTypeInfo(true, idType);
        }
        return new IdTypeInfo(false, typeof(int));
    }

    /// <summary>
    /// Checks if property has JsonIgnore attribute.
    /// Only called at metadata creation time — not in hot path.
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static bool HasJsonIgnoreAttribute(PropertyInfo prop)
    {
        return Attribute.IsDefined(prop, typeof(JsonIgnoreAttribute)) ||
               Attribute.IsDefined(prop, typeof(System.Text.Json.Serialization.JsonIgnoreAttribute));
    }

    /// <summary>
    /// Checks if property has ToonIgnore attribute.
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static bool HasToonIgnoreAttribute(PropertyInfo prop)
    {
        return false;
    }

    /// <summary>
    /// Check if a property type is unsupported for serialization.
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static bool IsUnsupportedPropertyType(Type type)
    {
        if (type.IsByRef || type.IsByRefLike)
            return true;

        if (type.IsPointer)
            return true;

        if (typeof(Type).IsAssignableFrom(type) || typeof(MemberInfo).IsAssignableFrom(type))
            return true;

        return false;
    }

    /// <summary>
    /// Checks if collection contains primitive elements.
    /// Only called at metadata/config creation time — not in hot path.
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static bool IsPrimitiveElementCollection(in Type type)
    {
        if (ReferenceEquals(type, StringType)) return false;

        Type? elementType = null;
        if (type.IsArray)
            elementType = type.GetElementType();
        else if (type.IsGenericType && typeof(System.Collections.IEnumerable).IsAssignableFrom(type))
        {
            var genericArgs = type.GetGenericArguments();
            if (genericArgs.Length == 1) elementType = genericArgs[0];
        }

        if (elementType == null) return false;
        return IsPrimitiveOrString(elementType) || elementType.IsEnum;
    }

    /// <summary>
    /// Gets or creates a list factory for a given element type.
    /// Accepts capacity parameter: capacity &lt;= 0 uses parameterless constructor,
    /// capacity &gt; 0 uses List&lt;T&gt;(capacity) constructor.
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static Func<int, IList> GetOrCreateListFactory(in Type elementType)
    {
        return ListFactoryCache.GetOrAdd(elementType, static t =>
        {
            var listType = ListGenericType.MakeGenericType(t);
            var capacityParam = System.Linq.Expressions.Expression.Parameter(typeof(int), "capacity");

            // new List<T>() - parameterless
            var defaultCtor = listType.GetConstructor(Type.EmptyTypes)!;
            var newDefault = System.Linq.Expressions.Expression.New(defaultCtor);

            // new List<T>(capacity) - with capacity
            var capacityCtor = listType.GetConstructor(new[] { typeof(int) })!;
            var newWithCapacity = System.Linq.Expressions.Expression.New(capacityCtor, capacityParam);

            // capacity > 0 ? new List<T>(capacity) : new List<T>()
            var condition = System.Linq.Expressions.Expression.Condition(
                System.Linq.Expressions.Expression.GreaterThan(capacityParam, System.Linq.Expressions.Expression.Constant(0)),
                System.Linq.Expressions.Expression.Convert(newWithCapacity, typeof(IList)),
                System.Linq.Expressions.Expression.Convert(newDefault, typeof(IList)));

            return System.Linq.Expressions.Expression.Lambda<Func<int, IList>>(condition, capacityParam).Compile();
        });
    }

    /// <summary>
    /// Checks if value is the default value for its type.
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static bool IsDefaultValue(in object id, in Type idType)
    {
        if (ReferenceEquals(idType, IntType)) return (int)id == 0;
        if (ReferenceEquals(idType, LongType)) return (long)id == 0;
        if (ReferenceEquals(idType, GuidType)) return (Guid)id == Guid.Empty;
        if (ReferenceEquals(idType, ShortType)) return (short)id == 0;
        if (ReferenceEquals(idType, ByteType)) return (byte)id == 0;
        if (ReferenceEquals(idType, UIntType)) return (uint)id == 0;
        if (ReferenceEquals(idType, ULongType)) return (ulong)id == 0;
        if (ReferenceEquals(idType, UShortType)) return (ushort)id == 0;
        if (ReferenceEquals(idType, SByteType)) return (sbyte)id == 0;
        return false;
    }
    
    #endregion
}
