using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Text;
using Sbroenne.ExcelMcp.Generators.Common;

namespace Sbroenne.ExcelMcp.Generators.Mcp;

/// <summary>
/// Generates MCP Server tool classes from Core [ServiceCategory] interfaces.
/// Discovers interfaces from referenced assemblies (Core) and generates
/// [McpServerToolType] classes with properly typed parameters.
///
/// Key features:
/// - Tool action stays a typed enum for discoverable required actions
/// - Optional [FromString] enum parameters stay strings to avoid nullable-enum schema sentinels
/// - TimeSpan parameters become int (seconds) for JSON compatibility
/// - FileOrValue parameters generate dual parameters (value + file path)
/// - XML docs from Core interfaces become MCP tool descriptions
/// </summary>
[Generator]
public class McpToolGenerator : IIncrementalGenerator
{
    public void Initialize(IncrementalGeneratorInitializationContext context)
    {
        context.RegisterSourceOutput(context.CompilationProvider,
            static (spc, compilation) =>
            {
                var services = DiscoverServices(compilation);
                if (services.Count == 0)
                    return;

                foreach (var info in services)
                {
                    var code = GenerateToolClass(info);
                    spc.AddSource($"McpTool.{info.CategoryPascal}.g.cs", SourceText.From(code, Encoding.UTF8));
                }
            });
    }

    /// <summary>
    /// Discovers [ServiceCategory] interfaces from referenced assemblies and extracts ServiceInfo.
    /// Skips generation for any tool name that already has a manual [McpServerTool] implementation
    /// in the current compilation, preventing duplicate tool registration.
    /// </summary>
    private static List<ServiceInfo> DiscoverServices(Compilation compilation)
    {
        var manualToolNames = DiscoverManualToolNames(compilation);
        var result = new List<ServiceInfo>();

        foreach (var reference in compilation.References)
        {
            if (compilation.GetAssemblyOrModuleSymbol(reference) is not IAssemblySymbol assembly)
                continue;

            foreach (var type in GetAllTypes(assembly.GlobalNamespace))
            {
                if (type.TypeKind != TypeKind.Interface)
                    continue;

                var info = ServiceInfoExtractor.ExtractServiceInfo(type);
                if (info != null && info.HasMcpToolAttribute && !manualToolNames.Contains(info.McpToolName))
                    result.Add(info);
            }
        }

        return result;
    }

    /// <summary>
    /// Discovers tool names that are manually defined in the current compilation via
    /// [McpServerTool(Name = "...")] attributes on methods. Used to avoid generating
    /// tools that would conflict with manual implementations.
    /// </summary>
    private static HashSet<string> DiscoverManualToolNames(Compilation compilation)
    {
        var result = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

        foreach (var type in GetAllTypes(compilation.Assembly.GlobalNamespace))
        {
            foreach (var member in type.GetMembers())
            {
                if (member is not IMethodSymbol method)
                    continue;

                foreach (var attr in method.GetAttributes())
                {
                    if (attr.AttributeClass?.Name != "McpServerToolAttribute")
                        continue;

                    foreach (var namedArg in attr.NamedArguments)
                    {
                        if (namedArg.Key == "Name" && namedArg.Value.Value is string name)
                        {
                            result.Add(name);
                        }
                    }
                }
            }
        }

        return result;
    }

    private static IEnumerable<INamedTypeSymbol> GetAllTypes(INamespaceSymbol ns)
    {
        foreach (var type in ns.GetTypeMembers())
            yield return type;
        foreach (var child in ns.GetNamespaceMembers())
            foreach (var type in GetAllTypes(child))
                yield return type;
    }

