import { app } from "electron"
import Fastify, { FastifyInstance, FastifyReply, FastifyRequest } from "fastify"
import cors from "@fastify/cors"
import { Server as MCPServer } from "@modelcontextprotocol/sdk/server/index.js"
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"
import { CallToolRequestSchema, ListToolsRequestSchema, isInitializeRequest } from "@modelcontextprotocol/sdk/types.js"
import crypto from "crypto"
import fs from "fs"
import os from "os"
import path from "path"
import QRCode from "qrcode"
import { configStore, recordingsFolder } from "./config"
import { getConversationIdValidationError } from "./conversation-id"
import {
  getResolvedRemoteServerApiKey,
  hasConfiguredRemoteServerApiKey,
} from "./remote-server-secret"
import {
  DISCORD_SECRET_MASK,
  getDiscordLifecycleAction,
  getDiscordResolvedDefaultProfileId,
  getMaskedDiscordBotToken,
} from "./discord-config"
import { diagnosticsService } from "./diagnostics"
import { discordService } from "./discord-service"
import { getErrorMessage } from "./error-utils"
import {
  checkCloudflaredInstalled,
  checkCloudflaredLoggedIn,
  getCloudflareTunnelStatus,
  listCloudflareTunnels,
  startCloudflareTunnel,
  startNamedCloudflareTunnel,
  stopCloudflareTunnel,
} from "./cloudflare-tunnel"
import { mcpService, MCPToolResult, WHATSAPP_SERVER_NAME, handleWhatsAppToggle } from "./mcp-service"
import { processTranscriptWithAgentMode } from "./llm"
import { processTranscriptWithACPAgent } from "./acp-main-agent"
import { resolveMainAcpAgentSelection } from "./main-agent-selection"
import { state, agentProcessManager, agentSessionStateManager, toolApprovalManager } from "./state"
import { conversationService } from "./conversation-service"
import {
  getConversationVideoAssetPath,
  getConversationVideoMimeTypeFromFileName,
} from "./conversation-video-assets"
import { getConversationImageAssetPath } from "./conversation-image-assets"
import { AgentProgressUpdate, SessionProfileSnapshot, LoopConfig, Config, MCPConfig, MCPServerConfig, OAuthConfig, normalizeAgentProfileRole } from "../shared/types"
import { getBranchMessageIndexMap } from "@shared/conversation-progress"
import { inferTransportType } from "../shared/mcp-utils"
import { agentSessionTracker } from "./agent-session-tracker"
import { emergencyStopAll } from "./emergency-stop"
import { sendMessageNotification, isPushEnabled, clearBadgeCount } from "./push-notification-service"
import { skillsService } from "./skills-service"
import { knowledgeNotesService } from "./knowledge-notes-service"
import { sanitizeAgentProfileConnection, VALID_AGENT_PROFILE_CONNECTION_TYPES } from "./agent-profile-connection-sanitize"
import { isRuntimeTool } from "./runtime-tools"
import { agentProfileService, createSessionSnapshotFromProfile, toolConfigToMcpServerConfig } from "./agent-profile-service"
import { generateTTS } from "./tts-service"
import { getRendererHandlers } from "@egoist/tipc/main"
import {
  getAcpSessionForClientSessionToken,
  getAppSessionForAcpSession,
  getPendingAppSessionForClientSessionToken,
} from "./acp-session-state"
import {
  MANUAL_RELEASES_URL,
  checkForUpdatesAndDownload,
  downloadLatestReleaseAsset,
  getUpdateInfo,
  openManualReleasesPage,
  openDownloadedReleaseAsset,
  revealDownloadedReleaseAsset,
} from "./updater"
import {
  WINDOWS,
  getWindowRendererHandlers,
  hideFloatingPanelWindow,
  resetFloatingPanelPositionAndSize,
  setPanelMode,
  showMainWindow,
  showPanelWindow,
} from "./window"
import {
  setTrackedAgentSessionSnoozed,
  snoozeAgentSessionsAndHidePanelWindow,
} from "./floating-panel-session-state"
import { messageQueueService } from "./message-queue-service"
import type {
  OperatorActionResponse,
  OperatorApiKeyRotationResponse,
  OperatorAuditEntry,
  OperatorAuditResponse,
  OperatorDiagnosticReport,
  OperatorDiagnosticReportSaveResponse,
  OperatorHealthSnapshot,
  OperatorIntegrationsSummary,
  OperatorLogSummary,
  OperatorMessageQueuesResponse,
  OperatorConversationsResponse,
  ModelPreset,
  ModelPresetSummary,
  ModelPresetsResponse,
  ModelPresetMutationResponse,
  OperatorRecentErrorsResponse,
  OperatorRemoteServerStatus,
  OperatorRuntimeStatus,
  OperatorSessionsSummary,
  OperatorSystemMetrics,
  OperatorTunnelStatus,
  OperatorUpdaterStatus,
  OperatorWhatsAppIntegrationSummary,
} from "@dotagents/shared"
import type { RendererHandlers } from "./renderer-handlers"
import { INJECTED_RUNTIME_TOOL_TRANSPORT_NAME, RESERVED_RUNTIME_TOOL_SERVER_NAMES } from "../shared/runtime-tool-names"

let server: FastifyInstance | null = null
let lastError: string | undefined

const REMOTE_SERVER_SECRET_MASK = "••••••••"
const CONVERSATION_IMAGE_MIME_TYPES: Record<string, string> = {
  ".png": "image/png",
  ".apng": "image/apng",
  ".gif": "image/gif",
  ".jpg": "image/jpeg",
  ".jpeg": "image/jpeg",
  ".webp": "image/webp",
  ".bmp": "image/bmp",
  ".avif": "image/avif",
}

function getConversationImageMimeTypeFromFileName(fileName: string): string {
  return CONVERSATION_IMAGE_MIME_TYPES[path.extname(fileName).toLowerCase()] || "application/octet-stream"
}
const OPERATOR_AUDIT_LOG_LIMIT = 200
const OPERATOR_AUDIT_DEVICE_HEADER_KEYS = ["x-device-id", "x-dotagents-device-id"] as const
const SENSITIVE_OPERATOR_SETTINGS_KEYS = new Set([
  "remoteServerEnabled",
  "remoteServerPort",
  "remoteServerBindAddress",
  "remoteServerApiKey",
  "remoteServerLogLevel",
  "remoteServerCorsOrigins",
  "remoteServerOperatorAllowDeviceIds",
  "remoteServerAutoShowPanel",
  "remoteServerTerminalQrEnabled",
  "hideDockIcon",
  "launchAtLogin",
  "themePreference",
  "textInputEnabled",
  "panelPosition",
  "panelDragEnabled",
  "floatingPanelAutoShow",
  "hidePanelWhenMainFocused",
  "cloudflareTunnelMode",
  "cloudflareTunnelAutoStart",
  "cloudflareTunnelId",
  "cloudflareTunnelName",
  "cloudflareTunnelCredentialsPath",
  "cloudflareTunnelHostname",
  "whatsappEnabled",
  "whatsappAllowFrom",
  "whatsappOperatorAllowFrom",
  "whatsappAutoReply",
  "whatsappLogMessages",
  "discordEnabled",
  "discordBotToken",
  "discordDmEnabled",
  "discordRequireMention",
  "discordAllowUserIds",
  "discordAllowGuildIds",
  "discordAllowChannelIds",
  "discordAllowRoleIds",
  "discordDmAllowUserIds",
  "discordOperatorAllowUserIds",
  "discordOperatorAllowGuildIds",
  "discordOperatorAllowChannelIds",
  "discordOperatorAllowRoleIds",
  "discordDefaultProfileId",
  "discordLogMessages",
  "langfuseEnabled",
  "langfusePublicKey",
  "langfuseSecretKey",
  "langfuseBaseUrl",
  "localTraceLoggingEnabled",
  "localTraceLogPath",
  "openaiApiKey",
  "openaiBaseUrl",
  "groqApiKey",
  "groqBaseUrl",
  "geminiApiKey",
  "geminiBaseUrl",
  "chatgptWebAccessToken",
  "chatgptWebSessionToken",
  "chatgptWebBaseUrl",
])
const operatorAuditLog: OperatorAuditEntry[] = []
const operatorAuditLogPath = path.join(app.getPath("userData"), "operator-audit-log.jsonl")
let operatorAuditLogLoaded = false

function sanitizeStringList(values: unknown[]): string[] {
  return [...new Set(
    values
      .filter((value: unknown): value is string => typeof value === "string")
      .map((value) => value.trim())
      .filter(Boolean),
  )]
}

function getSavedModelPresets(cfg: Pick<Config, "modelPresets">): ModelPreset[] {
  return Array.isArray(cfg.modelPresets) ? cfg.modelPresets : []
}

function normalizeModelPresetString(value: unknown, maxLength: number): string | undefined {
  if (typeof value !== "string") return undefined
  return value.trim().slice(0, maxLength)
}

async function getMergedModelPresetState(cfg: Pick<Config, "modelPresets" | "openaiApiKey" | "currentModelPresetId">): Promise<{
  defaultModelPresetId: string
  presets: ModelPreset[]
}> {
  const { DEFAULT_MODEL_PRESET_ID, getBuiltInModelPresets } = await import("@dotagents/shared")
  const builtInPresets = getBuiltInModelPresets()
  const savedPresets = getSavedModelPresets(cfg)
  const builtInIds = new Set(builtInPresets.map((preset) => preset.id))

  const mergedBuiltInPresets = builtInPresets.map((builtIn) => {
    const savedOverride = savedPresets.find((preset) => preset.id === builtIn.id)
    const mergedPreset: ModelPreset = savedOverride
      ? { ...builtIn, ...savedOverride, isBuiltIn: true }
      : { ...builtIn, isBuiltIn: true }

    if (mergedPreset.id === DEFAULT_MODEL_PRESET_ID && !mergedPreset.apiKey && cfg.openaiApiKey) {
      return { ...mergedPreset, apiKey: cfg.openaiApiKey }
    }

    return mergedPreset
  })

  const customPresets = savedPresets
    .filter((preset) => !builtInIds.has(preset.id))
    .map((preset) => ({ ...preset, isBuiltIn: false }))

  return {
    defaultModelPresetId: DEFAULT_MODEL_PRESET_ID,
    presets: [...mergedBuiltInPresets, ...customPresets],
  }
}

function formatModelPresetSummary(preset: ModelPreset): ModelPresetSummary {
  const { apiKey: _apiKey, ...summary } = preset
  const hasApiKey = typeof preset.apiKey === "string" && preset.apiKey.length > 0

  return {
    ...summary,
    isBuiltIn: preset.isBuiltIn ?? false,
    apiKey: hasApiKey ? REMOTE_SERVER_SECRET_MASK : "",
    hasApiKey,
  }
}

async function buildModelPresetsResponse(cfg: Config): Promise<ModelPresetsResponse> {
  const { defaultModelPresetId, presets } = await getMergedModelPresetState(cfg)
  return {
    currentModelPresetId: cfg.currentModelPresetId || defaultModelPresetId,
    presets: presets.map(formatModelPresetSummary),
  }
}

function buildModelPresetMutationResponse(
  presetsResponse: ModelPresetsResponse,
  options: { preset?: ModelPreset; deletedPresetId?: string } = {},
): ModelPresetMutationResponse {
  return {
    success: true,
    ...presetsResponse,
    ...(options.preset ? { preset: formatModelPresetSummary(options.preset) } : {}),
    ...(options.deletedPresetId ? { deletedPresetId: options.deletedPresetId } : {}),
  }
}

function getModelPresetActivationUpdates(preset: ModelPreset): Partial<Config> {
  const updates: Partial<Config> = {
    currentModelPresetId: preset.id,
    openaiBaseUrl: preset.baseUrl,
    openaiApiKey: preset.apiKey || "",
  }
  const agentModel = preset.agentModel || preset.mcpToolsModel
  if (agentModel) {
    updates.agentOpenaiModel = agentModel
    updates.mcpToolsOpenaiModel = agentModel
  }
  if (preset.transcriptProcessingModel) {
    updates.transcriptPostProcessingOpenaiModel = preset.transcriptProcessingModel
  }
  return updates
}

function buildModelPresetUpdatePatch(body: unknown, existingPreset: ModelPreset): Partial<ModelPreset> {
  const requestBody = body && typeof body === "object" && !Array.isArray(body)
    ? body as Record<string, unknown>
    : {}
  const patch: Partial<ModelPreset> = {}

  if (!existingPreset.isBuiltIn) {
    const name = normalizeModelPresetString(requestBody.name, 120)
    const baseUrl = normalizeModelPresetString(requestBody.baseUrl, 500)
    if (name !== undefined) patch.name = name
    if (baseUrl !== undefined) patch.baseUrl = baseUrl
  }

  const apiKey = normalizeModelPresetString(requestBody.apiKey, 2000)
  if (apiKey !== undefined && apiKey !== REMOTE_SERVER_SECRET_MASK && apiKey.length > 0) {
    patch.apiKey = apiKey
  }

  const agentModel = normalizeModelPresetString(requestBody.agentModel ?? requestBody.mcpToolsModel, 300)
  if (agentModel !== undefined) {
    patch.agentModel = agentModel
    patch.mcpToolsModel = agentModel
  }

  const transcriptProcessingModel = normalizeModelPresetString(requestBody.transcriptProcessingModel, 300)
  if (transcriptProcessingModel !== undefined) {
    patch.transcriptProcessingModel = transcriptProcessingModel
  }

  return patch
}

async function upsertModelPresetOverride(
  cfg: Config,
  presetId: string,
  patch: Partial<ModelPreset>,
): Promise<ModelPreset[]> {
  const savedPresets = getSavedModelPresets(cfg)
  const savedIndex = savedPresets.findIndex((preset) => preset.id === presetId)
  const { presets } = await getMergedModelPresetState(cfg)
  const existingPreset = presets.find((preset) => preset.id === presetId)
  if (!existingPreset) return savedPresets

  const nextPreset: ModelPreset = {
    ...existingPreset,
    ...patch,
    isBuiltIn: existingPreset.isBuiltIn ?? false,
    updatedAt: Date.now(),
  }

  if (savedIndex >= 0) {
    return savedPresets.map((preset) => (preset.id === presetId ? nextPreset : preset))
  }

  return [...savedPresets, nextPreset]
}

// Track webContents IDs that already have a pending did-finish-load notification queued,
// to avoid registering multiple once-listeners if notifyConversationHistoryChanged() is
// called several times while a window is still loading.
const pendingNotificationWebContentsIds = new Set<number>()

/**
 * Notify all renderer windows that conversation history has changed.
 * Used after remote server creates or modifies conversations (e.g. from mobile).
 * Defers the notification if the window's renderer is still loading to avoid dropped events.
 * Uses pendingNotificationWebContentsIds to deduplicate deferred listeners.
 */
function notifyConversationHistoryChanged(): void {
  const notifiedWebContentsIds = new Set<number>()
  for (const windowId of ["main", "panel"] as const) {
    const win = WINDOWS.get(windowId)
    if (!win || win.isDestroyed() || win.webContents.isDestroyed()) {
      continue
    }
    if (notifiedWebContentsIds.has(win.webContents.id)) {
      continue
    }

    notifiedWebContentsIds.add(win.webContents.id)
    const sendNotification = () => {
      pendingNotificationWebContentsIds.delete(win.webContents.id)
      try {
        getRendererHandlers<RendererHandlers>(win.webContents).conversationHistoryChanged?.send()
      } catch (err) {
        diagnosticsService.logWarning("remote-server", `Failed to notify ${windowId} window about conversation history changes: ${err instanceof Error ? err.message : String(err)}`)
      }
    }

    if (win.webContents.isLoading()) {
      // Only register a did-finish-load listener if one isn't already pending for this webContents,
      // to avoid listener buildup when called multiple times during window load.
      if (!pendingNotificationWebContentsIds.has(win.webContents.id)) {
        pendingNotificationWebContentsIds.add(win.webContents.id)
        win.webContents.once("did-finish-load", sendNotification)
        // If the window is destroyed before it finishes loading, clean up to prevent
        // the webContents ID from being permanently retained in the pending set.
        win.webContents.once("destroyed", () => {
          pendingNotificationWebContentsIds.delete(win.webContents.id)
        })
      }
    } else {
      sendNotification()
    }
  }
}

interface AcpMcpRequestContext {
  appSessionId: string
  profileSnapshot: SessionProfileSnapshot
}

const INVALID_ACP_SESSION_CONTEXT_ERROR = "Unauthorized: invalid ACP session context"

interface InjectedMcpTransportState {
  server: MCPServer
  transport: StreamableHTTPServerTransport
}

interface OperatorAuditContext {
  action?: string
  success?: boolean
  details?: Record<string, unknown>
  failureReason?: string
}

const injectedMcpTransportsByToken = new Map<string, Map<string, InjectedMcpTransportState>>()

function getAcpMcpRequestContext(
  acpSessionToken: string | undefined,
): AcpMcpRequestContext | undefined {
  if (!acpSessionToken) return undefined

  const acpSessionId = getAcpSessionForClientSessionToken(acpSessionToken)
  const appSessionId = (acpSessionId
    ? getAppSessionForAcpSession(acpSessionId)
    : undefined) ?? getPendingAppSessionForClientSessionToken(acpSessionToken)
  if (!appSessionId) return undefined

  const profileSnapshot = agentSessionStateManager.getSessionProfileSnapshot(appSessionId)
    ?? agentSessionTracker.getSessionProfileSnapshot(appSessionId)

  if (!profileSnapshot) return undefined

  return {
    appSessionId,
    profileSnapshot,
  }
}

function getInjectedRuntimeToolsForAcpSession(
  acpSessionToken: string | undefined,
): { requestContext: AcpMcpRequestContext; tools: Array<{ name: string; description: string; inputSchema: unknown }> } | undefined {
  const requestContext = getAcpMcpRequestContext(acpSessionToken)
  if (!requestContext) return undefined

  const tools = mcpService.getAvailableToolsForProfile(requestContext.profileSnapshot.mcpServerConfig)
    .filter((tool) => isRuntimeTool(tool.name))
    .map((tool) => ({
      name: tool.name,
      description: tool.description,
      inputSchema: tool.inputSchema,
    }))

  return { requestContext, tools }
}

function getInjectedMcpTransportSessionMap(acpSessionToken: string): Map<string, InjectedMcpTransportState> {
  const existing = injectedMcpTransportsByToken.get(acpSessionToken)
  if (existing) return existing

  const created = new Map<string, InjectedMcpTransportState>()
  injectedMcpTransportsByToken.set(acpSessionToken, created)
  return created
}

function createInjectedMcpServer(acpSessionToken: string): MCPServer {
  const server = new MCPServer(
    {
      name: INJECTED_RUNTIME_TOOL_TRANSPORT_NAME,
      version: "1.0.0",
    },
    {
      capabilities: {
        tools: {},
      },
    },
  )

  server.setRequestHandler(ListToolsRequestSchema, async () => {
    const injectedRuntimeTools = getInjectedRuntimeToolsForAcpSession(acpSessionToken)
    if (!injectedRuntimeTools) {
      throw new Error(INVALID_ACP_SESSION_CONTEXT_ERROR)
    }

    return { tools: injectedRuntimeTools.tools }
  })

  server.setRequestHandler(CallToolRequestSchema, async (request) => {
    const injectedRuntimeTools = getInjectedRuntimeToolsForAcpSession(acpSessionToken)
    if (!injectedRuntimeTools) {
      return {
        content: [{ type: "text", text: INVALID_ACP_SESSION_CONTEXT_ERROR }],
        isError: true,
      }
    }

    const { name, arguments: args } = request.params
    if (!name || typeof name !== "string") {
      return {
        content: [{ type: "text", text: "Missing or invalid 'name' parameter" }],
        isError: true,
      }
    }

    if (!injectedRuntimeTools.tools.some((tool) => tool.name === name)) {
      return {
        content: [{ type: "text", text: `Unknown runtime tool: ${name}` }],
        isError: true,
      }
    }

    const result = await mcpService.executeToolCall(
      { name, arguments: args || {} } as any,
      undefined,
      false,
      injectedRuntimeTools.requestContext.appSessionId,
      injectedRuntimeTools.requestContext.profileSnapshot.mcpServerConfig,
    )

    return result
      ? {
        content: result.content,
        isError: result.isError,
      }
      : {
        content: [{ type: "text", text: "Tool execution returned null" }],
        isError: true,
      }
  })

  return server
}

/**
 * Detects if we're running in a headless/terminal environment
 * This helps auto-print QR codes when no GUI is available
 */
function isHeadlessEnvironment(): boolean {
  // Linux without DISPLAY or WAYLAND_DISPLAY is headless
  if (process.platform === "linux") {
    const hasDisplay = process.env.DISPLAY || process.env.WAYLAND_DISPLAY
    if (!hasDisplay) {
      return true
    }
  }
  // Check for explicit terminal mode flag
  if (process.env.DOTAGENTS_TERMINAL_MODE === "1") {
    return true
  }
  return false
}

/**
 * Prints a QR code to the terminal for mobile app pairing
 * @param url The server URL (e.g., http://192.168.1.100:3210/v1)
 * @param apiKey The API key for authentication
 * @returns true if QR code was printed successfully, false on error
 */
async function printTerminalQRCode(url: string, apiKey: string): Promise<boolean> {
  const qrValue = `dotagents://config?baseUrl=${encodeURIComponent(url)}&apiKey=${encodeURIComponent(apiKey)}`

  try {
    // Generate QR code as terminal-friendly ASCII art
    const qrString = await QRCode.toString(qrValue, {
      type: "terminal",
      small: true,
      errorCorrectionLevel: "M"
    })

    console.log("\n" + "=".repeat(60))
    console.log("📱 Mobile App Connection QR Code")
    console.log("=".repeat(60))
    console.log("\nScan this QR code with the DotAgents mobile app to connect:\n")
    console.log(qrString)
    console.log("Server URL:", url)
    console.log("API Key:", redact(apiKey))
    console.log("\n" + "=".repeat(60) + "\n")

    diagnosticsService.logInfo("remote-server", "Terminal QR code printed for mobile app pairing")
    return true
  } catch (err) {
    console.error("[Remote Server] Failed to generate terminal QR code:", err)
    diagnosticsService.logError("remote-server", "Failed to generate terminal QR code", err)
    return false
  }
}

function redact(value?: string) {
  if (!value) return ""
  if (value.length <= 8) return "***"
  return `${value.slice(0, 4)}...${value.slice(-4)}`
}

function generateRemoteServerApiKey(): string {
  return crypto.randomBytes(32).toString("hex")
}

function sanitizeOperatorAuditText(value: string | undefined, maxLength: number = 160): string | undefined {
  if (!value) {
    return undefined
  }

  const sanitized = value.replace(/\s+/g, " ").trim()
  if (!sanitized) {
    return undefined
  }

  return sanitized.length > maxLength ? sanitized.slice(0, maxLength) : sanitized
}

function sanitizeOperatorAuditValue(value: unknown, depth: number = 0): unknown {
  if (value === null || value === undefined) {
    return undefined
  }

  if (typeof value === "boolean" || typeof value === "number") {
    return value
  }

  if (typeof value === "string") {
    return sanitizeOperatorAuditText(value)
  }

  if (Array.isArray(value)) {
    const sanitizedItems = value
      .slice(0, 20)
      .map((entry) => sanitizeOperatorAuditValue(entry, depth + 1))
      .filter((entry) => entry !== undefined)

    return sanitizedItems.length > 0 ? sanitizedItems : undefined
  }

  if (typeof value === "object" && depth < 2) {
    const sanitized: Record<string, unknown> = {}

    for (const [key, entry] of Object.entries(value)) {
      if (/(token|secret|api.?key|password|credential)/i.test(key)) {
        continue
      }

      const sanitizedEntry = sanitizeOperatorAuditValue(entry, depth + 1)
      if (sanitizedEntry !== undefined) {
        sanitized[key] = sanitizedEntry
      }
    }

    return Object.keys(sanitized).length > 0 ? sanitized : undefined
  }

  return undefined
}

function sanitizeOperatorAuditDetails(details: Record<string, unknown> | undefined): Record<string, unknown> | undefined {
  const sanitized = sanitizeOperatorAuditValue(details)
  return sanitized && typeof sanitized === "object" && !Array.isArray(sanitized)
    ? sanitized as Record<string, unknown>
    : undefined
}

function getRequestHeaderValue(request: Pick<FastifyRequest, "headers">, headerName: string): string | undefined {
  const rawValue = request.headers[headerName]
  const value = Array.isArray(rawValue) ? rawValue[0] : rawValue
  return typeof value === "string" ? sanitizeOperatorAuditText(value) : undefined
}

function getOperatorAuditPath(request: FastifyRequest): string {
  return request.url.split("?")[0] || "/"
}

