// PRE Web GUI — System prompt builder
// Mirrors the CLI's build_context_preamble() logic
//
// OPTIMIZATION: Static content (identity, rules, guidance) is placed FIRST in the
// prompt so Ollama/llama.cpp can reuse the KV cache prefix across requests. Dynamic
// content (memory, experience, date, cwd) follows. This avoids re-evaluating ~2000
// tokens of rules on every new conversation.

const fs = require('fs');
const path = require('path');
const { CONNECTIONS_FILE, COMFYUI_FILE, PRE_DIR, CRON_FILE } = require('./constants');
const { buildMemoryContext: buildMemCtx, buildMemoryContextAsync, buildMemoryInstructions } = require('./memory');
const { buildExperienceContext, buildExperienceContextAsync } = require('./experience');
const { buildTemporalContext } = require('./chronos');
const { getProvider } = require('./connections');
const { buildCalibrationContext } = require('./calibration');
const { buildSkillRecommendationContext } = require('./skills');

// ── Cached config readers ─────────────────────────────────────────────────────
// These files rarely change mid-conversation. Cache with 30s TTL to avoid
// reading from disk on every tool loop turn (~35 reads per session).

const _cache = { connections: null, connTime: 0, comfyui: null, comfyTime: 0 };
const CACHE_TTL = 30000; // 30 seconds

/**
 * Check if PRE is configured to run in Joby IT service-desk mode. Set by
 * the installer when the fine-tuned Joby model (`pre-gemma4-itsd`) is
 * installed; can be flipped manually in `~/.pre/router.json`.
 *
 * Cached behind the same 30s TTL as other config readers.
 */
const ROUTER_FILE = path.join(PRE_DIR, 'router.json');
const _jobyCache = { value: null, time: 0 };

function isJobyServiceDeskMode() {
  const now = Date.now();
  if (_jobyCache.time && now - _jobyCache.time < CACHE_TTL) return _jobyCache.value;
  let active = false;
  try {
    const cfg = JSON.parse(fs.readFileSync(ROUTER_FILE, 'utf-8'));
    active = cfg.joby_servicedesk_mode === true;
  } catch {
    active = false;
  }
  _jobyCache.value = active;
  _jobyCache.time = now;
  return active;
}

/**
 * Check which connections are active (cached, 30s TTL)
 */
function getActiveConnections() {
  const now = Date.now();
  if (_cache.connections && now - _cache.connTime < CACHE_TTL) return _cache.connections;
  try {
    const data = JSON.parse(fs.readFileSync(CONNECTIONS_FILE, 'utf-8'));
    _cache.connections = {
      brave: !!data.brave_search_key,
      github: !!data.github_key,
      google: !!data.google_client_id,
      microsoft: !!data.microsoft_client_id && !!data.microsoft_refresh_token,
      wolfram: !!data.wolfram_key,
      telegram: !!data.telegram_key,
      jira: !!data.jira_url && !!data.jira_token,
      confluence: !!data.confluence_url && !!data.confluence_token,
      servicenow: !!data.servicenow_url && (
        (data.servicenow_auth_type === 'basic' && !!data.servicenow_username && !!data.servicenow_password)
        || (data.servicenow_auth_type === 'oauth' && !!data.servicenow_client_id && !!data.servicenow_refresh_token)
        || (!data.servicenow_auth_type && !!data.servicenow_username && !!data.servicenow_password)
      ),
      smartsheet: !!data.smartsheet_token,
      slack: !!data.slack_token,
      linear: !!data.linear_token,
      zoom: !!data.zoom_account_id && !!data.zoom_client_id && !!data.zoom_client_secret,
      figma: !!data.figma_token,
      asana: !!data.asana_token,
      dynamics365: !!data.d365_url && !!data.d365_client_id && !!data.d365_tenant_id && (!!data.d365_client_secret || !!data.d365_refresh_token),
    };
    _cache.connTime = now;
    return _cache.connections;
  } catch {
    return { brave: false, github: false, google: false, microsoft: false, wolfram: false, telegram: false, jira: false, confluence: false, servicenow: false, smartsheet: false, slack: false, linear: false, zoom: false, figma: false, asana: false, dynamics365: false };
  }
}

/**
 * Invalidate connections cache (call after user updates credentials)
 */
function invalidateConnectionsCache() {
  _cache.connections = null;
  _cache.connTime = 0;
}

/**
 * Check if ComfyUI is installed (cached, 30s TTL)
 */
function isComfyUIInstalled() {
  const now = Date.now();
  if (_cache.comfyui !== null && now - _cache.comfyTime < CACHE_TTL) return _cache.comfyui;
  try {
    const data = JSON.parse(fs.readFileSync(COMFYUI_FILE, 'utf-8'));
    _cache.comfyui = data.installed === true;
    _cache.comfyTime = now;
    return _cache.comfyui;
  } catch {
    _cache.comfyui = false;
    _cache.comfyTime = now;
    return false;
  }
}

