/**
 * user-folder.ts
 *
 * Tools for discovering and executing user-authored Playwright scripts:
 *   - ListUserAssets: browse scripts / templates / recordings in data/users/{userId}/
 *   - RunUserScript: validate params, decrypt session storageState if needed, delegate to runUserScript()
 */

import { existsSync, readdirSync, statSync, readFileSync, mkdirSync, writeFileSync, renameSync, unlinkSync } from 'node:fs';
import { join, extname } from 'node:path';
import matter from 'gray-matter';
import { ToolDef } from '../../llm/openai-compat.js';
import { ToolContext, ToolResult } from './core.js';
import { loadConfig } from '../../config.js';
import { parseScript } from '../../user-folder/frontmatter.js';
import { renderTemplate } from '../../user-folder/template-renderer.js';
import {
  userRoot,
  assertOwnerAccess,
  resolveUserSubdir,
} from '../../user-folder/paths.js';
import {
  upsertMemoryEntry,
  deleteMemoryEntry,
  readMemoryEntry,
  MEMORY_TYPES,
  type MemoryType,
} from '../../user-folder/memory.js';
import type { BrowserSessionRepo } from '../../db/browser-session-repo.js';
import { resolveAndRunUserScript } from '../../user-folder/script-orchestrator.js';
import { recorder } from '../../engine/browser-recorder.js';

// ── Injected deps (server.ts / worker.ts call setUserFolderToolDeps) ─────────

interface UserFolderDeps {
  sessRepo: BrowserSessionRepo;
  masterKeyPath: string;
  userFolderRoot: string;
  /**
   * Optional audit-log sink. server.ts wires this to Repository.addAuditLog
   * so RunUserScript invocations land in the same audit_log table as other
   * security-relevant actions. Left optional because tests / standalone CLI
   * use of the tool layer doesn't need it.
   */
  auditLog?: (action: string, detail: object, jobId?: string | null) => void;
}

let _deps: UserFolderDeps | null = null;

export function setUserFolderToolDeps(deps: UserFolderDeps | null): void {
  _deps = deps;
}

// ── Helpers ───────────────────────────────────────────────────────────────────

function getUserFolderRoot(): string {
  return _deps?.userFolderRoot ?? './data/users';
}

// ── Tool definitions ──────────────────────────────────────────────────────────

/** Regex for valid memory entry names: alphanumeric, dash, underscore; no extension. */
const MEMORY_NAME_RE = /^[a-zA-Z0-9_-]+$/;

/** Regex for valid template names: alphanumeric, dash, underscore, dot; no path separators. */
const TEMPLATE_NAME_RE = /^[a-zA-Z0-9_.-]+$/;

