/**
 * Anthropic tool definitions for the madarX orchestrator.
 * Each tool maps to a runtime function. executeTool() dispatches by name.
 */

import fs from 'node:fs';
import path from 'node:path';
import {
  appendAudit,
  createAgent,
  upsertSchedule,
  competitorReport,
  marketingPlan,
  heartbeatTick,
  claudeDryRun,
  getState,
  createArtifact,
  updateArtifact,
  deleteArtifact,
  moveArtifact,
  listAgents,
  setAgentWhatsAppNotify,
  findArtifacts,
  getArtifact,
} from './runtime.js';
import { runAgentTick } from './agent-runner.js';
import { chatWithClaude } from './claude.js';
import { runHermesCommand, isHermesAvailable } from './hermes.js';
import { createHermesJob, startHermesJobWorker } from './hermes-jobs.js';
import { buildHermesArtifactPrompt, enforceHermesArtifactFormat } from './hermes-format.js';
import { getDesignStyle } from './agent-config.js';
import { stripEmoji } from './artifact-format.js';
import {
  brandInstructionText,
  getBrandProfile,
  importBrandGuidelinesFromContent,
  importBrandGuidelinesFromPath,
  importBrandLogoFromPath,
} from './brand.js';
import { resolveSafePath } from './tools/safety.js';
import { executeRegistryTool, getEnabledToolSchemas, listMarketplaceTools, setToolEnabled } from './tools/registry.js';
import { listMarketplaceItems } from './marketplace.js';
import { createTask, listTasks } from './delegation.js';
import { runSingleReport } from './mines/diligence.js';
import { getBridgeHealth, sendWhatsAppDocument, sendWhatsAppMessage } from './whatsapp.js';
import { listProjects } from './projects.js';
import {
  createSpace,
  deleteSpace,
  listSpaces,
  listSpacesWithArtifactIds,
  moveArtifactToSpace,
  renameSpace,
  setActiveSpace,
} from './dashboard-spaces.js';

// ── generate_ui implementation ────────────────────────────────────────────────

const DESIGN_RULES = `You are a senior UI/UX engineer producing a production-grade HTML file.

ABSOLUTE BANS — zero tolerance:
- ZERO EMOJIS — anywhere, ever. No emoji icons, no decorative emoji, no bullet emoji, no status emoji, no Unicode symbols used as graphics. This rule has no exceptions regardless of style or context.
- ALL icons and illustrations must be hand-drawn inline SVG. Draw them in the Lucide/Heroicons style: 24×24 viewBox, 1.5-2px stroke, strokeLinecap="round", strokeLinejoin="round", fill="none". For decorative illustrations use CSS shapes or geometric SVG — never clip-art, stock emoji, or Unicode glyphs.
- NO generic dark themes with uniform gray cards and no hierarchy
- NO default system fonts (Inter, Roboto, Arial) without design justification
- NO gradient text
- NO side-stripe colored card borders (left/top accent strips) — this is AI slop
- NO centered-everything monotonous layouts — break symmetry, create visual tension
- NO uniform identical card grids — vary sizes, emphasis, weight
- NO placeholder-quality elements — every part must be production-ready

TYPOGRAPHY:
- Clear scale: display (56-96px, lh 0.9-1.0), heading (24-40px, lh 1.1-1.2), body (15-17px, lh 1.6-1.7), caption (11-13px)
- Tight letter-spacing on large type (−0.04em to −0.08em), normal on body
- Import one quality Google Font or use system-ui with a strong type scale

COLOR:
- Max 2 accent colors, intentional palette, not random
- Strong contrast ratios — text must be readable
- Use color purposefully to guide hierarchy

LAYOUT:
- Asymmetric or varied — not monotonous equal grids
- Generous whitespace (48-96px section padding)
- Clear visual hierarchy directing the eye

MOTION:
- Subtle transitions: 150-300ms ease
- No bounce, elastic, or excessive animation
- Hover states on all interactive elements

VISIBILITY — non-negotiable:
- ALL content must be fully visible immediately on page load — no exceptions
- NEVER set opacity: 0, visibility: hidden, or display: none on any content element as an initial state
- NEVER use IntersectionObserver, scroll event listeners, or any scroll-based reveals
- NEVER use [data-aos], AOS library, ScrollReveal, or any third-party scroll animation library
- CSS transitions and animations (fade-in, slide-in) are only allowed for elements that START visible (opacity: 1)

JAVASCRIPT — strict limits:
- Use JavaScript only for essential interactions: tab switching, accordion open/close, modal open/close, slide-deck navigation
- NO scroll-triggered effects or animations of any kind
- NO IntersectionObserver, scroll event listeners, or scroll-based reveals
- NO parallax, sticky-header shrink effects, or scroll-based class toggling
- NO animation libraries (GSAP, AOS, Animate.css, etc.)
- All motion must be CSS transitions/animations only — not JavaScript-driven
- If in doubt, leave JavaScript out entirely

PRESENTATIONS / SLIDE DECKS — when the request is for a multi-page deck, pitch, slides, or "presentation":
- Wrap slides in a container with class="slides" (or include the word "slide" in the container class). Each slide gets class="slide" (or similar — the substring "slide" must appear in the class name). This signals the renderer that these elements use intentional opacity/visibility toggling.
- Show exactly one slide at a time. Toggle a CSS class (e.g. ".slide.active") to show the current slide. Use CSS opacity/visibility OR display:none — both work because slide containers are exempt from the renderer's reveal pass.
- Navigation controls must be <button> elements, NOT <a href="#"> anchors. Listen for click events plus keyboard ArrowLeft / ArrowRight on document.
- Show a slide counter formatted with leading zero: "01 / 09". Update it whenever the active slide changes.
- Do not use horizontal scroll, transform: translateX(-N*100%) sliders, or any technique that depends on the iframe being a specific size — slides should fit the iframe viewport individually.
- Each slide should fill the iframe viewport (100vh / 100vw container, slides absolutely positioned or flex-centered).

TECHNICAL:
- Single complete HTML file, CSS embedded in <style>
- No external dependencies except Google Fonts
- Fully responsive (mobile / tablet / desktop)
- Semantic HTML5

OUTPUT: HTML only. No markdown fences. No explanation. Start with <!DOCTYPE html>.`;

const STYLE_ADDONS = {
  balanced: '',
  minimal: '\nSTYLE — MINIMAL: Extreme restraint. Near-monochrome palette (black, white, one subtle accent). Maximum whitespace. Typography-first layout. No decorative elements. Every pixel earns its place.',
  bold: '\nSTYLE — BOLD: High contrast. Large commanding typography. Strong visual weight. Dark backgrounds or very high-contrast light. Dramatic hierarchy. Assertive, confident presence.',
  soft: '\nSTYLE — SOFT: Warm gentle palette. Rounded corners everywhere (16-24px). Subtle soft shadows. Light, airy feel. Pastel or muted accent colors. Comfortable, inviting reading experience.',
};

async function generateUi({ description, project_id = null }) {
  const style = getDesignStyle();
  const brand = getBrandProfile();
  const systemPrompt = `${DESIGN_RULES}${STYLE_ADDONS[style] ?? ''}\n\n${brandInstructionText(brand)}`;
  const response = await chatWithClaude(
    [{ role: 'user', content: `Build this UI: ${description}` }],
    systemPrompt
  );
  const content = stripEmoji(response.content[0]?.text?.trim() ?? '');
  const title = description.slice(0, 80);
  const result = createArtifact({ title, kind: 'html', content, project_id });
  return { ...result, style };
}

async function sendToHermes({ prompt, title, session_id, model, timeout_ms, project_id = null }) {
  const hermesPrompt = buildHermesArtifactPrompt(prompt);
  const hermes = await runHermesCommand({ prompt: hermesPrompt, session_id, model, timeout_ms });
  const artifactTitle = title || `Hermes result: ${prompt.slice(0, 60)}`;
  const formatted = enforceHermesArtifactFormat(hermes.response, { title: artifactTitle });

  // Artifact body is the Hermes answer only — no prompt echo, no trace dump.
  // Format violations still surface so the human can see why output was reshaped.
  const violationNote = formatted.violations?.length
    ? `\n\n---\n\n*Format enforcement — source: ${formatted.source}; violations: ${formatted.violations.join(', ')}*`
    : '';
  const content = `${formatted.content || '(no response)'}${violationNote}`;

  return {
    ...createArtifact({ title: artifactTitle, kind: 'markdown', content, project_id }),
    hermes_session_id: hermes.session_id,
    hermes_model: hermes.model,
    format_source: formatted.source,
    format_ok: formatted.ok,
    format_violations: formatted.violations,
    mocked: hermes.mocked === true,
  };
}