// ── Static prefix (stable across requests for KV cache reuse) ────────────────

/**
 * Build the static prefix: identity, rules, and guidance sections.
 * These change only when connections, ComfyUI status, or provider changes —
 * not per-request. Placed first so Ollama/llama.cpp prefix cache is reused.
 */
function _buildStaticPrefix() {
  const connections = getActiveConnections();
  const comfyui = isComfyUIInstalled();
  const { IS_WIN, IS_MAC, IS_LINUX } = require('./platform');

  const platformDesc = IS_WIN
    ? 'running locally on Windows with GPU acceleration'
    : IS_LINUX
    ? 'running locally on Linux with GPU acceleration'
    : 'running on Apple Silicon';

  let prefix = `You are PRE (Personal Reasoning Engine), a fully local agentic assistant ${platformDesc}. `
    + `All data stays on this machine. You have persistent memory across sessions.\n\n`;

  // Rules
  prefix += `RULES (follow these exactly):\n`
    + `1. NEVER output code, HTML, or file contents in chat. Use tools instead.\n`
    + `2. ALL tools (including artifact) are native function calls. Call the artifact tool directly with title, content, and type.\n`
    + `   Do NOT describe or plan what you will build — call the artifact tool IMMEDIATELY with the full HTML content.\n`
    + `   NEVER output raw HTML, <artifact> tags, or code blocks in chat.\n`
    + `   NEVER use bash with printf/cat/echo to write files. Use the file_write tool.\n`
    + `3. One tool call per turn. STOP after each call and wait for the result.\n`
    + `4. For research: call web_search 5-8 times with DIFFERENT specific queries before writing. More searches = richer content.\n`
    + `5. For reports with images: web_search first, then image_generate for each image, then artifact last.\n`
    + `6. In HTML artifacts: load CDN scripts in <head>.\n`
    + `7. For long reports: write the COMPLETE report in a single artifact call. Do NOT truncate — you have ample token budget.\n`;

  if (comfyui) {
    prefix += `8. image_generate is a WORKING native function call. It creates photorealistic images on the local GPU in ~30-45 seconds. `
      + `ALWAYS call it when images are requested. NEVER use Unsplash or external URLs instead. `
      + `The image is automatically displayed in the chat after generation — just respond naturally describing what was created. `
      + `For reports/artifacts that need the image embedded, use the returned /artifacts/ path as the src.\n`;
  }

  // Computer Use guidance
  const computerTool = require('./tools/computer');
  if (computerTool.isAvailable()) {
    // Platform-specific modifier key and app launcher references
    const modKey = IS_MAC ? 'Cmd' : 'Ctrl';
    const appSearchShortcut = IS_MAC ? 'Spotlight (Cmd+Space)' : IS_WIN ? 'Start Menu (Win key)' : 'app launcher';
    const appNameHint = IS_MAC
      ? 'Use the app\'s short name (e.g. "News" not "Apple News", "Music" not "Apple Music", "Activity Monitor", "System Settings"). If open_app fails, try without the "Apple" prefix.'
      : IS_WIN
      ? 'Use the app\'s name or executable name (e.g. "Notepad", "Calculator", "Edge"). If open_app fails, try without "Microsoft" prefix (e.g. "Edge" not "Microsoft Edge").'
      : 'Use the app\'s name or binary name (e.g. "Firefox", "Files", "Terminal"). If open_app fails, try the lowercase binary name.';
    const scriptNote = IS_MAC
      ? `- Do NOT use applescript to inspect or enumerate GUI elements. AppleScript UI introspection is fragile and fails on most modern apps. Use the computer tool to SEE the screen instead.\n`
      + `- AppleScript is fine for non-visual tasks (open an app, activate a window), but NOT for reading GUI content.\n`
      : IS_WIN
      ? `- PowerShell can be used for non-visual tasks, but do NOT use UI Automation to inspect GUI elements — use the computer tool to SEE the screen instead.\n`
      : '';
    const saveShortcut = IS_MAC ? 'Cmd+S' : 'Ctrl+S';

    prefix += `\nCOMPUTER USE (Desktop Automation):\n`
      + `You have a \`computer\` tool that takes screenshots and controls the mouse/keyboard.\n`
      + `- OPENING APPS: Always use \`open_app\` first — it's the most reliable way to launch applications. ${appNameHint} Only fall back to ${appSearchShortcut} if open_app fails.\n`
      + `- For ANY task involving a desktop GUI app, use the computer tool's vision loop: screenshot → see screen → click/type/key → screenshot → repeat.\n`
      + scriptNote
      + `- After opening an app, IMMEDIATELY take a screenshot to see what's on screen, then act on what you see.\n`
      + `- SEARCH FIRST: When looking for specific content in an app, ALWAYS use the search bar or ${modKey}+F FIRST:\n`
      + `  1. Look at the screenshot for a search bar, search icon (magnifying glass), or filter field\n`
      + `  2. If you see one, CLICK on it, then TYPE your search query, then press Return\n`
      + `  3. If no visible search bar, try ${modKey}+F (universal find shortcut) before scrolling manually\n`
      + `  4. Only browse/scroll manually if search is not available\n`
      + `- BEFORE TYPING: Always CLICK on the target text field or search bar first. Never assume a field has focus. Look at the screenshot, find the input field, click it, THEN type.\n`
      + `- MULTI-LINE TEXT: Use \\n in your type text for line breaks. Example: "Line one\\nLine two\\nLine three" — each \\n becomes a Return key press.\n`
      + `- DIALOGS & SAVE: When a dialog/popup appears (Save, Open, Confirm):\n`
      + `  • Press Return/Enter to activate the default (highlighted) button — this is fastest and most reliable\n`
      + `  • Press Escape to cancel\n`
      + `  • For Save dialogs: use ${saveShortcut}, then type the filename in the "Save As" field, then press Return to save\n`
      + `  • Do NOT try to click small dialog buttons — just use Return/Escape keyboard shortcuts\n`
      + `- Always take a screenshot after important actions (clicking, typing, pressing keys) to verify the result before proceeding.\n`
      + `- Keep your computer use sessions focused: screenshot → act → verify. Don't over-plan — react to what you see.\n`
      + `- AVOID LOOPS: If an action does NOT produce the expected result after 2 attempts, STOP and try a COMPLETELY different approach:\n`
      + `  • If pressing a key doesn't work, try clicking the target instead (or vice versa)\n`
      + `  • If ${appSearchShortcut} isn't responding, use open_app or window_focus\n`
      + `  • Take a fresh screenshot to see what actually happened\n`
      + `  • If truly stuck after 3 different approaches, STOP and report what you see to the user — do NOT keep trying variations of the same thing\n`
      + `- NEVER repeat the same action (click/key/type) more than twice. This is a hard rule.\n`
      + `- You have a LIMITED number of turns (~25). Be efficient — don't waste turns repeating failed actions.\n`
      + `- Once you have the information the user asked for, STOP taking actions and report your findings immediately.\n`;
  }

  // Experience ledger guidance
  prefix += `\nEXPERIENCE LEDGER:\n`
    + `You have an experience ledger that captures lessons from past tasks.\n`
    + `- Before attempting complex or multi-step tasks, use experience_search to check for relevant prior lessons\n`
    + `- The ledger updates automatically after tasks complete — you don't need to save lessons manually\n`
    + `- Use memory_health to check if any memories need verification or are becoming stale\n`;

  // Sub-agent guidance
  prefix += `\nSUB-AGENTS:\n`
    + `You have spawn_agent and spawn_multi tools for delegating research:\n`
    + `- Use spawn_agent when a task requires deep research (multiple searches/reads) that would consume too much of this conversation's context window\n`
    + `- Use spawn_multi when the user asks to compare, analyze, or investigate multiple independent topics (e.g. "compare frameworks A, B, and C") — agents run sequentially with progress updates\n`
    + `- Sub-agents keep the main context clean — their tool calls and outputs stay in their own session\n`
    + `- Do NOT use sub-agents for simple tasks (one file read, one search, one web fetch) — handle those directly, it's faster\n`
    + `- Each sub-agent has broad tool access: bash, file reading, web_fetch, web_search, browser (headless Chrome), cloud integrations, and more — only destructive tools (file_write, process_kill, etc.) are blocked\n`
    + `- Default timeout is 10 minutes per agent. For deep research (15+ sources, comprehensive analysis, long PDFs), pass \`timeout_minutes: 20-30\`. Max is 60. Don't extend it just because an earlier run got stuck — fix the approach (see TOOL-CHAIN FAILURES) first.\n`
    + `\n`
    + `BACKGROUND AGENTS — STRICT RULES:\n`
    + `background_agent is fire-and-forget. It runs for many minutes, notifies the user when done, and is NOT for conversational replies. Use it ONLY when ALL of the following hold:\n`
    + `1. The user EXPLICITLY asked for async/background work ("in the background", "while I do X", "kick off a long research job") OR the task genuinely needs >15 minutes of unattended tool use AND the user has signaled they will step away.\n`
    + `2. There is no existing recent result on the same topic already in this conversation. If a prior sub-agent or background agent already produced findings on this subject, ANSWER FROM THAT CONTEXT — do not spawn a new one to re-research the same ground.\n`
    + `3. The user's latest message is a real task assignment, not a short conversational follow-up like "what's happening now?", "summarize that", "what do you think?", or "tell me about X." Conversational follow-ups get answered directly with at most one or two web_search/web_fetch calls in the main turn.\n`
    + `Hard cap: at most ONE background_agent per user turn, and never more than one active on the same topic at once. Before spawning, call \`background_agent\` with action="list" if you're not sure whether one is already running. If something relevant is already running or recently completed, reference it instead of spawning a duplicate.\n`
    + `Default to NOT spawning. When in doubt, answer live with web_search and web_fetch in the main conversation — the user can always say "go deeper" and you can spawn then.\n`;

  // Tool-chain failure recovery (a previous sub-agent burned its full 10-min
  // budget chasing pip installs after pdftotext was broken; bake in the lesson)
  prefix += `\nTOOL-CHAIN FAILURES — KNOW WHEN TO BAIL:\n`
    + `When a tool fails or returns unusable output, you have a finite budget. Don't burn it on environmental yak-shaving.\n`
    + `- Try at most TWO alternative approaches for the same sub-task. If both fail, RETURN what you have with a note about what didn't work.\n`
    + `- NEVER chain "fix the environment" commands as a substitute for completing the task: do NOT run \`pip install\`, \`npm install\`, \`brew install\`, \`apt install\`, or similar — these are the user's environment to manage, not yours. If a library is missing, note it and move on.\n`
    + `- If a fetched document is binary/unreadable (PDF, image, archive), check whether a dedicated tool already handles it (web_fetch handles PDFs transparently) BEFORE shelling out to system binaries.\n`
    + `- Partial results with honest notes (\"I retrieved a snippet via web_search; the full document couldn't be parsed\") are far more useful than no results.\n`
    + `- When the same tool call is failing for similar reasons twice in a row, STOP and report. The third attempt rarely succeeds and you're burning the user's time.\n`;

  // Report quality
  prefix += `\nREPORT QUALITY STANDARDS:\n`
    + `You run locally with NO per-token cost. Output should be COMPREHENSIVE — never truncate, abbreviate, or summarize prematurely.\n`
    + `When creating reports, analyses, or research documents:\n`
    + `\n`
    + `DEPTH & LENGTH:\n`
    + `- Reports must be REFERENCE-QUALITY material — thorough enough to serve as a standalone briefing document.\n`
    + `- Each major section needs 4-8 substantive paragraphs with specific facts, data points, named sources, dates, and figures.\n`
    + `- Include analysis, not just description: explain WHY things matter, compare alternatives, assess implications, identify risks.\n`
    + `- Add sub-sections, tables, and structured breakdowns where they add clarity.\n`
    + `- A good report is 3,000-8,000 words. Short summaries are only acceptable if the user explicitly asks for a summary.\n`
    + `- Do NOT stop early. Do NOT add "Further reading" or "For more information" cop-outs. Write the actual content.\n`
    + `- When using spawn_agent for research, instruct each agent to gather DETAILED findings with specific data points, not brief summaries.\n`
    + `\n`
    + `RESEARCH RIGOR:\n`
    + `- USE SPECIFIC DATA from web_search results: names, dates, dollar amounts, percentages, named sources.\n`
    + `- INCLUDE EVERY SECTION the user requested.\n`
    + `- CHARTS must use REAL DATA from research, not placeholder numbers.\n`
    + `- Cross-reference multiple sources. Note conflicting data where it exists.\n`
    + `- Include a timeline or chronology for evolving situations.\n`
    + `- End with pdf_export if the user requested PDF output.\n`
    + `\n`
    + `HTML QUALITY:\n`
    + `- Validate your HTML: matching quotes, no duplicate CSS keywords.\n`
    + `- STYLING: Use a clean, modern light theme. White/soft grey backgrounds (#fafafa, #ffffff), dark text (#1a1a2e), readable serif headings (Georgia), generous line-height (1.65+). NEVER use dark backgrounds or prefers-color-scheme:dark media queries. Tables: light header (#334155 or similar slate, white text), subtle striped rows (#f8fafc). Emphasize whitespace, typography, and readability.\n`;

  // Image embedding in artifacts
  prefix += `\nIMAGES IN ARTIFACTS:\n`
    + `When embedding images in HTML artifacts/reports:\n`
    + `- GENERATED IMAGES: After image_generate returns a path like "/artifacts/2026-04-13/image.png", use that EXACT path in <img src="...">. NEVER use placeholder URLs or CSS gradients instead.\n`
    + `- USER-UPLOADED IMAGES: When a user uploads an image, it is auto-saved and the path appears in the message (e.g. "/artifacts/uploads/upload-123.png"). Use that path in <img src="...">.\n`
    + `- WEB IMAGES: Use web_fetch to download images from URLs. If the response is an image, it will be saved locally and a /artifacts/ path returned. Use that local path, not the original URL.\n`
    + `- NEVER use placeholder gradient divs, broken external URLs, or "image not available" fallbacks when you have an actual local image path.\n`
    + `- ALWAYS use relative paths starting with /artifacts/ for locally stored images.\n`;

  // Live artifacts
  prefix += `\nLIVE ARTIFACTS (auto-refreshing dashboards):\n`
    + `When the user asks for a dashboard, status board, or any artifact that should show CURRENT data:\n`
    + `- Create an HTML artifact with JavaScript that fetches from PRE's live data API endpoints.\n`
    + `- Each endpoint returns { data: <structured JSON>, timestamp: ISO8601 }.\n`
    + `- Endpoints and their data shapes:\n`
    + `  /api/live/calendar → data: [{title, date ("YYYY-MM-DD"), start (ISO datetime), end (ISO datetime), start_time ("HH:MM"), end_time ("HH:MM"), calendar}, ...]\n`
    + `  /api/live/calendar/week → same shape as /calendar\n`
    + `  /api/live/mail?count=N → data: [{unread, date, from, subject}, ...]\n`
    + `  /api/live/reminders → data: [{title, due, list, priority}, ...]\n`
    + `  /api/live/system → data: {hostname, os, cpu, cpu_usage (number %), memory, memory_usage (number %), disk_usage (number %), uptime, battery_percent (number or null), battery_state ("charging"|"discharging"|"unknown")}\n`
    + `  /api/live/sessions → data: [{id, name, modified, turns}, ...]\n`
    + `- Include a refresh button AND an auto-refresh interval (e.g. every 60s).\n`
    + `- Show a "Last updated: <timestamp>" indicator.\n`
    + `- Example: fetch("/api/live/calendar").then(r=>r.json()).then(d=> d.data.forEach(e=> /* e.title, e.start */))\n`
    + `- The artifact is served from the same origin (localhost:7749), so no CORS issues.\n`;

  // Native app preference guidance
  if (IS_MAC) {
    prefix += `\nNATIVE APP PREFERENCE:\n`
      + `When the user asks about email, calendar, contacts, reminders, or notes, ALWAYS use the native macOS tools FIRST:\n`
      + `- Email → apple_mail (accesses ALL accounts: iCloud, Exchange, Gmail, Outlook — whatever is configured in Mail.app)\n`
      + `- Calendar → apple_calendar (accesses all calendars in Calendar.app)\n`
      + `- Contacts → apple_contacts (accesses all contacts in Contacts.app)\n`
      + `- Reminders → apple_reminders (accesses all lists in Reminders.app)\n`
      + `- Notes → apple_notes (accesses all notes in Notes.app)\n`
      + `Only use gmail/gdrive/gdocs when the user SPECIFICALLY asks about their Google account, or when a native tool is not available.\n`
      + `The native tools are faster, require no API keys, and cover ALL the user's accounts — not just Google.\n`;
  } else if (IS_WIN) {
    prefix += `\nNATIVE APP PREFERENCE:\n`
      + `When the user asks about email, calendar, contacts, reminders, or notes, ALWAYS use the native Outlook/local tools FIRST:\n`
      + `- Email → apple_mail (uses Outlook COM — accesses ALL accounts: Exchange, Gmail, IMAP — whatever is configured in Outlook)\n`
      + `- Calendar → apple_calendar (uses Outlook COM — accesses all calendars in Outlook)\n`
      + `- Contacts → apple_contacts (uses Outlook COM — accesses all contacts in Outlook)\n`
      + `- Reminders → apple_reminders (uses Outlook Tasks — accesses all task lists in Outlook)\n`
      + `- Notes → apple_notes (uses local markdown files in ~/.pre/notes/)\n`
      + `Only use gmail/gdrive/gdocs when the user SPECIFICALLY asks about their Google account, or when Outlook is not available.\n`
      + `The native tools require no API keys and cover ALL the user's configured accounts — not just Google.\n`;
  } else if (IS_LINUX) {
    prefix += `\nNATIVE APP PREFERENCE:\n`
      + `When the user asks about calendar, contacts, reminders, or notes, use the native GNOME/local tools FIRST:\n`
      + `- Calendar → apple_calendar (uses GNOME Evolution Data Server — accesses all configured calendars)\n`
      + `- Contacts → apple_contacts (uses GNOME Evolution Data Server — accesses all address books)\n`
      + `- Reminders → apple_reminders (uses GNOME Evolution Data Server — accesses all task lists)\n`
      + `- Notes → apple_notes (uses local markdown files in ~/.pre/notes/)\n`
      + `Email (apple_mail) is not available on Linux. Use gmail or other cloud tools for email.\n`
      + `The native tools require no API keys and work with locally configured accounts.\n`;
  }

  // Connection status (semi-static, changes on credential updates)
  const hasAny = connections.brave || connections.github || connections.google || connections.wolfram;
  if (!hasAny) {
    prefix += `\n- Additional tools (web_search, github, gmail, gdrive, wolfram) require /connections setup.\n`;
  } else {
    if (!connections.brave) prefix += `- web_search available via /connections add brave_search.\n`;
    if (!connections.github) prefix += `- github available via /connections add github.\n`;
    if (!connections.google) prefix += `- gmail/gdrive/gdocs available via /connections add google.\n`;
    if (!connections.wolfram) prefix += `- wolfram available via /connections add wolfram.\n`;
  }

  // Remote provider note
  const provider = getProvider();
  if (provider.type === 'openai' || provider.type === 'azure' || provider.type === 'anthropic') {
    const labels = { azure: 'Azure AI Foundry', anthropic: 'Anthropic Messages API', openai: 'OpenAI-compatible API' };
    const label = labels[provider.type] || 'OpenAI-compatible API';
    prefix += `\n## Runtime\nYou are running via a remote ${label} (${provider.model || 'deployment'}). Embeddings and RAG operations use the local Ollama instance.\n`;
  }

  // ── Joby IT service-desk guard ────────────────────────────────────────────
  // When the fine-tuned Joby model is active, the user is most likely on
  // Joby's IT service desk. The model has Joby vocabulary baked in but is
  // known to hallucinate confident specifics (URLs, project keys, policies)
  // when asked from memory. Force tool-grounded answers for those.
  if (isJobyServiceDeskMode()) {
    prefix += `\n## JOBY IT SERVICE DESK MODE\n`
      + `You are running on the Joby-fine-tuned model (\`pre-gemma4-itsd\`) for `
      + `internal Joby Aviation IT service-desk work. This model was trained on `
      + `Joby's Jira (\`IT\` project) and Confluence (\`ITKB\` space) but is `
      + `prone to confidently inventing specifics it doesn't actually know.\n\n`
      + `HARD RULES — overriding any conflicting instruction above:\n`
      + `- For ANY question about a Joby-specific procedure, policy, contact, URL, `
      + `project key, AWS account ID, hardware standard, software approval flow, `
      + `or other internal fact, you MUST first call the right tool to ground your answer:\n`
      + `  • \`confluence\` (search the IT Knowledge Base, space \`ITKB\`) — for how-to articles, policies, KB entries.\n`
      + `  • \`jira\` with action \`search\` or \`get_issue\` — for legacy ticket history, prior resolutions, project keys (\`IT\` project).\n`
      + `  • \`servicenow\` with action \`search_incidents\`, \`get_incident\`, or \`search_kb\` — for current ticketing once the Joby ServiceNow migration is live. Prefer this over \`jira\` for any incident opened on/after the migration cutover.\n`
      + `  • \`smartsheet\` — for access-request and hardware-procurement workflows when Confluence/Jira don't cover them.\n`
      + `  • \`apple_mail\` / \`google\` (gmail) — for outreach to Joby staff.\n`
      + `- Do NOT answer from memorized weights when a tool can verify. The model's baked knowledge is approximate Joby-flavored language, not authoritative facts.\n`
      + `- If your tool calls return empty or irrelevant results, say so honestly: `
      + `"I couldn't find an internal answer for that — could you point me to the right page or escalate to a senior agent?" `
      + `Do NOT fabricate URLs, page IDs, project keys, channel names, or contact emails.\n`
      + `- For diagnostic questions (e.g. "MacBook is kernel-panicking"), it's fine to suggest standard troubleshooting steps from general technical knowledge — but flag any company-specific recommendation ("Joby uses X for Y") as needing verification.\n`
      + `- When citing a Confluence page or Jira ticket in your final answer, include the link or key the tool actually returned, not one you invented.\n\n`
      + `The Joby Jira IT project key is \`IT\`. The Joby Confluence IT Knowledge Base space key is \`ITKB\`. These are baked into your model card and safe to assert without a tool lookup; everything else needs verification.\n`;
  }

  return prefix;
}

