using System.Linq.Expressions;
using System.Reflection;

namespace Modules.Library.Domain.EntityBuilders;

public abstract class Builder<T>
{
    public abstract T Build();

    protected static Func<T> CreateInstanceFunction()
    {
        //var ex = Expression.MemberInit(Expression.New(typeof(T)));
        var type = typeof(T);
        var ctor = type.GetConstructor(BindingFlags.Instance | BindingFlags.NonPublic, []) ??
            throw new Exception($"Private parameterless constructor is not found for type [{type.Name}]");
        var ex = Expression.New(ctor);
        var lambda = Expression.Lambda<Func<T>>(ex);
        return lambda.Compile();
    }

    protected static Action<TBuilder, T> SetInstanceFieldsAction<TBuilder>()
    {
        var entityType = typeof(T);
        var builderType = typeof(TBuilder);
        var builder = Expression.Parameter(builderType, "builder");
        var entity = Expression.Parameter(entityType, "entity");

        var expressions = new List<Expression>();

        var fields = builderType.GetFields(BindingFlags.NonPublic | BindingFlags.Instance);

        foreach (var prop in entityType.GetProperties())
        {
            var field = builderType.GetField(BuilderFieldConventionName(prop.Name), BindingFlags.NonPublic | BindingFlags.Instance);
            if (field != null)
            {
                var builderField = Expression.Field(builder, field);
                Expression<Func<object, FieldInfo, object?>> fieldGetValueLambda = (obj, fieldInfo) => fieldInfo.GetValue(obj);
                var fieldGetValueExpr = Expression.Invoke(fieldGetValueLambda, [builder, Expression.Constant(field)]);
                if (IsReadonlyCollection(prop.PropertyType))
                {
                    var entityField = entityType.GetField(field.Name, BindingFlags.Instance | BindingFlags.NonPublic);
                    if (entityField != null) expressions.Add(Expression.Assign(Expression.Field(entity, entityField), builderField));
                }
                else
                {
                    //var field = fields.FirstOrDefault(q => q.Name == BuilderFieldConventionName(prop.Name));
                    var entityProp = Expression.Property(entity, prop);

                    Expression<Action<object, PropertyInfo, object>> propertySetValueExpr = (dst, property, value) => property.SetValue(dst, value);

                    if (IsNullable(builderField) && !IsNullable(entityProp))
                    {
                        var valueProperty = typeof(Nullable<>).GetProperty("Value")!;

                        Expression<Func<object, FieldInfo, object>> fieldGetValueFromNullableLambda = (obj, fieldInfo) => valueProperty.GetValue(fieldInfo.GetValue(obj))!;
                        var fieldGetValueFromNullableExpr = Expression.Invoke(fieldGetValueFromNullableLambda, [builder, Expression.Constant(field)]);

                        var setFromNullableExpr = Expression.Invoke(propertySetValueExpr, [entity, Expression.Constant(prop), fieldGetValueFromNullableExpr]);
                        expressions.Add(Expression.IfThen(Expression.IsTrue(IsNullExpression(builderField)), setFromNullableExpr));
                    }
                    else
                    {
                        expressions.Add(Expression.Invoke(propertySetValueExpr, [entity, Expression.Constant(prop), fieldGetValueExpr]));
                    }

                    //expressions.Add(cond);
                }
            }
        }

        expressions.Add(entity);

        var lambda = Expression.Lambda<Action<TBuilder, T>>(Expression.Block(expressions), builder, entity);
        return lambda.Compile();
    }

    private static Expression GetValue(MemberExpression member) => Expression.Convert(member, typeof(object));

    private static Expression GetValueFromNullable(Expression objExpression, MemberExpression member)
    {
        var valueProperty = typeof(Nullable<>).GetProperty("Value")!;
        var methodInfo = valueProperty.GetType().GetMethod("GetValue") ?? throw new Exception("Method \"GetValue\" is not found for nullable property");
        return Expression.Call(objExpression, methodInfo, GetValue(member));
    }

    private static Expression IsNullExpression(MemberExpression member)
    {
        var info = member.Member;
        return Expression.Equal(Expression.Convert(member, typeof(Nullable<>).MakeGenericType(MemberType(info))), Expression.Constant(null));
    }

    private static bool IsNullable(MemberExpression member) => MemberType(member.Member) == typeof(Nullable<>);

    private static Type MemberType(MemberInfo info) => info.MemberType switch
    {
        MemberTypes.Event => ((EventInfo)info).EventHandlerType!,
        MemberTypes.Field => ((FieldInfo)info).FieldType,
        MemberTypes.Method => ((MethodInfo)info).ReturnType,
        MemberTypes.Property => ((PropertyInfo)info).PropertyType,
        _ => throw new NotImplementedException()
    };

    private static bool IsReadonlyCollection(Type collectionType) => collectionType.IsGenericType &&
        collectionType.GetGenericTypeDefinition() == typeof(IReadOnlyCollection<>);

    private static string BuilderFieldConventionName(string propertyName) =>
        string.Concat('_', char.ToLower(propertyName[0]), propertyName[1..]);

}