using System;
using System.Diagnostics;
using System.IO;
using System.Threading;
using System.Collections.Concurrent;
using System.Collections.Generic;
using GxMcp.Worker.Services;
using GxMcp.Worker.Helpers;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System.Reflection;
using System.Windows.Forms;

namespace GxMcp.Worker
{
    class Program
    {
        public static readonly BlockingCollection<string> CommandQueue = new BlockingCollection<string>();
        public static readonly BlockingCollection<string> SdkCommandQueue = new BlockingCollection<string>();
        public static readonly ConcurrentQueue<Action> BackgroundQueue = new ConcurrentQueue<Action>();

        // FR#20 (v2.6.6 Stream B): soft-reload coordination. When a genexus_worker_reload
        // with mode=soft arrives, we flip this flag, drain the queues, persist
        // BackgroundJobRegistry state, and exit cleanly with code 0 so the gateway can
        // respawn. Read by every dispatch loop that needs to honor a quiesce request.
        public static volatile bool ShuttingDown = false;
        // Exit code 17 = "busy" (FR#19 single-instance reject). Distinct from 0
        // (clean shutdown / soft reload), 1 (init failure), and the process-crash codes.
        public const int ExitCodeBusy = 17;
        public const int ExitCodeSoftReload = 0;

        public static SingleInstanceLock InstanceLock { get; private set; }
        // PERFORMANCE (W-B3): signal that wakes the background worker immediately on Enqueue,
        // instead of busy-polling with Thread.Sleep(100). WaitOne(100) keeps a safety timeout
        // so the loop still re-checks shutdown state if a signal is missed.
        public static readonly AutoResetEvent BackgroundSignal = new AutoResetEvent(false);

        public static void EnqueueBackground(Action work)
        {
            if (work == null) return;
            BackgroundQueue.Enqueue(work);
            try { BackgroundSignal.Set(); } catch { }
        }
        private static readonly BlockingCollection<string> _outputQueue = new BlockingCollection<string>();
        private static readonly BlockingCollection<string> _errorQueue = new BlockingCollection<string>();
        private static CommandDispatcher _dispatcher;
        private static TextWriter _originalOut;
        private static TextWriter _originalError;
        private static StreamWriter _pipeWriter;
        private static Form _bridgeForm;
        private static readonly ManualResetEvent _uiReady = new ManualResetEvent(false);

