import fs from "node:fs/promises";
import type { Command } from "commander";
import { callGatewayFromCli } from "openclaw/plugin-sdk/gateway-runtime";
import type { OpenClawConfig } from "../api.js";
import { applyMemoryWikiMutation } from "./apply.js";
import {
  importChatGptConversations,
  rollbackChatGptImportRun,
  type ChatGptImportResult,
  type ChatGptRollbackResult,
} from "./chatgpt-import.js";
import { compileMemoryWikiVault } from "./compile.js";
import {
  resolveMemoryWikiConfig,
  WIKI_SEARCH_BACKENDS,
  WIKI_SEARCH_CORPORA,
  type MemoryWikiPluginConfig,
  type ResolvedMemoryWikiConfig,
} from "./config.js";
import { ingestMemoryWikiSource } from "./ingest.js";
import { lintMemoryWikiVault } from "./lint.js";
import {
  probeObsidianCli,
  runObsidianCommand,
  runObsidianDaily,
  runObsidianOpen,
  runObsidianSearch,
} from "./obsidian.js";
import {
  getMemoryWikiPage,
  searchMemoryWiki,
  WIKI_SEARCH_MODES,
  type WikiSearchMode,
} from "./query.js";
import { syncMemoryWikiImportedSources } from "./source-sync.js";
import type { MemoryWikiImportedSourceSyncResult } from "./source-sync.js";
import {
  buildMemoryWikiDoctorReport,
  renderMemoryWikiDoctor,
  renderMemoryWikiStatus,
  type MemoryWikiDoctorReport,
  type MemoryWikiStatus,
  resolveMemoryWikiStatus,
} from "./status.js";
import { initializeMemoryWikiVault } from "./vault.js";

const WIKI_GATEWAY_TIMEOUT_MS = "30000";
const GATEWAY_TERMINAL_STRING_MAX_CHARS = 2_000;
const GATEWAY_RESPONSE_MAX_ARRAY_ITEMS = 10_000;
const GATEWAY_RESPONSE_MAX_STRING_CHARS = 10_000;
const GATEWAY_RESPONSE_MAX_CODE_CHARS = 256;
const ANSI_ESCAPE_SEQUENCE_PATTERN = new RegExp(
  String.raw`(?:\x1B\[[0-?]*[ -/]*[@-~]|\x1B[@-Z\\-_]|\x9B[0-?]*[ -/]*[@-~])`,
  "g",
);
const TERMINAL_CONTROL_CHARACTER_PATTERN = new RegExp(String.raw`[\x00-\x1F\x7F-\x9F]+`, "g");
const UNICODE_FORMAT_CONTROL_PATTERN = /[\u061C\u200B-\u200F\u202A-\u202E\u2060-\u206F\uFEFF]/g;

type WikiStatusCommandOptions = {
  json?: boolean;
};

type WikiDoctorCommandOptions = {
  json?: boolean;
};

type WikiInitCommandOptions = {
  json?: boolean;
};

type WikiCompileCommandOptions = {
  json?: boolean;
};

type WikiLintCommandOptions = {
  json?: boolean;
};

type WikiIngestCommandOptions = {
  json?: boolean;
  title?: string;
};

type WikiSearchCommandOptions = {
  json?: boolean;
  maxResults?: number;
  backend?: ResolvedMemoryWikiConfig["search"]["backend"];
  corpus?: ResolvedMemoryWikiConfig["search"]["corpus"];
  mode?: WikiSearchMode;
};

type WikiGetCommandOptions = {
  json?: boolean;
  from?: number;
  lines?: number;
  backend?: ResolvedMemoryWikiConfig["search"]["backend"];
  corpus?: ResolvedMemoryWikiConfig["search"]["corpus"];
};

type WikiApplySynthesisCommandOptions = {
  json?: boolean;
  body?: string;
  bodyFile?: string;
  sourceId?: string[];
  contradiction?: string[];
  question?: string[];
  confidence?: number;
  status?: string;
};

type WikiApplyMetadataCommandOptions = {
  json?: boolean;
  sourceId?: string[];
  contradiction?: string[];
  question?: string[];
  confidence?: number;
  clearConfidence?: boolean;
  status?: string;
};

type WikiBridgeImportCommandOptions = {
  json?: boolean;
};

type WikiUnsafeLocalImportCommandOptions = {
  json?: boolean;
};

type WikiChatGptImportCommandOptions = {
  json?: boolean;
  dryRun?: boolean;
  export?: string;
};

type WikiChatGptRollbackCommandOptions = {
  json?: boolean;
};

type WikiObsidianSearchCommandOptions = {
  json?: boolean;
};

type WikiObsidianOpenCommandOptions = {
  json?: boolean;
};

type WikiObsidianCommandCommandOptions = {
  json?: boolean;
};

type WikiObsidianDailyCommandOptions = {
  json?: boolean;
};

function isResolvedMemoryWikiConfig(
  config: MemoryWikiPluginConfig | ResolvedMemoryWikiConfig | undefined,
): config is ResolvedMemoryWikiConfig {
  return Boolean(
    config &&
    "vaultMode" in config &&
    "vault" in config &&
    "bridge" in config &&
    "obsidian" in config &&
    "unsafeLocal" in config,
  );
}

