// deck.ts
// Deck owns Fastify route registration, static serving, and response wiring.


import Fastify from "fastify";
import cors from "@fastify/cors";
import fs from "node:fs";
import path from "node:path";
import * as crypto from "node:crypto";
import { spawn, execFile } from "node:child_process";

import {
  ampIn,  bodyOf,  recordRoute,  recordCheck,  getDeckControlConfig} from "./deckControl";
import {
  getRecorderState,  getRecorderStatus,  requestRecorderPause,  resumeRecorder,  cancelRemainingRecorderTasks,  wakeRecorder} from "./recorder";
import { controller } from "./controller";
import { initEQ, getReports, getRecentActivity, recordChatIn } from "./eq";
import { handleInquiry, updateProjectMap } from "./inquiryLine";
import { capture } from "./capture";
import {
  readLog,  clearLog,  loadTape,  loadDubsTape,  setDubsClearAfter,  clearTapeItems,  approveManifestOnTape,  rejectManifestOnTape} from "./playback";
import {
  recoveryShellHtml,  getTimeline,  listRecoverySources,  undo,  restoreFromScrape,  restoreFromBackup,  inputManifest,  listRecentDubUndoCandidates,  prepareDubUndoManifest,  createRestorePoint,  listRestorePoints,  restoreFromPoint} from "./restore";
import {
  projectLookup,  updateProjectIndex,  createProjectSerialNo,  toProjectFolderName,  toProjectLocation,  saveProjectFile,
  getProjectHealthReport,  getEligibleProjectFolders,  renameProject,  repairProjectLocation} from "./mgrSNP";
import {
  getSettings,  saveSettings} from "./mgrSNP";
import {
  getUserTier,  isAuthenticated,  isLicensed,  getUser,  getAuthority,  setTelemetryConsent,  establishPortalSession,  clearSession as clearAuthState} from "./hypauth/auth";
import {
  login,  register,  checkActivation,  keepalive,  requestAuthorityPacket,  requestSystemManifest,  requestUpdateManifest,
  reportSystemIntegrity} from "./hypauth/hypauthcall";
import { runEditorSystemAction } from "./systemActions";
import {
  ROOT,  UI_PATH,  UI_CSS_PATH,  UI_TABS_CSS_PATH,  UI_TABS_HTML_PATH,  UI_JS_PATH,  MONITOR_LAUNCH_GATE_JS_PATH,
  UPDATE_STATE_PATH,  MGRSNP_JS_PATH,  SYSMENU_JS_PATH,  DUB_UNDO_JS_PATH,  EDITOR_JS_PATH,  PROJECT_SWITCH_JS_PATH,
  PROJECT_SWITCH_CSS_PATH,  PROJECT_OUTLINE_JS_PATH,  MONITOR_PROJECT_JS_PATH,  FAVICON_PATH,
  ASSET_FAVICON_PATH,  WEBMANIFEST_PATH,  NODARAMA_WHITE_PNG_PATH,  ASSETS_DIR,  isInsideRoot} from "./pathList";

type DeckShutdownHandler = (reason: string) => void | Promise<void>;

function fail(reply: any, code: number, error: string, message?: string, extra: Record<string, any> = {}) {
  return reply.code(code).send({ ok: false, error, ...(message ? { message } : {}), ...extra });
}

function existsFile(filePath: string): boolean {
  try { return fs.existsSync(filePath) && fs.statSync(filePath).isFile(); }
  catch { return false; }
}

function existsDir(filePath: string): boolean {
  try { return fs.existsSync(filePath) && fs.statSync(filePath).isDirectory(); }
  catch { return false; }
}

function readJsonFile(filePath: string, fallback: any = {}) {
  try { return JSON.parse(fs.readFileSync(filePath, "utf8")); }
  catch { return fallback; }
}

function serveTextFile(reply: any, filePath: string, type: string, label: string) {
  if (!existsFile(filePath)) return reply.code(404).type("text/plain").send(`${label} not found`);
  return reply.type(type).send(fs.readFileSync(filePath, "utf8"));
}

function serveBinaryFile(reply: any, filePath: string, type: string, label: string) {
  if (!existsFile(filePath)) return reply.code(404).type("text/plain").send(`${label} not found`);
  return reply.type(type).send(fs.readFileSync(filePath));
}

function mimeForAsset(filePath: string): string {
  const ext = path.extname(filePath).toLowerCase();
  if (ext === ".png") return "image/png";
  if (ext === ".jpg" || ext === ".jpeg") return "image/jpeg";
  if (ext === ".gif") return "image/gif";
  if (ext === ".svg") return "image/svg+xml";
  if (ext === ".ico") return "image/x-icon";
  if (ext === ".css") return "text/css";
  if (ext === ".js") return "application/javascript";
  if (ext === ".json") return "application/json";
  if (ext === ".html") return "text/html";
  if (ext === ".ttf") return "font/ttf";
  if (ext === ".woff2") return "font/woff2";
  return "application/octet-stream";
}

function isExplicitAbsolutePath(input: string): boolean {
  const clean = String(input || "").trim();
  return /^[A-Za-z]:[\\/]/.test(clean) || clean.startsWith("\\\\");
}

