#!/usr/bin/env node
"use strict";

const fs = require("fs");
const http = require("http");
const os = require("os");
const path = require("path");
const { execFile, spawn } = require("child_process");

const VERSION = "HL-G001-0.5.8";
const DEFAULT_PORT = Number(process.env.HARDWARE_LAB_BACKEND_PORT || process.env.PORT || 8765);
const PROJECT_ROOT = path.resolve(__dirname, "..");
const DEFAULT_TIMEOUT_MS = 8000;
const EVIDENCE_DIR = path.join(PROJECT_ROOT, "output", "hardware-lab-evidence");
const MSPM0_PACK_FILE = "TexasInstruments.MSPM0G1X0X_G3X0X_DFP.1.3.1.pack";
const DEFAULT_MSPM0_PACK = path.join("C:\\Users\\DZ\\AppData\\Local\\Temp", MSPM0_PACK_FILE);
const DEFAULT_FLASH_CONFIRM = "I_UNDERSTAND_FLASH_RISK";

const TOOL_CHECKS = [
  { name: "node", args: ["--version"] },
  { name: "python", args: ["--version"] },
  { name: "pyocd", args: ["--version"] },
  { name: "cmake", args: ["--version"] },
  { name: "mingw32-make", args: ["--version"] },
  { name: "make", args: ["--version"] },
  { name: "ninja", args: ["--version"] },
  { name: "arm-none-eabi-gcc", args: ["--version"] },
  { name: "STM32_Programmer_CLI", args: ["--version"] },
  { name: "st-info", args: ["--version"] },
  { name: "st-flash", args: ["--version"] },
];

function jsonResponse(ok, message, data = {}, logs = [], error = null) {
  return {
    ok,
    message,
    data,
    logs: Array.isArray(logs) ? logs : [String(logs)],
    error,
    timestamp: new Date().toISOString(),
  };
}

function sendJson(res, statusCode, payload) {
  res.writeHead(statusCode, {
    "Content-Type": "application/json; charset=utf-8",
    "Cache-Control": "no-store",
    "Access-Control-Allow-Origin": "*",
    "Access-Control-Allow-Methods": "GET,POST,OPTIONS",
    "Access-Control-Allow-Headers": "Content-Type",
    "Access-Control-Allow-Private-Network": "true",
  });
  res.end(JSON.stringify(payload, null, 2));
}

function sendStreamHeaders(res) {
  res.writeHead(200, {
    "Content-Type": "application/x-ndjson; charset=utf-8",
    "Cache-Control": "no-store, no-transform",
    "Connection": "keep-alive",
    "X-Accel-Buffering": "no",
    "Access-Control-Allow-Origin": "*",
    "Access-Control-Allow-Methods": "GET,POST,OPTIONS",
    "Access-Control-Allow-Headers": "Content-Type",
    "Access-Control-Allow-Private-Network": "true",
  });
}

function writeStreamEvent(res, event = {}) {
  res.write(`${JSON.stringify({ ...event, timestamp: new Date().toISOString() })}\n`);
}

function contentTypeFor(filePath) {
  const ext = path.extname(filePath).toLowerCase();
  return {
    ".html": "text/html; charset=utf-8",
    ".css": "text/css; charset=utf-8",
    ".js": "text/javascript; charset=utf-8",
    ".json": "application/json; charset=utf-8",
    ".svg": "image/svg+xml",
    ".png": "image/png",
    ".jpg": "image/jpeg",
    ".jpeg": "image/jpeg",
    ".webp": "image/webp",
    ".zip": "application/zip",
    ".ico": "image/x-icon",
    ".md": "text/markdown; charset=utf-8",
    ".txt": "text/plain; charset=utf-8",
  }[ext] || "application/octet-stream";
}

function staticPathFor(urlPathname) {
  let pathname = decodeURIComponent(urlPathname || "/");
  if (pathname === "/" || pathname === "/hardware-lab") pathname = "/hardware-lab.html";
  if (pathname === "/planner") pathname = "/planner.html";
  const relativePath = pathname.replace(/^\/+/, "");
  if (!relativePath || relativePath.includes("\0")) return null;
  const fullPath = path.resolve(PROJECT_ROOT, relativePath);
  const rootWithSep = PROJECT_ROOT.endsWith(path.sep) ? PROJECT_ROOT : `${PROJECT_ROOT}${path.sep}`;
  if (fullPath !== PROJECT_ROOT && !fullPath.startsWith(rootWithSep)) return null;
  return fullPath;
}

function sendStaticFile(res, filePath) {
  try {
    const stat = fs.existsSync(filePath) ? fs.statSync(filePath) : null;
    if (!stat || !stat.isFile()) return false;
    res.writeHead(200, {
      "Content-Type": contentTypeFor(filePath),
      "Cache-Control": "no-store",
      "Access-Control-Allow-Origin": "*",
      "Access-Control-Allow-Methods": "GET,POST,OPTIONS",
      "Access-Control-Allow-Headers": "Content-Type",
      "Access-Control-Allow-Private-Network": "true",
    });
    fs.createReadStream(filePath).pipe(res);
    return true;
  } catch (error) {
    sendJson(res, 500, jsonResponse(false, "Static preview failed", { filePath }, [], error.message || String(error)));
    return true;
  }
}

function ensureEvidenceDir() {
  fs.mkdirSync(EVIDENCE_DIR, { recursive: true });
}

function safeFileStamp() {
  return new Date().toISOString().replace(/[:.]/g, "-");
}

function redactPayload(payload = {}) {
  const copy = { ...payload };
  if (copy.confirmFlash) copy.confirmFlash = "[redacted]";
  return copy;
}

function writeBackendEvidence(action, payload, result) {
  try {
    ensureEvidenceDir();
    const filename = `${safeFileStamp()}-${String(action || "api").replace(/[^a-z0-9-]/gi, "_")}.json`;
    const fullPath = path.join(EVIDENCE_DIR, filename);
    fs.writeFileSync(
      fullPath,
      JSON.stringify(
        {
          action,
          payload: redactPayload(payload),
          result,
          backend: { version: VERSION, pid: process.pid, cwd: process.cwd(), projectRoot: PROJECT_ROOT },
        },
        null,
        2,
      ),
      "utf8",
    );
    return fullPath;
  } catch (error) {
    console.warn(`Failed to write backend evidence: ${error.message || error}`);
    return null;
  }
}

function clearBackendEvidenceFiles(payload = {}) {
  ensureEvidenceDir();
  const evidenceRoot = path.resolve(EVIDENCE_DIR);
  const dryRun = boolFromPayload(payload.dryRun);
  const deleted = [];
  const skipped = [];

  for (const entry of fs.readdirSync(evidenceRoot, { withFileTypes: true })) {
    const name = entry.name;
    const fullPath = path.resolve(evidenceRoot, name);
    if (fullPath !== evidenceRoot && !fullPath.startsWith(evidenceRoot + path.sep)) {
      skipped.push({ name, reason: "outside evidence directory" });
      continue;
    }
    if (!entry.isFile() || path.extname(name).toLowerCase() !== ".json") {
      skipped.push({ name, reason: "not a json evidence file" });
      continue;
    }
    if (!dryRun) fs.unlinkSync(fullPath);
    deleted.push(name);
  }

  return jsonResponse(true, `${dryRun ? "Would clear" : "Cleared"} ${deleted.length} backend evidence file(s)`, {
    evidenceDir: EVIDENCE_DIR,
    dryRun,
    deletedCount: deleted.length,
    deleted,
    skipped,
  }, [
    `evidenceDir: ${EVIDENCE_DIR}`,
    `deleted json files: ${deleted.length}`,
  ]);
}

function existingDirectoryFromPayload(payload = {}) {
  const raw = String(payload.projectPath || payload.path || payload.initialPath || "").trim();
  if (!raw) return null;
  const resolved = path.resolve(raw);
  try {
    const stat = fs.existsSync(resolved) ? fs.statSync(resolved) : null;
    if (stat?.isDirectory()) return resolved;
    if (stat?.isFile()) return path.dirname(resolved);
  } catch {
    return null;
  }
  return null;
}