function sanitizeGatewayStringForTerminal(value: string): string {
  const truncated =
    value.length > GATEWAY_TERMINAL_STRING_MAX_CHARS
      ? value.slice(0, GATEWAY_TERMINAL_STRING_MAX_CHARS)
      : value;
  const sanitized = truncated
    .replace(ANSI_ESCAPE_SEQUENCE_PATTERN, "")
    .replace(TERMINAL_CONTROL_CHARACTER_PATTERN, " ")
    .replace(UNICODE_FORMAT_CONTROL_PATTERN, "");
  return value.length > GATEWAY_TERMINAL_STRING_MAX_CHARS
    ? `${sanitized}... [truncated]`
    : sanitized;
}

function escapeGatewayJsonForTerminal(json: string): string {
  return json.replace(UNICODE_FORMAT_CONTROL_PATTERN, (char) => {
    const codePoint = char.codePointAt(0);
    return typeof codePoint === "number" ? `\\u${codePoint.toString(16).padStart(4, "0")}` : "";
  });
}

function writeOutput(output: string, writer: Pick<NodeJS.WriteStream, "write"> = process.stdout) {
  writer.write(output.endsWith("\n") ? output : `${output}\n`);
}

function shouldRouteBridgeRuntimeThroughGateway(config: ResolvedMemoryWikiConfig): boolean {
  return (
    config.vaultMode === "bridge" && config.bridge.enabled && config.bridge.readMemoryArtifacts
  );
}

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

function isBoundedGatewayString(
  value: unknown,
  maxChars = GATEWAY_RESPONSE_MAX_STRING_CHARS,
): value is string {
  return typeof value === "string" && value.length <= maxChars;
}

function isStringArray(
  value: unknown,
  maxChars = GATEWAY_RESPONSE_MAX_STRING_CHARS,
): value is string[] {
  return (
    Array.isArray(value) &&
    value.length <= GATEWAY_RESPONSE_MAX_ARRAY_ITEMS &&
    value.every((item) => isBoundedGatewayString(item, maxChars))
  );
}

function hasNumberFields(value: Record<string, unknown>, keys: readonly string[]): boolean {
  return keys.every((key) => typeof value[key] === "number");
}

function isWarningList(value: unknown): boolean {
  return (
    Array.isArray(value) &&
    value.length <= GATEWAY_RESPONSE_MAX_ARRAY_ITEMS &&
    value.every(
      (item) =>
        isRecord(item) &&
        isBoundedGatewayString(item.code, GATEWAY_RESPONSE_MAX_CODE_CHARS) &&
        isBoundedGatewayString(item.message),
    )
  );
}

function isMemoryWikiStatus(value: unknown): value is MemoryWikiStatus {
  if (!isRecord(value)) {
    return false;
  }
  const bridge = value.bridge;
  const obsidianCli = value.obsidianCli;
  const unsafeLocal = value.unsafeLocal;
  const pageCounts = value.pageCounts;
  const sourceCounts = value.sourceCounts;
  return (
    isBoundedGatewayString(value.vaultMode, GATEWAY_RESPONSE_MAX_CODE_CHARS) &&
    isBoundedGatewayString(value.renderMode, GATEWAY_RESPONSE_MAX_CODE_CHARS) &&
    isBoundedGatewayString(value.vaultPath) &&
    typeof value.vaultExists === "boolean" &&
    (typeof value.bridgePublicArtifactCount === "number" ||
      value.bridgePublicArtifactCount === null) &&
    isRecord(bridge) &&
    typeof bridge.enabled === "boolean" &&
    isRecord(obsidianCli) &&
    typeof obsidianCli.enabled === "boolean" &&
    typeof obsidianCli.requested === "boolean" &&
    typeof obsidianCli.available === "boolean" &&
    (isBoundedGatewayString(obsidianCli.command) || obsidianCli.command === null) &&
    isRecord(unsafeLocal) &&
    typeof unsafeLocal.allowPrivateMemoryCoreAccess === "boolean" &&
    typeof unsafeLocal.pathCount === "number" &&
    isRecord(pageCounts) &&
    hasNumberFields(pageCounts, ["source", "entity", "concept", "synthesis", "report"]) &&
    isRecord(sourceCounts) &&
    hasNumberFields(sourceCounts, ["native", "bridge", "bridgeEvents", "unsafeLocal", "other"]) &&
    isWarningList(value.warnings)
  );
}

function isMemoryWikiDoctorReport(value: unknown): value is MemoryWikiDoctorReport {
  return (
    isRecord(value) &&
    typeof value.healthy === "boolean" &&
    typeof value.warningCount === "number" &&
    isMemoryWikiStatus(value.status) &&
    Array.isArray(value.fixes) &&
    value.fixes.length <= GATEWAY_RESPONSE_MAX_ARRAY_ITEMS &&
    value.fixes.every(
      (item) =>
        isRecord(item) &&
        isBoundedGatewayString(item.code, GATEWAY_RESPONSE_MAX_CODE_CHARS) &&
        isBoundedGatewayString(item.message),
    )
  );
}