async function startHermesJob({ prompt, title, session_id, model, timeout_ms, cwd, project_id = null }) {
  const job = createHermesJob({ prompt, title, session_id, model, timeout_ms, cwd, project_id });
  if (process.env.HERMES_JOB_DISABLE_SPAWN !== '1') {
    startHermesJobWorker(job.id);
  }
  return {
    job_id: job.id,
    artifact_id: job.artifact_id,
    run_id: job.run_id,
    file_path: job.file_path,
    status: job.status,
    title: job.title,
  };
}

async function importBrandGuidelinesTool(input = {}) {
  let brand;
  let importedFrom = null;

  if (input.file_path) {
    const { path: absPath } = resolveSafePath(input.file_path);
    const stat = fs.statSync(absPath);
    if (!stat.isFile()) throw new Error('Brand guidelines path is not a file');
    if (stat.size > 2_000_000) throw new Error(`Brand guidelines file is too large: ${stat.size} bytes`);
    const ext = path.extname(absPath).toLowerCase();
    if (!['.html', '.htm', '.md', '.txt', '.css', '.svg', '.pdf'].includes(ext)) {
      throw new Error('Supported guidelines formats: .html, .md, .txt, .css, .svg, .pdf');
    }
    brand = importBrandGuidelinesFromPath(absPath);
    importedFrom = absPath;
  } else if (input.content) {
    brand = importBrandGuidelinesFromContent(input.content, input.filename || 'inline-brand-guidelines.md');
    importedFrom = input.filename || 'inline content';
  } else {
    throw new Error('Provide either file_path or content');
  }

  if (input.logo_path) {
    const { path: logoPath } = resolveSafePath(input.logo_path);
    const stat = fs.statSync(logoPath);
    if (!stat.isFile()) throw new Error('Logo path is not a file');
    if (stat.size > 1_000_000) throw new Error(`Logo file is too large: ${stat.size} bytes`);
    const ext = path.extname(logoPath).toLowerCase();
    if (!['.svg', '.png', '.jpg', '.jpeg', '.webp'].includes(ext)) {
      throw new Error('Supported logo formats: .svg, .png, .jpg, .jpeg, .webp');
    }
    brand = importBrandLogoFromPath(logoPath);
  }

  return {
    ok: true,
    imported_from: importedFrom,
    brand: {
      active: brand.active,
      company_name: brand.company_name,
      colors: brand.colors,
      fonts: brand.fonts,
      voice: brand.voice,
      document_types: brand.document_types,
      presentation_style: brand.presentation_style,
      guidelines_path: brand.guidelines_path,
      logo_path: brand.logo_path,
    },
  };
}

function textFromClaudeResponse(response) {
  return (response?.content || [])
    .filter(part => part?.type === 'text' || typeof part?.text === 'string')
    .map(part => part.text || '')
    .join('\n')
    .trim();
}

function fallbackSkillsMarkdown({ name, mission, tools }) {
  const toolLine = tools.length ? tools.join(', ') : 'web search, browser, files';
  return `# ${name} Skills

## Mission
${mission}

## Core playbooks
- Plan each run from the mission above, then execute against the schedule window.
- Use only the approved tools (${toolLine}); never reach for tools the owner has not enabled.
- Save every output as a local artifact under your project's artifacts folder (workspace/projects/<slug>/artifacts/).
- Hand off any external send, post, deployment, or destructive command to the main agent for approval before acting.
- After each run, update heartbeat.json with status, summary, and next-run plan.
`;
}

async function generateAgentSkillsMarkdown({ name, mission, tools }) {
  try {
    const response = await chatWithClaude(
      [{
        role: 'user',
        content: `Write the skills.md file for a new local sub-agent.

<agent_name>${name}</agent_name>
<agent_mission>${mission}</agent_mission>
<allowed_tools>${tools.join(', ') || 'web-search, browser, files, cron'}</allowed_tools>

Return a markdown document with:
- An H1 title "# ${name} Skills"
- A short "## Mission" section restating the mission in one line.
- A "## Core playbooks" section with 4 to 6 bullets, each a concrete, mission-specific play this agent will run (not generic boilerplate). Bullets must reflect the mission and use the listed tools where relevant.
- A "## Operating limits" section with 2-3 bullets covering approval, workspace boundaries, and heartbeat updates.

Hard rules: zero emojis, no decorative symbols, no horizontal rules, plain markdown only, no extra commentary outside the file content.`,
      }],
      'You write concise, mission-specific playbooks for local-first AI agents. Output the markdown file content only, no preamble.'
    );
    const text = textFromClaudeResponse(response);
    if (!text) return null;
    return text;
  } catch {
    return null;
  }
}

async function createAgentWithFocusedPrompt(input) {
  const tools = Array.isArray(input.tools)
    ? input.tools
    : String(input.tools || '').split(',').map(s => s.trim()).filter(Boolean);
  const name = input.name || 'madarX Custom Agent';
  const mission = input.mission || 'Support the user with scheduled local work.';
  let systemPrompt = null;
  try {
    const response = await chatWithClaude(
      [{
        role: 'user',
        content: `Create a focused system prompt for this scheduled local agent.

<agent_name>${name}</agent_name>
<agent_mission>${mission}</agent_mission>
<allowed_tools>${tools.join(', ')}</allowed_tools>

Return only the system prompt text. It must preserve human approval before external sends/posts/deployments and keep work inside the local workspace.`,
      }],
      'You write concise, operational system prompts for local-first AI agents. Return plain text only.'
    );
    systemPrompt = textFromClaudeResponse(response);
  } catch {
    systemPrompt = null;
  }

  const llmSkills = await generateAgentSkillsMarkdown({ name, mission, tools });
  const skills_markdown = stripEmoji(llmSkills || fallbackSkillsMarkdown({ name, mission, tools }));

  const payload = { ...input, name, mission, tools, skills_markdown };
  if (systemPrompt) payload.system_prompt = systemPrompt;
  if (input.whatsapp_notify_chat_id !== undefined) {
    payload.whatsapp_notify_chat_id = input.whatsapp_notify_chat_id || null;
  }
  return createAgent(payload);
}

// ── Marketplace self-management implementations ───────────────────────────────

/**
 * list_available_tools — pure read; no side effects.
 * Returns every entry from listMarketplaceTools() filtered/shaped for the LLM.
 */
function listAvailableToolsHandler({ category, enabled_only } = {}) {
  const all = listMarketplaceTools();
  let items = all;
  if (category) {
    const needle = String(category).toLowerCase();
    items = items.filter(t => (t.category || '').toLowerCase() === needle);
  }
  if (enabled_only) {
    items = items.filter(t => t.enabled);
  }
  return {
    ok: true,
    count: items.length,
    tools: items.map(t => ({
      name: t.name,
      title: t.title,
      description: t.description,
      category: t.category,
      enabled: t.enabled,
      available: t.available,
      risk_level: t.risk_level,
      allowlist_required: t.allowlist_required,
      missing_env: t.missing_env,
    })),
  };
}

/**
 * install_tool — enables a marketplace tool by name.
 * Tools rated irreversible_external surface an informational note;
 * the actual irreversible gate runs inside executeRegistryTool when the tool
 * is invoked — not at install time (enabling ≠ executing).
 */
function installToolHandler({ tool_name } = {}) {
  if (!tool_name || typeof tool_name !== 'string') {
    return { ok: false, error: 'tool_name is required' };
  }
  try {
    const updated = setToolEnabled(tool_name, true);
    if (!updated) return { ok: false, error: `Unknown tool: ${tool_name}` };
    const result = {
      ok: true,
      status: 'enabled',
      tool_name: updated.name,
      title: updated.title,
      category: updated.category,
      risk_level: updated.risk_level,
    };
    if (updated.missing_env && updated.missing_env.length > 0) {
      result.missing_credentials = updated.missing_env;
      result.note = `Tool enabled but not yet usable — configure: ${updated.missing_env.join(', ')}`;
    }
    if (updated.risk_level === 'irreversible_external') {
      result.warning = 'This tool is rated irreversible_external. Each call will require owner approval regardless of trust mode.';
    }
    return result;
  } catch (err) {
    return { ok: false, error: err.message };
  }
}