    /// <summary>
    /// Generates a complete MCP tool class for a service category.
    /// </summary>
    private static string GenerateToolClass(ServiceInfo info)
    {
        var sb = new StringBuilder();
        var hasProgress = info.Methods.Any(m => m.HasProgressParameter);

        sb.AppendLine("// <auto-generated />");
        sb.AppendLine("// Generator version: nullable-fix-v2");
        sb.AppendLine("#nullable enable");
        sb.AppendLine("#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member");
        sb.AppendLine();
        sb.AppendLine("using System.ComponentModel;");
        sb.AppendLine("using System.Threading;");
        sb.AppendLine("using ModelContextProtocol.Server;");
        sb.AppendLine("using Sbroenne.ExcelMcp.Generated;");
        if (hasProgress)
        {
            sb.AppendLine("using ModelContextProtocol;");
            sb.AppendLine("using Sbroenne.ExcelMcp.ComInterop;");
            sb.AppendLine("using Sbroenne.ExcelMcp.McpServer.Progress;");
        }
        sb.AppendLine();
        sb.AppendLine("namespace Sbroenne.ExcelMcp.McpServer.Tools;");
        sb.AppendLine();

        // Class XML doc
        sb.AppendLine("/// <summary>");
        sb.AppendLine($"/// Generated MCP tool for {info.CategoryPascal} operations.");
        sb.AppendLine("/// </summary>");
        sb.AppendLine("[McpServerToolType]");

        var className = GetClassName(info);
        sb.AppendLine($"public static partial class {className}");
        sb.AppendLine("{");

        // Generate the tool method
        GenerateToolMethod(sb, info, hasProgress);

        sb.AppendLine("}");
        return sb.ToString();
    }

    /// <summary>
    /// Generates the MCP tool method with XML docs, attributes, parameters, and body.
    /// </summary>
    private static void GenerateToolMethod(StringBuilder sb, ServiceInfo info, bool hasProgress)
    {
        var enumTypeName = $"{info.CategoryPascal}Action";

        // Get all exposed parameters (aggregated across methods)
        var exposedParams = ServiceInfoExtractor.GetAllExposedParameters(info);

        // Build the enhanced parameter list for MCP
        var mcpParams = BuildMcpParameters(info, exposedParams);

        // XML doc: interface-level summary
        if (!string.IsNullOrEmpty(info.XmlDocSummary))
        {
            sb.AppendLine("    /// <summary>");
            // Wrap the summary text, respecting line breaks
            foreach (var line in WrapXmlDocLines(info.XmlDocSummary))
            {
                sb.AppendLine($"    /// {line}");
            }
            sb.AppendLine("    /// </summary>");
        }

        // XML doc: parameter descriptions
        sb.AppendLine($"    /// <param name=\"action\">The action to perform</param>");
        if (!info.NoSession)
        {
            sb.AppendLine($"    /// <param name=\"session_id\">Session ID from file 'open' action</param>");
        }
        foreach (var p in mcpParams)
        {
            if (!string.IsNullOrEmpty(p.Description))
            {
                var escapedDesc = EscapeXml(p.Description);
                sb.AppendLine($"    /// <param name=\"{p.Name}\">{escapedDesc}</param>");
            }
        }

        // Attributes
        var title = info.McpToolTitle ?? $"Excel {info.CategoryPascal} Operations";
        var destructive = info.McpToolDestructive ? "true" : "false";
        sb.AppendLine($"    [McpServerTool(Name = \"{info.McpToolName}\", Title = \"{title}\", Destructive = {destructive})]");

        var category = info.McpToolCategory ?? "data";
        sb.AppendLine($"    [McpMeta(\"category\", \"{category}\")]");
        sb.AppendLine($"    [McpMeta(\"requiresSession\", {(!info.NoSession).ToString().ToLower()})]");

        // Method-level [Description] from [McpTool(Description = "...")] attribute.
        // Source generators can't read XML docs from metadata references.
        if (!string.IsNullOrEmpty(info.McpToolDescription))
        {
            var methodDesc = EscapeStringLiteral(info.McpToolDescription);
            sb.AppendLine($"    [Description(\"{methodDesc}\")]");
        }

        // Method signature — non-partial because MCP SDK's XmlToDescriptionGenerator
        // cannot see our generator output to create a matching defining declaration.
        var methodName = GetMethodName(info);
        sb.Append($"    public static string {methodName}(");
        sb.AppendLine();

        // Keep action as a required enum so schema consumers get a strict enum without nullable sentinels.
        sb.AppendLine($"        [Description(\"The action to perform\")] {enumTypeName} action,");

        // Session parameter (if required)
        if (!info.NoSession)
        {
            sb.Append("        [Description(\"Session ID from file 'open' action\")] string session_id");
            sb.Append(",");
            sb.AppendLine();
        }

        // Exposed parameters are always optional at the MCP tool surface because each action
        // uses a different subset. Emit C# optional defaults instead of [DefaultValue] attributes
        // so nullable schema parameters stay optional without SDK-inserted enum sentinels.
        for (int i = 0; i < mcpParams.Count; i++)
        {
            var p = mcpParams[i];
            var defaultExpr = p.DefaultExpression ?? "null";
            if (!string.IsNullOrEmpty(p.Description))
            {
                var desc = EscapeStringLiteral(p.Description);
                sb.Append($"        [Description(\"{desc}\")] {p.McpTypeName} {p.Name} = {defaultExpr}");
            }
            else
            {
                sb.Append($"        {p.McpTypeName} {p.Name} = {defaultExpr}");
            }
            sb.Append(",");
            sb.AppendLine();
        }

        // DI-injected progress parameter (no [Description] — resolved from RequestServiceProvider)
        if (hasProgress)
        {
            sb.AppendLine("        IProgress<ProgressNotificationValue> progress = default!,");
        }
        sb.AppendLine("        CancellationToken cancellationToken = default");
        sb.AppendLine("    )");
        sb.AppendLine("    {");

        // Method body: pre-processing and RouteAction call
        GenerateMethodBody(sb, info, mcpParams, enumTypeName, hasProgress);

        sb.AppendLine("    }");
    }