async function handleSelectFolder(payload = {}) {
  const initialPath = existingDirectoryFromPayload(payload) || os.homedir();
  if (boolFromPayload(payload.dryRun)) {
    return jsonResponse(true, "Folder picker dry-run", {
      path: initialPath,
      selectedPath: initialPath,
      dryRun: true,
      picker: "windows-modern-file-open-dialog",
    }, [`initialPath=${initialPath}`]);
  }
  if (process.platform !== "win32") {
    return jsonResponse(false, "Folder picker is only available on Windows backend", {
      platform: process.platform,
      initialPath,
    }, [], "unsupported platform");
  }

  const script = `
$ErrorActionPreference = 'Stop'
$source = @'
using System;
using System.IO;
using System.Runtime.InteropServices;

namespace HardwareLab {
  public static class ModernFolderPicker {
    private const int ERROR_CANCELLED_HRESULT = unchecked((int)0x800704C7);

    [DllImport("user32.dll")]
    private static extern IntPtr GetForegroundWindow();

    [DllImport("shell32.dll", CharSet = CharSet.Unicode, PreserveSig = false)]
    private static extern void SHCreateItemFromParsingName(
      [MarshalAs(UnmanagedType.LPWStr)] string pszPath,
      IntPtr pbc,
      ref Guid riid,
      [MarshalAs(UnmanagedType.Interface)] out IShellItem ppv);

    [ComImport]
    [Guid("DC1C5A9C-E88A-4DDE-A5A1-60F82A20AEF7")]
    private class FileOpenDialog {
    }

    [ComImport]
    [Guid("43826D1E-E718-42EE-BC55-A1E261C37BFE")]
    [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    private interface IShellItem {
      void BindToHandler(IntPtr pbc, ref Guid bhid, ref Guid riid, out IntPtr ppv);
      void GetParent(out IShellItem ppsi);
      void GetDisplayName(SIGDN sigdnName, out IntPtr ppszName);
      void GetAttributes(uint sfgaoMask, out uint psfgaoAttribs);
      void Compare(IShellItem psi, uint hint, out int piOrder);
    }

    private enum SIGDN : uint {
      FILESYSPATH = 0x80058000
    }

    [Flags]
    private enum FOS : uint {
      OVERWRITEPROMPT = 0x00000002,
      STRICTFILETYPES = 0x00000004,
      NOCHANGEDIR = 0x00000008,
      PICKFOLDERS = 0x00000020,
      FORCEFILESYSTEM = 0x00000040,
      ALLNONSTORAGEITEMS = 0x00000080,
      NOVALIDATE = 0x00000100,
      ALLOWMULTISELECT = 0x00000200,
      PATHMUSTEXIST = 0x00000800,
      FILEMUSTEXIST = 0x00001000,
      CREATEPROMPT = 0x00002000,
      SHAREAWARE = 0x00004000,
      NOREADONLYRETURN = 0x00008000,
      NOTESTFILECREATE = 0x00010000,
      HIDEMRUPLACES = 0x00020000,
      HIDEPINNEDPLACES = 0x00040000,
      NODEREFERENCELINKS = 0x00100000,
      OKBUTTONNEEDSINTERACTION = 0x00200000,
      DONTADDTORECENT = 0x02000000,
      FORCESHOWHIDDEN = 0x10000000,
      DEFAULTNOMINIMODE = 0x20000000,
      FORCEPREVIEWPANEON = 0x40000000
    }

    [ComImport]
    [Guid("d57c7288-d4ad-4768-be02-9d969532d960")]
    [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    private interface IFileOpenDialog {
      [PreserveSig]
      int Show(IntPtr parent);
      void SetFileTypes(uint cFileTypes, IntPtr rgFilterSpec);
      void SetFileTypeIndex(uint iFileType);
      void GetFileTypeIndex(out uint piFileType);
      void Advise(IntPtr pfde, out uint pdwCookie);
      void Unadvise(uint dwCookie);
      void SetOptions(FOS fos);
      void GetOptions(out FOS pfos);
      void SetDefaultFolder(IShellItem psi);
      void SetFolder(IShellItem psi);
      void GetFolder(out IShellItem ppsi);
      void GetCurrentSelection(out IShellItem ppsi);
      void SetFileName([MarshalAs(UnmanagedType.LPWStr)] string pszName);
      void GetFileName([MarshalAs(UnmanagedType.LPWStr)] out string pszName);
      void SetTitle([MarshalAs(UnmanagedType.LPWStr)] string pszTitle);
      void SetOkButtonLabel([MarshalAs(UnmanagedType.LPWStr)] string pszText);
      void SetFileNameLabel([MarshalAs(UnmanagedType.LPWStr)] string pszLabel);
      void GetResult(out IShellItem ppsi);
      void AddPlace(IShellItem psi, int fdap);
      void SetDefaultExtension([MarshalAs(UnmanagedType.LPWStr)] string pszDefaultExtension);
      void Close(int hr);
      void SetClientGuid(ref Guid guid);
      void ClearClientData();
      void SetFilter(IntPtr pFilter);
      void GetResults(out IntPtr ppenum);
      void GetSelectedItems(out IntPtr ppsai);
    }

    public static string PickFolder(string initialPath, string title) {
      IFileOpenDialog dialog = null;
      IShellItem initialFolder = null;
      IShellItem result = null;
      try {
        dialog = (IFileOpenDialog)new FileOpenDialog();
        FOS options;
        dialog.GetOptions(out options);
        dialog.SetOptions(options | FOS.PICKFOLDERS | FOS.FORCEFILESYSTEM | FOS.PATHMUSTEXIST | FOS.NOCHANGEDIR);
        dialog.SetTitle(String.IsNullOrWhiteSpace(title) ? "Select project folder" : title);
        dialog.SetOkButtonLabel("Select Folder");

        if (!String.IsNullOrWhiteSpace(initialPath) && Directory.Exists(initialPath)) {
          Guid shellItemGuid = typeof(IShellItem).GUID;
          SHCreateItemFromParsingName(initialPath, IntPtr.Zero, ref shellItemGuid, out initialFolder);
          if (initialFolder != null) {
            dialog.SetDefaultFolder(initialFolder);
            dialog.SetFolder(initialFolder);
          }
        }

        int hr = dialog.Show(GetForegroundWindow());
        if (hr == ERROR_CANCELLED_HRESULT) return null;
        if (hr != 0) Marshal.ThrowExceptionForHR(hr);

        dialog.GetResult(out result);
        IntPtr pathPtr;
        result.GetDisplayName(SIGDN.FILESYSPATH, out pathPtr);
        try {
          return Marshal.PtrToStringUni(pathPtr);
        } finally {
          if (pathPtr != IntPtr.Zero) Marshal.FreeCoTaskMem(pathPtr);
        }
      } finally {
        if (result != null) Marshal.FinalReleaseComObject(result);
        if (initialFolder != null) Marshal.FinalReleaseComObject(initialFolder);
        if (dialog != null) Marshal.FinalReleaseComObject(dialog);
      }
    }
  }
}
'@
Add-Type -TypeDefinition $source -Language CSharp
$initial = $env:HL_INITIAL_DIR
if ($env:HL_VALIDATE_PICKER -eq '1') {
  [pscustomobject]@{ ok=$true; path=$initial; cancelled=$false; picker='windows-modern-file-open-dialog'; validated=$true } | ConvertTo-Json -Compress
  exit 0
}
$selected = [HardwareLab.ModernFolderPicker]::PickFolder($initial, 'Select embedded project folder')
if ([string]::IsNullOrWhiteSpace($selected)) {
  [pscustomobject]@{ ok=$false; path=$null; cancelled=$true; picker='windows-modern-file-open-dialog' } | ConvertTo-Json -Compress
} else {
  [pscustomobject]@{ ok=$true; path=$selected; cancelled=$false; picker='windows-modern-file-open-dialog' } | ConvertTo-Json -Compress
}
  `.trim();
  const result = await execRaw("powershell.exe", ["-NoProfile", "-Sta", "-ExecutionPolicy", "Bypass", "-Command", powershellUtf8Script(script)], {
    timeoutMs: 300000,
    env: {
      HL_INITIAL_DIR: initialPath,
      HL_VALIDATE_PICKER: boolFromPayload(payload.validatePicker) ? "1" : "",
    },
    windowsHide: false,
  });
  const parsed = parseJsonMaybe(result.stdout)[0];
  if (parsed?.ok && parsed.path) {
    const selectedPath = path.resolve(parsed.path);
    const entry = fs.existsSync(selectedPath) ? detectBuildEntry(selectedPath) : null;
    return jsonResponse(true, `Project folder selected: ${selectedPath}`, {
      path: selectedPath,
      selectedPath,
      entry,
      picker: parsed.picker || "windows-modern-file-open-dialog",
      validated: Boolean(parsed.validated),
    }, [`selectedPath=${selectedPath}`]);
  }
  if (parsed?.cancelled) {
    return jsonResponse(false, "Folder selection cancelled", {
      cancelled: true,
      initialPath,
      picker: parsed.picker || "windows-modern-file-open-dialog",
    }, ["folder selection cancelled"], "cancelled");
  }
  return jsonResponse(false, "Folder picker failed", {
    initialPath,
    stdout: result.stdout,
    stderr: result.stderr,
    code: result.code,
  }, normalizeToolOutput(result), result.error || result.stderr.trim() || "folder picker failed");
}

async function handleOpenFolder(payload = {}) {
  const folderPath = existingDirectoryFromPayload(payload);
  if (!folderPath) {
    return jsonResponse(false, "Folder not found", {
      requestedPath: payload.projectPath || payload.path || null,
    }, ["Select a valid project folder first."], "folder not found");
  }
  if (boolFromPayload(payload.dryRun)) {
    return jsonResponse(true, "Open folder dry-run", {
      path: folderPath,
      dryRun: true,
    }, [`folder=${folderPath}`]);
  }
  if (process.platform === "win32") {
    const child = spawn("explorer.exe", [folderPath], {
      detached: true,
      stdio: "ignore",
      windowsHide: false,
    });
    child.unref();
    return jsonResponse(true, `Opened folder: ${folderPath}`, { path: folderPath }, [`explorer.exe ${folderPath}`]);
  }
  const opener = process.platform === "darwin" ? "open" : "xdg-open";
  const child = spawn(opener, [folderPath], { detached: true, stdio: "ignore" });
  child.unref();
  return jsonResponse(true, `Opened folder: ${folderPath}`, { path: folderPath }, [`${opener} ${folderPath}`]);
}

function commandQuote(value) {
  return `"${String(value).replaceAll('"', '\\"')}"`;
}

function powershellUtf8Script(script) {
  return [
    "$utf8 = [System.Text.UTF8Encoding]::new($false)",
    "[Console]::InputEncoding = $utf8",
    "[Console]::OutputEncoding = $utf8",
    "$OutputEncoding = $utf8",
    script,
  ].join("\n");
}

function normalizeToolOutput(result) {
  const output = [result.stdout, result.stderr].filter(Boolean).join("\n").trim();
  return output.split(/\r?\n/).filter(Boolean);
}

function execRaw(file, args = [], options = {}) {
  return new Promise((resolve) => {
    const startedAt = Date.now();
    execFile(
      file,
      args,
      {
        cwd: options.cwd || PROJECT_ROOT,
        env: { ...process.env, ...(options.env || {}) },
        timeout: options.timeoutMs || DEFAULT_TIMEOUT_MS,
        maxBuffer: options.maxBuffer || 1024 * 1024 * 4,
        windowsHide: options.windowsHide ?? true,
      },
      (error, stdout, stderr) => {
        resolve({
          ok: !error,
          file,
          args,
          cwd: options.cwd || PROJECT_ROOT,
          stdout: String(stdout || ""),
          stderr: String(stderr || ""),
          code: typeof error?.code === "number" ? error.code : error ? 1 : 0,
          signal: error?.signal || null,
          error: error ? error.message : null,
          durationMs: Date.now() - startedAt,
        });
      },
    );
  });
}

async function findCommand(name) {
  if (process.platform === "win32") {
    const result = await execRaw("where.exe", [name], { timeoutMs: 2500 });
    const found = result.stdout.split(/\r?\n/).map((line) => line.trim()).find(Boolean);
    return found ? { ok: true, path: found } : { ok: false, error: result.stderr.trim() || result.error || "not found" };
  }

  const result = await execRaw("sh", ["-lc", `command -v ${name}`], { timeoutMs: 2500 });
  const found = result.stdout.trim().split(/\r?\n/)[0];
  return found ? { ok: true, path: found } : { ok: false, error: result.stderr.trim() || result.error || "not found" };
}

async function runTool(name, args = [], options = {}) {
  const found = options.path ? { ok: true, path: options.path } : await findCommand(name);
  if (!found.ok) {
    return {
      ok: false,
      skipped: true,
      command: name,
      args,
      stdout: "",
      stderr: "",
      code: null,
      error: `${name} not found`,
      durationMs: 0,
    };
  }

  const commandPath = found.path;
  if (process.platform === "win32" && /\.(cmd|bat)$/i.test(commandPath)) {
    const line = [commandQuote(commandPath), ...args.map(commandQuote)].join(" ");
    return execRaw("cmd.exe", ["/d", "/s", "/c", line], options);
  }
  return execRaw(commandPath, args, options);
}

function emitStreamEvent(options = {}, event = {}) {
  if (typeof options.onEvent === "function") {
    options.onEvent(event);
  }
}

function commandText(command) {
  const parts = Array.isArray(command) ? command : [String(command || "")];
  return parts.map(commandQuote).join(" ");
}

function createStreamLineSink(kind, options, pushText) {
  let buffer = "";
  return {
    push(chunk) {
      const text = chunk instanceof Buffer ? chunk.toString("utf8") : String(chunk || "");
      pushText(text);
      buffer += text;
      const parts = buffer.split(/\r\n|\n|\r/g);
      buffer = parts.pop() || "";
      for (const part of parts) {
        const line = part.trimEnd();
        if (line) emitStreamEvent(options, { type: kind, step: options.step || null, phase: options.phase || null, line });
      }
    },
    flush() {
      const line = buffer.trimEnd();
      if (line) emitStreamEvent(options, { type: kind, step: options.step || null, phase: options.phase || null, line });
      buffer = "";
    },
  };
}

function execRawStream(file, args = [], options = {}) {
  return new Promise((resolve) => {
    const startedAt = Date.now();
    const cwd = options.cwd || PROJECT_ROOT;
    const displayCommand = options.displayCommand || [file, ...args];
    const emitBase = {
      step: options.step || null,
      phase: options.phase || null,
      command: displayCommand,
      commandText: commandText(displayCommand),
      cwd,
    };
    let stdout = "";
    let stderr = "";
    let finished = false;
    let timedOut = false;
    let child = null;
    let timer = null;

    const stdoutSink = createStreamLineSink("stdout", options, (text) => {
      stdout += text;
    });
    const stderrSink = createStreamLineSink("stderr", options, (text) => {
      stderr += text;
    });

    function finish(code, signal, errorMessage = null) {
      if (finished) return;
      finished = true;
      if (timer) clearTimeout(timer);
      stdoutSink.flush();
      stderrSink.flush();
      const durationMs = Date.now() - startedAt;
      const ok = !errorMessage && !timedOut && code === 0;
      emitStreamEvent(options, {
        type: "command-done",
        ...emitBase,
        ok,
        code,
        signal: timedOut ? "timeout" : signal || null,
        durationMs,
        error: ok ? null : errorMessage || (timedOut ? `command timed out after ${options.timeoutMs || DEFAULT_TIMEOUT_MS}ms` : null),
      });
      resolve({
        ok,
        file,
        args,
        cwd,
        stdout,
        stderr,
        code: typeof code === "number" ? code : ok ? 0 : 1,
        signal: timedOut ? "timeout" : signal || null,
        error: ok ? null : errorMessage || (timedOut ? `command timed out after ${options.timeoutMs || DEFAULT_TIMEOUT_MS}ms` : null),
        durationMs,
      });
    }

    emitStreamEvent(options, { type: "cmd", ...emitBase });

    try {
      child = spawn(file, args, {
        cwd,
        env: {
          ...process.env,
          PYTHONUNBUFFERED: "1",
          PYTHONIOENCODING: "utf-8",
          ...(options.env || {}),
        },
        windowsHide: true,
      });
    } catch (error) {
      emitStreamEvent(options, { type: "error", ...emitBase, error: error.message || String(error) });
      finish(1, null, error.message || String(error));
      return;
    }

    child.stdout?.on("data", (chunk) => stdoutSink.push(chunk));
    child.stderr?.on("data", (chunk) => stderrSink.push(chunk));
    child.on("error", (error) => {
      emitStreamEvent(options, { type: "error", ...emitBase, error: error.message || String(error) });
      finish(1, null, error.message || String(error));
    });
    child.on("close", (code, signal) => finish(code, signal));

    timer = setTimeout(() => {
      timedOut = true;
      emitStreamEvent(options, {
        type: "stderr",
        step: options.step || null,
        phase: options.phase || null,
        line: `command timed out after ${options.timeoutMs || DEFAULT_TIMEOUT_MS}ms`,
      });
      try {
        child.kill();
      } catch {
        finish(1, "timeout", "failed to kill timed-out command");
      }
    }, options.timeoutMs || DEFAULT_TIMEOUT_MS);
  });
}

