using System.Reflection;
using System.Text.Json;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using TodoList.Application.Interfaces;

namespace TodoList.Application.DynamicApi;

public class DynamicApiMiddleware
{
    private readonly RequestDelegate _next;
    private readonly IServiceProvider _serviceProvider;
    private static readonly Dictionary<string, Type> _serviceTypeCache = new Dictionary<string, Type>(StringComparer.OrdinalIgnoreCase);

    static DynamicApiMiddleware()
    {
        InitializeServiceTypeCache();
    }

    private static void InitializeServiceTypeCache()
    {
        var assembly = typeof(IDynamicApiService).Assembly;
        var serviceTypes = assembly.GetTypes()
            .Where(t => t.IsInterface && typeof(IDynamicApiService).IsAssignableFrom(t))
            .ToList();

        foreach (var type in serviceTypes)
        {
            var cleanName = type.Name.EndsWith("Service") 
                ? type.Name.Substring(0, type.Name.Length - "Service".Length) 
                : type.Name;
            
            if (cleanName.StartsWith("I") && cleanName.Length > 1 && char.IsUpper(cleanName[1]))
            {
                cleanName = cleanName.Substring(1);
            }
            
            if (cleanName.EndsWith("App"))
            {
                cleanName = cleanName.Substring(0, cleanName.Length - "App".Length);
            }
            
            _serviceTypeCache[cleanName] = type;
        }
    }

    public DynamicApiMiddleware(RequestDelegate next, IServiceProvider serviceProvider)
    {
        _next = next;
        _serviceProvider = serviceProvider;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        var path = context.Request.Path.Value ?? string.Empty;
        var segments = path.Split('/', StringSplitOptions.RemoveEmptyEntries);

        if (segments.Length < 2 || segments[0] != "api")
        {
            await _next(context);
            return;
        }

        var serviceName = segments[1];
        var serviceType = FindDynamicApiService(serviceName);

        if (serviceType == null)
        {
            await _next(context);
            return;
        }

        if (!IsRemoteServiceEnabled(serviceType))
        {
            await _next(context);
            return;
        }

        using var scope = _serviceProvider.CreateScope();
        var scopedServiceProvider = scope.ServiceProvider;
        var service = scopedServiceProvider.GetService(serviceType);
        if (service == null)
        {
            await _next(context);
            return;
        }

        var method = FindMethod(serviceType, context.Request.Method, segments.Skip(2).ToArray(), context);
        if (method == null)
        {
            await _next(context);
            return;
        }

        if (!IsRemoteServiceEnabled(method))
        {
            await _next(context);
            return;
        }

        try
        {
            var result = await InvokeMethod(service, method, context);
            await WriteResponse(context, result, null, method.Name);
        }
        catch (Exception ex)
        {
            await WriteResponse(context, null, ex, method.Name);
        }
    }

    private Type? FindDynamicApiService(string serviceName)
    {
        _serviceTypeCache.TryGetValue(serviceName, out var serviceType);
        return serviceType;
    }

    private bool IsRemoteServiceEnabled(Type type)
    {
        var attribute = type.GetCustomAttribute<RemoteServiceAttribute>();
        return attribute == null || attribute.IsEnabled;
    }

    private bool IsRemoteServiceEnabled(MethodInfo method)
    {
        var attribute = method.GetCustomAttribute<RemoteServiceAttribute>();
        return attribute == null || attribute.IsEnabled;
    }