    /// <summary>
    /// Generates the method body that calls ServiceRegistry.RouteAction.
    /// </summary>
    private static void GenerateMethodBody(StringBuilder sb, ServiceInfo info, List<McpParameter> mcpParams, string enumTypeName, bool hasProgress)
    {
        var registryName = info.CategoryPascal;
        var toolName = info.McpToolName;

        var hasPreProcessing = false;
        foreach (var p in mcpParams)
        {
            if (p.PreProcessingCode != null)
            {
                if (!hasPreProcessing)
                {
                    sb.AppendLine("        // Pre-process parameters");
                    hasPreProcessing = true;
                }
                sb.AppendLine($"        {p.PreProcessingCode}");
            }
        }
        if (hasPreProcessing) sb.AppendLine();

        // Set ambient progress context so DispatchToCore can inject it into Core methods
        if (hasProgress)
        {
            sb.AppendLine("        ProgressContext.Current = new McpProgressAdapter(progress);");
            sb.AppendLine("        try");
            sb.AppendLine("        {");
        }

        var indent = hasProgress ? "            " : "        ";

        sb.AppendLine($"{indent}using var cancellationScope = ExcelToolsBase.PushCancellationToken(cancellationToken);");
        sb.AppendLine();

        sb.AppendLine($"{indent}return ExcelToolsBase.ExecuteToolAction(");
        sb.AppendLine($"{indent}    \"{toolName}\",");
        sb.AppendLine($"{indent}    ServiceRegistry.{registryName}.ToActionString(action),");
        sb.AppendLine($"{indent}    () => ServiceRegistry.{registryName}.RouteAction(");
        sb.AppendLine($"{indent}        action,");

        if (!info.NoSession)
        {
            sb.AppendLine($"{indent}        session_id,");
        }
        else
        {
            sb.AppendLine($"{indent}        \"\",");
        }

        sb.AppendLine($"{indent}        ExcelToolsBase.ForwardToServiceFunc,");

        // Named arguments to RouteAction
        for (int i = 0; i < mcpParams.Count; i++)
        {
            var p = mcpParams[i];
            var routeArgName = p.RouteActionParamName;
            var routeArgValue = p.RouteActionValue;
            var comma = i < mcpParams.Count - 1 ? "," : "";
            sb.AppendLine($"{indent}        {routeArgName}: {routeArgValue}{comma}");
        }

        sb.AppendLine($"{indent}    ));");

        // Close progress try/finally block
        if (hasProgress)
        {
            sb.AppendLine("        }");
            sb.AppendLine("        finally");
            sb.AppendLine("        {");
            sb.AppendLine("            ProgressContext.Current = null;");
            sb.AppendLine("        }");
        }
    }

