/**
 * The `/decide` command surface. Harness-agnostic: it talks to a small UI port, so the interactive
 * flows are driven by the pi/omp harness in production and by a scripted double in tests.
 */

import { listChatCatalog } from "./chat.ts";
import {
  applySetting,
  BACKEND_IDS,
  clearSetting,
  formatSettingValue,
  maskKey,
  mergeConfig,
  readConfigFile,
  resolveConfig,
  saveConfigFile,
  selectBackend,
  SETTABLE_FIELDS,
  SETTABLE_TARGETS,
  SUGGESTED_KEY_ENV,
  type BackendConfig,
  type BackendId,
  type BackendSelection,
  type DeciderConfig,
} from "./config.ts";
import { describeModelRoute, listCatalog } from "./decisions.ts";
import { buildGroups, normalizeState, runGroup } from "./dispatch.ts";
import { JevError, messageOf } from "./errors.ts";
import {
  formatAnswers,
  formatComposite,
  formatConfigStatus,
  formatGate,
  formatModelList,
  type AnswerReport,
  type GroupMeta,
} from "./format.ts";
import { detectHarness } from "./harness.ts";
import { normalizeQuestions, type ConformResult, type JevQuestion, type JsonValue, type QuestionRoute } from "./questions.ts";
import { compositeScore, evaluateGate, parseWeights, SHAPES, type ShapeKind } from "./shapes.ts";

/** The UI port the command surface needs; pi and omp both satisfy it. */
export interface CommandContext {
  ui: {
    select(title: string, options: string[], options2?: unknown): Promise<string | undefined>;
    input(title: string, placeholder?: string): Promise<string | undefined>;
    editor(title: string, prefill?: string): Promise<string | undefined>;
    confirm(title: string, message: string, options2?: unknown): Promise<boolean>;
    notify(message: string, type?: "info" | "warning" | "error"): void;
  };
  signal?: AbortSignal;
}

const HELP_TEXT = [
  "/decide                     resolved config, per-backend state, live probe (default)",
  "/decide add [backend]       add/configure a backend: key, model, base URL (guided)",
  "/decide set [t] [f] [v]     set one field, e.g. set openrouter model ~typesafe/jev-latest (guided)",
  "/decide unset [t] [f]       drop a field, or a whole backend block (guided)",
  "/decide question [text]     ask your own question; Enter through the prefills = smoke test",
  "                            shapes: single | fanout | gate | composite",
  "/decide models [b] [text]   models reported by a backend catalogue",
  "/decide setup               guided setup incl. the default-backend choice",
  "/decide help                this text",
  "",
  "Aliases: /jev = /decide; setup = init; unset = remove = rm; question = ask.",
  `Backends: ${BACKEND_IDS.join(", ")}. Targets for set/unset: ${SETTABLE_TARGETS.join(", ")}.`,
  `Every value lives in <agentDir>/decider.json (harness ${detectHarness()}); keys may be "$ENV_VAR" references.`,
].join("\n");

export const SMOKE_STATE = "Help! My payouts have been failing for 3 days.";
export const SMOKE_INSTRUCTION = "Does this convey urgency?";

const QUESTION_TYPE_OPTIONS = [
  "noul — P(yes) in [0,1]",
  "choice — pick one option from a set",
  "score — position across ordered levels",
];

export function createCommandHandler(agentDir: string) {
  return async function handleCommand(args: string, ctx: CommandContext): Promise<void> {
    const parts = args.trim() === "" ? [] : args.trim().split(/\s+/);
    const subcommand = (parts[0] ?? "status").toLowerCase();
    try {
      const cfg = resolveConfig({ agentDir });
      if (subcommand === "help") {
        ctx.ui.notify(HELP_TEXT, "info");
        return;
      }
      if (subcommand === "setup" || subcommand === "init") {
        await runSetup(cfg, ctx);
        return;
      }
      if (subcommand === "add") {
        await runAdd(cfg, parts[1], ctx);
        return;
      }
      if (subcommand === "set") {
        await runSet(cfg, parts.slice(1), ctx);
        return;
      }
      if (subcommand === "unset" || subcommand === "remove" || subcommand === "rm") {
        await runUnset(cfg, parts.slice(1), ctx);
        return;
      }
      if (subcommand === "models") {
        const [rest, backendId] = splitBackendArg(parts.slice(1));
        await showModels(cfg, backendId, rest, ctx);
        return;
      }
      if (subcommand === "question" || subcommand === "ask") {
        const [rest, backendId] = splitBackendArg(parts.slice(1));
        await runQuestion(cfg, backendId, rest, ctx);
        return;
      }
      if (subcommand !== "status") {
        ctx.ui.notify(`Unknown subcommand "${subcommand}".\n\n${HELP_TEXT}`, "warning");
        return;
      }
      await showStatus(cfg, ctx);
    } catch (error) {
      ctx.ui.notify(
        `decide: ${messageOf(error)}${error instanceof JevError && error.hint ? `\n${error.hint}` : ""}`,
        "error",
      );
    }
  };
}

