/**
 * TaskLens pi 扩展。
 *
 * 把 TaskLens 知识库的能力注册成 pi 的 agent 工具(17 个)与斜杠命令(4 个)。
 * 所有工具都通过 `tasklens-cli`(NDJSON over stdio)调用 Rust core,因此桌面
 * 应用、浏览器开发服务器与 agent 对知识库的语义完全一致。
 *
 * 工具名与参数与 MCP server 版本(`src-tauri` 之外的 MCP 交付物)保持一致:
 * `vault_list` / `vault_read` / … / `vault_git_sync`。
 */

import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process";
import process from "node:process";
import type {
  AgentToolResult,
  ExtensionAPI,
  ExtensionCommandContext,
  ExtensionContext,
  ToolDefinition,
} from "@earendil-works/pi-coding-agent";
import { Type } from "typebox";

/** 工具总数:session_start 的提示与测试都靠它对齐。 */
const TOOL_COUNT = 17;
const TIMEOUT_MS = 60_000;
/** vault_read 单次返回的正文字符上限,避免撑爆上下文。 */
const READ_LIMIT = 20_000;
/** task_brief 里正文的字符上限。 */
const BRIEF_BODY_LIMIT = 8_000;
/** vault_list 单次最多渲染的条目数。 */
const LIST_LIMIT = 200;

const CLI_BIN = process.platform === "win32" ? "tasklens-cli.exe" : "tasklens-cli";

const CLI_MISSING_HINT =
  `未找到 \`${CLI_BIN}\`。请让 TASK_LENS_CLI 指向二进制绝对路径,` +
  "或先执行 `bun run core:build` 并把 target/debug 加入 PATH。";

const VAULT_CONVENTIONS =
  "Vault 约定:笔记类型写在 frontmatter `type:`(目录只是分类,类型目录下的子目录不改变类型)——" +
  "`任务`(默认 `任务/`,字段 status/priority/category/created/owner/related_to)、" +
  "`日志`(默认 `工作日志/`,字段 date)、`经验`(默认 `经验/`)、`成果`(默认 `成果/`)、" +
  "`记忆碎片`(默认 `记忆碎片/`,字段 created/related_to)。" +
  "目录可在设置里改,动手前用 vault_list 看该类型笔记实际在哪。" +
  "任务正文分节:`## 背景`、`## 子任务`(`- [ ]` 清单)、`## 变更日志`(`- 日期: 内容`)。" +
  '关联用 wikilink:related_to: ["[[软包工作-任务]]"]。';

// ─────────────────────────── CLI 传输层 ───────────────────────────

type VaultPayload = { ok: true; data: unknown } | { ok: false; error: string };

function isRecord(value: unknown): value is Record<string, unknown> {
  return typeof value === "object" && value !== null && !Array.isArray(value);
}

function messageOf(cause: unknown): string {
  return cause instanceof Error ? cause.message : String(cause);
}

function describeSpawnError(cause: unknown): string {
  if (isRecord(cause) && cause.code === "ENOENT") return CLI_MISSING_HINT;
  return `无法启动 \`${CLI_BIN}\`:${messageOf(cause)}`;
}

function stderrTail(stderr: string): string {
  const trimmed = stderr.trim();
  return trimmed ? `(stderr:${trimmed.slice(-500)})` : "";
}

function parseResponse(line: string, stderr: string): VaultPayload {
  if (!line) {
    return { ok: false, error: `tasklens-cli 没有返回任何内容${stderrTail(stderr)}` };
  }
  let parsed: unknown;
  try {
    parsed = JSON.parse(line);
  } catch {
    return { ok: false, error: `tasklens-cli 返回了非法 JSON:${line.slice(0, 200)}` };
  }
  if (isRecord(parsed) && parsed.ok === true) return { ok: true, data: parsed.data };
  if (isRecord(parsed) && parsed.ok === false && typeof parsed.error === "string") {
    return { ok: false, error: parsed.error };
  }
  return { ok: false, error: `tasklens-cli 返回了未知结构:${line.slice(0, 200)}` };
}

/**
 * 执行一条知识库命令:spawn CLI,写一行 JSON,读一行响应。
 * 任何失败(找不到二进制、超时、非法 JSON、命令报错)都转成 `{ok:false}`,
 * 不抛异常,保证扩展不会把 pi 带崩。
 */
function callVault(
  command: string,
  args: Record<string, unknown>,
  signal?: AbortSignal,
): Promise<VaultPayload> {
  const cli = process.env.TASK_LENS_CLI?.trim() || CLI_BIN;
  const vault = process.env.TASK_LENS_VAULT?.trim();
  const argv = vault ? ["--vault", vault] : [];

  return new Promise<VaultPayload>((resolve) => {
    let settled = false;
    let child: ChildProcessWithoutNullStreams | undefined;
    let timer: NodeJS.Timeout | undefined;

    const finish = (result: VaultPayload): void => {
      if (settled) return;
      settled = true;
      if (timer) clearTimeout(timer);
      signal?.removeEventListener("abort", onAbort);
      child?.kill();
      resolve(result);
    };
    const onAbort = (): void => finish({ ok: false, error: `已取消:\`${command}\`` });

    if (signal?.aborted) {
      finish({ ok: false, error: `已取消:\`${command}\`` });
      return;
    }

    try {
      child = spawn(cli, argv, { stdio: ["pipe", "pipe", "pipe"], windowsHide: true });
    } catch (cause) {
      finish({ ok: false, error: describeSpawnError(cause) });
      return;
    }

    const running = child;
    timer = setTimeout(() => {
      finish({ ok: false, error: `\`${command}\` 超过 ${TIMEOUT_MS / 1000} 秒没有响应,已终止进程。` });
    }, TIMEOUT_MS);
    signal?.addEventListener("abort", onAbort, { once: true });

    let stdout = "";
    let stderr = "";
    running.stdout.setEncoding("utf8");
    running.stderr.setEncoding("utf8");

    running.stdout.on("data", (chunk: string) => {
      stdout += chunk;
      const newline = stdout.indexOf("\n");
      if (newline === -1) return;
      finish(parseResponse(stdout.slice(0, newline).trim(), stderr));
    });
    running.stderr.on("data", (chunk: string) => {
      stderr += chunk;
    });
    running.on("error", (cause) => finish({ ok: false, error: describeSpawnError(cause) }));
    running.on("close", (code) => {
      finish({
        ok: false,
        error: `tasklens-cli 提前退出(退出码 ${code ?? "?"})${stderrTail(stderr)}`,
      });
    });
    // stdin 提前断开(EPIPE)不算错误,响应仍会从 stdout 读到。
    running.stdin.on("error", () => {});
    running.stdin.end(`${JSON.stringify({ cmd: command, args })}\n`);
  });
}