    /// <summary>
    /// Builds MCP parameter descriptors from the exposed parameters.
    /// Handles type conversions: optional FromString enums stay strings, TimeSpan → int seconds, etc.
    /// </summary>
    private static List<McpParameter> BuildMcpParameters(ServiceInfo info, List<ExposedParameter> exposedParams)
    {
        var result = new List<McpParameter>();

        // Build a lookup of param info by exposed name for type resolution
        var paramInfoByName = new Dictionary<string, ParameterInfo>(StringComparer.OrdinalIgnoreCase);
        foreach (var method in info.Methods)
        {
            foreach (var p in method.Parameters)
            {
                var exposedName = p.ExposedName ?? p.Name;
                if (!paramInfoByName.ContainsKey(exposedName))
                    paramInfoByName[exposedName] = p;
            }
        }

        foreach (var ep in exposedParams)
        {
            paramInfoByName.TryGetValue(ep.Name, out var pInfo);
            var snakeName = StringHelper.ToSnakeCase(ep.Name);
            var defaultExpr = GetDefaultExpression(ep.TypeName, pInfo);

            // Determine MCP type and conversion
            if (pInfo != null && pInfo.IsEnum && pInfo.EnumTypeName != null)
            {
                if (pInfo.IsFromString)
                {
                    // IMPORTANT: keep optional [FromString] enums as strings in MCP.
                    // The MCP SDK emits a nullable sentinel in enum schemas for nullable enum parameters,
                    // which strict clients (for example Gemini) reject. ServiceRegistry already parses the
                    // raw string into the enum, so this preserves behavior and CLI/MCP parity.
                    result.Add(new McpParameter(
                        name: snakeName,
                        mcpTypeName: "string?",
                        routeActionParamName: ep.Name,
                        routeActionValue: snakeName,
                        description: ep.DescriptionWithRequired,
                        defaultExpression: "null",
                        preProcessingCode: null));
                }
                else
                {
                    var localVarName = $"_{ep.Name}Parsed";
                    var parseExpression = $"System.Enum.TryParse<{pInfo.EnumTypeName}>({snakeName}?.Replace(\"-\", string.Empty).Replace(\"_\", string.Empty), ignoreCase: true, out var parsed{StringHelper.ToPascalCase(ep.Name)}) ? parsed{StringHelper.ToPascalCase(ep.Name)} : default";
                    var preProcessingCode = defaultExpr == "null"
                        ? $"var {localVarName} = !string.IsNullOrEmpty({snakeName}) ? ({pInfo.EnumTypeName}?)({parseExpression}) : null;"
                        : $"var {localVarName} = !string.IsNullOrEmpty({snakeName}) ? {parseExpression} : {defaultExpr};";

                    result.Add(new McpParameter(
                        name: snakeName,
                        mcpTypeName: "string?",
                        routeActionParamName: ep.Name,
                        routeActionValue: localVarName,
                        description: ep.DescriptionWithRequired,
                        defaultExpression: "null",
                        preProcessingCode: preProcessingCode));
                }
            }
            else if (ep.TypeName.Contains("TimeSpan"))
            {
                // TimeSpan → int seconds in MCP
                var secondsName = ep.Name.EndsWith("timeout", StringComparison.OrdinalIgnoreCase)
                    ? ep.Name + "Seconds"
                    : ep.Name + "Seconds";
                var snakeSecondsName = StringHelper.ToSnakeCase(secondsName);
                // Use the original name for MCP (the int seconds version)
                var localVarName = $"_{ep.Name}";
                result.Add(new McpParameter(
                    name: snakeSecondsName,
                    mcpTypeName: "int?",
                    routeActionParamName: ep.Name,
                    routeActionValue: localVarName,
                    description: ep.DescriptionWithRequired != null
                        ? ep.DescriptionWithRequired + " (in seconds)"
                        : "Timeout in seconds",
                    defaultExpression: "null",
                    preProcessingCode: $"var {localVarName} = {snakeSecondsName}.HasValue ? System.TimeSpan.FromSeconds({snakeSecondsName}.Value) : (System.TimeSpan?)null;"));
            }
            else if (ep.TypeName.StartsWith("System.Collections.Generic.List<string>") ||
                     ep.TypeName.StartsWith("List<string>"))
            {
                // List<string> → string (JSON array) in MCP, parse via ParseJsonList
                var localVarName = $"_{ep.Name}Parsed";
                result.Add(new McpParameter(
                    name: snakeName,
                    mcpTypeName: "string?",
                    routeActionParamName: ep.Name,
                    routeActionValue: localVarName,
                    description: ep.DescriptionWithRequired != null
                        ? ep.DescriptionWithRequired + " (JSON array, e.g., '[\"value1\",\"value2\"]')"
                        : "JSON array of strings",
                    defaultExpression: "null",
                    preProcessingCode: $"var {localVarName} = Sbroenne.ExcelMcp.Core.Utilities.ParameterTransforms.ParseJsonList({snakeName}, nameof({snakeName}));"));
            }
            else
            {
                // Direct passthrough — type matches between MCP and RouteAction
                var mcpType = ep.TypeName;

                // All exposed params are optional in MCP (not all actions use every param).
                // If the default is null, ensure the type is nullable to match RouteAction.
                if (defaultExpr == "null" && !mcpType.EndsWith("?"))
                    mcpType += "?";

                result.Add(new McpParameter(
                    name: snakeName,
                    mcpTypeName: mcpType,
                    routeActionParamName: ep.Name,
                    routeActionValue: snakeName,
                    description: ep.DescriptionWithRequired,
                    defaultExpression: defaultExpr,
                    preProcessingCode: null));
            }
        }

        return result;
    }

