using System.Linq.Expressions;

namespace AyCode.Core.Serializers.Expressions;

/// <summary>
/// Helper class for converting Expression trees to/from AcExpressionNode DTO.
/// Format-agnostic - use AcJsonSerializer or AcBinarySerializer to serialize the nodes.
/// </summary>
public static class AcExpressionHelper
{
    #region Expression Conversion

    /// <summary>
    /// Converts an Expression to AcExpressionNode DTO.
    /// </summary>
    public static AcExpressionNode ToNode(Expression expression)
    {
        return AcExpressionConverter.ToNode(expression);
    }

    /// <summary>
    /// Converts a typed Expression to AcExpressionNode DTO.
    /// </summary>
    public static AcExpressionNode ToNode<TEntity, TResult>(Expression<Func<TEntity, TResult>> expression)
    {
        return AcExpressionConverter.ToNode(expression);
    }

    /// <summary>
    /// Rebuilds an Expression from AcExpressionNode DTO.
    /// </summary>
    public static Expression FromNode(AcExpressionNode node, Type? entityType = null)
    {
        return AcExpressionRebuilder.FromNode(node, entityType);
    }

    /// <summary>
    /// Rebuilds a typed Expression from AcExpressionNode DTO.
    /// </summary>
    public static Expression<Func<TEntity, TResult>> FromNode<TEntity, TResult>(AcExpressionNode node)
    {
        return AcExpressionRebuilder.FromNode<TEntity, TResult>(node);
    }

    #endregion

    #region IQueryable Conversion

    /// <summary>
    /// Converts an IQueryable's expression tree to AcExpressionNode.
    /// </summary>
    public static AcExpressionNode QueryToNode<T>(IQueryable<T> query)
    {
        return ToNode(query.Expression);
    }

    /// <summary>
    /// Applies an AcExpressionNode query to an IQueryable source.
    /// </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);
    }

    #endregion

    #region Private Helpers

    /// <summary>
    /// Rebuilds a query expression, replacing the source with the provided expression.
    /// </summary>
    private static Expression RebuildQueryExpression(Expression sourceExpression, AcExpressionNode node, Type entityType)
    {
        if (node is { NodeType: ExpressionType.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 == ExpressionType.Lambda)
        {
            var rebuilder = new AcExpressionRebuilder();
            var lambda = rebuilder.Rebuild(node, entityType);
            
            var whereMethod = typeof(Queryable).GetMethods()
                .First(m => m.Name == "Where" && m.GetParameters().Length == 2)
                .MakeGenericMethod(entityType);

            return Expression.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.)
    /// </summary>
    private static Expression RebuildMethodCallChain(Expression sourceExpression, AcExpressionNode node, Type entityType)
    {
        // First, process the inner expression (the source of this method call)
        Expression currentSource;
        
        if (node.Arguments?.Count > 0 && node.Arguments[0].NodeType == ExpressionType.Call)
        {
            // Recursive: rebuild the inner chain first
            currentSource = RebuildMethodCallChain(sourceExpression, node.Arguments[0], entityType);
        }
        else
        {
            // Base case: use the provided source
            currentSource = sourceExpression;
        }

        // Now apply this method call
        var methodName = node.MethodName!;
        var declaringType = node.DeclaringType != null ? Type.GetType(node.DeclaringType) : typeof(Queryable);

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

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

        // Build arguments
        var rebuilder = new AcExpressionRebuilder();
        var arguments = new List<Expression> { currentSource };

        // Skip first argument (it's the source) and rebuild the rest
        if (node.Arguments?.Count > 1)
        {
            for (var i = 1; i < node.Arguments.Count; i++)
            {
                var argNode = node.Arguments[i];
                
                if (argNode is { NodeType: ExpressionType.Quote, Operand: not null })
                {
                    // Quoted lambda - unquote and rebuild
                    var lambda = rebuilder.Rebuild(argNode.Operand, entityType);
                    arguments.Add(Expression.Quote(lambda));
                }
                else if (argNode.NodeType == ExpressionType.Lambda)
                {
                    var lambda = rebuilder.Rebuild(argNode, entityType);
                    arguments.Add(Expression.Quote(lambda));
                }
                else
                {
                    arguments.Add(rebuilder.Rebuild(argNode, entityType));
                }
            }
        }

        return Expression.Call(method, arguments);
    }

    private static System.Reflection.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
}