export const TOOL_DEFS: Record<string, ToolDef> = {
  UpdateUserMemory: {
    type: 'function',
    function: {
      name: 'UpdateUserMemory',
      description:
        'Writes or deletes a persistent memory entry in the caller\'s user folder. ' +
        'Entries are indexed in MEMORY.md (auto-injected into system prompt) and stored ' +
        'as individual fact files. Details via ReadToolDoc({ name: "UpdateUserMemory" }).',
      parameters: {
        type: 'object',
        properties: {
          action: {
            type: 'string',
            enum: ['upsert', 'delete'],
            description: '"upsert" to create/update, "delete" to remove.',
          },
          name: {
            type: 'string',
            description: 'Entry identifier (alphanumeric, dash, underscore; no .md extension).',
          },
          type: {
            type: 'string',
            enum: ['user', 'feedback', 'project', 'reference'],
            description: 'Category of the memory entry. Required for upsert.',
          },
          description: {
            type: 'string',
            description: 'One-line summary shown in MEMORY.md index. Required for upsert.',
          },
          body: {
            type: 'string',
            description: 'Full content of the fact file. Required for upsert.',
          },
        },
        required: ['action', 'name'],
      },
    },
  },

  ReadUserMemory: {
    type: 'function',
    function: {
      name: 'ReadUserMemory',
      description:
        'Loads a specific memory entry from the caller\'s user folder. ' +
        'Returns frontmatter + body. Details via ReadToolDoc({ name: "ReadUserMemory" }).',
      parameters: {
        type: 'object',
        properties: {
          name: {
            type: 'string',
            description: 'Entry identifier to load (no .md extension).',
          },
        },
        required: ['name'],
      },
    },
  },

  ReadUserTemplate: {
    type: 'function',
    function: {
      name: 'ReadUserTemplate',
      description:
        'Loads a template file from the caller\'s templates/ subdir. ' +
        'Returns the raw body (frontmatter optional). Useful for boilerplate / report templates / canned snippets. ' +
        'Details via ReadToolDoc({ name: "ReadUserTemplate" }).',
      parameters: {
        type: 'object',
        properties: {
          name: {
            type: 'string',
            description: 'Template file name (with or without .md extension).',
          },
        },
        required: ['name'],
      },
    },
  },

  RenderUserTemplate: {
    type: 'function',
    function: {
      name: 'RenderUserTemplate',
      description:
        'Renders a template from templates/ by substituting {{var}} placeholders with caller-supplied params. ' +
        'Frontmatter params spec is validated and defaults are applied. Unknown placeholders are left literal. ' +
        'Details via ReadToolDoc({ name: "RenderUserTemplate" }).',
      parameters: {
        type: 'object',
        properties: {
          name: {
            type: 'string',
            description: 'Template file name (with or without .md extension).',
          },
          params: {
            type: 'object',
            description: 'Key-value params matching the template\'s frontmatter param spec.',
          },
        },
        required: ['name'],
      },
    },
  },

  ListUserAssets: {
    type: 'function',
    function: {
      name: 'ListUserAssets',
      description:
        'Lists user-authored scripts, browser-macros, templates, and recordings in the caller\'s folder. ' +
        'Details via ReadToolDoc({ name: "ListUserAssets" }).',
      parameters: {
        type: 'object',
        properties: {
          kind: {
            type: 'string',
            enum: ['scripts', 'browser-macros', 'templates', 'recordings', 'all'],
            description: 'Which category to list. Default "all".',
          },
        },
        required: [],
      },
    },
  },

  RunUserScript: {
    type: 'function',
    function: {
      name: 'RunUserScript',
      description:
        'Executes a user-authored script from the caller\'s user folder. ' +
        'Use kind="script" (default) for plain Node scripts in scripts/, ' +
        'or kind="browser-macro" for Playwright scripts in browser-macros/. ' +
        'Node only — NOT for Python: plain scripts run under Node --permission (no child_process), ' +
        'so a JS wrapper that shells out to python WILL fail; run Python directly with the Bash tool (pip pre-baked). ' +
        'Details via ReadToolDoc({ name: "RunUserScript" }).',
      parameters: {
        type: 'object',
        properties: {
          name: {
            type: 'string',
            description: 'Script filename (with or without .js extension).',
          },
          params: {
            type: 'object',
            description: 'Key-value params matching the script\'s frontmatter param spec.',
          },
          kind: {
            type: 'string',
            enum: ['script', 'browser-macro'],
            description: '"script" (default): plain Node, scripts/ dir, main({ params }). "browser-macro": Playwright, browser-macros/ dir, main({ context, params }).',
          },
        },
        required: ['name'],
      },
    },
  },

  WriteUserScript: {
    type: 'function',
    function: {
      name: 'WriteUserScript',
      description:
        'ユーザーフォルダの scripts/ または browser-macros/ に script を作成・上書きする。' +
        'kind="script" は scripts/ (plain Node、main({ params }) 形式)、' +
        'kind="browser-macro" は browser-macros/ (Playwright、main({ context, params }) 形式)。' +
        'Node 専用 — Python 不可: python を呼ぶだけの JS ラッパーを書かないこと (plain は --permission で child_process 不可)。python は Bash ツールで直接実行する (pip は pre-baked)。' +
        '詳細は ReadToolDoc({ name: "WriteUserScript" })。',
      parameters: {
        type: 'object',
        properties: {
          name: {
            type: 'string',
            description: 'ファイル名 (slug)。`.js` は自動補完される。例: "fetch-and-clean"',
          },
          kind: {
            type: 'string',
            enum: ['script', 'browser-macro'],
            description: '"script" (default、plain Node) または "browser-macro" (Playwright)',
          },
          content: {
            type: 'string',
            description: 'ファイル全文。YAML frontmatter (params spec) + main() を含むこと',
          },
          overwrite: {
            type: 'boolean',
            description: '既存ファイルを上書きするかどうか (default: false)。false かつ既存なら error',
          },
        },
        required: ['name', 'content'],
      },
    },
  },

  WriteUserTemplate: {
    type: 'function',
    function: {
      name: 'WriteUserTemplate',
      description:
        'ユーザーフォルダの templates/ にテンプレートを作成・上書きする。' +
        '本文は Markdown、frontmatter で params 仕様を宣言可能 (ReadUserTemplate/RenderUserTemplate と互換)。' +
        '詳細は ReadToolDoc({ name: "WriteUserTemplate" })。',
      parameters: {
        type: 'object',
        properties: {
          name: {
            type: 'string',
            description: 'ファイル名 (slug)。`.md` は自動補完される',
          },
          content: {
            type: 'string',
            description: 'ファイル全文。frontmatter + 本文',
          },
          overwrite: {
            type: 'boolean',
            description: '既存ファイルを上書きするかどうか (default: false)',
          },
        },
        required: ['name', 'content'],
      },
    },
  },
};