        [STAThread]
        static void Main(string[] args)
        {
            try {
                // Force UTF-8 on worker stdio before capturing the original writers.
                Console.InputEncoding = System.Text.Encoding.UTF8;
                Console.OutputEncoding = new System.Text.UTF8Encoding(false);

                // ELITE: Start output threads first to capture all logs immediately
                StartOutputThreads();
                Console.SetOut(new QueueWriter(_outputQueue));
                Console.SetError(new QueueWriter(_errorQueue));

                // Ensure culture is Portuguese-Brazil for SDK character mapping
                try {
                    System.Threading.Thread.CurrentThread.CurrentCulture = new System.Globalization.CultureInfo("pt-BR");
                    System.Threading.Thread.CurrentThread.CurrentUICulture = new System.Globalization.CultureInfo("pt-BR");
                } catch { }

                AppDomain.CurrentDomain.UnhandledException += (s, e) => {
                    try
                    {
                        var ex = e.ExceptionObject as Exception;
                        var proc = System.Diagnostics.Process.GetCurrentProcess();
                        long memMb = proc.WorkingSet64 / (1024 * 1024);
                        long privMb = proc.PrivateMemorySize64 / (1024 * 1024);
                        var uptime = DateTime.UtcNow - proc.StartTime.ToUniversalTime();
                        Logger.Error(
                            "[WORKER-CRASH] terminating=" + e.IsTerminating
                            + " memMB=" + memMb + " privMB=" + privMb
                            + " uptimeSec=" + (int)uptime.TotalSeconds
                            + " gcMemMB=" + (GC.GetTotalMemory(false) / (1024 * 1024))
                            + " threadCount=" + proc.Threads.Count
                            + " exType=" + (ex?.GetType().FullName ?? "<null>")
                            + " exMsg=" + (ex?.Message ?? "<null>")
                            + Environment.NewLine + "Stack: " + (ex?.ToString() ?? "<null>"));
                    }
                    catch { Logger.Error("FATAL UNHANDLED EXCEPTION: " + (e.ExceptionObject as Exception)?.ToString()); }
                };

                System.Threading.Tasks.TaskScheduler.UnobservedTaskException += (s, e) => {
                    Logger.Error("[WORKER-CRASH] UNOBSERVED TASK EXCEPTION: " + e.Exception?.ToString());
                    e.SetObserved();
                };

                WriteLine("WORKER_HANDSHAKE_START");
                Logger.Info("Worker process started (STA Mode).");

                // Friction 2026-05-22: reap orphaned MSBuild.exe processes whose
                // parent worker died mid-build (worker_reload force=true, crash, etc.).
                // Safe heuristic: only kill MSBuild.exe whose ProcessParentId points
                // to a PID that no longer exists. Doesn't touch the user's IDE / VS.
                try { ReapOrphanMsbuilds(); }
                catch (Exception ex) { Logger.Warn("[ORPHAN-REAP] failed: " + ex.Message); }

                // ELITE: Configuration Resolve Logic (Env > Local Config > Error)
                string gxPath = Environment.GetEnvironmentVariable("GX_PROGRAM_DIR");
                string kbPath = Environment.GetEnvironmentVariable("GX_KB_PATH");

                if (string.IsNullOrEmpty(gxPath) || string.IsNullOrEmpty(kbPath))
                {
                    var config = LoadLocalConfig();
                    if (config != null)
                    {
                        if (string.IsNullOrEmpty(gxPath)) gxPath = config["InstallationPath"]?.ToString();
                        if (string.IsNullOrEmpty(kbPath)) kbPath = config["KBPath"]?.ToString();
                    }
                }

                if (string.IsNullOrEmpty(gxPath))
                    throw new Exception("GX_PROGRAM_DIR not specified in environment or local config.json.");

                // FR#19 (v2.6.6 Stream B): refuse to start when another worker already
                // serves this (kbPath, workerExe) pair. We resolve the cli-arg kbPath
                // first so the lock key matches whatever the gateway intended.
                string lockKb = kbPath;
                for (int i = 0; i < args.Length; i++)
                {
                    if (args[i] == "--kb" && i + 1 < args.Length) { lockKb = args[i + 1]; break; }
                }
                string workerExePath = "";
                try { workerExePath = Process.GetCurrentProcess().MainModule?.FileName ?? Assembly.GetEntryAssembly()?.Location ?? ""; }
                catch (Exception lockEx) { Logger.Error("[SingleInstanceLock] could not resolve own exe path: " + lockEx.Message); }
                string lockDir = "";
                try { lockDir = Path.GetDirectoryName(workerExePath) ?? Path.GetTempPath(); }
                catch { lockDir = Path.GetTempPath(); }
                InstanceLock = SingleInstanceLock.TryAcquire(lockKb ?? "", workerExePath, lockDir);
                if (!InstanceLock.Acquired)
                {
                    int existing = InstanceLock.ExistingPid ?? -1;
                    // Bypass the QueueWriter to guarantee the gateway sees this message
                    // even before the output thread fully spins up.
                    try { _originalOut?.WriteLine("WORKER_HANDSHAKE_REJECT_BUSY pid=" + existing); _originalOut?.Flush(); } catch { }
                    Logger.Info("[SingleInstanceLock] reject_busy existingPid=" + existing + " key=" + InstanceLock.Key);
                    Environment.Exit(ExitCodeBusy);
                    return;
                }
                Logger.Info("[SingleInstanceLock] acquired pid=" + Process.GetCurrentProcess().Id + " lockPath=" + InstanceLock.LockPath);
                // Advertise soft-reload support in the handshake so the gateway can
                // default to mode=soft for callers that don't specify a mode.
                WriteLine("WORKER_HANDSHAKE_FEATURES soft_reload=true single_instance=true");

                AppDomain.CurrentDomain.AssemblyResolve += (sender, resolveArgs) => {
                    try {
                        string assemblyName = new AssemblyName(resolveArgs.Name).Name + ".dll";
                        string assemblyPath = Path.Combine(gxPath, assemblyName);
                        if (File.Exists(assemblyPath)) return Assembly.LoadFrom(assemblyPath);
                    } catch { }
                    return null;
                };

                string pipeName = Environment.GetEnvironmentVariable("GX_MCP_PIPE");
                if (!string.IsNullOrEmpty(pipeName))
                {
                    try {
                        var pipeClient = new System.IO.Pipes.NamedPipeClientStream(".", pipeName, System.IO.Pipes.PipeDirection.InOut, System.IO.Pipes.PipeOptions.Asynchronous);
                        pipeClient.Connect(30000);
                        var writer = new StreamWriter(pipeClient, new System.Text.UTF8Encoding(false)) { AutoFlush = true };
                        var reader = new StreamReader(pipeClient, new System.Text.UTF8Encoding(false));
                        
                        _pipeWriter = writer;
                        Console.SetIn(reader);
                        Logger.Info($"[Worker] Connected to IPC Pipe {pipeName} successfully.");
                    } catch (Exception ex) {
                        Logger.Error($"[Worker] IPC Pipe Connection Error: {ex.Message}. Falling back to STDIO.");
                    }
                }

                // v2.6.6 live-test finding: ArtechTask.cctor activates the
                // GxServiceManager process-singleton and throws GxException
                // "Service Manager já foi ativado" if SM is already active
                // when the cctor first runs. Our worker previously activated SM
                // via KbService.OpenKB → ArtechTask cctor would throw on first
                // in-process build, then the type was permanently dead for the
                // worker's lifetime (cctor failures cache forever in .NET).
                //
                // The IDE avoids this by triggering ArtechTask FIRST. We mirror
                // that: warm the cctor BEFORE InitializeSdk so ArtechTask gets
                // to set up SM on its own terms; later SDK Initialize calls and
                // KbService.OpenKB then reuse the same activated SM cleanly.
                //
                // Best-effort: any failure here is logged and ignored — the
                // in-process build path will fall back to MSBuild.exe spawn.
                TryWarmupArtechTaskCctor(gxPath);

                InitializeSdk(gxPath);
                _dispatcher = CommandDispatcher.Instance;
                
                // Check command line arguments for --kb
                for (int i = 0; i < args.Length; i++)
                {
                    if (args[i] == "--kb" && i + 1 < args.Length)
                    {
                        kbPath = args[i + 1];
                        break;
                    }
                }

                if (!string.IsNullOrEmpty(kbPath))
                {
                    try {
                        Logger.Info($"Worker auto-opening KB: {kbPath}");
                        _dispatcher.GetKbService().OpenKB(kbPath);
                    } catch (Exception ex) {
                        Logger.Error($"Worker failed to auto-open KB: {ex.Message}");
                    }
                }

                Logger.Info("Worker SDK ready.");

                // FR#20: best-effort restore of BackgroundJobRegistry rehydration state
                // written by the previous (soft-reloaded) worker. JobEntry lives in the
                // gateway, but we surface the file path here so a future worker-side
                // job system can pick it up — and so soft reload tests can observe the
                // file lifecycle without touching the gateway.
                try
                {
                    string stateDir = Path.Combine(Path.GetDirectoryName(workerExePath) ?? Path.GetTempPath(), "state");
                    string jobsFile = Path.Combine(stateDir, "jobs.json");
                    if (File.Exists(jobsFile))
                    {
                        Logger.Info("[SoftReload] found pending jobs snapshot at " + jobsFile + " (size=" + new FileInfo(jobsFile).Length + ")");
                        // Snapshot is consumed by the gateway through worker_reload_jobs_path
                        // notification; we just leave it for the next pickup. Removed only
                        // by the consumer to avoid races on dual-restart scenarios.
                        SendNotification("notifications/worker/jobs_restored", new {
                            path = jobsFile,
                            sizeBytes = new FileInfo(jobsFile).Length
                        });
                    }
                }
                catch (Exception jrEx) { Logger.Error("[SoftReload] jobs snapshot probe failed: " + jrEx.Message); }


                // Start External KB Watcher
                var watcher = new KbWatcherService(_dispatcher.GetKbService(), (name, type, time) => {
                    SendNotification("notifications/resources/updated", new {
                        name = name,
                        type = type,
                        updatedAt = time,
                        external = true
                    });
                });
                watcher.Start();

                var readerThread = new Thread(() => {
                    while (true) {
                        string line = Console.ReadLine();
                        if (line == null) break;
                        if (line.Trim().Equals("ping", StringComparison.OrdinalIgnoreCase) || line.Contains("\"method\":\"ping\"") || line.Contains("\"action\":\"Ping\""))
                        {
                            WriteLine("{\"jsonrpc\":\"2.0\",\"result\":{\"status\":\"Ready\"},\"id\":\"heartbeat\"}");
                            if (!line.Contains("\"method\"")) continue;
                        }
                        if (!string.IsNullOrWhiteSpace(line)) CommandQueue.Add(line);
                    }
                    CommandQueue.CompleteAdding();
                }) { IsBackground = true, Name = "HeartbeatReader" };
                readerThread.Start();

                                // DEDICATED SDK WORKER THREAD (STA with WinForms Bridge)
                var sdkWorker = new Thread(() => {
                    Logger.Info("SDK Worker Thread started (WinForms Bridge enabled).");
                    _bridgeForm = new Form { 
                        ShowInTaskbar = false, 
                        WindowState = FormWindowState.Minimized,
                        Visible = false 
                    };
                    
                    // The 'Quiet Mode' loop: process commands on the UI thread
                    var pollTimer = new System.Windows.Forms.Timer { Interval = 10 };
                    pollTimer.Tick += (s, e) => {
                        if (SdkCommandQueue.TryTake(out string line))
                        {
                            try { ProcessCommand(line); }
                            catch (Exception ex) { Logger.Error("SDK Command Error: " + ex.Message); }
                        }
                    };
                    
                    _bridgeForm.Load += (s, e) => {
                        pollTimer.Start();
                        _uiReady.Set();
                        Logger.Debug("WinForms Bridge Ready.");
                    };
                    
                    Application.Run(_bridgeForm);
                }) { IsBackground = true, Name = "SdkWorker", Priority = ThreadPriority.AboveNormal };
                sdkWorker.SetApartmentState(ApartmentState.STA);
                sdkWorker.Start();
                
                _uiReady.WaitOne(10000); // Wait for the bridge to come online

                // DEDICATED STA BACKGROUND TASK THREAD
                var backgroundWorker = new Thread(() => {
                    Logger.Info("Background STA Worker Thread started.");
                    while (!CommandQueue.IsCompleted) {
                        if (BackgroundQueue.TryDequeue(out var action)) {
                            try { action(); }
                            catch (Exception ex) { Logger.Error("Background Task Error: " + ex.Message); }
                        } else {
                            // PERFORMANCE (W-B3): wait for signal from EnqueueBackground; 100ms
                            // fallback re-checks CommandQueue.IsCompleted on shutdown.
                            BackgroundSignal.WaitOne(100);
                        }
                    }
                }) { IsBackground = true, Name = "BackgroundWorker" };
                backgroundWorker.SetApartmentState(ApartmentState.STA);
                backgroundWorker.Start();

                // MAIN DISPATCHER LOOP
                while (!CommandQueue.IsCompleted || CommandQueue.Count > 0)
                {
                    if (CommandQueue.TryTake(out string line, 100))
                    {
                        if (_dispatcher.IsThreadSafe(line))
                            System.Threading.Tasks.Task.Run(() => ProcessCommand(line));
                        else
                            SdkCommandQueue.Add(line);
                    }
                }

                Logger.Info("Input EOF reached. Shutting down...");
                SdkCommandQueue.CompleteAdding();
                while (!SdkCommandQueue.IsCompleted || SdkCommandQueue.Count > 0)
                {
                    Thread.Sleep(50);
                }
                Logger.Info("Worker shutting down safely.");
            } catch (Exception ex) {
                Logger.Error($"Main FATAL: {ex.Message}");
            }
        }