function resolveFromRoot(input: string): string {
  const clean = String(input || "").trim().replace(/\\/g, "/");
  const relative = clean.replace(/^\/+/, "");
  const abs = !clean || clean === "."
    ? path.resolve(ROOT)
    : isExplicitAbsolutePath(clean)
      ? path.resolve(clean)
      : path.resolve(ROOT, relative);
  if (!isInsideRoot(abs, ROOT)) throw new Error("path_outside_nv_root");
  return abs;
}

function readPackageVersion(): string {
  return String(readJsonFile(path.join(ROOT, "package.json"), {})?.version || "0.0.0");
}

function readUpdateState(): Record<string, any> {
  try {
    if (!existsFile(UPDATE_STATE_PATH)) return { ok: true, phase: "starting", message: "Starting Nodarama" };
    return { ok: true, ...JSON.parse(fs.readFileSync(UPDATE_STATE_PATH, "utf8")) };
  } catch (err: any) {
    return { ok: false, phase: "unknown", message: "Starting Nodarama", error: err?.message || String(err) };
  }
}

function monitorShellHtml(): string {
  const shell = fs.readFileSync(UI_PATH, "utf8");
  const tabs = existsFile(UI_TABS_HTML_PATH) ? fs.readFileSync(UI_TABS_HTML_PATH, "utf8") : "";
  return shell.replace("<!-- MONITOR_TABS_HTML -->", tabs);
}

function startupSplashHtml(): string {
  return `<!doctype html><html><head><meta charset="utf-8"/><title>Nodarama Starting</title>
<style>body{margin:0;height:100vh;display:grid;place-items:center;background:#0a0a0c;color:white;font-family:monospace}.box{text-align:center}.sub{color:#aaa;margin-top:10px}</style>
</head><body><div class="box"><h1>Nodarama is starting</h1><div id="msg">Checking startup state...</div><div class="sub">This window will continue after startup.</div></div>
<script>async function poll(){try{const r=await fetch('/update/state',{cache:'no-store'});const s=await r.json();if(s.message)msg.textContent=s.message;if(s.phase==='current'||s.phase==='failed'){setTimeout(()=>location.replace('/'),800);return;}}catch{}setTimeout(poll,900)}poll();</script>
</body></html>`;
}

async function readCombinedPolicyText() {
  const files = [
    "https://nodarama.com/policy/privacy-policy.html",
    "https://nodarama.com/policy/personal-use-agreement.html",
    "https://nodarama.com/policy/terms-of-service.html"
  ];
  return { ok: true, version: "beta-agreement-web", source: "nodarama.com", files, text: files.join("\n") };
}

function hashFileIfPresent(relPath: string) {
  try {
    const abs = resolveFromRoot(relPath);
    if (!existsFile(abs)) return { path: relPath, exists: false };
    const buf = fs.readFileSync(abs);
    return { path: relPath, exists: true, sha256: crypto.createHash("sha256").update(buf).digest("hex"), size: buf.length };
  } catch (err: any) {
    return { path: relPath, exists: false, error: err?.message || String(err) };
  }
}

async function buildLocalIntegrityReport() {
  const manifest = await requestSystemManifest();
  const files = Array.isArray(manifest?.files) ? manifest.files : [];
  const items = files.map((file: any) => hashFileIfPresent(String(file?.path || "").trim())).filter((item: any) => item.path);
  return {
    ok: true,
    clientVersion: readPackageVersion(),
    manifestVersion: String(manifest?.manifestVersion || "system-manifest-v1"),
    hashAlgorithm: "sha256",
    items,
    summary: { fileCount: items.length, issueCount: items.filter((item: any) => !item.exists || item.error).length },
    serverManifest: manifest
  };
}

function openFolder(folderPath: string): void {
  if (process.platform === "win32") spawn("explorer.exe", [folderPath], { detached: true, stdio: "ignore" }).unref();
  else if (process.platform === "darwin") spawn("open", [folderPath], { detached: true, stdio: "ignore" }).unref();
  else spawn("xdg-open", [folderPath], { detached: true, stdio: "ignore" }).unref();
}

function createProject(rawName: string): { name: string; path: string; location: string; serialNo: string } | null {
  const name = String(rawName || "").trim();
  const folder = toProjectFolderName(name);
  if (!name || !folder || folder === "universal") return null;

  const location = toProjectLocation(name);
  const absFolder = path.resolve(ROOT, location.replace(/^\.\/+/, ""));
  const serialNo = createProjectSerialNo();
  const projectFile = path.join(absFolder, "project.json");
  const doc = {
    name,
    serialNo,
    summary: "",
    notes: "",
    details: { name, serialNo, summary: "", notes: "" }
  };

  fs.mkdirSync(absFolder, { recursive: true });
  if (!existsFile(projectFile)) fs.writeFileSync(projectFile, JSON.stringify(doc, null, 2), "utf8");
  updateProjectIndex();
  const indexed = projectLookup({ serialNo }).project || projectLookup({ name }).project;
  return indexed ? { name: indexed.name, path: indexed.location, location: indexed.location, serialNo: indexed.serialNo } : { name, path: location, location, serialNo };
}