async function runToolStream(name, args = [], options = {}) {
  const found = options.path ? { ok: true, path: options.path } : await findCommand(name);
  if (!found.ok) {
    const result = {
      ok: false,
      skipped: true,
      command: name,
      args,
      stdout: "",
      stderr: "",
      code: null,
      error: `${name} not found`,
      durationMs: 0,
    };
    emitStreamEvent(options, {
      type: "error",
      step: options.step || null,
      phase: options.phase || null,
      command: [name, ...args],
      commandText: commandText([name, ...args]),
      error: result.error,
    });
    return result;
  }

  const commandPath = found.path;
  if (process.platform === "win32" && /\.(cmd|bat)$/i.test(commandPath)) {
    const line = [commandQuote(commandPath), ...args.map(commandQuote)].join(" ");
    return execRawStream("cmd.exe", ["/d", "/s", "/c", line], {
      ...options,
      displayCommand: [name, ...args],
    });
  }
  return execRawStream(commandPath, args, {
    ...options,
    displayCommand: [name, ...args],
  });
}

function runToolLive(name, args = [], options = {}) {
  return typeof options.onEvent === "function" ? runToolStream(name, args, options) : runTool(name, args, options);
}

function boolFromPayload(value) {
  if (value === true) return true;
  if (typeof value === "string") return /^(true|1|yes|on)$/i.test(value.trim());
  return false;
}

function isArtifactFile(filePath) {
  return /\.(hex|elf|axf|bin|out)$/i.test(String(filePath || ""));
}

function walkFiles(root, options = {}) {
  if (!root || !fs.existsSync(root)) return [];
  const maxDepth = options.maxDepth ?? 5;
  const maxFiles = options.maxFiles ?? 5000;
  const files = [];
  function visit(current, depth) {
    if (depth > maxDepth || files.length >= maxFiles) return;
    let entries = [];
    try {
      entries = fs.readdirSync(current, { withFileTypes: true });
    } catch {
      return;
    }
    for (const entry of entries) {
      if (files.length >= maxFiles) return;
      const fullPath = path.join(current, entry.name);
      if (entry.isDirectory()) {
        if (/^(\.git|node_modules|site|site_cloudflare_ascii|site_edgeone_ascii)$/i.test(entry.name)) continue;
        visit(fullPath, depth + 1);
      } else if (entry.isFile()) {
        files.push(fullPath);
      }
    }
  }
  visit(root, 0);
  return files;
}

function artifactInfo(fullPath, freshSinceMs = null) {
  const stat = fs.statSync(fullPath);
  const normalizedFreshSince = Number(freshSinceMs || 0);
  return {
    path: fullPath,
    name: path.basename(fullPath),
    ext: path.extname(fullPath).toLowerCase(),
    size: stat.size,
    updatedAt: stat.mtime.toISOString(),
    updatedAtMs: stat.mtimeMs,
    fresh: normalizedFreshSince > 0 ? stat.mtimeMs >= normalizedFreshSince - 2000 : false,
  };
}

function readJsonFileMaybe(fullPath) {
  try {
    if (!fs.existsSync(fullPath)) return null;
    return JSON.parse(fs.readFileSync(fullPath, "utf8"));
  } catch {
    return null;
  }
}

function relativeToProject(projectRoot, fullPath) {
  return path.relative(projectRoot, fullPath).replace(/\\/g, "/");
}

function findMspm0Sdk(payload = {}) {
  const requested = String(payload.mspm0SdkPath || payload.sdkPath || process.env.MSPM0_SDK_INSTALL_DIR || "").trim();
  if (requested && fs.existsSync(path.join(requested, "imports.mak"))) {
    return { ok: true, path: requested, source: requested === process.env.MSPM0_SDK_INSTALL_DIR ? "env" : "payload" };
  }
  const tiRoot = "C:\\ti";
  const common = [
    "C:\\ti\\mspm0_sdk_2_07_00_05",
    "C:\\ti\\mspm0_sdk_2_04_00_06",
  ];
  const scanned = fs.existsSync(tiRoot)
    ? fs.readdirSync(tiRoot, { withFileTypes: true })
      .filter((entry) => entry.isDirectory() && /^mspm0_sdk_/i.test(entry.name))
      .map((entry) => path.join(tiRoot, entry.name))
    : [];
  const candidates = Array.from(new Set([...common, ...scanned]))
    .filter((dir) => fs.existsSync(path.join(dir, "imports.mak")))
    .sort((a, b) => b.localeCompare(a, undefined, { numeric: true, sensitivity: "base" }));
  if (!fs.existsSync(tiRoot) && !candidates.length) return { ok: false, error: "C:\\ti not found" };
  return candidates[0] ? { ok: true, path: candidates[0], source: "auto" } : { ok: false, error: "No C:\\ti\\mspm0_sdk_*\\imports.mak found" };
}

async function findArmGccRoot(payload = {}) {
  const requested = String(payload.gccArmCompiler || payload.gccArmCompilerPath || process.env.GCC_ARMCOMPILER || "").trim();
  if (requested && fs.existsSync(path.join(requested, "bin", process.platform === "win32" ? "arm-none-eabi-gcc.exe" : "arm-none-eabi-gcc"))) {
    return { ok: true, path: requested, source: requested === process.env.GCC_ARMCOMPILER ? "env" : "payload" };
  }
  const found = await findCommand("arm-none-eabi-gcc");
  if (!found.ok) return { ok: false, error: found.error };
  return { ok: true, path: path.dirname(path.dirname(found.path)), source: "path" };
}

async function buildMakeEnvironment(entry, payload = {}) {
  const env = {};
  const hints = [];
  let makefileText = "";
  try {
    makefileText = fs.readFileSync(entry.makefile, "utf8");
  } catch {
    makefileText = "";
  }
  const needsMspm0Sdk = /MSPM0_SDK_INSTALL_DIR|imports\.mak|mspm0/i.test(`${entry.makefile}\n${makefileText}`);
  if (needsMspm0Sdk) {
    const sdk = findMspm0Sdk(payload);
    if (sdk.ok) {
      env.MSPM0_SDK_INSTALL_DIR = sdk.path.replace(/\\/g, "/");
      hints.push(`MSPM0_SDK_INSTALL_DIR=${env.MSPM0_SDK_INSTALL_DIR} (${sdk.source})`);
    } else {
      hints.push(`MSPM0_SDK_INSTALL_DIR not resolved: ${sdk.error}`);
    }
  }
  const needsGcc = /GCC_ARMCOMPILER|arm-none-eabi/i.test(`${entry.makefile}\n${makefileText}`);
  if (needsGcc) {
    const gcc = await findArmGccRoot(payload);
    if (gcc.ok) {
      env.GCC_ARMCOMPILER = gcc.path.replace(/\\/g, "/");
      hints.push(`GCC_ARMCOMPILER=${env.GCC_ARMCOMPILER} (${gcc.source})`);
    } else {
      hints.push(`GCC_ARMCOMPILER not resolved: ${gcc.error}`);
    }
  }
  return { env, hints };
}

async function toolStatus(check) {
  const found = await findCommand(check.name);
  if (!found.ok) {
    return {
      available: false,
      path: null,
      version: null,
      error: found.error,
    };
  }
  const versionRun = await runTool(check.name, check.args, { path: found.path, timeoutMs: 3500 });
  return {
    available: true,
    path: found.path,
    version: normalizeToolOutput(versionRun)[0] || null,
    error: versionRun.ok ? null : versionRun.error || versionRun.stderr.trim() || "version check failed",
  };
}

async function collectToolSummary() {
  const pairs = await Promise.all(TOOL_CHECKS.map(async (check) => [check.name, await toolStatus(check)]));
  return Object.fromEntries(pairs);
}

function parseRequestBody(req) {
  return new Promise((resolve, reject) => {
    let raw = "";
    req.on("data", (chunk) => {
      raw += chunk;
      if (raw.length > 1024 * 1024) {
        reject(new Error("request body too large"));
        req.destroy();
      }
    });
    req.on("end", () => {
      if (!raw.trim()) {
        resolve({});
        return;
      }
      try {
        resolve(JSON.parse(raw));
      } catch (error) {
        reject(new Error(`invalid JSON body: ${error.message}`));
      }
    });
    req.on("error", reject);
  });
}

function resolveProjectPath(projectPath) {
  const raw = String(projectPath || "").trim();
  if (!raw) return { ok: false, error: "projectPath is empty", path: null };
  const resolved = path.resolve(raw);
  if (!fs.existsSync(resolved)) return { ok: false, error: `projectPath does not exist: ${resolved}`, path: resolved };
  const stat = fs.statSync(resolved);
  if (!stat.isDirectory()) return { ok: false, error: `projectPath is not a directory: ${resolved}`, path: resolved };
  return { ok: true, path: resolved, stat };
}

function parseJsonMaybe(text) {
  const raw = String(text || "").trim();
  if (!raw) return [];
  const candidates = [
    raw,
    ...raw.split(/\r?\n/g).map((line) => line.trim()).filter(Boolean).reverse(),
  ];
  for (const candidate of candidates) {
    if (!candidate.startsWith("{") && !candidate.startsWith("[")) continue;
    try {
      const parsed = JSON.parse(candidate);
      return Array.isArray(parsed) ? parsed : [parsed];
    } catch {
      // Try the next candidate line; some tools print warnings before JSON.
    }
  }
  return [];
}

function normalizeDeviceList(value) {
  if (!value) return [];
  return (Array.isArray(value) ? value : [value]).filter(Boolean);
}

function extractComPort(value) {
  return String(value || "").match(/\bCOM\d+\b/i)?.[0]?.toUpperCase() || "";
}

function hasBrokenWindowsText(value) {
  return /[\uFFFD]/.test(String(value || ""));
}

function cleanWindowsText(value) {
  const text = String(value || "").trim();
  return text && !hasBrokenWindowsText(text) ? text : "";
}

function normalizeWindowsUsbDevice(device = {}) {
  const rawName = String(device.Name || device.name || "");
  const rawDescription = String(device.Description || device.description || "");
  const rawDeviceId = String(device.DeviceID || device.deviceId || "");
  const rawService = String(device.Service || device.service || "");
  const port = extractComPort(`${rawName} ${rawDescription} ${rawDeviceId}`);
  const isBluetooth = /BTHENUM|BTHMODEM|bluetooth/i.test(`${rawName} ${rawDescription} ${rawDeviceId} ${rawService}`);
  const isUsbSerial = /usbser|USB\\|VID_/i.test(`${rawDeviceId} ${rawService}`);
  let name = cleanWindowsText(rawName);
  let description = cleanWindowsText(rawDescription);
  if (!name && port) name = `${isBluetooth ? "Bluetooth Serial" : isUsbSerial ? "USB Serial Device" : "Serial Port"} (${port})`;
  if (!description && port) description = isBluetooth ? "Bluetooth Serial" : isUsbSerial ? "USB Serial Device" : "Serial Port";
  return {
    name,
    description,
    manufacturer: cleanWindowsText(device.Manufacturer || device.manufacturer || ""),
    pnpClass: cleanWindowsText(device.PNPClass || device.pnpClass || ""),
    deviceId: rawDeviceId,
    status: cleanWindowsText(device.Status || device.status || ""),
    service: rawService,
    port,
    isBluetooth,
  };
}