        /// <summary>
        /// FR#20: drain in-flight commands, persist gateway job state via stdout notification,
        /// close the KB, and exit cleanly so the gateway can respawn against a fresh binary.
        /// Returns the worker-side ack JSON; the gateway sees the clean exit and re-launches.
        /// </summary>
        public static string PerformSoftReload(int drainTimeoutMs)
        {
            var sw = System.Diagnostics.Stopwatch.StartNew();
            try
            {
                Logger.Info("[SoftReload] beginning soft reload drainTimeoutMs=" + drainTimeoutMs);
                ShuttingDown = true;

                // Signal the gateway: persist your JobRegistry NOW before we exit.
                // The gateway listens for this and dumps to publish\worker\state\jobs.json.
                try
                {
                    string stateDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "state");
                    try { Directory.CreateDirectory(stateDir); }
                    catch (Exception dirEx) { Logger.Error("[SoftReload] state dir create failed: " + dirEx.Message); }
                    SendNotification("notifications/worker/persist_jobs_request", new {
                        path = Path.Combine(stateDir, "jobs.json"),
                        reason = "soft_reload"
                    });
                }
                catch (Exception nEx) { Logger.Error("[SoftReload] persist_jobs notification failed: " + nEx.Message); }

                // Drain main queue (already STA-routed) — let in-flight commands flush.
                var deadline = DateTime.UtcNow.AddMilliseconds(Math.Max(1000, drainTimeoutMs));
                while (DateTime.UtcNow < deadline)
                {
                    if (CommandQueue.Count == 0 && SdkCommandQueue.Count == 0 && BackgroundQueue.IsEmpty) break;
                    Thread.Sleep(100);
                }
                Logger.Info("[SoftReload] drain finished pendingMain=" + CommandQueue.Count +
                            " pendingSdk=" + SdkCommandQueue.Count + " pendingBg=" + BackgroundQueue.Count +
                            " elapsedMs=" + sw.ElapsedMilliseconds);

                // Close KB best-effort. KbService has no explicit Close — releasing the dynamic
                // reference is the closest we have; the SDK disposes on AppDomain unload.
                try
                {
                    var kb = _dispatcher?.GetKbService()?.GetKB();
                    if (kb != null)
                    {
                        try { kb.Close(); } catch (Exception closeEx) { Logger.Error("[SoftReload] kb.Close() threw: " + closeEx.Message); }
                    }
                }
                catch (Exception kbEx) { Logger.Error("[SoftReload] KB close best-effort failed: " + kbEx.Message); }

                // Schedule exit on a background thread so the dispatcher can return the
                // ack to the gateway BEFORE the process dies.
                var exitDelay = System.Threading.Tasks.Task.Run(async () =>
                {
                    await System.Threading.Tasks.Task.Delay(750);
                    try { InstanceLock?.Dispose(); } catch (Exception lockEx) { Logger.Error("[SoftReload] lock dispose: " + lockEx.Message); }
                    Logger.Info("[SoftReload] exiting code=" + ExitCodeSoftReload + " totalMs=" + sw.ElapsedMilliseconds);
                    Environment.Exit(ExitCodeSoftReload);
                });

                return "{\"status\":\"Accepted\",\"mode\":\"soft\",\"drainMs\":" + sw.ElapsedMilliseconds +
                       ",\"note\":\"Worker draining and exiting code 0 in ~750ms; gateway will respawn against current binary.\"}";
            }
            catch (Exception ex)
            {
                Logger.Error("[SoftReload] failed: " + ex.Message);
                return "{\"status\":\"Error\",\"mode\":\"soft\",\"error\":\"" + ex.Message.Replace("\"", "\\\"") + "\"}";
            }
        }