function isMemoryWikiImportResult(value: unknown): value is MemoryWikiImportedSourceSyncResult {
  return (
    isRecord(value) &&
    hasNumberFields(value, [
      "importedCount",
      "updatedCount",
      "skippedCount",
      "removedCount",
      "artifactCount",
      "workspaces",
    ]) &&
    isStringArray(value.pagePaths) &&
    typeof value.indexesRefreshed === "boolean" &&
    isStringArray(value.indexUpdatedFiles) &&
    isBoundedGatewayString(value.indexRefreshReason, GATEWAY_RESPONSE_MAX_CODE_CHARS)
  );
}

function validateWikiGatewayResult(
  method: "wiki.status" | "wiki.doctor" | "wiki.bridge.import",
  value: unknown,
): MemoryWikiStatus | MemoryWikiDoctorReport | MemoryWikiImportedSourceSyncResult {
  if (method === "wiki.status" && isMemoryWikiStatus(value)) {
    return value;
  }
  if (method === "wiki.doctor" && isMemoryWikiDoctorReport(value)) {
    return value;
  }
  if (method === "wiki.bridge.import" && isMemoryWikiImportResult(value)) {
    return value;
  }
  throw new Error(`Invalid Gateway response for ${method}.`);
}

async function callWikiGateway(method: "wiki.status"): Promise<MemoryWikiStatus>;
async function callWikiGateway(method: "wiki.doctor"): Promise<MemoryWikiDoctorReport>;
async function callWikiGateway(
  method: "wiki.bridge.import",
): Promise<MemoryWikiImportedSourceSyncResult>;
async function callWikiGateway(method: "wiki.status" | "wiki.doctor" | "wiki.bridge.import") {
  const result = await callGatewayFromCli(method, { timeout: WIKI_GATEWAY_TIMEOUT_MS }, undefined, {
    progress: false,
  });
  return validateWikiGatewayResult(method, result);
}

function normalizeCliStringList(values?: string[]): string[] | undefined {
  if (!values) {
    return undefined;
  }
  const normalized = values
    .map((value) => value.trim())
    .filter(Boolean)
    .filter((value, index, all) => all.indexOf(value) === index);
  return normalized.length > 0 ? normalized : undefined;
}

function collectCliValues(value: string, acc: string[] = []) {
  acc.push(value);
  return acc;
}

function parseWikiSearchEnumOption<T extends string>(
  value: string,
  allowed: readonly T[],
  label: string,
): T {
  if ((allowed as readonly string[]).includes(value)) {
    return value as T;
  }
  throw new Error(`Invalid ${label}: ${value}. Expected one of: ${allowed.join(", ")}`);
}

async function resolveWikiApplyBody(params: { body?: string; bodyFile?: string }): Promise<string> {
  if (params.body?.trim()) {
    return params.body;
  }
  if (params.bodyFile?.trim()) {
    return await fs.readFile(params.bodyFile, "utf8");
  }
  throw new Error("wiki apply synthesis requires --body or --body-file.");
}

type MemoryWikiMutationResult = Awaited<ReturnType<typeof applyMemoryWikiMutation>>;

function formatMemoryWikiMutationSummary(result: MemoryWikiMutationResult, json?: boolean): string {
  if (json) {
    return JSON.stringify(result, null, 2);
  }
  return `${result.changed ? "Updated" : "No changes for"} ${result.pagePath} via ${result.operation}. ${result.compile.updatedFiles.length > 0 ? `Refreshed ${result.compile.updatedFiles.length} index file${result.compile.updatedFiles.length === 1 ? "" : "s"}.` : "Indexes unchanged."}`;
}

function formatJsonOrText<T>(
  result: T,
  json: boolean | undefined,
  render: (result: T) => string,
): string {
  return json ? JSON.stringify(result, null, 2) : render(result);
}

function formatGatewayJsonOrText<T>(
  result: T,
  json: boolean | undefined,
  render: (result: T) => string,
): string {
  return json
    ? escapeGatewayJsonForTerminal(JSON.stringify(result, null, 2))
    : sanitizeGatewayStringForTerminal(render(result));
}

async function runWikiCommandWithSummary<T>(params: {
  json?: boolean;
  stdout?: Pick<NodeJS.WriteStream, "write">;
  run: () => Promise<T>;
  render: (result: T) => string;
}): Promise<T> {
  const result = await params.run();
  writeOutput(formatJsonOrText(result, params.json, params.render), params.stdout);
  return result;
}

async function runSyncedWikiCommandWithSummary<T>(params: {
  config: ResolvedMemoryWikiConfig;
  appConfig?: OpenClawConfig;
  json?: boolean;
  stdout?: Pick<NodeJS.WriteStream, "write">;
  run: () => Promise<T>;
  render: (result: T) => string;
}): Promise<T> {
  await syncMemoryWikiImportedSources({ config: params.config, appConfig: params.appConfig });
  return runWikiCommandWithSummary(params);
}

function addWikiSearchConfigOptions<T extends Command>(command: T): T {
  return command
    .option(
      "--backend <backend>",
      `Search backend (${WIKI_SEARCH_BACKENDS.join(", ")})`,
      (value: string) => parseWikiSearchEnumOption(value, WIKI_SEARCH_BACKENDS, "backend"),
    )
    .option(
      "--corpus <corpus>",
      `Search corpus (${WIKI_SEARCH_CORPORA.join(", ")})`,
      (value: string) => parseWikiSearchEnumOption(value, WIKI_SEARCH_CORPORA, "corpus"),
    );
}