// ── UpdateUserMemory implementation ──────────────────────────────────────────

async function executeUpdateUserMemory(
  input: Record<string, unknown>,
  ctx: ToolContext,
): Promise<ToolResult> {
  if (!ctx.userId) {
    return { output: 'UpdateUserMemory requires an authenticated user', isError: true };
  }

  const action = input['action'] as string | undefined;
  if (action !== 'upsert' && action !== 'delete') {
    return { output: 'UpdateUserMemory: "action" must be "upsert" or "delete"', isError: true };
  }

  const name = input['name'];
  if (typeof name !== 'string' || !name.trim()) {
    return { output: 'UpdateUserMemory: "name" parameter is required', isError: true };
  }
  if (!MEMORY_NAME_RE.test(name)) {
    return {
      output:
        'UpdateUserMemory: "name" must contain only alphanumeric characters, dashes, or underscores (no spaces, no .md extension)',
      isError: true,
    };
  }

  const folderRoot = getUserFolderRoot();

  if (action === 'delete') {
    const deleted = deleteMemoryEntry(folderRoot, ctx.userId, name);
    if (!deleted) {
      return { output: `UpdateUserMemory: entry "${name}" not found`, isError: true };
    }
    return { output: `Memory deleted: ${name} (moved to trash)`, isError: false };
  }

  // action === 'upsert'
  const rawType = input['type'];
  if (!rawType || !MEMORY_TYPES.includes(rawType as MemoryType)) {
    return {
      output: `UpdateUserMemory: "type" must be one of: ${MEMORY_TYPES.join(', ')}`,
      isError: true,
    };
  }

  const description = input['description'];
  if (typeof description !== 'string' || !description.trim()) {
    return { output: 'UpdateUserMemory: "description" is required for upsert', isError: true };
  }

  const body = input['body'];
  if (typeof body !== 'string') {
    return { output: 'UpdateUserMemory: "body" is required for upsert', isError: true };
  }

  try {
    upsertMemoryEntry(folderRoot, ctx.userId, {
      name,
      type: rawType as MemoryType,
      description: description.trim(),
      body,
    });
    return { output: `Memory upserted: ${name}`, isError: false };
  } catch (err) {
    return { output: `UpdateUserMemory: ${(err as Error).message}`, isError: true };
  }
}

// ── ReadUserMemory implementation ─────────────────────────────────────────────

async function executeReadUserMemory(
  input: Record<string, unknown>,
  ctx: ToolContext,
): Promise<ToolResult> {
  if (!ctx.userId) {
    return { output: 'ReadUserMemory requires an authenticated user', isError: true };
  }

  const name = input['name'];
  if (typeof name !== 'string' || !name.trim()) {
    return { output: 'ReadUserMemory: "name" parameter is required', isError: true };
  }
  if (!MEMORY_NAME_RE.test(name)) {
    return {
      output: 'ReadUserMemory: "name" must contain only alphanumeric characters, dashes, or underscores',
      isError: true,
    };
  }

  const folderRoot = getUserFolderRoot();
  const entry = readMemoryEntry(folderRoot, ctx.userId, name);
  if (!entry) {
    return { output: `ReadUserMemory: entry "${name}" not found`, isError: true };
  }

  const output = [
    `# Memory: ${entry.meta.name}`,
    `**Type**: ${entry.meta.type}`,
    `**Description**: ${entry.meta.description}`,
    '',
    entry.body.trim(),
  ].join('\n');

  return { output, isError: false };
}