    private MethodInfo? FindMethod(Type serviceType, string httpMethod, string[] pathSegments, HttpContext context)
    {
        var methods = serviceType.GetMethods(BindingFlags.Public | BindingFlags.Instance);
        
        var matchedMethods = methods.Where(m => IsRemoteServiceEnabled(m)).ToList();

        // First, try to handle special case: /api/task/13/toggle
        if (pathSegments.Length >= 2 && httpMethod == "PATCH")
        {
            var potentialMethodName = pathSegments[1];
            var potentialParamValue = pathSegments[0];
            
            // Try to find method with matching name and single parameter
            foreach (var method in matchedMethods)
            {
                if (MatchesHttpMethod(method, httpMethod))
                {
                    var parameters = method.GetParameters();
                    if (parameters.Length == 1 && IsSimpleType(parameters[0].ParameterType))
                    {
                        // Try to match with normalized method name
                        var normalizedMethodName = method.Name;
                        if (normalizedMethodName.EndsWith("Async", StringComparison.OrdinalIgnoreCase))
                        {
                            normalizedMethodName = normalizedMethodName.Substring(0, normalizedMethodName.Length - 5);
                        }
                        
                        if (normalizedMethodName.Equals(potentialMethodName, StringComparison.OrdinalIgnoreCase))
                        {
                            return method;
                        }
                        
                        // For ToggleCompleteAsync, try to match with "toggle"
                        if (normalizedMethodName.StartsWith("Toggle", StringComparison.OrdinalIgnoreCase) &&
                            potentialMethodName.Equals("toggle", StringComparison.OrdinalIgnoreCase))
                        {
                            return method;
                        }
                    }
                }
            }
        }

        // Try to match with method name from path segments
        var methodName = pathSegments.Length > 0 ? pathSegments[0] : null;
        if (!string.IsNullOrEmpty(methodName))
        {
            var exactMatch = matchedMethods.FirstOrDefault(m => 
                m.Name.Equals(methodName, StringComparison.OrdinalIgnoreCase) &&
                MatchesHttpMethod(m, httpMethod));
            
            if (exactMatch != null)
                return exactMatch;

            // Try to match method names with different naming conventions
            foreach (var method in matchedMethods)
            {
                if (MatchesHttpMethod(method, httpMethod))
                {
                    // For GetSubTasksAsync, try to match with "subtasks"
                    var normalizedMethodName = method.Name;
                    if (normalizedMethodName.StartsWith("Get", StringComparison.OrdinalIgnoreCase))
                    {
                        normalizedMethodName = normalizedMethodName.Substring(3);
                    }
                    if (normalizedMethodName.EndsWith("Async", StringComparison.OrdinalIgnoreCase))
                    {
                        normalizedMethodName = normalizedMethodName.Substring(0, normalizedMethodName.Length - 5);
                    }
                    
                    if (normalizedMethodName.Equals(methodName, StringComparison.OrdinalIgnoreCase))
                    {
                        return method;
                    }

                    // For GetActiveTasksAsync, try to match with "active"
                    if (normalizedMethodName.Equals($"{methodName}Tasks", StringComparison.OrdinalIgnoreCase))
                    {
                        return method;
                    }
                }
            }
        }

        // First, try to handle special case: /api/task/13/subtasks
        if (pathSegments.Length >= 2)
        {
            var potentialMethodName = pathSegments[1];
            var potentialParamValue = pathSegments[0];
            
            // Try to find method with matching name and single parameter
            foreach (var method in matchedMethods)
            {
                if (MatchesHttpMethod(method, httpMethod))
                {
                    var parameters = method.GetParameters();
                    if (parameters.Length == 1 && IsSimpleType(parameters[0].ParameterType))
                    {
                        // Try to match with normalized method name
                        var normalizedMethodName = method.Name;
                        if (normalizedMethodName.StartsWith("Get", StringComparison.OrdinalIgnoreCase))
                        {
                            normalizedMethodName = normalizedMethodName.Substring(3);
                        }
                        if (normalizedMethodName.EndsWith("Async", StringComparison.OrdinalIgnoreCase))
                        {
                            normalizedMethodName = normalizedMethodName.Substring(0, normalizedMethodName.Length - 5);
                        }
                        
                        if (normalizedMethodName.Equals(potentialMethodName, StringComparison.OrdinalIgnoreCase))
                        {
                            return method;
                        }
                    }
                }
            }
        }

        foreach (var method in matchedMethods)
        {
            if (MatchesHttpMethod(method, httpMethod) && MatchesRoute(method, pathSegments))
            {
                return method;
            }
        }

        // Try to match methods with parameters from path
        foreach (var method in matchedMethods)
        {
            if (MatchesHttpMethod(method, httpMethod))
            {
                var parameters = method.GetParameters();
                if (parameters.Length > 0 && pathSegments.Length > 0)
                {
                    // Check if all path segments can be mapped to parameters
                    bool canMapParameters = true;
                    for (int i = 0; i < pathSegments.Length; i++)
                    {
                        if (i >= parameters.Length)
                        {
                            canMapParameters = false;
                            break;
                        }
                        if (!IsSimpleType(parameters[i].ParameterType))
                        {
                            canMapParameters = false;
                            break;
                        }
                    }
                    if (canMapParameters)
                    {
                        // Try to convert the first path segment to the parameter type
                        // to avoid trying to convert non-numeric strings to numbers
                        try
                        {
                            ConvertValue(pathSegments[0], parameters[0].ParameterType);
                            return method;
                        }
                        catch
                        {
                            // If conversion fails, skip this method
                            continue;
                        }
                    }
                }
            }
        }

        return null;
    }