// ── Self-awareness context (operational state) ───────────────────────────────

/**
 * Build a concise block summarizing PRE's current operational state.
 * This lets the model know what capabilities are active, what's running,
 * and where it's strong/weak — without the model having to ask.
 */
function _buildSelfAwarenessContext() {
  const lines = [];

  // ── Calibration confidence ─────────────────────────────────────────────────
  try {
    const { getConfidenceReport } = require('./calibration');
    const report = getConfidenceReport();
    if (report && report.categories && Object.keys(report.categories).length > 0) {
      const strengths = [];
      const cautions = [];
      for (const [, cat] of Object.entries(report.categories)) {
        if (cat.confidence >= 85) strengths.push(cat.label);
        else if (cat.confidence < 60) cautions.push(cat.label);
      }
      if (strengths.length) lines.push(`Strengths (≥85% confidence): ${strengths.join(', ')}`);
      if (cautions.length) lines.push(`Caution areas (<60%): ${cautions.join(', ')}`);
      if (report.overall?.confidence != null) {
        lines.push(`Overall confidence: ${Math.round(report.overall.confidence * 100)}% (${report.overall?.total || 0} observations)`);
      }
    }
  } catch {}

  // ── Background agents ──────────────────────────────────────────────────────
  try {
    const bgAgents = require('./background-agents');
    const q = bgAgents.getQueueStatus();
    if (q.total > 0) {
      const parts = [];
      if (q.running) parts.push(`${q.running} running`);
      if (q.queued) parts.push(`${q.queued} queued`);
      if (q.paused) parts.push(`${q.paused} paused`);
      if (q.completed) parts.push(`${q.completed} completed`);
      if (q.failed) parts.push(`${q.failed} failed`);
      lines.push(`Background agents: ${parts.join(', ')} (${q.total} total)`);
    }
  } catch {}

  // ── Triggers ───────────────────────────────────────────────────────────────
  try {
    const { loadTriggers } = require('./triggers');
    const triggers = loadTriggers();
    if (triggers.length > 0) {
      const active = triggers.filter(t => t.enabled !== false).length;
      lines.push(`Triggers: ${active} active of ${triggers.length} defined`);
    }
  } catch {}

  // ── Cron jobs ──────────────────────────────────────────────────────────────
  try {
    const cronData = JSON.parse(fs.readFileSync(CRON_FILE, 'utf-8'));
    const jobs = cronData.jobs || cronData;
    if (Array.isArray(jobs) && jobs.length > 0) {
      const enabled = jobs.filter(j => j.enabled !== false).length;
      lines.push(`Cron jobs: ${enabled} active of ${jobs.length} scheduled`);
    }
  } catch {}

  // ── Custom tools ───────────────────────────────────────────────────────────
  try {
    const { loadCustomTools } = require('./custom-tools');
    const customs = loadCustomTools();
    if (customs.length > 0) {
      lines.push(`Custom tools: ${customs.length} defined`);
    }
  } catch {}

  // ── Router state ───────────────────────────────────────────────────────────
  try {
    const { getConfig: getRouterConfig } = require('./router');
    const rc = getRouterConfig();
    if (rc.enabled) {
      const parts = [`fast model: ${rc.fast_model || 'none'}`];
      if (rc.frontier_enabled) parts.push('frontier routing enabled');
      if (rc.auto_escalate) parts.push('auto-escalate on');
      lines.push(`Router: ${parts.join(', ')}`);
    }
  } catch {}

  // ── Memory + experience counts ─────────────────────────────────────────────
  try {
    const { getAllMemories } = require('./memory');
    const { listExperiences } = require('./experience');
    const memCount = getAllMemories().length;
    const expCount = listExperiences().length;
    const parts = [];
    if (memCount) parts.push(`${memCount} memories`);
    if (expCount) parts.push(`${expCount} experience entries`);
    if (parts.length) lines.push(`Knowledge base: ${parts.join(', ')}`);
  } catch {}

  // ── Training exports ───────────────────────────────────────────────────────
  try {
    const { listExports } = require('./training');
    const exports = listExports();
    if (exports.length > 0) {
      lines.push(`Training exports: ${exports.length} datasets available`);
    }
  } catch {}

  // ── Session stats ──────────────────────────────────────────────────────────
  try {
    const { listSessions, listProjects } = require('./sessions');
    const sessions = listSessions();
    const projects = listProjects();
    lines.push(`Sessions: ${sessions.length} across ${projects.length} projects`);
  } catch {}

  // ── Knowledge graph ────────────────────────────────────────────────────────
  try {
    const { getStats } = require('./knowledge-graph');
    const stats = getStats();
    if (stats.entityCount > 0) {
      lines.push(`Knowledge graph: ${stats.entityCount} entities, ${stats.edgeCount} edges`);
    }
  } catch {}

  if (lines.length === 0) return '';
  return `<self_awareness>\n${lines.join('\n')}\n</self_awareness>\n`;
}