        // v2.6.6: one-shot warmup of Artech.MsBuild.Common.ArtechTask so its
        // static ctor activates GxServiceManager BEFORE InitializeSdk / OpenKB
        // get a chance to. Order matters: cctor failures cache permanently per
        // AppDomain, so this MUST succeed on the first try. If it throws here
        // (e.g. SDK install layout mismatch) the in-process build path will be
        // dead for this worker session, but the external MSBuild.exe fallback
        // still works.
        private static void TryWarmupArtechTaskCctor(string gxPath)
        {
            try
            {
                string asmPath = Path.Combine(gxPath, "Genexus.MsBuild.Tasks.dll");
                // No File.Exists pre-check — LoadFrom throws a clean
                // FileNotFoundException that we unwrap below.
                var asm = Assembly.LoadFrom(asmPath);
                // SpecifyOneOnly inherits from ArtechTask; touching the type
                // triggers ArtechTask's static initializer.
                var t = asm.GetType("Genexus.MsBuild.Tasks.SpecifyOneOnly", throwOnError: false);
                if (t == null)
                {
                    Logger.Warn("[ArtechTask-warmup] SpecifyOneOnly type not found — skipping");
                    return;
                }
                var artechBase = t.BaseType;
                while (artechBase != null && artechBase.FullName != "Artech.MsBuild.Common.ArtechTask")
                    artechBase = artechBase.BaseType;
                if (artechBase == null)
                {
                    Logger.Warn("[ArtechTask-warmup] ArtechTask not in inheritance chain of SpecifyOneOnly — skipping");
                    return;
                }
                System.Runtime.CompilerServices.RuntimeHelpers.RunClassConstructor(artechBase.TypeHandle);
                Logger.Info("[ArtechTask-warmup] OK — Service Manager activated by ArtechTask cctor");
            }
            catch (Exception ex)
            {
                int depth = 0;
                for (Exception e = ex; e != null && depth < 6; e = e.InnerException, depth++)
                {
                    Logger.Error("[ArtechTask-warmup] ex[" + depth + "] "
                                 + e.GetType().FullName + ": " + e.Message);
                }
            }
        }