    private bool MatchesHttpMethod(MethodInfo method, string httpMethod)
    {
        if (method.GetCustomAttribute<HttpGetAttribute>() != null)
            return httpMethod == "GET";
        
        if (method.GetCustomAttribute<HttpPostAttribute>() != null)
            return httpMethod == "POST";
        
        if (method.GetCustomAttribute<HttpPutAttribute>() != null)
            return httpMethod == "PUT";
        
        if (method.GetCustomAttribute<HttpDeleteAttribute>() != null)
            return httpMethod == "DELETE";
        
        if (method.GetCustomAttribute<HttpPatchAttribute>() != null)
            return httpMethod == "PATCH";

        // For ToggleCompleteAsync, use PATCH method
        if (method.Name.StartsWith("Toggle", StringComparison.OrdinalIgnoreCase))
        {
            return httpMethod == "PATCH";
        }

        return GetHttpVerbByConvention(method.Name) == httpMethod;
    }

    private string GetHttpVerbByConvention(string methodName)
    {
        if (methodName.StartsWith("Get", StringComparison.OrdinalIgnoreCase))
            return "GET";
        
        if (methodName.StartsWith("Put", StringComparison.OrdinalIgnoreCase) ||
            methodName.StartsWith("Update", StringComparison.OrdinalIgnoreCase))
            return "PUT";
        
        if (methodName.StartsWith("Delete", StringComparison.OrdinalIgnoreCase) ||
            methodName.StartsWith("Remove", StringComparison.OrdinalIgnoreCase))
            return "DELETE";
        
        if (methodName.StartsWith("Post", StringComparison.OrdinalIgnoreCase) ||
            methodName.StartsWith("Create", StringComparison.OrdinalIgnoreCase) ||
            methodName.StartsWith("Insert", StringComparison.OrdinalIgnoreCase))
            return "POST";
        
        if (methodName.StartsWith("Patch", StringComparison.OrdinalIgnoreCase))
            return "PATCH";

        return "POST";
    }

    private bool MatchesRoute(MethodInfo method, string[] pathSegments)
    {
        var httpGetAttr = method.GetCustomAttribute<HttpGetAttribute>();
        var httpPostAttr = method.GetCustomAttribute<HttpPostAttribute>();
        var httpPutAttr = method.GetCustomAttribute<HttpPutAttribute>();
        var httpDeleteAttr = method.GetCustomAttribute<HttpDeleteAttribute>();
        var httpPatchAttr = method.GetCustomAttribute<HttpPatchAttribute>();

        var route = httpGetAttr?.Route ?? httpPostAttr?.Route ?? 
                     httpPutAttr?.Route ?? httpDeleteAttr?.Route ?? httpPatchAttr?.Route;

        if (!string.IsNullOrEmpty(route))
        {
            var routeSegments = route.Split('/', StringSplitOptions.RemoveEmptyEntries);
            if (routeSegments.Length > 0 && pathSegments.Length > 0)
            {
                return routeSegments[0].Equals(pathSegments[0], StringComparison.OrdinalIgnoreCase);
            }
            return routeSegments.Length == 0 && pathSegments.Length == 0;
        }

        if (pathSegments.Length == 0)
        {
            return true;
        }

        // Try to match with full method name
        if (method.Name.Equals(pathSegments[0], StringComparison.OrdinalIgnoreCase))
        {
            return true;
        }

        // Try to match with normalized method name (without Get/Async prefix/suffix)
        var normalizedMethodName = method.Name;
        if (normalizedMethodName.StartsWith("Get", StringComparison.OrdinalIgnoreCase))
        {
            normalizedMethodName = normalizedMethodName.Substring(3);
        }
        if (normalizedMethodName.EndsWith("Async", StringComparison.OrdinalIgnoreCase))
        {
            normalizedMethodName = normalizedMethodName.Substring(0, normalizedMethodName.Length - 5);
        }

        return normalizedMethodName.Equals(pathSegments[0], StringComparison.OrdinalIgnoreCase);
    }