// ── ReadUserTemplate implementation ──────────────────────────────────────────

async function executeReadUserTemplate(
  input: Record<string, unknown>,
  ctx: ToolContext,
): Promise<ToolResult> {
  if (!ctx.userId) {
    return { output: 'ReadUserTemplate requires an authenticated user', isError: true };
  }

  const rawName = input['name'];
  if (typeof rawName !== 'string' || !rawName.trim()) {
    return { output: 'ReadUserTemplate: "name" parameter is required', isError: true };
  }

  // Strip .md suffix so callers can pass either form; re-add once
  const baseName = rawName.replace(/\.md$/i, '');

  if (baseName.length === 0 || baseName.length > 128) {
    return { output: 'ReadUserTemplate: "name" must be 1–128 characters', isError: true };
  }
  if (!TEMPLATE_NAME_RE.test(baseName)) {
    return {
      output:
        'ReadUserTemplate: "name" must contain only alphanumeric characters, dashes, underscores, or dots',
      isError: true,
    };
  }

  const folderRoot = getUserFolderRoot();
  let templatePath: string;
  try {
    templatePath = resolveUserSubdir(folderRoot, ctx.userId, 'templates', `${baseName}.md`);
  } catch (err) {
    return { output: `ReadUserTemplate: ${(err as Error).message}`, isError: true };
  }

  if (!existsSync(templatePath)) {
    return { output: `ReadUserTemplate: template "${baseName}" not found`, isError: true };
  }

  let raw: string;
  try {
    raw = readFileSync(templatePath, 'utf-8');
  } catch (err) {
    return { output: `ReadUserTemplate: failed to read template: ${(err as Error).message}`, isError: true };
  }

  // Best-effort frontmatter parse (not required for templates)
  let body = raw;
  const fmLines: string[] = [];
  try {
    const parsed = matter(raw);
    const data = parsed.data as Record<string, unknown>;
    if (Object.keys(data).length > 0) {
      for (const [k, v] of Object.entries(data)) {
        fmLines.push(`${k}: ${JSON.stringify(v)}`);
      }
    }
    // Normalize leading newline from gray-matter
    body = parsed.content.startsWith('\n') ? parsed.content.slice(1) : parsed.content;
  } catch {
    // If parse fails, fall back to raw content as body
    body = raw;
  }

  const parts: string[] = [`# Template: ${baseName}`];
  if (fmLines.length > 0) {
    parts.push('');
    parts.push('## Frontmatter');
    parts.push(...fmLines);
  }
  parts.push('');
  parts.push('## Body');
  parts.push(body.trim());

  return { output: parts.join('\n'), isError: false };
}

// ── RenderUserTemplate implementation ───────────────────────────────────────

async function executeRenderUserTemplate(
  input: Record<string, unknown>,
  ctx: ToolContext,
): Promise<ToolResult> {
  if (!ctx.userId) {
    return { output: 'RenderUserTemplate requires an authenticated user', isError: true };
  }

  const rawName = input['name'];
  if (typeof rawName !== 'string' || !rawName.trim()) {
    return { output: 'RenderUserTemplate: "name" parameter is required', isError: true };
  }

  const baseName = rawName.replace(/\.md$/i, '');
  if (baseName.length === 0 || baseName.length > 128) {
    return { output: 'RenderUserTemplate: "name" must be 1–128 characters', isError: true };
  }
  if (!TEMPLATE_NAME_RE.test(baseName)) {
    return {
      output:
        'RenderUserTemplate: "name" must contain only alphanumeric characters, dashes, underscores, or dots',
      isError: true,
    };
  }

  const folderRoot = getUserFolderRoot();
  let templatePath: string;
  try {
    templatePath = resolveUserSubdir(folderRoot, ctx.userId, 'templates', `${baseName}.md`);
  } catch (err) {
    return { output: `RenderUserTemplate: ${(err as Error).message}`, isError: true };
  }

  if (!existsSync(templatePath)) {
    return { output: `RenderUserTemplate: template "${baseName}" not found`, isError: true };
  }

  let raw: string;
  try {
    raw = readFileSync(templatePath, 'utf-8');
  } catch (err) {
    return {
      output: `RenderUserTemplate: failed to read template: ${(err as Error).message}`,
      isError: true,
    };
  }

  // Parse frontmatter via parseScript — shares the params schema with scripts.
  // Templates without frontmatter render as a pass-through (no param substitution).
  let parsed;
  try {
    parsed = parseScript(raw);
  } catch (err) {
    return {
      output: `RenderUserTemplate: invalid frontmatter: ${(err as Error).message}`,
      isError: true,
    };
  }

  const rawParams = (input['params'] as Record<string, unknown> | undefined) ?? {};

  try {
    const rendered = renderTemplate(parsed.body, parsed.frontmatter.params, rawParams);
    return { output: rendered, isError: false };
  } catch (err) {
    return { output: `RenderUserTemplate: ${(err as Error).message}`, isError: true };
  }
}