        private static void InitializeSdk(string gxPath)
        {
            try {
                Logger.Debug($"Setting current directory to {gxPath}");
                Directory.SetCurrentDirectory(gxPath);
                
                var archAsm = Assembly.LoadFrom(Path.Combine(gxPath, "Artech.Architecture.Common.dll"));
                var contextServiceType = archAsm.GetType("Artech.Architecture.Common.Services.ContextService");
                contextServiceType?.GetMethod("Initialize", BindingFlags.Public | BindingFlags.Static)?.Invoke(null, null);

                var blAsm = Assembly.LoadFrom(Path.Combine(gxPath, "Artech.Architecture.BL.Framework.dll"));
                var blCommonType = blAsm.GetType("Artech.Architecture.BL.Framework.Services.CommonServices");
                blCommonType?.GetMethod("Initialize", BindingFlags.Public | BindingFlags.Static)?.Invoke(null, null);

                var uiAsm = Assembly.LoadFrom(Path.Combine(gxPath, "Artech.Architecture.UI.Framework.dll"));
                var uiType = uiAsm.GetType("Artech.Architecture.UI.Framework.Services.UIServices");
                uiType?.GetMethod("Initialize", BindingFlags.Public | BindingFlags.Static)?.Invoke(null, null);

                var commonAsm = Assembly.LoadFrom(Path.Combine(gxPath, "Artech.Genexus.Common.dll"));
                var initType = commonAsm.GetType("Artech.Genexus.Common.KBModelObjectsInitializer");
                initType?.GetMethod("Initialize", BindingFlags.Public | BindingFlags.Static)?.Invoke(null, null);

                var connAsm = Assembly.LoadFrom(Path.Combine(gxPath, "Connector.dll"));
                var connType = connAsm.GetType("Artech.Core.Connector");
                connType?.GetMethod("Initialize", BindingFlags.Public | BindingFlags.Static)?.Invoke(null, null);
                connType?.GetMethod("Start", BindingFlags.Public | BindingFlags.Static)?.Invoke(null, null);
                
                var kbBaseType = archAsm.GetType("Artech.Architecture.Common.Objects.KnowledgeBase");
                var factoryProp = kbBaseType?.GetProperty("KBFactory", BindingFlags.Public | BindingFlags.Static);
                if (factoryProp != null) {
                    var factoryType = connAsm.GetType("Connector.KBFactory");
                    if (factoryType != null) {
                        factoryProp.SetValue(null, Activator.CreateInstance(factoryType));
                        Logger.Info("KBFactory Linked successfully.");
                    }
                }
                
                Logger.Info("Full SDK Initialization SUCCESS.");
            } catch (Exception ex) { 
                Logger.Error("CRITICAL Init Error: " + ex.Message); 
            }
        }