async function invoke(
  command: string,
  args: Record<string, unknown>,
  signal: AbortSignal | undefined,
  format: (data: unknown) => string,
): Promise<VaultToolResult> {
  const result = await callVault(command, args, signal);
  if (!result.ok) return text(`错误:${result.error}`);
  try {
    return text(format(result.data));
  } catch (cause) {
    return text(`错误:解析知识库返回失败:${messageOf(cause)}`);
  }
}

// ─────────────────────────── 参数校验 ───────────────────────────

type Checked<T> = { ok: true; value: T } | { ok: false; error: string };

function checkPath(raw: unknown): Checked<string> {
  if (typeof raw !== "string" || !raw.trim()) {
    return { ok: false, error: "`path` 必须是非空的知识库内相对路径。" };
  }
  // 有些模型会在路径前带 @,pi 内置工具也会先剥掉。
  let value = raw.trim().replace(/^@/, "").replace(/\\/g, "/");
  if (value.startsWith("//") || value.startsWith("/") || /^[a-zA-Z]:/.test(value)) {
    return { ok: false, error: `\`path\` 必须是知识库内相对路径,不能是绝对路径:${raw}` };
  }
  const segments = value.split("/").filter((segment) => segment !== "" && segment !== ".");
  if (segments.some((segment) => segment === "..")) {
    return { ok: false, error: `\`path\` 不能包含 \`..\`:${raw}` };
  }
  value = segments.join("/");
  if (!value) return { ok: false, error: "`path` 必须是非空的知识库内相对路径。" };
  return { ok: true, value };
}

function checkNonEmptyString(raw: unknown, name: string): Checked<string> {
  if (typeof raw !== "string" || !raw.trim()) {
    return { ok: false, error: `\`${name}\` 必须是非空字符串。` };
  }
  return { ok: true, value: raw };
}

function checkLine(raw: unknown, name: string): Checked<number> {
  if (typeof raw !== "number" || !Number.isInteger(raw) || raw < 1) {
    return { ok: false, error: `\`${name}\` 必须是 ≥1 的整数行号(1 表示文件第一行)。` };
  }
  return { ok: true, value: raw };
}

function checkUpdates(raw: unknown): Checked<Record<string, unknown>> {
  if (!isRecord(raw)) {
    return { ok: false, error: "`updates` 必须是一个对象(键为字段名,值为字段内容)。" };
  }
  return { ok: true, value: raw };
}

function checkRemovals(raw: unknown): Checked<string[]> {
  if (raw === undefined || raw === null) return { ok: true, value: [] };
  if (!Array.isArray(raw) || raw.some((item) => typeof item !== "string")) {
    return { ok: false, error: "`removals` 必须是字符串数组(要删除的 frontmatter 字段名)。" };
  }
  return { ok: true, value: raw.filter((item): item is string => typeof item === "string") };
}

function checkLines(raw: unknown): Checked<number[]> {
  if (!Array.isArray(raw) || raw.length === 0) {
    return { ok: false, error: "`lines` 必须是至少含一个行号的数组。" };
  }
  const lines: number[] = [];
  for (const item of raw) {
    const checked = checkLine(item, "lines[]");
    if (!checked.ok) return { ok: false, error: checked.error };
    lines.push(checked.value);
  }
  return { ok: true, value: lines };
}

function checkLimit(raw: unknown): Checked<number | undefined> {
  if (raw === undefined || raw === null) return { ok: true, value: undefined };
  if (typeof raw !== "number" || !Number.isInteger(raw) || raw < 1) {
    return { ok: false, error: "`limit` 必须是 ≥1 的整数。" };
  }
  return { ok: true, value: raw };
}

function errorText(error: string): VaultToolResult {
  return text(`参数错误:${error}`);
}

// ─────────────────────────── 输出格式化 ───────────────────────────

/** 工具返回:一段给 agent 读的 Markdown 文本。 */
type VaultToolResult = AgentToolResult<Record<string, unknown>>;

function text(body: string): VaultToolResult {
  return { content: [{ type: "text", text: body }], details: {} };
}

function str(value: unknown, fallback = ""): string {
  return typeof value === "string" ? value : fallback;
}

function num(value: unknown, fallback = 0): number {
  return typeof value === "number" && Number.isFinite(value) ? value : fallback;
}

function list(value: unknown): unknown[] {
  return Array.isArray(value) ? value : [];
}

function pad2(value: number): string {
  return String(value).padStart(2, "0");
}

function formatWhen(ms: number): string {
  if (!Number.isFinite(ms) || ms <= 0) return "-";
  const date = new Date(ms);
  return `${date.getFullYear()}-${pad2(date.getMonth() + 1)}-${pad2(date.getDate())} ${pad2(date.getHours())}:${pad2(date.getMinutes())}`;
}

function todayIso(): string {
  const date = new Date();
  return `${date.getFullYear()}-${pad2(date.getMonth() + 1)}-${pad2(date.getDate())}`;
}

function escapeCell(value: string): string {
  return value.replace(/\|/g, "\\|").replace(/\r?\n/g, " ");
}