function addWikiApplyMutationOptions<T extends Command>(command: T): T {
  return command
    .option("--source-id <id>", "Source id", collectCliValues)
    .option("--contradiction <text>", "Contradiction note", collectCliValues)
    .option("--question <text>", "Open question", collectCliValues)
    .option("--confidence <n>", "Confidence score between 0 and 1", (value: string) =>
      Number(value),
    )
    .option("--status <status>", "Page status");
}

export async function runWikiStatus(params: {
  config: ResolvedMemoryWikiConfig;
  appConfig?: OpenClawConfig;
  json?: boolean;
  stdout?: Pick<NodeJS.WriteStream, "write">;
}) {
  const routeThroughGateway = shouldRouteBridgeRuntimeThroughGateway(params.config);
  const status = routeThroughGateway
    ? await callWikiGateway("wiki.status")
    : await (async () => {
        await syncMemoryWikiImportedSources({ config: params.config, appConfig: params.appConfig });
        return await resolveMemoryWikiStatus(params.config, {
          appConfig: params.appConfig,
        });
      })();
  writeOutput(
    routeThroughGateway
      ? formatGatewayJsonOrText(status, params.json, renderMemoryWikiStatus)
      : formatJsonOrText(status, params.json, renderMemoryWikiStatus),
    params.stdout,
  );
  return status;
}

export async function runWikiDoctor(params: {
  config: ResolvedMemoryWikiConfig;
  appConfig?: OpenClawConfig;
  json?: boolean;
  stdout?: Pick<NodeJS.WriteStream, "write">;
}) {
  const routeThroughGateway = shouldRouteBridgeRuntimeThroughGateway(params.config);
  const report = routeThroughGateway
    ? await callWikiGateway("wiki.doctor")
    : await (async () => {
        await syncMemoryWikiImportedSources({ config: params.config, appConfig: params.appConfig });
        return buildMemoryWikiDoctorReport(
          await resolveMemoryWikiStatus(params.config, {
            appConfig: params.appConfig,
          }),
        );
      })();
  if (!report.healthy) {
    process.exitCode = 1;
  }
  writeOutput(
    routeThroughGateway
      ? formatGatewayJsonOrText(report, params.json, renderMemoryWikiDoctor)
      : formatJsonOrText(report, params.json, renderMemoryWikiDoctor),
    params.stdout,
  );
  return report;
}

export async function runWikiInit(params: {
  config: ResolvedMemoryWikiConfig;
  json?: boolean;
  stdout?: Pick<NodeJS.WriteStream, "write">;
}) {
  return runWikiCommandWithSummary({
    json: params.json,
    stdout: params.stdout,
    run: () => initializeMemoryWikiVault(params.config),
    render: (value) =>
      `Initialized wiki vault at ${value.rootDir} (${value.createdDirectories.length} dirs, ${value.createdFiles.length} files).`,
  });
}

export async function runWikiCompile(params: {
  config: ResolvedMemoryWikiConfig;
  appConfig?: OpenClawConfig;
  json?: boolean;
  stdout?: Pick<NodeJS.WriteStream, "write">;
}) {
  return runSyncedWikiCommandWithSummary({
    config: params.config,
    appConfig: params.appConfig,
    json: params.json,
    stdout: params.stdout,
    run: () => compileMemoryWikiVault(params.config),
    render: (value) =>
      `Compiled wiki vault at ${value.vaultRoot} (${value.pages.length} pages, ${value.updatedFiles.length} indexes updated).`,
  });
}

export async function runWikiLint(params: {
  config: ResolvedMemoryWikiConfig;
  appConfig?: OpenClawConfig;
  json?: boolean;
  stdout?: Pick<NodeJS.WriteStream, "write">;
}) {
  return runSyncedWikiCommandWithSummary({
    config: params.config,
    appConfig: params.appConfig,
    json: params.json,
    stdout: params.stdout,
    run: () => lintMemoryWikiVault(params.config),
    render: (value) =>
      `Linted wiki vault at ${value.vaultRoot} (${value.issueCount} issues, report: ${value.reportPath}).`,
  });
}

export async function runWikiIngest(params: {
  config: ResolvedMemoryWikiConfig;
  inputPath: string;
  title?: string;
  json?: boolean;
  stdout?: Pick<NodeJS.WriteStream, "write">;
}) {
  return runWikiCommandWithSummary({
    json: params.json,
    stdout: params.stdout,
    run: () =>
      ingestMemoryWikiSource({
        config: params.config,
        inputPath: params.inputPath,
        title: params.title,
      }),
    render: (value) =>
      `Ingested ${value.sourcePath} into ${value.pagePath}. Refreshed ${value.indexUpdatedFiles.length} index file${value.indexUpdatedFiles.length === 1 ? "" : "s"}.`,
  });
}