// ── ListUserAssets implementation ─────────────────────────────────────────────

async function executeListUserAssets(
  input: Record<string, unknown>,
  ctx: ToolContext,
): Promise<ToolResult> {
  if (!ctx.userId) {
    return { output: 'ListUserAssets requires an authenticated user', isError: true };
  }

  // owner is always ctx.userId — folder is per-user, no cross-user param
  const kind = (input['kind'] as string | undefined) ?? 'all';
  const folderRoot = getUserFolderRoot();
  const userDir = userRoot(folderRoot, ctx.userId);

  const lines: string[] = [`User folder for ${ctx.userId}:`];

  // Scripts (plain Node runtime)
  if (kind === 'scripts' || kind === 'all') {
    const scriptsDir = join(userDir, 'scripts');
    const scriptEntries: string[] = [];

    if (existsSync(scriptsDir)) {
      const files = readdirSync(scriptsDir)
        .filter((f) => extname(f) === '.js')
        .sort();

      for (const file of files) {
        const scriptPath = join(scriptsDir, file);
        try {
          const source = readFileSync(scriptPath, 'utf-8');
          const parsed = parseScript(source);
          const { description, params } = parsed.frontmatter;
          const paramStr = params.map((p) => `${p.name}:${p.type}`).join(', ');
          const entry = `  - ${file}: "${description}" — params: [${paramStr}]`;
          scriptEntries.push(entry);
        } catch (err) {
          scriptEntries.push(`  - ${file}: (parse error: ${(err as Error).message})`);
        }
      }
    }

    lines.push(`Scripts (${scriptEntries.length}):`);
    if (scriptEntries.length === 0) {
      lines.push('  (none)');
    } else {
      lines.push(...scriptEntries);
    }
  }

  // Browser Macros (Playwright runtime)
  if (kind === 'browser-macros' || kind === 'all') {
    const macrosDir = join(userDir, 'browser-macros');
    const macroEntries: string[] = [];

    if (existsSync(macrosDir)) {
      const files = readdirSync(macrosDir)
        .filter((f) => extname(f) === '.js')
        .sort();

      for (const file of files) {
        const macroPath = join(macrosDir, file);
        try {
          const source = readFileSync(macroPath, 'utf-8');
          const parsed = parseScript(source);
          const { description, params, sessionProfileId } = parsed.frontmatter;
          const paramStr = params.map((p) => `${p.name}:${p.type}`).join(', ');
          let entry = `  - ${file}: "${description}" — params: [${paramStr}]`;
          if (sessionProfileId !== undefined) {
            entry += ` — session: ${sessionProfileId}`;
          }
          macroEntries.push(entry);
        } catch (err) {
          macroEntries.push(`  - ${file}: (parse error: ${(err as Error).message})`);
        }
      }
    }

    lines.push(`Browser Macros (${macroEntries.length}):`);
    if (macroEntries.length === 0) {
      lines.push('  (none)');
    } else {
      lines.push(...macroEntries);
    }
  }

  // Templates
  if (kind === 'templates' || kind === 'all') {
    const templatesDir = join(userDir, 'templates');
    const templateEntries: string[] = [];

    if (existsSync(templatesDir)) {
      const files = readdirSync(templatesDir).sort();
      for (const file of files) {
        try {
          const stat = statSync(join(templatesDir, file));
          if (stat.isFile()) {
            templateEntries.push(
              `  - ${file} (${stat.size} bytes, ${stat.mtime.toISOString()})`,
            );
          }
        } catch {
          // skip unreadable entries
        }
      }
    }

    lines.push(`Templates (${templateEntries.length}):`);
    if (templateEntries.length === 0) {
      lines.push('  (none)');
    } else {
      lines.push(...templateEntries);
    }
  }

  // Recordings
  if (kind === 'recordings' || kind === 'all') {
    const recordingsDir = join(userDir, 'recordings');
    const recordingEntries: string[] = [];

    if (existsSync(recordingsDir)) {
      const files = readdirSync(recordingsDir).sort();
      for (const file of files) {
        try {
          const stat = statSync(join(recordingsDir, file));
          if (stat.isFile()) {
            recordingEntries.push(
              `  - ${file} (${stat.size} bytes, ${stat.mtime.toISOString()})`,
            );
          }
        } catch {
          // skip
        }
      }
    }

    lines.push(`Recordings (${recordingEntries.length}):`);
    if (recordingEntries.length === 0) {
      lines.push('  (none)');
    } else {
      lines.push(...recordingEntries);
    }
  }

  return { output: lines.join('\n'), isError: false };
}

