/**
 * user-folder.ts
 *
 * Tools for the per-user folder (data/users/{userId}/):
 *   - UpdateUserMemory / ReadUserMemory: persistent memory entries
 *   - ListUserAssets: browse browser-macros / recordings
 *   - RunUserScript / WriteUserScript: Playwright browser-macros
 *
 * Note: plain-Node scripts/ and templates/ tools were retired in 2026-06 —
 * reusable knowledge belongs in Skills, ad-hoc code runs via the Bash tool.
 */

import { existsSync, readdirSync, statSync, readFileSync, mkdirSync, writeFileSync, renameSync, unlinkSync } from 'node:fs';
import { join, extname } from 'node:path';
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 {
  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_-]+$/;

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'],
      },
    },
  },

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

  RunUserScript: {
    type: 'function',
    function: {
      name: 'RunUserScript',
      description:
        'Executes a user-authored Playwright browser-macro from the caller\'s browser-macros/ folder ' +
        '(main({ context, params }) signature, optional session_profile_id frontmatter). ' +
        'For ad-hoc code (Node, Python, …) use the Bash tool instead. ' +
        'Details via ReadToolDoc({ name: "RunUserScript" }).',
      parameters: {
        type: 'object',
        properties: {
          name: {
            type: 'string',
            description: 'Macro filename (with or without .js extension).',
          },
          params: {
            type: 'object',
            description: 'Key-value params matching the macro\'s frontmatter param spec.',
          },
        },
        required: ['name'],
      },
    },
  },

  WriteUserScript: {
    type: 'function',
    function: {
      name: 'WriteUserScript',
      description:
        'ユーザーフォルダの browser-macros/ に Playwright マクロを作成・上書きする ' +
        '(main({ context, params }) 形式)。' +
        'アドホックなコード実行 (Node / Python 等) は Bash ツールを使うこと。' +
        '詳細は ReadToolDoc({ name: "WriteUserScript" })。',
      parameters: {
        type: 'object',
        properties: {
          name: {
            type: 'string',
            description: 'ファイル名 (slug)。`.js` は自動補完される。例: "nightly-login-check"',
          },
          content: {
            type: 'string',
            description: 'ファイル全文。YAML frontmatter (params spec) + main() を含むこと',
          },
          overwrite: {
            type: 'boolean',
            description: '既存ファイルを上書きするかどうか (default: false)。false かつ既存なら error',
          },
        },
        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 };
}

// ── 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}:`];

  // 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);
    }
  }

  // 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 browser-macros are disabled (set tools.user_scripts_enabled: true in config.yaml).',
      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) ?? {};

  // Plain-Node scripts were retired (2026-06): everything is a browser-macro.
  // Reject an explicit kind="script" with a pointer to the replacement.
  if (input['kind'] === 'script') {
    return {
      output:
        'RunUserScript: plain-Node scripts/ were retired. Run ad-hoc code with the Bash tool, ' +
        'or keep reusable procedures in Skills. This tool now runs browser-macros only.',
      isError: true,
    };
  }
  const kind = 'browser-macro' as const;

  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 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 };
  }
  // Plain-Node scripts were retired (2026-06): everything is a browser-macro.
  if (input['kind'] === 'script') {
    return {
      output:
        'WriteUserScript: plain-Node scripts/ were retired. Keep reusable procedures in Skills ' +
        'and run ad-hoc code with the Bash tool. This tool now writes browser-macros only.',
      isError: true,
    };
  }
  const kind = 'browser-macro' as const;

  // 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({context, params}) {…} or module.exports = async function main(…)). ' +
        'See ReadToolDoc({ name: "WriteUserScript" }) for examples.',
      isError: true,
    };
  }

  const userFolderRoot = getUserFolderRoot();
  const subdir = '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,
  };
}

// ── 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 === 'ListUserAssets') return executeListUserAssets(input, ctx);
  if (name === 'RunUserScript') return executeRunUserScript(input, ctx);
  if (name === 'WriteUserScript') return executeWriteUserScript(input, ctx);
  return null;
}