/**
 * uninstall_tool — disables a marketplace tool by name.
 */
function uninstallToolHandler({ tool_name } = {}) {
  if (!tool_name || typeof tool_name !== 'string') {
    return { ok: false, error: 'tool_name is required' };
  }
  try {
    const updated = setToolEnabled(tool_name, false);
    if (!updated) return { ok: false, error: `Unknown tool: ${tool_name}` };
    return {
      ok: true,
      status: 'disabled',
      tool_name: updated.name,
      title: updated.title,
    };
  } catch (err) {
    return { ok: false, error: err.message };
  }
}

const TOOL_SMITH_AGENT_ID = 'tool-smith-agent';
const CAPABILITY_RATE_WINDOW_MS = 60 * 60 * 1000; // 1h
const CAPABILITY_RATE_MAX = 5; // max open build requests per window

function tokenize(s) {
  return String(s || '')
    .toLowerCase()
    .replace(/[._-]+/g, ' ')
    .split(/[^a-z0-9]+/)
    .filter((w) => w.length >= 3);
}

/**
 * Best-effort dedupe: scan every existing capability (tools incl. composites +
 * connector endpoints, and skills) for one that already covers the request. A
 * false negative just means a duplicate build is queued; a false positive is
 * avoided by requiring real token overlap, not a single common word.
 */
function findMatchingCapability(description) {
  const want = new Set(tokenize(description));
  if (!want.size) return null;
  const candidates = [];
  try {
    for (const t of listMarketplaceTools()) {
      candidates.push({ name: t.name, kind: 'tool', text: `${t.name} ${t.title || ''} ${t.description || ''}` });
    }
  } catch { /* ignore */ }
  try {
    for (const s of listMarketplaceItems({ kind: 'skill' })) {
      candidates.push({ name: s.slug, kind: 'skill', text: `${s.slug} ${s.title || ''} ${s.description || ''}` });
    }
  } catch { /* ignore */ }
  for (const c of candidates) {
    // Direct name mention is a strong signal.
    const nameTokens = tokenize(c.name);
    if (nameTokens.length && nameTokens.every((tk) => want.has(tk))) {
      return { name: c.name, kind: c.kind };
    }
    // Otherwise require >=2 overlapping meaningful tokens with the name/title.
    const titleTokens = new Set(tokenize(c.text));
    let overlap = 0;
    for (const w of want) if (titleTokens.has(w)) overlap++;
    if (overlap >= 2 && nameTokens.some((tk) => want.has(tk))) {
      return { name: c.name, kind: c.kind };
    }
  }
  return null;
}

function requestCapabilityHandler(input = {}, context = {}) {
  const description = String(input.capability_description || '').trim();
  if (!description) return { ok: false, error: 'capability_description is required' };

  // 1) Dedupe — never spawn a build for a capability that already exists.
  const match = findMatchingCapability(description);
  if (match) {
    return {
      ok: true,
      status: 'exists',
      match,
      message: `A capability already covers this: ${match.name} (${match.kind}). Grant it instead of rebuilding.`,
    };
  }

  // 2) Rate-limit — cap concurrent build requests so a loop can't flood Tool Smith.
  try {
    const cutoff = Date.now() - CAPABILITY_RATE_WINDOW_MS;
    const recent = listTasks({ assignee_agent_id: TOOL_SMITH_AGENT_ID, limit: 50 })
      .filter((t) => {
        if (['done', 'cancelled'].includes(t.status)) return false;
        const ts = Date.parse(t.created_at || '');
        return Number.isFinite(ts) && ts >= cutoff;
      });
    if (recent.length >= CAPABILITY_RATE_MAX) {
      return {
        ok: false,
        status: 'rate_limited',
        message: `Too many capability build requests in flight (${recent.length}). Wait for some to finish.`,
      };
    }
  } catch { /* non-fatal — proceed */ }

  // 3) Delegate — file a build task; createTask wakes tool-smith-agent via heartbeat.
  try {
    const target = input.target_agent_id ? String(input.target_agent_id) : null;
    const task = createTask({
      title: `Build capability: ${description.slice(0, 80)}`,
      description: [
        `A capability is needed that does not exist yet:`,
        `  "${description}"`,
        '',
        'Choose the right authoring path and build it:',
        '  - composite tool (chain existing tools), or',
        '  - REST connector (new API + secret-by-reference), or',
        '  - skill (instruction recipe), or',
        '  - propose_tool (non-REST; staged code, human merge).',
        '',
        target ? `Grant the result to agent: ${target}.` : 'Return the slug so it can be granted to the requester.',
      ].join('\n'),
      acceptance_criteria: input.acceptance_criteria
        || 'Capability authored and its slug returned so it can be granted to the requesting agent.',
      assignee_agent_id: TOOL_SMITH_AGENT_ID,
      created_by_agent_id: context.agent_id || 'main-orchestrator',
      priority: 'p1',
      project_id: context.project_id || null,
    });
    return {
      ok: true,
      status: 'requested',
      task_id: task.id,
      assignee: TOOL_SMITH_AGENT_ID,
      target_agent_id: target,
      message: `Delegated to Tool Smith (task ${task.id}). It will author the capability and return a slug to grant.`,
    };
  } catch (err) {
    return { ok: false, status: 'error', error: 'tool_smith_unavailable', message: String(err?.message || err) };
  }
}

// ── Tool definitions (Anthropic tool_use schema) ──────────────────────────────

