using System.Reflection;
using System.Text.Json;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using JsonRpcEndpointSample.Rpc.Abstractions;

namespace JsonRpcEndpointSample.Rpc.Discovery;

public static class RpcServiceCollectionExtensions
{
    public static IServiceCollection AddMcpTools(
        this IServiceCollection services,
        IConfiguration configuration)
    {
        McpToolsOptions options = new McpToolsOptions();
        options.PluginFolder = configuration["McpTools:PluginFolder"];

        return services.AddMcpTools(options);
    }

    public static IServiceCollection AddMcpTools(
        this IServiceCollection services,
        Action<McpToolsOptions> configureOptions)
    {
        McpToolsOptions options = new McpToolsOptions();
        configureOptions(options);

        return services.AddMcpTools(options);
    }

    public static IServiceCollection AddMcpTools(
        this IServiceCollection services,
        McpToolsOptions options)
    {
        List<Assembly> assemblies = LoadRpcAssemblies(options);
        return services.AddRpcHandlersFromAssemblies(assemblies.ToArray());
    }

    public static IServiceCollection AddRpcHandlersFromAssemblies(
        this IServiceCollection services,
        params Assembly[] assemblies)
    {
        RpcJsonSchemaGenerator schemaGenerator = new RpcJsonSchemaGenerator();

        foreach (Assembly assembly in assemblies)
        {
            if (!IsRpcAssembly(assembly))
            {
                continue;
            }

            Type[] types = assembly.GetTypes();

            foreach (Type type in types)
            {
                if (!IsConcreteRpcHandler(type))
                {
                    continue;
                }

                RpcMethodAttribute? methodAttribute = type.GetCustomAttribute<RpcMethodAttribute>();
                if (methodAttribute == null)
                {
                    throw new InvalidOperationException(
                        "RPC handler '" + type.FullName + "' is missing RpcMethodAttribute.");
                }

                RpcHandlerTypes handlerTypes = GetHandlerTypes(type);

                services.AddKeyedTransient(typeof(IRpcRequestHandler), methodAttribute.Method, type);

                RpcMethodDescriptor methodDescriptor = CreateMethodDescriptor(
                    methodAttribute,
                    handlerTypes.RequestType,
                    handlerTypes.ResponseType);

                services.AddSingleton(methodDescriptor);

                ExposeAsMcpToolAttribute? exposeAsMcpToolAttribute =
                    type.GetCustomAttribute<ExposeAsMcpToolAttribute>();

                if (exposeAsMcpToolAttribute != null)
                {
                    RpcToolDescriptor toolDescriptor = CreateToolDescriptor(
                        methodAttribute,
                        handlerTypes.RequestType,
                        handlerTypes.ResponseType,
                        schemaGenerator,
                        methodDescriptor.ExampleRequestJson);

                    services.AddSingleton(toolDescriptor);
                }
            }
        }

        return services;
    }

    private static List<Assembly> LoadRpcAssemblies(McpToolsOptions options)
    {
        List<Assembly> assemblies = new List<Assembly>();

        if (string.IsNullOrWhiteSpace(options.PluginFolder))
        {
            Assembly[] loadedAssemblies = AppDomain.CurrentDomain.GetAssemblies();
            foreach (Assembly assembly in loadedAssemblies)
            {
                if (IsRpcAssembly(assembly))
                {
                    assemblies.Add(assembly);
                }
            }

            return assemblies;
        }

        string pluginFolder = GetAbsolutePluginFolder(options.PluginFolder);
        if (!Directory.Exists(pluginFolder))
        {
            throw new DirectoryNotFoundException("MCP tools plugin folder was not found: " + pluginFolder);
        }

        string[] assemblyFiles = Directory.GetFiles(pluginFolder, "*.dll", SearchOption.TopDirectoryOnly);
        foreach (string assemblyFile in assemblyFiles)
        {
            Assembly assembly = Assembly.LoadFrom(assemblyFile);
            if (IsRpcAssembly(assembly))
            {
                assemblies.Add(assembly);
            }
        }

        return assemblies;
    }

    private static string GetAbsolutePluginFolder(string pluginFolder)
    {
        if (Path.IsPathRooted(pluginFolder))
        {
            return pluginFolder;
        }

        string baseDirectory = AppContext.BaseDirectory;
        return Path.GetFullPath(Path.Combine(baseDirectory, pluginFolder));
    }