/** `models openrouter gpt` -> backend override + filter text. */
function splitBackendArg(parts: string[]): [string, BackendId | undefined] {
  const first = parts[0];
  if (first !== undefined && (BACKEND_IDS as readonly string[]).includes(first)) {
    return [parts.slice(1).join(" "), first as BackendId];
  }
  return [parts.join(" "), undefined];
}

async function showStatus(cfg: DeciderConfig, ctx: CommandContext) {
  let selected: BackendConfig | undefined;
  let probe: string | undefined;
  try {
    selected = selectBackend(cfg);
  } catch (error) {
    probe = `no usable backend: ${messageOf(error)}`;
  }
  if (selected !== undefined) probe = await probeBackend(cfg, selected, ctx);
  ctx.ui.notify(formatConfigStatus(cfg, selected), selected === undefined ? "warning" : "info");
  if (probe !== undefined) ctx.ui.notify(probe, "info");
}

async function probeBackend(cfg: DeciderConfig, backend: BackendConfig, ctx: CommandContext): Promise<string> {
  try {
    if (backend.kind === "chat") {
      const names = await listChatCatalog(backend, cfg.modelsPath, ctx.signal);
      const wanted = backend.model;
      if (wanted === undefined) return `${backend.id}: catalogue lists ${names.length} model(s); no model configured`;
      return `${backend.id} catalogue → ${names.length} model(s); ${wanted} ${names.includes(wanted) ? "is available" : "is NOT in the list"}`;
    }
    const models = await listCatalog(backend, cfg.modelsPath, ctx.signal);
    if (backend.model !== undefined && models.some((model) => model.name === backend.model)) {
      return `${backend.id} catalogue → ${backend.model} available (${models.length} entries)`;
    }
    if (models.length === 0) return `${backend.id} catalogue returned no models`;
    try {
      const route = await describeModelRoute(backend, cfg.modelsPath, backend.model ?? "", ctx.signal);
      const parts = [
        route.name === undefined ? route.id : `${route.id} (${route.name})`,
        ...(route.modality === undefined ? [] : [route.modality]),
        ...(route.outputModalities.length === 0 ? [] : [`out: ${route.outputModalities.join(",")}`]),
      ];
      return `${backend.id} catalogue → ${backend.model} absent from ${models.length} entries but resolves: ${parts.join(" · ")}`;
    } catch (error) {
      return `${backend.id} catalogue → ${backend.model} absent from ${models.length} entries; route lookup failed: ${messageOf(error)}`;
    }
  } catch (error) {
    return `${backend.id} probe failed: ${messageOf(error)}`;
  }
}

async function showModels(cfg: DeciderConfig, backendId: BackendId | undefined, filter: string, ctx: CommandContext) {
  const backend = selectBackend(cfg, backendId);
  const names =
    backend.kind === "chat"
      ? await listChatCatalog(backend, cfg.modelsPath, ctx.signal)
      : (await listCatalog(backend, cfg.modelsPath, ctx.signal)).map((model) =>
          model.releaseDate === undefined ? model.name : `${model.name} (${model.releaseDate})`,
        );
  const needle = filter.trim().toLowerCase();
  const filtered = needle === "" ? names : names.filter((name) => name.toLowerCase().includes(needle));
  ctx.ui.notify(formatModelList(`${backend.id} catalogue @ ${backend.baseUrl}`, filtered), "info");
}

interface RawQuestionInput {
  id: string;
  type: string;
  instructions: string;
  criteria?: unknown;
}