function classifyUsbDevice(device) {
  const text = [
    device.name,
    device.description,
    device.manufacturer,
    device.pnpClass,
    device.deviceId,
    device.service,
  ].filter(Boolean).join(" ");
  const bluetoothSerial = device.isBluetooth || /BTHENUM|BTHMODEM|bluetooth/i.test(text);
  const cmsisDap = /cmsis[-\s]?dap|daplink|vid_faed|pid_4870|hid.*dap/i.test(text);
  const stlink = /st-?link|stlink|stmicroelectronics|vid_0483.*pid_37(4|5)/i.test(text);
  const serialBridge = !bluetoothSerial && /\bCOM\d+\b|usb serial|usb[-\s]?uart|usbser|ch340|cp210|ft232|cdc/i.test(text);
  const vidPid = text.match(/VID_([0-9A-F]{4}).*PID_([0-9A-F]{4})/i);
  return {
    ...device,
    vid: vidPid?.[1]?.toUpperCase() || null,
    pid: vidPid?.[2]?.toUpperCase() || null,
    kind: cmsisDap ? "cmsis-dap" : stlink ? "stlink" : serialBridge ? "serial-bridge" : bluetoothSerial ? "bluetooth-serial" : "usb",
    matches: { cmsisDap, stlink, serialBridge },
  };
}

async function scanWindowsUsb() {
  if (process.platform !== "win32") {
    return {
      ok: true,
      devices: [],
      cmsisDap: [],
      stlink: [],
      serialBridge: [],
      logs: ["USB PnP scan is only available on Windows."],
      error: null,
    };
  }

  const script = [
    "$ErrorActionPreference = 'SilentlyContinue'",
    "$patterns = 'DAPLink|CMSIS-DAP|CMSIS DAP|ST-LINK|STLINK|STMicroelectronics|VID_FAED|PID_4870|VID_0483|PID_374|USB Serial|USB UART|CH340|CP210|FT232'",
    "$devices = @(Get-CimInstance Win32_PnPEntity | Where-Object {",
    "  ($_.Name -match $patterns) -or ($_.Description -match $patterns) -or ($_.Manufacturer -match $patterns) -or ($_.DeviceID -match $patterns) -or ($_.PNPClass -match 'Ports|USB|HIDClass')",
    "} | Select-Object Name,Description,Manufacturer,PNPClass,DeviceID,Status,Service)",
    "$devices | ConvertTo-Json -Depth 5 -Compress",
  ].join("; ");
  const result = await execRaw("powershell.exe", ["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", powershellUtf8Script(script)], {
    timeoutMs: 10000,
    maxBuffer: 1024 * 1024 * 6,
  });
  const devices = normalizeDeviceList(parseJsonMaybe(result.stdout))
    .map((device) => classifyUsbDevice(normalizeWindowsUsbDevice(device)));
  return {
    ok: result.ok,
    devices,
    cmsisDap: devices.filter((device) => device.matches.cmsisDap),
    stlink: devices.filter((device) => device.matches.stlink),
    serialBridge: devices.filter((device) => device.matches.serialBridge),
    stdout: result.stdout,
    stderr: result.stderr,
    logs: normalizeToolOutput(result).slice(0, 40),
    error: result.ok ? null : result.error || result.stderr.trim() || "Windows USB scan failed",
  };
}

async function scanSerialPorts() {
  if (process.platform !== "win32") {
    const result = await runTool("python", ["-c", "import glob,json; print(json.dumps([{'device':p,'name':p,'description':'serial'} for p in glob.glob('/dev/tty*')]))"], {
      timeoutMs: 5000,
    });
    return {
      ok: result.ok,
      ports: parseJsonMaybe(result.stdout),
      logs: normalizeToolOutput(result),
      error: result.ok ? null : result.error,
    };
  }

  const script = [
    "$ports = @()",
    "$cim = @(Get-CimInstance Win32_SerialPort -ErrorAction SilentlyContinue)",
    "foreach ($p in $cim) { $ports += [pscustomobject]@{ device=$p.DeviceID; name=$p.Name; description=$p.Description; pnpDeviceId=$p.PNPDeviceID } }",
    "if ($ports.Count -eq 0) { foreach ($p in [System.IO.Ports.SerialPort]::GetPortNames()) { $ports += [pscustomobject]@{ device=$p; name=$p; description='SerialPort'; pnpDeviceId='' } } }",
    "$ports | ConvertTo-Json -Depth 4 -Compress",
  ].join("; ");
  const result = await execRaw("powershell.exe", ["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", powershellUtf8Script(script)], {
    timeoutMs: 7000,
  });
  return {
    ok: result.ok,
    ports: normalizeDeviceList(parseJsonMaybe(result.stdout)).map((port) => {
      const device = String(port.DeviceID || port.device || port.name || "").trim();
      const rawName = String(port.Name || port.name || "");
      const rawDescription = String(port.Description || port.description || "");
      const pnpDeviceId = String(port.PNPDeviceID || port.pnpDeviceId || "");
      return {
        device: extractComPort(device) || device,
        name: cleanWindowsText(rawName) || extractComPort(`${rawName} ${pnpDeviceId}`) || device,
        description: cleanWindowsText(rawDescription) || "Serial Port",
        pnpDeviceId,
      };
    }),
    logs: normalizeToolOutput(result),
    error: result.ok ? null : result.error || result.stderr.trim(),
  };
}

function parsePyocdProbes(stdout) {
  const lines = String(stdout || "").split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
  if (lines.some((line) => /no available debug probes/i.test(line))) {
    return [];
  }
  return lines
    .filter((line) => /cmsis|dap|stlink|board|\b[0-9a-f]{8,}\b/i.test(line))
    .map((line) => {
      const uid = line.match(/\b([0-9A-Fa-f]{8,})\b/)?.[1] || null;
      return { uid, line };
    })
    .filter((probe) => probe.uid);
}

async function scanPyocd(stream = null) {
  const onEvent = stream && typeof stream.onEvent === "function" ? stream.onEvent : null;
  const result = await runToolLive("pyocd", ["list", "--probes"], {
    timeoutMs: 25000,
    onEvent,
    step: "gate",
    phase: "pyocd-list",
  });
  return {
    ok: result.ok,
    probes: result.ok ? parsePyocdProbes(result.stdout) : [],
    stdout: result.stdout,
    stderr: result.stderr,
    logs: normalizeToolOutput(result),
    error: result.ok ? null : result.error || result.stderr.trim() || "pyocd scan failed",
  };
}

function parseStlinkProbes(text, source) {
  const raw = String(text || "");
  if (/no\s+(st-?link|debug\s+probe)|not\s+found|no\s+probe/i.test(raw)) {
    return [];
  }
  const lines = raw.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
  const probes = [];
  for (const line of lines) {
    if (!/st-?link|serial|sn\s*[:=]|probe|debugger|stlink/i.test(line)) continue;
    const uid =
      line.match(/\b(?:sn|serial(?:\s+number)?)\s*[:=]\s*([0-9A-Za-z]{8,})\b/i)?.[1] ||
      line.match(/\b([0-9A-Fa-f]{12,})\b/)?.[1] ||
      null;
    if (uid || /st-?link/i.test(line)) {
      probes.push({ uid, line, source });
    }
  }
  return probes;
}

async function scanStlinkCube() {
  const result = await runTool("STM32_Programmer_CLI", ["-l"], { timeoutMs: 15000 });
  const output = [result.stdout, result.stderr].filter(Boolean).join("\n");
  return {
    ok: result.ok,
    probes: result.ok ? parseStlinkProbes(output, "STM32_Programmer_CLI") : [],
    stdout: result.stdout,
    stderr: result.stderr,
    logs: normalizeToolOutput(result),
    error: result.ok ? null : result.error || result.stderr.trim() || "STM32_Programmer_CLI scan failed",
  };
}

async function scanStlinkInfo() {
  const result = await runTool("st-info", ["--probe"], { timeoutMs: 9000 });
  const output = [result.stdout, result.stderr].filter(Boolean).join("\n");
  return {
    ok: result.ok,
    probes: result.ok ? parseStlinkProbes(output, "st-info") : [],
    stdout: result.stdout,
    stderr: result.stderr,
    logs: normalizeToolOutput(result),
    error: result.ok ? null : result.error || result.stderr.trim() || "st-info scan failed",
  };
}

async function scanStlink() {
  const [cube, stInfo] = await Promise.all([scanStlinkCube(), scanStlinkInfo()]);
  const found = new Map();
  for (const probe of [...cube.probes, ...stInfo.probes]) {
    const key = String(probe.uid || probe.line || "").trim().toLowerCase();
    if (key && !found.has(key)) found.set(key, probe);
  }
  return {
    ok: cube.ok || stInfo.ok,
    probes: Array.from(found.values()),
    cube,
    stInfo,
    logs: [...cube.logs.slice(0, 24), ...stInfo.logs.slice(0, 24)],
    error: cube.ok || stInfo.ok ? null : [cube.error, stInfo.error].filter(Boolean).join("; "),
  };
}

async function handleHealth() {
  const tools = await collectToolSummary();
  const availableCount = Object.values(tools).filter((tool) => tool.available).length;
  const data = {
    version: VERSION,
    cwd: process.cwd(),
    projectRoot: PROJECT_ROOT,
    pid: process.pid,
    platform: process.platform,
    node: process.version,
    hostname: os.hostname(),
    tools,
  };
  return jsonResponse(true, `Hardware Lab backend online: ${availableCount}/${TOOL_CHECKS.length} tools visible`, data, [
    `version=${VERSION}`,
    `cwd=${process.cwd()}`,
    `projectRoot=${PROJECT_ROOT}`,
  ]);
}