export const CORE_TOOLS = [
  {
    name: 'create_agent',
    description:
      'Create a new named sub-agent in the local workspace with files and optional schedule. Pick the engine based on what the agent actually does (see engine field).',
    input_schema: {
      type: 'object',
      properties: {
        name: { type: 'string', description: 'Human-readable agent name.' },
        mission: { type: 'string', description: 'What this agent does (its standing job — used as the default instruction every run).' },
        expected_output: {
          type: 'string',
          description: "OPTIONAL standing deliverable contract: what a GOOD result looks like every run — format, required sections, acceptance criteria, definition of done. Set this whenever the agent has a repeatable output shape so runs are consistent and verifiable. A per-call run_agent override or a delegated task's acceptance_criteria takes precedence over this for a specific run.",
        },
        schedule: {
          type: 'string',
          description: 'Cron-like schedule: "daily HH:MM", "every 5m", or "on demand".',
        },
        tools: {
          type: 'array',
          items: { type: 'string' },
          description: 'madarX tool identifiers the agent is allowed to use (e.g. "web_search", "web_fetch", "file_read", "create_artifact"). Lowercase snake_case, NOT Claude Code tool names.',
        },
        type: { type: 'string', description: 'Agent type label, e.g. "custom" or "intelligence".' },
        engine: {
          type: 'string',
          enum: ['anthropic', 'claude-code', 'ollama', 'codex', 'hermes'],
          description: 'DEFAULT is "claude-code" — uses the local Claude Code CLI with permissions from .claude/settings.json (Read/Glob/Grep/WebSearch/WebFetch auto-allowed; edits scoped to workspace/). Use this for almost everything. "anthropic" only when the agent needs the madarx tool marketplace specifically (create_artifact, generate_ui, whatsapp_send, competitor_report, marketing_plan — these do not exist under claude-code). "ollama" for cheap local-LLM work. "codex" for OpenAI Codex CLI. "hermes" rare. If the user does not specify, leave engine unset and the default applies.',
        },
        whatsapp_notify_chat_id: {
          type: 'string',
          description: 'OPTIONAL WhatsApp chat id (e.g. 9715XXXXXXXX@s.whatsapp.net, or plain digits). When set, every scheduled run that produces a report artifact will auto-deliver the rendered HTML file to this chat. The bridge must be paired; if not, delivery is skipped silently. Set this only when the owner explicitly says "send to whatsapp" or "deliver to me on whatsapp".',
        },
      },
      required: ['name', 'mission'],
    },
  },
  {
    name: 'set_agent_whatsapp_notify',
    description:
      'Enable, change, or disable WhatsApp delivery of scheduled-run artifacts for an existing agent. Pass an empty string or null in chat_id to disable. Use this when the owner says "send X to me on whatsapp", "stop whatsapping me X", or wants to switch the notify chat. The bridge must be paired for delivery to actually happen; the setting is stored either way.',
    input_schema: {
      type: 'object',
      properties: {
        agent_id: { type: 'string', description: 'Slug id of the agent. Use get_state to look it up if unsure.' },
        agent_name: { type: 'string', description: 'Fuzzy human-readable name (used only when agent_id is missing).' },
        chat_id: {
          type: 'string',
          description: 'WhatsApp chat id to receive scheduled HTML artifacts. Pass empty string to disable.',
        },
      },
      required: ['chat_id'],
    },
  },
  {
    name: 'upsert_schedule',
    description: 'Create or update a recurring schedule for an existing agent.',
    input_schema: {
      type: 'object',
      properties: {
        agent_id: { type: 'string', description: 'Slug ID of the agent.' },
        schedule: {
          type: 'string',
          description: 'Schedule spec: "daily HH:MM", "every 5m", etc.',
        },
        action_kind: {
          type: 'string',
          description:
            'What the schedule runs: "agent-tick", "competitor-report", "marketing-plan", or "heartbeat".',
        },
        enabled: { type: 'boolean', description: 'Whether the schedule is active (default true).' },
      },
      required: ['agent_id', 'schedule'],
    },
  },
  {
    name: 'competitor_report',
    description: 'Generate a daily competitor intelligence report and save it as a local artifact.',
    input_schema: {
      type: 'object',
      properties: {
        triggered_by: { type: 'string', description: 'Source label for the audit log.' },
      },
    },
  },
  {
    name: 'marketing_plan',
    description: 'Generate a 7-day marketing content plan and save it as a local artifact.',
    input_schema: {
      type: 'object',
      properties: {
        triggered_by: { type: 'string', description: 'Source label for the audit log.' },
      },
    },
  },
  {
    name: 'heartbeat_tick',
    description: 'Update the heartbeat timestamp for all generated agents.',
    input_schema: {
      type: 'object',
      properties: {
        triggered_by: { type: 'string', description: 'Source label for the audit log.' },
      },
    },
  },
  {
    name: 'claude_dry_run',
    description:
      'Prepare a Claude Code command string for human review. Never executes automatically — requires owner approval.',
    input_schema: {
      type: 'object',
      properties: {
        prompt: {
          type: 'string',
          description: 'The task description to pass to the Claude Code runner.',
        },
      },
      required: ['prompt'],
    },
  },
  {
    name: 'get_state',
    description:
      'Retrieve a LIGHTWEIGHT snapshot of runtime state: agents, runs, schedules, connectors, and artifact metadata only. Artifact bodies are stripped to a short snippet — use find_artifacts to search and show_artifact to load full content for a specific id. Use this as a directory; never expect full document bodies here.',
    input_schema: {
      type: 'object',
      properties: {},
    },
  },
  {
    name: 'create_artifact',
    description:
      'Save a written-content artifact (report, list, plan, brief, summary, doc). Provide the body as well-structured markdown and madarX will render it into a styled HTML document automatically, with a one-click "Download .md" for the source. Do NOT include the user prompt, instructions, or meta commentary inside the body — only the result content. Do NOT use for full UI/landing pages — use generate_ui instead.',
    input_schema: {
      type: 'object',
      properties: {
        title: { type: 'string', description: 'Short, human-readable artifact title shown in the document header.' },
        kind: {
          type: 'string',
          enum: ['markdown', 'text'],
          description: 'Always "markdown" unless the content is truly plain text with no structure.',
        },
        content: {
          type: 'string',
          description: 'The full body as GitHub-flavored markdown. Use headings (##), lists, tables, and bold sparingly. No emojis. Result content only — never echo the prompt.',
        },
        project_id: {
          type: 'string',
          description: 'Optional destination project id or slug. Omit to use the active/default project.',
        },
        space_id: {
          type: 'string',
          description: 'Optional dashboard space id where the artifact should appear. Omit to use the calling agent default or the main space.',
        },
        dedupe_key: {
          type: 'string',
          description: 'Optional idempotency key. Pass empty string only when intentionally creating multiple artifacts with the same title in one day.',
        },
      },
      required: ['title', 'kind', 'content'],
    },
  },
  {
    name: 'update_artifact',
    description:
      'Update an existing artifact with revised or extended content. Use this instead of create_artifact when continuing, refining, or expanding a previous result for the same task. Keeps the same artifact visible and editable rather than spawning a new one.',
    input_schema: {
      type: 'object',
      properties: {
        artifact_id: { type: 'string', description: 'ID of the artifact to update.' },
        title: { type: 'string', description: 'New title — omit to keep the existing title.' },
        content: {
          type: 'string',
          description: 'Full updated body as GitHub-flavored markdown. Must be the complete content, not a diff.',
        },
      },
      required: ['artifact_id', 'content'],
    },
  },
  {
    name: 'delete_artifact',
    description:
      'Permanently delete a saved artifact from the dashboard. Use only when the owner explicitly asks to remove or trash an artifact, or when the previous artifact is being replaced by a brand-new one and the prior result must not linger. This is irreversible from the agent side; the artifact file on disk is unlinked as well.',
    input_schema: {
      type: 'object',
      properties: {
        artifact_id: { type: 'string', description: 'ID of the artifact to delete.' },
      },
      required: ['artifact_id'],
    },
  },
  {
    name: 'move_artifact',
    description:
      'Move an existing artifact from one project to another. Updates the artifact\'s project_id, relocates the file on disk into the target project\'s artifacts directory, and writes changelog entries on both source and target projects. Use this when the owner asks to "move", "reassign", "relocate", or "transfer" an artifact between projects/spaces. The artifact id and title stay the same; only the project membership and disk location change.',
    input_schema: {
      type: 'object',
      properties: {
        artifact_id: { type: 'string', description: 'ID of the artifact to move.' },
        target_project_id: {
          type: 'string',
          description: 'Destination project id or slug (e.g. "planify" or "personal"). Use list_projects to discover valid values.',
        },
      },
      required: ['artifact_id', 'target_project_id'],
    },
  },
  {
    name: 'read_artifact',
    description:
      'Load the full content for a specific artifact after finding it. Use this before editing, summarizing, or continuing an existing document. Returns rendered content and markdown source when available.',
    input_schema: {
      type: 'object',
      properties: {
        artifact_id: { type: 'string', description: 'Exact artifact id to read.' },
        title_query: { type: 'string', description: 'Optional fuzzy title query when artifact_id is not known. Must resolve to one artifact.' },
        max_chars: { type: 'number', description: 'Maximum content characters to return, default 12000.' },
      },
    },
  },
  {
    name: 'list_spaces',
    description:
      'List dashboard spaces/tabs. Use include_artifact_ids when the owner asks which artifacts are displayed in each space.',
    input_schema: {
      type: 'object',
      properties: {
        include_artifact_ids: { type: 'boolean', description: 'When true, include artifact_ids grouped under each space.' },
      },
    },
  },
  {
    name: 'create_space',
    description: 'Create a new dashboard space/tab where artifacts can be displayed.',
    input_schema: {
      type: 'object',
      properties: {
        name: { type: 'string', description: 'Short dashboard space name.' },
      },
      required: ['name'],
    },
  },
  {
    name: 'rename_space',
    description: 'Rename an existing dashboard space/tab.',
    input_schema: {
      type: 'object',
      properties: {
        space_id: { type: 'string', description: 'Space id to rename.' },
        name: { type: 'string', description: 'New short space name.' },
      },
      required: ['space_id', 'name'],
    },
  },
  {
    name: 'set_active_space',
    description: 'Make a dashboard space/tab the active space.',
    input_schema: {
      type: 'object',
      properties: {
        space_id: { type: 'string', description: 'Space id to activate.' },
      },
      required: ['space_id'],
    },
  },
  {
    name: 'delete_space',
    description:
      'Delete a dashboard space/tab. Artifacts are not deleted; they are moved to a fallback space. Do not use to delete artifacts.',
    input_schema: {
      type: 'object',
      properties: {
        space_id: { type: 'string', description: 'Space id to delete.' },
      },
      required: ['space_id'],
    },
  },
  {
    name: 'move_artifact_to_space',
    description:
      'Display an existing artifact in a different dashboard space/tab, or remove it from a custom space by moving it back to the default main space. The artifact id, project, file, and content stay unchanged.',
    input_schema: {
      type: 'object',
      properties: {
        artifact_id: { type: 'string', description: 'Artifact id to move between dashboard spaces.' },
        space_id: { type: 'string', description: 'Destination dashboard space id. Use "default" to remove from a custom space back to main.' },
      },
      required: ['artifact_id', 'space_id'],
    },
  },
  {
    name: 'import_brand_guidelines',
    description:
      'Import a local brand guidelines file or pasted guidelines content, extract brand colors/fonts/voice/document defaults, and set it as the default brand system for future documents, presentations, and generated UI artifacts. Use this when the owner provides a brand guidelines HTML/Markdown/text file path, pastes guidelines content, or asks to update branding.',
    input_schema: {
      type: 'object',
      properties: {
        file_path: {
          type: 'string',
          description: 'Path to a local .html, .md, .txt, .css, .svg, or .pdf brand guidelines file under an allowed root.',
        },
        content: {
          type: 'string',
          description: 'Pasted brand guidelines content when no local file path is available.',
        },
        filename: {
          type: 'string',
          description: 'Optional filename for pasted content, e.g. brand-guidelines.md.',
        },
        logo_path: {
          type: 'string',
          description: 'Optional local path to the primary brand logo file (.svg, .png, .jpg, .jpeg, .webp).',
        },
      },
    },
  },
  {
    name: 'generate_ui',
    description:
      'Build a production-quality HTML/CSS UI — landing page, dashboard, component, presentation, or any visual interface. Automatically applies professional design rules (typography scale, intentional color, no generic AI patterns). Use this for ALL HTML/UI creation requests.',
    input_schema: {
      type: 'object',
      properties: {
        description: {
          type: 'string',
          description: 'What to build — be specific: layout, sections, content, purpose, and any brand or color hints.',
        },
      },
      required: ['description'],
    },
  },
  {
    name: 'send_to_hermes',
    description:
      'Send a command or task to the local Hermes agent and save the final Hermes response as a dashboard artifact. Use this when the user asks Hermes to do work, wants a Hermes result, or asks to route a command through Hermes.',
    input_schema: {
      type: 'object',
      properties: {
        prompt: {
          type: 'string',
          description: 'The exact task or command Hermes should run.',
        },
        title: {
          type: 'string',
          description: 'Optional artifact title for the Hermes result.',
        },
        session_id: {
          type: 'string',
          description: 'Optional stable Hermes session id. Usually omit.',
        },
        model: {
          type: 'string',
          description: 'Optional Hermes model override. Usually omit so Hermes uses its configured model.',
        },
        timeout_ms: {
          type: 'number',
          description: 'Optional timeout in milliseconds. Use 600000 or higher for project run/build tasks.',
        },
      },
      required: ['prompt'],
    },
  },
  {
    name: 'start_hermes_job',
    description:
      'Start a long-running local Hermes job in the background and immediately create a live-updating dashboard artifact. Use for running projects, builds, tests, installs, dev servers, or multi-step Hermes work that may take more than a minute.',
    input_schema: {
      type: 'object',
      properties: {
        prompt: {
          type: 'string',
          description: 'The full task Hermes should perform.',
        },
        title: {
          type: 'string',
          description: 'Artifact/job title.',
        },
        session_id: {
          type: 'string',
          description: 'Optional Hermes session id.',
        },
        model: {
          type: 'string',
          description: 'Optional Hermes model override.',
        },
        timeout_ms: {
          type: 'number',
          description: 'Optional job timeout in milliseconds. Use 1800000 for long project tasks.',
        },
        cwd: {
          type: 'string',
          description: 'Optional absolute working directory for the project Hermes should run.',
        },
      },
      required: ['prompt'],
    },
  },
  {
    name: 'run_agent',
    description:
      'Run an existing sub-agent NOW (on demand, outside its normal schedule). Use this when the owner says "run X", "trigger X", "do today\'s run of X", etc. The run executes synchronously and publishes live tool-call events to the dashboard. Returns the run summary and a short result preview.',
    input_schema: {
      type: 'object',
      properties: {
        agent_id: { type: 'string', description: 'Slug id (e.g. "uae-ai-initiatives-tracker"). Prefer this if known. Use list_agents / get_state first to look it up.' },
        agent_name: { type: 'string', description: 'Fuzzy human-readable name (e.g. "UAE tracker"). Used only if agent_id is not provided. Case-insensitive substring match against agent names; ambiguous matches error.' },
        wake_reason: { type: 'string', description: 'Short label for the audit log, e.g. "owner requested via whatsapp".' },
        instruction: {
          type: 'string',
          description: "OPTIONAL task-specific instruction for THIS run only — what to do right now. Overrides the agent's standing mission for this single run; never saved onto the agent. Use it to dispatch an existing agent at an ad-hoc job (e.g. \"compare this week's numbers to last week and flag anomalies\"). Omit to run the agent's standing mission. The agent can still only use the tools it was granted — this does not expand its permissions.",
        },
        expected_output: {
          type: 'string',
          description: "OPTIONAL deliverable contract for THIS run only — what a complete, correct result must contain (format, sections, acceptance criteria). Overrides the agent's standing expected_output for this run. Use it to pin down what 'done' looks like for an ad-hoc instruction.",
        },
      },
    },
  },
  {
    name: 'mines_run_report',
    description:
      'Generate ONE mines diligence report for a project as a single action (the workflow authors the number-free narrative and the report tool renders every figure from cited database rows). Use report_kind "pfs_readiness_report" when the owner asks to "assess PFS readiness". The report\'s LLM authoring runs on the configured engine (Claude Code CLI for the personal tier, Anthropic API for production) — it works even when no Anthropic API key is set. Returns the artifact id.',
    input_schema: {
      type: 'object',
      properties: {
        project_id: { type: 'string', description: 'mines_projects.id. Use list/get_state or find the project first if unknown.' },
        report_kind: {
          type: 'string',
          description: 'One of: pfs_readiness_report, due_diligence_memo, investor_presentation, data_quality_report, interpretation_memo.',
        },
        engine: {
          type: 'string',
          description: 'OPTIONAL engine override: "claude-code" (personal CLI subscription) or "anthropic" (production API). Defaults to the mines agents\' configured engine. This is a cost/scale tier, not a capability change.',
        },
      },
      required: ['project_id', 'report_kind'],
    },
  },
  {
    name: 'find_artifacts',
    description:
      'Search past artifacts (reports, briefs, generated UIs) by title, agent, or kind. Use when the owner asks for "the latest X report", "send me yesterday\'s brief", or "what did <agent> produce". Returns up to 50 rows, most recent first, with id/title/kind/agent_name/created_at — NOT the full content. To deliver one, follow up with send_artifact_to_whatsapp. SEARCH BROADLY: title_query does word-level matching (every word must appear, any order), but if the first query returns 0 rows, RETRY with a shorter / broader query — try just the noun ("brief", "report") before concluding nothing exists. Real titles often include words the owner did not say (e.g. "AI Daily Brief scheduled report" vs the owner saying "AI brief").',
    input_schema: {
      type: 'object',
      properties: {
        title_query: { type: 'string', description: 'Fuzzy substring of the artifact title (case-insensitive). E.g. "conflict" matches "Conflict brief 2026-05-13".' },
        agent_id: { type: 'string', description: 'Filter to one specific agent id.' },
        agent_name: { type: 'string', description: 'Fuzzy substring of the agent name (case-insensitive). E.g. "conflict" matches "Conflict Brief Agent".' },
        kind: { type: 'string', description: 'Filter by kind: "html", "document" (rendered markdown), or other.' },
        project_id: { type: 'string', description: 'Filter to one project id or slug.' },
        space_id: { type: 'string', description: 'Filter to one dashboard space id, e.g. "default".' },
        limit: { type: 'number', description: 'Max rows to return (default 10, max 50).' },
      },
    },
  },
  {
    name: 'send_artifact_to_whatsapp',
    description:
      'Deliver an existing artifact as an .html file to a WhatsApp chat. Use after find_artifacts (or get_state) to send a past report on demand — e.g. "send me the latest conflict brief on whatsapp". Pass either artifact_id (preferred) or title_query (fuzzy match — must resolve to one row). If chat_id is omitted on the WhatsApp channel, defaults to the chat the owner is messaging from. The WhatsApp bridge must be paired.',
    input_schema: {
      type: 'object',
      properties: {
        artifact_id: { type: 'string', description: 'Exact artifact id (preferred). Get this from find_artifacts.' },
        title_query: { type: 'string', description: 'Fuzzy substring of the title. Used only when artifact_id is omitted; ambiguous matches error.' },
        chat_id: { type: 'string', description: 'WhatsApp chat id to deliver to. Omit on inbound WhatsApp to send back to the same chat.' },
      },
    },
  },
  {
    name: 'whatsapp_send',
    description:
      'Send a WhatsApp message to a chat the owner has added to the allowlist. Use sparingly for proactive owner alerts and follow-ups (e.g., "scheduled report is ready"). The bridge must be paired and online; if not, this returns an error.',
    input_schema: {
      type: 'object',
      properties: {
        chat_id: {
          type: 'string',
          description: 'Allowlisted WhatsApp chat id (full JID like 9715XXXXXXXX@s.whatsapp.net, or plain digits which are normalized).',
        },
        text: {
          type: 'string',
          description: 'Plain text message body. WhatsApp does not render markdown — keep it conversational.',
        },
      },
      required: ['chat_id', 'text'],
    },
  },
  {
    name: 'list_available_tools',
    description:
      'List all marketplace tools with their enabled status, availability, risk level, and missing credentials. Use this before saying "I cannot do X" to check whether a tool exists. Filter by category (travel, web, github, email, supabase, vercel, local_machine, browser, voice, calendar) or by enabled_only to see what is currently active.',
    input_schema: {
      type: 'object',
      properties: {
        category: {
          type: 'string',
          description: 'Optional category filter: travel, web, github, email, supabase, vercel, local_machine, browser, voice, calendar.',
        },
        enabled_only: {
          type: 'boolean',
          description: 'When true, return only currently-enabled tools.',
        },
      },
    },
  },
  {
    name: 'install_tool',
    description:
      'Enable a marketplace tool by name so it becomes available for immediate use. Call this after list_available_tools identifies a disabled tool that can satisfy the principal\'s request. If the tool has missing credentials, the response includes a missing_credentials list — surface this to the principal. Tools rated irreversible_external will still require per-call owner approval when invoked.',
    input_schema: {
      type: 'object',
      properties: {
        tool_name: {
          type: 'string',
          description: 'Exact tool name from list_available_tools, e.g. "flight_search" or "hotel_rates".',
        },
      },
      required: ['tool_name'],
    },
  },
  {
    name: 'uninstall_tool',
    description:
      'Disable a marketplace tool by name. Use when the principal asks to turn off a tool or when a tool is causing issues.',
    input_schema: {
      type: 'object',
      properties: {
        tool_name: {
          type: 'string',
          description: 'Exact tool name to disable, e.g. "email_send".',
        },
      },
      required: ['tool_name'],
    },
  },
  {
    name: 'request_capability',
    description:
      'Delegate to Tool Smith to BUILD a capability that does not exist yet. Use when an agent needs a tool/skill/integration the system lacks (e.g. "log meetings to Notion" with no Notion tool). First dedupes against existing capabilities; if none matches, files a build task for the tool-smith-agent which authors a composite tool, a REST connector, a skill, or proposes code. Returns the task id. The authored capability is still subject to all runtime gates when used.',
    input_schema: {
      type: 'object',
      properties: {
        capability_description: {
          type: 'string',
          description: 'Plain-language description of the capability needed, e.g. "create pages in Notion" or "summarize a daily competitor digest".',
        },
        target_agent_id: {
          type: 'string',
          description: 'Optional id of the agent that should be granted the capability once built.',
        },
        acceptance_criteria: {
          type: 'string',
          description: 'Optional explicit acceptance criteria for the build.',
        },
      },
      required: ['capability_description'],
    },
  },
];