        private static void ProcessCommand(string line)
        {
            try {
                var obj = JObject.Parse(line);
                string idJson = obj["id"]?.ToString() ?? "null";
                string method = obj["method"]?.ToString();
                string correlationId = obj["params"]?["correlationId"]?.ToString() ?? "n/a";
                Logger.Info($"[WORKER] Command: {method} ({idJson}) [cid:{correlationId}]");
                string result = _dispatcher.Dispatch(line);
                SendResponse(result, idJson);
            } catch (Exception ex) { Logger.Error("ProcessCommand Error: " + ex.Message); }
        }

        private static void SendResponse(string result, string id)
        {
            try {
                object resultObj;
                try { resultObj = JToken.Parse(result); } catch { resultObj = result; }
                var response = new { jsonrpc = "2.0", result = resultObj, id = id };
                WriteLine(JsonConvert.SerializeObject(response, Formatting.None));
            } catch (Exception ex) { Logger.Error("SendResponse Error: " + ex.Message); }
        }

        public static void SendNotification(string method, object @params)
        {
            try {
                var notification = new { jsonrpc = "2.0", method = method, @params = @params };
                WriteLine(JsonConvert.SerializeObject(notification, Formatting.None));
            } catch (Exception ex) { Logger.Error("SendNotification Error: " + ex.Message); }
        }