function makeReadinessDiagnostics(payload, tools, pyocd, stlink, serial, usb) {
  const targetProbe = String(payload?.targetProbe || "").trim();
  const programmer = programmerKind(payload);
  const wantsStlink = programmer === "stlink";
  const pyocdProbeCount = pyocd.probes.length;
  const stlinkProbeCount = stlink.probes.length;
  const cmsisUsbCount = usb.cmsisDap.length;
  const stlinkUsbCount = usb.stlink.length;
  const serialCount = serial.ports.length;
  const targetSeenByPyocd = Boolean(targetProbe && probeMatchesUid(pyocd, targetProbe));
  const targetSeenByStlink = Boolean(targetProbe && stlinkProbeMatchesUid(stlink, targetProbe));
  const targetSeenByUsb = Boolean(targetProbe && usb.devices.some((device) => {
    const line = [device.name, device.description, device.deviceId].filter(Boolean).join(" ").toLowerCase();
    return line.includes(targetProbe.toLowerCase());
  }));
  const checks = [
    { id: "backend", level: "ok", label: "Backend", value: VERSION },
    { id: "pyocdTool", level: tools.pyocd?.available ? "ok" : "warn", label: "pyOCD", value: tools.pyocd?.available ? "available" : "missing" },
    { id: "cubeTool", level: tools.STM32_Programmer_CLI?.available ? "ok" : "warn", label: "STM32CubeProgrammer", value: tools.STM32_Programmer_CLI?.available ? "available" : "missing" },
    { id: "windowsCmsisDap", level: cmsisUsbCount > 0 ? "ok" : "warn", label: "Windows CMSIS-DAP", value: `${cmsisUsbCount} device(s)` },
    { id: "pyocdProbes", level: pyocdProbeCount > 0 ? "ok" : "warn", label: "pyOCD probe list", value: `${pyocdProbeCount} probe(s)` },
    { id: "windowsStlink", level: stlinkUsbCount > 0 ? "ok" : "warn", label: "Windows STLink", value: `${stlinkUsbCount} device(s)` },
    { id: "stlinkProbes", level: stlinkProbeCount > 0 ? "ok" : "warn", label: "STLink scan", value: `${stlinkProbeCount} probe(s)` },
    { id: "serialPorts", level: serialCount > 0 ? "ok" : "warn", label: "Serial ports", value: `${serialCount} port(s)` },
  ];

  let verdict = "No programmer ready";
  let level = "warn";
  let nextAction = "Connect DAPLink/STLink USB, target power, GND, SWDIO, SWCLK, and NRST/RST, then scan again.";
  if (!wantsStlink && targetSeenByPyocd) {
    verdict = "Target DAPLink is visible to pyOCD";
    level = "ok";
    nextAction = "Build + Flash can proceed after a fresh build and real-flash confirmation.";
  } else if (wantsStlink && targetSeenByStlink) {
    verdict = "Target STLink is visible to STM32 tooling";
    level = "ok";
    nextAction = "Build + Flash can proceed after a fresh build and real-flash confirmation.";
  } else if (!wantsStlink && cmsisUsbCount > 0 && pyocdProbeCount === 0) {
    verdict = "Windows sees CMSIS-DAP, but pyOCD cannot list it";
    nextAction = "Close other SWD owners, replug DAPLink, check driver/HID access, then rescan before flashing.";
  } else if (wantsStlink && stlinkUsbCount > 0 && stlinkProbeCount === 0) {
    verdict = "Windows sees STLink, but STM32 tooling cannot list it";
    nextAction = "Check STM32CubeProgrammer install/driver, close other SWD owners, replug STLink, then rescan.";
  } else if (targetProbe && targetSeenByUsb) {
    verdict = "Target serial appears in Windows USB, but programmer scan does not own it";
    nextAction = "Treat this as a driver/tool ownership issue; do not flash until the target appears in pyOCD/STLink scan.";
  } else if (!wantsStlink && pyocdProbeCount > 0) {
    verdict = "CMSIS-DAP probe(s) exist, but not the configured UID";
    nextAction = `Choose the exact UID from scan output or reconnect the expected target ${targetProbe || ""}.`.trim();
  } else if (wantsStlink && stlinkProbeCount > 0) {
    verdict = "STLink probe(s) exist, but not the configured serial";
    nextAction = `Choose the exact STLink serial from scan output or reconnect the expected target ${targetProbe || ""}.`.trim();
  }

  return {
    programmer,
    targetProbe: targetProbe || null,
    level,
    verdict,
    nextAction,
    target: {
      seenByUsb: targetSeenByUsb,
      seenByPyocd: targetSeenByPyocd,
      seenByStlink: targetSeenByStlink,
    },
    counts: {
      pyocdProbes: pyocdProbeCount,
      stlinkProbes: stlinkProbeCount,
      windowsCmsisDap: cmsisUsbCount,
      windowsStlink: stlinkUsbCount,
      serialPorts: serialCount,
      usbDevices: usb.devices.length,
    },
    checks,
  };
}

async function handleScan(payload = {}) {
  const [tools, pyocd, stlink, serial, usb] = await Promise.all([collectToolSummary(), scanPyocd(), scanStlink(), scanSerialPorts(), scanWindowsUsb()]);
  const project = resolveProjectPath(payload.projectPath);
  const buildEntry = project.ok ? detectBuildEntry(project.path) : null;
  const probeCount = pyocd.probes.length;
  const stlinkCount = stlink.probes.length;
  const portCount = serial.ports.length;
  const diagnostics = makeReadinessDiagnostics(payload, tools, pyocd, stlink, serial, usb);
  const ready = Boolean((tools.pyocd?.available && probeCount > 0) || (tools.STM32_Programmer_CLI?.available && stlinkCount > 0));
  const logs = [
    `pyocd=${tools.pyocd?.available ? "available" : "missing"}`,
    `cmsisDapProbes=${probeCount}`,
    `stlink=${tools.STM32_Programmer_CLI?.available ? "available" : "missing"}`,
    `stlinkProbes=${stlinkCount}`,
    `serialPorts=${portCount}`,
    `windowsUsbDevices=${usb.devices.length}`,
    `windowsCmsisDap=${usb.cmsisDap.length}`,
    `windowsStlink=${usb.stlink.length}`,
    `readiness=${diagnostics.level}: ${diagnostics.verdict}`,
    `nextAction=${diagnostics.nextAction}`,
    project.ok ? `buildEntry=${buildEntry.type}: ${buildEntry.reason}` : `project=${project.error}`,
    ...pyocd.logs.slice(0, 20),
    ...stlink.logs.slice(0, 24),
    ...serial.logs.slice(0, 20),
    ...usb.logs.slice(0, 20),
  ];
  return jsonResponse(true, `Scan complete: ${probeCount} CMSIS-DAP probe(s), ${stlinkCount} STLink probe(s), ${portCount} serial port(s)`, {
    ready,
    tools,
    pyocd,
    stlink,
    serial,
    usb,
    diagnostics,
    project: project.ok ? { ok: true, path: project.path, entry: buildEntry } : { ok: false, path: project.path, error: project.error },
  }, logs);
}

function summarizeProject(projectRoot) {
  const topFiles = new Set();
  try {
    for (const entry of fs.readdirSync(projectRoot, { withFileTypes: true })) {
      if (entry.isFile()) topFiles.add(entry.name);
    }
  } catch {
    // resolveProjectPath already guards this path; keep the summary resilient.
  }
  const files = walkFiles(projectRoot, { maxDepth: 3, maxFiles: 2000 });
  const markerFiles = files.filter((file) => {
    const name = path.basename(file);
    return /^CMakeLists\.txt$/i.test(name) ||
      /^CMake(User)?Presets\.json$/i.test(name) ||
      /^GNUmakefile$/i.test(name) ||
      /^makefile$/i.test(name) ||
      /^Makefile(\..+)?$/i.test(name) ||
      /\.uvprojx$/i.test(name) ||
      /^\.ccsproject$/i.test(name) ||
      /^\.project$/i.test(name) ||
      /\.syscfg$/i.test(name);
  });
  const cmakePresets = readJsonFileMaybe(path.join(projectRoot, "CMakePresets.json"));
  const buildPresets = Array.isArray(cmakePresets?.buildPresets) ? cmakePresets.buildPresets.map((preset) => preset.name).filter(Boolean) : [];
  const configurePresets = Array.isArray(cmakePresets?.configurePresets) ? cmakePresets.configurePresets.map((preset) => preset.name).filter(Boolean) : [];
  return {
    root: projectRoot,
    markers: markerFiles.map((file) => relativeToProject(projectRoot, file)).sort(),
    cmake: {
      hasLists: fs.existsSync(path.join(projectRoot, "CMakeLists.txt")),
      hasPresets: fs.existsSync(path.join(projectRoot, "CMakePresets.json")),
      configurePresets,
      buildPresets,
    },
    makefiles: markerFiles
      .filter((file) => /(^|\/|\\)(GNUmakefile|makefile|Makefile(\..+)?)$/i.test(file))
      .map((file) => relativeToProject(projectRoot, file)),
    keilProjects: markerFiles.filter((file) => /\.uvprojx$/i.test(file)).map((file) => relativeToProject(projectRoot, file)),
    ccsProjects: markerFiles.filter((file) => /(^|\/|\\)(\.ccsproject|\.project)$/i.test(file)).map((file) => relativeToProject(projectRoot, file)),
    syscfgFiles: markerFiles.filter((file) => /\.syscfg$/i.test(file)).map((file) => relativeToProject(projectRoot, file)),
    topFiles: Array.from(topFiles).sort().slice(0, 80),
  };
}

function chooseMakefile(projectRoot, summary) {
  const priority = [
    "Makefile",
    "makefile",
    "GNUmakefile",
    "Makefile.mspm0g3507",
    "Makefile.mspm0",
    "Makefile.gcc",
    "gcc/makefile",
    "gcc/Makefile",
    "ticlang/makefile",
    "ticlang/Makefile",
    "gnu/makefile",
    "gnu/Makefile",
  ].map((name) => path.join(projectRoot, name));
  for (const candidate of priority) {
    if (fs.existsSync(candidate)) return candidate;
  }
  const topLevelMakefile = summary.makefiles
    .map((rel) => path.join(projectRoot, rel))
    .find((file) => path.dirname(file) === projectRoot);
  return topLevelMakefile || null;
}

function detectBuildEntry(projectRoot) {
  const summary = summarizeProject(projectRoot);
  const cmakeLists = path.join(projectRoot, "CMakeLists.txt");
  const buildDir = path.join(projectRoot, "build");
  const buildNinja = path.join(buildDir, "build.ninja");
  const buildMakefile = path.join(buildDir, "Makefile");
  const cmakeBuildPreset = summary.cmake.buildPresets[0] || null;

  if (fs.existsSync(cmakeLists)) {
    return {
      type: "cmake",
      root: projectRoot,
      buildDir,
      configured: fs.existsSync(buildNinja) || fs.existsSync(buildMakefile),
      buildPreset: cmakeBuildPreset,
      configurePresets: summary.cmake.configurePresets,
      buildPresets: summary.cmake.buildPresets,
      summary,
      reason: "CMakeLists.txt found",
    };
  }

  const makefile = chooseMakefile(projectRoot, summary);
  if (makefile) {
    return {
      type: "makefile",
      root: projectRoot,
      makefile,
      cwd: path.dirname(makefile),
      configured: true,
      summary,
      reason: `${path.basename(makefile)} found`,
    };
  }

  if (summary.keilProjects.length) {
    return {
      type: "keil",
      root: projectRoot,
      projectFile: path.join(projectRoot, summary.keilProjects[0]),
      configured: false,
      summary,
      reason: "Keil .uvprojx found; command-line build is not configured yet",
    };
  }

  if (summary.ccsProjects.length || summary.syscfgFiles.length) {
    return {
      type: "ccs",
      root: projectRoot,
      projectFile: summary.ccsProjects[0] ? path.join(projectRoot, summary.ccsProjects[0]) : null,
      configured: false,
      summary,
      reason: "CCS/SysConfig project markers found; command-line build is not configured yet",
    };
  }

  return {
    type: "unknown",
    root: projectRoot,
    configured: false,
    summary,
    reason: "No CMakeLists.txt or Makefile found",
  };
}