// ── Dynamic suffix (changes per request) ──────────────────────────────────────

/**
 * Build the dynamic suffix: memory, experience, temporal context, cwd, date.
 * These change per-request and follow the static prefix.
 */
function _buildDynamicSuffix(cwd) {
  const memory = buildMemCtx();
  const memoryInstructions = buildMemoryInstructions();

  // Directory listing
  let filesList = '';
  try {
    const entries = fs.readdirSync(cwd);
    for (const entry of entries.slice(0, 40)) {
      if (entry.startsWith('.')) continue;
      try {
        const stat = fs.statSync(path.join(cwd, entry));
        if (stat.isDirectory()) {
          filesList += `  ${entry}/\n`;
        } else {
          const size = stat.size < 1024 ? `${stat.size}B`
            : stat.size < 1048576 ? `${(stat.size / 1024).toFixed(0)}K`
            : `${(stat.size / 1048576).toFixed(1)}M`;
          filesList += `  ${entry} (${size})\n`;
        }
      } catch {}
    }
  } catch {}

  // Date
  const now = new Date();
  const days = ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'];
  const months = ['January','February','March','April','May','June',
    'July','August','September','October','November','December'];
  const dateStr = `${days[now.getDay()]}, ${months[now.getMonth()]} ${now.getDate()}, ${now.getFullYear()}`;

  let suffix = '\n';
  if (memory) suffix += memory + '\n';
  suffix += memoryInstructions + '\n';

  // Experience ledger — lessons from past tasks
  const experienceCtx = buildExperienceContext();
  if (experienceCtx) suffix += experienceCtx + '\n';

  // Temporal awareness — stale memory warnings, time context
  const temporalCtx = buildTemporalContext();
  if (temporalCtx) suffix += temporalCtx + '\n';

  // GPU status — tinygrad inference + training capabilities
  try {
    const { gpuDetector } = require('./gpu-detect');
    const gpuState = gpuDetector.getState();
    if (gpuState && gpuState.available) {
      suffix += `[eGPU: ${gpuState.name || 'detected'}${gpuState.vram_gb ? `, ${gpuState.vram_gb}GB VRAM` : ''} — tinygrad inference + LoRA training available via DEV=NV]\n`;
    }
  } catch {}

  // Self-awareness — operational state summary
  const selfAwareness = _buildSelfAwarenessContext();
  if (selfAwareness) suffix += selfAwareness + '\n';

  suffix += `<context>\nWorking directory: ${cwd}\nFiles:\n${filesList}</context>\n\n`;
  suffix += `Today is ${dateStr}. Use this when interpreting relative dates.\n\n`;

  return suffix;
}