export async function runWikiSearch(params: {
  config: ResolvedMemoryWikiConfig;
  appConfig?: OpenClawConfig;
  query: string;
  maxResults?: number;
  searchBackend?: ResolvedMemoryWikiConfig["search"]["backend"];
  searchCorpus?: ResolvedMemoryWikiConfig["search"]["corpus"];
  mode?: WikiSearchMode;
  json?: boolean;
  stdout?: Pick<NodeJS.WriteStream, "write">;
}) {
  if (params.mode && !(WIKI_SEARCH_MODES as readonly string[]).includes(params.mode)) {
    throw new Error(`wiki search --mode must be one of: ${WIKI_SEARCH_MODES.join(", ")}.`);
  }
  await syncMemoryWikiImportedSources({ config: params.config, appConfig: params.appConfig });
  const results = await searchMemoryWiki({
    config: params.config,
    appConfig: params.appConfig,
    query: params.query,
    maxResults: params.maxResults,
    searchBackend: params.searchBackend,
    searchCorpus: params.searchCorpus,
    mode: params.mode,
  });
  const summary = params.json
    ? JSON.stringify(results, null, 2)
    : results.length === 0
      ? "No wiki or memory results."
      : results
          .map(
            (result, index) =>
              `${index + 1}. ${result.title} (${result.corpus}/${result.kind})\nPath: ${result.path}${typeof result.startLine === "number" && typeof result.endLine === "number" ? `\nLines: ${result.startLine}-${result.endLine}` : ""}${result.provenanceLabel ? `\nProvenance: ${result.provenanceLabel}` : ""}${result.matchedClaimId ? `\nClaim: ${result.matchedClaimId}` : ""}${result.evidenceKinds && result.evidenceKinds.length > 0 ? `\nEvidence: ${result.evidenceKinds.join(", ")}` : ""}\nSnippet: ${result.snippet}`,
          )
          .join("\n\n");
  writeOutput(summary, params.stdout);
  return results;
}

export async function runWikiGet(params: {
  config: ResolvedMemoryWikiConfig;
  appConfig?: OpenClawConfig;
  lookup: string;
  fromLine?: number;
  lineCount?: number;
  searchBackend?: ResolvedMemoryWikiConfig["search"]["backend"];
  searchCorpus?: ResolvedMemoryWikiConfig["search"]["corpus"];
  json?: boolean;
  stdout?: Pick<NodeJS.WriteStream, "write">;
}) {
  await syncMemoryWikiImportedSources({ config: params.config, appConfig: params.appConfig });
  const result = await getMemoryWikiPage({
    config: params.config,
    appConfig: params.appConfig,
    lookup: params.lookup,
    fromLine: params.fromLine,
    lineCount: params.lineCount,
    searchBackend: params.searchBackend,
    searchCorpus: params.searchCorpus,
  });
  const summary = params.json
    ? JSON.stringify(result, null, 2)
    : (result?.content ?? `Wiki page not found: ${params.lookup}`);
  writeOutput(summary, params.stdout);
  return result;
}

export async function runWikiApplySynthesis(params: {
  config: ResolvedMemoryWikiConfig;
  appConfig?: OpenClawConfig;
  title: string;
  body?: string;
  bodyFile?: string;
  sourceIds?: string[];
  contradictions?: string[];
  questions?: string[];
  confidence?: number;
  status?: string;
  json?: boolean;
  stdout?: Pick<NodeJS.WriteStream, "write">;
}) {
  const sourceIds = normalizeCliStringList(params.sourceIds);
  if (!sourceIds) {
    throw new Error("wiki apply synthesis requires at least one --source-id.");
  }
  const body = await resolveWikiApplyBody({ body: params.body, bodyFile: params.bodyFile });
  await syncMemoryWikiImportedSources({ config: params.config, appConfig: params.appConfig });
  const result = await applyMemoryWikiMutation({
    config: params.config,
    mutation: {
      op: "create_synthesis",
      title: params.title,
      body,
      sourceIds,
      ...(normalizeCliStringList(params.contradictions)
        ? { contradictions: normalizeCliStringList(params.contradictions) }
        : {}),
      ...(normalizeCliStringList(params.questions)
        ? { questions: normalizeCliStringList(params.questions) }
        : {}),
      ...(typeof params.confidence === "number" ? { confidence: params.confidence } : {}),
      ...(params.status?.trim() ? { status: params.status.trim() } : {}),
    },
  });
  writeOutput(formatMemoryWikiMutationSummary(result, params.json), params.stdout);
  return result;
}

export async function runWikiApplyMetadata(params: {
  config: ResolvedMemoryWikiConfig;
  appConfig?: OpenClawConfig;
  lookup: string;
  sourceIds?: string[];
  contradictions?: string[];
  questions?: string[];
  confidence?: number;
  clearConfidence?: boolean;
  status?: string;
  json?: boolean;
  stdout?: Pick<NodeJS.WriteStream, "write">;
}) {
  await syncMemoryWikiImportedSources({ config: params.config, appConfig: params.appConfig });
  const result = await applyMemoryWikiMutation({
    config: params.config,
    mutation: {
      op: "update_metadata",
      lookup: params.lookup,
      ...(normalizeCliStringList(params.sourceIds)
        ? { sourceIds: normalizeCliStringList(params.sourceIds) }
        : {}),
      ...(normalizeCliStringList(params.contradictions)
        ? { contradictions: normalizeCliStringList(params.contradictions) }
        : {}),
      ...(normalizeCliStringList(params.questions)
        ? { questions: normalizeCliStringList(params.questions) }
        : {}),
      ...(params.clearConfidence
        ? { confidence: null }
        : typeof params.confidence === "number"
          ? { confidence: params.confidence }
          : {}),
      ...(params.status?.trim() ? { status: params.status.trim() } : {}),
    },
  });
  writeOutput(formatMemoryWikiMutationSummary(result, params.json), params.stdout);
  return result;
}