    private static string GetDefaultExpression(string typeName, ParameterInfo? pInfo)
    {
        if (pInfo?.DefaultValue != null)
            return pInfo.DefaultValue;

        // All optional params default to null
        if (typeName.EndsWith("?"))
            return "null";

        // Bool defaults
        if (typeName == "bool")
            return "false";

        // Int defaults
        if (typeName == "int")
            return "0";

        return "null";
    }

    private static string GetClassName(ServiceInfo info)
    {
        // Use CategoryPascal which is already correctly cased
        // e.g., "PowerQuery" → "ExcelPowerQueryTool"
        return $"Excel{info.CategoryPascal}Tool";
    }

    private static string GetMethodName(ServiceInfo info)
    {
        // Use CategoryPascal which is already correctly cased
        // e.g., "PowerQuery" → "ExcelPowerQuery"
        return $"Excel{info.CategoryPascal}";
    }

    private static string[] WrapXmlDocLines(string text)
    {
        // Split on actual newlines, trim each line
        return text.Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries)
            .Select(l => l.Trim())
            .Where(l => l.Length > 0)
            .ToArray();
    }

    private static string EscapeXml(string text)
    {
        return text
            .Replace("&", "&amp;")
            .Replace("<", "&lt;")
            .Replace(">", "&gt;")
            .Replace("\"", "&quot;");
    }

    /// <summary>
    /// Escapes a string for use inside a C# string literal (double-quoted).
    /// </summary>
    private static string EscapeStringLiteral(string text)
    {
        return text
            .Replace("\\", "\\\\")
            .Replace("\"", "\\\"")
            .Replace("\r", "")
            .Replace("\n", " ");
    }

    /// <summary>
    /// Represents a parameter as it appears in the generated MCP tool method.
    /// </summary>
    private sealed class McpParameter
    {
        public string Name { get; }
        public string McpTypeName { get; }
        public string RouteActionParamName { get; }
        public string RouteActionValue { get; }
        public string? Description { get; }
        public string? DefaultExpression { get; }
        public string? PreProcessingCode { get; }

        public McpParameter(string name, string mcpTypeName, string routeActionParamName,
            string routeActionValue, string? description, string? defaultExpression,
            string? preProcessingCode)
        {
            Name = name;
            McpTypeName = mcpTypeName;
            RouteActionParamName = routeActionParamName;
            RouteActionValue = routeActionValue;
            Description = description;
            DefaultExpression = defaultExpression;
            PreProcessingCode = preProcessingCode;
        }
    }
}