function syncAccessTierFromAuth(): string {
  const tier = String(getUserTier() || "limited");
  const state = controller.state();

  if (tier === "backstage") {
    const hasSelectedProject = Boolean(state.projectSerial && state.projectMode === "project");
    if (hasSelectedProject) controller.boot.tier("backstage");
    else controller.boot.backstage();
    return tier;
  }

  controller.boot.tier(tier);
  return tier;
}
export async function startDeck(port = 5054, onShutdownRequest?: DeckShutdownHandler) {
  try { await initEQ(); }
  catch (err: any) { capture.system({ tag: "EQ", event: "eq_init_degraded", error: err?.message ?? String(err) }); }

  const app = Fastify({ logger: false });
  await app.register(cors, {
    origin: (origin, cb) => {
      const allowedOrigins = [
        "https://chatgpt.com",
        "https://chat.openai.com",
        "https://gemini.google.com",
        "https://claude.ai",
        "https://copilot.microsoft.com",
        "https://aistudio.google.com"
      ];
      if (!origin) return cb(null, true);
      if (allowedOrigins.includes(origin)) return cb(null, true);
      if (/^chrome-extension:\/\/[a-z]{32}$/.test(origin)) return cb(null, true);
      return cb(null, false);
    },
    credentials: true
  });
  app.addHook("onRequest", async (req, reply) => await ampIn(req, reply));

  // Shutdown
  const browserClients = new Map<string, number>();
  let browserShutdownTimer: NodeJS.Timeout | null = null;

  function clearBrowserShutdownTimer() {
    if (!browserShutdownTimer) return;
    clearTimeout(browserShutdownTimer);
    browserShutdownTimer = null;
  }

  function pruneBrowserClients(maxAgeMs = 9000) {
    const cutoff = Date.now() - maxAgeMs;
    for (const [clientId, seenAt] of browserClients.entries()) {
      if (seenAt < cutoff) browserClients.delete(clientId);
    }
  }

  function scheduleBrowserShutdown(reason: string) {
    clearBrowserShutdownTimer();
    browserShutdownTimer = setTimeout(() => {
      pruneBrowserClients(12000);
      if (browserClients.size > 0) return;
      capture.system({ tag: "Deck", event: "shutdown_requested", reason, browserClients: 0 });
      if (onShutdownRequest) {
        try { void onShutdownRequest(reason); } catch {}
      }
    }, 12000);
  }

  app.post("/shutdown/browser-heartbeat", async (req) => {
    const clientId = String(bodyOf(req).clientId || "").trim();
    if (!clientId) return { ok: false, error: "missing_client_id" };
    browserClients.set(clientId, Date.now());
    clearBrowserShutdownTimer();
    return { ok: true, clients: browserClients.size };
  });

  app.post("/shutdown/browser-gone", async (req) => {
    const clientId = String(bodyOf(req).clientId || "").trim();
    const reason = String(bodyOf(req).reason || "browser_close").trim() || "browser_close";
    if (clientId) browserClients.delete(clientId);
    pruneBrowserClients();
    if (browserClients.size <= 0) scheduleBrowserShutdown(reason);
    return { ok: true, pendingShutdown: browserClients.size <= 0, clients: browserClients.size, reason };
  });

  app.post("/shutdown/browser-close", async (req) => {
    const reason = String(bodyOf(req).reason || "browser_close_legacy").trim() || "browser_close_legacy";
    pruneBrowserClients();
    if (browserClients.size <= 0) scheduleBrowserShutdown(reason);
    return { ok: true, pendingShutdown: browserClients.size <= 0, clients: browserClients.size, reason };
  });

  // Auth
  app.post("/auth/login", async (req, reply) => {
    const b = bodyOf(req);
    const email = String(b.email || "").trim();
    const password = String(b.password || "").trim();
    if (!email || !password) return fail(reply, 400, "missing_credentials");

    const result = await login(email, password);
    if (!result.ok) return reply.code(Number((result as any).status || 401)).send(result);

    const tier = syncAccessTierFromAuth();
    capture.system({ tag: "Deck", event: "user_login", userId: result.user?.userId, tier });
    return result;
  });

  app.post("/auth/portal-return", async (req, reply) => {
    try {
      const user = establishPortalSession(bodyOf(req));
      const tier = syncAccessTierFromAuth();
      capture.system({ tag: "Deck", event: "portal_return_login", userId: user.userId, tier });
      return {
        ok: true,
        authenticated: true,
        email: user.email,
        user: { userId: user.userId, email: user.email },
        tier,
        accessTier: tier,
        licensed: user.licensed,
        authority: user.authority
      };
    } catch (err: any) {
      return fail(reply, 400, "invalid_portal_auth_payload", err?.message ?? String(err));
    }
  });

  app.post("/auth/register", async (req, reply) => {
    const b = bodyOf(req);
    const email = String(b.email || "").trim();
    const password = String(b.password || "").trim();
    if (!email || !password) return fail(reply, 400, "missing_credentials");
    const result = await register(email, password, {
      deviceName: String(b.deviceName || "NV Station").trim(),
      deviceFingerprint: String(b.deviceFingerprint || "").trim()
    });
    if (!result.ok) return reply.code(Number((result as any).status || 400)).send(result);
    syncAccessTierFromAuth();
    return result;
  });

  app.post("/auth/activation-status", async (req, reply) => {
    const b = bodyOf(req);
    const activationId = String(b.activationId || "").trim();
    if (!activationId) return fail(reply, 400, "missing_activation_id");
    const result = await checkActivation(activationId, {
      deviceName: String(b.deviceName || "NV Station").trim(),
      deviceFingerprint: String(b.deviceFingerprint || "").trim()
    });
    syncAccessTierFromAuth();
    return result;
  });

  app.post("/auth/logout", async () => {
    clearAuthState();
    controller.boot.tier("limited");
    capture.system({ tag: "Deck", event: "user_logout" });
    return { ok: true };
  });

  app.get("/auth/status", async () => {
    const user = getUser();
    const authority = getAuthority();
    const tier = syncAccessTierFromAuth();
    return {
      ok: true,
      authenticated: isAuthenticated(),
      licensed: isLicensed(),
      email: isAuthenticated() ? user.email : null,
      accessTier: tier,
      tier,
      authority,
      telemetry: user.telemetry
    };
  });

  app.post("/auth/telemetry", async (req) => {
    const logs = Boolean(bodyOf(req).logs);
    const telemetry = setTelemetryConsent(logs);
    capture.system({ tag: "Deck", event: "telemetry_consent_updated", logs });
    return { ok: true, telemetry };
  });

  // UI/static
  app.get("/", async (req: any, reply) => {
    if (String(req?.query?.startup || "").trim() === "1") return reply.type("text/html").send(startupSplashHtml());
    if (String(req?.query?.recovery || "").trim() === "1") return reply.type("text/html").send(recoveryShellHtml());
    if (existsFile(UI_PATH)) return reply.type("text/html").send(monitorShellHtml());
    return reply.type("text/html").send(recoveryShellHtml());
  });

  app.get("/update/state", async () => readUpdateState());
  app.get("/monitor.css", async (_req, reply) => serveTextFile(reply, UI_CSS_PATH, "text/css", "monitor.css"));
  app.get("/monitorTabs.css", async (_req, reply) => serveTextFile(reply, UI_TABS_CSS_PATH, "text/css", "monitorTabs.css"));
  app.get("/projectSwitch.css", async (_req, reply) => serveTextFile(reply, PROJECT_SWITCH_CSS_PATH, "text/css", "projectSwitch.css"));
  app.get("/editor.css", async (_req, reply) => serveTextFile(reply, path.join(ROOT, "editor.css"), "text/css", "editor.css"));
  app.get("/monitorTabProject.css", async (_req, reply) => serveTextFile(reply, path.join(ROOT, "monitorTabProject.css"), "text/css", "monitorTabProject.css"));
  app.get("/monitorTabEditor.css", async (_req, reply) => serveTextFile(reply, path.join(ROOT, "monitorTabEditor.css"), "text/css", "monitorTabEditor.css"));
  app.get("/monitorTabDubs.css", async (_req, reply) => serveTextFile(reply, path.join(ROOT, "monitorTabDubs.css"), "text/css", "monitorTabDubs.css"));
  app.get("/monitorTabHistory.css", async (_req, reply) => serveTextFile(reply, path.join(ROOT, "monitorTabHistory.css"), "text/css", "monitorTabHistory.css"));
  app.get("/monitorTabSettings.css", async (_req, reply) => serveTextFile(reply, path.join(ROOT, "monitorTabSettings.css"), "text/css", "monitorTabSettings.css"));
  app.get("/monitorTabVisualizer.css", async (_req, reply) => serveTextFile(reply, path.join(ROOT, "monitorTabVisualizer.css"), "text/css", "monitorTabVisualizer.css"));
  app.get("/dubtapes.css", async (_req, reply) => serveTextFile(reply, path.join(ROOT, "dubtapes.css"), "text/css", "dubtapes.css"));
  app.get("/dubtapes-rendertest", async (_req, reply) => serveTextFile(reply, path.join(ROOT, "dubtapes-rendertest.html"), "text/html", "dubtapes-rendertest.html"));
  app.get("/dubtapes-rendertest.html", async (_req, reply) => serveTextFile(reply, path.join(ROOT, "dubtapes-rendertest.html"), "text/html", "dubtapes-rendertest.html"));
  app.get("/dubtapes-rendertest.js", async (_req, reply) => serveTextFile(reply, path.join(ROOT, "dubtapes-rendertest.js"), "application/javascript", "dubtapes-rendertest.js"));
  app.get("/monitorTabs.html", async (_req, reply) => serveTextFile(reply, UI_TABS_HTML_PATH, "text/html", "monitorTabs.html"));
  app.get("/monitor.js", async (_req, reply) => serveTextFile(reply, UI_JS_PATH, "application/javascript", "monitor.js"));
  app.get("/monitorProject.js", async (_req, reply) => serveTextFile(reply, MONITOR_PROJECT_JS_PATH, "application/javascript", "monitorProject.js"));
  app.get("/monitorLaunchGate.js", async (_req, reply) => serveTextFile(reply, MONITOR_LAUNCH_GATE_JS_PATH, "application/javascript", "monitorLaunchGate.js"));
  app.get("/monitorTour.js", async (_req, reply) => serveTextFile(reply, path.join(ROOT, "monitorTour.js"), "application/javascript", "monitorTour.js"));
  app.get("/mgrSNP.js", async (_req, reply) => serveTextFile(reply, MGRSNP_JS_PATH, "application/javascript", "mgrSNP.js"));
  app.get("/sysMenu.js", async (_req, reply) => serveTextFile(reply, SYSMENU_JS_PATH, "application/javascript", "sysMenu.js"));
  app.get("/dubUndo.js", async (_req, reply) => serveTextFile(reply, DUB_UNDO_JS_PATH, "application/javascript", "dubUndo.js"));
  app.get("/editor.js", async (_req, reply) => serveTextFile(reply, EDITOR_JS_PATH, "application/javascript", "editor.js"));
  app.get("/projectSwitch.js", async (_req, reply) => serveTextFile(reply, PROJECT_SWITCH_JS_PATH, "application/javascript", "projectSwitch.js"));
  app.get("/projectOutline.js", async (_req, reply) => serveTextFile(reply, PROJECT_OUTLINE_JS_PATH, "application/javascript", "projectOutline.js"));
  app.get("/favicon.ico", async (_req, reply) => serveBinaryFile(reply, existsFile(FAVICON_PATH) ? FAVICON_PATH : ASSET_FAVICON_PATH, "image/x-icon", "favicon.ico"));
  app.get("/site.webmanifest", async (_req, reply) => serveTextFile(reply, WEBMANIFEST_PATH, "application/manifest+json", "site.webmanifest"));
  app.get("/nodaramaWhite.png", async (_req, reply) => serveBinaryFile(reply, NODARAMA_WHITE_PNG_PATH, "image/png", "nodaramaWhite.png"));
  app.get<{ Params: { "*": string } }>("/assets/*", async (req, reply) => {
    const assetPath = path.resolve(ASSETS_DIR, String(req.params["*"] || "").replace(/\\/g, "/"));
    if (!isInsideRoot(assetPath, ASSETS_DIR) || !existsFile(assetPath)) return reply.code(404).type("text/plain").send("asset not found");
    return reply.type(mimeForAsset(assetPath)).send(fs.readFileSync(assetPath));
  });

  // Health/status/system
  app.get("/health", async () => ({ ok: true }));
  app.get("/status", async () => {
    let tier = "limited";
    let recorder: any = {};
    let deckControl: any = {};
    let controllerStatus: any = {};
    let authStatus: any = {};
    const activeProject = (() => { try { return controller.activeProjectInquiry(); } catch { return null; } })();

    try { tier = syncAccessTierFromAuth(); } catch {}
    try { recorder = getRecorderStatus(); } catch (err: any) { recorder = { error: err?.message || String(err) }; }
    try { deckControl = getDeckControlConfig(); } catch (err: any) { deckControl = { error: err?.message || String(err) }; }
    try { controllerStatus = controller.summary(); } catch (err: any) { controllerStatus = { ok: false, error: err?.message || String(err), activeProject }; }
    try {
      authStatus = {
        authenticated: isAuthenticated(),
        tier,
        licensed: isLicensed(),
        authority: getAuthority()
      };
    } catch (err: any) {
      authStatus = { authenticated: false, tier, error: err?.message || String(err) };
    }

    return {
      ok: true,
      ...recorder,
      activeProject: activeProject?.activeProject || null,
      projectRoot: activeProject?.projectRoot || null,
      projectMode: activeProject?.projectMode || null,
      isBackstage: Boolean(activeProject?.isBackstage),
      deckControl,
      controller: controllerStatus,
      auth: authStatus
    };
  });

  app.get("/policy/combined", async () => await readCombinedPolicyText());
  app.get("/docs/recording-contract", async (_req, reply) => {
    const filePath = path.join(ROOT, "docs", "VerbatimRecordingContract.txt");
    if (!existsFile(filePath)) return fail(reply, 404, "recording_contract_not_found");
    return { ok: true, file: "docs/VerbatimRecordingContract.txt", text: fs.readFileSync(filePath, "utf8") };
  });
  app.get("/system/authority", async (_req, reply) => { try { return await requestAuthorityPacket(); } catch (err: any) { return fail(reply, 503, err?.message || String(err)); } });
  app.get("/system/manifest", async (_req, reply) => { try { return await requestSystemManifest(); } catch (err: any) { return fail(reply, 503, err?.message || String(err)); } });
  app.get("/update/manifest", async (req: any, reply) => { try { return await requestUpdateManifest(String(req?.query?.channel || "stable")); } catch (err: any) { return fail(reply, 503, err?.message || String(err)); } });
  app.get("/system/integrity/local", async (_req, reply) => { try { return await buildLocalIntegrityReport(); } catch (err: any) { return fail(reply, 500, err?.message || String(err)); } });
  app.post("/system/integrity/report", async (_req, reply) => {
    try {
      const local = await buildLocalIntegrityReport();
      return { ok: true, local, server: await reportSystemIntegrity(local) };
    } catch (err: any) { return fail(reply, 503, err?.message || String(err)); }
  });

  // Settings/projects
  app.get("/settings", async () => {
    const settings = getSettings();
    return { ok: true, settings: { ...settings, projects: projectLookup({ all: true }).projects, release: settings.release || { channel: "beta", version: readPackageVersion(), autoUpdate: true } } };
  });
  app.post("/settings/save", async (req) => {
    const next = saveSettings(bodyOf(req));
    capture.system({ tag: "Deck", event: "settings_saved" });
    return { ok: true, settings: next };
  });
    
  app.get("/project/header-meta", async () => controller.respondProject());

  app.get("/projects", async () => {
    const synced = projectLookup({ all: true });
    const active = controller.activeProjectInquiry();
    return {
      ok: true,
      projects: synced.projects,
      activeProject: active.activeProject || null,
      projectName: active.projectName || null,
      projectSerial: active.projectSerial || null,
      projectRoot: active.projectRoot || null,
      projectMode: active.projectMode || null,
      isBackstage: Boolean(active.isBackstage)
    };
  });
  app.get("/projects/health", async () => ({ ok: true, ...getProjectHealthReport() }));
  app.post("/projects/scan", async (_req, reply) => {
    try {
      const result = updateProjectMap();
      capture.system({ tag: "Deck", event: "project_map_scanned", count: Array.isArray(result?.prjMap) ? result.prjMap.length : 0 });
      return result;
    } catch (err: any) {
      return fail(reply, 400, "project_scan_failed", err?.message ?? String(err));
    }
  });
  app.get("/projects/eligible-folders", async () => ({ ok: true, folders: getEligibleProjectFolders() }));
  app.post("/projects/open-folder", async (_req, reply) => {
    try {
      const active = controller.activeProjectInquiry();
      const folderPath = active.projectRoot ? path.resolve(active.projectRoot) : path.resolve(ROOT);
      if (!existsDir(folderPath)) throw new Error("project_folder_missing");
      openFolder(folderPath);
      capture.system({ tag: "Deck", event: "project_folder_opened", folderPath });
      return { ok: true, folderPath };
    } catch (err: any) { return fail(reply, 400, err?.message ?? String(err)); }
  });
  app.post("/projects/rename", async (req, reply) => {
    const b = bodyOf(req);
    const result = renameProject(String(b.currentName || b.name || "").trim(), String(b.nextName || "").trim());
    if (!result.ok || !result.project) return fail(reply, 400, result.error || "project_rename_failed", result.message || "Project rename failed");
    capture.system({ tag: "Deck", event: "project_renamed", to: result.project.name });
    return { ok: true, project: result.project };
  });
  app.post("/projects/repair-location", async (req, reply) => {
    const b = bodyOf(req);
    const result = repairProjectLocation(String(b.name || "").trim(), String(b.location || "").trim());
    return result ? { ok: true, project: result } : fail(reply, 400, "project location repair failed");
  });
  app.post("/projects/select", async (req, reply) => {
    const b = bodyOf(req);
    const result = controller.initProject({
      name: String(b.name || "").trim(),
      serialNo: String(b.serialNo || b.id || "").trim(),
      key: String(b.key || b.projectKey || b.identifier || "").trim(),
      location: String(b.location || "").trim(),
      path: String(b.path || "").trim(),
      projectFile: String(b.projectFile || "").trim(),
      source: String(b.source || "monitor").trim()
    });
    if (!result.ok) return fail(reply, 400, result.error || "project_switch_failed", result.message || "Project switch failed");
    capture.system({ tag: "Deck", event: "project_switched", project: result.activeProject, serialNo: result.projectSerial, source: result.source });
    return result;
  });
  app.post("/projects/create", async (req, reply) => {
    const name = String(bodyOf(req).name || "").trim();
    if (!name) return fail(reply, 400, "missing project name");
    const tier = syncAccessTierFromAuth();
    const existingProjects = projectLookup({ all: true }).projects;
    if (tier === "limited" && existingProjects.length >= 2) {
      return fail(reply, 403, "project_limit_reached", "Limited access includes 2 project slots. Upgrade to Unlimited for unlimited projects.");
    }
    const result = createProject(name);
    if (!result) return fail(reply, 400, "project creation failed");
    capture.system({ tag: "Deck", event: "project_created", project: result.name, serialNo: result.serialNo });
    return { ok: true, project: result.name, serialNo: result.serialNo, path: result.path };
  });
  app.post("/projects/custom-add", async (req, reply) => {
    const b = bodyOf(req);
    const name = String(b.name || "").trim();
    const location = String(b.location || b.path || "").trim();
    if (!name) return fail(reply, 400, "missing_project_name", "Enter a project name.");
    if (!location) return fail(reply, 400, "missing_project_location", "Enter a folder path.");

    const existingProjects = projectLookup({ all: true }).projects;
    const existingAtLocation = projectLookup({ path: location }).project;
    if (existingAtLocation) return fail(reply, 409, "custom_project_already_registered", "That folder is already registered as a project.", { project: existingAtLocation });

    const tier = syncAccessTierFromAuth();
    if (tier === "limited" && existingProjects.length >= 2) {
      return fail(reply, 403, "project_limit_reached", "Limited access includes 2 project slots. Upgrade to Unlimited for unlimited projects.");
    }

    const settings = getSettings();
    const customProjects = Array.isArray(settings.customProjects) ? settings.customProjects : [];
    saveSettings({ customProjects: [...customProjects, { name, location }] } as any);
    const project = projectLookup({ path: location }).project || projectLookup({ name }).project;
    if (!project) return fail(reply, 400, "custom_project_registration_failed", "Could not register that folder as a project.");

    capture.system({ tag: "Deck", event: "custom_project_registered", project: project.name, serialNo: project.serialNo, location: project.location });
    return { ok: true, project };
  });
  app.post("/projects/save", async (req, reply) => {
    const b = bodyOf(req);
    if (typeof b.content !== "string") return fail(reply, 400, "missing content");

    const active = controller.activeProjectInquiry();
    if (active.projectMode !== "project" || active.isBackstage || active.projectSerial === "00") {
      return fail(reply, 403, "project_overview_requires_active_project", "Project overview saves only apply to a selected project, not the System scope.");
    }

    const name = String(active.projectName || active.activeProject || "").trim();
    if (!name) return fail(reply, 400, "missing active project");

    const result = saveProjectFile(name, b.content);
    if (!result) return fail(reply, 500, "project save failed");
    capture.system({ tag: "Deck", event: "project_file_saved", project: name });
    return { ok: true, project: name, filePath: result.filePath };
  });
  app.post("/projects/document/save", async (req, reply) => {
    const b = bodyOf(req);
    const filePath = String(b.filePath || "").trim();
    if (!filePath || typeof b.content !== "string") return fail(reply, 400, !filePath ? "missing project file path" : "missing content");
    if (filePath.replace(/\\/g, "/").replace(/^\.\//, "") === "project.json") {
      const active = controller.activeProjectInquiry();
      if (active.projectMode !== "project" || active.isBackstage || active.projectSerial === "00") {
        return fail(reply, 403, "project_overview_requires_active_project", "project.json can only be saved for a selected normal project.");
      }
    }
    const abs = controller.resolveProjectPath(filePath);
    const rel = filePath.replace(/\\/g, "/");
    const before = existsFile(abs) ? fs.readFileSync(abs, "utf8") : "";
    const changed = before !== b.content;
    fs.mkdirSync(path.dirname(abs), { recursive: true });
    fs.writeFileSync(abs, b.content, "utf8");
    capture.code({ action: "editor-save", filePath: rel, before, after: b.content, changed, outcome: "direct-save" });
    capture.system({ tag: "Editor", event: "file_saved", filePath: rel, changed });
    return { ok: true, filePath: rel };
  });

  // Recorder controls/routes
  app.post("/pause", async (req) => {
    const b = bodyOf(req);
    if (b.cancelRemaining) return { ok: true, ...(await cancelRemainingRecorderTasks()) };
    if (b.paused === false) return { ok: true, ...resumeRecorder(), wake: await wakeRecorder() };
    return { ok: true, ...requestRecorderPause() };
  });
  app.post("/record/check", async () => await recordCheck());
  app.post("/record", recordRoute("manual"));
  app.post("/manual/record", recordRoute("manual"));
  app.post("/bridge/record", recordRoute("amp"));
  app.post("/verbatim/input", recordRoute("aux"));
  app.post("/verbatim/output", async (req, reply) => await handleInquiry(bodyOf(req), reply));
  app.post("/import/record", recordRoute("manual"));
  app.post("/remote/record", recordRoute("amp"));
  app.post("/update/record", recordRoute("update"));

  // Playback/tape
  app.get("/playback", async () => ({ ok: true, tape: await loadTape() }));
  app.get("/dubs/playback", async () => ({ ok: true, tape: await loadDubsTape() }));
  app.post("/dubs/clear", async () => ({ ok: true, cleared: true, ...(await setDubsClearAfter()) }));
  app.post("/playback/clear", async (req) => ({ ok: true, ...(await clearTapeItems(Array.isArray(bodyOf(req).kinds) ? bodyOf(req).kinds.map(String) : [])) }));
  app.post("/playback/approve", async (req) => {
    const b = bodyOf(req);
    const dubID = await approveManifestOnTape(b.manifest, { source: String(b.source || "monitor"), kind: String(b.kind || "diagnostic") as any });
    return { ok: true, dubID, recorderStatus: "YES" };
  });

  app.post("/playback/reject", async (req, reply) => {
    try {
      const b = bodyOf(req);
      const dubID = await rejectManifestOnTape({ dubID: String(b.dubID || "").trim(), manifest: b.manifest, source: String(b.source || "monitor") });
      return { ok: true, dubID, recorderStatus: "NO" };
    } catch (err: any) {
      return fail(reply, 404, err?.message || "proposal_not_found");
    }
  });

  // EQ
  app.post("/eq/chat", async (req, reply) => {
    const message = String(bodyOf(req).message || "").trim();
    if (!message) return fail(reply, 400, "missing message");
    const report = await recordChatIn(message);
    return { ok: true, reply: report ? String((report as any).summary || "AI responded.") : "(AI unavailable — check LM Studio)" };
  });
  app.get("/eq/reports", async () => ({ ok: true, reports: await getReports(100) }));
  app.get("/eq/activity", async () => {
    const messages = await getRecentActivity(40);
    return { ok: true, count: messages.length, messages };
  });

  // Recovery/restore
  app.get("/recovery", async (_req, reply) => reply.type("text/html").send(recoveryShellHtml()));
  app.get("/recovery/timeline", async (req: any) => await getTimeline(Number(req?.query?.limit || 25)));
  app.get("/recovery/sources", async () => await listRecoverySources());
  app.post("/recovery/undo", async (req) => await undo({ count: Number(bodyOf(req).count || 0), dryRun: Boolean(bodyOf(req).dryRun) }));
  app.get("/recovery/undo/recent", async (req: any) => await listRecentDubUndoCandidates(Number(req?.query?.limit || 10)));
  app.post("/recovery/undo/dub", async (req, reply) => {
    const b = bodyOf(req);
    const prepared = await prepareDubUndoManifest({ dubID: String(b.dubID || "").trim() });
    if (!prepared?.ok || !prepared?.manifest) return fail(reply, 400, prepared?.error || prepared?.reason || "dub_undo_not_available");
    if (b.dryRun || b.apply === false) return prepared;
    const wake = await wakeRecorder(prepared.manifest, "undo");
    return { ok: true, prepared: { ...prepared, manifest: prepared.manifest }, wake };
  });
  app.post("/recovery/restore/scrape", async (req) => await restoreFromScrape({ name: String(bodyOf(req).name || "").trim(), dryRun: Boolean(bodyOf(req).dryRun) }));
  app.post("/recovery/restore/backup", async (req) => await restoreFromBackup({ name: String(bodyOf(req).name || "").trim(), dryRun: Boolean(bodyOf(req).dryRun) }));
  app.post("/recovery/manifest", async (req) => await inputManifest({ manifest: bodyOf(req).manifest || {}, dryRun: Boolean(bodyOf(req).dryRun) }));
  app.post("/restore/create", async (req, reply) => {
    try {
      const b = bodyOf(req);
      return { ok: true, ...(await createRestorePoint({ system: Boolean(b.system), settings: Boolean(b.settings), projectNames: Array.isArray(b.projectNames) ? b.projectNames.map(String) : [] })) };
    } catch (err: any) { return fail(reply, 400, err?.message ?? String(err)); }
  });
  app.get("/restore/list", async () => ({ ok: true, items: await listRestorePoints() }));
  app.post("/restore/apply", async (req, reply) => {
    try {
      const name = String(bodyOf(req).name || "").trim();
      if (!name) return fail(reply, 400, "missing restore point name");
      const result = await restoreFromPoint(name);
      return { ok: true, name: result.name, restoredFiles: result.restoredFiles };
    } catch (err: any) { return fail(reply, 400, err?.message ?? String(err)); }
  });

  // Read/logs
  app.post("/read/file", async (req, reply) => await handleInquiry({ ...bodyOf(req), request: "getfileinfo" }, reply));

  app.post("/read/tree", async (req, reply) => { try { const b = bodyOf(req); const root = String(b.root || ".").trim() || "."; const result = await capture.tree({ root, includeSizes: Boolean(b.includeSizes) }); return { ok: true, root: result.tree, projectRoot: result.root, activeProject: result.serial }; } catch (err: any) { return fail(reply, 400, err?.message ?? String(err)); } });

  app.get<{ Params: { stream: string } }>("/logs/:stream", async (req: any, reply) => {
    const stream = String(req.params.stream || "");
    if (!["system", "ai", "ai-chat", "ai-diagnostic", "code"].includes(stream)) return fail(reply, 400, "unknown log stream");
    const limit = Math.max(1, Math.min(2000, Number.isFinite(Number(req?.query?.limit)) ? Number(req.query.limit) : 200));
    const archiveDepth = Math.max(0, Math.min(24, Number.isFinite(Number(req?.query?.archiveDepth)) ? Number(req.query.archiveDepth) : 0));
    return { ok: true, stream, archiveDepth, lines: await readLog(stream as any, limit, archiveDepth) };
  });
  app.post<{ Params: { stream: string } }>("/logs/:stream/clear", async (req: any, reply) => {
    const stream = String(req.params.stream || "");
    if (!["system", "ai", "ai-chat", "ai-diagnostic", "code"].includes(stream)) return fail(reply, 400, "unknown log stream");
    await clearLog(stream as any);
    capture.system({ tag: "Deck", event: "log_view_reset", stream });
    return { ok: true, stream };
  });

  // Editor
  app.post("/editor/system-action", async (req, reply) => {
    const b = bodyOf(req);
    try {
      return { ok: true, ...(await runEditorSystemAction({ action: String(b.action || "").trim(), filePath: String(b.filePath || "").trim(), payload: b.payload })) };
    } catch (err: any) {
      capture.system({ tag: "Editor", event: "editor_system_action_failed", action: String(b.action || ""), filePath: String(b.filePath || ""), error: err?.message ?? String(err) });
      return fail(reply, 400, err?.message ?? String(err));
    }
  });

  await app.listen({ port, host: "127.0.0.1" });
  capture.system({ tag: "Deck", event: "server_started", port });
  console.log(`DECK running at http://127.0.0.1:${port}\nMONITOR at    http://127.0.0.1:${port}/`);

  setInterval(() => { keepalive().catch(() => {}); }, 6 * 60 * 60 * 1000);
}