const HERMES_TOOL_NAMES = new Set(['send_to_hermes', 'start_hermes_job']);

export function getTools() {
  const hermesOk = isHermesAvailable();
  const coreTools = hermesOk ? CORE_TOOLS : CORE_TOOLS.filter(t => !HERMES_TOOL_NAMES.has(t.name));
  return getEnabledToolSchemas(coreTools);
}

export const TOOLS = CORE_TOOLS;

export function getToolStatus() {
  return listMarketplaceTools();
}

// Hard cap to keep any single tool_result from blowing past the model context window.
// 60KB ≈ ~17k tokens — leaves room for system prompt, tools, and conversation under 200k.
const TOOL_RESULT_MAX_BYTES = 60_000;
const STATE_SNIPPET_BYTES = 180;
const STATE_ACTIVITY_LIMIT = 10;
const STATE_AGENT_MEMORY_LIMIT = 2;
const STATE_AUDIT_LIMIT = 6;
const STATE_RUNS_LIMIT = 8;
const STATE_APPROVALS_LIMIT = 6;
const STATE_HEARTBEAT_TASKS_LIMIT = 5;

function snippet(value, max = STATE_SNIPPET_BYTES) {
  const s = typeof value === 'string' ? value : (value == null ? '' : String(value));
  if (s.length <= max) return s;
  return s.slice(0, max) + `…[+${s.length - max} bytes, use show_artifact/find_artifacts]`;
}