    private async Task<object?> InvokeMethod(object service, MethodInfo method, HttpContext context)
    {
        var parameters = method.GetParameters();
        var args = new List<object?>();

        foreach (var param in parameters)
        {
            var fromBodyAttr = param.GetCustomAttribute<FromBodyAttribute>();
            var fromQueryAttr = param.GetCustomAttribute<FromQueryAttribute>();

            if (fromBodyAttr != null)
            {
                var dto = await ReadDtoFromBody(context, param.ParameterType);
                args.Add(dto);
            }
            else if (fromQueryAttr != null || IsSimpleType(param.ParameterType))
            {
                var value = BindParameterFromQueryOrPath(param, context);
                args.Add(value);
            }
            else
            {
                var dto = await ReadDtoFromBody(context, param.ParameterType);
                args.Add(dto);
            }
        }

        var result = method.Invoke(service, args.ToArray());

        if (result is not Task task) return result;
        await task;
        return task.GetType().GetProperty("Result")?.GetValue(task);

    }

    private bool IsSimpleType(Type type)
    {
        return type.IsPrimitive || 
               type == typeof(string) || 
               type == typeof(decimal) ||
               type == typeof(DateTime) ||
               type == typeof(Guid) ||
               Nullable.GetUnderlyingType(type) != null;
    }

    private object? BindParameterFromQueryOrPath(ParameterInfo param, HttpContext context)
    {
        var paramName = param.Name ?? string.Empty;
        
        // Try to get value from path
        var pathValue = GetPathValue(context, paramName);
        if (!string.IsNullOrEmpty(pathValue))
        {
            return ConvertValue(pathValue, param.ParameterType);
        }

        // Try to get value from query string
        var queryValue = context.Request.Query[paramName].FirstOrDefault();
        if (!string.IsNullOrEmpty(queryValue))
        {
            return ConvertValue(queryValue, param.ParameterType);
        }

        // For methods with single parameter, try to get value from path segments
        var segments = context.Request.Path.Value?.Split('/', StringSplitOptions.RemoveEmptyEntries);
        if (segments != null && segments.Length >= 3)
        {
            // For GET /api/task/13, segments = ["api", "task", "13"]
            // For GET /api/task/13/subtasks, segments = ["api", "task", "13", "subtasks"]
            var methodName = segments.Length > 3 ? segments[3] : null;
            var paramValue = segments[2];
            
            // Check if this is a method with single parameter
            var method = param.Member as MethodInfo;
            if (method != null && method.GetParameters().Length == 1)
            {
                return ConvertValue(paramValue, param.ParameterType);
            }
            
            // Special case for GetSubTasksAsync
            if (methodName?.Equals("subtasks", StringComparison.OrdinalIgnoreCase) == true && 
                paramName.Equals("parentTaskId", StringComparison.OrdinalIgnoreCase))
            {
                return ConvertValue(paramValue, param.ParameterType);
            }
        }

        if (param.ParameterType.IsValueType && Nullable.GetUnderlyingType(param.ParameterType) == null)
        {
            throw new ArgumentException($"Parameter '{paramName}' is required");
        }

        return null;
    }

    private object? ConvertValue(string value, Type targetType)
    {
        try
        {
            if (targetType == typeof(string))
                return value;
            
            if (targetType == typeof(int) || targetType == typeof(int?))
                return int.Parse(value);
            
            if (targetType == typeof(long) || targetType == typeof(long?))
                return long.Parse(value);
            
            if (targetType == typeof(bool) || targetType == typeof(bool?))
                return bool.Parse(value);
            
            if (targetType == typeof(decimal) || targetType == typeof(decimal?))
                return decimal.Parse(value);
            
            if (targetType == typeof(DateTime) || targetType == typeof(DateTime?))
                return DateTime.Parse(value);
            
            if (targetType == typeof(Guid) || targetType == typeof(Guid?))
                return Guid.Parse(value);

            return Convert.ChangeType(value, targetType);
        }
        catch
        {
            throw new ArgumentException($"Cannot convert '{value}' to {targetType.Name}");
        }
    }