function buildOperatorAuditActionFromPath(pathname: string): string {
  const normalized = pathname.replace(/^\/v1\/operator\/?/, "")
  return normalized ? normalized.replace(/\//g, "-") : "operator-action"
}

function getOperatorAuditDeviceId(request: Pick<FastifyRequest, "headers">): string | undefined {
  for (const headerName of OPERATOR_AUDIT_DEVICE_HEADER_KEYS) {
    const value = getRequestHeaderValue(request, headerName)
    if (value) {
      return sanitizeOperatorAuditText(value, 80)
    }
  }

  return undefined
}

function getOperatorAuditSource(request: FastifyRequest): OperatorAuditEntry["source"] | undefined {
  const ip = sanitizeOperatorAuditText(request.ip, 80)
  const origin = getRequestHeaderValue(request, "origin")
  const userAgent = sanitizeOperatorAuditText(getRequestHeaderValue(request, "user-agent"), 160)

  const source = {
    ...(ip ? { ip } : {}),
    ...(origin ? { origin } : {}),
    ...(userAgent ? { userAgent } : {}),
  }

  return Object.keys(source).length > 0 ? source : undefined
}

function isLoopbackIp(ip: string | undefined): boolean {
  if (!ip) return false
  return ip === "127.0.0.1" || ip === "::1" || ip === "::ffff:127.0.0.1"
}

function isProtectedOperatorAccessPath(pathname: string): boolean {
  return pathname === "/v1/settings"
    || pathname === "/v1/emergency-stop"
    || pathname === "/v1/agent-profiles/verify-command"
    || pathname.startsWith("/v1/mcp/")
    || pathname.startsWith("/v1/operator/")
}

function recordRejectedOperatorDeviceAttempt(request: FastifyRequest, failureReason: string): void {
  appendOperatorAuditEntry({
    timestamp: Date.now(),
    action: "device-access-denied",
    path: getOperatorAuditPath(request),
    success: false,
    ...(getOperatorAuditDeviceId(request) ? { deviceId: getOperatorAuditDeviceId(request) } : {}),
    ...(getOperatorAuditSource(request) ? { source: getOperatorAuditSource(request) } : {}),
    failureReason: sanitizeOperatorAuditText(failureReason, 120),
  })
}

function isOperatorAuditEntry(value: unknown): value is OperatorAuditEntry {
  if (!value || typeof value !== "object" || Array.isArray(value)) return false

  const entry = value as Record<string, unknown>
  return typeof entry.timestamp === "number"
    && typeof entry.action === "string"
    && typeof entry.path === "string"
    && typeof entry.success === "boolean"
}

function writeOperatorAuditLogToDisk(): void {
  try {
    fs.mkdirSync(path.dirname(operatorAuditLogPath), { recursive: true })
    const payload = operatorAuditLog.map((entry) => JSON.stringify(entry)).join("\n")
    fs.writeFileSync(operatorAuditLogPath, payload ? `${payload}\n` : "", "utf8")
  } catch (error) {
    diagnosticsService.logError("remote-server", "Failed to write operator audit log", error)
  }
}

function ensureOperatorAuditLogLoaded(): void {
  if (operatorAuditLogLoaded) return
  operatorAuditLogLoaded = true

  try {
    if (!fs.existsSync(operatorAuditLogPath)) return

    const raw = fs.readFileSync(operatorAuditLogPath, "utf8")
    if (!raw.trim()) return

    const parsedEntries = raw
      .split(/\r?\n/)
      .map((line) => line.trim())
      .filter(Boolean)
      .map((line) => {
        try {
          return JSON.parse(line) as unknown
        } catch {
          return null
        }
      })
      .filter((entry): entry is OperatorAuditEntry => isOperatorAuditEntry(entry))
      .slice(-OPERATOR_AUDIT_LOG_LIMIT)

    operatorAuditLog.splice(0, operatorAuditLog.length, ...parsedEntries)
  } catch (error) {
    diagnosticsService.logError("remote-server", "Failed to load operator audit log", error)
  }
}

function appendOperatorAuditEntry(entry: OperatorAuditEntry): void {
  ensureOperatorAuditLogLoaded()
  operatorAuditLog.push(entry)
  let shouldRewrite = false

  if (operatorAuditLog.length > OPERATOR_AUDIT_LOG_LIMIT) {
    operatorAuditLog.splice(0, operatorAuditLog.length - OPERATOR_AUDIT_LOG_LIMIT)
    shouldRewrite = true
  }

  if (shouldRewrite) {
    writeOperatorAuditLogToDisk()
    return
  }

  try {
    fs.mkdirSync(path.dirname(operatorAuditLogPath), { recursive: true })
    fs.appendFileSync(operatorAuditLogPath, `${JSON.stringify(entry)}\n`, "utf8")
  } catch (error) {
    diagnosticsService.logError("remote-server", "Failed to append operator audit entry", error)
  }
}

function recordOperatorAuditEvent(
  request: FastifyRequest,
  options: {
    action: string
    path?: string
    success: boolean
    details?: Record<string, unknown>
    failureReason?: string
  },
): void {
  const deviceId = getOperatorAuditDeviceId(request)
  const source = getOperatorAuditSource(request)
  const details = sanitizeOperatorAuditDetails(options.details)

  appendOperatorAuditEntry({
    timestamp: Date.now(),
    action: options.action,
    path: options.path ?? getOperatorAuditPath(request),
    success: options.success,
    ...(deviceId ? { deviceId } : {}),
    ...(source ? { source } : {}),
    ...(details ? { details } : {}),
    ...(options.failureReason ? { failureReason: sanitizeOperatorAuditText(options.failureReason, 120) } : {}),
  })
}

function buildOperatorAuditResponse(count: number = 20): OperatorAuditResponse {
  ensureOperatorAuditLogLoaded()
  const limit = clampOperatorCount(count, 20, 100)
  const entries = [...operatorAuditLog].slice(-limit).reverse()

  return {
    count: entries.length,
    entries,
  }
}

function getOperatorAuditContext(request: FastifyRequest): OperatorAuditContext {
  const rawRequest = request.raw as typeof request.raw & { operatorAuditContext?: OperatorAuditContext }
  if (!rawRequest.operatorAuditContext) {
    rawRequest.operatorAuditContext = {}
  }

  return rawRequest.operatorAuditContext
}

function setOperatorAuditContext(request: FastifyRequest, context: Partial<OperatorAuditContext>): void {
  Object.assign(getOperatorAuditContext(request), context)
}

function getSensitiveOperatorSettingsKeys(input: Record<string, unknown>): string[] {
  return Object.keys(input).filter((key) => SENSITIVE_OPERATOR_SETTINGS_KEYS.has(key))
}

/**
 * Gets a connectable IP address for the QR code URL
 * When bind is 0.0.0.0, we find the actual LAN IP that a mobile device can connect to.
 * When bind is 127.0.0.1 or localhost, the server is bound to loopback only and cannot
 * accept connections from mobile devices - we warn and return the original address.
 */
interface ConnectableIpOptions {
  warn?: boolean
}

function normalizeHostForComparison(host: string): string {
  const normalized = host.trim().toLowerCase()
  if (normalized.startsWith("[") && normalized.endsWith("]")) {
    return normalized.slice(1, -1)
  }
  return normalized
}

function isWildcardBindHost(host: string): boolean {
  const normalizedHost = normalizeHostForComparison(host)
  return normalizedHost === "0.0.0.0" || normalizedHost === "::"
}

function isLoopbackHost(host: string): boolean {
  const normalizedHost = normalizeHostForComparison(host)
  return normalizedHost === "127.0.0.1" || normalizedHost === "localhost" || normalizedHost === "::1"
}

function isConnectableIpv6Address(host: string): boolean {
  const normalizedHost = normalizeHostForComparison(host)
  // Zone-scoped addresses (e.g. fe80::1%en0) are not reliable for QR/deep-link URLs.
  if (normalizedHost.includes("%")) {
    return false
  }
  // Exclude unspecified/loopback/link-local/multicast ranges.
  if (normalizedHost === "::" || normalizedHost === "::1" || /^fe[89ab]/.test(normalizedHost) || normalizedHost.startsWith("ff")) {
    return false
  }
  return true
}

function formatHostForHttpUrl(host: string): string {
  const normalizedHost = host.trim()
  // IPv6 literals must be bracketed in URLs: http://[::1]:3210/v1
  if (normalizedHost.includes(":") && !normalizedHost.startsWith("[") && !normalizedHost.endsWith("]")) {
    return `[${normalizedHost}]`
  }
  return normalizedHost
}

function buildRemoteServerBaseUrl(host: string, port: number): string {
  return `http://${formatHostForHttpUrl(host)}:${port}/v1`
}

function getConnectableIp(bind: string, options: ConnectableIpOptions = {}): string {
  const { warn = true } = options
  const normalizedBind = normalizeHostForComparison(bind)

  // If bound to loopback, warn that mobile devices cannot connect
  if (isLoopbackHost(normalizedBind)) {
    if (warn) {
      console.warn(
        `[Remote Server] Warning: Server is bound to ${normalizedBind} (loopback only). ` +
        `Mobile devices on the same network cannot connect. ` +
        `Change bind address to 0.0.0.0 or your LAN IP for mobile access.`
      )
    }
    return normalizedBind
  }

  // If already a specific IP (not wildcard), use it
  if (!isWildcardBindHost(normalizedBind)) {
    return normalizedBind
  }

  const wildcardWantsIpv6 = normalizedBind === "::"
  let firstIpv4Address: string | undefined
  let firstIpv6Address: string | undefined

  // For wildcard binds, discover LAN-reachable interface addresses.
  const interfaces = os.networkInterfaces()
  for (const name of Object.keys(interfaces)) {
    const addrs = interfaces[name]
    if (!addrs) continue
    for (const addr of addrs) {
      if (addr.internal) {
        continue
      }
      if (addr.family === "IPv4" && !firstIpv4Address) {
        firstIpv4Address = addr.address
      }
      if (addr.family === "IPv6" && !firstIpv6Address && isConnectableIpv6Address(addr.address)) {
        firstIpv6Address = addr.address
      }
    }
  }

  if (wildcardWantsIpv6) {
    if (firstIpv6Address) {
      return firstIpv6Address
    }
    // Some platforms expose dual-stack sockets for ::, so IPv4 can still be connectable.
    if (firstIpv4Address) {
      return firstIpv4Address
    }
  } else if (firstIpv4Address) {
    return firstIpv4Address
  }

  // Fallback to the original bind address with a warning
  if (warn) {
    const expectedFamily = wildcardWantsIpv6 ? "IPv6" : "IPv4"
    console.warn(
      `[Remote Server] Warning: Could not find LAN ${expectedFamily} address. QR code will use ${normalizedBind} which may not be reachable from mobile devices.`
    )
  }
  return normalizedBind
}

function isUnconnectableHostForMobilePairing(host: string): boolean {
  return isWildcardBindHost(host) || isLoopbackHost(host)
}

function getConnectableBaseUrlForMobilePairing(
  bind: string,
  port: number,
  options: ConnectableIpOptions = {},
): string | undefined {
  const connectableHost = getConnectableIp(bind, options)
  if (isUnconnectableHostForMobilePairing(connectableHost)) {
    return undefined
  }
  return buildRemoteServerBaseUrl(connectableHost, port)
}

function resolveActiveModelId(cfg: any): string {
  const provider = cfg.agentProviderId || cfg.mcpToolsProviderId || "openai"
  if (provider === "openai") return cfg.agentOpenaiModel || cfg.mcpToolsOpenaiModel || "openai"
  if (provider === "groq") return cfg.agentGroqModel || cfg.mcpToolsGroqModel || "groq"
  if (provider === "gemini") return cfg.agentGeminiModel || cfg.mcpToolsGeminiModel || "gemini"
  if (provider === "chatgpt-web") return cfg.agentChatgptWebModel || cfg.mcpToolsChatgptWebModel || "gpt-5.4-mini"
  return String(provider)
}

function toOpenAIChatResponse(content: string, model: string) {
  return {
    id: `chatcmpl-${Date.now().toString(36)}`,
    object: "chat.completion",
    created: Math.floor(Date.now() / 1000),
    model,
    choices: [
      {
        index: 0,
        message: { role: "assistant", content },
        finish_reason: "stop",
      },
    ],
  }
}

function normalizeContent(content: any): string | null {
  if (!content) return null
  if (typeof content === "string") return content
  if (Array.isArray(content)) {
    const parts = content
      .map((p) => {
        if (typeof p === "string") return p
        if (p && typeof p === "object") {
          if (typeof p.text === "string") return p.text
          if (typeof p.content === "string") return p.content
        }
        return ""
      })
      .filter(Boolean)
    return parts.length ? parts.join(" ") : null
  }
  if (typeof content === "object" && content !== null) {
    if (typeof (content as any).text === "string") return (content as any).text
  }
  return null
}

function extractUserPrompt(body: any): string | null {
  try {
    if (!body || typeof body !== "object") return null

    if (Array.isArray(body.messages)) {
      for (let i = body.messages.length - 1; i >= 0; i--) {
        const msg = body.messages[i]
        const role = String(msg?.role || "").toLowerCase()
        if (role === "user") {
          const c = normalizeContent(msg?.content)
          if (c && c.trim()) return c.trim()
        }
      }
    }

    const prompt = normalizeContent(body.prompt)
    if (prompt && prompt.trim()) return prompt.trim()

    const input = normalizeContent(body.input)
    if (input && input.trim()) return input.trim()

    return null
  } catch {
    return null
  }
}

interface RunAgentOptions {
  prompt: string
  conversationId?: string
  profileId?: string
  onProgress?: (update: AgentProgressUpdate) => void
}

function formatConversationHistoryForApi(
  history: Array<{
    role: "user" | "assistant" | "tool"
    content: string
    displayContent?: string
    toolCalls?: any[]
    toolResults?: any[]
    timestamp?: number
  }>
): Array<{
  role: "user" | "assistant" | "tool"
  content: string
  toolCalls?: Array<{ name: string; arguments: any }>
  toolResults?: Array<{ success: boolean; content: string; error?: string }>
  timestamp?: number
}> {
  return history.map((entry) => ({
    role: entry.role,
    content: entry.content,
    toolCalls: entry.toolCalls?.map((tc: any) => ({
      name: tc.name,
      arguments: tc.arguments,
    })),
    toolResults: entry.toolResults?.map((tr: any) => {
      const contentText = Array.isArray(tr.content)
        ? tr.content.map((c: any) => c.text || c).join("\n")
        : String(tr.content || "")
      const isError = tr.isError ?? (tr.success === false)
      return {
        success: !isError,
        content: contentText,
        error: isError ? contentText : undefined,
      }
    }),
    timestamp: entry.timestamp,
  }))
}

export async function runAgent(options: RunAgentOptions): Promise<{
  content: string
  conversationId: string
  conversationHistory: Array<{
    role: "user" | "assistant" | "tool"
    content: string
    toolCalls?: Array<{ name: string; arguments: any }>
    toolResults?: Array<{ success: boolean; content: string; error?: string }>
    timestamp?: number
  }>
}> {
  const { prompt, conversationId: inputConversationId, profileId, onProgress } = options
  const cfg = configStore.get()

  // Set agent mode state for process management - ensure clean state
  state.isAgentModeActive = true
  state.shouldStopAgent = false
  state.agentIterationCount = 0

  // Load previous conversation history if conversationId is provided
  let previousConversationHistory: Array<{
    role: "user" | "assistant" | "tool"
    content: string
    toolCalls?: any[]
    toolResults?: any[]
    timestamp?: number
    branchMessageIndex?: number
  }> | undefined
  let conversationId = inputConversationId

  // Create or continue conversation - matching tipc.ts createMcpTextInput logic
  if (conversationId) {
    // Add user message to existing conversation BEFORE processing
    const updatedConversation = await conversationService.addMessageToConversation(
      conversationId,
      prompt,
      "user"
    )

    if (updatedConversation) {
      // Load conversation history excluding the message we just added (the current user input)
      // This matches tipc.ts processWithAgentMode behavior
      const messagesToConvert = updatedConversation.messages.slice(0, -1)
      const branchMessageIndexMap = getBranchMessageIndexMap(messagesToConvert)



      diagnosticsService.logInfo("remote-server", `Continuing conversation ${conversationId} with ${messagesToConvert.length} previous messages`)

      previousConversationHistory = messagesToConvert.map((msg, index) => ({
        role: msg.role,
        content: msg.content,
        ...(msg.displayContent ? { displayContent: msg.displayContent } : {}),
        toolCalls: msg.toolCalls,
        // Preserve timestamp for correct ordering in UI (matching tipc.ts)
        timestamp: msg.timestamp,
        // Convert toolResults from stored format to MCPToolResult format (matching tipc.ts)
        toolResults: msg.toolResults?.map((tr) => ({
          content: [
            {
              type: "text" as const,
              text: tr.success ? tr.content : (tr.error || tr.content),
            },
          ],
          isError: !tr.success,
        })),
        branchMessageIndex: branchMessageIndexMap[index],
      }))
    } else {
      // Conversation not found - create it with the provided ID to maintain session continuity
      diagnosticsService.logInfo("remote-server", `Conversation ${conversationId} not found, creating with provided ID`)
      const newConversation = await conversationService.createConversationWithId(conversationId, prompt, "user")
      // Update conversationId to use the actual persisted ID (which may be sanitized)
      // This ensures session lookup and later operations use the correct ID
      conversationId = newConversation.id
      // Mark that we've just created the conversation so we don't try to add another message below
      previousConversationHistory = []
      diagnosticsService.logInfo("remote-server", `Created new conversation with ID ${newConversation.id}`)
    }
  }

  // Create a new conversation if none exists (only when no conversationId was provided at all)
  if (!conversationId) {
    const newConversation = await conversationService.createConversationWithId(
      conversationService.generateConversationIdPublic(),
      prompt,
      "user"
    )
    conversationId = newConversation.id
    diagnosticsService.logInfo("remote-server", `Created new conversation ${conversationId}`)
  }

  // Try to find and revive an existing session for this conversation (matching tipc.ts)
  // Note: We use `conversationId` (which may be newly created) instead of `inputConversationId`
  // to ensure we find sessions for both existing and newly created conversations.
  // This fixes a bug where inputConversationId pointed to a non-existent conversation,
  // causing session lookup to fail and creating duplicate sessions.
  // Start snoozed unless remoteServerAutoShowPanel is enabled (affects both new and revived sessions)
  const startSnoozed = !cfg.remoteServerAutoShowPanel
  let existingSessionId: string | undefined
  if (conversationId) {
    const foundSessionId = agentSessionTracker.findSessionByConversationId(conversationId)
    if (foundSessionId) {
      // Check if session is already active - if so, preserve its current snooze state
      // This prevents unexpectedly hiding the progress UI for a session the user is watching
      const existingSession = agentSessionTracker.getSession(foundSessionId)
      const isAlreadyActive = existingSession && existingSession.status === "active"
      const snoozeForRevive = isAlreadyActive ? existingSession.isSnoozed ?? false : startSnoozed
      const revived = agentSessionTracker.reviveSession(foundSessionId, snoozeForRevive)
      if (revived) {
        existingSessionId = foundSessionId
        diagnosticsService.logInfo("remote-server", `Revived existing session ${existingSessionId}`)
      }
    }
  }

  // Determine profile snapshot for session isolation
  // If reusing an existing session, use its stored snapshot to maintain isolation
  // Only capture a new snapshot when creating a new session
  let profileSnapshot: SessionProfileSnapshot | undefined

  if (existingSessionId) {
    // Try to get the stored profile snapshot from the existing session
    profileSnapshot = agentSessionStateManager.getSessionProfileSnapshot(existingSessionId)
      ?? agentSessionTracker.getSessionProfileSnapshot(existingSessionId)
  }

  // Only capture a new snapshot if we don't have one from an existing session
  if (!profileSnapshot) {
    if (profileId) {
      const configuredProfile = agentProfileService.getById(profileId)
      if (!configuredProfile) {
        throw new Error(`Configured profile not found: ${profileId}`)
      }
      profileSnapshot = createSessionSnapshotFromProfile(configuredProfile)
    } else {
      const currentProfile = agentProfileService.getCurrentProfile()
      if (currentProfile) {
        profileSnapshot = createSessionSnapshotFromProfile(currentProfile)
      }
    }
  }

  // Start or reuse agent session
  const conversationTitle = prompt.length > 50 ? prompt.substring(0, 50) + "..." : prompt
  const sessionId = existingSessionId || agentSessionTracker.startSession(conversationId, conversationTitle, startSnoozed, profileSnapshot)

  const loadFormattedConversationHistory = async () => {
    const latestConversation = await conversationService.loadConversation(conversationId)
    return formatConversationHistoryForApi(latestConversation?.messages || [])
  }

  try {
    if (cfg.mainAgentMode === "acpx" && cfg.mainAgentName) {
      const mainAgentSelection = resolveMainAcpAgentSelection(
        cfg.mainAgentName,
        agentProfileService.getAll(),
        cfg.acpAgents || []
      )

      if ("error" in mainAgentSelection) {
        diagnosticsService.logWarning("remote-server", mainAgentSelection.error)
        return {
          content: mainAgentSelection.error,
          conversationId,
          conversationHistory: await loadFormattedConversationHistory(),
        }
      }

      const resolvedMainAgentName = mainAgentSelection.resolvedName
      if (mainAgentSelection.repairedName && mainAgentSelection.repairedName !== cfg.mainAgentName) {
        configStore.save({ ...cfg, mainAgentName: resolvedMainAgentName })
        diagnosticsService.logInfo(
          "remote-server",
          `acpx main agent \"${cfg.mainAgentName}\" not found. Auto-switched to \"${resolvedMainAgentName}\".`
        )
      }

      const runId = agentSessionStateManager.startSessionRun(sessionId, profileSnapshot)

      try {
        const result = await processTranscriptWithACPAgent(prompt, {
          agentName: resolvedMainAgentName,
          conversationId,
          sessionId,
          runId,
          onProgress,
          profileSnapshot,
        })

        if (result.success) {
          agentSessionTracker.completeSession(sessionId, "ACP agent completed successfully")
        } else {
          agentSessionTracker.errorSession(sessionId, result.error || "Unknown error")
        }

        const formattedHistory = await loadFormattedConversationHistory()
        notifyConversationHistoryChanged()

        return {
          content: result.response || result.error || "No response from agent",
          conversationId,
          conversationHistory: formattedHistory,
        }
      } finally {
        agentSessionStateManager.cleanupSession(sessionId)
      }
    }

    await mcpService.initialize()

    mcpService.registerExistingProcessesWithAgentManager()

    // Get available tools filtered by profile snapshot if available (for session isolation)
    // This ensures revived sessions use the same tool list they started with
    const availableTools = profileSnapshot?.mcpServerConfig
      ? mcpService.getAvailableToolsForProfile(profileSnapshot.mcpServerConfig)
      : mcpService.getAvailableTools()

    const executeToolCall = async (toolCall: any, onProgress?: (message: string) => void): Promise<MCPToolResult> => {
      // Pass sessionId for ACP router tools progress, and profileSnapshot.mcpServerConfig for session-aware server availability
      return await mcpService.executeToolCall(toolCall, onProgress, false, sessionId, profileSnapshot?.mcpServerConfig)
    }

    const agentResult = await processTranscriptWithAgentMode(
      prompt,
      availableTools,
      executeToolCall,
      cfg.mcpUnlimitedIterations ? Infinity : (cfg.mcpMaxIterations ?? 10),
      previousConversationHistory,
      conversationId,
      sessionId, // Pass session ID for progress routing
      onProgress, // Pass progress callback for SSE streaming
      profileSnapshot, // Pass profile snapshot for session isolation
    )

    // Mark session as completed
    agentSessionTracker.completeSession(sessionId, "Agent completed successfully")

    // Format conversation history for API response (convert MCPToolResult to ToolResult format)
    const formattedHistory = formatConversationHistoryForApi(agentResult.conversationHistory)

    // Notify renderer that conversation history has changed (for sidebar refresh)
    notifyConversationHistoryChanged()

    return { content: agentResult.content, conversationId, conversationHistory: formattedHistory }
  } catch (error) {
    // Mark session as errored
    const errorMessage = getErrorMessage(error)
    agentSessionTracker.errorSession(sessionId, errorMessage)

    // Conversation was already created/updated before agent execution started.
    // Refresh renderer history even on failure so UI reflects the latest persisted state.
    notifyConversationHistoryChanged()

    throw error
  } finally {
    // Clean up agent state to ensure next session starts fresh
    state.isAgentModeActive = false
    state.shouldStopAgent = false
    state.agentIterationCount = 0
  }
}

function recordHistory(transcript: string) {
  try {
    fs.mkdirSync(recordingsFolder, { recursive: true })
    const historyPath = path.join(recordingsFolder, "history.json")
    let history: Array<{ id: string; createdAt: number; duration: number; transcript: string }>
    try {
      history = JSON.parse(fs.readFileSync(historyPath, "utf8"))
    } catch {
      history = []
    }

    const item = {
      id: Date.now().toString(),
      createdAt: Date.now(),
      duration: 0,
      transcript,
    }
    history.push(item)
    fs.writeFileSync(historyPath, JSON.stringify(history))
  } catch (e) {
    diagnosticsService.logWarning(
      "remote-server",
      "Failed to record history item",
      e,
    )
  }
}

function clampOperatorCount(value: unknown, fallback: number, max: number): number {
  const parsed = typeof value === "number"
    ? value
    : typeof value === "string"
      ? Number.parseInt(value, 10)
      : Number.NaN

  if (!Number.isFinite(parsed)) {
    return fallback
  }

  return Math.max(1, Math.min(Math.trunc(parsed), max))
}

type RemoteServerLifecycleAction = "noop" | "start" | "stop" | "restart"

function getMaskedRemoteServerApiKey(apiKey: string | undefined): string {
  return apiKey ? REMOTE_SERVER_SECRET_MASK : ""
}

function areStringArraysEqual(a: string[], b: string[]): boolean {
  if (a.length !== b.length) {
    return false
  }

  return a.every((value, index) => value === b[index])
}

function getRemoteServerLifecycleAction(
  prev: Pick<Config, "remoteServerEnabled" | "remoteServerPort" | "remoteServerBindAddress" | "remoteServerApiKey" | "remoteServerLogLevel" | "remoteServerCorsOrigins">,
  next: Pick<Config, "remoteServerEnabled" | "remoteServerPort" | "remoteServerBindAddress" | "remoteServerApiKey" | "remoteServerLogLevel" | "remoteServerCorsOrigins">,
): RemoteServerLifecycleAction {
  const prevEnabled = !!prev.remoteServerEnabled
  const nextEnabled = !!next.remoteServerEnabled

  if (!prevEnabled && nextEnabled) {
    return "start"
  }

  if (prevEnabled && !nextEnabled) {
    return "stop"
  }

  if (!nextEnabled) {
    return "noop"
  }

  const runtimeChanged =
    (prev.remoteServerPort ?? 3210) !== (next.remoteServerPort ?? 3210)
    || (prev.remoteServerBindAddress ?? "127.0.0.1") !== (next.remoteServerBindAddress ?? "127.0.0.1")
    || (prev.remoteServerApiKey ?? "") !== (next.remoteServerApiKey ?? "")
    || (prev.remoteServerLogLevel ?? "info") !== (next.remoteServerLogLevel ?? "info")
    || !areStringArraysEqual(prev.remoteServerCorsOrigins ?? ["*"], next.remoteServerCorsOrigins ?? ["*"])

  return runtimeChanged ? "restart" : "noop"
}

function scheduleTaskAfterReply(
  reply: FastifyReply,
  description: string,
  task: () => Promise<void> | void,
): void {
  let hasRun = false

  const run = () => {
    if (hasRun) {
      return
    }
    hasRun = true

    setTimeout(() => {
      void Promise.resolve(task()).catch((error) => {
        diagnosticsService.logError("remote-server", `Failed to ${description}`, error)
      })
    }, 25)
  }

  reply.raw.once("finish", run)
  reply.raw.once("close", run)
}

function scheduleRemoteServerLifecycleActionAfterReply(
  reply: FastifyReply,
  action: RemoteServerLifecycleAction,
): void {
  if (action === "noop") {
    return
  }

  scheduleTaskAfterReply(reply, `apply remote server lifecycle action (${action})`, async () => {
    if (action === "start") {
      await startRemoteServer()
      return
    }

    if (action === "stop") {
      await stopRemoteServer()
      return
    }

    await restartRemoteServer()
  })
}

function buildOperatorRemoteServerStatus(): OperatorRemoteServerStatus {
  const status = getRemoteServerStatus()

  return {
    running: status.running,
    bind: status.bind,
    port: status.port,
    ...(status.url ? { url: status.url } : {}),
    ...(status.connectableUrl ? { connectableUrl: status.connectableUrl } : {}),
    ...(status.lastError ? { lastError: status.lastError } : {}),
  }
}

function buildOperatorTunnelStatus(): OperatorTunnelStatus {
  const status = getCloudflareTunnelStatus()

  return {
    running: status.running,
    starting: status.starting,
    mode: status.mode,
    ...(status.url ? { url: status.url } : {}),
    ...(status.error ? { error: status.error } : {}),
  }
}

function buildOperatorDiscordIntegrationSummary(): OperatorIntegrationsSummary["discord"] {
  const discordStatus = discordService.getStatus()

  return {
    available: discordStatus.available,
    enabled: discordStatus.enabled,
    connected: discordStatus.connected,
    connecting: discordStatus.connecting,
    ...(typeof discordStatus.tokenConfigured === "boolean" ? { tokenConfigured: discordStatus.tokenConfigured } : {}),
    ...(discordStatus.defaultProfileId ? { defaultProfileId: discordStatus.defaultProfileId } : {}),
    ...(discordStatus.defaultProfileName ? { defaultProfileName: discordStatus.defaultProfileName } : {}),
    ...(discordStatus.botUsername ? { botUsername: discordStatus.botUsername } : {}),
    ...(discordStatus.lastError ? { lastError: discordStatus.lastError } : {}),
    ...(discordStatus.lastEventAt ? { lastEventAt: discordStatus.lastEventAt } : {}),
    logs: buildOperatorLogSummary(discordService.getLogs()),
  }
}

async function buildOperatorTunnelSetupSummary(): Promise<{
  installed: boolean
  loggedIn: boolean
  mode: "quick" | "named"
  autoStart: boolean
  namedTunnelConfigured: boolean
  configuredTunnelId?: string
  configuredHostname?: string
  credentialsPathConfigured: boolean
  tunnelCount: number
  tunnels: Array<{ id: string; name: string; createdAt?: string }>
  error?: string
}> {
  const cfg = configStore.get()
  const installed = await checkCloudflaredInstalled()
  const loggedIn = installed ? await checkCloudflaredLoggedIn() : false

  let tunnels: Array<{ id: string; name: string; createdAt?: string }> = []
  let error: string | undefined

  if (installed && loggedIn) {
    const listResult = await listCloudflareTunnels()
    if (listResult.success) {
      tunnels = (listResult.tunnels ?? []).map((tunnel) => ({
        id: tunnel.id,
        name: tunnel.name,
        ...(tunnel.created_at ? { createdAt: tunnel.created_at } : {}),
      }))
    } else {
      error = listResult.error
    }
  }

  return {
    installed,
    loggedIn,
    mode: cfg.cloudflareTunnelMode ?? "quick",
    autoStart: !!cfg.cloudflareTunnelAutoStart,
    namedTunnelConfigured: !!cfg.cloudflareTunnelId && !!cfg.cloudflareTunnelHostname,
    ...(cfg.cloudflareTunnelId ? { configuredTunnelId: cfg.cloudflareTunnelId } : {}),
    ...(cfg.cloudflareTunnelHostname ? { configuredHostname: cfg.cloudflareTunnelHostname } : {}),
    credentialsPathConfigured: !!cfg.cloudflareTunnelCredentialsPath,
    tunnelCount: tunnels.length,
    tunnels,
    ...(error ? { error } : {}),
  }
}

async function startConfiguredCloudflareTunnel(): Promise<{
  success: boolean
  mode: "quick" | "named"
  url?: string
  error?: string
}> {
  const cfg = configStore.get()
  const mode = cfg.cloudflareTunnelMode ?? "quick"

  if (mode === "named") {
    const tunnelId = cfg.cloudflareTunnelId?.trim()
    const hostname = cfg.cloudflareTunnelHostname?.trim()

    if (!tunnelId || !hostname) {
      return {
        success: false,
        mode,
        error: "Named tunnel requires cloudflareTunnelId and cloudflareTunnelHostname",
      }
    }

    const result = await startNamedCloudflareTunnel({
      tunnelId,
      hostname,
      credentialsPath: cfg.cloudflareTunnelCredentialsPath?.trim() || undefined,
    })

    return {
      ...result,
      mode,
    }
  }

  const result = await startCloudflareTunnel()
  return {
    ...result,
    mode,
  }
}

function buildOperatorDiscordLogsResponse(count: number = 20): {
  count: number
  logs: Array<{ id: string; level: string; message: string; timestamp: number }>
} {
  const limit = clampOperatorCount(count, 20, 50)
  const logs = discordService.getLogs()
    .slice(-limit)
    .map(({ id, level, message, timestamp }) => ({ id, level, message, timestamp }))

  return {
    count: logs.length,
    logs,
  }
}

function getToolResultText(result: MCPToolResult): string | undefined {
  return result.content.find((entry) => entry.type === "text")?.text
}

function parseJsonRecord(value: string | undefined): Record<string, unknown> | undefined {
  if (!value) {
    return undefined
  }

  try {
    const parsed = JSON.parse(value)
    return parsed && typeof parsed === "object" && !Array.isArray(parsed)
      ? parsed as Record<string, unknown>
      : undefined
  } catch {
    return undefined
  }
}

function getSanitizedWhatsAppOperatorDetails(parsed: Record<string, unknown> | undefined): Record<string, unknown> | undefined {
  if (!parsed) {
    return undefined
  }

  const details: Record<string, unknown> = {}

  if (typeof parsed.status === "string") {
    details.status = parsed.status
  }
  if (typeof parsed.connected === "boolean") {
    details.connected = parsed.connected
  }
  if (typeof parsed.hasCredentials === "boolean") {
    details.hasCredentials = parsed.hasCredentials
  }
  if (typeof parsed.lastError === "string") {
    details.lastError = parsed.lastError
  }

  return Object.keys(details).length > 0 ? details : undefined
}

async function runOperatorWhatsAppAction(
  toolName: "whatsapp_connect" | "whatsapp_logout",
  action: string,
  successMessage: string,
): Promise<OperatorActionResponse> {
  try {
    const serverStatus = mcpService.getServerStatus()[WHATSAPP_SERVER_NAME]
    if (!serverStatus?.connected) {
      return {
        success: false,
        action,
        message: "WhatsApp server is not running. Enable WhatsApp in settings first.",
        error: "WhatsApp server is not running",
      }
    }

    const result = await mcpService.executeToolCall(
      { name: toolName, arguments: {} },
      undefined,
      true,
    )

    const text = getToolResultText(result)
    if (result.isError) {
      const message = text || `${action} failed`
      return {
        success: false,
        action,
        message,
        error: message,
      }
    }

    const parsed = parseJsonRecord(text)
    const details = getSanitizedWhatsAppOperatorDetails(parsed)
    const message = typeof parsed?.status === "string"
      ? `WhatsApp ${parsed.status}`
      : text || successMessage

    return {
      success: true,
      action,
      message,
      ...(details ? { details } : {}),
    }
  } catch (error) {
    const message = getErrorMessage(error)
    return {
      success: false,
      action,
      message,
      error: message,
    }
  }
}

async function buildOperatorHealthSnapshot(): Promise<OperatorHealthSnapshot> {
  const health = await diagnosticsService.performHealthCheck()

  return {
    checkedAt: Date.now(),
    overall: health.overall,
    checks: health.checks,
  }
}

function buildOperatorRecentErrorsResponse(count: number = 10): OperatorRecentErrorsResponse {
  const errors = diagnosticsService.getRecentErrors(clampOperatorCount(count, 10, 50))
    .map(({ timestamp, level, component, message }) => ({
      timestamp,
      level,
      component,
      message,
    }))

  return {
    count: errors.length,
    errors,
  }
}

async function buildOperatorConversationsResponse(count: number = 10): Promise<OperatorConversationsResponse> {
  const history = await conversationService.getConversationHistory()
  const clamped = clampOperatorCount(count, 10, 50)
  const items = history.slice(0, clamped).map(({ id, title, createdAt, updatedAt, messageCount, preview }) => ({
    id,
    title,
    createdAt,
    updatedAt,
    messageCount,
    preview: preview.length > 200 ? preview.slice(0, 200) + "…" : preview,
  }))
  return { count: items.length, conversations: items }
}

function buildOperatorLogSummary(entries: Array<{ timestamp: number; level?: string }>): OperatorLogSummary {
  const lastEntry = entries[entries.length - 1]
  let errorCount = 0
  let warningCount = 0
  let infoCount = 0

  for (const entry of entries) {
    if (entry.level === "error") {
      errorCount += 1
    } else if (entry.level === "warn" || entry.level === "warning") {
      warningCount += 1
    } else if (entry.level === "info") {
      infoCount += 1
    }
  }

  const hasLevelCounts = errorCount > 0 || warningCount > 0 || infoCount > 0

  return {
    total: entries.length,
    ...(lastEntry?.timestamp ? { lastTimestamp: lastEntry.timestamp } : {}),
    ...(hasLevelCounts ? { errorCount, warningCount, infoCount } : {}),
  }
}

async function buildOperatorWhatsAppIntegrationSummary(): Promise<OperatorWhatsAppIntegrationSummary> {
  const cfg = configStore.get()
  const serverStatus = mcpService.getServerStatus()[WHATSAPP_SERVER_NAME]
  const summary: OperatorWhatsAppIntegrationSummary = {
    enabled: !!cfg.whatsappEnabled,
    available: !!serverStatus?.connected,
    connected: false,
    serverConfigured: !!cfg.mcpConfig?.mcpServers?.[WHATSAPP_SERVER_NAME],
    serverConnected: !!serverStatus?.connected,
    autoReplyEnabled: !!cfg.whatsappAutoReply,
    logMessagesEnabled: !!cfg.whatsappLogMessages,
    allowedSenderCount: cfg.whatsappAllowFrom?.length ?? 0,
    ...(serverStatus?.error ? { lastError: serverStatus.error } : {}),
    logs: buildOperatorLogSummary(mcpService.getServerLogs(WHATSAPP_SERVER_NAME)),
  }

  if (!serverStatus?.connected) {
    return summary
  }

  try {
    const statusResult = await mcpService.executeToolCall(
      { name: "whatsapp_get_status", arguments: {} },
      undefined,
      true,
    )

    if (statusResult.isError) {
      const lastError = statusResult.content.find((entry) => entry.type === "text")?.text

      return {
        ...summary,
        ...(lastError ? { lastError } : {}),
      }
    }

    const textPayload = statusResult.content.find((entry) => entry.type === "text")?.text
    if (!textPayload) {
      return summary
    }

    let parsed: {
      connected?: boolean
      hasCredentials?: boolean
      lastError?: string
    }

    try {
      parsed = JSON.parse(textPayload) as {
        connected?: boolean
        hasCredentials?: boolean
        lastError?: string
      }
    } catch {
      return summary
    }

    return {
      ...summary,
      connected: !!parsed.connected,
      ...(typeof parsed.hasCredentials === "boolean" ? { hasCredentials: parsed.hasCredentials } : {}),
      ...(parsed.lastError ? { lastError: parsed.lastError } : {}),
    }
  } catch (error) {
    diagnosticsService.logWarning(
      "remote-server",
      `Failed to summarize WhatsApp integration status: ${getErrorMessage(error)}`,
    )

    return {
      ...summary,
      lastError: getErrorMessage(error),
    }
  }
}

async function buildOperatorIntegrationsSummary(): Promise<OperatorIntegrationsSummary> {
  const cfg = configStore.get()
  const pushTokens = cfg.pushNotificationTokens ?? []

  return {
    discord: buildOperatorDiscordIntegrationSummary(),
    whatsapp: await buildOperatorWhatsAppIntegrationSummary(),
    pushNotifications: {
      enabled: pushTokens.length > 0,
      tokenCount: pushTokens.length,
      platforms: [...new Set(pushTokens.map((token) => token.platform))].sort(),
    },
  }
}

function buildOperatorUpdaterStatus(): OperatorUpdaterStatus {
  const updateInfo = getUpdateInfo() as {
    currentVersion?: string
    updateAvailable?: boolean
    latestRelease?: { tagName?: string; name?: string; publishedAt?: string; url?: string; assets?: unknown[] }
    preferredAsset?: { name?: string; downloadUrl?: string }
    lastCheckedAt?: number
    error?: string
    lastDownloadedAsset?: { name?: string; downloadedAt?: number }
  } | null

  return {
    enabled: false,
    mode: "manual",
    currentVersion: app.getVersion(),
    updateInfo,
    manualReleasesUrl: MANUAL_RELEASES_URL,
    updateAvailable: updateInfo?.updateAvailable,
    lastCheckedAt: updateInfo?.lastCheckedAt,
    lastCheckError: updateInfo?.error,
    latestRelease: updateInfo?.latestRelease?.tagName && updateInfo.latestRelease.url
      ? {
        tagName: updateInfo.latestRelease.tagName,
        name: updateInfo.latestRelease.name,
        publishedAt: updateInfo.latestRelease.publishedAt,
        url: updateInfo.latestRelease.url,
        assetCount: Array.isArray(updateInfo.latestRelease.assets) ? updateInfo.latestRelease.assets.length : 0,
      }
      : undefined,
    preferredAsset: updateInfo?.preferredAsset?.name && updateInfo.preferredAsset.downloadUrl
      ? {
        name: updateInfo.preferredAsset.name,
        downloadUrl: updateInfo.preferredAsset.downloadUrl,
      }
      : undefined,
    lastDownloadedAt: updateInfo?.lastDownloadedAsset?.downloadedAt,
    lastDownloadedFileName: updateInfo?.lastDownloadedAsset?.name,
  }
}

function buildOperatorSystemMetrics(): OperatorSystemMetrics {
  const mem = process.memoryUsage()
  return {
    platform: os.platform(),
    arch: os.arch(),
    nodeVersion: process.version,
    electronVersion: process.versions.electron,
    appVersion: app.getVersion(),
    uptimeSeconds: Math.floor(os.uptime()),
    processUptimeSeconds: Math.floor(process.uptime()),
    memoryUsage: {
      heapUsedMB: Math.round(mem.heapUsed / 1048576 * 100) / 100,
      heapTotalMB: Math.round(mem.heapTotal / 1048576 * 100) / 100,
      rssMB: Math.round(mem.rss / 1048576 * 100) / 100,
    },
    cpuCount: os.cpus().length,
    totalMemoryMB: Math.round(os.totalmem() / 1048576),
    freeMemoryMB: Math.round(os.freemem() / 1048576),
    hostname: os.hostname(),
  }
}

function buildOperatorSessionsSummary(): OperatorSessionsSummary {
  const activeSessions = agentSessionTracker.getActiveSessions()
  const recentSessions = agentSessionTracker.getRecentSessions(10)

  return {
    activeSessions: activeSessions.length,
    recentSessions: recentSessions.length,
    activeSessionDetails: activeSessions.map(formatOperatorSessionSummary),
    recentSessionDetails: recentSessions.map(formatOperatorSessionSummary),
  }
}

function formatOperatorSessionSummary(session: ReturnType<typeof agentSessionTracker.getActiveSessions>[number]) {
  return {
    id: session.id,
    title: session.conversationTitle,
    status: session.status,
    startTime: session.startTime,
    endTime: session.endTime,
    currentIteration: session.currentIteration,
    maxIterations: session.maxIterations,
    isSnoozed: session.isSnoozed,
    profileId: session.profileSnapshot?.profileId,
    profileName: session.profileSnapshot?.profileName,
  }
}

function buildOperatorMessageQueuesResponse(): OperatorMessageQueuesResponse {
  const queues = messageQueueService.getAllQueues().map((queue) => ({
    conversationId: queue.conversationId,
    isPaused: messageQueueService.isQueuePaused(queue.conversationId),
    messageCount: queue.messages.length,
    messages: queue.messages,
  }))

  return {
    count: queues.length,
    totalMessages: queues.reduce((sum, queue) => sum + queue.messageCount, 0),
    queues,
  }
}

function buildOperatorMessageQueueActionResponse(
  action: string,
  success: boolean,
  message: string,
  details?: Record<string, unknown>,
): OperatorActionResponse {
  return {
    success,
    action,
    message,
    ...(success ? {} : { error: message }),
    ...(details ? { details } : {}),
  }
}

function normalizeOperatorQueuePathParam(value: string | undefined): string {
  return decodeURIComponent(value || "").trim()
}

function notifyInactiveSessionsCleared(): void {
  for (const windowId of ["main", "panel"] as const) {
    try {
      getWindowRendererHandlers(windowId)?.clearInactiveSessions?.send()
    } catch (error) {
      diagnosticsService.logError(
        "remote-server",
        `Failed to notify ${windowId} window after clearing inactive sessions`,
        error,
      )
    }
  }
}

function clearInactiveOperatorSessions(): number {
  const clearable = agentSessionTracker
    .getRecentSessions(100)
    .filter((session) => {
      if (session.status === "active") return false
      const conversationId = session.conversationId
      return !conversationId || messageQueueService.getQueue(conversationId).length === 0
    })

  const ids = new Set(clearable.map((session) => session.id))
  agentSessionTracker.clearCompletedSessions((session) => ids.has(session.id))
  notifyInactiveSessionsCleared()
  return clearable.length
}

function parseAgentSessionCandidateLimit(query: unknown): number | { error: string } {
  const record = query && typeof query === "object" ? query as Record<string, unknown> : {}
  const rawLimit = record.limit
  if (rawLimit === undefined || rawLimit === null || rawLimit === "") {
    return 20
  }

  const parsedLimit = typeof rawLimit === "number"
    ? rawLimit
    : typeof rawLimit === "string"
      ? Number(rawLimit)
      : Number.NaN

  if (!Number.isFinite(parsedLimit)) {
    return { error: "Session candidate limit must be a number" }
  }

  return Math.max(1, Math.min(100, Math.floor(parsedLimit)))
}

function formatAgentSessionCandidate(
  session: ReturnType<typeof agentSessionTracker.getActiveSessions>[number],
) {
  return {
    id: session.id,
    conversationId: session.conversationId,
    conversationTitle: session.conversationTitle,
    status: session.status,
    startTime: session.startTime,
    endTime: session.endTime,
  }
}

async function buildOperatorRuntimeStatus(): Promise<OperatorRuntimeStatus> {
  const now = Date.now()
  const recentErrors = diagnosticsService.getRecentErrors(100)

  return {
    timestamp: now,
    remoteServer: buildOperatorRemoteServerStatus(),
    health: await buildOperatorHealthSnapshot(),
    tunnel: buildOperatorTunnelStatus(),
    integrations: await buildOperatorIntegrationsSummary(),
    updater: buildOperatorUpdaterStatus(),
    system: buildOperatorSystemMetrics(),
    sessions: buildOperatorSessionsSummary(),
    recentErrors: {
      total: recentErrors.length,
      errorsInLastFiveMinutes: recentErrors.filter((entry) => now - entry.timestamp <= 5 * 60 * 1000).length,
    },
  }
}

function scheduleRemoteServerRestartFromOperator(): void {
  setTimeout(() => {
    void restartRemoteServer().catch((error) => {
      diagnosticsService.logError(
        "remote-server",
        "Failed to restart remote server from operator action",
        error,
      )
    })
  }, 50)
}

function scheduleAppRestartFromOperator(): void {
  setTimeout(() => {
    try {
      app.relaunch()
      app.quit()
    } catch (error) {
      diagnosticsService.logError(
        "remote-server",
        "Failed to restart app from operator action",
        error,
      )
    }
  }, 100)
}

/**
 * Starts the remote server, forcing it to be enabled regardless of config.
 * Used by --qr mode to ensure the server starts even if remoteServerEnabled is false.
 * Also skips the auto-print of QR codes since --qr mode handles that separately.
 */
export async function startRemoteServerForced(options: { bindAddressOverride?: string } = {}) {
  return startRemoteServerInternal({
    forceEnabled: true,
    skipAutoPrintQR: true,
    bindAddressOverride: options.bindAddressOverride,
  })
}

export async function startRemoteServer() {
  return startRemoteServerInternal({ forceEnabled: false, skipAutoPrintQR: false })
}

interface StartRemoteServerOptions {
  forceEnabled?: boolean
  skipAutoPrintQR?: boolean
  bindAddressOverride?: string
}

async function startRemoteServerInternal(options: StartRemoteServerOptions = {}) {
  const { forceEnabled = false, skipAutoPrintQR = false, bindAddressOverride } = options
  const cfg = configStore.get()
  if (!forceEnabled && !cfg.remoteServerEnabled) {
    diagnosticsService.logInfo(
      "remote-server",
      "Remote server not enabled in config; skipping start",
    )
    return { running: false }
  }

  const configuredApiKey = getResolvedRemoteServerApiKey(cfg)
  const hasConfiguredApiKey = hasConfiguredRemoteServerApiKey(cfg)
  if (!configuredApiKey && !hasConfiguredApiKey) {
    // Generate API key on first enable
    const key = generateRemoteServerApiKey()
    configStore.save({ ...cfg, remoteServerApiKey: key })
  } else if (!configuredApiKey) {
    diagnosticsService.logWarning(
      "remote-server",
      "Remote server API key is configured but could not be resolved; preserving configured value",
    )
  }

  if (server) {
    diagnosticsService.logInfo(
      "remote-server",
      "Remote server already running; restarting",
    )
    await stopRemoteServer()
  }

  lastError = undefined
  const logLevel = cfg.remoteServerLogLevel || "info"
  const bind = bindAddressOverride || cfg.remoteServerBindAddress || "127.0.0.1"
  const port = cfg.remoteServerPort || 3210

  // Fastify defaults the body limit to 1MB, which is too small for chat requests that
  // include long conversation histories. Raise it to 50MB to accommodate large payloads
  // (mobile clients send the full message history on each /v1/chat/completions call).
  const fastify = Fastify({ logger: { level: logLevel }, bodyLimit: 50 * 1024 * 1024 })

  // Configure CORS
  const corsOrigins = cfg.remoteServerCorsOrigins || ["*"]
  await fastify.register(cors, {
    // When origin is ["*"] or includes "*", use true to reflect the request origin
    // This is needed because credentials: true doesn't work with literal "*"
    origin: corsOrigins.includes("*") ? true : corsOrigins,
    methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
    allowedHeaders: ["Content-Type", "Authorization", "X-Device-Id", "X-DotAgents-Device-Id"],
    credentials: true,
    maxAge: 86400, // Cache preflight for 24 hours
    preflight: true, // Enable preflight pass-through
    strictPreflight: false, // Don't be strict about preflight requests
  })

  // Auth hook (skip for OPTIONS preflight requests)
  fastify.addHook("onRequest", async (req, reply) => {
    // Skip auth for OPTIONS requests (CORS preflight)
    if (req.method === "OPTIONS") {
      return
    }

    const auth = (req.headers["authorization"] || "").toString()
    const token = auth.startsWith("Bearer ") ? auth.slice(7) : ""
    const current = configStore.get()
    const currentApiKey = getResolvedRemoteServerApiKey(current)
    if (!token || !currentApiKey || token !== currentApiKey) {
      reply.code(401).send({ error: "Unauthorized" })
      return
    }

    const pathname = getOperatorAuditPath(req)
    const trustedDeviceIds = sanitizeStringList(current.remoteServerOperatorAllowDeviceIds ?? [])
    if (trustedDeviceIds.length === 0 || !isProtectedOperatorAccessPath(pathname) || isLoopbackIp(req.ip)) {
      return
    }

    const deviceId = getOperatorAuditDeviceId(req)
    if (!deviceId) {
      recordRejectedOperatorDeviceAttempt(req, "Missing trusted device ID")
      reply.code(403).send({ error: "Trusted device ID required for operator access" })
      return
    }

    if (!trustedDeviceIds.includes(deviceId)) {
      recordRejectedOperatorDeviceAttempt(req, "Device is not allowed for operator access")
      reply.code(403).send({ error: "Device not allowed for operator access" })
      // Must return — falling through here lets the request reach the route
      // handler and perform side effects despite operator access being denied.
      return
    }
  })

  fastify.addHook("onResponse", async (req, reply) => {
    if (req.method !== "POST") {
      return
    }

    const pathname = getOperatorAuditPath(req)
    if (!pathname.startsWith("/v1/operator/")) {
      return
    }

    const context = getOperatorAuditContext(req)
    recordOperatorAuditEvent(req, {
      action: context.action ?? buildOperatorAuditActionFromPath(pathname),
      path: pathname,
      success: context.success ?? reply.statusCode < 400,
      details: context.details,
      failureReason: context.failureReason ?? (reply.statusCode >= 400 ? `http-${reply.statusCode}` : undefined),
    })
  })

  // Routes
  // ============================================
  // Operator/Admin Endpoints
  // ============================================
  fastify.get("/v1/operator/status", async (_req, reply) => {
    try {
      return reply.send(await buildOperatorRuntimeStatus())
    } catch (error) {
      diagnosticsService.logError("remote-server", "Failed to build operator runtime status", error)
      return reply.code(500).send({ error: "Failed to build operator runtime status" })
    }
  })

  fastify.get("/v1/operator/health", async (_req, reply) => {
    try {
      return reply.send(await buildOperatorHealthSnapshot())
    } catch (error) {
      diagnosticsService.logError("remote-server", "Failed to build operator health snapshot", error)
      return reply.code(500).send({ error: "Failed to build operator health snapshot" })
    }
  })

  fastify.get("/v1/operator/diagnostics/report", async (_req, reply) => {
    try {
      const report = await diagnosticsService.generateDiagnosticReport()
      return reply.send(report satisfies OperatorDiagnosticReport)
    } catch (error) {
      diagnosticsService.logError("remote-server", "Failed to build operator diagnostic report", error)
      return reply.code(500).send({ error: "Failed to build operator diagnostic report" })
    }
  })

  fastify.post("/v1/operator/diagnostics/report/save", async (req, reply) => {
    try {
      const body = req.body as { filePath?: unknown } | null
      const filePath = typeof body?.filePath === "string" && body.filePath.trim()
        ? body.filePath.trim()
        : undefined
      const savedPath = await diagnosticsService.saveDiagnosticReport(filePath)
      const response: OperatorDiagnosticReportSaveResponse = {
        success: true,
        action: "diagnostic-report-save",
        message: `Diagnostic report saved to ${savedPath}`,
        filePath: savedPath,
      }
      setOperatorAuditContext(req, { action: response.action, success: true, details: { filePath: savedPath } })
      return reply.send(response)
    } catch (error) {
      setOperatorAuditContext(req, {
        action: "diagnostic-report-save",
        success: false,
        failureReason: "diagnostic-report-save-error",
      })
      diagnosticsService.logError("remote-server", "Failed to save operator diagnostic report", error)
      return reply.code(500).send({ error: "Failed to save operator diagnostic report" })
    }
  })

  fastify.get("/v1/operator/errors", async (req, reply) => {
    try {
      const query = req.query as { count?: string | number }
      return reply.send(buildOperatorRecentErrorsResponse(clampOperatorCount(query.count, 10, 50)))
    } catch (error) {
      diagnosticsService.logError("remote-server", "Failed to build operator recent errors", error)
      return reply.code(500).send({ error: "Failed to build operator recent errors" })
    }
  })

  fastify.post("/v1/operator/errors/clear", async (req, reply) => {
    diagnosticsService.clearErrorLog()
    const response: OperatorActionResponse = {
      success: true,
      action: "operator-errors-clear",
      message: "Operator error log cleared",
    }
    setOperatorAuditContext(req, { action: response.action, success: true })
    return reply.send(response)
  })

  fastify.get("/v1/operator/logs", async (req, reply) => {
    try {
      const query = req.query as { count?: string | number; level?: string }
      const count = clampOperatorCount(query.count, 20, 100)
      const validLevels = new Set(["error", "warning", "info"])
      const level = typeof query.level === "string" && validLevels.has(query.level)
        ? (query.level as "error" | "warning" | "info")
        : undefined
      const allEntries = diagnosticsService.getRecentErrors(count)
      const filtered = level ? allEntries.filter((e) => e.level === level) : allEntries
      return reply.send({
        count: filtered.length,
        level,
        logs: filtered.map(({ timestamp, level: lvl, component, message }) => ({
          timestamp,
          level: lvl,
          component,
          message,
        })),
      })
    } catch (error) {
      diagnosticsService.logError("remote-server", "Failed to build operator logs", error)
      return reply.code(500).send({ error: "Failed to build operator logs" })
    }
  })

  fastify.get("/v1/operator/audit", async (req, reply) => {
    try {
      const query = req.query as { count?: string | number }
      return reply.send(buildOperatorAuditResponse(clampOperatorCount(query.count, 20, 100)))
    } catch (error) {
      diagnosticsService.logError("remote-server", "Failed to build operator audit response", error)
      return reply.code(500).send({ error: "Failed to build operator audit response" })
    }
  })

  fastify.get("/v1/operator/conversations", async (req, reply) => {
    try {
      const query = req.query as { count?: string | number }
      return reply.send(await buildOperatorConversationsResponse(clampOperatorCount(query.count, 10, 50)))
    } catch (error) {
      diagnosticsService.logError("remote-server", "Failed to build operator conversations response", error)
      return reply.code(500).send({ error: "Failed to build operator conversations response" })
    }
  })

  fastify.get("/v1/operator/message-queues", async (_req, reply) => {
    try {
      return reply.send(buildOperatorMessageQueuesResponse())
    } catch (error) {
      diagnosticsService.logError("remote-server", "Failed to build operator message queues response", error)
      return reply.code(500).send({ error: "Failed to build operator message queues response" })
    }
  })

  fastify.post("/v1/operator/message-queues/:conversationId/pause", async (req, reply) => {
    const params = req.params as { conversationId?: string }
    const conversationId = normalizeOperatorQueuePathParam(params.conversationId)
    if (!conversationId) {
      return reply.code(400).send(buildOperatorMessageQueueActionResponse("message-queue-pause", false, "Missing conversation ID"))
    }
    messageQueueService.pauseQueue(conversationId)
    const response = buildOperatorMessageQueueActionResponse(
      "message-queue-pause",
      true,
      "Message queue paused",
      { conversationId },
    )
    setOperatorAuditContext(req, { action: response.action, success: true, details: response.details })
    return reply.send(response)
  })

  fastify.post("/v1/operator/message-queues/:conversationId/resume", async (req, reply) => {
    const params = req.params as { conversationId?: string }
    const conversationId = normalizeOperatorQueuePathParam(params.conversationId)
    if (!conversationId) {
      return reply.code(400).send(buildOperatorMessageQueueActionResponse("message-queue-resume", false, "Missing conversation ID"))
    }
    messageQueueService.resumeQueue(conversationId)
    const response = buildOperatorMessageQueueActionResponse(
      "message-queue-resume",
      true,
      "Message queue resumed",
      { conversationId, processingStarted: false },
    )
    setOperatorAuditContext(req, { action: response.action, success: true, details: response.details })
    return reply.send(response)
  })

  fastify.post("/v1/operator/message-queues/:conversationId/clear", async (req, reply) => {
    const params = req.params as { conversationId?: string }
    const conversationId = normalizeOperatorQueuePathParam(params.conversationId)
    if (!conversationId) {
      return reply.code(400).send(buildOperatorMessageQueueActionResponse("message-queue-clear", false, "Missing conversation ID"))
    }
    const success = messageQueueService.clearQueue(conversationId)
    const response = buildOperatorMessageQueueActionResponse(
      "message-queue-clear",
      success,
      success ? "Message queue cleared" : "Cannot clear a queue while a message is processing",
      { conversationId },
    )
    setOperatorAuditContext(req, { action: response.action, success, details: response.details, failureReason: success ? undefined : response.error })
    return reply.code(success ? 200 : 409).send(response)
  })

  fastify.delete("/v1/operator/message-queues/:conversationId/messages/:messageId", async (req, reply) => {
    const params = req.params as { conversationId?: string; messageId?: string }
    const conversationId = normalizeOperatorQueuePathParam(params.conversationId)
    const messageId = normalizeOperatorQueuePathParam(params.messageId)
    if (!conversationId || !messageId) {
      return reply.code(400).send(buildOperatorMessageQueueActionResponse("message-queue-message-remove", false, "Missing conversation ID or message ID"))
    }
    const success = messageQueueService.removeFromQueue(conversationId, messageId)
    const response = buildOperatorMessageQueueActionResponse(
      "message-queue-message-remove",
      success,
      success ? "Queued message removed" : "Cannot remove this queued message",
      { conversationId, messageId },
    )
    setOperatorAuditContext(req, { action: response.action, success, details: response.details, failureReason: success ? undefined : response.error })
    return reply.code(success ? 200 : 409).send(response)
  })

  fastify.post("/v1/operator/message-queues/:conversationId/messages/:messageId/retry", async (req, reply) => {
    const params = req.params as { conversationId?: string; messageId?: string }
    const conversationId = normalizeOperatorQueuePathParam(params.conversationId)
    const messageId = normalizeOperatorQueuePathParam(params.messageId)
    if (!conversationId || !messageId) {
      return reply.code(400).send(buildOperatorMessageQueueActionResponse("message-queue-message-retry", false, "Missing conversation ID or message ID"))
    }
    const success = messageQueueService.resetToPending(conversationId, messageId)
    const response = buildOperatorMessageQueueActionResponse(
      "message-queue-message-retry",
      success,
      success ? "Queued message marked for retry" : "Cannot retry this queued message",
      { conversationId, messageId, processingStarted: false },
    )
    setOperatorAuditContext(req, { action: response.action, success, details: response.details, failureReason: success ? undefined : response.error })
    return reply.code(success ? 200 : 409).send(response)
  })

  fastify.patch("/v1/operator/message-queues/:conversationId/messages/:messageId", async (req, reply) => {
    const params = req.params as { conversationId?: string; messageId?: string }
    const conversationId = normalizeOperatorQueuePathParam(params.conversationId)
    const messageId = normalizeOperatorQueuePathParam(params.messageId)
    if (!conversationId || !messageId) {
      return reply.code(400).send(buildOperatorMessageQueueActionResponse("message-queue-message-update", false, "Missing conversation ID or message ID"))
    }
    const body = req.body as { text?: unknown } | undefined
    const text = typeof body?.text === "string" ? body.text.trim() : ""
    if (!text) {
      return reply.code(400).send(buildOperatorMessageQueueActionResponse("message-queue-message-update", false, "Queued message text is required"))
    }
    const success = messageQueueService.updateMessageText(conversationId, messageId, text)
    const response = buildOperatorMessageQueueActionResponse(
      "message-queue-message-update",
      success,
      success ? "Queued message updated" : "Cannot update this queued message",
      { conversationId, messageId, processingStarted: false },
    )
    setOperatorAuditContext(req, { action: response.action, success, details: response.details, failureReason: success ? undefined : response.error })
    return reply.code(success ? 200 : 409).send(response)
  })

  fastify.get("/v1/operator/remote-server", async (_req, reply) => {
    return reply.send(buildOperatorRemoteServerStatus())
  })

  fastify.get("/v1/operator/tunnel", async (_req, reply) => {
    return reply.send(buildOperatorTunnelStatus())
  })

  fastify.get("/v1/operator/tunnel/setup", async (_req, reply) => {
    try {
      return reply.send(await buildOperatorTunnelSetupSummary())
    } catch (error) {
      diagnosticsService.logError("remote-server", "Failed to build tunnel setup summary", error)
      return reply.code(500).send({ error: "Failed to build tunnel setup summary" })
    }
  })

  fastify.get("/v1/operator/integrations", async (_req, reply) => {
    try {
      return reply.send(await buildOperatorIntegrationsSummary())
    } catch (error) {
      diagnosticsService.logError("remote-server", "Failed to build operator integrations summary", error)
      return reply.code(500).send({ error: "Failed to build operator integrations summary" })
    }
  })

  fastify.get("/v1/operator/providers/chatgpt-web/auth", async (_req, reply) => {
    try {
      const { getChatGptWebAuthStatus } = await import("./chatgpt-web-provider")
      return reply.send(await getChatGptWebAuthStatus())
    } catch (error) {
      diagnosticsService.logError("remote-server", "Failed to get ChatGPT Web auth status", error)
      return reply.code(500).send({ error: getErrorMessage(error) || "Failed to get ChatGPT Web auth status" })
    }
  })

  fastify.post("/v1/operator/providers/chatgpt-web/auth/login", async (_req, reply) => {
    try {
      const { loginChatGptWebOAuth } = await import("./chatgpt-web-provider")
      const status = await loginChatGptWebOAuth()
      return reply.send({
        success: true,
        status,
        message: "OpenAI Codex connected",
      })
    } catch (error) {
      diagnosticsService.logError("remote-server", "Failed to connect ChatGPT Web OAuth", error)
      try {
        const { getChatGptWebAuthStatus } = await import("./chatgpt-web-provider")
        return reply.code(500).send({
          success: false,
          status: await getChatGptWebAuthStatus(),
          message: "Failed to connect OpenAI Codex",
          error: getErrorMessage(error),
        })
      } catch {
        return reply.code(500).send({ error: getErrorMessage(error) || "Failed to connect OpenAI Codex" })
      }
    }
  })

  fastify.post("/v1/operator/providers/chatgpt-web/auth/logout", async (_req, reply) => {
    try {
      const { getChatGptWebAuthStatus, logoutChatGptWebOAuth } = await import("./chatgpt-web-provider")
      await logoutChatGptWebOAuth()
      return reply.send({
        success: true,
        status: await getChatGptWebAuthStatus(),
        message: "OpenAI Codex disconnected",
      })
    } catch (error) {
      diagnosticsService.logError("remote-server", "Failed to disconnect ChatGPT Web OAuth", error)
      try {
        const { getChatGptWebAuthStatus } = await import("./chatgpt-web-provider")
        return reply.code(500).send({
          success: false,
          status: await getChatGptWebAuthStatus(),
          message: "Failed to disconnect OpenAI Codex",
          error: getErrorMessage(error),
        })
      } catch {
        return reply.code(500).send({ error: getErrorMessage(error) || "Failed to disconnect OpenAI Codex" })
      }
    }
  })

  fastify.get("/v1/operator/updater", async (_req, reply) => {
    return reply.send(buildOperatorUpdaterStatus())
  })

  fastify.post("/v1/operator/updater/check", async (req, reply) => {
    const result = await checkForUpdatesAndDownload()
    const updateInfo = result.updateInfo as {
      error?: string
      updateAvailable?: boolean
      latestRelease?: { tagName?: string; url?: string }
      lastCheckedAt?: number
      currentVersion?: string
    } | null

    const response: OperatorActionResponse = updateInfo?.error
      ? {
        success: false,
        action: "updater-check",
        message: `Update check failed: ${updateInfo.error}`,
        error: updateInfo.error,
        details: {
          currentVersion: updateInfo.currentVersion,
          checkedAt: updateInfo.lastCheckedAt,
          releaseUrl: MANUAL_RELEASES_URL,
        },
      }
      : {
        success: true,
        action: "updater-check",
        message: updateInfo?.updateAvailable
          ? `Update available: ${updateInfo.latestRelease?.tagName || "latest release found"}`
          : "No newer release found.",
        details: {
          currentVersion: updateInfo?.currentVersion,
          checkedAt: updateInfo?.lastCheckedAt,
          updateAvailable: updateInfo?.updateAvailable ?? false,
          latestReleaseTag: updateInfo?.latestRelease?.tagName,
          releaseUrl: updateInfo?.latestRelease?.url || MANUAL_RELEASES_URL,
        },
      }

    // Audit is written once by the shared onResponse hook via the context below;
    // setOperatorAuditContext avoids duplicating entries for every POST /v1/operator/*.
    setOperatorAuditContext(req, {
      action: response.action,
      success: response.success,
      details: response.details,
      ...(response.error ? { failureReason: response.error } : {}),
    })
    return reply.send(response)
  })

  fastify.post("/v1/operator/updater/download-latest", async (req, reply) => {
    try {
      const result = await downloadLatestReleaseAsset()
      const response: OperatorActionResponse = {
        success: true,
        action: "updater-download-latest",
        message: `Downloaded ${result.downloadedAsset.name} to ${result.downloadedAsset.filePath}`,
        details: {
          fileName: result.downloadedAsset.name,
          filePath: result.downloadedAsset.filePath,
          downloadedAt: result.downloadedAsset.downloadedAt,
          releaseTag: result.updateInfo.latestRelease?.tagName,
        },
      }
      setOperatorAuditContext(req, {
        action: response.action,
        success: response.success,
        details: response.details,
      })
      return reply.send(response)
    } catch (error) {
      const message = error instanceof Error ? error.message : String(error)
      const response: OperatorActionResponse = {
        success: false,
        action: "updater-download-latest",
        message,
        error: message,
      }
      setOperatorAuditContext(req, {
        action: response.action,
        success: response.success,
        failureReason: message,
      })
      return reply.send(response)
    }
  })

  fastify.post("/v1/operator/updater/reveal-download", async (req, reply) => {
    try {
      const downloadedAsset = await revealDownloadedReleaseAsset()
      const response: OperatorActionResponse = {
        success: true,
        action: "updater-reveal-download",
        message: `Revealed ${downloadedAsset.name} in the desktop file manager.`,
        details: {
          fileName: downloadedAsset.name,
          filePath: downloadedAsset.filePath,
        },
      }
      setOperatorAuditContext(req, {
        action: response.action,
        success: response.success,
        details: response.details,
      })
      return reply.send(response)
    } catch (error) {
      const message = error instanceof Error ? error.message : String(error)
      const response: OperatorActionResponse = {
        success: false,
        action: "updater-reveal-download",
        message,
        error: message,
      }
      setOperatorAuditContext(req, {
        action: response.action,
        success: response.success,
        failureReason: message,
      })
      return reply.send(response)
    }
  })

  fastify.post("/v1/operator/updater/open-download", async (req, reply) => {
    try {
      const downloadedAsset = await openDownloadedReleaseAsset()
      const response: OperatorActionResponse = {
        success: true,
        action: "updater-open-download",
        message: `Opened ${downloadedAsset.name} on the desktop machine.`,
        details: {
          fileName: downloadedAsset.name,
          filePath: downloadedAsset.filePath,
        },
      }
      setOperatorAuditContext(req, {
        action: response.action,
        success: response.success,
        details: response.details,
      })
      return reply.send(response)
    } catch (error) {
      const message = error instanceof Error ? error.message : String(error)
      const response: OperatorActionResponse = {
        success: false,
        action: "updater-open-download",
        message,
        error: message,
      }
      setOperatorAuditContext(req, {
        action: response.action,
        success: response.success,
        failureReason: message,
      })
      return reply.send(response)
    }
  })

  fastify.post("/v1/operator/updater/open-releases", async (req, reply) => {
    try {
      const result = await openManualReleasesPage()
      const response: OperatorActionResponse = {
        success: true,
        action: "updater-open-releases",
        message: `Opened releases page: ${result.url}`,
        details: {
          url: result.url,
        },
      }
      setOperatorAuditContext(req, {
        action: response.action,
        success: response.success,
        details: response.details,
      })
      return reply.send(response)
    } catch (error) {
      const message = error instanceof Error ? error.message : String(error)
      const response: OperatorActionResponse = {
        success: false,
        action: "updater-open-releases",
        message,
        error: message,
      }
      setOperatorAuditContext(req, {
        action: response.action,
        success: response.success,
        failureReason: message,
      })
      return reply.send(response)
    }
  })

  fastify.get("/v1/operator/discord", async (_req, reply) => {
    return reply.send(buildOperatorDiscordIntegrationSummary())
  })

  fastify.get("/v1/operator/discord/logs", async (req, reply) => {
    const query = req.query as { count?: string | number }
    return reply.send(buildOperatorDiscordLogsResponse(clampOperatorCount(query.count, 20, 50)))
  })

  fastify.post("/v1/operator/discord/connect", async (req, reply) => {
    const result = await discordService.start()
    const response: OperatorActionResponse = result.success
      ? {
        success: true,
        action: "discord-connect",
        message: "Discord connection started",
        details: {
          connected: discordService.getStatus().connected,
        },
      }
      : {
        success: false,
        action: "discord-connect",
        message: result.error || "Failed to start Discord integration",
        error: result.error || "Failed to start Discord integration",
      }

    setOperatorAuditContext(req, {
      action: response.action,
      success: response.success,
      details: response.details,
      failureReason: response.success ? undefined : response.error || response.message,
    })
    return reply.send(response)
  })

  fastify.post("/v1/operator/discord/disconnect", async (req, reply) => {
    const result = await discordService.stop()
    const response: OperatorActionResponse = result.success
      ? {
        success: true,
        action: "discord-disconnect",
        message: "Discord integration stopped",
      }
      : {
        success: false,
        action: "discord-disconnect",
        message: result.error || "Failed to stop Discord integration",
        error: result.error || "Failed to stop Discord integration",
      }

    setOperatorAuditContext(req, {
      action: response.action,
      success: response.success,
      failureReason: response.success ? undefined : response.error || response.message,
    })
    return reply.send(response)
  })

  fastify.post("/v1/operator/discord/logs/clear", async (req, reply) => {
    discordService.clearLogs()
    setOperatorAuditContext(req, {
      action: "discord-clear-logs",
      success: true,
    })
    return reply.send({
      success: true,
      action: "discord-clear-logs",
      message: "Discord logs cleared",
    } satisfies OperatorActionResponse)
  })

  fastify.get("/v1/operator/whatsapp", async (_req, reply) => {
    try {
      return reply.send(await buildOperatorWhatsAppIntegrationSummary())
    } catch (error) {
      diagnosticsService.logError("remote-server", "Failed to build WhatsApp operator summary", error)
      return reply.code(500).send({ error: "Failed to build WhatsApp operator summary" })
    }
  })

  fastify.post("/v1/operator/whatsapp/connect", async (req, reply) => {
    const response = await runOperatorWhatsAppAction("whatsapp_connect", "whatsapp-connect", "WhatsApp connection initiated")
    setOperatorAuditContext(req, {
      action: response.action,
      success: response.success,
      details: response.details,
      failureReason: response.success ? undefined : response.error || response.message,
    })
    return reply.send(response)
  })

  fastify.post("/v1/operator/whatsapp/logout", async (req, reply) => {
    const response = await runOperatorWhatsAppAction("whatsapp_logout", "whatsapp-logout", "WhatsApp logout completed")
    setOperatorAuditContext(req, {
      action: response.action,
      success: response.success,
      details: response.details,
      failureReason: response.success ? undefined : response.error || response.message,
    })
    return reply.send(response)
  })

  fastify.post("/v1/operator/tunnel/start", async (req, reply) => {
    try {
      const remoteServerStatus = getRemoteServerStatus()
      if (!remoteServerStatus.running) {
        setOperatorAuditContext(req, {
          action: "tunnel-start",
          success: false,
          failureReason: "remote-server-not-running",
        })
        return reply.send({
          success: false,
          action: "tunnel-start",
          message: "Remote server must be running before a tunnel can be started",
          error: "Remote server is not running",
        } satisfies OperatorActionResponse)
      }

      const result = await startConfiguredCloudflareTunnel()
      const response: OperatorActionResponse = result.success
        ? {
          success: true,
          action: "tunnel-start",
          message: result.mode === "named" ? "Cloudflare named tunnel started" : "Cloudflare quick tunnel started",
          details: {
            mode: result.mode,
            ...(result.url ? { url: result.url } : {}),
          },
        }
        : {
          success: false,
          action: "tunnel-start",
          message: result.error || "Failed to start Cloudflare tunnel",
          error: result.error || "Failed to start Cloudflare tunnel",
          details: {
            mode: result.mode,
          },
        }

      setOperatorAuditContext(req, {
        action: response.action,
        success: response.success,
        details: response.details,
        failureReason: response.success ? undefined : response.error || response.message,
      })
      return reply.send(response)
    } catch (error) {
      setOperatorAuditContext(req, {
        action: "tunnel-start",
        success: false,
        failureReason: "tunnel-start-route-error",
      })
      diagnosticsService.logError("remote-server", "Failed to start tunnel from operator route", error)
      return reply.code(500).send({ error: "Failed to start tunnel" })
    }
  })

  fastify.post("/v1/operator/tunnel/stop", async (req, reply) => {
    try {
      await stopCloudflareTunnel()
      setOperatorAuditContext(req, {
        action: "tunnel-stop",
        success: true,
      })
      return reply.send({
        success: true,
        action: "tunnel-stop",
        message: "Cloudflare tunnel stopped",
      } satisfies OperatorActionResponse)
    } catch (error) {
      setOperatorAuditContext(req, {
        action: "tunnel-stop",
        success: false,
        failureReason: "tunnel-stop-route-error",
      })
      diagnosticsService.logError("remote-server", "Failed to stop tunnel from operator route", error)
      return reply.code(500).send({ error: "Failed to stop tunnel" })
    }
  })

  fastify.post("/v1/operator/actions/restart-remote-server", async (req, reply) => {
    const response: OperatorActionResponse = {
      success: true,
      action: "restart-remote-server",
      message: "Remote server restart scheduled",
      scheduled: true,
      details: {
        wasRunning: getRemoteServerStatus().running,
      },
    }

    setOperatorAuditContext(req, {
      action: response.action,
      success: true,
      details: response.details,
    })
    scheduleRemoteServerRestartFromOperator()
    return reply.send(response)
  })

  fastify.post("/v1/operator/actions/restart-app", async (req, reply) => {
    const response: OperatorActionResponse = {
      success: true,
      action: "restart-app",
      message: "Application restart scheduled",
      scheduled: true,
      details: {
        currentVersion: app.getVersion(),
      },
    }

    setOperatorAuditContext(req, {
      action: response.action,
      success: true,
      details: response.details,
    })
    scheduleAppRestartFromOperator()
    return reply.send(response)
  })

  fastify.post("/v1/operator/actions/stop-tts", async (req, reply) => {
    let notifiedWindows = 0
    for (const windowId of ["main", "panel"] as const) {
      try {
        getWindowRendererHandlers(windowId)?.stopAllTts?.send()
        notifiedWindows += 1
      } catch (error) {
        diagnosticsService.logError("remote-server", `Failed to stop TTS in ${windowId} window`, error)
      }
    }

    const response: OperatorActionResponse = {
      success: true,
      action: "stop-tts",
      message: "Desktop speech playback stop requested",
      details: { notifiedWindows },
    }
    setOperatorAuditContext(req, { action: response.action, success: true, details: response.details })
    return reply.send(response)
  })

  fastify.post("/v1/operator/windows/main/show", async (req, reply) => {
    const body = req.body as { route?: unknown } | null
    const route = typeof body?.route === "string" && ["/", "/settings", "/settings/models", "/sessions", "/plugins"].includes(body.route)
      ? body.route
      : undefined
    showMainWindow(route)
    const response: OperatorActionResponse = {
      success: true,
      action: "desktop-main-window-show",
      message: "Main window show requested",
      details: route ? { route } : undefined,
    }
    setOperatorAuditContext(req, { action: response.action, success: true, details: response.details })
    return reply.send(response)
  })

  fastify.post("/v1/operator/windows/panel/show", async (req, reply) => {
    showPanelWindow({})
    const response: OperatorActionResponse = {
      success: true,
      action: "desktop-panel-window-show",
      message: "Panel window show requested",
    }
    setOperatorAuditContext(req, { action: response.action, success: true })
    return reply.send(response)
  })

  fastify.post("/v1/operator/windows/panel/hide", async (req, reply) => {
    hideFloatingPanelWindow()
    const response: OperatorActionResponse = {
      success: true,
      action: "desktop-panel-window-hide",
      message: "Panel window hide requested",
    }
    setOperatorAuditContext(req, { action: response.action, success: true })
    return reply.send(response)
  })

  fastify.post("/v1/operator/windows/panel/reset", async (req, reply) => {
    resetFloatingPanelPositionAndSize(true)
    const response: OperatorActionResponse = {
      success: true,
      action: "desktop-panel-window-reset",
      message: "Panel position and size reset",
    }
    setOperatorAuditContext(req, { action: response.action, success: true })
    return reply.send(response)
  })

  fastify.post("/v1/operator/sessions/:sessionId/show", async (req, reply) => {
    const { sessionId } = req.params as { sessionId?: string }
    if (!sessionId) return reply.code(400).send({ error: "Missing session ID" })

    setTrackedAgentSessionSnoozed(sessionId, false)
    try {
      getWindowRendererHandlers("panel")?.focusAgentSession?.send(sessionId)
    } catch (error) {
      diagnosticsService.logError("remote-server", `Failed to focus agent session ${sessionId} in panel`, error)
    }
    setPanelMode("agent")
    showPanelWindow({})

    const response: OperatorActionResponse = {
      success: true,
      action: "agent-session-show",
      message: "Agent session shown in the desktop panel",
      details: { sessionId },
    }
    setOperatorAuditContext(req, { action: response.action, success: true, details: response.details })
    return reply.send(response)
  })

  fastify.post("/v1/operator/sessions/:sessionId/snooze", async (req, reply) => {
    const { sessionId } = req.params as { sessionId?: string }
    if (!sessionId) return reply.code(400).send({ error: "Missing session ID" })
    setTrackedAgentSessionSnoozed(sessionId, true)
    const response: OperatorActionResponse = {
      success: true,
      action: "agent-session-snooze",
      message: "Agent session snoozed",
      details: { sessionId },
    }
    setOperatorAuditContext(req, { action: response.action, success: true, details: response.details })
    return reply.send(response)
  })

  fastify.post("/v1/operator/sessions/:sessionId/unsnooze", async (req, reply) => {
    const { sessionId } = req.params as { sessionId?: string }
    if (!sessionId) return reply.code(400).send({ error: "Missing session ID" })
    setTrackedAgentSessionSnoozed(sessionId, false)
    const response: OperatorActionResponse = {
      success: true,
      action: "agent-session-unsnooze",
      message: "Agent session unsnoozed",
      details: { sessionId },
    }
    setOperatorAuditContext(req, { action: response.action, success: true, details: response.details })
    return reply.send(response)
  })

  fastify.post("/v1/operator/sessions/:sessionId/clear", async (req, reply) => {
    const { sessionId } = req.params as { sessionId?: string }
    if (!sessionId) return reply.code(400).send({ error: "Missing session ID" })
    const removed = agentSessionTracker.removeCompletedSession(sessionId)
    try {
      getWindowRendererHandlers("main")?.clearAgentSessionProgress?.send(sessionId)
      getWindowRendererHandlers("panel")?.clearAgentSessionProgress?.send(sessionId)
    } catch (error) {
      diagnosticsService.logError("remote-server", `Failed to notify renderer after clearing agent session ${sessionId}`, error)
    }
    const response: OperatorActionResponse = {
      success: true,
      action: "agent-session-clear",
      message: removed ? "Agent session dismissed" : "Agent session was not in recent sessions",
      details: { sessionId, removed },
    }
    setOperatorAuditContext(req, { action: response.action, success: true, details: response.details })
    return reply.send(response)
  })

  fastify.post("/v1/operator/sessions/clear-inactive", async (req, reply) => {
    const clearedCount = clearInactiveOperatorSessions()
    const response: OperatorActionResponse = {
      success: true,
      action: "agent-sessions-clear-inactive",
      message: `${clearedCount} inactive agent session${clearedCount === 1 ? "" : "s"} cleared`,
      details: { clearedCount },
    }
    setOperatorAuditContext(req, { action: response.action, success: true, details: response.details })
    return reply.send(response)
  })

  fastify.post("/v1/operator/sessions/snooze-and-hide-panel", async (req, reply) => {
    const body = req.body as { sessionIds?: unknown } | null
    const sessionIds = Array.isArray(body?.sessionIds)
      ? body.sessionIds.filter((value): value is string => typeof value === "string" && value.length > 0)
      : undefined
    const snoozedSessionIds = snoozeAgentSessionsAndHidePanelWindow(sessionIds)
    const response: OperatorActionResponse = {
      success: true,
      action: "agent-sessions-snooze-hide-panel",
      message: `${snoozedSessionIds.length} active agent session${snoozedSessionIds.length === 1 ? "" : "s"} snoozed`,
      details: { sessionIds: snoozedSessionIds },
    }
    setOperatorAuditContext(req, { action: response.action, success: true, details: response.details })
    return reply.send(response)
  })

  fastify.post("/v1/operator/actions/run-agent", async (req, reply) => {
    try {
      const body = req.body as { prompt?: string; conversationId?: string; profileId?: string } | null
      const prompt = typeof body?.prompt === "string" ? body.prompt.trim() : ""
      if (!prompt) {
        return reply.code(400).send({ error: "Missing prompt" })
      }

      setOperatorAuditContext(req, {
        action: "run-agent",
        success: true,
        details: { promptLength: prompt.length, conversationId: body?.conversationId },
      })

      diagnosticsService.logInfo("remote-server", `Operator run-agent: ${prompt.length} chars${body?.conversationId ? ` (conversation ${body.conversationId})` : ""}`)

      const result = await runAgent({
        prompt,
        conversationId: body?.conversationId,
        profileId: body?.profileId,
      })

      return reply.send({
        success: true,
        action: "run-agent",
        conversationId: result.conversationId,
        content: result.content,
        messageCount: result.conversationHistory.length,
      })
    } catch (error) {
      setOperatorAuditContext(req, {
        action: "run-agent",
        success: false,
        failureReason: "run-agent-error",
      })
      const errorMessage = getErrorMessage(error)
      diagnosticsService.logError("remote-server", `Operator run-agent failed: ${errorMessage}`, error)
      return reply.code(500).send({ error: `Agent execution failed: ${errorMessage}` })
    }
  })

  fastify.post("/v1/operator/access/rotate-api-key", async (req, reply) => {
    try {
      const cfg = configStore.get()
      const apiKey = generateRemoteServerApiKey()
      configStore.save({ ...cfg, remoteServerApiKey: apiKey })

      const response: OperatorApiKeyRotationResponse = {
        success: true,
        action: "rotate-api-key",
        message: "Remote server API key rotated",
        scheduled: true,
        restartScheduled: true,
        apiKey,
      }

      setOperatorAuditContext(req, {
        action: response.action,
        success: true,
        details: {
          restartScheduled: true,
        },
      })

      scheduleTaskAfterReply(reply, "restart remote server after API key rotation", async () => {
        await restartRemoteServer()
      })

      return reply.send(response)
    } catch (error) {
      setOperatorAuditContext(req, {
        action: "rotate-api-key",
        success: false,
        failureReason: "rotate-api-key-route-error",
      })
      diagnosticsService.logError("remote-server", "Failed to rotate remote server API key", error)
      return reply.code(500).send({ error: "Failed to rotate remote server API key" })
    }
  })

  fastify.post("/v1/chat/completions", async (req, reply) => {
    try {
      const body = req.body as any
      const prompt = extractUserPrompt(body)
      if (!prompt) {
        return reply.code(400).send({ error: "Missing user prompt" })
      }

      // Extract conversationId from request body (custom extension to OpenAI API)
      // Use undefined for absent/non-string values; treat empty string as absent
      const rawConversationId = typeof body.conversation_id === "string" ? body.conversation_id : undefined
      const conversationId = rawConversationId !== "" ? rawConversationId : undefined
      const rawProfileId = typeof body.profile_id === "string" ? body.profile_id : undefined
      const profileId = rawProfileId !== "" ? rawProfileId : undefined
      if (conversationId) {
        const conversationIdError = getConversationIdValidationError(conversationId)
        if (conversationIdError) {
          return reply.code(400).send({ error: conversationIdError })
        }
      }
      // Check if client wants SSE streaming
      const isStreaming = body.stream === true

      console.log("[remote-server] Chat request:", { conversationId: conversationId || "new", promptLength: prompt.length, streaming: isStreaming })
      diagnosticsService.logInfo("remote-server", `Handling completion request${conversationId ? ` for conversation ${conversationId}` : ""}${isStreaming ? " (streaming)" : ""}`)

      if (isStreaming) {
        // SSE streaming mode
        // Get the request origin for CORS
        const requestOrigin = req.headers.origin || "*"
        reply.raw.writeHead(200, {
          "Content-Type": "text/event-stream",
          "Cache-Control": "no-cache",
          "Connection": "keep-alive",
          "Access-Control-Allow-Origin": requestOrigin,
          "Access-Control-Allow-Credentials": "true",
        })

        // Helper to write SSE events
        const writeSSE = (data: object) => {
          reply.raw.write(`data: ${JSON.stringify(data)}\n\n`)
        }

        // Create progress callback that emits SSE events
        const onProgress = (update: AgentProgressUpdate) => {
          writeSSE({ type: "progress", data: update })
        }

        try {
          const result = await runAgent({ prompt, conversationId, profileId, onProgress })

          // Record as if user submitted a text input
          recordHistory(result.content)

          const model = resolveActiveModelId(configStore.get())

          // Send final "done" event with full response data
          writeSSE({
            type: "done",
            data: {
              content: result.content,
              conversation_id: result.conversationId,
              conversation_history: result.conversationHistory,
              model,
            },
          })

          // Send push notification by default if tokens are registered
          // Client can set send_push_notification: false to explicitly disable
          const shouldSendPush = body.send_push_notification !== false && isPushEnabled()
          if (shouldSendPush) {
            const conversationTitle = prompt.length > 30 ? prompt.substring(0, 30) + "..." : prompt
            // Fire and forget - don't block the response
            sendMessageNotification(result.conversationId, conversationTitle, result.content).catch((err) => {
              diagnosticsService.logWarning("remote-server", "Failed to send push notification", err)
            })
          }
        } catch (error: any) {
          // Send error event
          writeSSE({
            type: "error",
            data: { message: error?.message || "Internal Server Error" },
          })
        } finally {
          reply.raw.end()
        }

        // Return reply to indicate we've handled the response
        return reply
      }

      // Non-streaming mode (existing behavior)
      const result = await runAgent({ prompt, conversationId, profileId })

      // Record as if user submitted a text input
      recordHistory(result.content)

      const model = resolveActiveModelId(configStore.get())
      // Return standard OpenAI response with conversation_id as custom field
      const response = toOpenAIChatResponse(result.content, model)

      console.log("[remote-server] Chat response:", { conversationId: result.conversationId, responseLength: result.content.length })

      // Send push notification by default if tokens are registered
      // Client can set send_push_notification: false to explicitly disable
      const shouldSendPush = body.send_push_notification !== false && isPushEnabled()
      if (shouldSendPush) {
        const conversationTitle = prompt.length > 30 ? prompt.substring(0, 30) + "..." : prompt
        // Fire and forget - don't block the response
        sendMessageNotification(result.conversationId, conversationTitle, result.content).catch((err) => {
          diagnosticsService.logWarning("remote-server", "Failed to send push notification", err)
        })
      }

      return reply.send({
        ...response,
        conversation_id: result.conversationId, // Include conversation_id for client to use in follow-ups
        conversation_history: result.conversationHistory, // Include full conversation history with tool calls/results
      })
    } catch (error: any) {
      diagnosticsService.logError("remote-server", "Handler error", error)
      return reply.code(500).send({ error: "Internal Server Error" })
    }
  })

  fastify.get("/v1/models", async (_req, reply) => {
    const model = resolveActiveModelId(configStore.get())
    return reply.send({
      object: "list",
      data: [{ id: model, object: "model", owned_by: "system" }],
    })
  })

  // GET /v1/models/:providerId - Fetch available models for a provider
  fastify.get("/v1/models/:providerId", async (req, reply) => {
    try {
      const params = req.params as { providerId: string }
      const providerId = params.providerId

      const validProviders = ["openai", "groq", "gemini", "chatgpt-web"]
      if (!validProviders.includes(providerId)) {
        return reply.code(400).send({ error: `Invalid provider: ${providerId}. Valid providers: ${validProviders.join(", ")}` })
      }

      const { fetchAvailableModels } = await import("./models-service")
      const models = await fetchAvailableModels(providerId)

      return reply.send({
        providerId,
        models: models.map(m => ({
          id: m.id,
          name: m.name,
          description: m.description,
          context_length: m.context_length,
        })),
      })
    } catch (error: any) {
      diagnosticsService.logError("remote-server", "Failed to fetch models", error)
      return reply.code(500).send({ error: error?.message || "Failed to fetch models" })
    }
  })

  // ============================================
  // Settings Management Endpoints (for mobile app)
  // ============================================

  // GET /v1/profiles - List all profiles
  fastify.get("/v1/profiles", async (_req, reply) => {
    try {
      const profiles = agentProfileService.getUserProfiles()
      const currentProfile = agentProfileService.getCurrentProfile()
      return reply.send({
        profiles: profiles.map(p => ({
          id: p.id,
          name: p.displayName,
          isDefault: p.isDefault,
          createdAt: p.createdAt,
          updatedAt: p.updatedAt,
        })),
        currentProfileId: currentProfile?.id,
      })
    } catch (error: any) {
      diagnosticsService.logError("remote-server", "Failed to get profiles", error)
      return reply.code(500).send({ error: "Failed to get profiles" })
    }
  })

  // GET /v1/profiles/current - Get current profile details
  fastify.get("/v1/profiles/current", async (_req, reply) => {
    try {
      const profile = agentProfileService.getCurrentProfile()
      if (!profile) {
        return reply.code(404).send({ error: "No current profile set" })
      }
      return reply.send({
        id: profile.id,
        name: profile.displayName,
        isDefault: profile.isDefault,
        guidelines: profile.guidelines || "",
        systemPrompt: profile.systemPrompt,
        createdAt: profile.createdAt,
        updatedAt: profile.updatedAt,
      })
    } catch (error: any) {
      diagnosticsService.logError("remote-server", "Failed to get current profile", error)
      return reply.code(500).send({ error: "Failed to get current profile" })
    }
  })

  // POST /v1/profiles/current - Set current profile
  fastify.post("/v1/profiles/current", async (req, reply) => {
    try {
      const body = req.body as any
      const profileId = body?.profileId
      if (!profileId || typeof profileId !== "string") {
        return reply.code(400).send({ error: "Missing or invalid profileId" })
      }
      const profile = agentProfileService.setCurrentProfileStrict(profileId)
      // Apply the profile's MCP configuration
      const mcpServerConfig = toolConfigToMcpServerConfig(profile.toolConfig)
      mcpService.applyProfileMcpConfig(
        mcpServerConfig?.disabledServers,
        mcpServerConfig?.disabledTools,
        mcpServerConfig?.allServersDisabledByDefault,
        mcpServerConfig?.enabledServers,
          mcpServerConfig?.enabledRuntimeTools,
      )
      diagnosticsService.logInfo("remote-server", `Switched to profile: ${profile.displayName}`)
      return reply.send({
        success: true,
        profile: {
          id: profile.id,
          name: profile.name,
          isDefault: profile.isDefault,
        },
      })
    } catch (error: any) {
      diagnosticsService.logError("remote-server", "Failed to set current profile", error)
      // Return 404 if profile was not found, otherwise 500
      const isNotFound = error?.message?.includes("not found")
      return reply.code(isNotFound ? 404 : 500).send({ error: error?.message || "Failed to set current profile" })
    }
  })

  // GET /v1/profiles/:id/export - Export a profile as JSON
  fastify.get("/v1/profiles/:id/export", async (req, reply) => {
    try {
      const params = req.params as { id: string }
      const profileJson = agentProfileService.exportProfile(params.id)
      return reply.send({ profileJson })
    } catch (error: any) {
      diagnosticsService.logError("remote-server", "Failed to export profile", error)
      const isNotFound = error?.message?.includes("not found")
      return reply.code(isNotFound ? 404 : 500).send({ error: error?.message || "Failed to export profile" })
    }
  })

  // POST /v1/profiles/import - Import a profile from JSON
  fastify.post("/v1/profiles/import", async (req, reply) => {
    try {
      const body = req.body as any
      const profileJson = body?.profileJson
      if (!profileJson || typeof profileJson !== "string") {
        return reply.code(400).send({ error: "Missing or invalid profileJson" })
      }
      const profile = agentProfileService.importProfile(profileJson)
      diagnosticsService.logInfo("remote-server", `Imported profile: ${profile.displayName}`)
      return reply.send({
        success: true,
        profile: {
          id: profile.id,
          name: profile.displayName,
          isDefault: profile.isDefault,
        },
      })
    } catch (error: any) {
      diagnosticsService.logError("remote-server", "Failed to import profile", error)
      // Return 400 for JSON/validation errors, 500 for server errors
      const errorMessage = (error?.message ?? "").toLowerCase()
      const isValidationError = error instanceof SyntaxError ||
        errorMessage.includes("json") ||
        errorMessage.includes("invalid") ||
        errorMessage.includes("missing")
      return reply.code(isValidationError ? 400 : 500).send({ error: error?.message || "Failed to import profile" })
    }
  })

  // ============================================
  // Bundle Management Endpoints (for mobile app)
  // ============================================

  const BUNDLE_COMPONENT_KEYS = ["agentProfiles", "mcpServers", "skills", "repeatTasks", "knowledgeNotes"] as const
  type BundleComponentKey = typeof BUNDLE_COMPONENT_KEYS[number]

  const normalizeBundleComponents = (raw: unknown): Partial<Record<BundleComponentKey, boolean>> | undefined => {
    if (!raw || typeof raw !== "object" || Array.isArray(raw)) return undefined
    const record = raw as Record<string, unknown>
    const components: Partial<Record<BundleComponentKey, boolean>> = {}
    for (const key of BUNDLE_COMPONENT_KEYS) {
      if (record[key] !== undefined) {
        components[key] = record[key] === true
      }
    }
    return components
  }

  const getBundleAgentsDirsForMobile = async () => {
    const { globalAgentsFolder, resolveWorkspaceAgentsFolder } = await import("./config")
    const workspaceAgentsFolder = resolveWorkspaceAgentsFolder()
    return {
      globalAgentsFolder,
      targetAgentsFolder: workspaceAgentsFolder || globalAgentsFolder,
      agentsDirs: workspaceAgentsFolder ? [globalAgentsFolder, workspaceAgentsFolder] : [globalAgentsFolder],
    }
  }

  const writeTemporaryBundleFileForMobile = (bundleJson: string) => {
    const dir = fs.mkdtempSync(path.join(os.tmpdir(), "dotagents-mobile-bundle-"))
    const filePath = path.join(dir, `bundle-${crypto.randomUUID()}.dotagents`)
    fs.writeFileSync(filePath, bundleJson, "utf8")
    return { dir, filePath }
  }

  const cleanupTemporaryBundleFileForMobile = (dir: string) => {
    try {
      fs.rmSync(dir, { recursive: true, force: true })
    } catch {
      // best-effort temp cleanup
    }
  }

  const refreshRuntimeAfterMobileBundleImport = async () => {
    try {
      configStore.reload()
    } catch (error) {
      diagnosticsService.logError("remote-server", "Failed to reload config after mobile bundle import operation", error)
    }

    try {
      agentProfileService.reload()
    } catch (error) {
      diagnosticsService.logError("remote-server", "Failed to reload agent profiles after mobile bundle import operation", error)
    }

    try {
      skillsService.scanSkillsFolder()
    } catch (error) {
      diagnosticsService.logError("remote-server", "Failed to reload skills after mobile bundle import operation", error)
    }

    try {
      const { loopService } = await import("./loop-service")
      loopService.stopAllLoops()
      loopService.reload()
      loopService.resumeScheduling()
      loopService.startAllLoops()
    } catch (error) {
      diagnosticsService.logError("remote-server", "Failed to reload repeat tasks after mobile bundle import operation", error)
    }

    try {
      await knowledgeNotesService.reload()
    } catch (error) {
      diagnosticsService.logError("remote-server", "Failed to reload knowledge notes after mobile bundle import operation", error)
    }

    try {
      const config = configStore.get()
      const configuredServers = config.mcpConfig?.mcpServers || {}
      const serverStatusBeforeRefresh = mcpService.getServerStatus()

      for (const serverName of Object.keys(serverStatusBeforeRefresh)) {
        const serverConfig = configuredServers[serverName]
        const shouldBeStopped = !serverConfig || !!(serverConfig as MCPServerConfig).disabled
        if (!shouldBeStopped) continue

        const stopResult = await mcpService.stopServer(serverName)
        if (!stopResult.success) {
          diagnosticsService.logError("remote-server", `Failed to stop MCP server ${serverName} after mobile bundle import`, stopResult.error)
        }
      }

      const serverStatusAfterStops = mcpService.getServerStatus()
      for (const [serverName, serverConfig] of Object.entries(configuredServers)) {
        if ((serverConfig as MCPServerConfig).disabled) continue
        const status = serverStatusAfterStops[serverName]
        if (status?.runtimeEnabled === false || !status?.connected) continue

        const restartResult = await mcpService.restartServer(serverName)
        if (!restartResult.success) {
          diagnosticsService.logError("remote-server", `Failed to restart MCP server ${serverName} after mobile bundle import`, restartResult.error)
        }
      }

      await mcpService.initialize()
    } catch (error) {
      diagnosticsService.logError("remote-server", "Failed to reinitialize MCP after mobile bundle import operation", error)
    }
  }

  // POST /v1/bundles/export - Export a DotAgents bundle as JSON for mobile sharing
  fastify.post("/v1/bundles/export", async (req, reply) => {
    try {
      const body = (req.body ?? {}) as { name?: unknown; description?: unknown; components?: unknown }
      const { agentsDirs } = await getBundleAgentsDirsForMobile()
      const { exportBundle, exportBundleFromLayers } = await import("./bundle-service")
      const options = {
        name: typeof body.name === "string" && body.name.trim() ? body.name.trim() : "DotAgents Bundle",
        description: typeof body.description === "string" && body.description.trim() ? body.description.trim() : undefined,
        components: normalizeBundleComponents(body.components),
      }
      const bundle = agentsDirs.length > 1
        ? await exportBundleFromLayers(agentsDirs, options)
        : await exportBundle(agentsDirs[0], options)
      return reply.send({
        success: true,
        bundle,
        bundleJson: JSON.stringify(bundle, null, 2),
      })
    } catch (error: any) {
      diagnosticsService.logError("remote-server", "Failed to export bundle", error)
      return reply.code(500).send({ error: error?.message || "Failed to export bundle" })
    }
  })

  // POST /v1/bundles/import/preview - Preview a pasted DotAgents bundle
  fastify.post("/v1/bundles/import/preview", async (req, reply) => {
    let temp: { dir: string; filePath: string } | null = null
    try {
      const body = req.body as { bundleJson?: unknown }
      const bundleJson = typeof body.bundleJson === "string" ? body.bundleJson.trim() : ""
      if (!bundleJson) {
        return reply.code(400).send({ error: "bundleJson must be a non-empty string" })
      }

      JSON.parse(bundleJson)
      temp = writeTemporaryBundleFileForMobile(bundleJson)
      const { targetAgentsFolder } = await getBundleAgentsDirsForMobile()
      const { previewBundleWithConflicts } = await import("./bundle-service")
      const preview = previewBundleWithConflicts(temp.filePath, targetAgentsFolder)
      if (!preview.success || !preview.bundle || !preview.conflicts) {
        return reply.code(400).send({ error: preview.error || "Failed to preview bundle" })
      }

      return reply.send({
        success: true,
        preview: {
          bundle: preview.bundle,
          conflicts: preview.conflicts,
        },
      })
    } catch (error: any) {
      diagnosticsService.logError("remote-server", "Failed to preview bundle import request", error)
      return reply.code(400).send({ error: error?.message || "Failed to preview bundle import request" })
    } finally {
      if (temp) cleanupTemporaryBundleFileForMobile(temp.dir)
    }
  })

  // POST /v1/bundles/import - Import a pasted DotAgents bundle
  fastify.post("/v1/bundles/import", async (req, reply) => {
    let temp: { dir: string; filePath: string } | null = null
    try {
      const body = req.body as { bundleJson?: unknown; conflictStrategy?: unknown; components?: unknown }
      const bundleJson = typeof body.bundleJson === "string" ? body.bundleJson.trim() : ""
      if (!bundleJson) {
        return reply.code(400).send({ error: "bundleJson must be a non-empty string" })
      }
      const conflictStrategy = body.conflictStrategy === "overwrite" || body.conflictStrategy === "rename"
        ? body.conflictStrategy
        : "skip"

      JSON.parse(bundleJson)
      temp = writeTemporaryBundleFileForMobile(bundleJson)
      const { targetAgentsFolder } = await getBundleAgentsDirsForMobile()
      const { importBundle } = await import("./bundle-service")
      const result = await importBundle(temp.filePath, targetAgentsFolder, {
        conflictStrategy,
        components: normalizeBundleComponents(body.components),
      })
      await refreshRuntimeAfterMobileBundleImport()

      return reply.send(result)
    } catch (error: any) {
      diagnosticsService.logError("remote-server", "Failed to import bundle", error)
      return reply.code(400).send({ error: error?.message || "Failed to import bundle" })
    } finally {
      if (temp) cleanupTemporaryBundleFileForMobile(temp.dir)
    }
  })

  // GET /v1/operator/mcp - Operator-level MCP summary
  fastify.get("/v1/operator/mcp", async (_req, reply) => {
    try {
      const serverStatus = mcpService.getServerStatus()
      const servers = Object.entries(serverStatus)
        .filter(([name]) => name !== "dotagents-internal")
        .map(([name, status]) => ({
          name,
          connected: status.connected,
          toolCount: status.toolCount,
          enabled: (status.runtimeEnabled ?? true) && !status.configDisabled,
          error: status.error,
        }))
      return reply.send({
        totalServers: servers.length,
        connectedServers: servers.filter((s) => s.connected).length,
        totalTools: servers.reduce((sum, s) => sum + s.toolCount, 0),
        servers,
      })
    } catch (error) {
      diagnosticsService.logError("remote-server", "Failed to build operator MCP status", error)
      return reply.code(500).send({ error: "Failed to build operator MCP status" })
    }
  })

  // GET /v1/operator/mcp/tools - Operator-level MCP tool list
  fastify.get("/v1/operator/mcp/tools", async (req, reply) => {
    try {
      const query = req.query as { server?: string }
      const server = typeof query.server === "string" && query.server.trim() ? query.server.trim() : undefined
      const allTools = mcpService.getDetailedToolList()
      const tools = (server
        ? allTools.filter((tool) => tool.sourceName === server || tool.serverName === server)
        : allTools
      ).map((tool) => ({
        name: tool.name,
        description: tool.description ?? "",
        sourceKind: tool.sourceKind,
        sourceName: tool.sourceName,
        sourceLabel: tool.sourceLabel ?? tool.sourceName,
        serverName: tool.serverName,
        enabled: tool.enabled,
        serverEnabled: tool.serverEnabled,
      }))

      return reply.send({
        count: tools.length,
        ...(server ? { server } : {}),
        tools,
      })
    } catch (error) {
      diagnosticsService.logError("remote-server", "Failed to build operator MCP tools", error)
      return reply.code(500).send({ error: "Failed to build operator MCP tools" })
    }
  })

  // POST /v1/operator/mcp/tools/toggle - Enable/disable an MCP tool for the current profile/runtime
  fastify.post("/v1/operator/mcp/tools/toggle", async (req, reply) => {
    try {
      const body = req.body as { toolName?: string; enabled?: boolean } | null
      const toolName = typeof body?.toolName === "string" ? body.toolName.trim() : ""
      const enabled = body?.enabled

      if (!toolName || typeof enabled !== "boolean") {
        return reply.code(400).send({ error: "Missing toolName or enabled" })
      }

      const success = mcpService.setToolEnabled(toolName, enabled)
      if (!success) {
        setOperatorAuditContext(req, {
          action: "mcp-tool-toggle",
          success: false,
          failureReason: "tool-not-found-or-locked",
          details: { toolName, enabled },
        })
        return reply.code(404).send({ error: `Tool '${toolName}' could not be updated` })
      }

      const tool = mcpService.getDetailedToolList().find((entry) => entry.name === toolName)
      setOperatorAuditContext(req, {
        action: "mcp-tool-toggle",
        success: true,
        details: { toolName, enabled },
      })

      return reply.send({
        success: true,
        tool: tool ? {
          name: tool.name,
          description: tool.description ?? "",
          sourceKind: tool.sourceKind,
          sourceName: tool.sourceName,
          sourceLabel: tool.sourceLabel ?? tool.sourceName,
          serverName: tool.serverName,
          enabled: tool.enabled,
          serverEnabled: tool.serverEnabled,
        } : undefined,
      })
    } catch (error) {
      setOperatorAuditContext(req, {
        action: "mcp-tool-toggle",
        success: false,
        failureReason: "mcp-tool-toggle-error",
      })
      const errorMessage = getErrorMessage(error)
      diagnosticsService.logError("remote-server", `Operator MCP tool toggle failed: ${errorMessage}`, error)
      return reply.code(500).send({ error: `MCP tool toggle failed: ${errorMessage}` })
    }
  })

  // GET /v1/operator/mcp/:server/logs - Recent MCP server log preview
  fastify.get("/v1/operator/mcp/:server/logs", async (req, reply) => {
    try {
      const params = req.params as { server: string }
      const query = req.query as { count?: string }
      const serverName = decodeURIComponent(params.server || "").trim()
      if (!serverName) {
        return reply.code(400).send({ error: "Missing server name" })
      }

      const parsedCount = Number.parseInt(String(query.count ?? "20"), 10)
      const count = Number.isFinite(parsedCount) ? Math.max(1, Math.min(parsedCount, 100)) : 20
      const logs = mcpService.getServerLogs(serverName).slice(-count).reverse()
      return reply.send({ server: serverName, count: logs.length, logs })
    } catch (error) {
      const errorMessage = getErrorMessage(error)
      diagnosticsService.logError("remote-server", `Operator MCP logs failed: ${errorMessage}`, error)
      return reply.code(500).send({ error: `Failed to load MCP logs: ${errorMessage}` })
    }
  })

  // POST /v1/operator/mcp/:server/logs/clear - Clear MCP server logs
  fastify.post("/v1/operator/mcp/:server/logs/clear", async (req, reply) => {
    try {
      const params = req.params as { server: string }
      const serverName = decodeURIComponent(params.server || "").trim()
      if (!serverName) {
        return reply.code(400).send({ error: "Missing server name" })
      }

      mcpService.clearServerLogs(serverName)
      setOperatorAuditContext(req, {
        action: "mcp-clear-logs",
        success: true,
        details: { server: serverName },
      })
      return reply.send({ success: true, action: "mcp-clear-logs", server: serverName })
    } catch (error) {
      setOperatorAuditContext(req, {
        action: "mcp-clear-logs",
        success: false,
        failureReason: "mcp-clear-logs-error",
      })
      const errorMessage = getErrorMessage(error)
      diagnosticsService.logError("remote-server", `Operator MCP clear logs failed: ${errorMessage}`, error)
      return reply.code(500).send({ error: `Failed to clear MCP logs: ${errorMessage}` })
    }
  })

  // POST /v1/operator/actions/mcp-restart - Restart an MCP server
  fastify.post("/v1/operator/actions/mcp-restart", async (req, reply) => {
    try {
      const body = req.body as { server?: string } | null
      const serverName = typeof body?.server === "string" ? body.server.trim() : ""
      if (!serverName) {
        return reply.code(400).send({ error: "Missing server name" })
      }

      setOperatorAuditContext(req, {
        action: "mcp-restart",
        success: true,
        details: { server: serverName },
      })

      diagnosticsService.logInfo("remote-server", `Operator MCP restart: ${serverName}`)
      const result = await mcpService.restartServer(serverName)
      if (!result.success) {
        setOperatorAuditContext(req, {
          action: "mcp-restart",
          success: false,
          failureReason: result.error || "restart-failed",
        })
        return reply.code(400).send({ error: result.error || "Restart failed" })
      }

      return reply.send({ success: true, action: "mcp-restart", server: serverName })
    } catch (error) {
      setOperatorAuditContext(req, {
        action: "mcp-restart",
        success: false,
        failureReason: "mcp-restart-error",
      })
      const errorMessage = getErrorMessage(error)
      diagnosticsService.logError("remote-server", `Operator MCP restart failed: ${errorMessage}`, error)
      return reply.code(500).send({ error: `MCP restart failed: ${errorMessage}` })
    }
  })

  // POST /v1/operator/actions/mcp-start - Runtime-enable and start an MCP server
  fastify.post("/v1/operator/actions/mcp-start", async (req, reply) => {
    try {
      const body = req.body as { server?: string } | null
      const serverName = typeof body?.server === "string" ? body.server.trim() : ""
      if (!serverName) {
        return reply.code(400).send({ error: "Missing server name" })
      }

      const enabled = mcpService.setServerRuntimeEnabled(serverName, true)
      if (!enabled) {
        setOperatorAuditContext(req, {
          action: "mcp-start",
          success: false,
          failureReason: "server-not-found",
          details: { server: serverName },
        })
        return reply.code(404).send({ error: `Server '${serverName}' not found` })
      }

      const result = await mcpService.restartServer(serverName)
      if (!result.success) {
        setOperatorAuditContext(req, {
          action: "mcp-start",
          success: false,
          failureReason: result.error || "start-failed",
          details: { server: serverName },
        })
        return reply.code(400).send({ error: result.error || "Start failed" })
      }

      setOperatorAuditContext(req, {
        action: "mcp-start",
        success: true,
        details: { server: serverName },
      })
      return reply.send({ success: true, action: "mcp-start", server: serverName })
    } catch (error) {
      setOperatorAuditContext(req, {
        action: "mcp-start",
        success: false,
        failureReason: "mcp-start-error",
      })
      const errorMessage = getErrorMessage(error)
      diagnosticsService.logError("remote-server", `Operator MCP start failed: ${errorMessage}`, error)
      return reply.code(500).send({ error: `MCP start failed: ${errorMessage}` })
    }
  })

  // POST /v1/operator/actions/mcp-stop - Runtime-disable and stop an MCP server
  fastify.post("/v1/operator/actions/mcp-stop", async (req, reply) => {
    try {
      const body = req.body as { server?: string } | null
      const serverName = typeof body?.server === "string" ? body.server.trim() : ""
      if (!serverName) {
        return reply.code(400).send({ error: "Missing server name" })
      }

      const disabled = mcpService.setServerRuntimeEnabled(serverName, false)
      if (!disabled) {
        setOperatorAuditContext(req, {
          action: "mcp-stop",
          success: false,
          failureReason: "server-not-found",
          details: { server: serverName },
        })
        return reply.code(404).send({ error: `Server '${serverName}' not found` })
      }

      const result = await mcpService.stopServer(serverName)
      if (!result.success) {
        setOperatorAuditContext(req, {
          action: "mcp-stop",
          success: false,
          failureReason: result.error || "stop-failed",
          details: { server: serverName },
        })
        return reply.code(400).send({ error: result.error || "Stop failed" })
      }

      setOperatorAuditContext(req, {
        action: "mcp-stop",
        success: true,
        details: { server: serverName },
      })
      return reply.send({ success: true, action: "mcp-stop", server: serverName })
    } catch (error) {
      setOperatorAuditContext(req, {
        action: "mcp-stop",
        success: false,
        failureReason: "mcp-stop-error",
      })
      const errorMessage = getErrorMessage(error)
      diagnosticsService.logError("remote-server", `Operator MCP stop failed: ${errorMessage}`, error)
      return reply.code(500).send({ error: `MCP stop failed: ${errorMessage}` })
    }
  })

  // POST /v1/operator/actions/mcp-test - Test an MCP server configuration
  fastify.post("/v1/operator/actions/mcp-test", async (req, reply) => {
    try {
      const body = req.body as { server?: string } | null
      const serverName = typeof body?.server === "string" ? body.server.trim() : ""
      if (!serverName) {
        return reply.code(400).send({ error: "Missing server name" })
      }

      const serverConfig = configStore.get().mcpConfig?.mcpServers?.[serverName]
      if (!serverConfig) {
        setOperatorAuditContext(req, {
          action: "mcp-test",
          success: false,
          failureReason: "server-not-found",
          details: { server: serverName },
        })
        return reply.code(404).send({ error: `Server '${serverName}' not found` })
      }

      const result = await mcpService.testServerConnection(serverName, serverConfig)
      setOperatorAuditContext(req, {
        action: "mcp-test",
        success: result.success,
        failureReason: result.success ? undefined : result.error || "test-failed",
        details: { server: serverName, toolCount: result.toolCount },
      })

      if (!result.success) {
        return reply.code(400).send({ success: false, error: result.error || "Connection test failed" })
      }

      return reply.send({
        success: true,
        action: "mcp-test",
        server: serverName,
        toolCount: result.toolCount ?? 0,
      })
    } catch (error) {
      setOperatorAuditContext(req, {
        action: "mcp-test",
        success: false,
        failureReason: "mcp-test-error",
      })
      const errorMessage = getErrorMessage(error)
      diagnosticsService.logError("remote-server", `Operator MCP test failed: ${errorMessage}`, error)
      return reply.code(500).send({ error: `MCP test failed: ${errorMessage}` })
    }
  })

  const buildMcpServerSummary = (serverName: string) => {
    const serverStatus = mcpService.getServerStatus()
    const status = serverStatus[serverName]
    const serverConfig = configStore.get().mcpConfig?.mcpServers?.[serverName] as MCPServerConfig | undefined
    return {
      name: serverName,
      connected: !!status?.connected,
      toolCount: status?.toolCount ?? 0,
      enabled: (status?.runtimeEnabled ?? true) && !serverConfig?.disabled,
      runtimeEnabled: status?.runtimeEnabled ?? true,
      configDisabled: !!serverConfig?.disabled,
      error: status?.error,
    }
  }

  type RemoteMcpServerConfig = Omit<MCPServerConfig, "oauth"> & { oauth?: OAuthConfig | null }

  const isReservedRemoteMcpServerName = (serverName: string) => {
    const normalizedName = serverName.trim().toLowerCase()
    return RESERVED_RUNTIME_TOOL_SERVER_NAMES.some((reservedName) => reservedName.toLowerCase() === normalizedName)
  }

  const sanitizeStringRecord = (
    value: unknown,
    fieldLabel: string,
    serverName: string,
  ): Record<string, string> | undefined => {
    if (value === undefined) return undefined
    if (!value || typeof value !== "object" || Array.isArray(value)) {
      throw new Error(`Server "${serverName}": ${fieldLabel} must be an object`)
    }
    return Object.fromEntries(
      Object.entries(value as Record<string, unknown>).map(([key, entryValue]) => [key, String(entryValue)]),
    )
  }

  const sanitizeMcpOAuthConfig = (oauth: OAuthConfig | undefined): OAuthConfig | undefined => {
    if (!oauth) return undefined
    const safeConfig: OAuthConfig = {}
    if (typeof oauth.scope === "string" && oauth.scope.trim()) safeConfig.scope = oauth.scope.trim()
    if (typeof oauth.clientId === "string" && oauth.clientId.trim()) safeConfig.clientId = oauth.clientId.trim()
    if (typeof oauth.redirectUri === "string" && oauth.redirectUri.trim()) safeConfig.redirectUri = oauth.redirectUri.trim()
    if (typeof oauth.useDiscovery === "boolean") safeConfig.useDiscovery = oauth.useDiscovery
    if (typeof oauth.useDynamicRegistration === "boolean") safeConfig.useDynamicRegistration = oauth.useDynamicRegistration
    return Object.keys(safeConfig).length > 0 ? safeConfig : undefined
  }

  const sanitizeMcpServerConfig = (serverConfig: MCPServerConfig): MCPServerConfig => {
    const { oauth, ...safeConfig } = serverConfig
    const safeOAuth = sanitizeMcpOAuthConfig(oauth)
    return safeOAuth ? { ...safeConfig, oauth: safeOAuth } : safeConfig
  }

  const normalizeRemoteMcpOAuthConfig = (serverName: string, input: unknown): OAuthConfig | null | undefined => {
    if (input === undefined) return undefined
    if (input === null) return null
    if (!input || typeof input !== "object" || Array.isArray(input)) {
      throw new Error(`Server "${serverName}": OAuth config must be an object`)
    }

    const rawOAuth = input as Record<string, unknown>
    const oauth: OAuthConfig = {
      useDiscovery: rawOAuth.useDiscovery !== false,
      useDynamicRegistration: rawOAuth.useDynamicRegistration !== false,
    }
    if (rawOAuth.scope !== undefined) {
      if (typeof rawOAuth.scope !== "string") throw new Error(`Server "${serverName}": OAuth scope must be a string`)
      if (rawOAuth.scope.trim()) oauth.scope = rawOAuth.scope.trim()
    }
    if (rawOAuth.clientId !== undefined) {
      if (typeof rawOAuth.clientId !== "string") throw new Error(`Server "${serverName}": OAuth client ID must be a string`)
      if (rawOAuth.clientId.trim()) oauth.clientId = rawOAuth.clientId.trim()
    }
    if (rawOAuth.redirectUri !== undefined) {
      if (typeof rawOAuth.redirectUri !== "string") throw new Error(`Server "${serverName}": OAuth redirect URI must be a string`)
      if (rawOAuth.redirectUri.trim()) oauth.redirectUri = rawOAuth.redirectUri.trim()
    }
    if (rawOAuth.useDiscovery !== undefined && typeof rawOAuth.useDiscovery !== "boolean") {
      throw new Error(`Server "${serverName}": OAuth discovery flag must be a boolean`)
    }
    if (rawOAuth.useDynamicRegistration !== undefined && typeof rawOAuth.useDynamicRegistration !== "boolean") {
      throw new Error(`Server "${serverName}": OAuth dynamic registration flag must be a boolean`)
    }
    return oauth
  }

  const normalizeRemoteMcpServerConfig = (serverName: string, input: unknown): RemoteMcpServerConfig => {
    if (!input || typeof input !== "object" || Array.isArray(input)) {
      throw new Error("MCP server config must be an object")
    }

    const config = { ...(input as RemoteMcpServerConfig) }
    const transport = config.transport ?? (config.url?.trim()
      ? config.url.trim().toLowerCase().startsWith("ws") ? "websocket" : "streamableHttp"
      : "stdio")
    if (!["stdio", "websocket", "streamableHttp"].includes(transport)) {
      throw new Error(`Server "${serverName}": transport must be stdio, websocket, or streamableHttp`)
    }

    const normalized: RemoteMcpServerConfig = {
      transport,
      disabled: config.disabled === true,
    }

    if (config.timeout !== undefined) {
      if (typeof config.timeout !== "number" || !Number.isFinite(config.timeout) || config.timeout < 0) {
        throw new Error(`Server "${serverName}": timeout must be a non-negative number`)
      }
      normalized.timeout = Math.round(config.timeout)
    }

    if (transport === "stdio") {
      if (typeof config.command !== "string" || !config.command.trim()) {
        throw new Error(`Server "${serverName}": stdio transport requires a command`)
      }
      if (config.args !== undefined && (!Array.isArray(config.args) || config.args.some((arg) => typeof arg !== "string"))) {
        throw new Error(`Server "${serverName}": args must be an array of strings`)
      }
      if (config.env !== undefined && (typeof config.env !== "object" || Array.isArray(config.env))) {
        throw new Error(`Server "${serverName}": env must be an object`)
      }

      normalized.command = config.command.trim()
      normalized.args = config.args ?? []
      normalized.env = sanitizeStringRecord(config.env, "env", serverName)
      return normalized
    }

    if (typeof config.url !== "string" || !config.url.trim()) {
      throw new Error(`Server "${serverName}": ${transport} transport requires a URL`)
    }
    try {
      new URL(config.url.trim())
    } catch {
      throw new Error(`Server "${serverName}": URL is invalid`)
    }
    if (config.headers !== undefined && (typeof config.headers !== "object" || Array.isArray(config.headers))) {
      throw new Error(`Server "${serverName}": headers must be an object`)
    }

    normalized.url = config.url.trim()
    normalized.headers = sanitizeStringRecord(config.headers, "headers", serverName)
    const oauth = normalizeRemoteMcpOAuthConfig(serverName, Object.prototype.hasOwnProperty.call(config, "oauth") ? config.oauth : undefined)
    if (transport === "streamableHttp" && oauth !== undefined) {
      normalized.oauth = oauth
    }
    return normalized
  }

  const finalizeRemoteMcpServerConfig = (
    normalizedConfig: RemoteMcpServerConfig,
    existingConfig?: MCPServerConfig,
    preserveExistingOAuth: boolean = false,
  ): MCPServerConfig => {
    const hasOAuthField = Object.prototype.hasOwnProperty.call(normalizedConfig, "oauth")
    if (normalizedConfig.oauth === null) {
      const { oauth: _oauth, ...withoutOAuth } = normalizedConfig
      return withoutOAuth
    }
    if (normalizedConfig.oauth) {
      return existingConfig?.oauth
        ? {
            ...normalizedConfig,
            oauth: {
              ...existingConfig.oauth,
              ...normalizedConfig.oauth,
              ...(existingConfig.oauth.tokens ? { tokens: existingConfig.oauth.tokens } : {}),
              ...(existingConfig.oauth.pendingAuth ? { pendingAuth: existingConfig.oauth.pendingAuth } : {}),
            },
          }
        : normalizedConfig as MCPServerConfig
    }
    if (preserveExistingOAuth && existingConfig?.oauth && !hasOAuthField) {
      return { ...normalizedConfig, oauth: existingConfig.oauth }
    }
    return normalizedConfig as MCPServerConfig
  }

  const buildExportableMcpConfig = (): MCPConfig => {
    const servers = configStore.get().mcpConfig?.mcpServers || {}
    return {
      mcpServers: Object.fromEntries(
        Object.entries(servers)
          .filter(([serverName]) => !isReservedRemoteMcpServerName(serverName))
          .map(([serverName, serverConfig]) => [serverName, sanitizeMcpServerConfig(serverConfig as MCPServerConfig)]),
      ),
    }
  }

  // GET /v1/mcp/servers - List MCP servers with status
  fastify.get("/v1/mcp/servers", async (_req, reply) => {
    try {
      const serverStatus = mcpService.getServerStatus()
      const servers = Object.entries(serverStatus)
        .map(([name, status]) => ({
          name,
          connected: status.connected,
          toolCount: status.toolCount,
          enabled: status.runtimeEnabled && !status.configDisabled,
          runtimeEnabled: status.runtimeEnabled,
          configDisabled: status.configDisabled,
          error: status.error,
        }))
      return reply.send({ servers })
    } catch (error: any) {
      diagnosticsService.logError("remote-server", "Failed to get MCP servers", error)
      return reply.code(500).send({ error: "Failed to get MCP servers" })
    }
  })

  // GET /v1/mcp/config/export - Export user-configurable MCP server configs
  fastify.get("/v1/mcp/config/export", async (_req, reply) => {
    try {
      return reply.send({ success: true, config: buildExportableMcpConfig() })
    } catch (error: any) {
      diagnosticsService.logError("remote-server", "Failed to export MCP server configs", error)
      return reply.code(500).send({ error: error?.message || "Failed to export MCP server configs" })
    }
  })

  // POST /v1/mcp/config/import - Import user-configurable MCP server configs
  fastify.post("/v1/mcp/config/import", async (req, reply) => {
    try {
      const body = req.body as { config?: { mcpServers?: unknown } } | null
      const importedServers = body?.config?.mcpServers
      if (!importedServers || typeof importedServers !== "object" || Array.isArray(importedServers)) {
        return reply.code(400).send({ error: "Import body must include config.mcpServers" })
      }

      const currentConfig = configStore.get()
      const currentServers = currentConfig.mcpConfig?.mcpServers || {}
      const nextMcpServers: Record<string, MCPServerConfig> = { ...currentServers }
      const skippedReservedServerNames: string[] = []
      const importedServerNames: string[] = []

      for (const [rawServerName, rawServerConfig] of Object.entries(importedServers as Record<string, unknown>)) {
        const serverName = rawServerName.trim()
        if (!serverName) continue
        if (isReservedRemoteMcpServerName(serverName)) {
          skippedReservedServerNames.push(serverName)
          continue
        }

        const normalizedConfig = normalizeRemoteMcpServerConfig(serverName, rawServerConfig)
        nextMcpServers[serverName] = finalizeRemoteMcpServerConfig(
          normalizedConfig,
          currentServers[serverName] as MCPServerConfig | undefined,
          false,
        )
        importedServerNames.push(serverName)
      }

      configStore.save({
        ...currentConfig,
        mcpConfig: {
          ...(currentConfig.mcpConfig || { mcpServers: {} }),
          mcpServers: nextMcpServers,
        },
      })

      await Promise.all(importedServerNames.map(async (serverName) => {
        const serverConfig = nextMcpServers[serverName]
        const result = serverConfig.disabled
          ? await mcpService.stopServer(serverName)
          : await mcpService.restartServer(serverName)
        if (!result.success) {
          diagnosticsService.logError("remote-server", `MCP server ${serverName} imported but failed to restart`, result.error)
        }
      }))

      return reply.send({
        success: true,
        importedCount: importedServerNames.length,
        skippedReservedServerNames,
      })
    } catch (error: any) {
      diagnosticsService.logError("remote-server", "Failed to import MCP server configs", error)
      return reply.code(400).send({ error: error?.message || "Failed to import MCP server configs" })
    }
  })

  // GET /v1/mcp/servers/:name/config - Get one MCP server config for mobile editing
  fastify.get("/v1/mcp/servers/:name/config", async (req, reply) => {
    try {
      const params = req.params as { name: string }
      const serverName = decodeURIComponent(params.name || "").trim()
      const serverConfig = configStore.get().mcpConfig?.mcpServers?.[serverName] as MCPServerConfig | undefined
      if (!serverName || !serverConfig) {
        return reply.code(404).send({ error: `Server '${serverName}' not found` })
      }

      return reply.send({
        name: serverName,
        config: sanitizeMcpServerConfig(serverConfig),
        server: buildMcpServerSummary(serverName),
      })
    } catch (error: any) {
      diagnosticsService.logError("remote-server", "Failed to get MCP server config", error)
      return reply.code(500).send({ error: error?.message || "Failed to get MCP server config" })
    }
  })

  // PUT /v1/mcp/servers/:name/config - Create or replace one MCP server config
  fastify.put("/v1/mcp/servers/:name/config", async (req, reply) => {
    try {
      const params = req.params as { name: string }
      const body = req.body as { config?: unknown } | null
      const serverName = decodeURIComponent(params.name || "").trim()
      if (!serverName) {
        return reply.code(400).send({ error: "Missing server name" })
      }

      const currentConfig = configStore.get()
      const existingConfig = currentConfig.mcpConfig?.mcpServers?.[serverName] as MCPServerConfig | undefined
      const normalizedConfig = normalizeRemoteMcpServerConfig(serverName, body?.config)
      const nextServerConfig = finalizeRemoteMcpServerConfig(normalizedConfig, existingConfig, true)

      const nextMcpServers = {
        ...(currentConfig.mcpConfig?.mcpServers || {}),
        [serverName]: nextServerConfig,
      }
      configStore.save({
        ...currentConfig,
        mcpConfig: {
          ...(currentConfig.mcpConfig || { mcpServers: {} }),
          mcpServers: nextMcpServers,
        },
      })

      const lifecycleResult = nextServerConfig.disabled
        ? await mcpService.stopServer(serverName)
        : await mcpService.restartServer(serverName)

      if (!lifecycleResult.success) {
        diagnosticsService.logError("remote-server", `MCP server ${serverName} saved but failed to restart`, lifecycleResult.error)
      }

      return reply.send({
        name: serverName,
        config: sanitizeMcpServerConfig(nextServerConfig),
        server: buildMcpServerSummary(serverName),
      })
    } catch (error: any) {
      diagnosticsService.logError("remote-server", "Failed to save MCP server config", error)
      return reply.code(400).send({ error: error?.message || "Failed to save MCP server config" })
    }
  })

  // DELETE /v1/mcp/servers/:name - Delete one MCP server config
  fastify.delete("/v1/mcp/servers/:name", async (req, reply) => {
    try {
      const params = req.params as { name: string }
      const serverName = decodeURIComponent(params.name || "").trim()
      const currentConfig = configStore.get()
      const currentServers = currentConfig.mcpConfig?.mcpServers || {}
      if (!serverName || !currentServers[serverName]) {
        return reply.code(404).send({ error: `Server '${serverName}' not found` })
      }

      await mcpService.stopServer(serverName)
      const { [serverName]: _removed, ...nextMcpServers } = currentServers
      configStore.save({
        ...currentConfig,
        mcpRuntimeDisabledServers: (currentConfig.mcpRuntimeDisabledServers || []).filter((name) => name !== serverName),
        mcpConfig: {
          ...(currentConfig.mcpConfig || { mcpServers: {} }),
          mcpServers: nextMcpServers,
        },
      })

      return reply.send({ success: true, server: serverName })
    } catch (error: any) {
      diagnosticsService.logError("remote-server", "Failed to delete MCP server config", error)
      return reply.code(500).send({ error: error?.message || "Failed to delete MCP server config" })
    }
  })

  // GET /v1/mcp/servers/:name/oauth - Check OAuth status for one MCP server
  fastify.get("/v1/mcp/servers/:name/oauth", async (req, reply) => {
    try {
      const params = req.params as { name: string }
      const serverName = decodeURIComponent(params.name || "").trim()
      if (!serverName) {
        return reply.code(400).send({ error: "Missing server name" })
      }

      const status = await mcpService.getOAuthStatus(serverName)
      return reply.send(status)
    } catch (error: any) {
      diagnosticsService.logError("remote-server", "Failed to get MCP OAuth status", error)
      return reply.code(500).send({ error: error?.message || "Failed to get MCP OAuth status" })
    }
  })

  // POST /v1/mcp/servers/:name/oauth/start - Start OAuth flow for one MCP server
  fastify.post("/v1/mcp/servers/:name/oauth/start", async (req, reply) => {
    try {
      const params = req.params as { name: string }
      const serverName = decodeURIComponent(params.name || "").trim()
      if (!serverName) {
        return reply.code(400).send({ error: "Missing server name" })
      }

      const result = await mcpService.initiateOAuthFlow(serverName)
      return reply.send(result)
    } catch (error: any) {
      diagnosticsService.logError("remote-server", "Failed to start MCP OAuth", error)
      return reply.code(400).send({ error: error?.message || "Failed to start MCP OAuth" })
    }
  })

  // POST /v1/mcp/servers/:name/oauth/revoke - Revoke OAuth tokens for one MCP server
  fastify.post("/v1/mcp/servers/:name/oauth/revoke", async (req, reply) => {
    try {
      const params = req.params as { name: string }
      const serverName = decodeURIComponent(params.name || "").trim()
      if (!serverName) {
        return reply.code(400).send({ error: "Missing server name" })
      }

      const result = await mcpService.revokeOAuthTokens(serverName)
      if (!result.success) {
        return reply.code(400).send(result)
      }
      return reply.send(result)
    } catch (error: any) {
      diagnosticsService.logError("remote-server", "Failed to revoke MCP OAuth", error)
      return reply.code(500).send({ error: error?.message || "Failed to revoke MCP OAuth" })
    }
  })

  // POST /v1/mcp/servers/:name/toggle - Toggle MCP server enabled/disabled
  fastify.post("/v1/mcp/servers/:name/toggle", async (req, reply) => {
    try {
      const params = req.params as { name: string }
      const body = req.body as any
      const serverName = params.name
      const enabled = body?.enabled

      if (typeof enabled !== "boolean") {
        return reply.code(400).send({ error: "Missing or invalid 'enabled' boolean" })
      }

      const success = mcpService.setServerRuntimeEnabled(serverName, enabled)
      if (!success) {
        return reply.code(404).send({ error: `Server '${serverName}' not found` })
      }

      diagnosticsService.logInfo("remote-server", `Toggled MCP server ${serverName} to ${enabled ? "enabled" : "disabled"}`)
      return reply.send({
        success: true,
        server: serverName,
        enabled,
      })
    } catch (error: any) {
      diagnosticsService.logError("remote-server", "Failed to toggle MCP server", error)
      return reply.code(500).send({ error: error?.message || "Failed to toggle MCP server" })
    }
  })

  fastify.get("/v1/operator/model-presets", async (_req, reply) => {
    try {
      return reply.send(await buildModelPresetsResponse(configStore.get()))
    } catch (error: any) {
      diagnosticsService.logError("remote-server", "Failed to build model preset summaries", error)
      return reply.code(500).send({ error: error?.message || "Failed to build model preset summaries" })
    }
  })

  fastify.post("/v1/operator/model-presets", async (req, reply) => {
    try {
      const body = req.body as Record<string, unknown> | undefined
      const name = normalizeModelPresetString(body?.name, 120)
      const baseUrl = normalizeModelPresetString(body?.baseUrl, 500)
      if (!name) {
        return reply.code(400).send({ error: "Preset name is required" })
      }
      if (!baseUrl) {
        return reply.code(400).send({ error: "Preset base URL is required" })
      }

      const now = Date.now()
      const agentModel = normalizeModelPresetString(body?.agentModel ?? body?.mcpToolsModel, 300) || ""
      const preset: ModelPreset = {
        id: `custom-${crypto.randomUUID()}`,
        name,
        baseUrl,
        apiKey: normalizeModelPresetString(body?.apiKey, 2000) || "",
        isBuiltIn: false,
        createdAt: now,
        updatedAt: now,
        agentModel,
        mcpToolsModel: agentModel,
        transcriptProcessingModel: normalizeModelPresetString(body?.transcriptProcessingModel, 300) || "",
      }

      const cfg = configStore.get()
      const nextConfig = { ...cfg, modelPresets: [...getSavedModelPresets(cfg), preset] }
      configStore.save(nextConfig)
      return reply.send(buildModelPresetMutationResponse(await buildModelPresetsResponse(nextConfig), { preset }))
    } catch (error: any) {
      diagnosticsService.logError("remote-server", "Failed to create model preset", error)
      return reply.code(500).send({ error: error?.message || "Failed to create model preset" })
    }
  })

  fastify.patch("/v1/operator/model-presets/:presetId", async (req, reply) => {
    try {
      const params = req.params as { presetId?: string }
      const presetId = decodeURIComponent(params.presetId || "").trim()
      if (!presetId) {
        return reply.code(400).send({ error: "Missing preset ID" })
      }

      const cfg = configStore.get()
      const { presets } = await getMergedModelPresetState(cfg)
      const existingPreset = presets.find((preset) => preset.id === presetId)
      if (!existingPreset) {
        return reply.code(404).send({ error: "Model preset not found" })
      }

      const patch = buildModelPresetUpdatePatch(req.body, existingPreset)
      const modelPresets = await upsertModelPresetOverride(cfg, presetId, patch)
      const nextLookupState = await getMergedModelPresetState({ ...cfg, modelPresets })
      const updatedPreset = nextLookupState.presets.find((preset) => preset.id === presetId)
      if (!updatedPreset) {
        return reply.code(404).send({ error: "Model preset not found" })
      }

      const updates: Partial<Config> = { modelPresets }
      if ((cfg.currentModelPresetId || nextLookupState.defaultModelPresetId) === presetId) {
        Object.assign(updates, getModelPresetActivationUpdates(updatedPreset))
      }
      const nextConfig = { ...cfg, ...updates }
      configStore.save(nextConfig)
      return reply.send(buildModelPresetMutationResponse(await buildModelPresetsResponse(nextConfig), { preset: updatedPreset }))
    } catch (error: any) {
      diagnosticsService.logError("remote-server", "Failed to update model preset", error)
      return reply.code(500).send({ error: error?.message || "Failed to update model preset" })
    }
  })

  fastify.delete("/v1/operator/model-presets/:presetId", async (req, reply) => {
    try {
      const params = req.params as { presetId?: string }
      const presetId = decodeURIComponent(params.presetId || "").trim()
      if (!presetId) {
        return reply.code(400).send({ error: "Missing preset ID" })
      }

      const cfg = configStore.get()
      const { defaultModelPresetId, presets } = await getMergedModelPresetState(cfg)
      const preset = presets.find((candidate) => candidate.id === presetId)
      if (!preset) {
        return reply.code(404).send({ error: "Model preset not found" })
      }
      if (preset.isBuiltIn) {
        return reply.code(400).send({ error: "Built-in presets cannot be deleted" })
      }

      const modelPresets = getSavedModelPresets(cfg).filter((candidate) => candidate.id !== presetId)
      const updates: Partial<Config> = { modelPresets }
      const switchedToDefault = (cfg.currentModelPresetId || defaultModelPresetId) === presetId
      if (switchedToDefault) {
        const defaultPresetState = await getMergedModelPresetState({ ...cfg, modelPresets })
        const defaultPreset = defaultPresetState.presets.find((candidate) => candidate.id === defaultModelPresetId)
        if (defaultPreset) {
          Object.assign(updates, getModelPresetActivationUpdates(defaultPreset))
        } else {
          updates.currentModelPresetId = defaultModelPresetId
        }
      }

      const nextConfig = { ...cfg, ...updates }
      configStore.save(nextConfig)
      return reply.send(buildModelPresetMutationResponse(await buildModelPresetsResponse(nextConfig), { deletedPresetId: presetId }))
    } catch (error: any) {
      diagnosticsService.logError("remote-server", "Failed to delete model preset", error)
      return reply.code(500).send({ error: error?.message || "Failed to delete model preset" })
    }
  })

  // GET /v1/settings - Get relevant settings for mobile app
  fastify.get("/v1/settings", async (_req, reply) => {
    try {
      const cfg = configStore.get()
      const { defaultModelPresetId, presets } = await getMergedModelPresetState(cfg)

      return reply.send({
        // Agent model settings (agent* preferred; mcpTools* legacy aliases)
        agentProviderId: cfg.agentProviderId || cfg.mcpToolsProviderId || "openai",
        agentOpenaiModel: cfg.agentOpenaiModel || cfg.mcpToolsOpenaiModel,
        agentGroqModel: cfg.agentGroqModel || cfg.mcpToolsGroqModel,
        agentGeminiModel: cfg.agentGeminiModel || cfg.mcpToolsGeminiModel,
        agentChatgptWebModel: cfg.agentChatgptWebModel || cfg.mcpToolsChatgptWebModel,
        mcpToolsProviderId: cfg.mcpToolsProviderId || "openai",
        mcpToolsOpenaiModel: cfg.mcpToolsOpenaiModel,
        mcpToolsGroqModel: cfg.mcpToolsGroqModel,
        mcpToolsGeminiModel: cfg.mcpToolsGeminiModel,
        mcpToolsChatgptWebModel: cfg.mcpToolsChatgptWebModel,
        openaiReasoningEffort: cfg.openaiReasoningEffort,
        codexTextVerbosity: cfg.codexTextVerbosity,
        openaiApiKey: cfg.openaiApiKey ? REMOTE_SERVER_SECRET_MASK : "",
        openaiBaseUrl: cfg.openaiBaseUrl ?? "",
        groqApiKey: cfg.groqApiKey ? REMOTE_SERVER_SECRET_MASK : "",
        groqBaseUrl: cfg.groqBaseUrl ?? "",
        geminiApiKey: cfg.geminiApiKey ? REMOTE_SERVER_SECRET_MASK : "",
        geminiBaseUrl: cfg.geminiBaseUrl ?? "",
        chatgptWebAccessToken: cfg.chatgptWebAccessToken ? REMOTE_SERVER_SECRET_MASK : "",
        chatgptWebSessionToken: cfg.chatgptWebSessionToken ? REMOTE_SERVER_SECRET_MASK : "",
        chatgptWebBaseUrl: cfg.chatgptWebBaseUrl ?? "",
        // OpenAI compatible preset settings
        currentModelPresetId: cfg.currentModelPresetId || defaultModelPresetId,
        availablePresets: presets.map(formatModelPresetSummary),
        predefinedPrompts: (cfg.predefinedPrompts || []).map(prompt => ({
          id: prompt.id,
          name: prompt.name,
          content: prompt.content,
          createdAt: prompt.createdAt,
          updatedAt: prompt.updatedAt,
        })),
        // Feature toggles
        transcriptPostProcessingEnabled: cfg.transcriptPostProcessingEnabled ?? true,
        mcpRequireApprovalBeforeToolCall: cfg.mcpRequireApprovalBeforeToolCall ?? false,
        ttsEnabled: cfg.ttsEnabled ?? true,
        whatsappEnabled: cfg.whatsappEnabled ?? false,
        discordEnabled: cfg.discordEnabled ?? false,
        // Agent settings
        mcpMaxIterations: cfg.mcpMaxIterations ?? 10,
        // Streamer Mode
        streamerModeEnabled: cfg.streamerModeEnabled ?? false,
        // Speech-to-Text
        sttLanguage: cfg.sttLanguage ?? "",
        transcriptionPreviewEnabled: cfg.transcriptionPreviewEnabled ?? true,
        transcriptPostProcessingPrompt: cfg.transcriptPostProcessingPrompt ?? "",
        // Text-to-Speech
        ttsAutoPlay: cfg.ttsAutoPlay ?? true,
        ttsPreprocessingEnabled: cfg.ttsPreprocessingEnabled ?? true,
        ttsRemoveCodeBlocks: cfg.ttsRemoveCodeBlocks ?? true,
        ttsRemoveUrls: cfg.ttsRemoveUrls ?? true,
        ttsConvertMarkdown: cfg.ttsConvertMarkdown ?? true,
        ttsUseLLMPreprocessing: cfg.ttsUseLLMPreprocessing ?? false,
        // Agent settings (extended)
        mainAgentMode: cfg.mainAgentMode ?? "api",
        mcpMessageQueueEnabled: cfg.mcpMessageQueueEnabled ?? true,
        mcpAutoPasteEnabled: cfg.mcpAutoPasteEnabled ?? false,
        mcpAutoPasteDelay: cfg.mcpAutoPasteDelay ?? 1000,
        mcpVerifyCompletionEnabled: cfg.mcpVerifyCompletionEnabled ?? true,
        mcpFinalSummaryEnabled: cfg.mcpFinalSummaryEnabled ?? false,
        dualModelEnabled: cfg.dualModelEnabled ?? false,
        mcpUnlimitedIterations: cfg.mcpUnlimitedIterations ?? true,
        // Tool Execution
        mcpContextReductionEnabled: cfg.mcpContextReductionEnabled ?? true,
        mcpToolResponseProcessingEnabled: cfg.mcpToolResponseProcessingEnabled ?? true,
        mcpParallelToolExecution: cfg.mcpParallelToolExecution ?? true,
        // Remote Server
        remoteServerEnabled: cfg.remoteServerEnabled ?? false,
        remoteServerPort: cfg.remoteServerPort ?? 3210,
        remoteServerBindAddress: cfg.remoteServerBindAddress ?? "127.0.0.1",
        remoteServerApiKey: getMaskedRemoteServerApiKey(cfg.remoteServerApiKey),
        remoteServerLogLevel: cfg.remoteServerLogLevel ?? "info",
        remoteServerCorsOrigins: cfg.remoteServerCorsOrigins ?? ["*"],
        remoteServerOperatorAllowDeviceIds: cfg.remoteServerOperatorAllowDeviceIds ?? [],
        remoteServerAutoShowPanel: cfg.remoteServerAutoShowPanel ?? false,
        remoteServerTerminalQrEnabled: cfg.remoteServerTerminalQrEnabled ?? false,
        // Desktop shell / panel
        hideDockIcon: cfg.hideDockIcon ?? false,
        launchAtLogin: cfg.launchAtLogin ?? false,
        themePreference: cfg.themePreference ?? "system",
        textInputEnabled: cfg.textInputEnabled ?? true,
        panelPosition: cfg.panelPosition ?? "top-right",
        panelDragEnabled: cfg.panelDragEnabled ?? true,
        floatingPanelAutoShow: cfg.floatingPanelAutoShow ?? true,
        hidePanelWhenMainFocused: cfg.hidePanelWhenMainFocused ?? true,
        panelCustomPosition: cfg.panelCustomPosition,
        panelCustomSize: cfg.panelCustomSize,
        panelProgressSize: cfg.panelProgressSize,
        panelTextInputSize: cfg.panelTextInputSize,
        // Desktop conversation storage
        conversationsEnabled: cfg.conversationsEnabled ?? true,
        maxConversationsToKeep: cfg.maxConversationsToKeep ?? 1000,
        autoSaveConversations: cfg.autoSaveConversations ?? true,
        // Cloudflare Tunnel
        cloudflareTunnelMode: cfg.cloudflareTunnelMode ?? "quick",
        cloudflareTunnelAutoStart: cfg.cloudflareTunnelAutoStart ?? false,
        cloudflareTunnelId: cfg.cloudflareTunnelId ?? "",
        cloudflareTunnelName: cfg.cloudflareTunnelName ?? "",
        cloudflareTunnelCredentialsPath: cfg.cloudflareTunnelCredentialsPath ?? "",
        cloudflareTunnelHostname: cfg.cloudflareTunnelHostname ?? "",
        // WhatsApp (extended)
        whatsappAllowFrom: cfg.whatsappAllowFrom ?? [],
        whatsappOperatorAllowFrom: cfg.whatsappOperatorAllowFrom ?? [],
        whatsappAutoReply: cfg.whatsappAutoReply ?? false,
        whatsappLogMessages: cfg.whatsappLogMessages ?? false,
        // Discord (extended)
        discordBotToken: getMaskedDiscordBotToken(cfg),
        discordDmEnabled: cfg.discordDmEnabled ?? true,
        discordRequireMention: cfg.discordRequireMention ?? true,
        discordAllowUserIds: cfg.discordAllowUserIds ?? [],
        discordAllowGuildIds: cfg.discordAllowGuildIds ?? [],
        discordAllowChannelIds: cfg.discordAllowChannelIds ?? [],
        discordAllowRoleIds: cfg.discordAllowRoleIds ?? [],
        discordDmAllowUserIds: cfg.discordDmAllowUserIds ?? [],
        discordOperatorAllowUserIds: cfg.discordOperatorAllowUserIds ?? [],
        discordOperatorAllowGuildIds: cfg.discordOperatorAllowGuildIds ?? [],
        discordOperatorAllowChannelIds: cfg.discordOperatorAllowChannelIds ?? [],
        discordOperatorAllowRoleIds: cfg.discordOperatorAllowRoleIds ?? [],
        discordDefaultProfileId: getDiscordResolvedDefaultProfileId(cfg).profileId ?? "",
        discordLogMessages: cfg.discordLogMessages ?? false,
        // Langfuse
        langfuseEnabled: cfg.langfuseEnabled ?? false,
        langfusePublicKey: cfg.langfusePublicKey ?? "",
        langfuseSecretKey: cfg.langfuseSecretKey ? "••••••••" : "",
        langfuseBaseUrl: cfg.langfuseBaseUrl ?? "",
        localTraceLoggingEnabled: cfg.localTraceLoggingEnabled ?? false,
        localTraceLogPath: cfg.localTraceLogPath ?? "",
        // STT/TTS/Post-Processing Provider settings
        sttProviderId: cfg.sttProviderId || "openai",
        openaiSttLanguage: cfg.openaiSttLanguage || "",
        openaiSttModel: cfg.openaiSttModel || "whisper-1",
        groqSttLanguage: cfg.groqSttLanguage || "",
        groqSttModel: cfg.groqSttModel || "whisper-large-v3-turbo",
        groqSttPrompt: cfg.groqSttPrompt || "",
        parakeetNumThreads: cfg.parakeetNumThreads ?? 2,
        ttsProviderId: cfg.ttsProviderId || "openai",
        transcriptPostProcessingProviderId: cfg.transcriptPostProcessingProviderId || "openai",
        transcriptPostProcessingOpenaiModel: cfg.transcriptPostProcessingOpenaiModel || "",
        transcriptPostProcessingGroqModel: cfg.transcriptPostProcessingGroqModel || "",
        transcriptPostProcessingGeminiModel: cfg.transcriptPostProcessingGeminiModel || "",
        transcriptPostProcessingChatgptWebModel: cfg.transcriptPostProcessingChatgptWebModel || "",
        // acpx Agent settings
        mainAgentName: cfg.mainAgentName || "",
        // TTS voice/model per provider
        openaiTtsModel: cfg.openaiTtsModel || "gpt-4o-mini-tts",
        openaiTtsVoice: cfg.openaiTtsVoice || "alloy",
        openaiTtsSpeed: cfg.openaiTtsSpeed ?? 1.0,
        groqTtsModel: cfg.groqTtsModel || "canopylabs/orpheus-v1-english",
        groqTtsVoice: cfg.groqTtsVoice || "autumn",
        geminiTtsModel: cfg.geminiTtsModel || "gemini-2.5-flash-preview-tts",
        geminiTtsVoice: cfg.geminiTtsVoice || "Kore",
        edgeTtsModel: cfg.edgeTtsModel || "edge-tts",
        edgeTtsVoice: cfg.edgeTtsVoice || "en-US-AriaNeural",
        edgeTtsRate: cfg.edgeTtsRate ?? 1.0,
        kittenVoiceId: cfg.kittenVoiceId ?? 0,
        supertonicVoice: cfg.supertonicVoice || "M1",
        supertonicLanguage: cfg.supertonicLanguage || "en",
        supertonicSteps: cfg.supertonicSteps ?? 5,
        // acpx Agent list for agent selection
        acpxAgents: agentProfileService.getAll()
          .filter(p => p.connection.type === 'acpx' && p.enabled !== false)
          .map(p => ({ name: p.name, displayName: p.displayName })),
        // Session History (pinned/archived conversation IDs)
        pinnedSessionIds: Array.isArray(cfg.pinnedSessionIds) ? cfg.pinnedSessionIds.filter((id: unknown): id is string => typeof id === 'string') : [],
        archivedSessionIds: Array.isArray(cfg.archivedSessionIds) ? cfg.archivedSessionIds.filter((id: unknown): id is string => typeof id === 'string') : [],
      })
    } catch (error: any) {
      diagnosticsService.logError("remote-server", "Failed to get settings", error)
      return reply.code(500).send({ error: "Failed to get settings" })
    }
  })

  // PATCH /v1/settings - Update settings
  fastify.patch("/v1/settings", async (req, reply) => {
    let attemptedSensitiveSettingsKeys: string[] = []

    try {
      const body = req.body as any
      attemptedSensitiveSettingsKeys = body && typeof body === "object"
        ? getSensitiveOperatorSettingsKeys(body as Record<string, unknown>)
        : []
      const cfg = configStore.get()
      const updates: Partial<typeof cfg> = {}

      // Only allow updating specific settings
      if (typeof body.transcriptPostProcessingEnabled === "boolean") {
        updates.transcriptPostProcessingEnabled = body.transcriptPostProcessingEnabled
      }
      if (typeof body.mcpRequireApprovalBeforeToolCall === "boolean") {
        updates.mcpRequireApprovalBeforeToolCall = body.mcpRequireApprovalBeforeToolCall
      }
      if (typeof body.ttsEnabled === "boolean") {
        updates.ttsEnabled = body.ttsEnabled
      }
      if (typeof body.whatsappEnabled === "boolean") {
        updates.whatsappEnabled = body.whatsappEnabled
      }
      if (typeof body.discordEnabled === "boolean") {
        updates.discordEnabled = body.discordEnabled
      }
      if (typeof body.mcpMaxIterations === "number" && body.mcpMaxIterations >= 1 && body.mcpMaxIterations <= 100) {
        // Coerce to integer to avoid surprising iteration counts with floats
        updates.mcpMaxIterations = Math.floor(body.mcpMaxIterations)
      }
      // Agent model settings (agent* preferred; mcpTools* legacy aliases)
      const validProviders = ["openai", "groq", "gemini", "chatgpt-web"]
      const agentProviderId = typeof body.agentProviderId === "string" ? body.agentProviderId : body.mcpToolsProviderId
      if (typeof agentProviderId === "string" && validProviders.includes(agentProviderId)) {
        updates.agentProviderId = agentProviderId as "openai" | "groq" | "gemini" | "chatgpt-web"
        updates.mcpToolsProviderId = updates.agentProviderId
      }
      const agentOpenaiModel = typeof body.agentOpenaiModel === "string" ? body.agentOpenaiModel : body.mcpToolsOpenaiModel
      if (typeof agentOpenaiModel === "string") {
        updates.agentOpenaiModel = agentOpenaiModel
        updates.mcpToolsOpenaiModel = agentOpenaiModel
      }
      const agentGroqModel = typeof body.agentGroqModel === "string" ? body.agentGroqModel : body.mcpToolsGroqModel
      if (typeof agentGroqModel === "string") {
        updates.agentGroqModel = agentGroqModel
        updates.mcpToolsGroqModel = agentGroqModel
      }
      const agentGeminiModel = typeof body.agentGeminiModel === "string" ? body.agentGeminiModel : body.mcpToolsGeminiModel
      if (typeof agentGeminiModel === "string") {
        updates.agentGeminiModel = agentGeminiModel
        updates.mcpToolsGeminiModel = agentGeminiModel
      }
      const agentChatgptWebModel = typeof body.agentChatgptWebModel === "string" ? body.agentChatgptWebModel : body.mcpToolsChatgptWebModel
      if (typeof agentChatgptWebModel === "string") {
        updates.agentChatgptWebModel = agentChatgptWebModel
        updates.mcpToolsChatgptWebModel = agentChatgptWebModel
      }
      if (typeof body.openaiApiKey === "string" && body.openaiApiKey !== REMOTE_SERVER_SECRET_MASK) {
        updates.openaiApiKey = body.openaiApiKey.trim()
      }
      if (typeof body.openaiBaseUrl === "string") {
        updates.openaiBaseUrl = body.openaiBaseUrl.trim()
      }
      if (typeof body.groqApiKey === "string" && body.groqApiKey !== REMOTE_SERVER_SECRET_MASK) {
        updates.groqApiKey = body.groqApiKey.trim()
      }
      if (typeof body.groqBaseUrl === "string") {
        updates.groqBaseUrl = body.groqBaseUrl.trim()
      }
      if (typeof body.geminiApiKey === "string" && body.geminiApiKey !== REMOTE_SERVER_SECRET_MASK) {
        updates.geminiApiKey = body.geminiApiKey.trim()
      }
      if (typeof body.geminiBaseUrl === "string") {
        updates.geminiBaseUrl = body.geminiBaseUrl.trim()
      }
      if (typeof body.chatgptWebAccessToken === "string") {
        updates.chatgptWebAccessToken = body.chatgptWebAccessToken === REMOTE_SERVER_SECRET_MASK ? cfg.chatgptWebAccessToken : body.chatgptWebAccessToken.trim()
      }
      if (typeof body.chatgptWebSessionToken === "string") {
        updates.chatgptWebSessionToken = body.chatgptWebSessionToken === REMOTE_SERVER_SECRET_MASK ? cfg.chatgptWebSessionToken : body.chatgptWebSessionToken.trim()
      }
      if (typeof body.chatgptWebAccountId === "string") {
        updates.chatgptWebAccountId = body.chatgptWebAccountId
      }
      if (typeof body.chatgptWebBaseUrl === "string") {
        updates.chatgptWebBaseUrl = body.chatgptWebBaseUrl.trim()
      }
      if (typeof body.openaiReasoningEffort === "string" && ["none", "minimal", "low", "medium", "high", "xhigh"].includes(body.openaiReasoningEffort)) {
        updates.openaiReasoningEffort = body.openaiReasoningEffort as "none" | "minimal" | "low" | "medium" | "high" | "xhigh"
      }
      if (typeof body.codexTextVerbosity === "string" && ["low", "medium", "high"].includes(body.codexTextVerbosity)) {
        updates.codexTextVerbosity = body.codexTextVerbosity as "low" | "medium" | "high"
      }
      // OpenAI compatible preset - validate against known preset IDs
      if (typeof body.currentModelPresetId === "string") {
        const { presets } = await getMergedModelPresetState(cfg)
        const selectedPreset = presets.find((preset) => preset.id === body.currentModelPresetId)
        if (selectedPreset) {
          Object.assign(updates, getModelPresetActivationUpdates(selectedPreset))
        }
        // If preset ID is invalid, silently ignore to avoid breaking client
      }
      // Streamer Mode
      if (typeof body.streamerModeEnabled === "boolean") {
        updates.streamerModeEnabled = body.streamerModeEnabled
      }
      // Speech-to-Text
      if (typeof body.sttLanguage === "string") {
        updates.sttLanguage = body.sttLanguage
      }
      if (typeof body.transcriptionPreviewEnabled === "boolean") {
        updates.transcriptionPreviewEnabled = body.transcriptionPreviewEnabled
      }
      if (typeof body.transcriptPostProcessingPrompt === "string") {
        updates.transcriptPostProcessingPrompt = body.transcriptPostProcessingPrompt
      }
      // Text-to-Speech
      if (typeof body.ttsAutoPlay === "boolean") {
        updates.ttsAutoPlay = body.ttsAutoPlay
      }
      if (typeof body.ttsPreprocessingEnabled === "boolean") {
        updates.ttsPreprocessingEnabled = body.ttsPreprocessingEnabled
      }
      if (typeof body.ttsRemoveCodeBlocks === "boolean") {
        updates.ttsRemoveCodeBlocks = body.ttsRemoveCodeBlocks
      }
      if (typeof body.ttsRemoveUrls === "boolean") {
        updates.ttsRemoveUrls = body.ttsRemoveUrls
      }
      if (typeof body.ttsConvertMarkdown === "boolean") {
        updates.ttsConvertMarkdown = body.ttsConvertMarkdown
      }
      if (typeof body.ttsUseLLMPreprocessing === "boolean") {
        updates.ttsUseLLMPreprocessing = body.ttsUseLLMPreprocessing
      }
      // Agent settings
      const validAgentModes = ["api", "acpx"]
      if (typeof body.mainAgentMode === "string" && validAgentModes.includes(body.mainAgentMode)) {
        updates.mainAgentMode = body.mainAgentMode as "api" | "acpx"
      }
      if (typeof body.mcpMessageQueueEnabled === "boolean") {
        updates.mcpMessageQueueEnabled = body.mcpMessageQueueEnabled
      }
      if (typeof body.mcpAutoPasteEnabled === "boolean") {
        updates.mcpAutoPasteEnabled = body.mcpAutoPasteEnabled
      }
      if (
        typeof body.mcpAutoPasteDelay === "number"
        && Number.isFinite(body.mcpAutoPasteDelay)
        && body.mcpAutoPasteDelay >= 0
        && body.mcpAutoPasteDelay <= 60000
      ) {
        updates.mcpAutoPasteDelay = Math.floor(body.mcpAutoPasteDelay)
      }
      if (typeof body.mcpVerifyCompletionEnabled === "boolean") {
        updates.mcpVerifyCompletionEnabled = body.mcpVerifyCompletionEnabled
      }
      if (typeof body.mcpFinalSummaryEnabled === "boolean") {
        updates.mcpFinalSummaryEnabled = body.mcpFinalSummaryEnabled
      }

      if (typeof body.dualModelEnabled === "boolean") {
        updates.dualModelEnabled = body.dualModelEnabled
      }

      if (typeof body.mcpUnlimitedIterations === "boolean") {
        updates.mcpUnlimitedIterations = body.mcpUnlimitedIterations
      }
      // Tool Execution
      if (typeof body.mcpContextReductionEnabled === "boolean") {
        updates.mcpContextReductionEnabled = body.mcpContextReductionEnabled
      }
      if (typeof body.mcpToolResponseProcessingEnabled === "boolean") {
        updates.mcpToolResponseProcessingEnabled = body.mcpToolResponseProcessingEnabled
      }
      if (typeof body.mcpParallelToolExecution === "boolean") {
        updates.mcpParallelToolExecution = body.mcpParallelToolExecution
      }
      // Remote Server
      if (typeof body.remoteServerEnabled === "boolean") {
        updates.remoteServerEnabled = body.remoteServerEnabled
      }
      if (typeof body.remoteServerPort === "number" && Number.isInteger(body.remoteServerPort) && body.remoteServerPort >= 1 && body.remoteServerPort <= 65535) {
        updates.remoteServerPort = body.remoteServerPort
      }
      if (typeof body.remoteServerBindAddress === "string" && ["127.0.0.1", "0.0.0.0"].includes(body.remoteServerBindAddress)) {
        updates.remoteServerBindAddress = body.remoteServerBindAddress as "127.0.0.1" | "0.0.0.0"
      }
      if (typeof body.remoteServerApiKey === "string" && body.remoteServerApiKey !== REMOTE_SERVER_SECRET_MASK) {
        // Reject blank/whitespace-only keys: saving an empty string here would make
        // the onRequest auth hook reject all subsequent requests after the scheduled
        // restart, locking operators out until local config is repaired by hand.
        const trimmedKey = body.remoteServerApiKey.trim()
        if (trimmedKey.length > 0) {
          updates.remoteServerApiKey = trimmedKey
        }
      }
      if (typeof body.remoteServerLogLevel === "string" && ["error", "info", "debug"].includes(body.remoteServerLogLevel)) {
        updates.remoteServerLogLevel = body.remoteServerLogLevel as "error" | "info" | "debug"
      }
      if (Array.isArray(body.remoteServerCorsOrigins)) {
        updates.remoteServerCorsOrigins = body.remoteServerCorsOrigins
          .filter((value: unknown) => typeof value === "string")
          .map((value: string) => value.trim())
          .filter(Boolean)
      }
      if (Array.isArray(body.remoteServerOperatorAllowDeviceIds)) {
        updates.remoteServerOperatorAllowDeviceIds = sanitizeStringList(body.remoteServerOperatorAllowDeviceIds)
      }
      if (typeof body.remoteServerAutoShowPanel === "boolean") {
        updates.remoteServerAutoShowPanel = body.remoteServerAutoShowPanel
      }
      if (typeof body.remoteServerTerminalQrEnabled === "boolean") {
        updates.remoteServerTerminalQrEnabled = body.remoteServerTerminalQrEnabled
      }
      // Desktop shell / panel
      if (typeof body.hideDockIcon === "boolean") {
        updates.hideDockIcon = body.hideDockIcon
      }
      if (typeof body.launchAtLogin === "boolean") {
        updates.launchAtLogin = body.launchAtLogin
      }
      if (typeof body.themePreference === "string" && ["system", "light", "dark"].includes(body.themePreference)) {
        updates.themePreference = body.themePreference as "system" | "light" | "dark"
      }
      if (typeof body.textInputEnabled === "boolean") {
        updates.textInputEnabled = body.textInputEnabled
      }
      if (typeof body.panelPosition === "string" && ["top-right", "top-left", "bottom-right", "bottom-left", "custom"].includes(body.panelPosition)) {
        updates.panelPosition = body.panelPosition as "top-right" | "top-left" | "bottom-right" | "bottom-left" | "custom"
      }
      if (typeof body.panelDragEnabled === "boolean") {
        updates.panelDragEnabled = body.panelDragEnabled
      }
      if (typeof body.floatingPanelAutoShow === "boolean") {
        updates.floatingPanelAutoShow = body.floatingPanelAutoShow
      }
      if (typeof body.hidePanelWhenMainFocused === "boolean") {
        updates.hidePanelWhenMainFocused = body.hidePanelWhenMainFocused
      }
      // Desktop conversation storage
      if (typeof body.conversationsEnabled === "boolean") {
        updates.conversationsEnabled = body.conversationsEnabled
      }
      if (
        typeof body.maxConversationsToKeep === "number"
        && Number.isFinite(body.maxConversationsToKeep)
        && body.maxConversationsToKeep >= 1
        && body.maxConversationsToKeep <= 10000
      ) {
        updates.maxConversationsToKeep = Math.floor(body.maxConversationsToKeep)
      }
      if (typeof body.autoSaveConversations === "boolean") {
        updates.autoSaveConversations = body.autoSaveConversations
      }
      // Cloudflare Tunnel
      if (typeof body.cloudflareTunnelMode === "string" && ["quick", "named"].includes(body.cloudflareTunnelMode)) {
        updates.cloudflareTunnelMode = body.cloudflareTunnelMode as "quick" | "named"
      }
      if (typeof body.cloudflareTunnelAutoStart === "boolean") {
        updates.cloudflareTunnelAutoStart = body.cloudflareTunnelAutoStart
      }
      if (typeof body.cloudflareTunnelId === "string") {
        updates.cloudflareTunnelId = body.cloudflareTunnelId.trim()
      }
      if (typeof body.cloudflareTunnelName === "string") {
        updates.cloudflareTunnelName = body.cloudflareTunnelName.trim()
      }
      if (typeof body.cloudflareTunnelCredentialsPath === "string") {
        updates.cloudflareTunnelCredentialsPath = body.cloudflareTunnelCredentialsPath.trim()
      }
      if (typeof body.cloudflareTunnelHostname === "string") {
        updates.cloudflareTunnelHostname = body.cloudflareTunnelHostname.trim()
      }
      // WhatsApp (extended)
      if (Array.isArray(body.whatsappAllowFrom)) {
        updates.whatsappAllowFrom = sanitizeStringList(body.whatsappAllowFrom)
      }
      if (Array.isArray(body.whatsappOperatorAllowFrom)) {
        updates.whatsappOperatorAllowFrom = sanitizeStringList(body.whatsappOperatorAllowFrom)
      }
      if (typeof body.whatsappAutoReply === "boolean") {
        updates.whatsappAutoReply = body.whatsappAutoReply
      }
      if (typeof body.whatsappLogMessages === "boolean") {
        updates.whatsappLogMessages = body.whatsappLogMessages
      }
      // Discord (extended)
      if (typeof body.discordBotToken === "string" && body.discordBotToken !== DISCORD_SECRET_MASK) {
        updates.discordBotToken = body.discordBotToken
      }
      if (typeof body.discordDmEnabled === "boolean") {
        updates.discordDmEnabled = body.discordDmEnabled
      }
      if (typeof body.discordRequireMention === "boolean") {
        updates.discordRequireMention = body.discordRequireMention
      }
      if (Array.isArray(body.discordAllowUserIds)) {
        updates.discordAllowUserIds = sanitizeStringList(body.discordAllowUserIds)
      }
      if (Array.isArray(body.discordAllowGuildIds)) {
        updates.discordAllowGuildIds = sanitizeStringList(body.discordAllowGuildIds)
      }
      if (Array.isArray(body.discordAllowChannelIds)) {
        updates.discordAllowChannelIds = sanitizeStringList(body.discordAllowChannelIds)
      }
      if (Array.isArray(body.discordAllowRoleIds)) {
        updates.discordAllowRoleIds = sanitizeStringList(body.discordAllowRoleIds)
      }
      if (Array.isArray(body.discordDmAllowUserIds)) {
        updates.discordDmAllowUserIds = sanitizeStringList(body.discordDmAllowUserIds)
      }
      if (Array.isArray(body.discordOperatorAllowUserIds)) {
        updates.discordOperatorAllowUserIds = sanitizeStringList(body.discordOperatorAllowUserIds)
      }
      if (Array.isArray(body.discordOperatorAllowGuildIds)) {
        updates.discordOperatorAllowGuildIds = sanitizeStringList(body.discordOperatorAllowGuildIds)
      }
      if (Array.isArray(body.discordOperatorAllowChannelIds)) {
        updates.discordOperatorAllowChannelIds = sanitizeStringList(body.discordOperatorAllowChannelIds)
      }
      if (Array.isArray(body.discordOperatorAllowRoleIds)) {
        updates.discordOperatorAllowRoleIds = sanitizeStringList(body.discordOperatorAllowRoleIds)
      }
      if (typeof body.discordDefaultProfileId === "string") {
        updates.discordDefaultProfileId = body.discordDefaultProfileId
      }
      if (typeof body.discordLogMessages === "boolean") {
        updates.discordLogMessages = body.discordLogMessages
      }
      // Langfuse
      if (typeof body.langfuseEnabled === "boolean") {
        updates.langfuseEnabled = body.langfuseEnabled
      }
      if (typeof body.langfusePublicKey === "string") {
        updates.langfusePublicKey = body.langfusePublicKey
      }
      if (typeof body.langfuseSecretKey === "string" && body.langfuseSecretKey !== "••••••••") {
        updates.langfuseSecretKey = body.langfuseSecretKey
      }
      if (typeof body.langfuseBaseUrl === "string") {
        updates.langfuseBaseUrl = body.langfuseBaseUrl
      }
      if (typeof body.localTraceLoggingEnabled === "boolean") {
        updates.localTraceLoggingEnabled = body.localTraceLoggingEnabled
      }
      if (typeof body.localTraceLogPath === "string") {
        updates.localTraceLogPath = body.localTraceLogPath.trim() || undefined
      }
      // STT Provider
      const validSttProviders = ["openai", "groq", "parakeet"]
      if (typeof body.sttProviderId === "string" && validSttProviders.includes(body.sttProviderId)) {
        updates.sttProviderId = body.sttProviderId as "openai" | "groq" | "parakeet"
      }
      if (typeof body.openaiSttLanguage === "string") {
        updates.openaiSttLanguage = body.openaiSttLanguage || undefined
      }
      if (typeof body.openaiSttModel === "string") {
        updates.openaiSttModel = body.openaiSttModel
      }
      if (typeof body.groqSttLanguage === "string") {
        updates.groqSttLanguage = body.groqSttLanguage || undefined
      }
      if (typeof body.groqSttModel === "string") {
        updates.groqSttModel = body.groqSttModel
      }
      if (typeof body.groqSttPrompt === "string") {
        updates.groqSttPrompt = body.groqSttPrompt || undefined
      }
      if (typeof body.parakeetNumThreads === "number" && [1, 2, 4, 8].includes(body.parakeetNumThreads)) {
        updates.parakeetNumThreads = body.parakeetNumThreads as 1 | 2 | 4 | 8
      }
      // TTS Provider
      const validTtsProviders = ["openai", "groq", "gemini", "edge", "kitten", "supertonic"]
      if (typeof body.ttsProviderId === "string" && validTtsProviders.includes(body.ttsProviderId)) {
        updates.ttsProviderId = body.ttsProviderId as "openai" | "groq" | "gemini" | "edge" | "kitten" | "supertonic"
      }
      // Transcript Post-Processing Provider
      const validPostProcessingProviders = ["openai", "groq", "gemini", "chatgpt-web"]
      if (typeof body.transcriptPostProcessingProviderId === "string" && validPostProcessingProviders.includes(body.transcriptPostProcessingProviderId)) {
        updates.transcriptPostProcessingProviderId = body.transcriptPostProcessingProviderId as "openai" | "groq" | "gemini" | "chatgpt-web"
      }
      if (typeof body.transcriptPostProcessingOpenaiModel === "string") {
        updates.transcriptPostProcessingOpenaiModel = body.transcriptPostProcessingOpenaiModel
      }
      if (typeof body.transcriptPostProcessingGroqModel === "string") {
        updates.transcriptPostProcessingGroqModel = body.transcriptPostProcessingGroqModel
      }
      if (typeof body.transcriptPostProcessingGeminiModel === "string") {
        updates.transcriptPostProcessingGeminiModel = body.transcriptPostProcessingGeminiModel
      }
      if (typeof body.transcriptPostProcessingChatgptWebModel === "string") {
        updates.transcriptPostProcessingChatgptWebModel = body.transcriptPostProcessingChatgptWebModel
      }
      // acpx Agent settings
      if (typeof body.mainAgentName === "string") {
        updates.mainAgentName = body.mainAgentName
      }
      // OpenAI TTS settings
      if (typeof body.openaiTtsModel === "string") {
        updates.openaiTtsModel = body.openaiTtsModel as "gpt-4o-mini-tts" | "tts-1" | "tts-1-hd"
      }
      if (typeof body.openaiTtsVoice === "string") {
        updates.openaiTtsVoice = body.openaiTtsVoice as "alloy" | "echo" | "fable" | "onyx" | "nova" | "shimmer"
      }
      if (typeof body.openaiTtsSpeed === "number" && body.openaiTtsSpeed >= 0.25 && body.openaiTtsSpeed <= 4.0) {
        updates.openaiTtsSpeed = body.openaiTtsSpeed
      }
      // Groq TTS settings
      const validGroqTtsModels = ["canopylabs/orpheus-v1-english", "canopylabs/orpheus-arabic-saudi"] as const
      if (typeof body.groqTtsModel === "string" && validGroqTtsModels.includes(body.groqTtsModel as typeof validGroqTtsModels[number])) {
        updates.groqTtsModel = body.groqTtsModel as typeof validGroqTtsModels[number]
      }
      if (typeof body.groqTtsVoice === "string") {
        updates.groqTtsVoice = body.groqTtsVoice
      }
      // Gemini TTS settings
      const validGeminiTtsModels = ["gemini-2.5-flash-preview-tts", "gemini-2.5-pro-preview-tts"] as const
      if (typeof body.geminiTtsModel === "string" && validGeminiTtsModels.includes(body.geminiTtsModel as typeof validGeminiTtsModels[number])) {
        updates.geminiTtsModel = body.geminiTtsModel as typeof validGeminiTtsModels[number]
      }
      if (typeof body.geminiTtsVoice === "string") {
        updates.geminiTtsVoice = body.geminiTtsVoice
      }
      // Edge TTS settings
      if (typeof body.edgeTtsModel === "string") {
        updates.edgeTtsModel = body.edgeTtsModel as "edge-tts"
      }
      if (typeof body.edgeTtsVoice === "string") {
        updates.edgeTtsVoice = body.edgeTtsVoice
      }
      if (typeof body.edgeTtsRate === "number" && body.edgeTtsRate >= 0.5 && body.edgeTtsRate <= 2.0) {
        updates.edgeTtsRate = body.edgeTtsRate
      }
      if (typeof body.kittenVoiceId === "number" && Number.isInteger(body.kittenVoiceId) && body.kittenVoiceId >= 0 && body.kittenVoiceId <= 7) {
        updates.kittenVoiceId = body.kittenVoiceId as 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7
      }
      if (typeof body.supertonicVoice === "string") {
        updates.supertonicVoice = body.supertonicVoice as "M1" | "M2" | "M3" | "M4" | "M5" | "F1" | "F2" | "F3" | "F4" | "F5"
      }
      if (typeof body.supertonicLanguage === "string" && ["en", "ko", "es", "pt", "fr"].includes(body.supertonicLanguage)) {
        updates.supertonicLanguage = body.supertonicLanguage as "en" | "ko" | "es" | "pt" | "fr"
      }
      if (typeof body.supertonicSteps === "number" && Number.isInteger(body.supertonicSteps) && body.supertonicSteps >= 2 && body.supertonicSteps <= 10) {
        updates.supertonicSteps = body.supertonicSteps
      }

      // Session History (pinned/archived conversation IDs)
      if (Array.isArray(body.pinnedSessionIds) && body.pinnedSessionIds.every((id: unknown) => typeof id === "string")) {
        updates.pinnedSessionIds = body.pinnedSessionIds
      }
      if (Array.isArray(body.archivedSessionIds) && body.archivedSessionIds.every((id: unknown) => typeof id === "string")) {
        updates.archivedSessionIds = body.archivedSessionIds
      }

      // Predefined Prompts
      if (Array.isArray(body.predefinedPrompts)) {
        updates.predefinedPrompts = body.predefinedPrompts
      }

      if (Object.keys(updates).length === 0) {
        if (attemptedSensitiveSettingsKeys.length > 0) {
          recordOperatorAuditEvent(req, {
            action: "settings-sensitive-update",
            path: "/v1/settings",
            success: false,
            details: { attempted: attemptedSensitiveSettingsKeys },
            failureReason: "no-valid-settings-to-update",
          })
        }
        return reply.code(400).send({ error: "No valid settings to update" })
      }

      const nextConfig = { ...cfg, ...updates }
      const remoteServerLifecycleAction = getRemoteServerLifecycleAction(cfg, nextConfig)
      const sensitiveUpdatedKeys = Object.keys(updates).filter((key) => SENSITIVE_OPERATOR_SETTINGS_KEYS.has(key))
      configStore.save(nextConfig)
      diagnosticsService.logInfo("remote-server", `Updated settings: ${Object.keys(updates).join(", ")}`)

      if (updates.launchAtLogin !== undefined) {
        try {
          app.setLoginItemSettings({
            openAtLogin: !!updates.launchAtLogin,
            openAsHidden: true,
          })
        } catch (error) {
          diagnosticsService.logError("remote-server", "Failed to update launch-at-login from mobile settings", error)
        }
      }

      if (updates.hideDockIcon !== undefined && process.platform === "darwin") {
        try {
          if (updates.hideDockIcon && !WINDOWS.get("main")?.isVisible()) {
            app.setActivationPolicy("accessory")
            app.dock?.hide()
          } else {
            app.dock?.show()
            app.setActivationPolicy("regular")
          }
        } catch (error) {
          diagnosticsService.logError("remote-server", "Failed to update dock visibility from mobile settings", error)
        }
      }

      if (updates.themePreference !== undefined) {
        for (const windowId of ["main", "panel"] as const) {
          try {
            getWindowRendererHandlers(windowId)?.themeChanged?.send(updates.themePreference)
          } catch (error) {
            diagnosticsService.logError("remote-server", `Failed to notify ${windowId} window of theme update`, error)
          }
        }
      }

      const discordLifecycleAction = getDiscordLifecycleAction(cfg, nextConfig)
      if (discordLifecycleAction === "start") {
        await discordService.start()
      } else if (discordLifecycleAction === "restart") {
        await discordService.restart()
      } else if (discordLifecycleAction === "stop") {
        await discordService.stop()
      }

      // Trigger WhatsApp MCP server lifecycle if whatsappEnabled changed
      if (updates.whatsappEnabled !== undefined) {
        try {
          const prevEnabled = cfg.whatsappEnabled ?? false
          await handleWhatsAppToggle(prevEnabled, updates.whatsappEnabled)
        } catch (_e) {
          // lifecycle is best-effort
        }
      }

      scheduleRemoteServerLifecycleActionAfterReply(reply, remoteServerLifecycleAction)

      if (sensitiveUpdatedKeys.length > 0) {
        recordOperatorAuditEvent(req, {
          action: "settings-sensitive-update",
          path: "/v1/settings",
          success: true,
          details: {
            updated: sensitiveUpdatedKeys,
            ...(remoteServerLifecycleAction !== "noop" ? { remoteServerLifecycleAction } : {}),
            ...(discordLifecycleAction !== "noop" ? { discordLifecycleAction } : {}),
          },
        })
      }

      return reply.send({
        success: true,
        updated: Object.keys(updates),
      })
    } catch (error: any) {
      if (attemptedSensitiveSettingsKeys.length > 0) {
        recordOperatorAuditEvent(req, {
          action: "settings-sensitive-update",
          path: "/v1/settings",
          success: false,
          details: { attempted: attemptedSensitiveSettingsKeys },
          failureReason: "settings-update-error",
        })
      }
      diagnosticsService.logError("remote-server", "Failed to update settings", error)
      return reply.code(500).send({ error: error?.message || "Failed to update settings" })
    }
  })

  // ============================================
  // Conversation Recovery Endpoints (for mobile app)
  // ============================================

  // GET /v1/conversations/:id - Fetch conversation state for recovery
  fastify.get("/v1/conversations/:id", async (req, reply) => {
    try {
      const params = req.params as { id: string }
      const conversationId = params.id

      if (!conversationId || typeof conversationId !== "string") {
        return reply.code(400).send({ error: "Missing or invalid conversation ID" })
      }

      // Validate conversation ID format to prevent path traversal attacks
      const conversationIdError = getConversationIdValidationError(conversationId)
      if (conversationIdError) {
        return reply.code(400).send({ error: conversationIdError })
      }

      const conversation = await conversationService.loadConversation(conversationId)

      if (!conversation) {
        return reply.code(404).send({ error: "Conversation not found" })
      }

      diagnosticsService.logInfo("remote-server", `Fetched conversation ${conversationId} for recovery`)

      return reply.send({
        id: conversation.id,
        title: conversation.title,
        createdAt: conversation.createdAt,
        updatedAt: conversationService.getConversationActivityTimestamp(conversation),
        messages: conversation.messages.map(msg => ({
          id: msg.id,
          role: msg.role,
          content: msg.content,
          timestamp: msg.timestamp,
          toolCalls: msg.toolCalls,
          toolResults: msg.toolResults,
        })),
        metadata: conversation.metadata,
      })
    } catch (error: any) {
      diagnosticsService.logError("remote-server", "Failed to fetch conversation", error)
      return reply.code(500).send({ error: error?.message || "Failed to fetch conversation" })
    }
  })

  // GET /v1/conversations/:id/assets/images/:fileName - Stream conversation image asset for mobile
  fastify.get("/v1/conversations/:id/assets/images/:fileName", async (req, reply) => {
    try {
      const params = req.params as { id: string; fileName: string }
      const conversationId = params.id
      const fileName = params.fileName

      const conversationIdError = getConversationIdValidationError(conversationId)
      if (conversationIdError) {
        return reply.code(400).send({ error: conversationIdError })
      }

      let assetPath: string
      try {
        assetPath = getConversationImageAssetPath(conversationId, fileName)
      } catch (error) {
        return reply.code(400).send({ error: error instanceof Error ? error.message : "Invalid image asset" })
      }

      const stat = await fs.promises.stat(assetPath)
      if (!stat.isFile() || stat.size <= 0) {
        return reply.code(404).send({ error: "Image asset not found" })
      }

      reply.header("Content-Type", getConversationImageMimeTypeFromFileName(fileName))
      reply.header("Content-Length", String(stat.size))
      return reply.send(fs.createReadStream(assetPath))
    } catch (error: any) {
      if (error?.code === "ENOENT") {
        return reply.code(404).send({ error: "Image asset not found" })
      }
      diagnosticsService.logError("remote-server", "Failed to stream conversation image asset", error)
      return reply.code(500).send({ error: error?.message || "Failed to stream image asset" })
    }
  })

  // GET /v1/conversations/:id/assets/videos/:fileName - Stream conversation video asset for mobile
  fastify.get("/v1/conversations/:id/assets/videos/:fileName", async (req, reply) => {
    try {
      const params = req.params as { id: string; fileName: string }
      const conversationId = params.id
      const fileName = params.fileName

      const conversationIdError = getConversationIdValidationError(conversationId)
      if (conversationIdError) {
        return reply.code(400).send({ error: conversationIdError })
      }

      let assetPath: string
      try {
        assetPath = getConversationVideoAssetPath(conversationId, fileName)
      } catch (error) {
        return reply.code(400).send({ error: error instanceof Error ? error.message : "Invalid video asset" })
      }

      const stat = await fs.promises.stat(assetPath)
      if (!stat.isFile() || stat.size <= 0) {
        return reply.code(404).send({ error: "Video asset not found" })
      }

      const contentType = getConversationVideoMimeTypeFromFileName(fileName)
      const range = req.headers.range
      reply.header("Accept-Ranges", "bytes")
      reply.header("Content-Type", contentType)

      if (range) {
        const match = range.match(/^bytes=(\d*)-(\d*)$/)
        if (!match) {
          return reply.code(416).header("Content-Range", `bytes */${stat.size}`).send()
        }

        if (!match[1] && !match[2]) {
          return reply.code(416).header("Content-Range", `bytes */${stat.size}`).send()
        }

        const suffixLength = !match[1] && match[2] ? Number.parseInt(match[2], 10) : null
        const start = suffixLength !== null
          ? Math.max(stat.size - suffixLength, 0)
          : Number.parseInt(match[1], 10)
        const end = suffixLength !== null
          ? stat.size - 1
          : match[2] ? Number.parseInt(match[2], 10) : stat.size - 1
        if (!Number.isFinite(start) || !Number.isFinite(end) || start < 0 || end < start || start >= stat.size) {
          return reply.code(416).header("Content-Range", `bytes */${stat.size}`).send()
        }

        const boundedEnd = Math.min(end, stat.size - 1)
        const chunkSize = boundedEnd - start + 1
        reply.code(206)
        reply.header("Content-Range", `bytes ${start}-${boundedEnd}/${stat.size}`)
        reply.header("Content-Length", String(chunkSize))
        return reply.send(fs.createReadStream(assetPath, { start, end: boundedEnd }))
      }

      reply.header("Content-Length", String(stat.size))
      return reply.send(fs.createReadStream(assetPath))
    } catch (error: any) {
      if (error?.code === "ENOENT") {
        return reply.code(404).send({ error: "Video asset not found" })
      }
      diagnosticsService.logError("remote-server", "Failed to stream conversation video asset", error)
      return reply.code(500).send({ error: error?.message || "Failed to stream video asset" })
    }
  })

  // ============================================
  // Text-to-Speech Endpoints (for mobile app)
  // ============================================

  // POST /v1/tts/speak - Synthesize speech via the desktop's TTS providers.
  // Accepts { text, providerId?, voice?, model?, speed? } and returns raw
  // audio bytes with the appropriate Content-Type. Exists so the mobile
  // web/native clients can play Edge/OpenAI/Groq/etc. TTS without having to
  // embed provider keys or work around browser WebSocket header restrictions.
  fastify.post("/v1/tts/speak", async (req, reply) => {
    try {
      const body = (req.body ?? {}) as {
        text?: unknown
        providerId?: unknown
        voice?: unknown
        model?: unknown
        speed?: unknown
      }

      if (typeof body.text !== "string" || !body.text.trim()) {
        return reply.code(400).send({ error: "Missing or invalid 'text'" })
      }
      if (Buffer.byteLength(body.text, "utf8") > 32 * 1024) {
        return reply.code(413).send({ error: "Text too large (max 32 KB)" })
      }

      const result = await generateTTS(
        {
          text: body.text,
          providerId: typeof body.providerId === "string" ? body.providerId : undefined,
          voice: typeof body.voice === "string" ? body.voice : undefined,
          model: typeof body.model === "string" ? body.model : undefined,
          speed: typeof body.speed === "number" ? body.speed : undefined,
        },
        configStore.get(),
      )

      return reply
        .code(200)
        .header("Content-Type", result.mimeType)
        .header("X-TTS-Provider", result.provider)
        .send(Buffer.from(result.audio))
    } catch (error: any) {
      diagnosticsService.logError("remote-server", "TTS request failed", error)
      const message = error?.message || "TTS generation failed"
      // 400 for known user-facing validation errors, 502 for upstream/provider errors.
      const code = /not enabled|validation failed|Unsupported TTS provider|API key is required/i.test(message) ? 400 : 502
      return reply.code(code).send({ error: message })
    }
  })

  // ============================================
  // Push Notification Endpoints (for mobile app)
  // ============================================

  // POST /v1/push/register - Register a push notification token
  fastify.post("/v1/push/register", async (req, reply) => {
    try {
      const body = req.body as { token?: string; type?: string; platform?: string; deviceId?: string }

      if (!body.token || typeof body.token !== "string") {
        return reply.code(400).send({ error: "Missing or invalid token" })
      }

      if (!body.platform || !["ios", "android"].includes(body.platform)) {
        return reply.code(400).send({ error: "Invalid platform. Must be 'ios' or 'android'" })
      }

      const cfg = configStore.get()
      const existingTokens = cfg.pushNotificationTokens || []

      // Check if token already exists
      const existingIndex = existingTokens.findIndex(t => t.token === body.token)
      const newToken = {
        token: body.token,
        type: "expo" as const,
        platform: body.platform as "ios" | "android",
        registeredAt: Date.now(),
        deviceId: body.deviceId,
      }

      let updatedTokens: typeof existingTokens
      if (existingIndex >= 0) {
        // Update existing token
        updatedTokens = [...existingTokens]
        updatedTokens[existingIndex] = newToken
        diagnosticsService.logInfo("remote-server", `Updated push notification token for ${body.platform}`)
      } else {
        // Add new token
        updatedTokens = [...existingTokens, newToken]
        diagnosticsService.logInfo("remote-server", `Registered new push notification token for ${body.platform}`)
      }

      configStore.save({ ...cfg, pushNotificationTokens: updatedTokens })

      return reply.send({
        success: true,
        message: existingIndex >= 0 ? "Token updated" : "Token registered",
        tokenCount: updatedTokens.length,
      })
    } catch (error: any) {
      diagnosticsService.logError("remote-server", "Failed to register push token", error)
      return reply.code(500).send({ error: error?.message || "Failed to register push token" })
    }
  })

  // POST /v1/push/unregister - Unregister a push notification token
  fastify.post("/v1/push/unregister", async (req, reply) => {
    try {
      const body = req.body as { token?: string }

      if (!body.token || typeof body.token !== "string") {
        return reply.code(400).send({ error: "Missing or invalid token" })
      }

      const cfg = configStore.get()
      const existingTokens = cfg.pushNotificationTokens || []

      const filteredTokens = existingTokens.filter(t => t.token !== body.token)
      const removed = existingTokens.length > filteredTokens.length

      if (removed) {
        configStore.save({ ...cfg, pushNotificationTokens: filteredTokens })
        diagnosticsService.logInfo("remote-server", "Unregistered push notification token")
      }

      return reply.send({
        success: true,
        message: removed ? "Token unregistered" : "Token not found",
        tokenCount: filteredTokens.length,
      })
    } catch (error: any) {
      diagnosticsService.logError("remote-server", "Failed to unregister push token", error)
      return reply.code(500).send({ error: error?.message || "Failed to unregister push token" })
    }
  })

  // GET /v1/push/status - Get push notification status
  fastify.get("/v1/push/status", async (_req, reply) => {
    try {
      const cfg = configStore.get()
      const tokens = cfg.pushNotificationTokens || []

      return reply.send({
        enabled: tokens.length > 0,
        tokenCount: tokens.length,
        platforms: [...new Set(tokens.map(t => t.platform))],
      })
    } catch (error: any) {
      diagnosticsService.logError("remote-server", "Failed to get push status", error)
      return reply.code(500).send({ error: error?.message || "Failed to get push status" })
    }
  })

  // POST /v1/push/clear-badge - Clear badge count for a token (called when mobile app opens)
  fastify.post("/v1/push/clear-badge", async (req, reply) => {
    try {
      const body = req.body as { token?: string }

      if (!body.token || typeof body.token !== "string") {
        return reply.code(400).send({ error: "Missing or invalid token" })
      }

      clearBadgeCount(body.token)

      return reply.send({ success: true })
    } catch (error: any) {
      diagnosticsService.logError("remote-server", "Failed to clear badge count", error)
      return reply.code(500).send({ error: error?.message || "Failed to clear badge count" })
    }
  })

  // Helper function to validate message objects
  const validateMessages = (messages: Array<{ role: string; content: unknown }>): string | null => {
    const validRoles = ["user", "assistant", "tool"]
    for (let i = 0; i < messages.length; i++) {
      const msg = messages[i]
      if (msg === null || msg === undefined || typeof msg !== "object") {
        return `Invalid message ${i}: expected an object`
      }
      if (!msg.role || !validRoles.includes(msg.role)) {
        return `Invalid role in message ${i}: expected one of ${validRoles.join(", ")}`
      }
      if (typeof msg.content !== "string") {
        return `Invalid content in message ${i}: expected string`
      }
    }
    return null
  }

  // GET /v1/conversations - List all conversations
  fastify.get("/v1/conversations", async (_req, reply) => {
    try {
      const conversations = await conversationService.getConversationHistory()
      diagnosticsService.logInfo("remote-server", `Listed ${conversations.length} conversations`)
      return reply.send({ conversations })
    } catch (error: any) {
      diagnosticsService.logError("remote-server", "Failed to list conversations", error)
      return reply.code(500).send({ error: error?.message || "Failed to list conversations" })
    }
  })

  // POST /v1/conversations - Create a new conversation from mobile data
  fastify.post("/v1/conversations", async (req, reply) => {
    try {
      // Validate request body is a valid object
      if (!req.body || typeof req.body !== "object" || Array.isArray(req.body)) {
        return reply.code(400).send({ error: "Request body must be a JSON object" })
      }

      const body = req.body as {
        title?: string
        messages: Array<{
          role: "user" | "assistant" | "tool"
          content: string
          timestamp?: number
          toolCalls?: Array<{ name: string; arguments: any }>
          toolResults?: Array<{ success: boolean; content: string; error?: string }>
        }>
        createdAt?: number
        updatedAt?: number
      }

      if (!body.messages || !Array.isArray(body.messages) || body.messages.length === 0) {
        return reply.code(400).send({ error: "Missing or invalid messages array" })
      }

      // Validate each message object
      const validationError = validateMessages(body.messages)
      if (validationError) {
        return reply.code(400).send({ error: validationError })
      }

      const conversationId = conversationService.generateConversationIdPublic()
      const now = Date.now()

      // Generate title from first message if not provided
      const firstMessageContent = body.messages[0]?.content || ""
      const title = body.title || (firstMessageContent.length > 50
        ? `${firstMessageContent.slice(0, 50)}...`
        : firstMessageContent || "New Conversation")

      // Convert input messages to ConversationMessage format with IDs
      const messages = body.messages.map((msg, index) => ({
        id: `msg_${now}_${index}_${Math.random().toString(36).substr(2, 9)}`,
        role: msg.role,
        content: msg.content,
        timestamp: msg.timestamp ?? now,
        toolCalls: msg.toolCalls,
        toolResults: msg.toolResults,
      }))

      const conversation = {
        id: conversationId,
        title,
        createdAt: body.createdAt ?? now,
        updatedAt: body.updatedAt ?? now,
        messages,
      }

      await conversationService.saveConversation(conversation, true)
      diagnosticsService.logInfo("remote-server", `Created conversation ${conversationId} with ${messages.length} messages`)

      // Notify renderer that conversation history has changed (for sidebar refresh)
      notifyConversationHistoryChanged()

      return reply.code(201).send({
        id: conversation.id,
        title: conversation.title,
        createdAt: conversation.createdAt,
        updatedAt: conversationService.getConversationActivityTimestamp(conversation),
        messages: conversation.messages.map(msg => ({
          id: msg.id,
          role: msg.role,
          content: msg.content,
          timestamp: msg.timestamp,
          toolCalls: msg.toolCalls,
          toolResults: msg.toolResults,
        })),
      })
    } catch (error: any) {
      diagnosticsService.logError("remote-server", "Failed to create conversation", error)
      return reply.code(500).send({ error: error?.message || "Failed to create conversation" })
    }
  })

  // PUT /v1/conversations/:id - Update an existing conversation
  fastify.put("/v1/conversations/:id", async (req, reply) => {
    try {
      const params = req.params as { id: string }
      const conversationId = params.id

      if (!conversationId || typeof conversationId !== "string") {
        return reply.code(400).send({ error: "Missing or invalid conversation ID" })
      }

      // Validate conversation ID format to prevent path traversal attacks
      const conversationIdError = getConversationIdValidationError(conversationId)
      if (conversationIdError) {
        return reply.code(400).send({ error: conversationIdError })
      }

      // Validate request body is a valid object
      if (!req.body || typeof req.body !== "object" || Array.isArray(req.body)) {
        return reply.code(400).send({ error: "Request body must be a JSON object" })
      }

      const body = req.body as {
        title?: string
        messages?: Array<{
          role: "user" | "assistant" | "tool"
          content: string
          timestamp?: number
          toolCalls?: Array<{ name: string; arguments: any }>
          toolResults?: Array<{ success: boolean; content: string; error?: string }>
        }>
        updatedAt?: number
      }

      const now = Date.now()
      let conversation = await conversationService.loadConversation(conversationId)

      if (!conversation) {
        // Create new conversation with the provided ID
        if (!body.messages || !Array.isArray(body.messages) || body.messages.length === 0) {
          return reply.code(400).send({ error: "Conversation not found and no messages provided to create it" })
        }

        // Validate each message object
        const validationError = validateMessages(body.messages)
        if (validationError) {
          return reply.code(400).send({ error: validationError })
        }

        const firstMessageContent = body.messages[0]?.content || ""
        const title = body.title || (firstMessageContent.length > 50
          ? `${firstMessageContent.slice(0, 50)}...`
          : firstMessageContent || "New Conversation")

        const messages = body.messages.map((msg, index) => ({
          id: `msg_${now}_${index}_${Math.random().toString(36).substr(2, 9)}`,
          role: msg.role,
          content: msg.content,
          timestamp: msg.timestamp ?? now,
          toolCalls: msg.toolCalls,
          toolResults: msg.toolResults,
        }))

        conversation = {
          id: conversationId,
          title,
          createdAt: now,
          updatedAt: body.updatedAt ?? now,
          messages,
        }

        await conversationService.saveConversation(conversation, true)
        diagnosticsService.logInfo("remote-server", `Created conversation ${conversationId} via PUT with ${messages.length} messages`)
      } else {
        // Update existing conversation
        if (body.title !== undefined) {
          conversation.title = body.title
        }

        if (body.messages !== undefined && !Array.isArray(body.messages)) {
          return reply.code(400).send({ error: "messages field must be an array" })
        }

        if (body.messages && Array.isArray(body.messages)) {
          // Validate each message object
          const validationError = validateMessages(body.messages)
          if (validationError) {
            return reply.code(400).send({ error: validationError })
          }

          conversation.messages = body.messages.map((msg, index) => ({
            id: `msg_${now}_${index}_${Math.random().toString(36).substr(2, 9)}`,
            role: msg.role,
            content: msg.content,
            timestamp: msg.timestamp ?? now,
            toolCalls: msg.toolCalls,
            toolResults: msg.toolResults,
          }))
        }

        conversation.updatedAt = body.updatedAt ?? now

        await conversationService.saveConversation(conversation, true)
        diagnosticsService.logInfo("remote-server", `Updated conversation ${conversationId}`)
      }

      // Notify renderer that conversation history has changed (for sidebar refresh)
      notifyConversationHistoryChanged()

      return reply.send({
        id: conversation.id,
        title: conversation.title,
        createdAt: conversation.createdAt,
        updatedAt: conversationService.getConversationActivityTimestamp(conversation),
        messages: conversation.messages.map(msg => ({
          id: msg.id,
          role: msg.role,
          content: msg.content,
          timestamp: msg.timestamp,
          toolCalls: msg.toolCalls,
          toolResults: msg.toolResults,
        })),
      })
    } catch (error: any) {
      diagnosticsService.logError("remote-server", "Failed to update conversation", error)
      return reply.code(500).send({ error: error?.message || "Failed to update conversation" })
    }
  })

  // POST /v1/conversations/:id/branch - Branch an existing conversation at a message index
  fastify.post("/v1/conversations/:id/branch", async (req, reply) => {
    try {
      const params = req.params as { id: string }
      const conversationId = params.id

      if (!conversationId || typeof conversationId !== "string") {
        return reply.code(400).send({ error: "Missing or invalid conversation ID" })
      }

      const conversationIdError = getConversationIdValidationError(conversationId)
      if (conversationIdError) {
        return reply.code(400).send({ error: conversationIdError })
      }

      if (!req.body || typeof req.body !== "object" || Array.isArray(req.body)) {
        return reply.code(400).send({ error: "Request body must be a JSON object" })
      }

      const body = req.body as { messageIndex?: unknown }
      if (typeof body.messageIndex !== "number" || !Number.isInteger(body.messageIndex) || body.messageIndex < 0) {
        return reply.code(400).send({ error: "Missing or invalid messageIndex" })
      }

      const conversation = await conversationService.branchConversation(conversationId, body.messageIndex)
      if (!conversation) {
        return reply.code(404).send({ error: "Conversation or message not found" })
      }

      notifyConversationHistoryChanged()

      return reply.send({
        id: conversation.id,
        title: conversation.title,
        createdAt: conversation.createdAt,
        updatedAt: conversationService.getConversationActivityTimestamp(conversation),
        messages: conversation.messages.map(msg => ({
          id: msg.id,
          role: msg.role,
          content: msg.content,
          timestamp: msg.timestamp,
          toolCalls: msg.toolCalls,
          toolResults: msg.toolResults,
        })),
        metadata: conversation.metadata,
      })
    } catch (error: any) {
      diagnosticsService.logError("remote-server", "Failed to branch conversation", error)
      return reply.code(500).send({ error: error?.message || "Failed to branch conversation" })
    }
  })

  // DELETE /v1/conversations/:id - Delete a linked desktop conversation
  fastify.delete("/v1/conversations/:id", async (req, reply) => {
    try {
      const params = req.params as { id: string }
      const conversationId = params.id

      if (!conversationId || typeof conversationId !== "string") {
        return reply.code(400).send({ error: "Missing or invalid conversation ID" })
      }

      const conversationIdError = getConversationIdValidationError(conversationId)
      if (conversationIdError) {
        return reply.code(400).send({ error: conversationIdError })
      }

      await conversationService.deleteConversation(conversationId)
      notifyConversationHistoryChanged()

      return reply.send({ success: true, id: conversationId })
    } catch (error: any) {
      diagnosticsService.logError("remote-server", "Failed to delete conversation", error)
      return reply.code(500).send({ error: error?.message || "Failed to delete conversation" })
    }
  })

  // DELETE /v1/conversations - Delete all desktop conversations
  fastify.delete("/v1/conversations", async (_req, reply) => {
    try {
      await conversationService.deleteAllConversations()
      notifyConversationHistoryChanged()

      return reply.send({ success: true })
    } catch (error: any) {
      diagnosticsService.logError("remote-server", "Failed to delete all conversations", error)
      return reply.code(500).send({ error: error?.message || "Failed to delete all conversations" })
    }
  })

  // Kill switch endpoint - emergency stop all agent sessions
  fastify.post("/v1/emergency-stop", async (_req, reply) => {
    console.log("[KILLSWITCH] /v1/emergency-stop endpoint called")
    try {
      console.log("[KILLSWITCH] Loading emergency-stop module...")
      diagnosticsService.logInfo("remote-server", "Emergency stop triggered via API")

      console.log("[KILLSWITCH] Calling emergencyStopAll()...")
      const { before, after } = await emergencyStopAll()

      console.log(`[KILLSWITCH] Emergency stop completed. Killed ${before} processes. Remaining: ${after}`)
      diagnosticsService.logInfo(
        "remote-server",
        `Emergency stop completed. Killed ${before} processes. Remaining: ${after}`,
      )

      return reply.send({
        success: true,
        message: "Emergency stop executed",
        processesKilled: before,
        processesRemaining: after,
      })
    } catch (error: any) {
      console.error("[KILLSWITCH] Error during emergency stop:", error)
      diagnosticsService.logError("remote-server", "Emergency stop error", error)
      return reply.code(500).send({
        success: false,
        error: error?.message || "Emergency stop failed",
      })
    }
  })

  // MCP Protocol Endpoints - Expose DotAgents runtime tools to external agents
  // Support both Streamable HTTP MCP at /mcp/:acpSessionToken and legacy /tools/list,/tools/call shims.

  const listInjectedMcpTools = async (
    acpSessionToken: string | undefined,
    reply: any,
  ) => {
    try {
      const injectedRuntimeTools = getInjectedRuntimeToolsForAcpSession(acpSessionToken)
      if (!injectedRuntimeTools) {
        diagnosticsService.logWarning("remote-server", "Denied injected MCP tools/list request without valid ACP session context")
        return reply.code(401).send({ error: INVALID_ACP_SESSION_CONTEXT_ERROR })
      }

      return reply.send({ tools: injectedRuntimeTools.tools })
    } catch (error: any) {
      diagnosticsService.logError("remote-server", "MCP tools/list error", error)
      return reply.code(500).send({ error: error?.message || "Failed to list tools" })
    }
  }

  const callInjectedMcpTool = async (
    req: any,
    reply: any,
    acpSessionToken: string | undefined,
  ) => {
    try {
      const injectedRuntimeTools = getInjectedRuntimeToolsForAcpSession(acpSessionToken)
      if (!injectedRuntimeTools) {
        diagnosticsService.logWarning("remote-server", "Denied injected MCP tools/call request without valid ACP session context")
        return reply.code(401).send({ error: INVALID_ACP_SESSION_CONTEXT_ERROR })
      }

      const body = req.body as any
      const { name, arguments: args } = body

      if (!name || typeof name !== "string") {
        return reply.code(400).send({ error: "Missing or invalid 'name' parameter" })
      }

      if (!injectedRuntimeTools.tools.some((tool) => tool.name === name)) {
        return reply.code(400).send({ error: `Unknown runtime tool: ${name}` })
      }

      const result = await mcpService.executeToolCall(
        { name, arguments: args || {} } as any,
        undefined,
        false,
        injectedRuntimeTools.requestContext.appSessionId,
        injectedRuntimeTools.requestContext.profileSnapshot.mcpServerConfig,
      )

      if (!result) {
        return reply.code(500).send({ error: "Tool execution returned null" })
      }

      return reply.send({
        content: result.content,
        isError: result.isError,
      })
    } catch (error: any) {
      diagnosticsService.logError("remote-server", "MCP tools/call error", error)
      return reply.code(500).send({
        content: [{ type: "text", text: error?.message || "Tool execution failed" }],
        isError: true,
      })
    }
  }

  const handleInjectedMcpProtocolRequest = async (
    req: any,
    reply: any,
    acpSessionToken: string | undefined,
  ) => {
    const token = acpSessionToken?.trim()
    if (!token) {
      return reply.code(400).send({ error: "Missing ACP session token" })
    }

    const injectedRuntimeTools = getInjectedRuntimeToolsForAcpSession(token)
    if (!injectedRuntimeTools) {
      diagnosticsService.logWarning("remote-server", `Denied injected MCP ${req.method} request without valid ACP session context`)
      return reply.code(401).send({ error: INVALID_ACP_SESSION_CONTEXT_ERROR })
    }

    const rawSessionId = req.headers["mcp-session-id"]
    const sessionId = Array.isArray(rawSessionId) ? rawSessionId[0] : rawSessionId
    const sessions = getInjectedMcpTransportSessionMap(token)

    try {
      if (req.method === "POST") {
        let sessionState = sessionId ? sessions.get(sessionId) : undefined

        if (!sessionState) {
          if (sessionId) {
            return reply.code(404).send({
              jsonrpc: "2.0",
              error: { code: -32001, message: "Invalid MCP session ID" },
              id: null,
            })
          }

          if (!isInitializeRequest(req.body)) {
            return reply.code(400).send({
              jsonrpc: "2.0",
              error: { code: -32000, message: "Bad Request: No valid session ID provided" },
              id: null,
            })
          }

          const mcpServer = createInjectedMcpServer(token)
          const transport = new StreamableHTTPServerTransport({
            sessionIdGenerator: () => crypto.randomUUID(),
            enableJsonResponse: true,
            onsessioninitialized: (createdSessionId) => {
              getInjectedMcpTransportSessionMap(token).set(createdSessionId, {
                server: mcpServer,
                transport,
              })
            },
          })

          transport.onclose = async () => {
            if (transport.sessionId) {
              sessions.delete(transport.sessionId)
            }
            if (sessions.size === 0) {
              injectedMcpTransportsByToken.delete(token)
            }
            await mcpServer.close().catch(() => undefined)
          }

          await mcpServer.connect(transport)
          reply.hijack()
          await transport.handleRequest(req.raw, reply.raw, req.body)
          return reply
        }

        reply.hijack()
        await sessionState.transport.handleRequest(req.raw, reply.raw, req.body)
        return reply
      }

      if (!sessionId) {
        return reply.code(400).send({ error: "Missing MCP session ID" })
      }

      const sessionState = sessions.get(sessionId)
      if (!sessionState) {
        return reply.code(404).send({ error: "Invalid MCP session ID" })
      }

      reply.hijack()
      await sessionState.transport.handleRequest(req.raw, reply.raw)
      return reply
    } catch (error: any) {
      diagnosticsService.logError("remote-server", `Injected MCP ${req.method} error`, error)
      if (!reply.sent) {
        return reply.code(500).send({
          jsonrpc: "2.0",
          error: { code: -32603, message: error?.message || "Internal server error" },
          id: null,
        })
      }
      return reply
    }
  }

  fastify.post("/mcp/:acpSessionToken", async (req, reply) => {
    const params = req.params as { acpSessionToken?: string }
    return handleInjectedMcpProtocolRequest(req, reply, params?.acpSessionToken)
  })

  fastify.get("/mcp/:acpSessionToken", async (req, reply) => {
    const params = req.params as { acpSessionToken?: string }
    return handleInjectedMcpProtocolRequest(req, reply, params?.acpSessionToken)
  })

  fastify.delete("/mcp/:acpSessionToken", async (req, reply) => {
    const params = req.params as { acpSessionToken?: string }
    return handleInjectedMcpProtocolRequest(req, reply, params?.acpSessionToken)
  })

  // POST /mcp/tools/list - List all available injected runtime tools
  fastify.post("/mcp/tools/list", async (req, reply) => {
    const query = req.query as { acpSessionToken?: string } | undefined
    return listInjectedMcpTools(query?.acpSessionToken, reply)
  })

  fastify.post("/mcp/:acpSessionToken/tools/list", async (req, reply) => {
    const params = req.params as { acpSessionToken?: string }
    return listInjectedMcpTools(params?.acpSessionToken, reply)
  })

  // POST /mcp/tools/call - Execute an injected runtime tool
  fastify.post("/mcp/tools/call", async (req, reply) => {
    const query = req.query as { acpSessionToken?: string } | undefined
    return callInjectedMcpTool(req, reply, query?.acpSessionToken)
  })

  fastify.post("/mcp/:acpSessionToken/tools/call", async (req, reply) => {
    const params = req.params as { acpSessionToken?: string }
    return callInjectedMcpTool(req, reply, params?.acpSessionToken)
  })

  // ============================================
  // Skills Management Endpoints (for mobile app)
  // ============================================

  const formatSkillForMobile = (skill: ReturnType<typeof skillsService.getSkills>[number]) => {
    const currentProfile = agentProfileService.getCurrentProfile()
    const allEnabledByDefault = !currentProfile?.skillsConfig || !currentProfile.skillsConfig.allSkillsDisabledByDefault
    const enabledSkillIds = allEnabledByDefault
      ? skillsService.getSkills().map(s => s.id)
      : (currentProfile?.skillsConfig?.enabledSkillIds || [])

    return {
      id: skill.id,
      name: skill.name,
      description: skill.description,
      instructions: skill.instructions,
      enabled: true,
      enabledForProfile: enabledSkillIds.includes(skill.id),
      source: skill.source,
      createdAt: skill.createdAt,
      updatedAt: skill.updatedAt,
    }
  }

  const cleanupStaleSkillReferencesForMobile = async () => {
    const { globalAgentsFolder, resolveWorkspaceAgentsFolder } = await import("./config")
    const { getAgentsLayerPaths } = await import("./agents-files/modular-config")
    const { cleanupInvalidSkillReferencesInLayers } = await import("./agent-profile-skill-cleanup")

    const workspaceAgentsFolder = resolveWorkspaceAgentsFolder()
    const layers = workspaceAgentsFolder
      ? [getAgentsLayerPaths(globalAgentsFolder), getAgentsLayerPaths(workspaceAgentsFolder)]
      : [getAgentsLayerPaths(globalAgentsFolder)]

    cleanupInvalidSkillReferencesInLayers(layers, skillsService.getSkills().map(skill => skill.id))
    agentProfileService.reload()
  }

  // GET /v1/skills - List all skills
  fastify.get("/v1/skills", async (_req, reply) => {
    try {
      const skills = skillsService.getSkills()
      const currentProfile = agentProfileService.getCurrentProfile()
      // When skillsConfig is undefined or allSkillsDisabledByDefault is false, all skills are enabled
      const allEnabledByDefault = !currentProfile?.skillsConfig || !currentProfile.skillsConfig.allSkillsDisabledByDefault
      const enabledSkillIds = allEnabledByDefault ? skills.map(s => s.id) : (currentProfile?.skillsConfig?.enabledSkillIds || [])

      return reply.send({
        skills: skills.map(s => ({
          id: s.id,
          name: s.name,
          description: s.description,
          instructions: s.instructions,
          enabled: true,
          enabledForProfile: enabledSkillIds.includes(s.id),
          source: s.source,
          createdAt: s.createdAt,
          updatedAt: s.updatedAt,
        })),
        currentProfileId: currentProfile?.id,
      })
    } catch (error: any) {
      diagnosticsService.logError("remote-server", "Failed to get skills", error)
      return reply.code(500).send({ error: "Failed to get skills" })
    }
  })

  // GET /v1/skills/:id - Get a single skill for editing
  fastify.get("/v1/skills/:id", async (req, reply) => {
    try {
      const params = req.params as { id: string }
      const skill = skillsService.getSkill(params.id)
      if (!skill) {
        return reply.code(404).send({ error: "Skill not found" })
      }
      return reply.send({ skill: formatSkillForMobile(skill) })
    } catch (error: any) {
      diagnosticsService.logError("remote-server", "Failed to get skill", error)
      return reply.code(500).send({ error: error?.message || "Failed to get skill" })
    }
  })

  // POST /v1/skills - Create a skill
  fastify.post("/v1/skills", async (req, reply) => {
    try {
      const body = req.body as { name?: unknown; description?: unknown; instructions?: unknown }
      const name = typeof body.name === "string" ? body.name.trim() : ""
      const description = typeof body.description === "string" ? body.description.trim() : ""
      const instructions = typeof body.instructions === "string" ? body.instructions.trim() : ""

      if (!name || !instructions) {
        return reply.code(400).send({ error: "name and instructions are required" })
      }

      const skill = skillsService.createSkill(name, description, instructions, { source: "local" })
      agentProfileService.enableSkillForCurrentProfile(skill.id)
      return reply.code(201).send({ skill: formatSkillForMobile(skill) })
    } catch (error: any) {
      diagnosticsService.logError("remote-server", "Failed to create skill", error)
      return reply.code(500).send({ error: error?.message || "Failed to create skill" })
    }
  })

  // POST /v1/skills/import/markdown - Import a skill from SKILL.md content
  fastify.post("/v1/skills/import/markdown", async (req, reply) => {
    try {
      const body = req.body as { content?: unknown }
      const content = typeof body.content === "string" ? body.content.trim() : ""
      if (!content) {
        return reply.code(400).send({ error: "content must be a non-empty string" })
      }

      const skill = skillsService.importSkillFromMarkdown(content)
      agentProfileService.enableSkillForCurrentProfile(skill.id)
      return reply.code(201).send({ skill: formatSkillForMobile(skill) })
    } catch (error: any) {
      diagnosticsService.logError("remote-server", "Failed to import skill from Markdown", error)
      return reply.code(400).send({ error: error?.message || "Failed to import skill from Markdown" })
    }
  })

  // POST /v1/skills/import/github - Import one or more skills from a GitHub repository
  fastify.post("/v1/skills/import/github", async (req, reply) => {
    try {
      const body = req.body as { repoIdentifier?: unknown }
      const repoIdentifier = typeof body.repoIdentifier === "string" ? body.repoIdentifier.trim() : ""
      if (!repoIdentifier) {
        return reply.code(400).send({ error: "repoIdentifier must be a non-empty string" })
      }

      const result = await skillsService.importSkillFromGitHub(repoIdentifier)
      for (const skill of result.imported) {
        agentProfileService.enableSkillForCurrentProfile(skill.id)
      }

      const didFail = result.imported.length === 0 && result.errors.length > 0
      return reply.code(didFail ? 400 : 200).send({
        imported: result.imported.map(skill => formatSkillForMobile(skill)),
        errors: result.errors,
        ...(didFail ? { error: result.errors[0] || "No skills imported from GitHub" } : {}),
      })
    } catch (error: any) {
      diagnosticsService.logError("remote-server", "Failed to import skill from GitHub", error)
      return reply.code(500).send({ error: error?.message || "Failed to import skill from GitHub" })
    }
  })

  // POST /v1/skills/delete-multiple - Delete selected skills
  fastify.post("/v1/skills/delete-multiple", async (req, reply) => {
    try {
      const body = (req.body ?? {}) as { ids?: unknown }
      const ids = Array.isArray(body.ids)
        ? Array.from(new Set(body.ids.filter((id): id is string => typeof id === "string").map(id => id.trim()).filter(Boolean)))
        : []
      if (ids.length === 0) {
        return reply.code(400).send({ error: "ids must contain at least one skill id" })
      }

      const results = ids.map(id => {
        const success = skillsService.deleteSkill(id)
        return success ? { id, success } : { id, success, error: "Skill not found or could not be deleted" }
      })

      if (results.some(result => result.success)) {
        await cleanupStaleSkillReferencesForMobile()
      }

      return reply.send({
        deletedCount: results.filter(result => result.success).length,
        results,
      })
    } catch (error: any) {
      diagnosticsService.logError("remote-server", "Failed to delete selected skills", error)
      return reply.code(500).send({ error: error?.message || "Failed to delete selected skills" })
    }
  })

  // PATCH /v1/skills/:id - Update a skill
  fastify.patch("/v1/skills/:id", async (req, reply) => {
    try {
      const params = req.params as { id: string }
      const body = req.body as { name?: unknown; description?: unknown; instructions?: unknown }
      const updates: Partial<Pick<ReturnType<typeof skillsService.getSkills>[number], "name" | "description" | "instructions">> = {}

      if (body.name !== undefined) {
        if (typeof body.name !== "string" || body.name.trim() === "") {
          return reply.code(400).send({ error: "name must be a non-empty string when provided" })
        }
        updates.name = body.name.trim()
      }
      if (body.description !== undefined) {
        if (typeof body.description !== "string") {
          return reply.code(400).send({ error: "description must be a string when provided" })
        }
        updates.description = body.description.trim()
      }
      if (body.instructions !== undefined) {
        if (typeof body.instructions !== "string" || body.instructions.trim() === "") {
          return reply.code(400).send({ error: "instructions must be a non-empty string when provided" })
        }
        updates.instructions = body.instructions.trim()
      }

      const skill = skillsService.updateSkill(params.id, updates)
      return reply.send({ success: true, skill: formatSkillForMobile(skill) })
    } catch (error: any) {
      diagnosticsService.logError("remote-server", "Failed to update skill", error)
      const message = error?.message || "Failed to update skill"
      return reply.code(message.includes("not found") ? 404 : 500).send({ error: message })
    }
  })

  // GET /v1/skills/:id/export/markdown - Export a skill as SKILL.md content
  fastify.get("/v1/skills/:id/export/markdown", async (req, reply) => {
    try {
      const params = req.params as { id: string }
      const markdown = skillsService.exportSkillToMarkdown(params.id)
      return reply.send({ markdown })
    } catch (error: any) {
      diagnosticsService.logError("remote-server", "Failed to export skill to Markdown", error)
      const message = error?.message || "Failed to export skill to Markdown"
      return reply.code(message.includes("not found") ? 404 : 500).send({ error: message })
    }
  })

  // POST /v1/skills/:id/toggle-profile - Toggle skill for current profile
  fastify.post("/v1/skills/:id/toggle-profile", async (req, reply) => {
    try {
      const params = req.params as { id: string }

      // Validate the skill exists
      const skills = skillsService.getSkills()
      const skillExists = skills.some(s => s.id === params.id)
      if (!skillExists) {
        return reply.code(404).send({ error: "Skill not found" })
      }

      const currentProfile = agentProfileService.getCurrentProfile()
      if (!currentProfile) {
        return reply.code(400).send({ error: "No current profile set" })
      }

      const allSkillIds = skills.map(s => s.id)
      const updatedProfile = agentProfileService.toggleProfileSkill(currentProfile.id, params.id, allSkillIds)
      // Check enablement using the new semantics
      const isNowEnabled = !updatedProfile?.skillsConfig || !updatedProfile.skillsConfig.allSkillsDisabledByDefault
        ? true
        : (updatedProfile.skillsConfig.enabledSkillIds || []).includes(params.id)

      return reply.send({
        success: true,
        skillId: params.id,
        enabledForProfile: isNowEnabled,
      })
    } catch (error: any) {
      diagnosticsService.logError("remote-server", "Failed to toggle skill", error)
      return reply.code(500).send({ error: error?.message || "Failed to toggle skill" })
    }
  })

  // ============================================
  // Knowledge Notes Management Endpoints (for mobile app)
  // ============================================

  const serializeKnowledgeNote = (note: import("../shared/types").KnowledgeNote) => ({
    id: note.id,
    title: note.title,
    body: note.body,
    summary: note.summary,
    context: note.context,
    tags: note.tags,
    references: note.references,
    createdAt: note.createdAt,
    updatedAt: note.updatedAt,
  })

  const parseKnowledgeNoteFilter = (raw: unknown) => {
    const record = raw && typeof raw === "object" ? raw as Record<string, unknown> : {}
    const context = record.context === "auto" || record.context === "search-only"
      ? record.context as import("../shared/types").KnowledgeNoteContext
      : undefined
    const dateFilter = ["all", "7d", "30d", "90d", "year"].includes(String(record.dateFilter))
      ? record.dateFilter as import("../shared/types").KnowledgeNoteDateFilter
      : undefined
    const sort = ["relevance", "updated-desc", "updated-asc", "created-desc", "created-asc", "title-asc", "title-desc"].includes(String(record.sort))
      ? record.sort as import("../shared/types").KnowledgeNoteSort
      : undefined
    const limitValue = typeof record.limit === "number"
      ? record.limit
      : typeof record.limit === "string"
        ? Number(record.limit)
        : undefined
    const limit = typeof limitValue === "number" && Number.isFinite(limitValue)
      ? Math.max(0, Math.min(500, Math.floor(limitValue)))
      : undefined

    return { context, dateFilter, sort, limit }
  }

  // GET /v1/knowledge/notes - List all knowledge notes
  fastify.get("/v1/knowledge/notes", async (req, reply) => {
    try {
      const notes = await knowledgeNotesService.getAllNotes(parseKnowledgeNoteFilter(req.query))
      return reply.send({ notes: notes.map(serializeKnowledgeNote) })
    } catch (error: any) {
      diagnosticsService.logError("remote-server", "Failed to get knowledge notes", error)
      return reply.code(500).send({ error: "Failed to get knowledge notes" })
    }
  })

  // POST /v1/knowledge/notes/search - Search knowledge notes with filters
  fastify.post("/v1/knowledge/notes/search", async (req, reply) => {
    try {
      const body = req.body as Record<string, unknown> | undefined
      const query = typeof body?.query === "string" ? body.query.trim() : ""
      const filter = parseKnowledgeNoteFilter(body)
      const notes = query
        ? await knowledgeNotesService.searchNotes(query, filter)
        : await knowledgeNotesService.getAllNotes(filter)
      return reply.send({ notes: notes.map(serializeKnowledgeNote) })
    } catch (error: any) {
      diagnosticsService.logError("remote-server", "Failed to search knowledge notes", error)
      return reply.code(500).send({ error: error?.message || "Failed to search knowledge notes" })
    }
  })

  // POST /v1/knowledge/notes/delete - Delete selected knowledge notes
  fastify.post("/v1/knowledge/notes/delete", async (req, reply) => {
    try {
      const body = (req.body ?? {}) as { ids?: unknown }
      const ids = Array.isArray(body.ids)
        ? Array.from(new Set(body.ids.filter((id): id is string => typeof id === "string").map(id => id.trim()).filter(Boolean)))
        : []
      if (ids.length === 0) {
        return reply.code(400).send({ error: "ids must contain at least one note id" })
      }
      const result = await knowledgeNotesService.deleteMultipleNotes(ids)
      if (result.error) {
        return reply.code(500).send({ error: result.error, deletedCount: result.deletedCount })
      }
      return reply.send({ deletedCount: result.deletedCount })
    } catch (error: any) {
      diagnosticsService.logError("remote-server", "Failed to delete selected knowledge notes", error)
      return reply.code(500).send({ error: error?.message || "Failed to delete selected knowledge notes" })
    }
  })

  // POST /v1/knowledge/notes/delete-all - Delete all knowledge notes
  fastify.post("/v1/knowledge/notes/delete-all", async (_req, reply) => {
    try {
      const result = await knowledgeNotesService.deleteAllNotes()
      if (result.error) {
        return reply.code(500).send({ error: result.error, deletedCount: result.deletedCount })
      }
      return reply.send({ deletedCount: result.deletedCount })
    } catch (error: any) {
      diagnosticsService.logError("remote-server", "Failed to delete all knowledge notes", error)
      return reply.code(500).send({ error: error?.message || "Failed to delete all knowledge notes" })
    }
  })

  // GET /v1/knowledge/notes/:id - Get one knowledge note
  fastify.get("/v1/knowledge/notes/:id", async (req, reply) => {
    try {
      const params = req.params as { id: string }
      const note = await knowledgeNotesService.getNote(params.id)
      if (!note) return reply.code(404).send({ error: "Knowledge note not found" })
      return reply.send({ note: serializeKnowledgeNote(note) })
    } catch (error: any) {
      diagnosticsService.logError("remote-server", "Failed to get knowledge note", error)
      return reply.code(500).send({ error: error?.message || "Failed to get knowledge note" })
    }
  })

  // DELETE /v1/knowledge/notes/:id - Delete a knowledge note
  fastify.delete("/v1/knowledge/notes/:id", async (req, reply) => {
    try {
      const params = req.params as { id: string }
      const note = await knowledgeNotesService.getNote(params.id)
      if (!note) return reply.code(404).send({ error: "Knowledge note not found" })

      const success = await knowledgeNotesService.deleteNote(params.id)
      if (!success) {
        return reply.code(500).send({ error: "Failed to persist knowledge note deletion" })
      }

      return reply.send({ success: true, id: params.id })
    } catch (error: any) {
      diagnosticsService.logError("remote-server", "Failed to delete knowledge note", error)
      return reply.code(500).send({ error: error?.message || "Failed to delete knowledge note" })
    }
  })

  // ============================================
  // Agent Management Endpoints (for mobile app)
  // ============================================

  // GET /v1/agent-profiles - List all agent profiles (supports ?role=chat-agent|delegation-target|external-agent filter; user-profile is a legacy alias)
  fastify.get("/v1/agent-profiles", async (req, reply) => {
    try {
      const query = req.query as { role?: string }
      let profiles = agentProfileService.getAll()

      // Filter by role if specified
      if (query.role) {
        profiles = profiles.filter(p => {
          const role = p.role || (p.isUserProfile ? "chat-agent" : p.isAgentTarget ? "delegation-target" : "delegation-target")
          const requestedRole = normalizeAgentProfileRole(query.role)
          const profileRole = normalizeAgentProfileRole(role)
          return profileRole === requestedRole
        })
      }

      return reply.send({
        profiles: profiles.map(p => ({
          id: p.id,
          name: p.name,
          displayName: p.displayName,
          description: p.description,
          avatarDataUrl: p.avatarDataUrl,
          enabled: p.enabled,
          isBuiltIn: p.isBuiltIn,
          isUserProfile: p.isUserProfile,
          isAgentTarget: p.isAgentTarget,
          isDefault: p.isDefault,
          role: normalizeAgentProfileRole(p.role),
          connectionType: p.connection.type,
          autoSpawn: p.autoSpawn,
          guidelines: p.guidelines,
          systemPrompt: p.systemPrompt,
          createdAt: p.createdAt,
          updatedAt: p.updatedAt,
        })),
      })
    } catch (error: any) {
      diagnosticsService.logError("remote-server", "Failed to get agent profiles", error)
      return reply.code(500).send({ error: "Failed to get agent profiles" })
    }
  })

  // POST /v1/agent-profiles/reload - Rescan modular agent profile files from disk
  fastify.post("/v1/agent-profiles/reload", async (_req, reply) => {
    try {
      agentProfileService.reload()
      const profiles = agentProfileService.getAll()

      return reply.send({
        success: true,
        profiles: profiles.map(p => ({
          id: p.id,
          name: p.name,
          displayName: p.displayName,
          description: p.description,
          avatarDataUrl: p.avatarDataUrl,
          enabled: p.enabled,
          isBuiltIn: p.isBuiltIn,
          isUserProfile: p.isUserProfile,
          isAgentTarget: p.isAgentTarget,
          isDefault: p.isDefault,
          role: normalizeAgentProfileRole(p.role),
          connectionType: p.connection.type,
          autoSpawn: p.autoSpawn,
          guidelines: p.guidelines,
          systemPrompt: p.systemPrompt,
          createdAt: p.createdAt,
          updatedAt: p.updatedAt,
        })),
      })
    } catch (error: any) {
      diagnosticsService.logError("remote-server", "Failed to reload agent profiles", error)
      return reply.code(500).send({ error: error?.message || "Failed to reload agent profiles" })
    }
  })

  // POST /v1/agent-profiles/:id/toggle - Toggle agent profile enabled state
  fastify.post("/v1/agent-profiles/:id/toggle", async (req, reply) => {
    try {
      const params = req.params as { id: string }
      const profile = agentProfileService.getById(params.id)

      if (!profile) {
        return reply.code(404).send({ error: "Agent profile not found" })
      }

      const updated = agentProfileService.update(params.id, {
        enabled: !profile.enabled,
      })

      return reply.send({
        success: true,
        id: params.id,
        enabled: updated?.enabled ?? !profile.enabled,
      })
    } catch (error: any) {
      diagnosticsService.logError("remote-server", "Failed to toggle agent profile", error)
      return reply.code(500).send({ error: error?.message || "Failed to toggle agent profile" })
    }
  })

  // POST /v1/agent-profiles/verify-command - Verify an external agent command
  fastify.post("/v1/agent-profiles/verify-command", async (req, reply) => {
    try {
      const body = req.body as {
        command?: unknown
        args?: unknown
        cwd?: unknown
        probeArgs?: unknown
      }

      if (!body || typeof body !== "object" || Array.isArray(body)) {
        return reply.code(400).send({ error: "Request body must be a JSON object" })
      }
      if (typeof body.command !== "string" || body.command.trim() === "") {
        return reply.code(400).send({ error: "command is required and must be a non-empty string" })
      }
      if (body.args !== undefined && (!Array.isArray(body.args) || !body.args.every(arg => typeof arg === "string"))) {
        return reply.code(400).send({ error: "args must be an array of strings" })
      }
      if (body.probeArgs !== undefined && (!Array.isArray(body.probeArgs) || !body.probeArgs.every(arg => typeof arg === "string"))) {
        return reply.code(400).send({ error: "probeArgs must be an array of strings" })
      }
      if (body.cwd !== undefined && typeof body.cwd !== "string") {
        return reply.code(400).send({ error: "cwd must be a string" })
      }

      const { verifyExternalAgentCommand } = await import("./command-verification-service")
      const args = body.args === undefined ? undefined : body.args as string[]
      const cwd = typeof body.cwd === "string" ? body.cwd : undefined
      const probeArgs = body.probeArgs === undefined ? undefined : body.probeArgs as string[]
      const result = await verifyExternalAgentCommand({
        command: body.command,
        args,
        cwd,
        probeArgs,
      })

      return reply.send(result)
    } catch (error: any) {
      diagnosticsService.logError("remote-server", "Failed to verify external agent command", error)
      return reply.code(500).send({ error: error?.message || "Failed to verify external agent command" })
    }
  })

  // GET /v1/agent-profiles/:id - Get single agent profile with full detail
  fastify.get("/v1/agent-profiles/:id", async (req, reply) => {
    try {
      const params = req.params as { id: string }
      const profile = agentProfileService.getById(params.id)

      if (!profile) {
        return reply.code(404).send({ error: "Agent profile not found" })
      }

      return reply.send({
        profile: {
          id: profile.id,
          name: profile.name,
          displayName: profile.displayName,
          description: profile.description,
          avatarDataUrl: profile.avatarDataUrl,
          systemPrompt: profile.systemPrompt,
          guidelines: profile.guidelines,
          properties: profile.properties,
          modelConfig: profile.modelConfig,
          toolConfig: profile.toolConfig,
          skillsConfig: profile.skillsConfig,
          connection: profile.connection,
          isStateful: profile.isStateful,
          conversationId: profile.conversationId,
          role: normalizeAgentProfileRole(profile.role),
          enabled: profile.enabled,
          isBuiltIn: profile.isBuiltIn,
          isUserProfile: profile.isUserProfile,
          isAgentTarget: profile.isAgentTarget,
          isDefault: profile.isDefault,
          autoSpawn: profile.autoSpawn,
          createdAt: profile.createdAt,
          updatedAt: profile.updatedAt,
        },
      })
    } catch (error: any) {
      diagnosticsService.logError("remote-server", "Failed to get agent profile", error)
      return reply.code(500).send({ error: error?.message || "Failed to get agent profile" })
    }
  })

  // POST /v1/agent-profiles - Create a new agent profile
  fastify.post("/v1/agent-profiles", async (req, reply) => {
    try {
      const body = req.body as {
        displayName?: string
        description?: string
        avatarDataUrl?: string | null
        systemPrompt?: string
        guidelines?: string
        connectionType?: string
        connectionCommand?: string
        connectionArgs?: string
        connectionBaseUrl?: string
        connectionCwd?: string
        enabled?: boolean
        autoSpawn?: boolean
        modelConfig?: any
        toolConfig?: any
        skillsConfig?: any
        properties?: Record<string, string>
      }

      // Validate displayName
      if (!body.displayName || typeof body.displayName !== "string" || body.displayName.trim() === "") {
        return reply.code(400).send({ error: "displayName is required and must be a non-empty string" })
      }
      if (body.avatarDataUrl !== undefined && body.avatarDataUrl !== null && typeof body.avatarDataUrl !== "string") {
        return reply.code(400).send({ error: "avatarDataUrl must be a string or null" })
      }

      // Validate connectionType
      const connectionType = body.connectionType ?? "internal"
      if (!VALID_AGENT_PROFILE_CONNECTION_TYPES.includes(connectionType as any)) {
        return reply.code(400).send({ error: `connectionType must be one of: ${VALID_AGENT_PROFILE_CONNECTION_TYPES.join(", ")}` })
      }

      const connection: import("@shared/types").AgentProfileConnection = sanitizeAgentProfileConnection({
        connectionType: connectionType as import("./agent-profile-connection-sanitize").AgentProfileConnectionTypeValue,
        connectionCommand: body.connectionCommand,
        connectionArgs: body.connectionArgs,
        connectionBaseUrl: body.connectionBaseUrl,
        connectionCwd: body.connectionCwd,
      })

      // Create the profile
      const newProfile = agentProfileService.create({
        name: body.displayName.trim(),
        displayName: body.displayName.trim(),
        description: body.description,
        avatarDataUrl: body.avatarDataUrl ?? undefined,
        systemPrompt: body.systemPrompt,
        guidelines: body.guidelines,
        connection,
        enabled: body.enabled !== false,
        autoSpawn: body.autoSpawn,
        modelConfig: body.modelConfig,
        toolConfig: body.toolConfig,
        skillsConfig: body.skillsConfig,
        properties: body.properties,
        role: "delegation-target",
        isUserProfile: false,
        isAgentTarget: true,
      })

      return reply.code(201).send({
        profile: {
          id: newProfile.id,
          name: newProfile.name,
          displayName: newProfile.displayName,
          description: newProfile.description,
          avatarDataUrl: newProfile.avatarDataUrl,
          systemPrompt: newProfile.systemPrompt,
          guidelines: newProfile.guidelines,
          properties: newProfile.properties,
          modelConfig: newProfile.modelConfig,
          toolConfig: newProfile.toolConfig,
          skillsConfig: newProfile.skillsConfig,
          connection: newProfile.connection,
          isStateful: newProfile.isStateful,
          role: normalizeAgentProfileRole(newProfile.role),
          enabled: newProfile.enabled,
          isBuiltIn: newProfile.isBuiltIn,
          isUserProfile: newProfile.isUserProfile,
          isAgentTarget: newProfile.isAgentTarget,
          isDefault: newProfile.isDefault,
          autoSpawn: newProfile.autoSpawn,
          createdAt: newProfile.createdAt,
          updatedAt: newProfile.updatedAt,
        },
      })
    } catch (error: any) {
      diagnosticsService.logError("remote-server", "Failed to create agent profile", error)
      return reply.code(500).send({ error: error?.message || "Failed to create agent profile" })
    }
  })

  // PATCH /v1/agent-profiles/:id - Update an agent profile
  fastify.patch("/v1/agent-profiles/:id", async (req, reply) => {
    try {
      const params = req.params as { id: string }
      const body = req.body as {
        displayName?: string
        description?: string
        avatarDataUrl?: string | null
        systemPrompt?: string
        guidelines?: string
        connectionType?: string
        connectionCommand?: string
        connectionArgs?: string
        connectionBaseUrl?: string
        connectionCwd?: string
        enabled?: boolean
        autoSpawn?: boolean
        modelConfig?: any
        toolConfig?: any
        skillsConfig?: any
        properties?: Record<string, string>
      }

      const profile = agentProfileService.getById(params.id)
      if (!profile) {
        return reply.code(404).send({ error: "Agent profile not found" })
      }

      // Build updates object
      const updates: Partial<import("@shared/types").AgentProfile> = {}

      // For built-in agents, only allow updating certain fields
      if (profile.isBuiltIn) {
        // Allow toggling enabled, updating guidelines for built-in agents
        if (body.enabled !== undefined) updates.enabled = body.enabled
        if (body.guidelines !== undefined) updates.guidelines = body.guidelines
        if (body.autoSpawn !== undefined) updates.autoSpawn = body.autoSpawn
      } else {
        // For non-built-in agents, allow all field updates
        if (body.displayName !== undefined) {
          if (typeof body.displayName !== "string" || body.displayName.trim() === "") {
            return reply.code(400).send({ error: "displayName must be a non-empty string" })
          }
          updates.displayName = body.displayName.trim()
          updates.name = body.displayName.trim()
        }
        if (body.description !== undefined) updates.description = body.description
        if (body.avatarDataUrl !== undefined) {
          if (body.avatarDataUrl !== null && typeof body.avatarDataUrl !== "string") {
            return reply.code(400).send({ error: "avatarDataUrl must be a string or null" })
          }
          updates.avatarDataUrl = body.avatarDataUrl
        }
        if (body.systemPrompt !== undefined) updates.systemPrompt = body.systemPrompt
        if (body.guidelines !== undefined) updates.guidelines = body.guidelines
        if (body.enabled !== undefined) updates.enabled = body.enabled
        if (body.autoSpawn !== undefined) updates.autoSpawn = body.autoSpawn
        if (body.modelConfig !== undefined) updates.modelConfig = body.modelConfig
        if (body.toolConfig !== undefined) updates.toolConfig = body.toolConfig
        if (body.skillsConfig !== undefined) updates.skillsConfig = body.skillsConfig
        if (body.properties !== undefined) updates.properties = body.properties

        // Handle connection updates
        if (body.connectionType !== undefined || body.connectionCommand !== undefined ||
            body.connectionArgs !== undefined || body.connectionBaseUrl !== undefined ||
            body.connectionCwd !== undefined) {
          const connectionType = body.connectionType ?? profile.connection.type
          if (!VALID_AGENT_PROFILE_CONNECTION_TYPES.includes(connectionType as any)) {
            return reply.code(400).send({ error: `connectionType must be one of: ${VALID_AGENT_PROFILE_CONNECTION_TYPES.join(", ")}` })
          }

          updates.connection = sanitizeAgentProfileConnection(
            {
              connectionType: connectionType as import("./agent-profile-connection-sanitize").AgentProfileConnectionTypeValue,
              connectionCommand: body.connectionCommand,
              connectionArgs: body.connectionArgs,
              connectionBaseUrl: body.connectionBaseUrl,
              connectionCwd: body.connectionCwd,
            },
            profile.connection,
          )
        }
      }

      const updatedProfile = agentProfileService.update(params.id, updates)
      if (!updatedProfile) {
        return reply.code(500).send({ error: "Failed to update agent profile" })
      }

      return reply.send({
        success: true,
        profile: {
          id: updatedProfile.id,
          name: updatedProfile.name,
          displayName: updatedProfile.displayName,
          description: updatedProfile.description,
          avatarDataUrl: updatedProfile.avatarDataUrl,
          systemPrompt: updatedProfile.systemPrompt,
          guidelines: updatedProfile.guidelines,
          properties: updatedProfile.properties,
          modelConfig: updatedProfile.modelConfig,
          toolConfig: updatedProfile.toolConfig,
          skillsConfig: updatedProfile.skillsConfig,
          connection: updatedProfile.connection,
          isStateful: updatedProfile.isStateful,
          conversationId: updatedProfile.conversationId,
          role: normalizeAgentProfileRole(updatedProfile.role),
          enabled: updatedProfile.enabled,
          isBuiltIn: updatedProfile.isBuiltIn,
          isUserProfile: updatedProfile.isUserProfile,
          isAgentTarget: updatedProfile.isAgentTarget,
          isDefault: updatedProfile.isDefault,
          autoSpawn: updatedProfile.autoSpawn,
          createdAt: updatedProfile.createdAt,
          updatedAt: updatedProfile.updatedAt,
        },
      })
    } catch (error: any) {
      diagnosticsService.logError("remote-server", "Failed to update agent profile", error)
      return reply.code(500).send({ error: error?.message || "Failed to update agent profile" })
    }
  })

  // DELETE /v1/agent-profiles/:id - Delete an agent profile
  fastify.delete("/v1/agent-profiles/:id", async (req, reply) => {
    try {
      const params = req.params as { id: string }
      const profile = agentProfileService.getById(params.id)

      if (!profile) {
        return reply.code(404).send({ error: "Agent profile not found" })
      }

      if (profile.isBuiltIn) {
        return reply.code(403).send({ error: "Cannot delete built-in agent profiles" })
      }

      const success = agentProfileService.delete(params.id)
      if (!success) {
        return reply.code(500).send({ error: "Failed to delete agent profile" })
      }

      return reply.send({ success: true })
    } catch (error: any) {
      diagnosticsService.logError("remote-server", "Failed to delete agent profile", error)
      return reply.code(500).send({ error: error?.message || "Failed to delete agent profile" })
    }
  })

  // ============================================
  // Repeat Tasks Management Endpoints (for mobile app)
  // ============================================

  const getLoopProfileName = (profileId?: string) =>
    profileId ? agentProfileService.getById(profileId)?.displayName : undefined

  const loadLoopService = async () => {
    try {
      const { loopService } = await import("./loop-service")
      return loopService
    } catch {
      return null
    }
  }

  const LOOP_TIME_RE = /^(?:[01]\d|2[0-3]):[0-5]\d$/

  type ParseScheduleResult =
    | { ok: true; schedule?: LoopConfig["schedule"] | null }
    | { ok: false; error: string }

  /**
   * Parse/validate a `schedule` field from a request body. Returns:
   *   - ok: true with schedule undefined   → omit schedule
   *   - ok: true with schedule null        → explicitly clear an existing schedule
   *   - ok: true with a concrete schedule
   *   - ok: false with error               → send 400
   */
  function parseScheduleInput(raw: unknown): ParseScheduleResult {
    if (raw === undefined) return { ok: true, schedule: undefined }
    if (raw === null) return { ok: true, schedule: null }
    if (typeof raw !== "object") return { ok: false, error: "schedule must be an object, null, or omitted" }
    const obj = raw as Record<string, unknown>
    if (obj.type !== "daily" && obj.type !== "weekly") {
      return { ok: false, error: "schedule.type must be 'daily' or 'weekly'" }
    }
    if (!Array.isArray(obj.times) || obj.times.length === 0) {
      return { ok: false, error: "schedule.times must be a non-empty array" }
    }
    const times: string[] = []
    for (const t of obj.times) {
      if (typeof t !== "string" || !LOOP_TIME_RE.test(t.trim())) {
        return { ok: false, error: "schedule.times must all be HH:MM (24h) strings" }
      }
      const trimmed = t.trim()
      if (!times.includes(trimmed)) times.push(trimmed)
    }
    times.sort()
    if (obj.type === "daily") {
      return { ok: true, schedule: { type: "daily", times } }
    }
    if (!Array.isArray(obj.daysOfWeek) || obj.daysOfWeek.length === 0) {
      return { ok: false, error: "schedule.daysOfWeek must be a non-empty array for weekly schedules" }
    }
    const daysOfWeek: number[] = []
    for (const d of obj.daysOfWeek) {
      const n = typeof d === "number" ? d : Number(d)
      if (!Number.isInteger(n) || n < 0 || n > 6) {
        return { ok: false, error: "schedule.daysOfWeek values must be integers 0..6 (Sun..Sat)" }
      }
      if (!daysOfWeek.includes(n)) daysOfWeek.push(n)
    }
    daysOfWeek.sort((a, b) => a - b)
    return { ok: true, schedule: { type: "weekly", times, daysOfWeek } }
  }

  function parseLoopMaxIterationsInput(raw: unknown):
    | { ok: true; value?: number | null }
    | { ok: false; error: string } {
    if (raw === undefined) return { ok: true, value: undefined }
    if (raw === null) return { ok: true, value: null }
    if (
      typeof raw !== "number"
      || !Number.isFinite(raw)
      || !Number.isInteger(raw)
      || raw < 1
    ) {
      return { ok: false, error: "maxIterations must be a finite integer >= 1 when provided" }
    }
    return { ok: true, value: raw }
  }

  const formatLoopResponse = async (loop: LoopConfig) => {
    const status = (await loadLoopService())?.getLoopStatus(loop.id)

    return {
      id: loop.id,
      name: loop.name,
      prompt: loop.prompt,
      intervalMinutes: loop.intervalMinutes,
      enabled: loop.enabled,
      profileId: loop.profileId,
      profileName: getLoopProfileName(loop.profileId),
      runOnStartup: loop.runOnStartup,
      speakOnTrigger: loop.speakOnTrigger,
      continueInSession: loop.continueInSession,
      lastSessionId: loop.lastSessionId,
      runContinuously: loop.runContinuously,
      maxIterations: loop.maxIterations,
      lastRunAt: status?.lastRunAt ?? loop.lastRunAt,
      isRunning: status?.isRunning ?? false,
      nextRunAt: status?.nextRunAt,
      schedule: loop.schedule,
    }
  }

  // GET /v1/loops - List all repeat tasks
  fastify.get("/v1/loops", async (_req, reply) => {
    try {
      const loopService = await loadLoopService()
      const loops = loopService?.getLoops() ?? (configStore.get().loops || [])
      const statuses = loopService?.getLoopStatuses() ?? []

      const statusById = new Map(statuses.map(s => [s.id, s]))

      return reply.send({
        loops: loops.map(l => {
          const status = statusById.get(l.id)
          return {
            id: l.id,
            name: l.name,
            prompt: l.prompt,
            intervalMinutes: l.intervalMinutes,
            enabled: l.enabled,
            profileId: l.profileId,
            profileName: getLoopProfileName(l.profileId),
            runOnStartup: l.runOnStartup,
            speakOnTrigger: l.speakOnTrigger,
            continueInSession: l.continueInSession,
            lastSessionId: l.lastSessionId,
            runContinuously: l.runContinuously,
            maxIterations: l.maxIterations,
            lastRunAt: status?.lastRunAt ?? l.lastRunAt,
            isRunning: status?.isRunning ?? false,
            nextRunAt: status?.nextRunAt,
            schedule: l.schedule,
          }
        }),
      })
    } catch (error: any) {
      diagnosticsService.logError("remote-server", "Failed to get repeat tasks", error)
      return reply.code(500).send({ error: "Failed to get repeat tasks" })
    }
  })

  // POST /v1/loops/import/markdown - Import a repeat task from task.md content
  fastify.post("/v1/loops/import/markdown", async (req, reply) => {
    try {
      const body = req.body as { content?: unknown }
      const content = typeof body.content === "string" ? body.content.trim() : ""
      if (!content) {
        return reply.code(400).send({ error: "Repeat task Markdown content is required" })
      }

      const { parseTaskMarkdown } = await import("./agents-files/tasks")
      const parsedLoop = parseTaskMarkdown(content, {
        fallbackId: `loop_${Date.now()}_${Math.random().toString(36).slice(2, 11)}`,
      })
      if (!parsedLoop) {
        return reply.code(400).send({ error: "Invalid repeat task Markdown" })
      }

      const loopService = await loadLoopService()
      if (loopService) {
        const saved = loopService.saveLoop(parsedLoop)
        if (!saved) {
          return reply.code(500).send({ error: "Failed to persist repeat task" })
        }
        loopService.stopLoop(parsedLoop.id)
        if (parsedLoop.enabled) {
          loopService.startLoop(parsedLoop.id)
        }
      } else {
        const cfg = configStore.get()
        const loops = cfg.loops || []
        const existingIndex = loops.findIndex(loop => loop.id === parsedLoop.id)
        const updatedLoops = existingIndex >= 0
          ? loops.map((loop, index) => index === existingIndex ? parsedLoop : loop)
          : [...loops, parsedLoop]
        configStore.save({ ...cfg, loops: updatedLoops })
      }

      const savedLoop = loopService?.getLoop(parsedLoop.id) ?? parsedLoop
      return reply.code(201).send({ loop: await formatLoopResponse(savedLoop) })
    } catch (error: any) {
      diagnosticsService.logError("remote-server", "Failed to import repeat task from Markdown", error)
      return reply.code(400).send({ error: error?.message || "Failed to import repeat task" })
    }
  })

  // GET /v1/agent-sessions/candidates - List active and recent sessions for repeat-task pickers
  fastify.get("/v1/agent-sessions/candidates", async (req, reply) => {
    try {
      const limit = parseAgentSessionCandidateLimit(req.query)
      if (typeof limit !== "number") {
        return reply.code(400).send({ error: limit.error })
      }

      return reply.send({
        activeSessions: agentSessionTracker.getActiveSessions().map(formatAgentSessionCandidate),
        completedSessions: agentSessionTracker.getRecentSessions(limit).map(formatAgentSessionCandidate),
      })
    } catch (error: any) {
      diagnosticsService.logError("remote-server", "Failed to get agent session candidates", error)
      return reply.code(500).send({ error: error?.message || "Failed to get agent session candidates" })
    }
  })

  // POST /v1/agent-sessions/tool-approvals/:approvalId/respond - Approve or deny a pending tool call
  fastify.post("/v1/agent-sessions/tool-approvals/:approvalId/respond", async (req, reply) => {
    try {
      const params = req.params as { approvalId: string }
      const body = req.body as { approved?: unknown } | undefined
      const approvalId = params.approvalId

      if (!approvalId || typeof approvalId !== "string") {
        return reply.code(400).send({ error: "Missing or invalid approvalId" })
      }
      if (typeof body?.approved !== "boolean") {
        return reply.code(400).send({ error: "Missing or invalid approved" })
      }

      const success = toolApprovalManager.respondToApproval(approvalId, body.approved)
      return reply.send({ success, approvalId, approved: body.approved })
    } catch (error: any) {
      diagnosticsService.logError("remote-server", "Failed to respond to tool approval", error)
      return reply.code(500).send({ error: error?.message || "Failed to respond to tool approval" })
    }
  })

  // GET /v1/loops/:id/export/markdown - Export one repeat task as task.md content
  fastify.get("/v1/loops/:id/export/markdown", async (req, reply) => {
    try {
      const params = req.params as { id: string }
      const loopService = await loadLoopService()
      const loop = loopService?.getLoop(params.id) ?? (configStore.get().loops || []).find(task => task.id === params.id)
      if (!loop) {
        return reply.code(404).send({ error: "Repeat task not found" })
      }

      const { stringifyTaskMarkdown } = await import("./agents-files/tasks")
      return reply.send({
        success: true,
        loopId: params.id,
        markdown: stringifyTaskMarkdown(loop),
      })
    } catch (error: any) {
      diagnosticsService.logError("remote-server", "Failed to export repeat task to Markdown", error)
      return reply.code(500).send({ error: error?.message || "Failed to export repeat task" })
    }
  })

  // POST /v1/loops/:id/toggle - Toggle repeat task enabled state
  fastify.post("/v1/loops/:id/toggle", async (req, reply) => {
    try {
      const params = req.params as { id: string }
      const loopService = await loadLoopService()

      if (loopService) {
        const existing = loopService.getLoop(params.id)
        if (!existing) {
          return reply.code(404).send({ error: "Repeat task not found" })
        }

        const updated = { ...existing, enabled: !existing.enabled }
        const saved = loopService.saveLoop(updated)
        if (!saved) {
          return reply.code(500).send({ error: "Failed to persist repeat task toggle" })
        }

        if (updated.enabled) {
          loopService.startLoop(params.id)
        } else {
          loopService.stopLoop(params.id)
        }

        return reply.send({
          success: true,
          id: params.id,
          enabled: updated.enabled,
        })
      }

      const cfg = configStore.get()
      const loops = cfg.loops || []
      const loopIndex = loops.findIndex(l => l.id === params.id)

      if (loopIndex === -1) {
        return reply.code(404).send({ error: "Repeat task not found" })
      }

      const updatedLoops = [...loops]
      updatedLoops[loopIndex] = {
        ...updatedLoops[loopIndex],
        enabled: !updatedLoops[loopIndex].enabled,
      }

      configStore.save({ ...cfg, loops: updatedLoops })

      return reply.send({
        success: true,
        id: params.id,
        enabled: updatedLoops[loopIndex].enabled,
      })
    } catch (error: any) {
      diagnosticsService.logError("remote-server", "Failed to toggle repeat task", error)
      return reply.code(500).send({ error: error?.message || "Failed to toggle repeat task" })
    }
  })

  // POST /v1/loops/:id/run - Run a repeat task immediately
  fastify.post("/v1/loops/:id/run", async (req, reply) => {
    try {
      const params = req.params as { id: string }
      const loopService = await loadLoopService()

      if (loopService) {
        const loopExists = loopService.getLoop(params.id)
        if (!loopExists) {
          return reply.code(404).send({ error: "Repeat task not found" })
        }

        const triggered = await loopService.triggerLoop(params.id)

        if (!triggered) {
          return reply.code(409).send({ error: "Task is already running" })
        }

        return reply.send({ success: true, id: params.id })
      }

      return reply.code(503).send({ error: "Repeat task service is unavailable" })
    } catch (error: any) {
      diagnosticsService.logError("remote-server", "Failed to run repeat task", error)
      return reply.code(500).send({ error: error?.message || "Failed to run repeat task" })
    }
  })

  // POST /v1/knowledge/notes - Create a new knowledge note
  fastify.post("/v1/knowledge/notes", async (req, reply) => {
    try {
      const body = req.body as {
        id?: unknown
        title?: unknown
        body?: unknown
        summary?: unknown
        context?: unknown
        tags?: unknown
        references?: unknown
      }

      const noteBody = typeof body.body === "string" ? body.body.trim() : ""
      if (!noteBody) {
        return reply.code(400).send({ error: "body is required and must be a non-empty string" })
      }

      const note = knowledgeNotesService.createNote({
        id: typeof body.id === "string" && body.id.trim() ? body.id.trim() : undefined,
        title: typeof body.title === "string" && body.title.trim() ? body.title.trim() : undefined,
        body: noteBody,
        summary: typeof body.summary === "string" && body.summary.trim() ? body.summary.trim() : undefined,
        context: body.context === "auto" || body.context === "search-only" ? body.context : "search-only",
        tags: Array.isArray(body.tags) ? body.tags.filter((tag): tag is string => typeof tag === "string") : [],
        references: Array.isArray(body.references)
          ? body.references.filter((ref): ref is string => typeof ref === "string")
          : [],
      })

      const success = await knowledgeNotesService.saveNote(note)
      if (!success) {
        return reply.code(500).send({ error: "Failed to save knowledge note" })
      }

      const savedNote = await knowledgeNotesService.getNote(note.id)
      if (!savedNote) {
        return reply.code(500).send({ error: "Failed to load saved knowledge note" })
      }

      return reply.code(201).send({ note: serializeKnowledgeNote(savedNote) })
    } catch (error: any) {
      diagnosticsService.logError("remote-server", "Failed to create knowledge note", error)
      return reply.code(500).send({ error: error?.message || "Failed to create knowledge note" })
    }
  })

  // PATCH /v1/knowledge/notes/:id - Update a knowledge note
  fastify.patch("/v1/knowledge/notes/:id", async (req, reply) => {
    try {
      const params = req.params as { id: string }
      const body = req.body as {
        title?: unknown
        body?: unknown
        summary?: unknown
        context?: unknown
        tags?: unknown
        references?: unknown
      }

      const existing = await knowledgeNotesService.getNote(params.id)
      if (!existing) {
        return reply.code(404).send({ error: "Knowledge note not found" })
      }

      const updates: Record<string, unknown> = {}
      if (body.title !== undefined) {
        if (typeof body.title !== "string" || body.title.trim() === "") {
          return reply.code(400).send({ error: "title must be a non-empty string when provided" })
        }
        updates.title = body.title.trim()
      }
      if (body.body !== undefined) {
        if (typeof body.body !== "string" || body.body.trim() === "") {
          return reply.code(400).send({ error: "body must be a non-empty string when provided" })
        }
        updates.body = body.body.trim()
      }
      if (body.summary !== undefined) {
        if (typeof body.summary !== "string") {
          return reply.code(400).send({ error: "summary must be a string when provided" })
        }
        updates.summary = body.summary.trim() || undefined
      }
      if (body.context !== undefined) {
        if (body.context === "auto" || body.context === "search-only") {
          updates.context = body.context
        } else {
          return reply.code(400).send({ error: "context must be one of: auto, search-only" })
        }
      }
      if (body.tags !== undefined) {
        if (!Array.isArray(body.tags) || !body.tags.every((tag): tag is string => typeof tag === "string")) {
          return reply.code(400).send({ error: "tags must be an array of strings when provided" })
        }
        updates.tags = body.tags
      }
      if (body.references !== undefined) {
        if (!Array.isArray(body.references) || !body.references.every((ref): ref is string => typeof ref === "string")) {
          return reply.code(400).send({ error: "references must be an array of strings when provided" })
        }
        updates.references = body.references
      }

      const success = await knowledgeNotesService.updateNote(
        params.id,
        updates as Partial<Omit<import("../shared/types").KnowledgeNote, "id" | "createdAt">>,
      )
      if (!success) {
        return reply.code(500).send({ error: "Failed to update knowledge note" })
      }

      const updated = await knowledgeNotesService.getNote(params.id)
      if (!updated) {
        return reply.code(500).send({ error: "Failed to load updated knowledge note" })
      }

      return reply.send({ success: true, note: serializeKnowledgeNote(updated) })
    } catch (error: any) {
      diagnosticsService.logError("remote-server", "Failed to update knowledge note", error)
      return reply.code(500).send({ error: error?.message || "Failed to update knowledge note" })
    }
  })

  // POST /v1/loops - Create a new loop/repeat task
  fastify.post("/v1/loops", async (req, reply) => {
    try {
      const body = req.body as {
        name?: unknown
        prompt?: unknown
        intervalMinutes?: unknown
        enabled?: unknown
        profileId?: unknown
        runOnStartup?: unknown
        speakOnTrigger?: unknown
        continueInSession?: unknown
        lastSessionId?: unknown
        runContinuously?: unknown
        maxIterations?: unknown
        schedule?: unknown
      }

      const name = typeof body.name === "string" ? body.name.trim() : ""
      const prompt = typeof body.prompt === "string" ? body.prompt.trim() : ""
      if (!name || !prompt) {
        return reply.code(400).send({ error: "name and prompt are required and must be non-empty strings" })
      }

      if (
        body.intervalMinutes !== undefined
        && (
          typeof body.intervalMinutes !== "number"
          || !Number.isFinite(body.intervalMinutes)
          || !Number.isInteger(body.intervalMinutes)
          || body.intervalMinutes < 1
        )
      ) {
        return reply.code(400).send({ error: "intervalMinutes must be a finite integer >= 1 when provided" })
      }
      const intervalMinutes = typeof body.intervalMinutes === "number" ? body.intervalMinutes : 60
      if (body.enabled !== undefined && typeof body.enabled !== "boolean") {
        return reply.code(400).send({ error: "enabled must be a boolean when provided" })
      }
      if (body.profileId !== undefined && body.profileId !== null && typeof body.profileId !== "string") {
        return reply.code(400).send({ error: "profileId must be a string when provided" })
      }
      if (body.runContinuously !== undefined && typeof body.runContinuously !== "boolean") {
        return reply.code(400).send({ error: "runContinuously must be a boolean when provided" })
      }
      if (body.runOnStartup !== undefined && typeof body.runOnStartup !== "boolean") {
        return reply.code(400).send({ error: "runOnStartup must be a boolean when provided" })
      }
      if (body.speakOnTrigger !== undefined && typeof body.speakOnTrigger !== "boolean") {
        return reply.code(400).send({ error: "speakOnTrigger must be a boolean when provided" })
      }
      if (body.continueInSession !== undefined && typeof body.continueInSession !== "boolean") {
        return reply.code(400).send({ error: "continueInSession must be a boolean when provided" })
      }
      if (body.lastSessionId !== undefined && body.lastSessionId !== null && typeof body.lastSessionId !== "string") {
        return reply.code(400).send({ error: "lastSessionId must be a string or null when provided" })
      }
      const maxIterationsResult = parseLoopMaxIterationsInput(body.maxIterations)
      if (maxIterationsResult.ok === false) {
        return reply.code(400).send({ error: maxIterationsResult.error })
      }
      const scheduleResult = parseScheduleInput(body.schedule)
      if (scheduleResult.ok === false) {
        return reply.code(400).send({ error: scheduleResult.error })
      }
      const profileId = typeof body.profileId === "string" ? body.profileId.trim() : undefined
      const enabled = typeof body.enabled === "boolean" ? body.enabled : true
      const runContinuously = body.runContinuously === true
      const runOnStartup = body.runOnStartup === true
      const speakOnTrigger = body.speakOnTrigger === true
      const continueInSession = body.continueInSession === true
      const lastSessionId = typeof body.lastSessionId === "string" ? body.lastSessionId.trim() : undefined

      const id = `loop_${Date.now()}_${Math.random().toString(36).slice(2, 11)}`

      const newLoop: LoopConfig = {
        id,
        name,
        prompt,
        intervalMinutes,
        enabled,
        profileId: profileId || undefined,
        runOnStartup,
        speakOnTrigger,
        continueInSession,
        lastSessionId: continueInSession ? (lastSessionId || undefined) : undefined,
        runContinuously,
        ...(typeof maxIterationsResult.value === "number" ? { maxIterations: maxIterationsResult.value } : {}),
        ...(!runContinuously && scheduleResult.schedule && scheduleResult.schedule !== null
          ? { schedule: scheduleResult.schedule }
          : {}),
      }

      const loopService = await loadLoopService()
      if (loopService) {
        const saved = loopService.saveLoop(newLoop)
        if (!saved) {
          return reply.code(500).send({ error: "Failed to persist repeat task" })
        }

        if (newLoop.enabled) {
          loopService.startLoop(newLoop.id)
        }
      } else {
        const cfg = configStore.get()
        const loops = [...(cfg.loops || []), newLoop]
        configStore.save({ ...cfg, loops })
      }

      return reply.send({ loop: await formatLoopResponse(loopService?.getLoop(newLoop.id) ?? newLoop) })
    } catch (error: any) {
      diagnosticsService.logError("remote-server", "Failed to create repeat task", error)
      return reply.code(500).send({ error: error?.message || "Failed to create repeat task" })
    }
  })

  // PATCH /v1/loops/:id - Update a loop/repeat task
  fastify.patch("/v1/loops/:id", async (req, reply) => {
    try {
      const params = req.params as { id: string }
      const body = req.body as {
        name?: unknown
        prompt?: unknown
        intervalMinutes?: unknown
        enabled?: unknown
        profileId?: unknown
        runOnStartup?: unknown
        speakOnTrigger?: unknown
        continueInSession?: unknown
        lastSessionId?: unknown
        runContinuously?: unknown
        maxIterations?: unknown
        schedule?: unknown
      }

      const loopService = await loadLoopService()
      let existing: LoopConfig | undefined
      let cfg: ReturnType<typeof configStore.get> | undefined
      let loops: LoopConfig[] = []
      let loopIndex = -1

      if (loopService) {
        existing = loopService.getLoop(params.id)
      } else {
        cfg = configStore.get()
        loops = cfg.loops || []
        loopIndex = loops.findIndex(l => l.id === params.id)
        existing = loopIndex >= 0 ? loops[loopIndex] : undefined
      }

      if (!existing) {
        return reply.code(404).send({ error: "Repeat task not found" })
      }

      if (body.name !== undefined && (typeof body.name !== "string" || body.name.trim() === "")) {
        return reply.code(400).send({ error: "name must be a non-empty string when provided" })
      }
      if (body.prompt !== undefined && (typeof body.prompt !== "string" || body.prompt.trim() === "")) {
        return reply.code(400).send({ error: "prompt must be a non-empty string when provided" })
      }
      if (
        body.intervalMinutes !== undefined
        && (
          typeof body.intervalMinutes !== "number"
          || !Number.isFinite(body.intervalMinutes)
          || !Number.isInteger(body.intervalMinutes)
          || body.intervalMinutes < 1
        )
      ) {
        return reply.code(400).send({ error: "intervalMinutes must be a finite integer >= 1 when provided" })
      }
      if (body.enabled !== undefined && typeof body.enabled !== "boolean") {
        return reply.code(400).send({ error: "enabled must be a boolean when provided" })
      }
      if (body.profileId !== undefined && body.profileId !== null && typeof body.profileId !== "string") {
        return reply.code(400).send({ error: "profileId must be a string when provided" })
      }
      if (body.runContinuously !== undefined && typeof body.runContinuously !== "boolean") {
        return reply.code(400).send({ error: "runContinuously must be a boolean when provided" })
      }
      if (body.runOnStartup !== undefined && typeof body.runOnStartup !== "boolean") {
        return reply.code(400).send({ error: "runOnStartup must be a boolean when provided" })
      }
      if (body.speakOnTrigger !== undefined && typeof body.speakOnTrigger !== "boolean") {
        return reply.code(400).send({ error: "speakOnTrigger must be a boolean when provided" })
      }
      if (body.continueInSession !== undefined && typeof body.continueInSession !== "boolean") {
        return reply.code(400).send({ error: "continueInSession must be a boolean when provided" })
      }
      if (body.lastSessionId !== undefined && body.lastSessionId !== null && typeof body.lastSessionId !== "string") {
        return reply.code(400).send({ error: "lastSessionId must be a string or null when provided" })
      }
      const maxIterationsResult = parseLoopMaxIterationsInput(body.maxIterations)
      if (maxIterationsResult.ok === false) {
        return reply.code(400).send({ error: maxIterationsResult.error })
      }
      const scheduleResult = parseScheduleInput(body.schedule)
      if (scheduleResult.ok === false) {
        return reply.code(400).send({ error: scheduleResult.error })
      }

      const name = typeof body.name === "string" ? body.name.trim() : undefined
      const prompt = typeof body.prompt === "string" ? body.prompt.trim() : undefined
      const intervalMinutes =
        typeof body.intervalMinutes === "number"
          ? body.intervalMinutes
          : undefined
      const enabled = typeof body.enabled === "boolean" ? body.enabled : undefined
      const profileId = typeof body.profileId === "string" ? body.profileId.trim() : undefined
      const runContinuously = typeof body.runContinuously === "boolean" ? body.runContinuously : undefined
      const runOnStartup = typeof body.runOnStartup === "boolean" ? body.runOnStartup : undefined
      const speakOnTrigger = typeof body.speakOnTrigger === "boolean" ? body.speakOnTrigger : undefined
      const continueInSession = typeof body.continueInSession === "boolean" ? body.continueInSession : undefined
      const lastSessionId = typeof body.lastSessionId === "string" ? body.lastSessionId.trim() : undefined
      const updated: LoopConfig = {
        ...existing,
        ...(name !== undefined && { name }),
        ...(prompt !== undefined && { prompt }),
        ...(intervalMinutes !== undefined && { intervalMinutes }),
        ...(enabled !== undefined && { enabled }),
        ...(body.profileId !== undefined && { profileId: profileId || undefined }),
        ...(runOnStartup !== undefined && { runOnStartup }),
        ...(speakOnTrigger !== undefined && { speakOnTrigger }),
        ...(continueInSession !== undefined && { continueInSession }),
        ...(body.lastSessionId !== undefined && { lastSessionId: lastSessionId || undefined }),
        ...(runContinuously !== undefined && { runContinuously }),
        ...(maxIterationsResult.value !== undefined && maxIterationsResult.value !== null && { maxIterations: maxIterationsResult.value }),
      }
      if (maxIterationsResult.value === null) {
        delete updated.maxIterations
      }
      if (continueInSession === false || updated.continueInSession === false) {
        delete updated.lastSessionId
      }
      if (updated.runContinuously) {
        delete updated.schedule
      } else if (scheduleResult.schedule === null) {
        delete updated.schedule
      } else if (scheduleResult.schedule !== undefined) {
        updated.schedule = scheduleResult.schedule
        updated.runContinuously = false
      }

      if (loopService) {
        const saved = loopService.saveLoop(updated)
        if (!saved) {
          return reply.code(500).send({ error: "Failed to persist repeat task" })
        }

        if (updated.enabled) {
          loopService.stopLoop(params.id)
          loopService.startLoop(params.id)
        } else {
          loopService.stopLoop(params.id)
        }
      } else if (cfg && loopIndex >= 0) {
        const updatedLoops = [...loops]
        updatedLoops[loopIndex] = updated
        configStore.save({ ...cfg, loops: updatedLoops })
      }

      return reply.send({ success: true, loop: await formatLoopResponse(loopService?.getLoop(params.id) ?? updated) })
    } catch (error: any) {
      diagnosticsService.logError("remote-server", "Failed to update repeat task", error)
      return reply.code(500).send({ error: error?.message || "Failed to update repeat task" })
    }
  })

  // DELETE /v1/loops/:id - Delete a loop/repeat task
  fastify.delete("/v1/loops/:id", async (req, reply) => {
    try {
      const params = req.params as { id: string }
      const loopService = await loadLoopService()

      if (loopService) {
        const existing = loopService.getLoop(params.id)
        if (!existing) {
          return reply.code(404).send({ error: "Repeat task not found" })
        }

        const deleted = loopService.deleteLoop(params.id)
        if (!deleted) {
          return reply.code(500).send({ error: "Failed to delete repeat task" })
        }

        return reply.send({ success: true, id: params.id })
      }

      const cfg = configStore.get()
      const loops = cfg.loops || []
      const loopIndex = loops.findIndex(l => l.id === params.id)

      if (loopIndex === -1) {
        return reply.code(404).send({ error: "Repeat task not found" })
      }

      const updatedLoops = loops.filter(l => l.id !== params.id)
      configStore.save({ ...cfg, loops: updatedLoops })

      return reply.send({ success: true, id: params.id })
    } catch (error: any) {
      diagnosticsService.logError("remote-server", "Failed to delete repeat task", error)
      return reply.code(500).send({ error: error?.message || "Failed to delete repeat task" })
    }
  })

  try {
    await fastify.listen({ port, host: bind })
    diagnosticsService.logInfo(
      "remote-server",
      `Remote server listening at ${buildRemoteServerBaseUrl(bind, port)}`,
    )
    server = fastify

    // Print QR code to terminal for mobile app pairing
    // Auto-print in headless environments, or when explicitly requested
    // Skip if caller handles QR printing separately (e.g., --qr mode)
    // Suppress when streamer mode is enabled to prevent credential leakage
    if (!skipAutoPrintQR) {
      const currentCfg = configStore.get()
      const currentApiKey = getResolvedRemoteServerApiKey(currentCfg)
      if (currentApiKey && !currentCfg.streamerModeEnabled) {
        // In headless environments, always print the QR code
        // Otherwise, print if terminal QR is explicitly enabled
        if (isHeadlessEnvironment() || currentCfg.remoteServerTerminalQrEnabled) {
          const serverUrl = getConnectableBaseUrlForMobilePairing(bind, port)
          if (serverUrl) {
            await printTerminalQRCode(serverUrl, currentApiKey)
          } else {
            console.warn(
              `[Remote Server] Warning: Could not resolve a LAN-reachable URL for bind ${bind}. Skipping terminal QR code output.`
            )
          }
        }
      }
    }

    return { running: true, bind, port }
  } catch (err: any) {
    lastError = err?.message || String(err)
    diagnosticsService.logError("remote-server", "Failed to start server", err)
    server = null
    return { running: false, error: lastError }
  }
}

export async function stopRemoteServer() {
  if (server) {
    try {
      await server.close()
      diagnosticsService.logInfo("remote-server", "Remote server stopped")
    } catch (err) {
      diagnosticsService.logError("remote-server", "Error stopping server", err)
    } finally {
      server = null
    }
  }
}

export async function restartRemoteServer() {
  await stopRemoteServer()
  return startRemoteServer()
}

export function getRemoteServerStatus() {
  const cfg = configStore.get()
  const bind = cfg.remoteServerBindAddress || "127.0.0.1"
  const port = cfg.remoteServerPort || 3210
  const running = !!server
  const url = running ? buildRemoteServerBaseUrl(bind, port) : undefined
  const connectableUrl = running ? getConnectableBaseUrlForMobilePairing(bind, port, { warn: false }) : undefined
  return { running, url, connectableUrl, bind, port, lastError }
}

export function getRemoteServerPairingApiKey(): string {
  const cfg = configStore.get()
  if (cfg.streamerModeEnabled) {
    return ""
  }

  return getResolvedRemoteServerApiKey(cfg)
}

/**
 * Prints the QR code to the terminal for mobile app pairing
 * Can be called manually when the user wants to see the QR code
 * @param urlOverride Optional URL to use instead of the local server URL (e.g., Cloudflare tunnel URL)
 * @returns true if QR code was printed successfully, false if server is not running, no API key, streamer mode enabled, or QR generation failed
 */
export async function printQRCodeToTerminal(urlOverride?: string): Promise<boolean> {
  const cfg = configStore.get()
  const apiKey = getResolvedRemoteServerApiKey(cfg)
  if (!server || !apiKey) {
    console.log("[Remote Server] Cannot print QR code: server not running or no API key configured")
    return false
  }

  // Suppress QR output when streamer mode is enabled to prevent credential leakage
  if (cfg.streamerModeEnabled) {
    console.log("[Remote Server] Cannot print QR code: streamer mode is enabled")
    return false
  }

  let serverUrl: string
  if (urlOverride) {
    // Use the override URL (e.g., Cloudflare tunnel URL)
    // Ensure it ends with /v1
    serverUrl = urlOverride.endsWith("/v1") ? urlOverride : `${urlOverride}/v1`
  } else {
    const bind = cfg.remoteServerBindAddress || "127.0.0.1"
    const port = cfg.remoteServerPort || 3210
    const connectableBaseUrl = getConnectableBaseUrlForMobilePairing(bind, port)
    if (!connectableBaseUrl) {
      console.log("[Remote Server] Cannot print QR code: unable to resolve a LAN-reachable URL for the current bind address")
      return false
    }
    serverUrl = connectableBaseUrl
  }

  // Return the actual result from printTerminalQRCode to indicate success/failure
  return await printTerminalQRCode(serverUrl, apiKey)
}