async function handleBuild(payload, stream = null) {
  const onEvent = stream && typeof stream.onEvent === "function" ? stream.onEvent : null;
  const buildRunId = `${Date.now()}-${Math.random().toString(16).slice(2, 8)}`;
  const buildStartedAtMs = Date.now();
  const project = resolveProjectPath(payload.projectPath);
  if (!project.ok) {
    return jsonResponse(false, "Build not started: project path is invalid", { projectPath: project.path }, [], project.error);
  }

  const entry = detectBuildEntry(project.path);
  if (entry.type === "unknown") {
    return jsonResponse(false, "Build not started: no supported build entry found", { entry, projectSummary: entry.summary }, [entry.reason], entry.reason);
  }

  if (entry.type === "keil" || entry.type === "ccs") {
    return jsonResponse(false, `Build not started: ${entry.type.toUpperCase()} project needs command-line build configuration`, {
      entry,
      projectSummary: entry.summary,
      supportedNow: ["cmake", "makefile"],
    }, [
      entry.reason,
      "This Hardware Lab build path currently executes only CMake and Makefile projects.",
      "The project was identified so the UI can route setup work instead of flashing old artifacts.",
    ], entry.reason);
  }

  if (entry.type === "cmake") {
    if (!entry.configured && !entry.buildPreset) {
      return jsonResponse(false, "Build not started: CMake project is not configured yet", { entry }, [
        "CMakeLists.txt found",
        `missing configured build dir: ${entry.buildDir}`,
        entry.configurePresets.length ? `configure presets: ${entry.configurePresets.join(", ")}` : "no CMake configure preset detected",
      ], "Run cmake configure first, then retry build.");
    }
    const tool = await findCommand("cmake");
    if (!tool.ok) {
      return jsonResponse(false, "Build not started: cmake not found", { entry }, [], tool.error);
    }
    const commandArgs = entry.buildPreset ? ["--build", "--preset", entry.buildPreset] : ["--build", entry.buildDir];
    const result = await runToolLive("cmake", commandArgs, {
      path: tool.path,
      cwd: project.path,
      timeoutMs: 120000,
      onEvent,
      step: "build",
      phase: "cmake",
    });
    const artifacts = discoverImages(project.path, { freshSinceMs: buildStartedAtMs });
    const freshArtifacts = artifacts.filter((artifact) => artifact.fresh);
    return jsonResponse(result.ok, result.ok ? "Build finished" : "Build failed", {
      entry,
      projectSummary: entry.summary,
      command: ["cmake", ...commandArgs],
      code: result.code,
      durationMs: result.durationMs,
      buildRunId,
      buildStartedAtMs,
      buildFinishedAtMs: Date.now(),
      artifacts,
      freshArtifacts,
      stdout: result.stdout,
      stderr: result.stderr,
    }, [
      ...normalizeToolOutput(result).slice(-80),
      entry.buildPreset ? `cmake build preset=${entry.buildPreset}` : `cmake build dir=${entry.buildDir}`,
      `artifact candidates=${artifacts.length}`,
      `fresh artifacts=${freshArtifacts.length}`,
    ], result.ok ? null : result.error || result.stderr.trim());
  }

  const makeTools = ["mingw32-make", "make", "gmake"];
  let preferredMake = null;
  let found = null;
  for (const candidate of makeTools) {
    const candidateFound = await findCommand(candidate);
    if (candidateFound.ok) {
      preferredMake = candidate;
      found = candidateFound;
      break;
    }
  }
  if (!found) {
    return jsonResponse(false, "Build not started: make tool not found", { entry, preferredTools: makeTools }, [], "mingw32-make/make/gmake not found");
  }
  const buildCwd = entry.cwd || project.path;
  const makefileArg = path.dirname(entry.makefile) === buildCwd ? path.basename(entry.makefile) : entry.makefile;
  const makeEnv = await buildMakeEnvironment(entry, payload);
  const initialArgs = ["-f", makefileArg, "all"];
  let result = await runToolLive(preferredMake, initialArgs, {
    path: found.path,
    cwd: buildCwd,
    timeoutMs: 120000,
    env: makeEnv.env,
    onEvent,
    step: "build",
    phase: "make",
  });
  let artifacts = discoverImages(project.path, { freshSinceMs: buildStartedAtMs });
  let freshArtifacts = artifacts.filter((artifact) => artifact.fresh);
  let forcedRebuild = null;
  if (result.ok && freshArtifacts.length === 0) {
    const forcedArgs = ["-B", "-f", makefileArg, "all"];
    forcedRebuild = await runToolLive(preferredMake, forcedArgs, {
      path: found.path,
      cwd: buildCwd,
      timeoutMs: 120000,
      env: makeEnv.env,
      onEvent,
      step: "build",
      phase: "make-forced",
    });
    result = forcedRebuild;
    artifacts = discoverImages(project.path, { freshSinceMs: buildStartedAtMs });
    freshArtifacts = artifacts.filter((artifact) => artifact.fresh);
  }
  return jsonResponse(result.ok, result.ok ? "Build finished" : "Build failed", {
    entry,
    projectSummary: entry.summary,
    command: [preferredMake, ...(forcedRebuild ? ["-B", "-f", makefileArg, "all"] : initialArgs)],
    initialCommand: [preferredMake, ...initialArgs],
    forcedRebuild: forcedRebuild ? {
      command: [preferredMake, "-B", "-f", makefileArg, "all"],
      code: forcedRebuild.code,
      durationMs: forcedRebuild.durationMs,
      stdout: forcedRebuild.stdout,
      stderr: forcedRebuild.stderr,
    } : null,
    cwd: buildCwd,
    environmentHints: makeEnv.hints,
    code: result.code,
    durationMs: result.durationMs,
    buildRunId,
    buildStartedAtMs,
    buildFinishedAtMs: Date.now(),
    artifacts,
    freshArtifacts,
    stdout: result.stdout,
    stderr: result.stderr,
  }, [
    ...normalizeToolOutput(result).slice(-80),
    ...makeEnv.hints,
    forcedRebuild ? "fresh artifact retry: make -B all" : "fresh artifact retry: not needed",
    `artifact candidates=${artifacts.length}`,
    `fresh artifacts=${freshArtifacts.length}`,
  ], result.ok ? null : result.error || result.stderr.trim());
}

function pyocdTargetFor(chip) {
  const normalized = String(chip || "").toLowerCase();
  if (normalized.includes("mspm0g3507")) return "mspm0g3507";
  if (normalized.includes("stm32f407")) return "stm32f407vetx";
  if (normalized.includes("stm32f103")) return "stm32f103rc";
  return null;
}

function vectorAddressFor(chip) {
  const normalized = String(chip || "").toLowerCase();
  if (normalized.includes("mspm0")) return "0x00000000";
  if (normalized.includes("stm32")) return "0x08000000";
  return "0x00000000";
}

function resolveMspm0PackPath(payload = {}) {
  const requested = String(payload.packPath || process.env.HARDWARE_LAB_MSPM0_PACK || "").trim();
  const candidates = [
    requested,
    path.join(PROJECT_ROOT, "tools", "packs", MSPM0_PACK_FILE),
    path.join(PROJECT_ROOT, "files", "packs", MSPM0_PACK_FILE),
    path.join(PROJECT_ROOT, "packs", MSPM0_PACK_FILE),
    DEFAULT_MSPM0_PACK,
  ].filter(Boolean);
  for (const candidate of candidates) {
    if (fs.existsSync(candidate)) return candidate;
  }
  return candidates[0] || DEFAULT_MSPM0_PACK;
}

function packArgsFor(chip, payload = {}) {
  const normalized = String(chip || "").toLowerCase();
  if (!normalized.includes("mspm0")) return [];
  const packPath = resolveMspm0PackPath(payload);
  if (!fs.existsSync(packPath)) {
    return { error: `MSPM0 TI DFP pack missing: ${packPath}`, args: [], packPath };
  }
  return { error: null, args: ["--pack", packPath], packPath };
}

function probeMatchesUid(pyocd, uid) {
  const exact = String(uid || "").trim().toLowerCase();
  if (!exact) return false;
  return pyocd.probes.some((probe) => {
    const probeUid = String(probe.uid || "").trim().toLowerCase();
    const line = String(probe.line || "").toLowerCase();
    return probeUid === exact || line.includes(exact);
  });
}

function stlinkProbeMatchesUid(stlink, uid) {
  const exact = String(uid || "").trim().toLowerCase();
  if (!exact) return false;
  return stlink.probes.some((probe) => {
    const probeUid = String(probe.uid || "").trim().toLowerCase();
    const line = String(probe.line || "").toLowerCase();
    return probeUid === exact || line.includes(exact);
  });
}

function programmerKind(payload = {}) {
  const text = `${payload.programmerType || ""} ${payload.programmer || ""} ${payload.tool || ""}`.toLowerCase();
  return /st-?link|stlink|cube/.test(text) ? "stlink" : "daplink";
}

function isStm32Target(chip) {
  return /stm32/i.test(String(chip || ""));
}

function artifactFromPayload(projectRoot, payload = {}) {
  const requested = payload.artifactPath || payload.imagePath || payload.image || payload.artifact?.path;
  if (!requested) return { ok: false, error: "artifactPath is required", artifact: null };
  const fullPath = path.resolve(String(requested));
  const resolvedProject = path.resolve(projectRoot);
  if (fullPath !== resolvedProject && !fullPath.startsWith(resolvedProject + path.sep)) {
    return { ok: false, error: `artifactPath must stay inside projectPath: ${fullPath}`, artifact: null };
  }
  if (!fs.existsSync(fullPath)) return { ok: false, error: `artifactPath does not exist: ${fullPath}`, artifact: null };
  if (!isArtifactFile(fullPath)) return { ok: false, error: `artifact extension is not flashable: ${fullPath}`, artifact: null };
  return { ok: true, artifact: artifactInfo(fullPath, Number(payload.buildStartedAtMs || payload.freshSinceMs || 0)) };
}

function flashFrequencyFor(chip, payload = {}) {
  const requested = Number(payload.frequency || payload.swdFrequency || 0);
  if (requested > 0) return requested;
  return String(chip || "").toLowerCase().includes("mspm0") ? 500000 : 1000000;
}

function cubeFrequencyKhz(payload = {}) {
  const requested = Number(payload.cubeFrequencyKhz || payload.stlinkFrequencyKhz || payload.frequencyKhz || 0);
  return requested > 0 ? requested : 4000;
}

function cubeConnectionArgs(uid, payload = {}) {
  return ["-c", "port=SWD", `sn=${uid}`, "mode=UR", "reset=HWrst", `freq=${cubeFrequencyKhz(payload)}`];
}

async function runCubeStatusGate(uid, payload, stream = null) {
  const onEvent = stream && typeof stream.onEvent === "function" ? stream.onEvent : null;
  const found = await findCommand("STM32_Programmer_CLI");
  if (!found.ok) {
    return {
      ok: false,
      error: `STM32_Programmer_CLI not found: ${found.error}`,
      logs: [`STM32_Programmer_CLI not found: ${found.error}`],
    };
  }
  const args = cubeConnectionArgs(uid, payload);
  const result = await runToolLive("STM32_Programmer_CLI", args, {
    path: found.path,
    timeoutMs: 45000,
    onEvent,
    step: "flash",
    phase: "stlink-status",
  });
  return {
    ok: result.ok,
    command: ["STM32_Programmer_CLI", ...args],
    result,
    logs: normalizeToolOutput(result).slice(-120),
    error: result.ok ? null : result.error || result.stderr.trim() || "STM32CubeProgrammer status gate failed",
  };
}

async function runCubeFlash(uid, artifact, payload, stream = null) {
  const onEvent = stream && typeof stream.onEvent === "function" ? stream.onEvent : null;
  const found = await findCommand("STM32_Programmer_CLI");
  if (!found.ok) {
    return {
      ok: false,
      error: `STM32_Programmer_CLI not found: ${found.error}`,
      logs: [`STM32_Programmer_CLI not found: ${found.error}`],
    };
  }
  const ext = String(artifact.ext || path.extname(artifact.path)).toLowerCase();
  const writeArgs = ["-w", artifact.path];
  if (ext === ".bin") {
    const address = String(payload.flashAddress || payload.baseAddress || "").trim();
    if (!/^0x[0-9a-f]+$/i.test(address)) {
      return {
        ok: false,
        error: "STM32CubeProgrammer requires flashAddress for .bin artifacts, for example 0x08000000",
        logs: ["Missing flashAddress for .bin artifact."],
      };
    }
    writeArgs.push(address);
  }
  const args = [...cubeConnectionArgs(uid, payload), ...writeArgs, "-v", "-rst"];
  const result = await runToolLive("STM32_Programmer_CLI", args, {
    path: found.path,
    timeoutMs: 600000,
    cwd: path.dirname(artifact.path),
    onEvent,
    step: "flash",
    phase: "stlink-write",
  });
  return {
    ok: result.ok,
    command: ["STM32_Programmer_CLI", ...args],
    result,
    logs: normalizeToolOutput(result).slice(-180),
    error: result.ok ? null : result.error || result.stderr.trim() || "STM32CubeProgrammer flash failed",
  };
}

async function runPyocdStatusGate(target, uid, chip, payload, stream = null) {
  const onEvent = stream && typeof stream.onEvent === "function" ? stream.onEvent : null;
  const pack = packArgsFor(chip, payload);
  if (pack.error) {
    return { ok: false, error: pack.error, logs: [pack.error], packPath: pack.packPath };
  }
  const frequency = flashFrequencyFor(chip, payload);
  const args = [
    "commander",
    "--no-config",
    "-O",
    "cmsis_dap.deferred_transfers=false",
    "-O",
    "cmsis_dap.limit_packets=true",
    ...pack.args,
    "-u",
    uid,
    "-t",
    target,
    "-f",
    String(frequency),
    "-c",
    "status",
    "-c",
    "exit",
  ];
  const result = await runToolLive("pyocd", args, {
    timeoutMs: 30000,
    onEvent,
    step: "flash",
    phase: "pyocd-status",
  });
  return {
    ok: result.ok,
    command: ["pyocd", ...args],
    result,
    logs: normalizeToolOutput(result).slice(-80),
    error: result.ok ? null : result.error || result.stderr.trim() || "pyOCD status gate failed",
  };
}