function summarizeArtifact(a) {
  if (!a) return a;
  const body = typeof a.content === 'string' ? a.content : '';
  return {
    id: a.id,
    title: a.title,
    kind: a.kind,
    project_id: a.project_id ?? null,
    created_at: a.created_at,
    updated_at: a.updated_at,
    content_bytes: body.length,
    content_preview: body ? snippet(body) : '',
  };
}

function summarizeAgent(a) {
  if (!a) return a;
  const mem = Array.isArray(a.memory) ? a.memory.slice(-STATE_AGENT_MEMORY_LIMIT) : [];
  return {
    id: a.id,
    name: a.name,
    engine: a.engine,
    status: a.status,
    project_id: a.project_id ?? null,
    schedule: a.schedule,
    last_run_at: a.last_run_at ?? null,
    tools_count: Array.isArray(a.tools) ? a.tools.length : 0,
    memory_count: Array.isArray(a.memory) ? a.memory.length : 0,
    memory_recent: mem.map(m => (typeof m === 'string' ? snippet(m, 200) : m)),
  };
}

function summarizeActivity(rows) {
  return (Array.isArray(rows) ? rows.slice(0, STATE_ACTIVITY_LIMIT) : []).map(row => ({
    id: row.id,
    type: row.type,
    at: row.at,
    agent_id: row.agent_id ?? null,
    project_id: row.project_id ?? null,
    summary: snippet(row.summary ?? row.detail ?? '', 160),
  }));
}

function summarizeAudit(rows) {
  return (Array.isArray(rows) ? rows.slice(0, STATE_AUDIT_LIMIT) : []).map(row => ({
    id: row.id,
    type: row.type,
    at: row.at,
    actor: row.user_id || row.acting_on_behalf_of_user_id || null,
    target: row.action_target_id ?? null,
    note: snippet(row.note ?? row.detail ?? '', 160),
  }));
}

function summarizeRun(r) {
  if (!r) return r;
  return {
    id: r.id,
    agent_id: r.agent_id ?? null,
    project_id: r.project_id ?? null,
    status: r.status,
    started_at: r.started_at,
    finished_at: r.finished_at ?? null,
    summary: snippet(r.summary ?? r.note ?? '', 160),
  };
}

function summarizeApproval(a) {
  if (!a) return a;
  return {
    id: a.id,
    action_type: a.action_type,
    status: a.status,
    requested_at: a.requested_at,
    agent_id: a.agent_id ?? null,
    summary: snippet(a.summary ?? a.note ?? '', 160),
  };
}