export async function runWikiBridgeImport(params: {
  config: ResolvedMemoryWikiConfig;
  appConfig?: OpenClawConfig;
  json?: boolean;
  stdout?: Pick<NodeJS.WriteStream, "write">;
}) {
  const render = (value: MemoryWikiImportedSourceSyncResult) =>
    `Bridge import synced ${value.artifactCount} artifacts across ${value.workspaces} workspaces (${value.importedCount} new, ${value.updatedCount} updated, ${value.skippedCount} unchanged, ${value.removedCount} removed). Indexes ${value.indexesRefreshed ? `refreshed (${value.indexUpdatedFiles.length} files)` : `not refreshed (${value.indexRefreshReason})`}.`;
  if (shouldRouteBridgeRuntimeThroughGateway(params.config)) {
    const result = await callWikiGateway("wiki.bridge.import");
    writeOutput(formatGatewayJsonOrText(result, params.json, render), params.stdout);
    return result;
  }
  return runWikiCommandWithSummary({
    json: params.json,
    stdout: params.stdout,
    run: () =>
      syncMemoryWikiImportedSources({
        config: params.config,
        appConfig: params.appConfig,
      }),
    render,
  });
}

export async function runWikiUnsafeLocalImport(params: {
  config: ResolvedMemoryWikiConfig;
  appConfig?: OpenClawConfig;
  json?: boolean;
  stdout?: Pick<NodeJS.WriteStream, "write">;
}) {
  return runWikiCommandWithSummary({
    json: params.json,
    stdout: params.stdout,
    run: () =>
      syncMemoryWikiImportedSources({
        config: params.config,
        appConfig: params.appConfig,
      }),
    render: (value) =>
      `Unsafe-local import synced ${value.artifactCount} artifacts (${value.importedCount} new, ${value.updatedCount} updated, ${value.skippedCount} unchanged, ${value.removedCount} removed). Indexes ${value.indexesRefreshed ? `refreshed (${value.indexUpdatedFiles.length} files)` : `not refreshed (${value.indexRefreshReason})`}.`,
  });
}

export async function runWikiObsidianStatus(params: {
  config: ResolvedMemoryWikiConfig;
  json?: boolean;
  stdout?: Pick<NodeJS.WriteStream, "write">;
}) {
  return runWikiCommandWithSummary({
    json: params.json,
    stdout: params.stdout,
    run: () => probeObsidianCli(),
    render: (value) =>
      value.available
        ? `Obsidian CLI available at ${value.command}`
        : "Obsidian CLI is not available on PATH.",
  });
}

export async function runWikiObsidianSearch(params: {
  config: ResolvedMemoryWikiConfig;
  query: string;
  json?: boolean;
  stdout?: Pick<NodeJS.WriteStream, "write">;
}) {
  return runWikiCommandWithSummary({
    json: params.json,
    stdout: params.stdout,
    run: () => runObsidianSearch({ config: params.config, query: params.query }),
    render: (value) => value.stdout.trim(),
  });
}

export async function runWikiObsidianOpenCli(params: {
  config: ResolvedMemoryWikiConfig;
  vaultPath: string;
  json?: boolean;
  stdout?: Pick<NodeJS.WriteStream, "write">;
}) {
  return runWikiCommandWithSummary({
    json: params.json,
    stdout: params.stdout,
    run: () => runObsidianOpen({ config: params.config, vaultPath: params.vaultPath }),
    render: (value) => value.stdout.trim() || "Opened in Obsidian.",
  });
}

export async function runWikiObsidianCommandCli(params: {
  config: ResolvedMemoryWikiConfig;
  id: string;
  json?: boolean;
  stdout?: Pick<NodeJS.WriteStream, "write">;
}) {
  return runWikiCommandWithSummary({
    json: params.json,
    stdout: params.stdout,
    run: () => runObsidianCommand({ config: params.config, id: params.id }),
    render: (value) => value.stdout.trim() || "Command sent to Obsidian.",
  });
}

export async function runWikiObsidianDailyCli(params: {
  config: ResolvedMemoryWikiConfig;
  json?: boolean;
  stdout?: Pick<NodeJS.WriteStream, "write">;
}) {
  return runWikiCommandWithSummary({
    json: params.json,
    stdout: params.stdout,
    run: () => runObsidianDaily({ config: params.config }),
    render: (value) => value.stdout.trim() || "Opened today's daily note.",
  });
}