// ── RunUserScript implementation ──────────────────────────────────────────────

async function executeRunUserScript(
  input: Record<string, unknown>,
  ctx: ToolContext,
): Promise<ToolResult> {
  if (!ctx.userId) {
    return { output: 'RunUserScript requires an authenticated user', isError: true };
  }

  // Config gate: user scripts are opt-in for security
  const cfg = loadConfig();
  if (cfg.tools?.userScriptsEnabled !== true) {
    return {
      output:
        'User scripts are disabled (set tools.user_scripts_enabled: true in config.yaml). ' +
        'Note: plain-runtime scripts run under Node --permission, but browser-macros do not.',
      isError: true,
    };
  }

  // Optional per-user allowlist: even when the global gate is true, only listed
  // user ids may run scripts. Lets a shared install roll out script execution
  // to specific power users without granting it to everyone.
  const allowlist = cfg.tools?.userScriptsAllowUserids;
  if (Array.isArray(allowlist) && allowlist.length > 0 && !allowlist.includes(ctx.userId)) {
    _deps?.auditLog?.('user_script_denied', {
      userId: ctx.userId,
      reason: 'not in tools.user_scripts_allow_userids',
    }, ctx.taskId ?? null);
    return {
      output:
        `RunUserScript is gated to specific users; "${ctx.userId}" is not in ` +
        `tools.user_scripts_allow_userids. Ask an admin to add you.`,
      isError: true,
    };
  }

  const rawName = input['name'];
  if (typeof rawName !== 'string' || !rawName.trim()) {
    return { output: 'RunUserScript: "name" parameter is required', isError: true };
  }

  const params = (input['params'] as Record<string, unknown> | undefined) ?? {};

  const rawKind = input['kind'];
  const kind: 'script' | 'browser-macro' | undefined =
    rawKind === 'browser-macro' ? 'browser-macro' :
    rawKind === 'script' ? 'script' :
    undefined;

  const scriptBaseName = (rawName.endsWith('.js') ? rawName : `${rawName}.js`).replace(/\.js$/, '');

  const result = await resolveAndRunUserScript({
    rootDir: getUserFolderRoot(),
    userId: ctx.userId,
    name: rawName,
    params,
    kind,
    sessRepo: _deps?.sessRepo,
    masterKeyPath: _deps?.masterKeyPath,
    timeoutMs: 60_000,
  });

  if (!result.ok) {
    _deps?.auditLog?.('user_script_run', {
      userId: ctx.userId,
      scriptName: scriptBaseName,
      kind,
      subdir: result.subdir ?? null,
      ok: false,
      error: result.error.slice(0, 500),
    }, ctx.taskId ?? null);

    // Auto-enable the recorder for browser-macro failures (only if not already enabled).
    if (result.subdir === 'browser-macros' && ctx.taskId && !recorder.recordTo(ctx.taskId)) {
      recorder.enable(ctx.taskId, `${scriptBaseName}.next`);
    }
    const patchNotice = result.subdir === 'browser-macros'
      ? `\n\nThe recorder is now enabled for this task; subsequent BrowseWeb actions will be captured. ` +
        `On task complete, a candidate patch will be saved as browser-macros/${scriptBaseName}.next.js for review.`
      : '';
    return {
      output: `RunUserScript "${scriptBaseName}" failed: ${result.error}` + patchNotice,
      isError: true,
    };
  }

  _deps?.auditLog?.('user_script_run', {
    userId: ctx.userId,
    scriptName: scriptBaseName,
    kind,
    subdir: result.subdir,
    runtime: result.runtime,
    ok: true,
    durationMs: result.durationMs,
  }, ctx.taskId ?? null);

  let resultStr: string;
  if (result.result === undefined || result.result === null) {
    resultStr = String(result.result);
  } else if (typeof result.result === 'object' || Array.isArray(result.result)) {
    resultStr = JSON.stringify(result.result, null, 2);
  } else {
    resultStr = String(result.result);
  }

  let output = resultStr;
  if (result.logs.length > 0) {
    output += `\n\n[script logs]\n${result.logs.join('\n')}`;
  }
  return { output, isError: false };
}