/** `/decide question [instruction]` — ask your own question across the documented shapes. */
async function runQuestion(
  cfg: DeciderConfig,
  backendId: BackendId | undefined,
  presetInstruction: string,
  ctx: CommandContext,
) {
  const dialogs = { signal: ctx.signal };
  const backend = selectBackend(cfg, backendId);
  const model = backend.model;
  if (model === undefined || model.trim() === "") {
    ctx.ui.notify(`Backend "${backend.id}" has no model id configured. Run /decide add ${backend.id}.`, "warning");
    return;
  }

  const shapeLabel = await ctx.ui.select("Question shape", Object.values(SHAPES).map((shape) => shape.label), dialogs);
  const shape = Object.values(SHAPES).find((candidate) => candidate.label === shapeLabel);
  if (shape === undefined) return cancelled(ctx);

  const collected = await collectQuestions(shape.kind, presetInstruction, ctx);
  if (collected === undefined) return cancelled(ctx);

  const stateInput = await ctx.ui.editor("State (text, or a JSON object/array)", SMOKE_STATE);
  if (stateInput === undefined) return cancelled(ctx);
  const state = normalizeState(stateInput);

  let threshold = 0.5;
  if (shape.needsThreshold) {
    const typed = await ctx.ui.input("Threshold (0..1)", "0.8");
    if (typed === undefined) return cancelled(ctx);
    threshold = Number(typed.trim() === "" ? "0.8" : typed.trim());
    if (!Number.isFinite(threshold) || threshold < 0 || threshold > 1) {
      ctx.ui.notify(`Threshold must be a number in [0,1] (got "${typed}").`, "error");
      return;
    }
  }

  let weights: number[] = [];
  if (shape.needsWeights) {
    const typed = await ctx.ui.input(
      `Weights for ${collected.length} question(s), comma separated`,
      collected.map(() => "1").join(","),
    );
    if (typed === undefined) return cancelled(ctx);
    weights = parseWeights(typed, collected.length);
  }

  const { ids, questions } = normalizeQuestions(collected);
  const outcome = await runQuestions(cfg, backend, ids, questions, {}, state, ctx);
  const report: AnswerReport = { ids, questions, ...outcome.conform };
  const blocks = [formatAnswers(report, outcome.metas)];
  if (shape.kind === "gate") blocks.push(formatGate(evaluateGate(ids, outcome.conform.answers, threshold), threshold));
  if (shape.kind === "composite") blocks.push(formatComposite(compositeScore(ids, outcome.conform.answers, weights)));
  ctx.ui.notify(blocks.join("\n"), Object.keys(outcome.conform.issues).length > 0 ? "warning" : "info");
}

interface QuestionBatchOutcome {
  conform: ConformResult;
  metas: GroupMeta[];
}

/** One dispatch for a whole question set: group by backend+model, run the batches in parallel. */
async function runQuestions(
  cfg: DeciderConfig,
  backend: BackendConfig,
  ids: string[],
  questions: Record<string, JevQuestion>,
  routes: Record<string, QuestionRoute>,
  state: JsonValue,
  ctx: CommandContext,
): Promise<QuestionBatchOutcome> {
  const { groups, failures } = buildGroups(cfg, ids, routes, backend.id, undefined);
  const settled = await Promise.all(groups.map((group) => runGroup(group, state, questions, ctx.signal)));
  const conform: ConformResult = { answers: {}, issues: { ...failures }, notes: {} };
  const metas: GroupMeta[] = [];
  for (const outcome of settled) {
    Object.assign(conform.answers, outcome.conform.answers);
    Object.assign(conform.issues, outcome.conform.issues);
    Object.assign(conform.notes, outcome.conform.notes);
    metas.push(outcome.meta);
  }
  return { conform, metas };
}

/** Collect one question per pass, stopping when the shape has enough and the user says run. */
async function collectQuestions(
  kind: ShapeKind,
  preset: string,
  ctx: CommandContext,
): Promise<RawQuestionInput[] | undefined> {
  const dialogs = { signal: ctx.signal };
  const spec = SHAPES[kind];
  const collected: RawQuestionInput[] = [];
  let instruction = preset.trim();

  for (;;) {
    if (collected.length > 0) {
      const enough = collected.length >= spec.minQuestions;
      const answer = await ctx.ui.select(
        enough ? "Add another question?" : `${spec.kind} needs at least ${spec.minQuestions} questions`,
        enough ? ["run now", "add another"] : ["keep going"],
        dialogs,
      );
      if (answer === undefined) return undefined;
      if (answer === "run now") break;
    }
    if (instruction === "") {
      const typed = await ctx.ui.input("Question instructions (Enter = the suggestion)", SMOKE_INSTRUCTION);
      if (typed === undefined) return undefined;
      // A blank submit accepts the suggestion: that is what makes "Enter through" a smoke test.
      instruction = typed.trim() === "" ? SMOKE_INSTRUCTION : typed.trim();
    }
    const typeOption = await ctx.ui.select("Question type", QUESTION_TYPE_OPTIONS, dialogs);
    if (typeOption === undefined) return undefined;
    const type = typeOption.split(/[\s—]/)[0] ?? "noul";
    const criteria = await promptCriteria(type, ctx);
    if (!criteria.ok) return undefined;
    collected.push({
      id: questionId(instruction, collected),
      type,
      instructions: instruction,
      ...(criteria.value === undefined ? {} : { criteria: criteria.value }),
    });
    instruction = "";
    if (kind === "single") break;
  }
  return collected;
}