function summarizeHeartbeatTask(t) {
  if (!t) return t;
  return {
    id: t.id,
    schedule_id: t.schedule_id ?? null,
    agent_id: t.agent_id ?? null,
    status: t.status,
    scheduled_for: t.scheduled_for ?? null,
    attempts: t.attempts ?? 0,
  };
}

/**
 * Strip heavy fields (artifact bodies, full activity/audit detail) from getState()
 * before returning to the model. UI route /api/state still gets the full payload —
 * this only trims the tool_result the LLM sees so a single get_state call cannot
 * blow past the 200k context cap.
 */
export function summarizeStateForTool(full) {
  if (!full || typeof full !== 'object') return full;
  return {
    ok: full.ok,
    product: full.product,
    workspace: full.workspace,
    project_filter: full.project_filter,
    projects: full.projects,
    agents: Array.isArray(full.agents) ? full.agents.map(summarizeAgent) : full.agents,
    tasks: full.tasks,
    runs: Array.isArray(full.runs) ? full.runs.slice(0, STATE_RUNS_LIMIT).map(summarizeRun) : full.runs,
    audit: summarizeAudit(full.audit),
    activity: summarizeActivity(full.activity),
    schedules: full.schedules,
    connectors: full.connectors,
    artifacts: Array.isArray(full.artifacts) ? full.artifacts.map(summarizeArtifact) : full.artifacts,
    heartbeat_tasks: Array.isArray(full.heartbeat_tasks)
      ? full.heartbeat_tasks.slice(0, STATE_HEARTBEAT_TASKS_LIMIT).map(summarizeHeartbeatTask)
      : full.heartbeat_tasks,
    heartbeat_task_events_count: Array.isArray(full.heartbeat_task_events) ? full.heartbeat_task_events.length : 0,
    approvals: Array.isArray(full.approvals) ? full.approvals.slice(0, STATE_APPROVALS_LIMIT).map(summarizeApproval) : full.approvals,
    pending_approvals: Array.isArray(full.pending_approvals) ? full.pending_approvals.slice(0, STATE_APPROVALS_LIMIT).map(summarizeApproval) : full.pending_approvals,
    local_processes: full.local_processes,
    local_tool_audit_count: Array.isArray(full.local_tool_audit) ? full.local_tool_audit.length : 0,
    main_agent_trust_mode: full.main_agent_trust_mode,
    reporting_summary: full.reporting && typeof full.reporting === 'object'
      ? { keys: Object.keys(full.reporting) }
      : null,
    _note: 'Artifact bodies + heartbeat events + reporting stripped. Use find_artifacts/show_artifact for content; query specific endpoints (/api/heartbeat, /api/audit) for full lists.',
  };
}

/**
 * Hard backstop: truncate any tool_result that still exceeds TOOL_RESULT_MAX_BYTES.
 * Returns the value unchanged if under the cap, otherwise a string-truncated marker.
 */
export function clampToolResult(value) {
  let serialized;
  try { serialized = JSON.stringify(value); } catch { return value; }
  if (!serialized || serialized.length <= TOOL_RESULT_MAX_BYTES) return value;
  return {
    ok: false,
    truncated: true,
    original_bytes: serialized.length,
    max_bytes: TOOL_RESULT_MAX_BYTES,
    preview: serialized.slice(0, TOOL_RESULT_MAX_BYTES - 256),
    error: `tool_result exceeded ${TOOL_RESULT_MAX_BYTES} bytes (got ${serialized.length}); narrow the query or use show_artifact for a specific id.`,
  };
}

// ── Dispatcher ────────────────────────────────────────────────────────────────

/**
 * Execute a tool by name with the given input from Claude's tool_use block.
 * Returns a plain JS value (will be JSON-serialised into the tool_result block).
 *
 * @param {string} toolName
 * @param {object} input
 * @returns {unknown}
 */
export async function executeTool(toolName, input = {}, context = {}) {
  const result = await dispatchTool(toolName, input, context);
  return clampToolResult(result);
}

async function dispatchTool(toolName, input = {}, context = {}) {
  switch (toolName) {
    case 'create_agent':
      return createAgentWithFocusedPrompt(input);

    case 'upsert_schedule':
      return upsertSchedule(input);

    case 'competitor_report':
      return competitorReport({ triggered_by: input.triggered_by ?? 'claude' });

    case 'marketing_plan':
      return marketingPlan({ triggered_by: input.triggered_by ?? 'claude' });

    case 'heartbeat_tick':
      return heartbeatTick({ triggered_by: input.triggered_by ?? 'claude' });

    case 'claude_dry_run':
      return claudeDryRun({ prompt: input.prompt }, { triggered_by: 'claude' });

    case 'get_state': {
      const scope = input.project_id ?? null;
      const full = getState({ project_id: scope ? String(scope) : null });
      return summarizeStateForTool(full);
    }

    case 'list_projects':
      return { ok: true, projects: listProjects({ status: 'all' }) };

    case 'create_artifact': {
      const allowedKinds = new Set(['markdown', 'text']);
      const kind = allowedKinds.has(input.kind) ? input.kind : 'markdown';
      return createArtifact({
        ...input,
        kind,
        project_id: input.project_id ?? context.project_id ?? null,
        // Attribution: the calling agent (when known) — falls back to
        // 'main-orchestrator' inside createArtifact so legacy chat flows keep
        // their existing label.
        agent_id: input.agent_id ?? context.agent_id ?? null,
        // Idempotency: tag the artifact with the heartbeat task that produced
        // it so a retried tick collapses to the same row.
        heartbeat_task_id: input.heartbeat_task_id ?? context.heartbeat_task_id ?? null,
        dedupe_key: input.dedupe_key,
        space_id: input.space_id ?? context.space_id ?? null,
      });
    }

    case 'update_artifact':
      return updateArtifact(input.artifact_id, { title: input.title, content: input.content });

    case 'delete_artifact':
      return deleteArtifact(input.artifact_id);

    case 'move_artifact':
      return moveArtifact(input.artifact_id, { target_project_id: input.target_project_id });

    case 'list_spaces':
      return { ok: true, ...(input.include_artifact_ids ? listSpacesWithArtifactIds() : listSpaces()) };

    case 'create_space':
      return { ok: true, space: createSpace({ name: input.name }) };

    case 'rename_space':
      return { ok: true, space: renameSpace(input.space_id, input.name) };

    case 'set_active_space':
      return { ok: true, space: setActiveSpace(input.space_id) };

    case 'delete_space':
      return { ok: true, ...deleteSpace(input.space_id) };

    case 'move_artifact_to_space':
      return { ok: true, ...moveArtifactToSpace(input.artifact_id, input.space_id) };

    case 'import_brand_guidelines':
      return importBrandGuidelinesTool(input);

    case 'generate_ui':
      return generateUi({ ...input, project_id: input.project_id ?? context.project_id ?? null });

    case 'send_to_hermes':
      return sendToHermes({ ...input, project_id: input.project_id ?? context.project_id ?? null });

    case 'start_hermes_job':
      return startHermesJob({ ...input, project_id: input.project_id ?? context.project_id ?? null });

    case 'whatsapp_send':
      return sendWhatsAppMessage(input.chat_id, input.text);

    case 'run_agent':
      return runAgentOnDemand(input);

    case 'mines_run_report':
      // One-action mines report trigger from chat. The actual report tool call
      // is gated inside runSingleReport (executeRegistryTool under the mines
      // agent identity); the LLM authoring runs on the resolved engine so
      // chat-initiated mines work runs on the CLI tier too. Tenancy threaded.
      return runSingleReport({
        projectId: input.project_id,
        report_kind: input.report_kind,
        engine: input.engine || null,
        user_id: context.user_id || context.acting_on_behalf_of_user_id || 'local-user',
        org_id: context.org_id || 'personal-org',
      });

    case 'set_agent_whatsapp_notify':
      return setAgentWhatsAppNotifyTool(input);

    case 'find_artifacts':
      return findArtifactsTool(input);

    case 'read_artifact':
      return readArtifactTool(input);

    case 'send_artifact_to_whatsapp':
      return sendArtifactToWhatsAppTool(input, context);

    case 'list_available_tools':
      return listAvailableToolsHandler(input);

    case 'install_tool':
      return installToolHandler(input);

    case 'uninstall_tool':
      return uninstallToolHandler(input);

    case 'request_capability':
      return requestCapabilityHandler(input, context);

    default:
      return executeRegistryTool(toolName, input);
  }
}