        public static void WriteLine(string line) => _outputQueue.Add(line);
        public static void WriteError(string line) => _errorQueue.Add(line);

        // Friction 2026-05-22: on startup, kill MSBuild.exe instances whose parent
        // PID no longer exists. These accumulate when a build is cancelled or the
        // worker crashes mid-build (the /m worker nodes survive the parent kill on
        // net48 since Process.Kill(bool) doesn't exist). Opt-out:
        //   GXMCP_REAP_ORPHAN_MSBUILD=0
        private static void ReapOrphanMsbuilds()
        {
            if (string.Equals(Environment.GetEnvironmentVariable("GXMCP_REAP_ORPHAN_MSBUILD"), "0",
                              StringComparison.OrdinalIgnoreCase))
            {
                return;
            }

            int reaped = 0;
            try
            {
                // Snapshot every process ID once to test parent liveness in O(1).
                var allPids = new HashSet<int>();
                foreach (var p in System.Diagnostics.Process.GetProcesses())
                {
                    try { allPids.Add(p.Id); } finally { try { p.Dispose(); } catch { } }
                }

                using (var searcher = new System.Management.ManagementObjectSearcher(
                    "SELECT ProcessId, ParentProcessId, CommandLine FROM Win32_Process WHERE Name = 'MSBuild.exe'"))
                using (var results = searcher.Get())
                {
                    foreach (System.Management.ManagementObject mo in results)
                    {
                        try
                        {
                            int pid = Convert.ToInt32(mo["ProcessId"]);
                            int parent = Convert.ToInt32(mo["ParentProcessId"]);
                            // R5/R8: scope by command-line so we only touch GeneXus KB
                            // builds — VS / IDE MSBuilds and unrelated /m worker nodes
                            // are left alone even if their parent is gone. The
                            // command-line filter is the primary scope; the parent-alive
                            // check below is defense-in-depth against the
                            // GetProcesses()/WMI snapshot race.
                            var cmdLine = mo["CommandLine"] as string;
                            if (string.IsNullOrEmpty(cmdLine)) continue;
                            bool matchesGxBuild =
                                cmdLine.IndexOf("LastBuild.sln", StringComparison.OrdinalIgnoreCase) >= 0 ||
                                cmdLine.IndexOf("\\Desenv\\build\\", StringComparison.OrdinalIgnoreCase) >= 0;
                            if (!matchesGxBuild) continue;
                            // Parent process either gone, or PID got recycled into a different
                            // process — in either case, a /m worker node has no one waiting on it.
                            if (!allPids.Contains(parent))
                            {
                                try
                                {
                                    using (var child = System.Diagnostics.Process.GetProcessById(pid))
                                    {
                                        if (!child.HasExited) { child.Kill(); reaped++; }
                                    }
                                }
                                catch { /* race: already exited */ }
                            }
                        }
                        catch { }
                        finally { try { mo?.Dispose(); } catch { } }
                    }
                }
            }
            catch (Exception ex)
            {
                Logger.Warn("[ORPHAN-REAP] WMI scan failed: " + ex.Message);
                return;
            }

            if (reaped > 0)
                Logger.Info("[ORPHAN-REAP] killed " + reaped + " orphan MSBuild.exe process(es) at startup");
        }