    private string? GetPathValue(HttpContext context, string? paramName)
    {
        var segments = context.Request.Path.Value?.Split('/', StringSplitOptions.RemoveEmptyEntries);
        if (segments == null || segments.Length < 3)
            return null;

        // Try to find parameter in path segments
        // For GET /api/task/13, segments = ["api", "task", "13"]
        // For GET /api/task/13/subtasks, segments = ["api", "task", "13", "subtasks"]
        if (segments.Length >= 3)
        {
            // First check if paramName is "id" (common case)
            if (paramName?.Equals("id", StringComparison.OrdinalIgnoreCase) == true)
                return segments[2];
            
            // Then check if paramName is "parentTaskId" (for subtasks)
            if (paramName?.Equals("parentTaskId", StringComparison.OrdinalIgnoreCase) == true && segments.Length >= 3)
                return segments[2];
        }

        return null;
    }

    private async Task<object?> ReadDtoFromBody(HttpContext context, Type dtoType)
    {
        var body = await new StreamReader(context.Request.Body).ReadToEndAsync();
        if (string.IsNullOrWhiteSpace(body))
            return null;

        var options = new JsonSerializerOptions
        {
            PropertyNameCaseInsensitive = true,
            Converters = {
                new System.Text.Json.Serialization.JsonStringEnumConverter()
            }
        };

        return JsonSerializer.Deserialize(body, dtoType, options);
    }

    private async Task WriteResponse(HttpContext context, object? result, Exception? exception, string methodName)
    {
        context.Response.ContentType = "application/json";

        var message = methodName switch
        {
            _ when methodName.StartsWith("Get", StringComparison.OrdinalIgnoreCase) => "获取成功",
            _ when methodName.StartsWith("Create", StringComparison.OrdinalIgnoreCase) => "创建成功",
            _ when methodName.StartsWith("Update", StringComparison.OrdinalIgnoreCase) => "更新成功",
            _ when methodName.StartsWith("Delete", StringComparison.OrdinalIgnoreCase) => "删除成功",
            _ when methodName.StartsWith("Toggle", StringComparison.OrdinalIgnoreCase) => "操作成功",
            _ => "操作成功"
        };

        var errorMessage = methodName switch
        {
            _ when methodName.StartsWith("Get", StringComparison.OrdinalIgnoreCase) => "获取失败",
            _ when methodName.StartsWith("Create", StringComparison.OrdinalIgnoreCase) => "创建失败",
            _ when methodName.StartsWith("Update", StringComparison.OrdinalIgnoreCase) => "更新失败",
            _ when methodName.StartsWith("Delete", StringComparison.OrdinalIgnoreCase) => "删除失败",
            _ when methodName.StartsWith("Toggle", StringComparison.OrdinalIgnoreCase) => "操作失败",
            _ => "操作失败"
        };

        // Get friendly error message
        var errors = new List<string>();
        if (exception != null)
        {
            if (exception is Microsoft.EntityFrameworkCore.DbUpdateException dbEx)
            {
                var innerMessage = dbEx.InnerException?.Message ?? dbEx.Message;
                if (innerMessage.Contains("no such table", StringComparison.OrdinalIgnoreCase))
                {
                    errors.Add("数据库表不存在，请尝试重启应用或重新初始化数据库。");
                }
                else if (innerMessage.Contains("UNIQUE constraint failed", StringComparison.OrdinalIgnoreCase))
                {
                    errors.Add("该项已存在，请检查是否重复。");
                }
                else
                {
                    errors.Add($"数据库操作失败: {innerMessage}");
                }
            }
            else
            {
                errors.Add(exception.Message);
            }
        }

        var response = new
        {
            Success = exception == null,
            Data = result,
            Message = exception == null ? message : errorMessage,
            Errors = errors.Count > 0 ? errors : null
        };

        var json = JsonSerializer.Serialize(response, new JsonSerializerOptions
        {
            PropertyNamingPolicy = JsonNamingPolicy.CamelCase
        });

        context.Response.StatusCode = exception switch
        {
            KeyNotFoundException => 404,
            ArgumentException => 400,
            _ => 200
        };

        await context.Response.WriteAsync(json);
    }
}