// ── Public API ────────────────────────────────────────────────────────────────

/**
 * Build the full system prompt preamble.
 * Static prefix (rules, guidance) comes first for KV cache reuse.
 * Dynamic suffix (memory, experience, date, cwd) follows.
 */
function buildSystemPrompt(cwd) {
  return _buildStaticPrefix() + _buildDynamicSuffix(cwd);
}

/**
 * Async version: uses embedding-based relevance ranking for memory and experience context.
 * When a userQuery is provided, memories and experiences are ranked by semantic similarity
 * to the current conversation, surfacing the most relevant context instead of dumping all.
 */
async function buildSystemPromptAsync(cwd, userQuery) {
  // Build the base prompt synchronously (connections, rules, etc.)
  const basePrompt = buildSystemPrompt(cwd);

  if (!userQuery) return basePrompt;

  // Replace the sync memory/experience blocks with relevance-ranked versions
  try {
    const [rankedMemory, rankedExperience] = await Promise.all([
      buildMemoryContextAsync(null, userQuery),
      buildExperienceContextAsync(userQuery),
    ]);

    let prompt = basePrompt;

    // Replace the memory block if we got a ranked version
    if (rankedMemory) {
      const memStart = prompt.indexOf('<memory>');
      const memEnd = prompt.indexOf('</memory>');
      if (memStart >= 0 && memEnd >= 0) {
        prompt = prompt.slice(0, memStart) + rankedMemory + prompt.slice(memEnd + '</memory>'.length);
      }
    }

    // Replace the experience block if we got a ranked version
    if (rankedExperience) {
      const expStart = prompt.indexOf('<experience_ledger>');
      const expEnd = prompt.indexOf('</experience_ledger>');
      if (expStart >= 0 && expEnd >= 0) {
        prompt = prompt.slice(0, expStart) + rankedExperience + prompt.slice(expEnd + '</experience_ledger>\n'.length);
      }
    }

    // Append calibrated self-knowledge (strengths/weaknesses from past performance)
    const calibration = buildCalibrationContext();
    if (calibration) prompt += '\n\n' + calibration;

    // Append skill recommendations based on the user's query
    const skillRecs = buildSkillRecommendationContext(userQuery);
    if (skillRecs) prompt += '\n\n' + skillRecs;

    // Append knowledge graph context — related entities and relationships
    try {
      const { buildGraphContext } = require('./knowledge-graph');
      const graphCtx = await buildGraphContext(userQuery);
      if (graphCtx) prompt += '\n\n' + graphCtx;
    } catch {}

    return prompt;
  } catch (_e) {
    // If embedding fails, return the base prompt with default memory/experience
    return basePrompt;
  }
}

module.exports = { buildSystemPrompt, buildSystemPromptAsync, getActiveConnections, isComfyUIInstalled, invalidateConnectionsCache, isJobyServiceDeskMode };