async function runPyocdFlash(target, uid, chip, artifact, payload, stream = null) {
  const onEvent = stream && typeof stream.onEvent === "function" ? stream.onEvent : null;
  const pack = packArgsFor(chip, payload);
  if (pack.error) {
    return { ok: false, error: pack.error, logs: [pack.error], packPath: pack.packPath };
  }
  const frequency = flashFrequencyFor(chip, payload);
  const common = [
    "--no-config",
    "-O",
    "cmsis_dap.deferred_transfers=false",
    "-O",
    "cmsis_dap.limit_packets=true",
    ...pack.args,
    "-u",
    uid,
    "-t",
    target,
    "-f",
    String(frequency),
  ];
  const loadArgs = ["load", ...common, "-e", "sector", artifact.path];
  const load = await runToolLive("pyocd", loadArgs, {
    timeoutMs: 600000,
    cwd: path.dirname(artifact.path),
    onEvent,
    step: "flash",
    phase: "pyocd-load",
  });
  if (!load.ok) {
    return {
      ok: false,
      phase: "load",
      command: ["pyocd", ...loadArgs],
      load,
      logs: normalizeToolOutput(load).slice(-120),
      error: load.error || load.stderr.trim() || "pyOCD load failed",
    };
  }

  const vectorAddress = vectorAddressFor(chip);
  const verifyArgs = [
    "commander",
    ...common,
    "-c",
    "reset",
    "-c",
    "go",
    "-c",
    "status",
    "-c",
    `read32 ${vectorAddress} 4`,
    "-c",
    "exit",
  ];
  const verify = await runToolLive("pyocd", verifyArgs, {
    timeoutMs: 45000,
    cwd: path.dirname(artifact.path),
    onEvent,
    step: "flash",
    phase: "pyocd-verify",
  });
  return {
    ok: verify.ok,
    phase: verify.ok ? "done" : "verify",
    command: ["pyocd", ...loadArgs],
    verifyCommand: ["pyocd", ...verifyArgs],
    load,
    verify,
    vectorAddress,
    logs: [...normalizeToolOutput(load).slice(-80), ...normalizeToolOutput(verify).slice(-80)],
    error: verify.ok ? null : verify.error || verify.stderr.trim() || "pyOCD reset/status/readback failed",
  };
}

function discoverImages(projectRoot, options = {}) {
  if (!projectRoot || !fs.existsSync(projectRoot)) return [];
  const buildDir = path.join(projectRoot, "build");
  const roots = [buildDir, projectRoot].filter((candidate, index, list) => fs.existsSync(candidate) && list.indexOf(candidate) === index);
  const found = new Map();
  const freshSinceMs = Number(options.freshSinceMs || 0);
  for (const root of roots) {
    for (const fullPath of walkFiles(root, { maxDepth: root === buildDir ? 8 : 2 })) {
      if (isArtifactFile(fullPath) && !found.has(fullPath)) {
        found.set(fullPath, artifactInfo(fullPath, freshSinceMs));
      }
    }
  }
  return Array.from(found.values())
    .sort((a, b) => {
      if (a.fresh !== b.fresh) return a.fresh ? -1 : 1;
      return b.updatedAtMs - a.updatedAtMs;
    })
    .slice(0, options.limit || 24);
}

async function handlePyocdFlash(payload, stream = null) {
  const project = resolveProjectPath(payload.projectPath);
  const target = pyocdTargetFor(payload.targetChip);
  const images = project.ok ? discoverImages(project.path, { freshSinceMs: Number(payload.buildStartedAtMs || 0) }) : [];
  const pyocd = await scanPyocd(stream);
  const exactProbe = String(payload.targetProbe || "").trim();
  const safeGate = Boolean(boolFromPayload(payload.allowRealFlash) && payload.confirmFlash === DEFAULT_FLASH_CONFIRM);
  const artifactCheck = project.ok ? artifactFromPayload(project.path, payload) : { ok: false, error: project.error, artifact: null };
  const gateFailures = [];
  if (!project.ok) gateFailures.push(project.error);
  if (!target) gateFailures.push(`Unsupported targetChip for pyOCD: ${payload.targetChip || ""}`);
  if (!exactProbe) gateFailures.push("targetProbe must be an exact non-empty probe UID");
  if (exactProbe && !probeMatchesUid(pyocd, exactProbe)) gateFailures.push(`targetProbe not found in pyOCD scan: ${exactProbe}`);
  if (!artifactCheck.ok) gateFailures.push(artifactCheck.error);
  if (artifactCheck.ok && !artifactCheck.artifact.fresh) {
    gateFailures.push("artifact is not fresh for this build run; run /api/build first and pass buildStartedAtMs + artifactPath");
  }
  if (!safeGate) {
    gateFailures.push(`real flash confirmation missing: allowRealFlash=true and confirmFlash=${DEFAULT_FLASH_CONFIRM}`);
  }

  const data = {
    mode: gateFailures.length ? "blocked" : "real",
    projectPath: project.path,
    projectOk: project.ok,
    targetChip: payload.targetChip || null,
    pyocdTarget: target,
    targetProbe: exactProbe || null,
    images,
    selectedArtifact: artifactCheck.artifact,
    pyocd,
    safetyGate: {
      realFlashAllowed: safeGate,
      failures: gateFailures,
      required: {
        allowRealFlash: true,
        confirmFlash: DEFAULT_FLASH_CONFIRM,
        targetProbe: "exact non-empty probe UID",
        artifactPath: "fresh artifact returned by /api/build",
      },
    },
  };

  if (gateFailures.length) {
    const logs = [
      "Flash blocked before touching hardware.",
      ...gateFailures.map((failure) => `gate: ${failure}`),
      project.ok ? `projectPath OK: ${project.path}` : `projectPath invalid: ${project.error}`,
      target ? `pyOCD target candidate: ${target}` : "pyOCD target candidate missing",
      exactProbe ? `target probe requested: ${exactProbe}` : "target probe UID missing",
      `image candidates: ${images.length}`,
      `probe candidates: ${pyocd.probes.length}`,
    ];
    return jsonResponse(false, "Flash blocked by safety gate", data, logs, gateFailures.join("; "));
  }

  const statusGate = await runPyocdStatusGate(target, exactProbe, payload.targetChip, payload, stream);
  if (!statusGate.ok) {
    return jsonResponse(false, "Flash blocked: pyOCD status gate failed", {
      ...data,
      statusGate,
    }, statusGate.logs, statusGate.error);
  }

  const flashResult = await runPyocdFlash(target, exactProbe, payload.targetChip, artifactCheck.artifact, payload, stream);
  const logs = [
    "Flash gate passed; pyOCD real load executed.",
    project.ok ? `projectPath OK: ${project.path}` : `projectPath invalid: ${project.error}`,
    `pyOCD target: ${target}`,
    `target probe: ${exactProbe}`,
    `artifact: ${artifactCheck.artifact.path}`,
    ...statusGate.logs,
    ...flashResult.logs,
  ];
  return jsonResponse(flashResult.ok, flashResult.ok ? "Flash finished and reset/readback completed" : "Flash failed", {
    ...data,
    statusGate,
    flashResult,
  }, logs.slice(-180), flashResult.error);
}

async function handleStlinkFlash(payload, stream = null) {
  const project = resolveProjectPath(payload.projectPath);
  const images = project.ok ? discoverImages(project.path, { freshSinceMs: Number(payload.buildStartedAtMs || 0) }) : [];
  const stlink = await scanStlink();
  const exactProbe = String(payload.targetProbe || "").trim();
  const safeGate = Boolean(boolFromPayload(payload.allowRealFlash) && payload.confirmFlash === DEFAULT_FLASH_CONFIRM);
  const artifactCheck = project.ok ? artifactFromPayload(project.path, payload) : { ok: false, error: project.error, artifact: null };
  const gateFailures = [];
  if (!project.ok) gateFailures.push(project.error);
  if (!isStm32Target(payload.targetChip)) gateFailures.push(`Unsupported targetChip for STLink/CubeProgrammer: ${payload.targetChip || ""}`);
  if (!exactProbe) gateFailures.push("targetProbe must be an exact non-empty STLink serial number");
  if (exactProbe && !stlinkProbeMatchesUid(stlink, exactProbe)) gateFailures.push(`targetProbe not found in STLink scan: ${exactProbe}`);
  if (!artifactCheck.ok) gateFailures.push(artifactCheck.error);
  if (artifactCheck.ok && !artifactCheck.artifact.fresh) {
    gateFailures.push("artifact is not fresh for this build run; run /api/build first and pass buildStartedAtMs + artifactPath");
  }
  if (!safeGate) {
    gateFailures.push(`real flash confirmation missing: allowRealFlash=true and confirmFlash=${DEFAULT_FLASH_CONFIRM}`);
  }

  const data = {
    mode: gateFailures.length ? "blocked" : "real",
    programmer: "stlink",
    projectPath: project.path,
    projectOk: project.ok,
    targetChip: payload.targetChip || null,
    targetProbe: exactProbe || null,
    images,
    selectedArtifact: artifactCheck.artifact,
    stlink,
    safetyGate: {
      realFlashAllowed: safeGate,
      failures: gateFailures,
      required: {
        allowRealFlash: true,
        confirmFlash: DEFAULT_FLASH_CONFIRM,
        targetProbe: "exact non-empty STLink serial number",
        artifactPath: "fresh artifact returned by /api/build",
        tool: "STM32_Programmer_CLI",
      },
    },
  };

  if (gateFailures.length) {
    const logs = [
      "STLink flash blocked before touching hardware.",
      ...gateFailures.map((failure) => `gate: ${failure}`),
      project.ok ? `projectPath OK: ${project.path}` : `projectPath invalid: ${project.error}`,
      exactProbe ? `STLink probe requested: ${exactProbe}` : "STLink serial missing",
      `image candidates: ${images.length}`,
      `STLink candidates: ${stlink.probes.length}`,
    ];
    return jsonResponse(false, "STLink flash blocked by safety gate", data, logs, gateFailures.join("; "));
  }

  const statusGate = await runCubeStatusGate(exactProbe, payload, stream);
  if (!statusGate.ok) {
    return jsonResponse(false, "STLink flash blocked: CubeProgrammer status gate failed", {
      ...data,
      statusGate,
    }, statusGate.logs, statusGate.error);
  }

  const flashResult = await runCubeFlash(exactProbe, artifactCheck.artifact, payload, stream);
  const logs = [
    "STLink gate passed; STM32CubeProgrammer real write executed.",
    project.ok ? `projectPath OK: ${project.path}` : `projectPath invalid: ${project.error}`,
    `STLink probe: ${exactProbe}`,
    `artifact: ${artifactCheck.artifact.path}`,
    ...statusGate.logs,
    ...flashResult.logs,
  ];
  return jsonResponse(flashResult.ok, flashResult.ok ? "STLink flash finished and reset/verify completed" : "STLink flash failed", {
    ...data,
    statusGate,
    flashResult,
  }, logs.slice(-220), flashResult.error);
}

async function handleFlash(payload, stream = null) {
  return programmerKind(payload) === "stlink" ? handleStlinkFlash(payload, stream) : handlePyocdFlash(payload, stream);
}

