using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Binders;
using System.Reflection;

public static class ControllerExtensions
{
    public static async Task<IViewComponentResult> InvokeViewComponentWithModelBindingAsync(
        this Controller controller,
        Type viewComponentType)
    {
        
        var viewComponent = GetViewComponentInstance(controller, viewComponentType);
        var invokeMethod = GetInvokeMethod(viewComponentType);
        var parameters = invokeMethod.GetParameters();

        var parameterValues = await BindParametersAsync(controller, parameters);

        return await ExecuteViewComponentAsync(viewComponent, invokeMethod, parameterValues);
    }

    private static ViewComponent GetViewComponentInstance(Controller controller, Type viewComponentType)
    {
        var instance = controller.HttpContext.RequestServices.GetService(viewComponentType) as ViewComponent;
        if (instance == null)
            throw new InvalidOperationException($"ViewComponent {viewComponentType.Name} not found.");
        return instance;
    }

    private static MethodInfo GetInvokeMethod(Type viewComponentType)
    {
        var method = viewComponentType.GetMethod("InvokeAsync") ?? viewComponentType.GetMethod("Invoke");
        if (method == null)
            throw new InvalidOperationException("ViewComponent does not have Invoke/InvokeAsync method.");
        return method;
    }

    private static async Task<object[]> BindParametersAsync(Controller controller, ParameterInfo[] parameters)
    {
        var parameterValues = new object[parameters.Length];
        var modelBindingContexts = new DefaultModelBindingContext[parameters.Length];

        for (int i = 0; i < parameters.Length; i++)
        {
            var parameter = parameters[i];
            var bindingContext = CreateBindingContextAsync(controller, parameter)
                            .GetAwaiter()
                            .GetResult(); 
            var binder = GetModelBinder(controller, parameter);

            await binder.BindModelAsync(bindingContext);
            parameterValues[i] = bindingContext.Result.IsModelSet 
                ? bindingContext.Result.Model 
                : parameter.DefaultValue;
        }

        return parameterValues;
    }

    private static async Task<DefaultModelBindingContext> CreateBindingContextAsync(Controller controller, ParameterInfo parameter) 
    {
        var valueProviders = new List<IValueProvider>(); 
        var factoryContext = new ValueProviderFactoryContext(controller.ControllerContext);

        foreach (var factory in controller.ControllerContext.ValueProviderFactories) 
        {
            await factory.CreateValueProviderAsync(factoryContext); 
        }
        
        return new DefaultModelBindingContext 
        {
            ActionContext = controller.ControllerContext, 
            ModelState = controller.ModelState, 
            ModelMetadata = controller.MetadataProvider.GetMetadataForType(parameter.ParameterType), 
            FieldName = parameter.Name, 
            ModelName = parameter.Name, 
            ValueProvider = new CompositeValueProvider(valueProviders) 
        }; 
    }

    private static IModelBinder GetModelBinder(Controller controller, ParameterInfo parameter)
    {
        var metadata = controller.MetadataProvider.GetMetadataForType(parameter.ParameterType);
        return controller.ModelBinderFactory.CreateBinder(new ModelBinderFactoryContext
        {
            Metadata = metadata,
            BindingInfo = new BindingInfo { BindingSource = BindingSource.ModelBinding }
        });
    }

    private static async Task<IViewComponentResult> ExecuteViewComponentAsync(
        ViewComponent viewComponent,
        MethodInfo method,
        object[] parameters)
    {
        var result = method.Invoke(viewComponent, parameters);

        if (result is Task task)
        {
            await task;
            return (IViewComponentResult)((dynamic)task).Result;
        }

        return (IViewComponentResult)result;
    }
}