using System;
using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
using System.Collections.Generic;
using System.Linq;
using System.IO;
using System.Reflection;
using GxMcp.Gateway.Routers;

namespace GxMcp.Gateway
{
    public class McpRouter
    {
        public static readonly string ServerVersion = ResolveServerVersion();
        public const string SupportedProtocolVersion = "2025-11-25";

        private static string ResolveServerVersion()
        {
            // Prefer the InformationalVersion (set in the csproj via <InformationalVersion>),
            // fall back to FileVersion, then AssemblyVersion. release.ps1 keeps the csproj
            // in sync with package.json so this surface always matches the published build.
            var asm = Assembly.GetExecutingAssembly();
            try
            {
                var info = asm.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
                if (!string.IsNullOrWhiteSpace(info))
                {
                    int plus = info.IndexOf('+');
                    return plus > 0 ? info.Substring(0, plus) : info;
                }
                var file = asm.GetCustomAttribute<AssemblyFileVersionAttribute>()?.Version;
                if (!string.IsNullOrWhiteSpace(file)) return file;
                var name = asm.GetName().Version;
                if (name != null) return name.ToString(3);
            }
            catch { }
            return "0.0.0";
        }

        private static readonly string[] _objectParts = { "Source", "Rules", "Events", "Variables", "Structure", "Layout", "WebForm", "PatternInstance", "PatternVirtual" };
        private static readonly string[] _analysisIncludes = { "metadata", "variables", "signature", "structure" };
        private static readonly string[] _targetLanguages = { "CSharp", "TypeScript", "Java", "Python" };
        private static readonly string[] _visualSurfaces = { "Layout", "WebForm", "PatternInstance", "PatternVirtual" };
        private static readonly IReadOnlyDictionary<string, PromptDefinition> _promptDefinitions = BuildPromptDefinitions();
        private static readonly string[] _promptNames = _promptDefinitions.Keys.ToArray();
        private static readonly List<IMcpModuleRouter> _routers;
        private static JArray _toolDefinitions = new JArray();
        // PERFORMANCE (G-B3): hot-reload tool_definitions.json without restarting the gateway.
        // The watcher is kept in a static field so it is rooted for the lifetime of the process.
        // Debounced with a System.Threading.Timer because editors (e.g. VS Code) often fire
        // multiple Changed events for a single save.
        private static FileSystemWatcher? _toolDefinitionsWatcher;
        private static System.Threading.Timer? _toolDefinitionsReloadTimer;
        private static readonly object _toolDefinitionsReloadLock = new object();

        private sealed class PromptArgumentDefinition
        {
            public PromptArgumentDefinition(string name, string description, bool required, params string[] allowedValues)
            {
                Name = name;
                Description = description;
                Required = required;
                AllowedValues = allowedValues?.Where(value => !string.IsNullOrWhiteSpace(value)).ToArray() ?? Array.Empty<string>();
            }

            public string Name { get; }
            public string Description { get; }
            public bool Required { get; }
            public string[] AllowedValues { get; }
        }

        private sealed class PromptDefinition
        {
            public PromptDefinition(string name, string description, Func<JObject, string> buildMessage, params PromptArgumentDefinition[] arguments)
            {
                Name = name;
                Description = description;
                BuildMessage = buildMessage;
                Arguments = arguments ?? Array.Empty<PromptArgumentDefinition>();
            }

            public string Name { get; }
            public string Description { get; }
            public PromptArgumentDefinition[] Arguments { get; }
            public Func<JObject, string> BuildMessage { get; }
        }

        static McpRouter()
        {
            _routers = new List<IMcpModuleRouter>
            {
                new SearchRouter(),
                new ObjectRouter(),
                new AnalyzeRouter(),
                new SystemRouter(),
                new OperationsRouter()
            };

            LoadToolDefinitions();
            SetupToolDefinitionsWatcher();
        }

        private static void LoadToolDefinitions()
        {
            try
            {
                string exeDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) ?? "";
                string defPath = Path.Combine(exeDir, "tool_definitions.json");
                if (File.Exists(defPath))
                {
                    string json = File.ReadAllText(defPath);
                    _toolDefinitions = JArray.Parse(json);
                    Program.Log($"[McpRouter] Loaded {_toolDefinitions.Count} tool definitions from JSON.");
                }
                else
                {
                    Program.Log($"[McpRouter] ERROR: tool_definitions.json not found at {defPath}");
                }
            }
            catch (Exception ex)
            {
                Program.Log($"[McpRouter] ERROR loading tool definitions: {ex.Message}");
            }
        }