async function handleUart(payload) {
  const serialPort = String(payload.serialPort || payload.uartPort || payload.logPort || "").trim();
  const baudRate = Number(payload.baudRate || 115200);
  const seconds = Math.max(1, Math.min(Number(payload.seconds || 3), 10));
  const serial = await scanSerialPorts();
  if (!/^COM\d+$/i.test(serialPort)) {
    return jsonResponse(false, "UART capture not started: serialPort is not configured", {
      requestedPort: serialPort || null,
      baudRate,
      serial,
    }, ["Set serialPort or uartPort to a COM port, for example COM18."], "No COM port configured.");
  }

  const script = [
    `$port = New-Object System.IO.Ports.SerialPort '${serialPort.toUpperCase()}', ${baudRate}, 'None', 8, 'One'`,
    "$port.ReadTimeout = 200",
    "$port.Open()",
    `$end = (Get-Date).AddSeconds(${seconds})`,
    "$chunks = New-Object System.Collections.Generic.List[string]",
    "while ((Get-Date) -lt $end) { try { $chunks.Add($port.ReadExisting()) } catch {} ; Start-Sleep -Milliseconds 120 }",
    "$port.Close()",
    "$chunks -join ''",
  ].join("; ");
  const result = await execRaw("powershell.exe", ["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", powershellUtf8Script(script)], {
    timeoutMs: (seconds + 3) * 1000,
  });
  return jsonResponse(result.ok, result.ok ? "UART capture finished" : "UART capture failed", {
    serialPort: serialPort.toUpperCase(),
    baudRate,
    seconds,
    output: result.stdout,
    stderr: result.stderr,
    code: result.code,
    durationMs: result.durationMs,
  }, normalizeToolOutput(result).slice(-80), result.ok ? null : result.error || result.stderr.trim());
}

function attachBackendEvidence(action, payload, result) {
  const evidencePath = writeBackendEvidence(action, payload || {}, result);
  if (evidencePath) {
    result.data = { ...(result.data || {}), backendEvidencePath: evidencePath };
    result.logs = [...(result.logs || []), `backend evidence: ${evidencePath}`];
  }
  return result;
}

function emitStep(emit, step, status, extra = {}) {
  emit({ type: "step", step, status, ...extra });
}

function emitResponseLogs(emit, step, result, options = {}) {
  const logs = Array.isArray(result?.logs) ? result.logs : [];
  const filtered = options.summaryOnly
    ? logs.filter((line) => /artifact|fresh|gate|blocked|backend evidence|projectPath|target probe|pyOCD target|STLink/i.test(line))
    : logs;
  for (const line of filtered.slice(-80)) {
    emit({ type: "stdout", step, phase: "summary", line });
  }
}

async function streamBufferedStep(action, payload, emit, runner) {
  emitStep(emit, action, "running", { message: `${action} started` });
  const result = attachBackendEvidence(action, payload, await runner());
  emit({ type: "result", action, step: action, ok: result.ok, message: result.message, result });
  emitResponseLogs(emit, action, result);
  emitStep(emit, action, result.ok ? "done" : "fail", { message: result.message, error: result.error || null });
  return result;
}

async function streamBuildStep(payload, emit) {
  let commandSeen = false;
  emitStep(emit, "build", "running", { message: "build started" });
  const result = attachBackendEvidence("build", payload, await handleBuild(payload, {
    onEvent(event) {
      if (event.type === "cmd") commandSeen = true;
      emit({ action: "build", ...event });
    },
  }));
  emit({ type: "result", action: "build", step: "build", ok: result.ok, message: result.message, result });
  emitResponseLogs(emit, "build", result, { summaryOnly: commandSeen });
  emitStep(emit, "build", result.ok ? "done" : "fail", { message: result.message, error: result.error || null });
  return result;
}

function firstFreshArtifact(buildResult) {
  const data = buildResult?.data || {};
  const candidates = Array.isArray(data.freshArtifacts) ? data.freshArtifacts : [];
  return candidates.find((artifact) => isArtifactFile(artifact.path || "")) || null;
}

async function streamFlashStep(payload, emit) {
  let commandSeen = false;
  let gateClosed = false;
  emitStep(emit, "artifact", "running", { message: "fresh artifact gate" });
  const artifactCheck = payload.artifactPath ? { ok: true, artifactPath: payload.artifactPath } : { ok: false };
  emitStep(emit, "artifact", artifactCheck.ok ? "done" : "fail", {
    message: artifactCheck.ok ? `fresh artifact: ${payload.artifactPath}` : "artifactPath missing",
  });
  if (!artifactCheck.ok) {
    return jsonResponse(false, "Flash not started: artifactPath is required", { payload: redactPayload(payload) }, ["artifactPath missing"], "artifactPath is required");
  }

  emitStep(emit, "gate", "running", { message: "safety gate and probe scan" });
  const result = attachBackendEvidence("flash", payload, await handleFlash(payload, {
    onEvent(event) {
      if (!gateClosed && event.type === "cmd" && event.step === "flash") {
        gateClosed = true;
        emitStep(emit, "gate", "done", { message: "safety gate passed" });
        emitStep(emit, "flash", "running", { message: "flash command started" });
      }
      if (event.type === "cmd") commandSeen = true;
      emit({ action: "flash", ...event });
    },
  }));
  if (!gateClosed) {
    emitStep(emit, "gate", result.ok ? "done" : "fail", { message: result.message, error: result.error || null });
  }
  emit({ type: "result", action: "flash", step: "flash", ok: result.ok, message: result.message, result });
  emitResponseLogs(emit, "flash", result, { summaryOnly: commandSeen });
  emitStep(emit, "flash", result.ok ? "done" : "fail", { message: result.message, error: result.error || null });
  return result;
}

async function runStreamAction(action, payload, emit) {
  emit({ type: "start", action, message: `${action} stream started`, backendVersion: VERSION });

  if (action === "build") {
    return streamBuildStep(payload, emit);
  }

  if (action === "flash") {
    return streamFlashStep(payload, emit);
  }

  if (action !== "build-flash") {
    return jsonResponse(false, `Unknown stream action: ${action}`, { action }, [], "unknown stream action");
  }

  const health = await streamBufferedStep("health", payload, emit, () => handleHealth(payload));
  if (!health.ok) {
    return jsonResponse(false, "Build + Flash stopped: backend health failed", { healthResult: health }, health.logs, health.error);
  }

  const scan = await streamBufferedStep("scan", payload, emit, () => handleScan(payload));
  if (!scan.ok) {
    return jsonResponse(false, "Build + Flash stopped: scan failed", { healthResult: health, scanResult: scan }, scan.logs, scan.error);
  }

  const build = await streamBuildStep(payload, emit);
  if (!build.ok) {
    return jsonResponse(false, "Build + Flash stopped: build failed", { healthResult: health, scanResult: scan, buildResult: build }, build.logs, build.error);
  }

  emitStep(emit, "artifact", "running", { message: "selecting fresh artifact" });
  const artifact = firstFreshArtifact(build);
  if (!artifact) {
    emitStep(emit, "artifact", "fail", { message: "no fresh artifact" });
    return jsonResponse(false, "Build + Flash stopped: no fresh artifact", {
      healthResult: health,
      scanResult: scan,
      buildResult: build,
    }, ["No fresh artifact returned by build."], "no fresh artifact");
  }
  emit({ type: "artifact", step: "artifact", artifact, line: `fresh artifact selected: ${artifact.path}` });
  emitStep(emit, "artifact", "done", { message: `fresh artifact: ${artifact.path}` });

  const flashPayload = {
    ...payload,
    artifactPath: artifact.path,
    buildStartedAtMs: build.data?.buildStartedAtMs,
    buildRunId: build.data?.buildRunId,
  };
  const flash = await streamFlashStep(flashPayload, emit);

  let uart = null;
  if (flash.ok && String(payload.serialPort || payload.uartPort || payload.logPort || "").trim()) {
    uart = await streamBufferedStep("uart", payload, emit, () => handleUart({ ...payload, seconds: payload.seconds || 3 }));
  } else if (flash.ok) {
    emitStep(emit, "uart", "skipped", { message: "serialPort not configured" });
  }

  return jsonResponse(flash.ok, flash.ok ? "Build + Flash finished" : "Build + Flash failed", {
    healthResult: health,
    scanResult: scan,
    buildResult: build,
    selectedArtifact: artifact,
    flashResult: flash,
    uartResult: uart,
  }, [
    ...(build.logs || []).slice(-30),
    ...(flash.logs || []).slice(-80),
    ...(uart?.logs || []).slice(-30),
  ], flash.ok ? null : flash.error || "flash failed");
}

async function handleRunStream(payload, res) {
  sendStreamHeaders(res);
  const action = String(payload.action || payload.mode || "").trim();
  const emit = (event) => writeStreamEvent(res, event);
  try {
    const result = attachBackendEvidence(action || "run-stream", payload, await runStreamAction(action, payload, emit));
    emit({ type: "done", action, ok: result.ok, message: result.message, result });
  } catch (error) {
    const result = jsonResponse(false, "Hardware Lab stream error", {}, [], error.message || String(error));
    emit({ type: "error", action, error: error.message || String(error) });
    emit({ type: "done", action, ok: false, message: result.message, result });
  } finally {
    res.end();
  }
}

async function route(action, payload) {
  if (action === "health") return handleHealth(payload);
  if (action === "scan") return handleScan(payload);
  if (action === "build") return handleBuild(payload);
  if (action === "flash") return handleFlash(payload);
  if (action === "uart") return handleUart(payload);
  if (action === "clear-evidence") return clearBackendEvidenceFiles(payload);
  if (action === "select-folder") return handleSelectFolder(payload);
  if (action === "open-folder") return handleOpenFolder(payload);
  return jsonResponse(false, `Unknown API action: ${action}`, { action }, [], "unknown endpoint");
}

const server = http.createServer(async (req, res) => {
  res.setHeader("Access-Control-Allow-Origin", "*");
  res.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS");
  res.setHeader("Access-Control-Allow-Headers", "Content-Type");
  res.setHeader("Access-Control-Allow-Private-Network", "true");
  if (req.method === "OPTIONS") {
    res.writeHead(204);
    res.end();
    return;
  }

  const url = new URL(req.url || "/", `http://${req.headers.host || "127.0.0.1"}`);
  if (url.pathname === "/api") {
    sendJson(res, 200, jsonResponse(true, "Hardware Lab backend is running", {
      version: VERSION,
      endpoints: ["/api/health", "/api/scan", "/api/build", "/api/flash", "/api/uart", "/api/clear-evidence", "/api/select-folder", "/api/open-folder", "/api/run-stream"],
      preview: {
        local: `http://127.0.0.1:${DEFAULT_PORT}/hardware-lab.html`,
        online: "https://nuedc-components-atlas-public.pages.dev/hardware-lab.html",
      },
    }));
    return;
  }

  if (url.pathname === "/api/run-stream") {
    try {
      const payload = req.method === "GET" ? Object.fromEntries(url.searchParams.entries()) : await parseRequestBody(req);
      await handleRunStream(payload || {}, res);
    } catch (error) {
      sendJson(res, 500, jsonResponse(false, "Hardware Lab stream setup failed", {}, [], error.message || String(error)));
    }
    return;
  }

  if (req.method === "GET" && !url.pathname.startsWith("/api/")) {
    const staticPath = staticPathFor(url.pathname);
    if (staticPath && sendStaticFile(res, staticPath)) return;
  }

  const match = url.pathname.match(/^\/api\/([a-z-]+)$/);
  if (!match) {
    sendJson(res, 404, jsonResponse(false, "Not found", { path: url.pathname }, [], "not found"));
    return;
  }

  try {
    if (["clear-evidence", "select-folder", "open-folder"].includes(match[1]) && req.method !== "POST") {
      sendJson(res, 405, jsonResponse(false, `${match[1]} requires POST`, {}, [], "method not allowed"));
      return;
    }
    const payload = req.method === "GET" ? Object.fromEntries(url.searchParams.entries()) : await parseRequestBody(req);
    const result = await route(match[1], payload || {});
    sendJson(res, 200, match[1] === "clear-evidence" ? result : attachBackendEvidence(match[1], payload || {}, result));
  } catch (error) {
    sendJson(res, 500, jsonResponse(false, "Hardware Lab backend error", {}, [], error.message || String(error)));
  }
});

server.listen(DEFAULT_PORT, "127.0.0.1", () => {
  console.log(`Hardware Lab backend ${VERSION} listening on http://127.0.0.1:${DEFAULT_PORT}`);
  console.log(`Project root: ${PROJECT_ROOT}`);
});