        private static void StartOutputThreads()
        {
            _originalOut = Console.Out;
            _originalError = Console.Error;

            var outThread = new Thread(() => {
                foreach (var line in _outputQueue.GetConsumingEnumerable()) {
                    try { 
                        if (_pipeWriter != null) {
                            _pipeWriter.WriteLine(line);
                            _pipeWriter.Flush();
                        } else {
                            _originalOut.WriteLine(line); 
                            _originalOut.Flush(); 
                        }
                    } catch { }
                }
            }) { IsBackground = true, Name = "OutputWriter" };
            outThread.Start();

            var errThread = new Thread(() => {
                foreach (var line in _errorQueue.GetConsumingEnumerable()) {
                    try { _originalError.WriteLine(line); _originalError.Flush(); } catch { }
                }
            }) { IsBackground = true, Name = "ErrorWriter" };
            errThread.Start();
        }

        private static JObject LoadLocalConfig()
        {
            try {
                string exeDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
                string configPath = Path.Combine(exeDir, "config.json");
                if (File.Exists(configPath)) return JObject.Parse(File.ReadAllText(configPath));
            } catch { }
            return null;
        }
    }

    // PERFORMANCE (W-A5): the previous implementation took a lock per-character on every
    // Console.Write call. Every IPC line — JSON-RPC responses that can be tens of KB — paid
    // that cost. Here Write(string) and WriteLine(string) acquire the lock once per call and
    // handle the '\n'/'\r' split internally. Write(char) is preserved for completeness but no
    // longer the hot path.
    public class QueueWriter : TextWriter
    {
        private readonly BlockingCollection<string> _queue;
        private readonly System.Text.StringBuilder _buffer = new System.Text.StringBuilder();

        public QueueWriter(BlockingCollection<string> queue) { _queue = queue; }
        public override System.Text.Encoding Encoding => System.Text.Encoding.UTF8;

        public override void Write(char value)
        {
            if (value == '\r') return;
            lock (_buffer)
            {
                if (value == '\n')
                {
                    _queue.Add(_buffer.ToString());
                    _buffer.Clear();
                }
                else
                {
                    _buffer.Append(value);
                }
            }
        }

        public override void Write(string value)
        {
            if (string.IsNullOrEmpty(value)) return;
            lock (_buffer)
            {
                AppendInternal(value);
            }
        }

        public override void WriteLine(string value)
        {
            lock (_buffer)
            {
                if (!string.IsNullOrEmpty(value)) AppendInternal(value);
                _queue.Add(_buffer.ToString());
                _buffer.Clear();
            }
        }

        // Caller MUST hold _buffer lock.
        private void AppendInternal(string value)
        {
            int start = 0;
            for (int i = 0; i < value.Length; i++)
            {
                char c = value[i];
                if (c == '\r') continue;
                if (c == '\n')
                {
                    if (i > start) _buffer.Append(value, start, i - start);
                    _queue.Add(_buffer.ToString());
                    _buffer.Clear();
                    start = i + 1;
                }
            }
            if (start < value.Length)
            {
                // Append the trailing partial line (no terminator yet) in one shot,
                // skipping any stray '\r' characters between start and end.
                for (int j = start; j < value.Length; j++)
                {
                    char c = value[j];
                    if (c != '\r') _buffer.Append(c);
                }
            }
        }
    }
}