async function promptCriteria(
  type: string,
  ctx: CommandContext,
): Promise<{ ok: true; value?: unknown } | { ok: false }> {
  const dialogs = { signal: ctx.signal };
  if (type === "noul") {
    const text = await ctx.ui.editor("Noul criteria (optional)\nline 1 = what true means, line 2 = what false means", "");
    if (text === undefined) return { ok: false };
    const [yes, no] = text.split("\n").map((line) => line.trim());
    if ((yes ?? "") === "" && (no ?? "") === "") return { ok: true };
    return { ok: true, value: { ...((yes ?? "") === "" ? {} : { true: yes }), ...((no ?? "") === "" ? {} : { false: no }) } };
  }
  if (type === "choice") {
    const text = await ctx.ui.editor("Choice options: one per line, `option` or `option: rubric` (at least two)", "");
    if (text === undefined) return { ok: false };
    const options: Record<string, string | null> = {};
    for (const line of text.split("\n").map((entry) => entry.trim()).filter((entry) => entry !== "")) {
      const [option, ...rest] = line.split(":");
      const rubric = rest.join(":").trim();
      if ((option ?? "").trim() !== "") options[option.trim()] = rubric === "" ? null : rubric;
    }
    if (Object.keys(options).length < 2) {
      ctx.ui.notify("A choice question needs at least two options.", "error");
      return { ok: false };
    }
    return { ok: true, value: options };
  }
  const text = await ctx.ui.editor("Score levels: one per line, lowest first (at least two)", "");
  if (text === undefined) return { ok: false };
  const levels = text.split("\n").map((entry) => entry.trim()).filter((entry) => entry !== "");
  if (levels.length < 2) {
    ctx.ui.notify("A score question needs at least two levels.", "error");
    return { ok: false };
  }
  return { ok: true, value: levels };
}

/** Slug of the instructions, deduplicated against the ids already collected. */
function questionId(instruction: string, collected: RawQuestionInput[]): string {
  const base = instruction
    .toLowerCase()
    .replace(/[^a-z0-9]+/g, "_")
    .replace(/^_+|_+$/g, "")
    .slice(0, 32) || "question";
  const taken = new Set(collected.map((entry) => entry.id));
  if (!taken.has(base)) return base;
  for (let suffix = 2; ; suffix += 1) {
    const candidate = `${base}_${suffix}`;
    if (!taken.has(candidate)) return candidate;
  }
}

/** `/decide add [backend]` — guided configuration for one backend. */
async function runAdd(cfg: DeciderConfig, idArg: string | undefined, ctx: CommandContext) {
  const id = idArg ?? (await ctx.ui.select("Add which backend?", [...BACKEND_IDS], { signal: ctx.signal }));
  if (id === undefined) return cancelled(ctx);
  if (!(BACKEND_IDS as readonly string[]).includes(id)) {
    ctx.ui.notify(`Unknown backend "${id}". Backends: ${BACKEND_IDS.join(", ")}.`, "error");
    return;
  }
  await configureBackend(cfg, id as BackendId, ctx);
}