/** 选一个比正文里最长反引号串更长的围栏,正文含 ``` 也不会截断。 */
function fenceFor(content: string): string {
  const runs = content.match(/`+/g) ?? [];
  const longest = runs.reduce((max, run) => Math.max(max, run.length), 2);
  return "`".repeat(longest + 1);
}

function fenced(content: string, language = "markdown"): string {
  const fence = fenceFor(content);
  return `${fence}${language}\n${content}\n${fence}`;
}

function truncate(content: string, limit: number, label: string): string {
  if (content.length <= limit) return content;
  return `${content.slice(0, limit)}\n…(${label},已截断到 ${limit} 字符,原文 ${content.length} 字符)`;
}

function noteRefLines(refs: unknown[], prefix: string): string[] {
  if (refs.length === 0) return [`- ${prefix}:(无)`];
  return refs.map((raw) => {
    const ref = isRecord(raw) ? raw : {};
    const snippet = str(ref.snippet);
    const suffix = snippet ? ` — ${snippet}` : "";
    return `- ${prefix}:\`${str(ref.path)}\`(${str(ref.title) || "-"})${suffix}`;
  });
}

// ─────────────────────────── 各工具的输出 ───────────────────────────

function formatEntryTable(data: unknown, typeFilter: string | undefined): string {
  const payload = isRecord(data) ? data : {};
  const root = str(payload.root);
  const all = list(payload.entries);
  const matched = typeFilter
    ? all.filter((entry) => isRecord(entry) && str(entry.noteType) === typeFilter)
    : all;
  const shown = matched.slice(0, LIST_LIMIT);

  const header = typeFilter
    ? `Vault:\`${root}\`,类型 \`${typeFilter}\` 共 ${matched.length} 条`
    : `Vault:\`${root}\`,共 ${matched.length} 条`;
  if (shown.length === 0) {
    return `${header}\n\n(没有匹配的笔记。用 vault_search 全文检索,或确认 type 取值:任务 / 日志 / 经验 / 成果。)`;
  }

  const rows = shown.map((raw) => {
    const entry = isRecord(raw) ? raw : {};
    const frontmatter = isRecord(entry.frontmatter) ? entry.frontmatter : {};
    return [
      `\`${escapeCell(str(entry.path))}\``,
      escapeCell(str(entry.title) || str(entry.name) || "-"),
      escapeCell(str(entry.noteType) || "-"),
      escapeCell(str(frontmatter.status) || "-"),
      escapeCell(str(frontmatter.priority) || "-"),
      escapeCell(str(frontmatter.category) || "-"),
      `${num(entry.openTasks)}/${num(entry.doneTasks)}`,
      escapeCell(formatWhen(num(entry.modifiedMs))),
    ].join(" | ");
  });

  const table = [
    "| 路径 | 标题 | 类型 | 状态 | 优先级 | 分类 | 未完成/完成 | 更新时间 |",
    "| --- | --- | --- | --- | --- | --- | --- | --- |",
    ...rows.map((row) => `| ${row} |`),
  ].join("\n");

  const overflow =
    matched.length > shown.length ? `\n\n(只显示前 ${LIST_LIMIT} 条,共 ${matched.length} 条。)` : "";
  return `${header}\n\n${table}${overflow}`;
}

function formatRead(data: unknown): string {
  const note = isRecord(data) ? data : {};
  const path = str(note.path);
  const content = str(note.content);
  const meta = `\`${path}\`(${num(note.size)} 字节,更新于 ${formatWhen(num(note.modifiedMs))})`;
  const body = truncate(content, READ_LIMIT, "内容过长");
  return `${meta}\n\n${fenced(body)}`;
}

function formatSearch(data: unknown, query: string, limit: number | undefined): string {
  const payload = isRecord(data) ? data : {};
  const hits = list(payload.hits);
  const scope = limit ? `,limit=${limit}` : "";
  if (hits.length === 0) return `搜索 \`${query}\`:没有命中${scope}。`;
  const lines = hits.map((raw) => {
    const hit = isRecord(raw) ? raw : {};
    const type = str(hit.noteType);
    return `- \`${str(hit.path)}:${num(hit.line)}\`${type ? ` [${type}]` : ""} — ${str(hit.text)}`;
  });
  return `搜索 \`${query}\`:命中 ${hits.length} 条${scope}\n\n${lines.join("\n")}`;
}

function formatBrief(data: unknown): string {
  const brief = isRecord(data) ? data : {};
  const frontmatter = isRecord(brief.frontmatter) ? brief.frontmatter : {};
  const lines: string[] = [];

  lines.push(`# 任务简报:${str(brief.title) || str(brief.path)}`);
  lines.push("");
  lines.push(`- 路径:\`${str(brief.path)}\``);
  lines.push(`- 类型:${str(brief.noteType) || "未知"}`);
  lines.push(`- 子任务:${num(brief.openTasks)} 未完成 / ${num(brief.doneTasks)} 已完成`);
  const keys = Object.keys(frontmatter);
  if (keys.length > 0) {
    lines.push(`- Frontmatter:${keys.map((key) => `${key}=${JSON.stringify(frontmatter[key])}`).join(",")}`);
  }

  const subtasks = list(brief.subtasks);
  lines.push("", `## 子任务(${subtasks.length} 条,行号可用 vault_toggle_checkbox / vault_set_line / vault_delete_line)`);
  if (subtasks.length === 0) {
    lines.push("(无,可用 vault_append_item 往「子任务」小节追加 `- [ ] …`)");
  } else {
    for (const raw of subtasks) {
      const item = isRecord(raw) ? raw : {};
      const mark = item.checked === true ? "[x]" : "[ ]";
      lines.push(`- ${mark} 第 ${num(item.line)} 行:${str(item.text)}`);
    }
  }

  const changelog = list(brief.changelog);
  lines.push("", "## 变更日志");
  if (changelog.length === 0) lines.push("(无)");
  else for (const entry of changelog) lines.push(`- ${str(entry)}`);

  const workLogs = list(brief.workLogs);
  lines.push("", "## 提到它的日志");
  if (workLogs.length === 0) lines.push("(无)");
  else {
    for (const raw of workLogs) {
      const log = isRecord(raw) ? raw : {};
      lines.push(`- \`${str(log.path)}\`(${str(log.date)})`);
      for (const hit of list(log.lines)) lines.push(`  - ${str(hit)}`);
    }
  }

  const experiences = list(brief.experiences);
  const outcomes = list(brief.outcomes);
  const related = list(brief.related);
  const backlinks = list(brief.backlinks);
  lines.push("", "## 相关笔记");
  lines.push(...noteRefLines(experiences, "经验"));
  lines.push(...noteRefLines(outcomes, "成果"));
  lines.push(...noteRefLines(related, "关联(related_to)"));
  lines.push(...noteRefLines(backlinks, "反向链接"));

  const events = list(brief.events);
  lines.push("", `## 结构化事件(${events.length} 条)`);
  if (events.length === 0) lines.push("(无)");
  else {
    for (const raw of events) {
      const event = isRecord(raw) ? raw : {};
      lines.push(
        `- ${formatWhen(num(event.atMs))} · **${str(event.kind)}** · ${str(event.source) || "-"} — ${str(event.detail)}`,
      );
    }
  }

  const body = str(brief.body);
  if (body.trim()) {
    lines.push("", "## 正文", "", fenced(truncate(body, BRIEF_BODY_LIMIT, "正文过长")));
  }
  return lines.join("\n");
}

function formatEvents(data: unknown, pathFilter: string | undefined): string {
  const events = list(data);
  const scope = pathFilter ? `(path=${pathFilter})` : "";
  if (events.length === 0) return `没有事件记录${scope}。`;
  const lines = events.map((raw) => {
    const event = isRecord(raw) ? raw : {};
    return `- \`${formatWhen(num(event.atMs))}\` **${str(event.kind)}** \`${str(event.path)}\` — ${str(event.detail)}`;
  });
  return `事件 ${events.length} 条${scope}\n\n${lines.join("\n")}`;
}

function mutationConfirmation(action: string, data: unknown, detail: string): string {
  const note = isRecord(data) ? data : {};
  const suffix = detail ? `,${detail}` : "";
  return `已${action} \`${str(note.path)}\`${suffix}(当前 ${num(note.size)} 字节,更新于 ${formatWhen(num(note.modifiedMs))})。`;
}

function formatGitStatus(data: unknown): string {
  const status = isRecord(data) ? data : {};
  const detail = str(status.detail);
  if (status.isRepo !== true) {
    const reason = detail || "该知识库不是 Git 仓库";
    return `${reason}。需要先 \`git init\` 并设置远端,再回来同步。`;
  }
  const lines = [
    `- 仓库:是(分支 \`${str(status.branch) || "未知"}\`)`,
    `- 远端:${str(status.remote) || "(未设置)"}`,
    `- 上游:${str(status.upstream) || "(未设置)"}`,
    `- 领先/落后:${num(status.ahead)} / ${num(status.behind)}`,
    `- 本地变更:${num(status.changes)} 个文件`,
  ];
  if (detail) lines.push(`- 详情:${detail}`);
  return `Git 状态\n\n${lines.join("\n")}`;
}

function formatGitSync(data: unknown): string {
  const sync = isRecord(data) ? data : {};
  const steps = list(sync.steps);
  const lines = steps.map((raw) => {
    const step = isRecord(raw) ? raw : {};
    const mark = step.ok === true ? "✅" : "⚠️";
    const summary = str(step.summary);
    const detail = str(step.detail);
    return `- ${mark} ${str(step.action)}:${summary}${detail ? `(${detail})` : ""}`;
  });
  const status = formatGitStatus(sync.status);
  const headline = sync.ok === true ? "同步完成" : "同步未全部成功";
  return `${headline}\n\n${lines.join("\n") || "(没有执行任何步骤)"}\n\n${status}`;
}

// ─────────────────────────── 工具注册 ───────────────────────────

type PathParams = { path: string };
type ListParams = { type?: string };
type SearchParams = { query: string; limit?: number };
type EventsParams = { path?: string; limit?: number };
type CreateParams = { folder: string; name: string; content: string };
type WriteParams = { path: string; content: string };
type FrontmatterParams = {
  path: string;
  updates: Record<string, unknown>;
  removals?: string[];
  source?: string;
};
type SectionParams = { path: string; heading: string; text: string };
type AppendParams = { path: string; heading: string; item: string };
type SetLineParams = { path: string; line: number; text: string };
type InsertLineParams = { path: string; afterLine: number; text: string };
type DeleteLineParams = { path: string; line: number };
type ToggleParams = { path: string; lines: number[] };
type GitSyncParams = { message: string };

/** 一个知识库工具的定义,就是 pi SDK 的 `ToolDefinition`。 */
export type VaultTool = ToolDefinition;

/**
 * 17 个知识库工具的定义。
 *
 * 与传输方式无关:pi 扩展把它逐条交给 `pi.registerTool()`;把 pi 当进程内 SDK
 * 嵌进去的宿主(`agent-host/`)可以直接把它当 `createAgentSession({ customTools })`
 * 传入。模块导入本身没有任何副作用——不 spawn CLI、不读文件、不写 stdout。
 */
export const VAULT_TOOLS: VaultTool[] = [
  {
    name: "vault_list",
    label: "Vault 列表",
    description:
      `列出知识库里的笔记(可按 type 过滤),返回精简的表格(路径/标题/类型/状态/优先级/分类/未完成-完成/更新时间)。` +
      `什么时候用:开始任何知识库工作前先看全貌,或找某类笔记(如所有「任务」)。需要全文检索用 vault_search,需要单个任务的全部上下文用 vault_task_brief。${VAULT_CONVENTIONS}`,
    promptSnippet: "列出知识库笔记(可按 type=任务/日志/经验/成果 过滤)",
    promptGuidelines: [
      "Use vault_list 在动手前先了解知识库里已有哪些笔记,避免重复创建。",
    ],
    parameters: Type.Object({
      type: Type.Optional(Type.String({ description: "按笔记类型过滤:任务 / 日志 / 经验 / 成果" })),
    }),
    async execute(_toolCallId, params: ListParams, signal) {
      const type = params.type?.trim() || undefined;
      return invoke("list_vault", {}, signal, (data) => formatEntryTable(data, type));
    },
  },

  {
    name: "vault_read",
    label: "Vault 读取",
    description:
      "读取一篇笔记的原文(含 frontmatter),返回路径、大小、更新时间与全文。" +
      `什么时候用:需要看原文、确认行号或改正文之前。审计任务上下文优先用 vault_task_brief。超过 ${READ_LIMIT} 字符会截断并标注。`,
    promptSnippet: "读取知识库笔记原文(含 frontmatter 与行号参考)",
    promptGuidelines: [
      "Use vault_read 在做逐行手术(vault_set_line / vault_insert_line / vault_delete_line)前先看清行号。",
    ],
    parameters: Type.Object({
      path: Type.String({ description: "知识库内相对路径,如 任务/示例任务.md" }),
    }),
    async execute(_toolCallId, params: PathParams, signal) {
      const path = checkPath(params.path);
      if (!path.ok) return errorText(path.error);
      return invoke("read_note", { path: path.value }, signal, formatRead);
    },
  },

  {
    name: "vault_search",
    label: "Vault 检索",
    description:
      "在知识库全文里检索关键词,返回命中的路径、行号、类型与整行文本。" +
      "什么时候用:想找提到某个词/某个人/某个系统的笔记时;比 vault_list 更精确,比逐个 vault_read 更快。",
    promptSnippet: "全文检索知识库,返回命中笔记的路径与行号",
    parameters: Type.Object({
      query: Type.String({ description: "检索关键词" }),
      limit: Type.Optional(Type.Integer({ description: "最多返回多少条命中,默认 50" })),
    }),
    async execute(_toolCallId, params: SearchParams, signal) {
      const query = checkNonEmptyString(params.query, "query");
      if (!query.ok) return errorText(query.error);
      const limit = checkLimit(params.limit);
      if (!limit.ok) return errorText(limit.error);
      const args: Record<string, unknown> = { query: query.value };
      if (limit.value !== undefined) args.limit = limit.value;
      return invoke("search", args, signal, (data) => formatSearch(data, query.value, limit.value));
    },
  },

  {
    name: "vault_task_brief",
    label: "任务简报",
    description:
      "一次拿到一个任务的全部上下文:任务本身、子任务(含行号)、变更日志、提到它的日志行、相关经验/成果/关联/反向链接,以及结构化事件。" +
      `什么时候用:这是「信息梳理」的主入口——接手任务、写日志、沉淀经验、更新子任务之前先用它,避免反复 vault_read。${VAULT_CONVENTIONS}`,
    promptSnippet: "读取任务简报:子任务行号 + 变更日志 + 相关日志/经验/成果 + 事件",
    promptGuidelines: [
      "Use vault_task_brief 作为任务信息梳理的第一步,它一次给出子任务行号与全部关联,比多次 vault_read 更省上下文。",
    ],
    parameters: Type.Object({
      path: Type.String({ description: "任务笔记的知识库内相对路径" }),
    }),
    async execute(_toolCallId, params: PathParams, signal) {
      const path = checkPath(params.path);
      if (!path.ok) return errorText(path.error);
      return invoke("task_brief", { path: path.value }, signal, formatBrief);
    },
  },

  {
    name: "vault_events",
    label: "Vault 事件",
    description:
      "查询索引里记录的任务事件(创建、状态变更、frontmatter 改动等),可按笔记路径过滤、按数量截断。" +
      "什么时候用:想知道某篇笔记最近发生了什么、或整体最近的活动轨迹时。",
    promptSnippet: "列出知识库的结构化事件(可按 path 过滤)",
    parameters: Type.Object({
      path: Type.Optional(Type.String({ description: "只看这篇笔记的事件;省略表示全部" })),
      limit: Type.Optional(Type.Integer({ description: "最多返回多少条,默认 200" })),
    }),
    async execute(_toolCallId, params: EventsParams, signal) {
      const args: Record<string, unknown> = {};
      let pathFilter: string | undefined;
      if (params.path !== undefined) {
        const path = checkPath(params.path);
        if (!path.ok) return errorText(path.error);
        pathFilter = path.value;
        args.path = path.value;
      }
      const limit = checkLimit(params.limit);
      if (!limit.ok) return errorText(limit.error);
      if (limit.value !== undefined) args.limit = limit.value;
      return invoke("list_events", args, signal, (data) => formatEvents(data, pathFilter));
    },
  },

  {
    name: "vault_create_note",
    label: "新建笔记",
    description:
      "在知识库里新建一篇笔记(自动补 .md 后缀、自动建目录、已存在会报错)。" +
      `什么时候用:新建任务/日志/经验/成果时。请同时按约定写好 frontmatter 与正文分节;建完后用 vault_update_frontmatter 补字段。${VAULT_CONVENTIONS}`,
    promptSnippet: "新建知识库笔记(按约定写 frontmatter 与分节)",
    promptGuidelines: [
      "Use vault_create_note 新建笔记,并在 content 里直接按知识库约定写好 frontmatter(type/status/…)与正文分节。",
    ],
    parameters: Type.Object({
      folder: Type.String({ description: "目标文件夹,如 任务;留空表示知识库根目录" }),
      name: Type.String({ description: "笔记名(可带 .md,会自动去掉)" }),
      content: Type.String({ description: "笔记完整内容,含 frontmatter 与正文" }),
    }),
    async execute(_toolCallId, params: CreateParams, signal) {
      const name = checkNonEmptyString(params.name, "name");
      if (!name.ok) return errorText(name.error);
      const content = typeof params.content === "string" ? params.content : undefined;
      if (content === undefined) return errorText("`content` 必须是字符串。");
      const folder = typeof params.folder === "string" ? params.folder : "";
      return invoke("create_note", { folder, name: name.value, content }, signal, (data) =>
        mutationConfirmation("创建笔记", data, "记得随后用 vault_update_frontmatter 补全 type 等字段"),
      );
    },
  },

  {
    name: "vault_create_folder",
    label: "新建文件夹",
    description:
      "在知识库里新建文件夹(可多级,如 经验/会议纪要),已存在时也算成功。" +
      "什么时候用:需要把笔记归档到新目录、或知识库约定目录还没建时;建完再用 vault_create_note 往里放笔记。",
    promptSnippet: "在知识库内新建文件夹(可多级)",
    parameters: Type.Object({
      path: Type.String({ description: "要创建的知识库内相对目录,可多级,如 经验/会议纪要" }),
    }),
    async execute(_toolCallId, params: PathParams, signal) {
      const path = checkPath(params.path);
      if (!path.ok) return errorText(path.error);
      return invoke("create_folder", { path: path.value }, signal, (data) => {
        const info = isRecord(data) ? data : {};
        return `已创建文件夹 \`${str(info.path)}\`。`;
      });
    },
  },

  {
    name: "vault_write",
    label: "写入笔记",
    description:
      "整篇覆盖写入一篇已存在的笔记(不存在的路径也会被创建)。" +
      "什么时候用:需要整篇重写时。只改一段或几行时优先 vault_replace_section / vault_set_line,避免误伤其余内容。",
    promptSnippet: "整篇覆盖写入知识库笔记",
    parameters: Type.Object({
      path: Type.String({ description: "知识库内相对路径" }),
      content: Type.String({ description: "新的完整内容" }),
    }),
    async execute(_toolCallId, params: WriteParams, signal) {
      const path = checkPath(params.path);
      if (!path.ok) return errorText(path.error);
      if (typeof params.content !== "string") return errorText("`content` 必须是字符串。");
      return invoke("save_note", { path: path.value, content: params.content }, signal, (data) =>
        mutationConfirmation("写入", data, ""),
      );
    },
  },

  {
    name: "vault_update_frontmatter",
    label: "更新 Frontmatter",
    description:
      "按插入顺序更新/新增 frontmatter 字段,并可删除指定字段(removals)。" +
      `什么时候用:设置或修改 type/status/priority/category/created/owner/related_to 等字段时。${VAULT_CONVENTIONS}`,
    promptSnippet: "更新笔记 frontmatter 字段(含 related_to wikilink 关联)",
    promptGuidelines: [
      "Use vault_update_frontmatter 维护任务字段与 related_to 关联;related_to 的值用 [[笔记名]] wikilink 数组。",
    ],
    parameters: Type.Object({
      path: Type.String({ description: "知识库内相对路径" }),
      updates: Type.Object({}, { description: "要写入的字段(按插入顺序)", additionalProperties: true }),
      removals: Type.Optional(
        Type.Array(Type.String(), { description: "要删除的字段名" }),
      ),
      source: Type.Optional(Type.String({ description: "写入者标注,默认 app;agent 写入可填 pi" })),
    }),
    async execute(_toolCallId, params: FrontmatterParams, signal) {
      const path = checkPath(params.path);
      if (!path.ok) return errorText(path.error);
      const updates = checkUpdates(params.updates);
      if (!updates.ok) return errorText(updates.error);
      const removals = checkRemovals(params.removals);
      if (!removals.ok) return errorText(removals.error);

      const args: Record<string, unknown> = { path: path.value, updates: updates.value };
      if (removals.value.length > 0) args.removals = removals.value;
      if (typeof params.source === "string" && params.source.trim()) args.source = params.source.trim();

      const changed = [
        ...Object.keys(updates.value).map((key) => `+${key}`),
        ...removals.value.map((key) => `-${key}`),
      ];
      return invoke("update_frontmatter", args, signal, (data) =>
        mutationConfirmation("更新 frontmatter", data, changed.length > 0 ? changed.join("、") : "无字段变化"),
      );
    },
  },

  {
    name: "vault_replace_section",
    label: "替换小节",
    description:
      "按 `## 标题` 整体替换一个正文小节(小节不存在时会创建)。text 传空字符串表示清空该小节。" +
      `什么时候用:重写「## 背景」这类整段内容。「## 变更日志」「## 子任务」这类清单优先用 vault_append_item / vault_toggle_checkbox。${VAULT_CONVENTIONS}`,
    promptSnippet: "按 ## 标题整体替换正文小节(缺失则创建)",
    parameters: Type.Object({
      path: Type.String({ description: "知识库内相对路径" }),
      heading: Type.String({ description: "小节标题,如 背景(不用带 ##)" }),
      text: Type.String({ description: "新的小节正文" }),
    }),
    async execute(_toolCallId, params: SectionParams, signal) {
      const path = checkPath(params.path);
      if (!path.ok) return errorText(path.error);
      const heading = checkNonEmptyString(params.heading, "heading");
      if (!heading.ok) return errorText(heading.error);
      if (typeof params.text !== "string") return errorText("`text` 必须是字符串(清空小节传空字符串)。");
      return invoke(
        "replace_section",
        { path: path.value, heading: heading.value, text: params.text },
        signal,
        (data) => mutationConfirmation("替换小节", data, `## ${heading.value}`),
      );
    },
  },

  {
    name: "vault_append_item",
    label: "追加行",
    description:
      "在指定 `## 标题` 小节的末尾追加一行(小节不存在时会创建)。" +
      `什么时候用:往「## 子任务」追加 \`- [ ] …\`,或往「## 变更日志」追加 \`- 日期: 内容\`。${VAULT_CONVENTIONS}`,
    promptSnippet: "在 ## 子任务 / ## 变更日志 小节末尾追加一行",
    parameters: Type.Object({
      path: Type.String({ description: "知识库内相对路径" }),
      heading: Type.String({ description: "小节标题,如 子任务、变更日志" }),
      item: Type.String({ description: "要追加的整行内容,如 `- [ ] 联调支付` 或 `- 2026-09-12: 创建任务`" }),
    }),
    async execute(_toolCallId, params: AppendParams, signal) {
      const path = checkPath(params.path);
      if (!path.ok) return errorText(path.error);
      const heading = checkNonEmptyString(params.heading, "heading");
      if (!heading.ok) return errorText(heading.error);
      if (typeof params.item !== "string" || !params.item.trim()) {
        return errorText("`item` 必须是要追加的非空整行内容。");
      }
      return invoke(
        "append_body_item",
        { path: path.value, heading: heading.value, item: params.item },
        signal,
        (data) => mutationConfirmation("追加内容", data, `## ${heading.value}`),
      );
    },
  },

  {
    name: "vault_set_line",
    label: "改写行",
    description:
      "按 1 起算的行号改写正文某一行(行号可用 vault_read 或 vault_task_brief 获得)。" +
      "什么时候用:修正某一行的文本,尤其是子任务描述或变更日志条目。",
    promptSnippet: "按行号改写正文一行",
    parameters: Type.Object({
      path: Type.String({ description: "知识库内相对路径" }),
      line: Type.Integer({ description: "1 起算的行号" }),
      text: Type.String({ description: "该行的新内容" }),
    }),
    async execute(_toolCallId, params: SetLineParams, signal) {
      const path = checkPath(params.path);
      if (!path.ok) return errorText(path.error);
      const line = checkLine(params.line, "line");
      if (!line.ok) return errorText(line.error);
      if (typeof params.text !== "string") return errorText("`text` 必须是字符串。");
      return invoke(
        "set_body_line",
        { path: path.value, line: line.value, text: params.text },
        signal,
        (data) => mutationConfirmation("改写", data, `第 ${line.value} 行`),
      );
    },
  },

  {
    name: "vault_insert_line",
    label: "插入行",
    description:
      "在指定行(afterLine,1 起算)之后插入一行。" +
      "什么时候用:新增子任务、给清单补嵌套行;行号先用 vault_read 或 vault_task_brief 确认。",
    promptSnippet: "在某一行之后插入新行",
    parameters: Type.Object({
      path: Type.String({ description: "知识库内相对路径" }),
      afterLine: Type.Integer({ description: "在这行之后插入(1 起算);0 表示插到正文最前" }),
      text: Type.String({ description: "要插入的整行内容" }),
    }),
    async execute(_toolCallId, params: InsertLineParams, signal) {
      const path = checkPath(params.path);
      if (!path.ok) return errorText(path.error);
      if (typeof params.afterLine !== "number" || !Number.isInteger(params.afterLine) || params.afterLine < 0) {
        return errorText("`afterLine` 必须是 ≥0 的整数行号(0 表示插到正文最前)。");
      }
      if (typeof params.text !== "string") return errorText("`text` 必须是字符串。");
      return invoke(
        "insert_body_line",
        { path: path.value, afterLine: params.afterLine, text: params.text },
        signal,
        (data) => mutationConfirmation("插入行", data, `第 ${params.afterLine} 行之后`),
      );
    },
  },

  {
    name: "vault_delete_line",
    label: "删除行",
    description:
      "按 1 起算的行号删除正文一行。删除后其余行号会前移,连续删除时要从大到小、或每删一次重新读行号。" +
      "什么时候用:清掉作废的子任务或错误的日志行。",
    promptSnippet: "按行号删除正文一行",
    parameters: Type.Object({
      path: Type.String({ description: "知识库内相对路径" }),
      line: Type.Integer({ description: "要删除的 1 起算行号" }),
    }),
    async execute(_toolCallId, params: DeleteLineParams, signal) {
      const path = checkPath(params.path);
      if (!path.ok) return errorText(path.error);
      const line = checkLine(params.line, "line");
      if (!line.ok) return errorText(line.error);
      return invoke(
        "delete_body_line",
        { path: path.value, line: line.value },
        signal,
        (data) => mutationConfirmation("删除", data, `第 ${line.value} 行`),
      );
    },
  },

  {
    name: "vault_toggle_checkbox",
    label: "勾选复选框",
    description:
      "切换若干行上的 `- [ ]` / `- [x]` 状态(一次可传多行,行号 1 起算、来自 vault_task_brief 的子任务列表)。" +
      "什么时候用:完成任务、勾掉子任务时。「## 变更日志」请用 vault_append_item 追加而不是勾选。",
    promptSnippet: "勾选/取消勾选子任务复选框(按行号,可多行)",
    promptGuidelines: [
      "Use vault_toggle_checkbox 勾选子任务;先用 vault_task_brief 或 vault_read 拿到准确行号。",
    ],
    parameters: Type.Object({
      path: Type.String({ description: "知识库内相对路径" }),
      lines: Type.Array(Type.Integer(), { description: "要切换的 1 起算行号数组" }),
    }),
    async execute(_toolCallId, params: ToggleParams, signal) {
      const path = checkPath(params.path);
      if (!path.ok) return errorText(path.error);
      const lines = checkLines(params.lines);
      if (!lines.ok) return errorText(lines.error);
      return invoke(
        "toggle_checkbox",
        { path: path.value, lines: lines.value },
        signal,
        (data) => mutationConfirmation("切换复选框", data, `第 ${lines.value.join("、")} 行`),
      );
    },
  },

  {
    name: "vault_git_status",
    label: "Git 状态",
    description:
      "查看知识库的 Git 状态:是否仓库、分支、远端、领先/落后、本地变更数量。" +
      "什么时候用:同步前先看有没有东西要提交;也能快速确认知识库是否已纳入版本控制。",
    promptSnippet: "查看知识库的 Git 分支/远端/变更状态",
    parameters: Type.Object({}),
    async execute(_toolCallId, _params: Record<string, never>, signal) {
      return invoke("git_status", {}, signal, formatGitStatus);
    },
  },

  {
    name: "vault_git_sync",
    label: "Git 同步",
    description:
      "提交并同步知识库的 Git 改动(拉取、提交、推送,由 core 按仓库状态决定步骤)。" +
      "什么时候用:写完一批笔记后做版本化。先 vault_git_status 看清状态;message 建议写成 `<日期> <一句话概括>`,留空则用设置里的默认提交信息。",
    promptSnippet: "提交并推送知识库的 Git 改动",
    parameters: Type.Object({
      message: Type.String({ description: "提交信息;留空使用设置里的默认提交信息" }),
    }),
    async execute(_toolCallId, params: GitSyncParams, signal) {
      if (typeof params.message !== "string") return errorText("`message` 必须是字符串。");
      return invoke("git_sync", { message: params.message }, signal, formatGitSync);
    },
  },
];

function registerTools(pi: ExtensionAPI): void {
  for (const tool of VAULT_TOOLS) pi.registerTool(tool);
}

// ─────────────────────────── 斜杠命令 ───────────────────────────

function sendInstruction(pi: ExtensionAPI, ctx: ExtensionCommandContext, body: string): void {
  if (typeof pi.sendMessage === "function") {
    pi.sendMessage(
      { customType: "tasklens", content: body, display: true },
      { triggerTurn: true, deliverAs: "steer" },
    );
    return;
  }
  ctx.ui.notify(body, "info");
}

function scopedSuffix(args: string): string {
  const target = args.trim();
  return target ? `\n\n本次目标(用户指定):${target}` : "";
}

function registerCommands(pi: ExtensionAPI): void {
  pi.registerCommand("tasklens-file", {
    description: "把当前对话里的待办整理成 TaskLens 任务笔记(可跟一句补充说明)",
    handler: async (args, ctx) => {
      try {
        sendInstruction(
          pi,
          ctx,
          `请把当前对话里尚未落地的待办整理进 TaskLens 知识库:
1. 先调用 vault_list({ type: "任务" }) 看已有哪些任务,避免重复建笔记;必要时用 vault_search 查关键词。
2. 每个新任务用 vault_create_note 建到任务类型目录(默认 \`任务/\`;先看 vault_list 里已有任务实际在哪个目录,跟着它走),content 里按知识库约定直接写好 frontmatter(type: 任务、status、priority、category、created: ${todayIso()}、owner)与正文分节 \`## 背景\`、\`## 子任务\`(逐条 \`- [ ] …\`)、\`## 变更日志\`(\`- ${todayIso()}: 创建任务\`)。
3. 用 vault_update_frontmatter 补全字段,并用 related_to 的 [[wikilink]] 关联已有任务/日志。
4. 建完后用 vault_task_brief 复查子任务与关联,确认没有遗漏。
只整理对话里真实出现的待办,不要发明任务。最后汇报「新增/更新了哪些笔记」。${scopedSuffix(args)}`,
        );
      } catch (cause) {
        ctx.ui.notify(`发送 TaskLens 指令失败:${messageOf(cause)}`, "error");
      }
    },
  });

  pi.registerCommand("tasklens-today", {
    description: "梳理今天的日志,并把内容归并到任务/经验/成果",
    handler: async (args, ctx) => {
      try {
        const today = todayIso();
        sendInstruction(
          pi,
          ctx,
          `请梳理今天(${today})的日志:
1. 先用 vault_list({ type: "日志" }) 找到今天的日志(文件名格式可在设置里改,默认 \`工作日志/${today}-工作日志.md\`)并 vault_read;不存在就用 vault_create_note 建,frontmatter 写 \`type: 日志\`、\`date: ${today}\`。
2. 通读对话与日志,把内容归并到位:任务进展用 vault_task_brief 定位子任务行号后,用 vault_toggle_checkbox 勾选、用 vault_append_item 往 \`## 变更日志\` 追加 \`- ${today}: …\`;可复用的方法沉淀到经验类型目录(默认 \`经验/\`),产出成果沉淀到成果类型目录(默认 \`成果/\`)。
3. 新沉淀的笔记用 vault_update_frontmatter 的 related_to 反向关联任务([[wikilink]])。
4. 汇报:更新了哪些文件、勾掉了哪些子任务、新增了哪些经验/成果。${scopedSuffix(args)}`,
        );
      } catch (cause) {
        ctx.ui.notify(`发送 TaskLens 指令失败:${messageOf(cause)}`, "error");
      }
    },
  });

  pi.registerCommand("tasklens-distill", {
    description: "从已完成的任务里沉淀经验与成果(可用参数指定任务)",
    handler: async (args, ctx) => {
      try {
        sendInstruction(
          pi,
          ctx,
          `请从任务里沉淀经验与成果:
1. 用 vault_list({ type: "任务" }) 列出任务,挑出最近完成或阶段性完成的任务(status 为已完成,或变更日志已有结论的);候选不明确时先问我选哪个。
2. 用 vault_task_brief 读该任务的背景、子任务、变更日志、相关日志。
3. 把可复用的方法写成经验笔记(默认 \`经验/<主题>.md\`,frontmatter \`type: 经验\`),把可交付的产出写成成果笔记(默认 \`成果/<主题>.md\`,frontmatter \`type: 成果\`),都用 vault_create_note 创建,正文写清「背景 / 做法 / 结论」;目录以 vault_list 里该类型笔记实际所在为准。
4. 用 vault_update_frontmatter 在任务与新建笔记之间补 related_to([[wikilink]]),并用 vault_append_item 在任务 \`## 变更日志\` 追加 \`- ${todayIso()}: 沉淀经验/成果 [[…]]\`。
5. 汇报新建了哪些笔记、和哪个任务关联。${scopedSuffix(args)}`,
        );
      } catch (cause) {
        ctx.ui.notify(`发送 TaskLens 指令失败:${messageOf(cause)}`, "error");
      }
    },
  });

  pi.registerCommand("tasklens-sync", {
    description: "查看知识库的 Git 状态,必要时提交并同步",
    handler: async (args, ctx) => {
      try {
        sendInstruction(
          pi,
          ctx,
          `请同步 TaskLens 知识库的 Git:
1. 先调用 vault_git_status 看分支、远端、领先/落后与变更数量。
2. 如果不是 Git 仓库,直接告诉我需要先 \`git init\` / 设置远端,不要擅自初始化。
3. 若有本地变更,用 vault_git_sync({ message: "${todayIso()} <一句话概括本次改动>" }) 提交并同步;提交信息要说明改了什么。
4. 把 vault_git_status / vault_git_sync 的关键结果汇报给我(分支、领先/落后、提交与推送结果)。${scopedSuffix(args)}`,
        );
      } catch (cause) {
        ctx.ui.notify(`发送 TaskLens 指令失败:${messageOf(cause)}`, "error");
      }
    },
  });
}

// ─────────────────────────── 入口 ───────────────────────────

let announced = false;

/** 第一次会话时确认知识库可用,然后提示工具已就绪。 */
async function announceReady(ctx: ExtensionContext): Promise<void> {
  if (announced) return;
  announced = true;
  const probe = await callVault("active_vault", {});
  if (!probe.ok) return; // CLI 缺失或没选知识库:留给工具调用时给出明确错误
  const root = isRecord(probe.data) ? str(probe.data.root) : "";
  if (!root) return;
  ctx.ui.notify(`TaskLens 知识库工具已就绪(${TOOL_COUNT} 个):${root}`, "info");
}

export default function tasklensExtension(pi: ExtensionAPI): void {
  registerTools(pi);
  registerCommands(pi);
  pi.on("session_start", async (_event, ctx) => {
    try {
      await announceReady(ctx);
    } catch {
      // 提示失败不影响工具可用性。
    }
  });
}