function formatChatGptImportSummary(result: ChatGptImportResult): string {
  if (result.dryRun) {
    return `ChatGPT import dry run scanned ${result.conversationCount} conversations (${result.createdCount} new, ${result.updatedCount} updated, ${result.skippedCount} unchanged).`;
  }
  const runSuffix = result.runId ? ` Run id: ${result.runId}.` : "";
  return `ChatGPT import applied ${result.conversationCount} conversations (${result.createdCount} new, ${result.updatedCount} updated, ${result.skippedCount} unchanged). Refreshed ${result.indexUpdatedFiles.length} index file${result.indexUpdatedFiles.length === 1 ? "" : "s"}.${runSuffix}`;
}

function formatChatGptRollbackSummary(result: ChatGptRollbackResult): string {
  if (result.alreadyRolledBack) {
    return `ChatGPT import run ${result.runId} was already rolled back.`;
  }
  return `Rolled back ChatGPT import run ${result.runId} (${result.removedCount} removed, ${result.restoredCount} restored). Refreshed ${result.indexUpdatedFiles.length} index file${result.indexUpdatedFiles.length === 1 ? "" : "s"}.`;
}

export async function runWikiChatGptImport(params: {
  config: ResolvedMemoryWikiConfig;
  exportPath: string;
  dryRun?: boolean;
  json?: boolean;
  stdout?: Pick<NodeJS.WriteStream, "write">;
}) {
  return runWikiCommandWithSummary({
    json: params.json,
    stdout: params.stdout,
    run: () =>
      importChatGptConversations({
        config: params.config,
        exportPath: params.exportPath,
        dryRun: params.dryRun,
      }),
    render: formatChatGptImportSummary,
  });
}

export async function runWikiChatGptRollback(params: {
  config: ResolvedMemoryWikiConfig;
  runId: string;
  json?: boolean;
  stdout?: Pick<NodeJS.WriteStream, "write">;
}) {
  return runWikiCommandWithSummary({
    json: params.json,
    stdout: params.stdout,
    run: () =>
      rollbackChatGptImportRun({
        config: params.config,
        runId: params.runId,
      }),
    render: formatChatGptRollbackSummary,
  });
}