// ── WriteUserScript implementation ───────────────────────────────────────────

/** Slug regex: alphanumeric, dash, underscore only — no path separators. */
const SCRIPT_NAME_RE = /^[a-zA-Z0-9_-]+$/;

async function executeWriteUserScript(
  input: Record<string, unknown>,
  ctx: ToolContext,
): Promise<ToolResult> {
  if (!ctx.userId) {
    return { output: 'WriteUserScript: requires an authenticated user', isError: true };
  }

  const name = input['name'];
  const content = input['content'];
  const kind = (input['kind'] as string | undefined) ?? 'script';
  const overwrite = input['overwrite'] === true;

  if (typeof name !== 'string' || !name) {
    return { output: 'WriteUserScript: "name" parameter is required', isError: true };
  }
  if (typeof content !== 'string') {
    return { output: 'WriteUserScript: "content" must be a string', isError: true };
  }
  if (kind !== 'script' && kind !== 'browser-macro') {
    return { output: 'WriteUserScript: "kind" must be "script" or "browser-macro"', isError: true };
  }

  // Strip optional .js suffix; validate the base name as a slug
  const baseName = name.replace(/\.js$/i, '');
  if (!SCRIPT_NAME_RE.test(baseName)) {
    return {
      output:
        'WriteUserScript: "name" must contain only alphanumeric characters, dashes, or underscores (no spaces, no slashes)',
      isError: true,
    };
  }

  // Size limit: 256 KB
  const MAX_BYTES = 256 * 1024;
  if (Buffer.byteLength(content, 'utf-8') > MAX_BYTES) {
    return { output: `WriteUserScript: content exceeds ${MAX_BYTES} bytes`, isError: true };
  }

  // Shape sanity check: must contain a `main` definition
  if (!/\b(function\s+main|main\s*=|exports\.main|module\.exports)\b/.test(content)) {
    return {
      output:
        'WriteUserScript: content must define a `main` function ' +
        '(e.g. async function main({params}) {…} or module.exports = async function main(…)). ' +
        'See ReadToolDoc({ name: "WriteUserScript" }) for examples.',
      isError: true,
    };
  }

  const userFolderRoot = getUserFolderRoot();
  const subdir = kind === 'script' ? 'scripts' : 'browser-macros';

  let targetPath: string;
  try {
    targetPath = resolveUserSubdir(userFolderRoot, ctx.userId, subdir, `${baseName}.js`);
  } catch (err) {
    return { output: `WriteUserScript: ${(err as Error).message}`, isError: true };
  }

  const targetDir = join(targetPath, '..');
  try {
    mkdirSync(targetDir, { recursive: true });
  } catch (err) {
    return { output: `WriteUserScript: failed to create ${subdir}/: ${(err as Error).message}`, isError: true };
  }

  if (existsSync(targetPath) && !overwrite) {
    return {
      output: `WriteUserScript: ${subdir}/${baseName}.js already exists. Pass overwrite: true to replace.`,
      isError: true,
    };
  }

  // Atomic write: temp file then rename
  const tmpPath = `${targetPath}.tmp.${Date.now()}`;
  try {
    writeFileSync(tmpPath, content, { encoding: 'utf-8', mode: 0o600 });
    renameSync(tmpPath, targetPath);
  } catch (err) {
    try { unlinkSync(tmpPath); } catch { /* ignore */ }
    return { output: `WriteUserScript: write failed: ${(err as Error).message}`, isError: true };
  }

  const bytes = Buffer.byteLength(content, 'utf-8');
  _deps?.auditLog?.('user_script_written', { userId: ctx.userId, subdir, filename: `${baseName}.js`, kind, bytes }, ctx.taskId ?? null);

  return {
    output: `WriteUserScript: wrote ${subdir}/${baseName}.js (${bytes} bytes, kind=${kind})`,
    isError: false,
  };
}