    private static bool IsRpcAssembly(Assembly assembly)
    {
        RpcAssemblyAttribute? attribute = assembly.GetCustomAttribute<RpcAssemblyAttribute>();
        return attribute != null;
    }

    private static bool IsConcreteRpcHandler(Type type)
    {
        if (!type.IsClass)
        {
            return false;
        }

        if (type.IsAbstract)
        {
            return false;
        }

        return typeof(IRpcRequestHandler).IsAssignableFrom(type);
    }

    private static RpcHandlerTypes GetHandlerTypes(Type handlerType)
    {
        Type? currentType = handlerType;

        while (currentType != null)
        {
            if (currentType.IsGenericType)
            {
                Type genericTypeDefinition = currentType.GetGenericTypeDefinition();
                if (genericTypeDefinition == typeof(RpcRequestHandler<,>))
                {
                    Type[] arguments = currentType.GetGenericArguments();
                    return new RpcHandlerTypes(arguments[0], arguments[1]);
                }
            }

            currentType = currentType.BaseType;
        }

        throw new InvalidOperationException(
            "RPC handler '" + handlerType.FullName + "' must inherit RpcRequestHandler<TRequest, TResponse>.");
    }

    private static RpcMethodDescriptor CreateMethodDescriptor(
        RpcMethodAttribute methodAttribute,
        Type requestType,
        Type responseType)
    {
        RpcMethodDescriptor descriptor = new RpcMethodDescriptor();
        descriptor.Method = methodAttribute.Method;
        descriptor.Description = methodAttribute.Description;
        descriptor.RequestType = requestType.Name;
        descriptor.ResponseType = responseType.Name;
        descriptor.ExampleRequestJson = CreateExampleJsonRpcRequest(methodAttribute.Method, requestType);
        return descriptor;
    }

    private static RpcToolDescriptor CreateToolDescriptor(
        RpcMethodAttribute methodAttribute,
        Type requestType,
        Type responseType,
        RpcJsonSchemaGenerator schemaGenerator,
        string exampleJsonRpcRequest)
    {
        RpcToolDescriptor descriptor = new RpcToolDescriptor();
        descriptor.Name = methodAttribute.Method;
        descriptor.Description = methodAttribute.Description;
        descriptor.IsReadOnly = methodAttribute.IsReadOnly;
        descriptor.InvokeVia = "/rpc or /mcp tools/call";
        descriptor.InputSchema = schemaGenerator.CreateSchema(requestType);
        descriptor.OutputSchema = schemaGenerator.CreateSchema(responseType);
        descriptor.ExampleJsonRpcRequest = exampleJsonRpcRequest;
        return descriptor;
    }

    private static string CreateExampleJsonRpcRequest(string method, Type requestType)
    {
        Dictionary<string, object?> requestEnvelope = new Dictionary<string, object?>();
        requestEnvelope.Add("jsonrpc", "2.0");
        requestEnvelope.Add("method", method);
        requestEnvelope.Add("params", CreateExampleObject(requestType));
        requestEnvelope.Add("id", "1");

        return JsonSerializer.Serialize(requestEnvelope);
    }

    private static Dictionary<string, object?> CreateExampleObject(Type type)
    {
        Dictionary<string, object?> example = new Dictionary<string, object?>();
        PropertyInfo[] properties = type.GetProperties(BindingFlags.Instance | BindingFlags.Public);

        foreach (PropertyInfo property in properties)
        {
            example.Add(RpcNameConverter.ToCamelCase(property.Name), CreateExampleValue(property.PropertyType));
        }

        return example;
    }

    private static object? CreateExampleValue(Type type)
    {
        Type actualType = Nullable.GetUnderlyingType(type) ?? type;

        if (actualType == typeof(string))
        {
            return "sample";
        }

        if (actualType == typeof(int) || actualType == typeof(long) || actualType == typeof(short))
        {
            return 1;
        }

        if (actualType == typeof(decimal) || actualType == typeof(double) || actualType == typeof(float))
        {
            return 1.0;
        }

        if (actualType == typeof(bool))
        {
            return true;
        }

        if (actualType == typeof(DateTime))
        {
            return "2026-01-01T00:00:00Z";
        }

        return null;
    }

    private sealed class RpcHandlerTypes
    {
        public RpcHandlerTypes(Type requestType, Type responseType)
        {
            RequestType = requestType;
            ResponseType = responseType;
        }

        public Type RequestType
        {
            get;
        }

        public Type ResponseType
        {
            get;
        }
    }
}