function findArtifactsTool(input) {
  const rows = findArtifacts({
    title_query: input.title_query,
    agent_id: input.agent_id,
    agent_name: input.agent_name,
    kind: input.kind,
    project_id: input.project_id,
    space_id: input.space_id,
    limit: input.limit,
  });
  return {
    ok: true,
    count: rows.length,
    artifacts: rows.map(r => ({
      id: r.id,
      title: r.title || '(untitled)',
      kind: r.kind,
      agent_name: r.agent_name || null,
      agent_id: r.agent_id || null,
      created_at: r.created_at,
      project_id: r.project_id || null,
      space_id: r.space_id || null,
      path: r.path,
    })),
  };
}

function readArtifactTool(input) {
  const resolved = resolveArtifact({ artifact_id: input.artifact_id, title_query: input.title_query });
  if (!resolved.ok) return resolved;
  const artifact = resolved.artifact;
  if (artifact.is_protected) {
    return { ok: false, error: 'Artifact is passphrase-protected; cannot be read from voice without unlock.' };
  }
  if (artifact.encrypted) {
    return { ok: false, error: 'Artifact body is encrypted at rest; voice read path does not decrypt.' };
  }
  const cap = Math.min(Math.max(Number(input.max_chars) || 12000, 1000), 14000);
  const rendered = String(artifact.content || '');
  const source = artifact.source_md ? String(artifact.source_md) : null;
  const body = source || rendered;
  const truncated = body.length > cap;
  return {
    ok: true,
    artifact: {
      id: artifact.id,
      title: artifact.title || '(untitled)',
      kind: artifact.kind,
      agent_id: artifact.agent_id || null,
      created_at: artifact.created_at,
      path: artifact.path || null,
      project_id: artifact.project_id || null,
      space_id: artifact.space_id || null,
      content_format: source ? 'markdown' : 'rendered',
      total_chars: body.length,
      returned_chars: Math.min(body.length, cap),
      truncated,
      content: truncated ? body.slice(0, cap) : body,
    },
  };
}

function resolveArtifact({ artifact_id, title_query }) {
  if (artifact_id) {
    const row = getArtifact(artifact_id);
    if (!row) return { ok: false, error: `No artifact with id "${artifact_id}".` };
    return { ok: true, artifact: row };
  }
  if (title_query) {
    const rows = findArtifacts({ title_query, limit: 5 });
    if (rows.length === 0) return { ok: false, error: `No artifact title matches "${title_query}".` };
    if (rows.length > 1) {
      return {
        ok: false,
        error: `Ambiguous match for "${title_query}". Candidates: ${rows.map(r => `${r.title} (${r.id})`).join('; ')}. Pass artifact_id to disambiguate.`,
      };
    }
    return { ok: true, artifact: getArtifact(rows[0].id) };
  }
  return { ok: false, error: 'Pass either artifact_id or title_query.' };
}

async function sendArtifactToWhatsAppTool(input, context) {
  const resolved = resolveArtifact(input);
  if (!resolved.ok) return resolved;
  const artifact = resolved.artifact;
  if (artifact.kind !== 'html' && artifact.kind !== 'document') {
    return { ok: false, error: `Artifact kind "${artifact.kind}" is not HTML-renderable; only html and document artifacts can be sent.` };
  }
  if (!artifact.content || !String(artifact.content).trim()) {
    return { ok: false, error: `Artifact ${artifact.id} has no rendered content to send.` };
  }
  const chatId = (input.chat_id && String(input.chat_id).trim()) || context.chat_id || null;
  if (!chatId) return { ok: false, error: 'No chat_id available — pass chat_id, or call this from the WhatsApp channel where the sender is known.' };

  const health = await getBridgeHealth();
  if (!health || health.ok === false || health.pairing_state !== 'paired') {
    return { ok: false, error: `WhatsApp bridge not paired (state=${health?.pairing_state || 'unknown'}). Pair via /whatsapp before retrying.` };
  }

  const slug = String(artifact.title || 'artifact')
    .toLowerCase()
    .replace(/[^a-z0-9]+/g, '-')
    .replace(/^-+|-+$/g, '')
    .slice(0, 60) || 'artifact';
  try {
    const result = await sendWhatsAppDocument({
      chat_id: chatId,
      content: artifact.content,
      filename: `${slug}.html`,
      mimetype: 'text/html',
      caption: artifact.title || undefined,
    });
    return {
      ok: true,
      artifact_id: artifact.id,
      title: artifact.title,
      chat_id: chatId,
      message_id: result?.message_id || null,
    };
  } catch (err) {
    return { ok: false, error: `Bridge send failed: ${err.message}`, artifact_id: artifact.id };
  }
}

function setAgentWhatsAppNotifyTool(input) {
  const resolved = resolveAgentId(input);
  if (!resolved.ok) return { ok: false, error: resolved.error };
  const chatId = input.chat_id == null ? null : String(input.chat_id).trim() || null;
  const result = setAgentWhatsAppNotify(resolved.agent.id, chatId);
  return { ...result, agent_name: resolved.agent.name };
}

function resolveAgentId({ agent_id, agent_name }) {
  if (agent_id) {
    const all = listAgents();
    const exact = all.find(a => a.id === agent_id);
    if (exact) return { ok: true, agent: exact };
    return { ok: false, error: `No agent with id "${agent_id}". Use get_state to list agents.` };
  }
  if (agent_name) {
    const needle = String(agent_name).toLowerCase().trim();
    const all = listAgents();
    const exact = all.find(a => (a.name || '').toLowerCase() === needle);
    if (exact) return { ok: true, agent: exact };
    const partial = all.filter(a => (a.name || '').toLowerCase().includes(needle) || (a.id || '').toLowerCase().includes(needle));
    if (partial.length === 1) return { ok: true, agent: partial[0] };
    if (partial.length === 0) return { ok: false, error: `No agent matches "${agent_name}". Existing: ${all.map(a => a.name).join(', ') || '(none)'}` };
    return { ok: false, error: `Ambiguous match for "${agent_name}". Candidates: ${partial.map(a => `${a.name} (${a.id})`).join(', ')}. Pass agent_id to disambiguate.` };
  }
  return { ok: false, error: 'Pass either agent_id or agent_name.' };
}

async function runAgentOnDemand(input) {
  const resolved = resolveAgentId(input);
  if (!resolved.ok) return { ok: false, error: resolved.error };
  const agent = resolved.agent;
  const wake_reason = input.wake_reason || 'on-demand from main agent';
  const instruction = typeof input.instruction === 'string' && input.instruction.trim() ? input.instruction.trim() : null;
  const expected_output = typeof input.expected_output === 'string' && input.expected_output.trim() ? input.expected_output.trim() : null;
  try {
    // Per-call overrides are auditable but NOT persisted onto the agent row —
    // they shape this single run's prompt only (see runAgentTick precedence).
    if (instruction || expected_output) {
      appendAudit({
        type: 'agent.run.override',
        summary: `Ad-hoc run of ${agent.name} with per-call contract`,
        status: 'complete',
        meta: { agent_id: agent.id, wake_reason, has_instruction: Boolean(instruction), has_expected_output: Boolean(expected_output) },
      });
    }
    const run = await runAgentTick(agent.id, { triggered_by: 'main_agent', wake_reason, instruction, expected_output });
    if (!run) return { ok: false, error: `runAgentTick returned null for ${agent.id}` };
    return {
      ok: true,
      agent_id: agent.id,
      agent_name: agent.name,
      run_id: run.id,
      status: run.status || 'complete',
      title: run.artifact_title || run.title,
      preview: (run.preview || '').slice(0, 600),
      // Addressable artifact row (for pinning to the canvas). `artifact` stays
      // the file-path string for backward compatibility.
      artifact_id: run.artifact_id || null,
      artifact: run.artifact || null,
    };
  } catch (err) {
    return { ok: false, error: err.message, agent_id: agent.id };
  }
}