// ── WriteUserTemplate implementation ─────────────────────────────────────────

/** Slug regex for templates: same rules as scripts but allow dots too (e.g. v2.0). */
const TEMPLATE_SLUG_RE = /^[a-zA-Z0-9_-]+$/;

async function executeWriteUserTemplate(
  input: Record<string, unknown>,
  ctx: ToolContext,
): Promise<ToolResult> {
  if (!ctx.userId) {
    return { output: 'WriteUserTemplate: requires an authenticated user', isError: true };
  }

  const name = input['name'];
  const content = input['content'];
  const overwrite = input['overwrite'] === true;

  if (typeof name !== 'string' || !name) {
    return { output: 'WriteUserTemplate: "name" parameter is required', isError: true };
  }
  if (typeof content !== 'string') {
    return { output: 'WriteUserTemplate: "content" must be a string', isError: true };
  }

  // Strip optional .md suffix; validate as a slug
  const baseName = name.replace(/\.md$/i, '');
  if (!TEMPLATE_SLUG_RE.test(baseName)) {
    return {
      output:
        'WriteUserTemplate: "name" must contain only alphanumeric characters, dashes, or underscores (no spaces, no slashes)',
      isError: true,
    };
  }

  // Size limit: 128 KB
  const MAX_BYTES = 128 * 1024;
  if (Buffer.byteLength(content, 'utf-8') > MAX_BYTES) {
    return { output: `WriteUserTemplate: content exceeds ${MAX_BYTES} bytes`, isError: true };
  }

  const userFolderRoot = getUserFolderRoot();

  let targetPath: string;
  try {
    targetPath = resolveUserSubdir(userFolderRoot, ctx.userId, 'templates', `${baseName}.md`);
  } catch (err) {
    return { output: `WriteUserTemplate: ${(err as Error).message}`, isError: true };
  }

  const targetDir = join(targetPath, '..');
  try {
    mkdirSync(targetDir, { recursive: true });
  } catch (err) {
    return { output: `WriteUserTemplate: failed to create templates/: ${(err as Error).message}`, isError: true };
  }

  if (existsSync(targetPath) && !overwrite) {
    return {
      output: `WriteUserTemplate: templates/${baseName}.md already exists. Pass overwrite: true to replace.`,
      isError: true,
    };
  }

  // Atomic write
  const tmpPath = `${targetPath}.tmp.${Date.now()}`;
  try {
    writeFileSync(tmpPath, content, { encoding: 'utf-8', mode: 0o600 });
    renameSync(tmpPath, targetPath);
  } catch (err) {
    try { unlinkSync(tmpPath); } catch { /* ignore */ }
    return { output: `WriteUserTemplate: write failed: ${(err as Error).message}`, isError: true };
  }

  const bytes = Buffer.byteLength(content, 'utf-8');
  _deps?.auditLog?.('user_template_written', { userId: ctx.userId, filename: `${baseName}.md`, bytes }, ctx.taskId ?? null);

  return {
    output: `WriteUserTemplate: wrote templates/${baseName}.md (${bytes} bytes)`,
    isError: false,
  };
}

// ── Dispatch ──────────────────────────────────────────────────────────────────

export async function executeTool(
  name: string,
  input: Record<string, unknown>,
  ctx: ToolContext,
): Promise<ToolResult | null> {
  if (name === 'UpdateUserMemory') return executeUpdateUserMemory(input, ctx);
  if (name === 'ReadUserMemory') return executeReadUserMemory(input, ctx);
  if (name === 'ReadUserTemplate') return executeReadUserTemplate(input, ctx);
  if (name === 'RenderUserTemplate') return executeRenderUserTemplate(input, ctx);
  if (name === 'ListUserAssets') return executeListUserAssets(input, ctx);
  if (name === 'RunUserScript') return executeRunUserScript(input, ctx);
  if (name === 'WriteUserScript') return executeWriteUserScript(input, ctx);
  if (name === 'WriteUserTemplate') return executeWriteUserTemplate(input, ctx);
  return null;
}