export function registerWikiCli(
  program: Command,
  pluginConfig?: MemoryWikiPluginConfig | ResolvedMemoryWikiConfig,
  appConfig?: OpenClawConfig,
) {
  const config = isResolvedMemoryWikiConfig(pluginConfig)
    ? pluginConfig
    : resolveMemoryWikiConfig(pluginConfig);
  const wiki = program.command("wiki").description("Inspect and initialize the memory wiki vault");

  wiki
    .command("status")
    .description("Show wiki vault status")
    .option("--json", "Print JSON")
    .action(async (opts: WikiStatusCommandOptions) => {
      await runWikiStatus({ config, appConfig, json: opts.json });
    });

  wiki
    .command("doctor")
    .description("Audit wiki vault setup and report actionable fixes")
    .option("--json", "Print JSON")
    .action(async (opts: WikiDoctorCommandOptions) => {
      await runWikiDoctor({ config, appConfig, json: opts.json });
    });

  wiki
    .command("init")
    .description("Initialize the wiki vault layout")
    .option("--json", "Print JSON")
    .action(async (opts: WikiInitCommandOptions) => {
      await runWikiInit({ config, json: opts.json });
    });

  wiki
    .command("compile")
    .description("Refresh generated wiki indexes")
    .option("--json", "Print JSON")
    .action(async (opts: WikiCompileCommandOptions) => {
      await runWikiCompile({ config, appConfig, json: opts.json });
    });

  wiki
    .command("lint")
    .description("Lint the wiki vault and write a report")
    .option("--json", "Print JSON")
    .action(async (opts: WikiLintCommandOptions) => {
      await runWikiLint({ config, appConfig, json: opts.json });
    });

  wiki
    .command("ingest")
    .description("Ingest a local file into the wiki sources folder")
    .argument("<path>", "Local file path to ingest")
    .option("--title <title>", "Override the source title")
    .option("--json", "Print JSON")
    .action(async (inputPath: string, opts: WikiIngestCommandOptions) => {
      await runWikiIngest({ config, inputPath, title: opts.title, json: opts.json });
    });

  addWikiSearchConfigOptions(
    wiki
      .command("search")
      .description("Search wiki pages and, when configured, the active memory corpus")
      .argument("<query>", "Search query")
      .option("--max-results <n>", "Maximum results", (value: string) => Number(value))
      .option("--mode <mode>", `Search mode (${WIKI_SEARCH_MODES.join(", ")})`),
  )
    .option("--json", "Print JSON")
    .action(async (query: string, opts: WikiSearchCommandOptions) => {
      await runWikiSearch({
        config,
        appConfig,
        query,
        maxResults: opts.maxResults,
        searchBackend: opts.backend,
        searchCorpus: opts.corpus,
        mode: opts.mode,
        json: opts.json,
      });
    });

  addWikiSearchConfigOptions(
    wiki
      .command("get")
      .description("Read a wiki page by id or relative path, with optional active-memory fallback")
      .argument("<lookup>", "Relative path or page id")
      .option("--from <n>", "Start line", (value: string) => Number(value))
      .option("--lines <n>", "Number of lines", (value: string) => Number(value)),
  )
    .option("--json", "Print JSON")
    .action(async (lookup: string, opts: WikiGetCommandOptions) => {
      await runWikiGet({
        config,
        appConfig,
        lookup,
        fromLine: opts.from,
        lineCount: opts.lines,
        searchBackend: opts.backend,
        searchCorpus: opts.corpus,
        json: opts.json,
      });
    });

  const apply = wiki.command("apply").description("Apply narrow wiki mutations");
  addWikiApplyMutationOptions(
    apply
      .command("synthesis")
      .description("Create or refresh a synthesis page with managed summary content")
      .argument("<title>", "Synthesis title")
      .option("--body <text>", "Summary body text")
      .option("--body-file <path>", "Read summary body text from a file"),
  )
    .option("--json", "Print JSON")
    .action(async (title: string, opts: WikiApplySynthesisCommandOptions) => {
      await runWikiApplySynthesis({
        config,
        appConfig,
        title,
        body: opts.body,
        bodyFile: opts.bodyFile,
        sourceIds: opts.sourceId,
        contradictions: opts.contradiction,
        questions: opts.question,
        confidence: opts.confidence,
        status: opts.status,
        json: opts.json,
      });
    });
  addWikiApplyMutationOptions(
    apply
      .command("metadata")
      .description("Update metadata on an existing page")
      .argument("<lookup>", "Relative path or page id"),
  )
    .option("--clear-confidence", "Remove any stored confidence value")
    .option("--json", "Print JSON")
    .action(async (lookup: string, opts: WikiApplyMetadataCommandOptions) => {
      await runWikiApplyMetadata({
        config,
        appConfig,
        lookup,
        sourceIds: opts.sourceId,
        contradictions: opts.contradiction,
        questions: opts.question,
        confidence: opts.confidence,
        clearConfidence: opts.clearConfidence,
        status: opts.status,
        json: opts.json,
      });
    });

  const bridge = wiki
    .command("bridge")
    .description("Import public memory artifacts into the wiki vault");
  bridge
    .command("import")
    .description("Sync bridge-backed memory artifacts into wiki source pages")
    .option("--json", "Print JSON")
    .action(async (opts: WikiBridgeImportCommandOptions) => {
      await runWikiBridgeImport({ config, appConfig, json: opts.json });
    });

  const unsafeLocal = wiki
    .command("unsafe-local")
    .description("Import explicitly configured private local paths into wiki source pages");
  unsafeLocal
    .command("import")
    .description("Sync unsafe-local configured paths into wiki source pages")
    .option("--json", "Print JSON")
    .action(async (opts: WikiUnsafeLocalImportCommandOptions) => {
      await runWikiUnsafeLocalImport({ config, appConfig, json: opts.json });
    });

  const chatgpt = wiki
    .command("chatgpt")
    .description("Import ChatGPT export history into wiki source pages");
  chatgpt
    .command("import")
    .description("Import a ChatGPT export into draft wiki source pages")
    .requiredOption("--export <path>", "ChatGPT export directory or conversations.json path")
    .option("--dry-run", "Preview changes without writing", false)
    .option("--json", "Print JSON")
    .action(async (opts: WikiChatGptImportCommandOptions) => {
      await runWikiChatGptImport({
        config,
        exportPath: opts.export!,
        dryRun: opts.dryRun,
        json: opts.json,
      });
    });
  chatgpt
    .command("rollback")
    .description("Roll back a previously applied ChatGPT import run")
    .argument("<run-id>", "Import run id")
    .option("--json", "Print JSON")
    .action(async (runId: string, opts: WikiChatGptRollbackCommandOptions) => {
      await runWikiChatGptRollback({
        config,
        runId,
        json: opts.json,
      });
    });

  const obsidian = wiki.command("obsidian").description("Run official Obsidian CLI helpers");
  obsidian
    .command("status")
    .description("Probe the Obsidian CLI")
    .option("--json", "Print JSON")
    .action(async (opts: WikiStatusCommandOptions) => {
      await runWikiObsidianStatus({ config, json: opts.json });
    });
  obsidian
    .command("search")
    .description("Search the current Obsidian vault")
    .argument("<query>", "Search query")
    .option("--json", "Print JSON")
    .action(async (query: string, opts: WikiObsidianSearchCommandOptions) => {
      await runWikiObsidianSearch({ config, query, json: opts.json });
    });
  obsidian
    .command("open")
    .description("Open a file in Obsidian by vault-relative path")
    .argument("<path>", "Vault-relative path")
    .option("--json", "Print JSON")
    .action(async (vaultPath: string, opts: WikiObsidianOpenCommandOptions) => {
      await runWikiObsidianOpenCli({ config, vaultPath, json: opts.json });
    });
  obsidian
    .command("command")
    .description("Execute an Obsidian command palette command by id")
    .argument("<id>", "Obsidian command id")
    .option("--json", "Print JSON")
    .action(async (id: string, opts: WikiObsidianCommandCommandOptions) => {
      await runWikiObsidianCommandCli({ config, id, json: opts.json });
    });
  obsidian
    .command("daily")
    .description("Open today's daily note in Obsidian")
    .option("--json", "Print JSON")
    .action(async (opts: WikiObsidianDailyCommandOptions) => {
      await runWikiObsidianDailyCli({ config, json: opts.json });
    });
}