        private static void SetupToolDefinitionsWatcher()
        {
            try
            {
                string exeDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) ?? "";
                if (string.IsNullOrEmpty(exeDir) || !Directory.Exists(exeDir)) return;

                _toolDefinitionsWatcher = new FileSystemWatcher(exeDir, "tool_definitions.json")
                {
                    NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.Size | NotifyFilters.CreationTime
                };

                FileSystemEventHandler onChange = (_, __) =>
                {
                    // PERFORMANCE (G-B3): coalesce a burst of events into a single reload 500ms later.
                    lock (_toolDefinitionsReloadLock)
                    {
                        _toolDefinitionsReloadTimer?.Dispose();
                        _toolDefinitionsReloadTimer = new System.Threading.Timer(_ =>
                        {
                            try
                            {
                                LoadToolDefinitions();
                            }
                            catch (Exception ex)
                            {
                                Program.Log($"[McpRouter] tool_definitions reload failed: {ex.Message}");
                            }
                        }, null, 500, System.Threading.Timeout.Infinite);
                    }
                };

                _toolDefinitionsWatcher.Changed += onChange;
                _toolDefinitionsWatcher.Created += onChange;
                _toolDefinitionsWatcher.Renamed += (_, e) => onChange(_, e);
                _toolDefinitionsWatcher.EnableRaisingEvents = true;
            }
            catch (Exception ex)
            {
                Program.Log($"[McpRouter] FileSystemWatcher setup failed: {ex.Message}");
            }
        }

        public const string McpAxiSchemaVersion = "mcp-axi/2";

        private static JObject BuildInitializeResponse()
        {
            var removed = new JArray();
            foreach (var kvp in RemovedToolsRegistry.Map)
            {
                removed.Add(new JObject
                {
                    ["name"] = kvp.Key,
                    ["replacedBy"] = kvp.Value.ReplacedBy,
                    ["argHint"] = kvp.Value.ArgHint
                });
            }

            return new JObject
            {
                ["protocolVersion"] = SupportedProtocolVersion,
                ["capabilities"] = new JObject
                {
                    ["prompts"] = new JObject { ["listChanged"] = false },
                    ["tools"] = new JObject { ["listChanged"] = true },
                    ["resources"] = new JObject { ["listChanged"] = true, ["subscribe"] = true },
                    ["completion"] = new JObject()
                },
                ["serverInfo"] = new JObject
                {
                    ["name"] = "genexus-mcp-server",
                    ["version"] = ServerVersion
                },
                ["_meta"] = new JObject
                {
                    ["schemaVersion"] = McpAxiSchemaVersion,
                    ["removedTools"] = removed
                }
            };
        }

        public static object? Handle(JObject request)
        {
            string? method = request["method"]?.ToString();
            switch (method)
            {
                case "initialize":
                    return BuildInitializeResponse();
                case "tools/list":
                    return new { tools = _toolDefinitions };
                case "resources/list":
                    return new
                    {
                        resources = new[]
                        {
                            new { uri = "genexus://kb/index-status", name = "KB Index Status", description = "Current indexing status for the active Knowledge Base." },
                            new { uri = "genexus://kb/health", name = "Gateway Health Report", description = "Health report for the GeneXus MCP worker and gateway." },
                            new { uri = "genexus://kb/agent-playbook", name = "GeneXus Agent Playbook", description = "Recommended MCP workflow to operate this GeneXus server in an agent-native, Git-friendly way." },
                            new { uri = "genexus://kb/llm-playbook", name = "LLM CLI+MCP Playbook", description = "Protocol-first guide for choosing CLI vs MCP, token-efficient calls, and timeout/lifecycle handling." },
                            new { uri = "genexus://objects", name = "GeneXus Objects Index", description = "Browsable index of all objects in the KB." },
                            new { uri = "genexus://attributes", name = "GeneXus Attributes", description = "Browsable list of all attributes." }
                        }
                    };
                case "resources/read":
                    return BuildStaticResourceResponse(request);
                case "resources/templates/list":
                    return new
                    {
                        resourceTemplates = new[]
                        {
                            new
                            {
                                uriTemplate = "genexus://objects/{name}/part/{part}",
                                name = "GeneXus Object Part",
                                description = "Read a specific part of a GeneXus object such as Source, Rules, Events, Variables, Structure, or Layout."
                            },
                            new
                            {
                                uriTemplate = "genexus://objects/{name}/variables",
                                name = "GeneXus Object Variables",
                                description = "Read the variable declarations for a GeneXus object."
                            },
                            new
                            {
                                uriTemplate = "genexus://objects/{name}/navigation",
                                name = "GeneXus Navigation",
                                description = "Read the navigation analysis for a GeneXus object."
                            },
                            new
                            {
                                uriTemplate = "genexus://objects/{name}/hierarchy",
                                name = "GeneXus Hierarchy",
                                description = "Read the dependency hierarchy for a GeneXus object."
                            },
                            new
                            {
                                uriTemplate = "genexus://objects/{name}/data-context",
                                name = "GeneXus Data Context",
                                description = "Read attributes, variables, and inferred data context for a GeneXus object."
                            },
                            new
                            {
                                uriTemplate = "genexus://objects/{name}/ui-context",
                                name = "GeneXus UI Context",
                                description = "Read UI structure and controls for a GeneXus object."
                            },
                            new
                            {
                                uriTemplate = "genexus://objects/{name}/conversion-context",
                                name = "GeneXus Conversion Context",
                                description = "Read consolidated conversion context for a GeneXus object."
                            },
                            new
                            {
                                uriTemplate = "genexus://objects/{name}/pattern-metadata",
                                name = "GeneXus Pattern Metadata",
                                description = "Read pattern metadata detected for a GeneXus object."
                            },
                            new
                            {
                                uriTemplate = "genexus://objects/{name}/summary",
                                name = "GeneXus Object Summary",
                                description = "Read an LLM-oriented summary for a GeneXus object."
                            },
                            new
                            {
                                uriTemplate = "genexus://objects/{name}/indexes",
                                name = "GeneXus Visual Indexes",
                                description = "Read visual indexes for a Transaction or Table."
                            },
                            new
                            {
                                uriTemplate = "genexus://objects/{name}/logic-structure",
                                name = "GeneXus Logic Structure",
                                description = "Read the logical structure for a Transaction or Table."
                            },
                            new
                            {
                                uriTemplate = "genexus://attributes/{name}",
                                name = "GeneXus Attribute Metadata",
                                description = "Read metadata for a specific GeneXus attribute."
                            },
                            new
                            {
                                uriTemplate = "genexus://kb/tool-help/{name}",
                                name = "GeneXus Tool Help",
                                description = "Long-form help for a single MCP tool: prefixes, modes, examples, defaults."
                            }
                        }
                    };
                case "completion/complete":
                    return HandleCompletion(request);
                case "prompts/list":
                    return new { prompts = BuildPromptCatalog() };
                case "prompts/get":
                    return BuildPromptResponse(request);
                case "ping":
                    return new { };
                default:
                    return null;
            }
        }

        private static object HandleCompletion(JObject request)
        {
            var paramsObj = request["params"] as JObject;
            var argument = paramsObj?["argument"] as JObject;
            string argumentName = argument?["name"]?.ToString() ?? "";
            string currentValue = argument?["value"]?.ToString() ?? "";
            string refType = paramsObj?["ref"]?["type"]?.ToString() ?? "";
            string refName = paramsObj?["ref"]?["name"]?.ToString() ?? "";
            string uriTemplate = paramsObj?["ref"]?["uriTemplate"]?.ToString() ?? "";

            IEnumerable<string> values = Enumerable.Empty<string>();

            if (argumentName == "part")
            {
                values = _objectParts;
            }
            else if (argumentName == "language" || argumentName == "targetLanguage")
            {
                values = _targetLanguages;
            }
            else if (argumentName == "include")
            {
                values = _analysisIncludes;
            }
            else if (argumentName == "prompt")
            {
                values = _promptNames;
            }
            else if (refType == "ref/resource")
            {
                if (uriTemplate.Contains("/part/{part}", StringComparison.OrdinalIgnoreCase))
                    values = _objectParts;
                else if (uriTemplate.Contains("/conversion-context", StringComparison.OrdinalIgnoreCase))
                    values = _analysisIncludes;
            }
            else if (refType == "ref/prompt" && TryGetPromptArgumentDefinition(refName, argumentName, out var promptArgument))
            {
                values = promptArgument.AllowedValues;
            }
            else if (refType == "ref/tool")
            {
                if (refName == "genexus_read")
                    values = _objectParts;
                else if (refName == "genexus_inspect")
                    values = _analysisIncludes;
                else if (refName == "genexus_forge")
                    values = _targetLanguages;
                else if (refName == "genexus_lifecycle")
                    values = new[] { "build", "rebuild", "reorg", "validate", "sync", "index", "status", "result" };
                else if (refName == "genexus_properties")
                    values = new[] { "get", "set" };
                else if (refName == "genexus_asset")
                    values = new[] { "find", "read", "write" };
                else if (refName == "genexus_history")
                    values = new[] { "list", "get_source", "save", "restore" };
                else if (refName == "genexus_structure")
                    values = new[] { "get_visual", "update_visual", "get_indexes", "get_logic" };
                else if (refName == "genexus_refactor")
                    values = new[] { "RenameAttribute", "RenameVariable", "RenameObject", "ExtractProcedure" };
                else if (refName == "prompts/get")
                    values = _promptNames;
            }

            var filteredValues = values
                .Where(value => value.StartsWith(currentValue, StringComparison.OrdinalIgnoreCase))
                .Distinct(StringComparer.OrdinalIgnoreCase)
                .Select(value => new { value })
                .ToArray();

            return new
            {
                completion = new
                {
                    values = filteredValues
                }
            };
        }

        private static object[] BuildPromptCatalog()
        {
            return _promptDefinitions.Values
                .Select(prompt => new
                {
                    name = prompt.Name,
                    description = prompt.Description,
                    arguments = prompt.Arguments.Select(argument => new
                    {
                        name = argument.Name,
                        description = argument.Description,
                        required = argument.Required,
                        allowedValues = argument.AllowedValues.Length > 0 ? argument.AllowedValues : null
                    }).ToArray()
                })
                .Cast<object>()
                .ToArray();
        }

        private static object BuildPromptResponse(JObject request)
        {
            var paramsObj = request["params"] as JObject;
            string promptName = paramsObj?["name"]?.ToString() ?? "";
            var args = paramsObj?["arguments"] as JObject ?? new JObject();
            if (!_promptDefinitions.TryGetValue(promptName, out var prompt))
            {
                return new
                {
                    description = "Unknown prompt.",
                    messages = new[]
                    {
                        CreatePromptMessage($"Prompt '{promptName}' is not defined by this server.")
                    }
                };
            }

            string? validationError = ValidatePromptArguments(prompt, args);
            if (!string.IsNullOrWhiteSpace(validationError))
            {
                return new
                {
                    description = "Invalid prompt arguments.",
                    messages = new[]
                    {
                        CreatePromptMessage(validationError)
                    }
                };
            }

            return new
            {
                description = prompt.Description,
                messages = new[]
                {
                    CreatePromptMessage(prompt.BuildMessage(args))
                }
            };
        }

        private static object CreatePromptMessage(string text)
        {
            return new
            {
                role = "user",
                content = new
                {
                    type = "text",
                    text
                }
            };
        }

        private static string BuildExplainObjectPrompt(string name, string part)
        {
            return
                $"Explain the GeneXus object '{name}'. " +
                $"Start from resource 'genexus://objects/{name}/part/{part}', then use 'genexus://objects/{name}/variables', " +
                $"'genexus://objects/{name}/navigation', and 'genexus://objects/{name}/summary'. " +
                "Summarize purpose, data flow, external dependencies, and risky assumptions. " +
                "If important context is missing, say exactly which additional resource should be read next.";
        }

        private static string BuildConvertObjectPrompt(string name, string targetLanguage)
        {
            return
                $"Prepare the GeneXus object '{name}' for conversion to {targetLanguage}. " +
                $"Read 'genexus://objects/{name}/conversion-context', 'genexus://objects/{name}/variables', " +
                $"'genexus://objects/{name}/navigation', and 'genexus://objects/{name}/summary' first. " +
                "Produce: semantic summary, target architecture assumptions, unsupported features, manual review items, and a translation plan. " +
                "Do not invent framework behavior that is not grounded in the retrieved context.";
        }

        private static string BuildReviewTransactionPrompt(string name)
        {
            return
                $"Review the Transaction '{name}'. " +
                $"Read 'genexus://objects/{name}/part/Structure', 'genexus://objects/{name}/part/Rules', " +
                $"'genexus://objects/{name}/data-context', and 'genexus://objects/{name}/summary'. " +
                "Focus on data integrity, inferred business rules, side effects, and migration risks. " +
                "Report findings first, then open questions, then recommended changes.";
        }

        private static string BuildRefactorProcedurePrompt(string name)
        {
            return
                $"Refactor the Procedure '{name}' without changing behavior. " +
                $"Read 'genexus://objects/{name}/part/Source', 'genexus://objects/{name}/variables', " +
                $"'genexus://objects/{name}/navigation', and 'genexus://objects/{name}/summary'. " +
                "Identify duplicated logic, implicit dependencies, and extraction opportunities. " +
                "Return a stepwise refactor plan before proposing code changes.";
        }

        private static string BuildGenerateTestsPrompt(string name)
        {
            return
                $"Generate a test plan for the GeneXus object '{name}'. " +
                $"Ground the analysis in 'genexus://objects/{name}/summary', 'genexus://objects/{name}/variables', " +
                $"'genexus://objects/{name}/navigation', and the primary source part under 'genexus://objects/{name}/part/Source'. " +
                "List normal cases, edge cases, integration dependencies, and regression risks. " +
                "Prefer deterministic assertions over vague behavioral checks.";
        }

        private static string BuildTraceDependenciesPrompt(string name)
        {
            return
                $"Trace dependencies for the GeneXus object '{name}'. " +
                $"Use 'genexus://objects/{name}/hierarchy', 'genexus://objects/{name}/navigation', " +
                $"'genexus://objects/{name}/summary', and if needed 'genexus_query' with 'usedby:{name}'. " +
                "Separate direct dependencies, indirect dependencies, and likely impact zones. " +
                "Call out where the trace is inferred versus explicitly grounded in retrieved data.";
        }

        private static string BuildAgentShipChangePrompt(string goal, string objectName, string part)
        {
            string normalizedPart = string.IsNullOrWhiteSpace(part) ? "Source" : part;
            string objectSpecificGuidance = string.IsNullOrWhiteSpace(objectName)
                ? "Start with `genexus_query` and the KB-level resources to identify the smallest object set involved before editing anything. "
                : $"Treat '{objectName}' as the primary object. Read 'genexus://objects/{objectName}/summary', 'genexus://objects/{objectName}/part/{normalizedPart}', 'genexus://objects/{objectName}/variables', and 'genexus://objects/{objectName}/hierarchy' before proposing edits. ";

            return
                $"Execute a controlled GeneXus change with the goal '{goal}'. " +
                "Start by reading 'genexus://kb/agent-playbook'. " +
                objectSpecificGuidance +
                "Use MCP discovery instead of hardcoded assumptions, keep the blast radius explicit, and prefer the smallest reversible change set. " +
                "If editing is required, re-read the exact target before mutation, persist the change, then verify with a re-read plus the appropriate lifecycle command (`validate`, `build`, or `test`). " +
                "Finish with a Git-ready change summary listing modified objects, verification evidence, and open risks.";
        }

        private static string BuildVisualChangePrompt(string name, string changeGoal, string preferredSurface)
        {
            string normalizedSurface = string.IsNullOrWhiteSpace(preferredSurface) ? "PatternInstance" : preferredSurface;
            return
                $"Plan and validate a GeneXus visual metadata change for '{name}' with the goal '{changeGoal}'. " +
                "Start by reading 'genexus://kb/agent-playbook'. " +
                $"Inspect 'genexus://objects/{name}/ui-context', 'genexus://objects/{name}/pattern-metadata', and 'genexus://objects/{name}/part/{normalizedSurface}' first. " +
                "Determine the authoritative surface before editing: base layout, raw WebForm metadata, or pattern-owned metadata. " +
                "If assets are involved, inspect `genexus_asset` metadata before changing any binary file. " +
                "After the write, re-read the exact same surface and report whether persistence is confirmed or still blocked.";
        }

        private static string BuildBootstrapLlmPrompt(string goal)
        {
            string goalHint = string.IsNullOrWhiteSpace(goal)
                ? "If the user goal is unknown, ask one concise clarifying question before editing."
                : $"User goal: '{goal}'. Prioritize next calls for this goal.";

            return
                "Bootstrap this GeneXus MCP session in protocol-first mode. " +
                "Start with discovery (`tools/list`, `resources/list`, `prompts/list`). " +
                "Read `genexus://kb/llm-playbook` and summarize: when to use AXI CLI vs MCP, pagination/field-shaping defaults, and timeout follow-up via `genexus_lifecycle(op:<operationId>)`. " +
                $"{goalHint} " +
                "Then propose the next 3 deterministic calls with explicit arguments.";
        }

        private static IReadOnlyDictionary<string, PromptDefinition> BuildPromptDefinitions()
        {
            var prompts = new[]
            {
                new PromptDefinition(
                    "gx_bootstrap_llm",
                    "Bootstrap an LLM session with protocol-first CLI+MCP usage guidance.",
                    args => BuildBootstrapLlmPrompt(args["goal"]?.ToString() ?? string.Empty),
                    new PromptArgumentDefinition("goal", "Optional current user objective to prioritize the next MCP calls.", false)),
                new PromptDefinition(
                    "gx_explain_object",
                    "Explain a GeneXus object using source, variables, navigation, and summary context.",
                    args => BuildExplainObjectPrompt(
                        args["name"]?.ToString() ?? string.Empty,
                        args["part"]?.ToString() ?? "Source"),
                    new PromptArgumentDefinition("name", "GeneXus object name.", true),
                    new PromptArgumentDefinition("part", "Primary part to emphasize during the explanation.", false, _objectParts)),
                new PromptDefinition(
                    "gx_convert_object",
                    "Prepare a GeneXus object for conversion to another language using conversion context and target-specific guidance.",
                    args => BuildConvertObjectPrompt(
                        args["name"]?.ToString() ?? string.Empty,
                        args["targetLanguage"]?.ToString() ?? "CSharp"),
                    new PromptArgumentDefinition("name", "GeneXus object name.", true),
                    new PromptArgumentDefinition("targetLanguage", "Target language for conversion.", true, _targetLanguages)),
                new PromptDefinition(
                    "gx_review_transaction",
                    "Review a Transaction object with focus on structure, rules, and generated impact.",
                    args => BuildReviewTransactionPrompt(args["name"]?.ToString() ?? string.Empty),
                    new PromptArgumentDefinition("name", "Transaction object name.", true)),
                new PromptDefinition(
                    "gx_refactor_procedure",
                    "Refactor a Procedure with attention to readability, side effects, and migration safety.",
                    args => BuildRefactorProcedurePrompt(args["name"]?.ToString() ?? string.Empty),
                    new PromptArgumentDefinition("name", "Procedure object name.", true)),
                new PromptDefinition(
                    "gx_generate_tests",
                    "Generate a test plan from source, variables, navigation, and business context.",
                    args => BuildGenerateTestsPrompt(args["name"]?.ToString() ?? string.Empty),
                    new PromptArgumentDefinition("name", "GeneXus object name.", true)),
                new PromptDefinition(
                    "gx_trace_dependencies",
                    "Trace upstream and downstream dependencies for a GeneXus object.",
                    args => BuildTraceDependenciesPrompt(args["name"]?.ToString() ?? string.Empty),
                    new PromptArgumentDefinition("name", "GeneXus object name.", true)),
                new PromptDefinition(
                    "gx_agent_ship_change",
                    "Guide an agent through a controlled GeneXus change with MCP discovery, verification, and Git-ready reporting.",
                    args => BuildAgentShipChangePrompt(
                        args["goal"]?.ToString() ?? string.Empty,
                        args["objectName"]?.ToString() ?? string.Empty,
                        args["part"]?.ToString() ?? "Source"),
                    new PromptArgumentDefinition("goal", "User-visible outcome or change objective.", true),
                    new PromptArgumentDefinition("objectName", "Primary GeneXus object when the scope is already known.", false),
                    new PromptArgumentDefinition("part", "Primary part to inspect first when an object is known.", false, _objectParts)),
                new PromptDefinition(
                    "gx_agent_visual_change",
                    "Guide an agent through a visual metadata change while resolving the authoritative GeneXus surface first.",
                    args => BuildVisualChangePrompt(
                        args["name"]?.ToString() ?? string.Empty,
                        args["changeGoal"]?.ToString() ?? string.Empty,
                        args["preferredSurface"]?.ToString() ?? "PatternInstance"),
                    new PromptArgumentDefinition("name", "GeneXus object name.", true),
                    new PromptArgumentDefinition("changeGoal", "Requested UI or metadata change.", true),
                    new PromptArgumentDefinition("preferredSurface", "Best initial guess for the authoritative editable surface.", false, _visualSurfaces))
            };

            return prompts.ToDictionary(prompt => prompt.Name, StringComparer.Ordinal);
        }

        private static string? ValidatePromptArguments(PromptDefinition prompt, JObject args)
        {
            foreach (var argument in prompt.Arguments)
            {
                string value = args[argument.Name]?.ToString() ?? string.Empty;
                if (argument.Required && string.IsNullOrWhiteSpace(value))
                {
                    return $"Missing required argument '{argument.Name}' for prompt '{prompt.Name}'.";
                }

                if (!string.IsNullOrWhiteSpace(value) &&
                    argument.AllowedValues.Length > 0 &&
                    !argument.AllowedValues.Contains(value, StringComparer.OrdinalIgnoreCase))
                {
                    return $"Invalid value '{value}' for argument '{argument.Name}' in prompt '{prompt.Name}'. Allowed values: {string.Join(", ", argument.AllowedValues)}.";
                }
            }

            return null;
        }

        private static bool TryGetPromptArgumentDefinition(string promptName, string argumentName, out PromptArgumentDefinition argument)
        {
            argument = null!;
            if (!_promptDefinitions.TryGetValue(promptName, out var prompt))
            {
                return false;
            }

            var found = prompt.Arguments.FirstOrDefault(candidate =>
                string.Equals(candidate.Name, argumentName, StringComparison.OrdinalIgnoreCase));

            if (found == null)
            {
                return false;
            }

            argument = found;
            return true;
        }

        private static object? BuildStaticResourceResponse(JObject request)
        {
            string uri = request["params"]?["uri"]?.ToString() ?? string.Empty;

            if (string.Equals(uri, "genexus://kb/health", StringComparison.OrdinalIgnoreCase))
            {
                return new
                {
                    contents = new[]
                    {
                        new
                        {
                            uri = "genexus://kb/health",
                            mimeType = "text/markdown",
                            text = BuildHealthReport()
                        }
                    }
                };
            }

            if (string.Equals(uri, "genexus://kb/agent-playbook", StringComparison.OrdinalIgnoreCase))
            {
                return new
                {
                    contents = new[]
                    {
                        new
                        {
                            uri = "genexus://kb/agent-playbook",
                            mimeType = "text/markdown",
                            text = BuildAgentPlaybook()
                        }
                    }
                };
            }

            if (string.Equals(uri, "genexus://kb/llm-playbook", StringComparison.OrdinalIgnoreCase))
            {
                return new
                {
                    contents = new[]
                    {
                        new
                        {
                            uri = "genexus://kb/llm-playbook",
                            mimeType = "text/markdown",
                            text = BuildLlmCliMcpPlaybook()
                        }
                    }
                };
            }

            const string toolHelpPrefix = "genexus://kb/tool-help/";
            if (uri.StartsWith(toolHelpPrefix, StringComparison.OrdinalIgnoreCase))
            {
                string toolName = uri.Substring(toolHelpPrefix.Length);
                string? text = ToolHelpCatalog.Get(toolName);
                if (text == null) return null;

                return new
                {
                    contents = new[]
                    {
                        new
                        {
                            uri,
                            mimeType = "text/markdown",
                            text
                        }
                    }
                };
            }

            return null;
        }

        private static string BuildHealthReport()
        {
            var sb = new System.Text.StringBuilder();
            sb.AppendLine("# Gateway Health Report");
            sb.AppendLine();
            sb.AppendLine("## Latency");

            var pool = Program.GetWorkerPool();
            var aliases = pool?.GetKnownAliases() ?? System.Array.Empty<string>();

            sb.AppendLine("| KB | spawnMs samples | spawnMs p50 | spawnMs p95 | spawnMs lastMs | sdkInitMs lastMs |");
            sb.AppendLine("|---|---|---|---|---|---|");

            if (aliases.Count == 0)
            {
                sb.AppendLine("| _(no KBs tracked)_ | — | — | — | — | — |");
            }
            else
            {
                foreach (var alias in aliases)
                {
                    var (count, p50, p95) = Program.OperationTracker.GetSpawnStats(alias);
                    var worker = pool?.TryGetWorker(alias);
                    var lastSpawn = worker?.SpawnMs?.ToString() ?? "n/a";
                    var lastSdkInit = worker?.SdkInitMs?.ToString() ?? "n/a";
                    sb.AppendLine($"| {alias} | {count} | {p50:0.#} | {p95:0.#} | {lastSpawn} | {lastSdkInit} |");
                }
            }

            return sb.ToString();
        }

        private static string BuildAgentPlaybook()
        {
            return
                "# GeneXus Agent Playbook\n\n" +
                "Use this server in an agent-native way:\n" +
                "1. Start with MCP discovery (`tools/list`, `resources/list`, `resources/templates/list`, `prompts/list`).\n" +
                "2. Prefer resources for read-only grounding and use tool calls only for mutations or deeper analysis.\n" +
                "3. Keep GeneXus artifacts reviewable and Git-friendly: small diffs, explicit blast radius, and post-write verification.\n" +
                "4. For code or metadata changes, re-read before editing, write once, then confirm persistence with a second read.\n" +
                "5. Close the loop with the relevant lifecycle action (`validate`, `build`, `test`, or `index`) instead of stopping at a successful write.\n" +
                "6. When the authoritative surface is unclear, inspect summary, hierarchy, ui-context, pattern metadata, and visual parts before mutating anything.\n" +
                "7. Treat assets and visual metadata as first-class artifacts: inspect metadata first, then opt into heavy content only when necessary.\n\n" +
                "Current server strengths:\n" +
                "- MCP-first gateway and discovery\n" +
                "- Source, metadata, pattern, and asset operations\n" +
                "- Prompt and completion support\n\n" +
                "Current caution points:\n" +
                "- Some visual metadata flows still require practical persistence confirmation.\n" +
                "- Extension lint warnings are legacy debt; runtime validation is stronger than stylistic cleanliness today.\n" +
                "- Prompt flows are grounded, but the agent must still choose the smallest safe change set.";
        }

        private static string BuildLlmCliMcpPlaybook()
        {
            return
                "# LLM CLI+MCP Playbook\n\n" +
                "Use this server with protocol-first rules:\n" +
                "1. Use AXI CLI for bootstrap and environment checks (`home`, `status`, `doctor --mcp-smoke`, `tools list`, `config show`).\n" +
                "2. Use MCP tools for KB operations (`genexus_query`, `genexus_list_objects`, `genexus_read`, `genexus_edit`, `genexus_lifecycle`).\n" +
                "3. For list/read operations, always set `limit`/`offset`; prefer narrow, paginated requests.\n" +
                "4. For `genexus_query` and `genexus_list_objects`, use `fields` or `axiCompact=true` to reduce tokens.\n" +
                "5. Parse MCP tool payload from `result.content[0].text` as JSON.\n" +
                "6. Expect additive metadata: `meta.schemaVersion=mcp-axi/2`, `meta.tool`, plus collection helpers (`returned`, `total`, `empty`, `hasMore`, `nextOffset`) when inferable.\n" +
                "7. If `result.isError=true` and `operationId` is present, treat as running operation and poll `genexus_lifecycle(action='status'|'result', target='op:<operationId>')`.\n" +
                "8. For safe mutation flows, use patch `dryRun` first, then apply and re-read for persistence confirmation.\n\n" +
                "Recommended bootstrap sequence:\n" +
                "- `tools/list`\n" +
                "- `resources/list`\n" +
                "- `prompts/list`\n" +
                "- `resources/read` for `genexus://kb/llm-playbook`";
        }

        public static object? ConvertResourceCall(JObject request)
        {
            string uri = request["params"]?["uri"]?.ToString() ?? "";
            if (string.IsNullOrEmpty(uri)) return null;

            if (uri == "genexus://kb/index-status") return new { module = "KB", action = "GetIndexStatus" };
            if (uri == "genexus://kb/health") return new { module = "Health", action = "GetReport" };
            if (uri == "genexus://objects") return new { module = "Search", action = "Query", target = "", limit = 200 };
            if (uri == "genexus://attributes") return new { module = "Search", action = "Query", target = "type:Attribute", limit = 200 };

            if (TryReadObjectResource(uri, out var objectResource))
                return objectResource;

            if (uri.StartsWith("genexus://attributes/", StringComparison.OrdinalIgnoreCase))
            {
                string name = uri.Replace("genexus://attributes/", "");
                return new { module = "Read", action = "GetAttribute", target = name };
            }

            return null;
        }

        private static bool TryReadObjectResource(string uri, out object? resourceCall)
        {
            resourceCall = null;
            const string objectPrefix = "genexus://objects/";
            if (!uri.StartsWith(objectPrefix, StringComparison.OrdinalIgnoreCase)) return false;

            string relativePath = uri.Substring(objectPrefix.Length).Trim('/');
            if (string.IsNullOrWhiteSpace(relativePath)) return false;

            string[] segments = relativePath.Split('/', StringSplitOptions.RemoveEmptyEntries);
            if (segments.Length == 0) return false;

            string name = segments[0];
            if (string.IsNullOrWhiteSpace(name)) return false;

            if (segments.Length == 1)
            {
                resourceCall = new { module = "Read", action = "ExtractSource", target = name, part = "Source" };
                return true;
            }

            string resourceKind = segments[1];
            switch (resourceKind.ToLowerInvariant())
            {
                case "part":
                    string part = segments.Length >= 3 ? segments[2] : "Source";
                    resourceCall = new { module = "Read", action = "ExtractSource", target = name, part };
                    return true;
                case "source":
                    resourceCall = new { module = "Read", action = "ExtractSource", target = name, part = "Source" };
                    return true;
                case "variables":
                    resourceCall = new { module = "Read", action = "GetVariables", target = name };
                    return true;
                case "navigation":
                    resourceCall = new { module = "Analyze", action = "GetNavigation", target = name };
                    return true;
                case "hierarchy":
                    resourceCall = new { module = "Analyze", action = "GetHierarchy", target = name };
                    return true;
                case "data-context":
                    resourceCall = new { module = "Analyze", action = "GetDataContext", target = name };
                    return true;
                case "ui-context":
                    resourceCall = new { module = "UI", action = "GetUIContext", target = name };
                    return true;
                case "conversion-context":
                    resourceCall = new { module = "Analyze", action = "GetConversionContext", target = name };
                    return true;
                case "pattern-metadata":
                    resourceCall = new { module = "Analyze", action = "GetPatternMetadata", target = name };
                    return true;
                case "summary":
                    resourceCall = new { module = "Analyze", action = "Summarize", target = name };
                    return true;
                case "indexes":
                    resourceCall = new { module = "Structure", action = "GetVisualIndexes", target = name };
                    return true;
                case "logic-structure":
                    resourceCall = new { module = "Structure", action = "GetLogicStructure", target = name };
                    return true;
                default:
                    return false;
            }
        }

        public static object? ConvertToolCall(JObject request)
        {
            string? method = request["method"]?.ToString();
            if (method != "tools/call") return null;

            var paramsObj = request["params"] as JObject;
            string? toolName = paramsObj?["name"]?.ToString();
            var args = paramsObj?["arguments"] as JObject;

            if (string.IsNullOrEmpty(toolName)) return null;

            foreach (var router in _routers)
            {
                var converted = router.ConvertToolCall(toolName, args);
                if (converted != null) return converted;
            }

            return null;
        }

        internal static void StripNulls(JObject obj)
        {
            var toRemove = new List<string>();
            foreach (var prop in obj.Properties())
            {
                if (prop.Value is null || prop.Value.Type == JTokenType.Null)
                    toRemove.Add(prop.Name);
                else if (prop.Value is JObject child)
                    StripNulls(child);
                else if (prop.Value is JArray arr)
                    foreach (var item in arr) if (item is JObject o) StripNulls(o);
            }
            foreach (var name in toRemove) obj.Remove(name);
        }

        /// <summary>
        /// Attaches a <c>_meta.background_jobs</c> snapshot to <paramref name="toolResult"/> when the session has
        /// running or unseen-completed jobs in <paramref name="registry"/>. Marks completed jobs as seen so they
        /// surface exactly once. No-ops when the snapshot is empty.
        /// </summary>
        internal static void PiggybackJobs(JObject toolResult, string sessionId, BackgroundJobRegistry registry)
        {
            var snapshot = registry.SnapshotForSession(sessionId);
            if (snapshot.Count == 0) return;

            var jobsArr = new JArray(snapshot.Select(j => new JObject
            {
                ["id"] = j.Id,
                ["status"] = j.Status,
                ["summary"] = j.Summary,
                ["completed_at"] = j.CompletedAt?.ToString("o"),
                ["estimated_seconds"] = j.EstimatedSeconds
            }));

            // The LLM reads content[0].text (a serialized JSON string), not the wrapper JObject.
            var content = toolResult["content"] as JArray;
            var first = content?[0] as JObject;
            var textToken = first?["text"];
            if (textToken != null)
            {
                JObject? inner;
                try { inner = JObject.Parse(textToken.ToString()); }
                catch { return; /* non-JSON text payload — leave alone */ }
                var meta = (JObject?)inner["_meta"] ?? new JObject();
                meta["background_jobs"] = jobsArr;
                inner["_meta"] = meta;
                first["text"] = inner.ToString(Newtonsoft.Json.Formatting.None);
            }
            else
            {
                // Error envelopes have no content array — attach _meta to the result root so jobs still surface.
                var meta = (JObject?)toolResult["_meta"] ?? new JObject();
                meta["background_jobs"] = jobsArr;
                toolResult["_meta"] = meta;
            }
            registry.MarkSeen(sessionId, snapshot.Select(j => j.Id));
        }

        /// <summary>
        /// Resolves a background-job ID from lifecycle tool arguments.
        /// Tries <c>job_id</c>, then <c>jobId</c>, then <c>target</c> (the lifecycle tool's
        /// conventional parameter), returning the first non-empty value found.
        /// </summary>
        internal static string? ResolveJobId(JObject? args)
        {
            var v = args?["job_id"]?.ToString();
            if (!string.IsNullOrWhiteSpace(v)) return StripOpPrefix(v);
            v = args?["jobId"]?.ToString();
            if (!string.IsNullOrWhiteSpace(v)) return StripOpPrefix(v);
            v = args?["target"]?.ToString();
            if (!string.IsNullOrWhiteSpace(v)) return StripOpPrefix(v);
            return null;
        }

        // v2.6.2 (Item B follow-up): callers pass `target=op:<jobId>` to lifecycle cancel,
        // but JobRegistry keys are the raw GUID. Without stripping here, Cancel falls
        // through to the OperationTracker path and returns NotFound even when the job
        // is registered.
        private static string StripOpPrefix(string s)
        {
            if (s == null) return null;
            return s.StartsWith("op:", StringComparison.OrdinalIgnoreCase) ? s.Substring(3) : s;
        }

        /// <summary>
        /// Returns a terse error envelope containing only <c>message</c>, <c>code</c>, and <c>hint</c>.
        /// Stack traces and full SDK diagnostics are dropped by default. Pass <paramref name="verbose"/> = true
        /// (via the <c>verbose_errors</c> tool argument) to get the full original envelope.
        /// </summary>
        internal static JObject TrimErrorEnvelope(JObject error, bool verbose)
        {
            if (verbose) return error; // pass-through
            var trimmed = new JObject();
            // first line of message only
            var msg = error["message"]?.ToString() ?? error["error"]?.ToString() ?? "Unknown error";
            var firstLine = msg.Split('\n')[0].Trim();
            trimmed["message"] = firstLine;
            if (error["code"] != null) trimmed["code"] = error["code"];
            if (error["hint"] != null) trimmed["hint"] = error["hint"];
            return trimmed;
        }

        // FR#10 (friction-report 2026-05-14): builds of 50-70s would force 3 polls at the
        // old 25s cap, each consuming a turn + the full Output blob. 90s lets a single
        // long-poll cover most builds in one round-trip.
        public const int MaxLongPollSeconds = 90;

        /// <summary>
        /// Long-polls <paramref name="registry"/> for <paramref name="jobId"/> until it reaches a terminal
        /// state or <paramref name="waitSeconds"/> elapses (clamped 0–<see cref="MaxLongPollSeconds"/>).
        /// Returns a status envelope.
        /// <list type="bullet">
        ///   <item><c>wait_seconds=0</c> (or omitted) → immediate single poll, no blocking.</item>
        ///   <item>Unknown job → envelope with <c>error="unknown_job_id"</c>.</item>
        ///   <item>Terminal job → returns immediately regardless of <paramref name="waitSeconds"/>.</item>
        /// </list>
        /// </summary>
        internal static async Task<JObject> LongPollJob(
            BackgroundJobRegistry registry,
            string jobId,
            int waitSeconds)
        {
            // Clamp wait_seconds to [0, MaxLongPollSeconds]
            waitSeconds = Math.Min(Math.Max(waitSeconds, 0), MaxLongPollSeconds);

            var deadline = DateTime.UtcNow.AddSeconds(waitSeconds);
            JobEntry? job;

            do
            {
                job = registry.Get(jobId);
                if (job == null || job.Status != "running" || waitSeconds == 0)
                    break;
                await Task.Delay(250).ConfigureAwait(false);
            }
            while (DateTime.UtcNow < deadline);

            if (job == null)
            {
                return new JObject
                {
                    ["error"] = "unknown_job_id",
                    ["job_id"] = jobId
                };
            }

            return new JObject
            {
                ["job_id"] = job.Id,
                ["status"] = job.Status,
                ["summary"] = job.Summary,
                ["completed_at"] = job.CompletedAt?.ToString("o"),
                ["estimated_seconds"] = job.EstimatedSeconds,
                ["result"] = job.Result
            };
        }

        // v2.6.4 (#18): lifecycle action=result for op:<id> reads the stored
        // JobEntry result. Extracted from Program.cs so it can be unit-tested
        // and so the result-envelope shape stays in lockstep with status long-poll.
        // isError is set when the job terminated in failed/cancelled — callers
        // that branch on isError get a clear pass/fail signal.
        internal static (JObject envelope, bool isError) BuildJobResultEnvelope(JobEntry job)
        {
            if (job == null)
                throw new ArgumentNullException(nameof(job));

            if (string.Equals(job.Status, "running", StringComparison.OrdinalIgnoreCase))
            {
                var pending = new JObject
                {
                    ["status"] = "Pending",
                    ["operationId"] = job.Id,
                    ["message"] = "Operation still running. Poll genexus_lifecycle action=status target=op:" + job.Id + " (with wait_seconds>0 to long-poll), then call result once it terminates.",
                    ["startedAt"] = job.StartedAt.ToString("o"),
                    ["estimated_seconds"] = job.EstimatedSeconds
                };
                return (pending, isError: false);
            }

            var terminal = new JObject
            {
                ["status"] = job.Status,
                ["operationId"] = job.Id,
                ["kind"] = job.Kind,
                ["summary"] = job.Summary,
                ["startedAt"] = job.StartedAt.ToString("o"),
                ["completedAt"] = job.CompletedAt?.ToString("o")
            };
            if (job.Result != null) terminal["result"] = job.Result;
            bool isErr = string.Equals(job.Status, "failed", StringComparison.OrdinalIgnoreCase)
                      || string.Equals(job.Status, "cancelled", StringComparison.OrdinalIgnoreCase);
            return (terminal, isErr);
        }
    }
}