/** The guided per-backend flow shared by `/decide add` and `/decide setup`. */
async function configureBackend(
  cfg: DeciderConfig,
  targetId: BackendId,
  ctx: CommandContext,
  extras: { backend?: BackendSelection } = {},
) {
  const dialogs = { signal: ctx.signal };
  const backend = cfg.backends[targetId];
  const values: Record<string, unknown> = {};

  const keyMode = await ctx.ui.select(
    `API key for ${targetId} (current: ${backend.apiKeySource})`,
    ["reference an environment variable (recommended)", "store a literal key in the config file", "keep current", "clear the key"],
    dialogs,
  );
  if (keyMode === undefined) return cancelled(ctx);
  if (keyMode.startsWith("reference")) {
    const typed = await ctx.ui.input(`Environment variable holding the ${targetId} key`, SUGGESTED_KEY_ENV[targetId]);
    if (typed === undefined) return cancelled(ctx);
    const name = typed.trim() === "" ? SUGGESTED_KEY_ENV[targetId] : typed.trim();
    values.apiKey = `$${name}`;
    if (!process.env[name]?.trim()) {
      ctx.ui.notify(`Note: $${name} is not set in this process yet — export it before the next call.`, "warning");
    }
  } else if (keyMode.startsWith("store")) {
    const key = await ctx.ui.input(`API key (stored in ${cfg.configPath}; the dialog echoes in plain text)`);
    if (key === undefined || key.trim() === "") return cancelled(ctx);
    values.apiKey = key.trim();
  } else if (keyMode.startsWith("clear")) {
    values.apiKey = undefined;
  }

  if (!keyMode.startsWith("clear")) {
    const model = await ctx.ui.input(`Model id for ${targetId} (current: ${backend.model ?? "not set"})`, backend.model ?? "");
    if (model === undefined) return cancelled(ctx);
    if (model.trim() !== "") values.model = model.trim();
    const baseUrl = await ctx.ui.input(`Base URL for ${targetId} (current: ${backend.baseUrl})`, backend.baseUrl);
    if (baseUrl === undefined) return cancelled(ctx);
    if (baseUrl.trim() !== "" && baseUrl.trim() !== backend.baseUrl) values.baseUrl = baseUrl.trim();
  }

  const rows = [
    `file       ${cfg.configPath}`,
    ...(extras.backend === undefined ? [] : [`backend    ${extras.backend}`]),
    `target     ${targetId}`,
    ...Object.entries(values).map(
      ([key, value]) => `${key.padEnd(10)} ${formatSettingValue(key, targetId, value)}`,
    ),
  ];
  const confirmed = await ctx.ui.confirm("Write config?", rows.join("\n"), dialogs);
  if (!confirmed) return cancelled(ctx);

  const root = readConfigFile(cfg.configPath);
  saveConfigFile(
    cfg.configPath,
    mergeConfig(root, {
      ...(extras.backend === undefined ? {} : { backend: extras.backend }),
      backendBlock: { id: targetId, values },
    }),
  );
  ctx.ui.notify(`Wrote ${cfg.configPath}\n\n${rows.join("\n")}\n\nRun /decide status to verify.`, "info");
}

/** `/decide set [target] [field] [value]` — interactive when any part is missing. */
async function runSet(cfg: DeciderConfig, parts: string[], ctx: CommandContext) {
  const dialogs = { signal: ctx.signal };
  const [targetArg, fieldArg, ...rest] = parts;
  let target: string | undefined = targetArg;
  let field: string | undefined = fieldArg;
  if (target === undefined) {
    const picked = await ctx.ui.select("Set which target?", [...SETTABLE_TARGETS], dialogs);
    if (picked === undefined) return cancelled(ctx);
    target = picked;
  }
  const resolvedTarget = target === "backend" ? "root" : target;
  // `set backend llm` puts the value where a field name would go, so handle it before field lookup.
  if (target === "backend" && field !== undefined) {
    await writeSetting(cfg, target, field, field, ctx);
    return;
  }
  const table = SETTABLE_FIELDS[resolvedTarget as BackendId | "root"];
  if (table === undefined) {
    ctx.ui.notify(`Unknown target "${target}". Targets: ${SETTABLE_TARGETS.join(", ")}.`, "error");
    return;
  }
  if (field === undefined) {
    const picked = await ctx.ui.select(
      `Set which field of ${resolvedTarget}?`,
      Object.keys(table).map((name) => `${name} — now ${resolvedFieldValue(cfg, resolvedTarget, name)}`),
      dialogs,
    );
    if (picked === undefined) return cancelled(ctx);
    field = picked.split(" — ")[0];
  }
  const spec = field === undefined ? undefined : table[field];
  if (spec === undefined) {
    ctx.ui.notify(`Unknown field "${field}" for ${resolvedTarget}. Writable: ${Object.keys(table).join(", ")}.`, "error");
    return;
  }
  // `set backend llm` names the value in the field slot; there is no separate value argument.
  const shortcut = target === "backend" && field !== undefined;
  let raw = shortcut ? field : rest.join(" ").trim();
  if (raw === "") {
    const typed = await ctx.ui.input(`${resolvedTarget}.${field}`, resolvedFieldValue(cfg, resolvedTarget, field));
    if (typed === undefined) return cancelled(ctx);
    raw = typed.trim();
    if (raw === "") return cancelled(ctx);
  }
  await writeSetting(cfg, target, field as string, raw, ctx);
}

/** Write one validated setting, then report it with credentials masked. */
async function writeSetting(
  cfg: DeciderConfig,
  target: string,
  field: string,
  raw: string,
  ctx: CommandContext,
) {
  const resolvedTarget = target === "backend" ? "root" : target;
  const label = resolvedTarget === "root" ? field : `${resolvedTarget}.${field}`;
  saveConfigFile(cfg.configPath, applySetting(readConfigFile(cfg.configPath), target, field, raw));
  ctx.ui.notify(
    `Wrote ${cfg.configPath}\n${label} = ${formatSettingValue(field, resolvedTarget, raw)}\n\nRun /decide status to verify.`,
    "info",
  );
}

/** `/decide unset [target] [field]` — drop one field, or a whole backend block. */
async function runUnset(cfg: DeciderConfig, parts: string[], ctx: CommandContext) {
  const dialogs = { signal: ctx.signal };
  const [targetArg, fieldArg] = parts;
  let target: string | undefined = targetArg;
  let field: string | undefined = fieldArg;
  if (target === undefined) {
    const picked = await ctx.ui.select("Unset from which target?", [...SETTABLE_TARGETS], dialogs);
    if (picked === undefined) return cancelled(ctx);
    target = picked;
  }
  const resolvedTarget = target === "backend" ? "root" : target;
  const table = SETTABLE_FIELDS[resolvedTarget as BackendId | "root"];
  if (table === undefined) {
    ctx.ui.notify(`Unknown target "${target}". Targets: ${SETTABLE_TARGETS.join(", ")}.`, "error");
    return;
  }
  if (field === undefined && resolvedTarget !== "root") {
    const picked = await ctx.ui.select(`Unset what from ${target}?`, ["(whole block)", ...Object.keys(table)], dialogs);
    if (picked === undefined) return cancelled(ctx);
    field = picked === "(whole block)" ? undefined : picked;
  }
  const what = field === undefined ? `the ${target} block` : `${resolvedTarget}.${field}`;
  const confirmed = await ctx.ui.confirm("Unset?", `Remove ${what} from ${cfg.configPath}`, dialogs);
  if (!confirmed) return cancelled(ctx);
  saveConfigFile(cfg.configPath, clearSetting(readConfigFile(cfg.configPath), target, field));
  ctx.ui.notify(`Wrote ${cfg.configPath}\nRemoved ${what}.\n\nRun /decide status to verify.`, "info");
}

/** Resolved value of one writable field, for dialog labels and prefills. */
function resolvedFieldValue(cfg: DeciderConfig, target: string, field: string): string {
  if (target === "root") {
    if (field === "backend") return `${cfg.backend} (${cfg.backendSource})`;
    if (field === "modelsPath") return cfg.modelsPath;
    return "not set";
  }
  const value = (cfg.backends[target as BackendId] as unknown as Record<string, unknown>)[field];
  if (value === undefined) return "not set";
  if (field === "apiKey") return maskKey(value as string);
  return String(value);
}

/** `/decide setup`: pick the default backend, then run the guided flow for it. */
async function runSetup(cfg: DeciderConfig, ctx: CommandContext) {
  const dialogs = { signal: ctx.signal };
  const selection = await ctx.ui.select(`Default backend (current: ${cfg.backend})`, ["auto", ...BACKEND_IDS], dialogs);
  if (selection === undefined) return cancelled(ctx);
  const targetId = (selection === "auto"
    ? await ctx.ui.select("Configure which backend?", [...BACKEND_IDS], dialogs)
    : selection) as BackendId | undefined;
  if (targetId === undefined) return cancelled(ctx);
  await configureBackend(cfg, targetId, ctx, { backend: selection as BackendSelection });
}

function cancelled(ctx: CommandContext) {
  ctx.ui.notify("Cancelled.", "warning");
}
