/**
 * /voice — dictate the prompt.
 *
 *   /voice              toggle (on uses the last mode, hold by default)
 *   /voice hold|tap     turn on in that mode
 *   /voice off          turn off
 *   /voice key <key>    push-to-talk key: space (default) or a combo like alt+m
 *   /voice auto-send on|off   send on release when the text has 3+ words
 *   /voice status       settings, recorder and server availability
 *
 * Hold mode: hold Space to talk, release to stop; the transcript is typed
 * into the prompt where the cursor was and stays editable (nothing is sent
 * unless auto-send is on). Tap mode: Space on an empty prompt starts,
 * Space again stops (Enter stops and sends). Esc or Ctrl+C cancels and
 * puts the prompt back as it was.
 *
 * Speech goes through the OmniRush backend (POST <gateway>/audio/
 * transcriptions, device token) segment by segment while you talk, via the
 * shared voice core (./voice/core, vendored from the desktop app).
 * Recordings are disposable: audio exists only in memory until its
 * segment's text is back, is never written to disk or logged, and never
 * reaches the session or the session uploader — only the text you send
 * becomes the prompt.
 */

import fs from "node:fs";
import path from "node:path";
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { omniDir, resolveOrigin } from "./auth";
import { sharedRefresher } from "./refresh";
import {
  CircuitBreaker,
  RemoteTranscriber,
  VoiceError,
  VoiceSession,
  buildKeyterms,
  countWords,
  isVoiceError,
  padInsertion,
  type AudioSource,
  type TranscribeRequest,
  type Transcriber,
  type VoiceResult,
  type VoiceSnapshot,
} from "./voice/core";
import { RecorderUnavailableError, openRecorder, realProbe, remoteReason, testCapture, type PcmSource } from "./voice/capture";
import {
  KeyController,
  isCancelKey,
  kittyMatches,
  legacyMatches,
  parseKittyKey,
  parseVoiceKey,
  type VoiceKey,
  type VoiceMode,
} from "./voice/keys";
import { ALL_KEYS_FLAGS, CORE_FLAGS, kittyToLegacy, setFlagsSequence } from "./voice/kitty";
import { loadVoiceSettings, saveVoiceSettings, type VoiceSettings } from "./voice/settings";

const WIDGET = "omnirush-voice";
const STATUS = "omnirush-voice";
const PRIVACY_NOTICE =
  "Voice is transcribed by our cloud speech provider. omnirush.ai doesn't store your audio; the provider keeps a copy for up to 30 days.";

/** The gateway's transcription route: <gateway>/audio/transcriptions. */
export function transcriptionEndpoint(env: NodeJS.ProcessEnv = process.env): string {
  const gateway = (env.OMNIRUSH_GATEWAY_URL || "").trim().replace(/\/+$/, "") || `${resolveOrigin(env)}/v1`;
  return `${gateway}/audio/transcriptions`;
}

function currentToken(): string | null {
  const refresher = sharedRefresher();
  return refresher.auth?.accessToken || (process.env.OMNIRUSH_TOKEN || "").trim() || null;
}

/** RemoteTranscriber with the device bearer and one single-flight refresh on 401. */
export function createGatewayTranscriber(
  endpoint = transcriptionEndpoint(),
  deps: { token?: () => string | null; refresh?: (token: string) => Promise<boolean>; fetch?: typeof fetch } = {},
): Transcriber {
  const token = deps.token ?? currentToken;
  const refresh = deps.refresh ?? ((expired: string) => sharedRefresher().refresh(expired));
  let bearer: string | null = null;
  const remote = new RemoteTranscriber({
    url: endpoint,
    headers: (): Record<string, string> => (bearer ? { Authorization: `Bearer ${bearer}` } : {}),
    ...(deps.fetch ? { fetch: deps.fetch as any } : {}),
  });
  return {
    async transcribe(request: TranscribeRequest, signal: AbortSignal) {
      bearer = token();
      if (!bearer) throw new VoiceError("signed_out", "Not signed in — run `omnirush login` first.");
      try {
        return await remote.transcribe(request, signal);
      } catch (error) {
        if (!(isVoiceError(error) && error.kind === "signed_out" && error.status === 401)) throw error;
        const expired = bearer;
        const refreshed = await refresh(expired).catch(() => false);
        const next = token();
        if (!refreshed || !next || next === expired) throw error;
        bearer = next;
        return remote.transcribe(request, signal);
      }
    },
  };
}

export interface VoiceAvailability {
  available: boolean;
  reason: string | null;
  detail?: string;
}

/** GET <gateway>/audio/transcriptions: is dictation on for this account? Never throws. */
export async function checkAvailability(
  endpoint = transcriptionEndpoint(),
  deps: { token?: () => string | null; fetch?: typeof fetch } = {},
): Promise<VoiceAvailability> {
  const token = (deps.token ?? currentToken)();
  if (!token) return { available: false, reason: "signed_out" };
  const doFetch = deps.fetch ?? globalThis.fetch;
  try {
    const response = await doFetch(endpoint, {
      headers: { Authorization: `Bearer ${token}`, Accept: "application/json" },
      signal: AbortSignal.timeout(5000),
    });
    const text = await response.text().catch(() => "");
    let body: any = null;
    try {
      body = text ? JSON.parse(text) : null;
    } catch {
      body = null;
    }
    if (response.status === 404 || response.status === 405) return { available: false, reason: "not_supported" };
    if (response.status === 401) return { available: false, reason: "signed_out" };
    if (!response.ok) {
      const code = typeof body?.detail === "string" ? body.detail : body?.detail?.code ?? body?.error?.code ?? null;
      if (code === "voice_unavailable") return { available: false, reason: "voice_unavailable" };
      return { available: false, reason: "server_error", detail: `HTTP ${response.status}` };
    }
    if (body && body.available === false) return { available: false, reason: body.reason ?? "voice_unavailable" };
    return { available: true, reason: null };
  } catch (error: any) {
    return { available: false, reason: "unreachable", detail: String(error?.message ?? error) };
  }
}

export function availabilityMessage(result: VoiceAvailability): string {
  switch (result.reason) {
    case "signed_out":
      return "Voice needs you signed in — run `omnirush login` first.";
    case "not_supported":
      return "Voice isn't available on this OmniRush server yet (it has no transcription route).";
    case "voice_unavailable":
      return "Voice is currently unavailable: the OmniRush speech service is turned off for your account. Try again later.";
    case "unreachable":
      return `Voice can't reach the OmniRush server (${result.detail ?? "network error"}).`;
    default:
      return `Voice is currently unavailable (${result.detail ?? result.reason ?? "unknown error"}).`;
  }
}

/** A readable line for a failed dictation. */
export function voiceErrorMessage(error: VoiceError): string {
  switch (error.kind) {
    case "unavailable":
      return "Voice is currently unavailable: the OmniRush speech service is turned off for your account.";
    case "no_signal":
      return "No audio came from the microphone — check the input device (and, on macOS, that this terminal may use the microphone).";
    case "no_speech":
      return "No speech detected.";
    case "signed_out":
      return "Voice needs you signed in — run `omnirush login` again.";
    case "breaker_open":
      return "Voice paused after repeated errors. Try again in a moment.";
    default:
      return error.message;
  }
}

// ---------------------------------------------------------------------------
// Keyterm context: repo, branch, files the session touched

function gitInfo(cwd: string): { repo: string | null; branch: string | null } {
  let dir = path.resolve(cwd);
  for (let i = 0; i < 40; i++) {
    const dotGit = path.join(dir, ".git");
    try {
      let gitDir = dotGit;
      const stat = fs.statSync(dotGit);
      if (stat.isFile()) {
        const pointer = /gitdir:\s*(.+)/.exec(fs.readFileSync(dotGit, "utf8"))?.[1]?.trim();
        if (pointer) gitDir = path.resolve(dir, pointer);
      }
      const head = fs.readFileSync(path.join(gitDir, "HEAD"), "utf8").trim();
      const branch = /^ref: refs\/heads\/(.+)$/.exec(head)?.[1] ?? null;
      return { repo: path.basename(dir), branch };
    } catch {
      /* not here */
    }
    const parent = path.dirname(dir);
    if (parent === dir) break;
    dir = parent;
  }
  return { repo: path.basename(path.resolve(cwd)) || null, branch: null };
}

function topLevelNames(cwd: string): string[] {
  try {
    return fs
      .readdirSync(cwd)
      .filter((name) => !name.startsWith(".") && name !== "node_modules")
      .slice(0, 20);
  } catch {
    return [];
  }
}

// ---------------------------------------------------------------------------

interface EditorLike {
  getText(): string;
  getCursor(): { line: number; col: number };
  getLines?(): string[];
  insertTextAtCursor(text: string): void;
  handleInput(data: string): void;
  isShowingAutocomplete?(): boolean;
}

function isEditorLike(component: any): component is EditorLike {
  return (
    !!component &&
    typeof component.getText === "function" &&
    typeof component.getCursor === "function" &&
    typeof component.insertTextAtCursor === "function" &&
    typeof component.handleInput === "function"
  );
}

function textAroundCursor(editor: EditorLike): { before: string; after: string } {
  const lines = editor.getLines?.() ?? editor.getText().split("\n");
  const { line, col } = editor.getCursor();
  const current = lines[line] ?? "";
  return { before: current.slice(0, col), after: current.slice(col) };
}

/** Delete `text` right before the cursor, only if it is exactly what is there. */
function eraseBeforeCursor(editor: EditorLike, text: string): boolean {
  if (!text) return true;
  const { before } = textAroundCursor(editor);
  if (!before.endsWith(text)) return false;
  for (let i = 0; i < [...text].length; i++) editor.handleInput("\x7f");
  return true;
}

function formatElapsed(ms: number): string {
  const s = Math.floor(ms / 1000);
  return `${Math.floor(s / 60)}:${String(s % 60).padStart(2, "0")}`;
}

const BARS = ["▁", "▂", "▃", "▄", "▅", "▆", "▇", "█"];
function bars(levels: number[]): string {
  return levels.map((l) => BARS[Math.min(7, Math.floor(Math.max(0, l) * 8))]).join("");
}

type UiState =
  | { kind: "idle" }
  | { kind: "warmup" }
  | { kind: "starting" }
  | { kind: "recording"; snapshot: VoiceSnapshot | null }
  | { kind: "finalizing"; snapshot: VoiceSnapshot | null };

export default function (pi: ExtensionAPI) {
  const dir = omniDir();
  let settings: VoiceSettings = loadVoiceSettings(dir);
  let ctxRef: any = null;
  let tui: any = null;
  let theme: any = null;
  let unsubscribe: (() => void) | null = null;
  let controller: KeyController | null = null;
  let voiceKey: VoiceKey | null = null;
  let kittyAllKeys = false;
  let ui: UiState = { kind: "idle" };
  let renderTimer: ReturnType<typeof setInterval> | null = null;
  const breaker = new CircuitBreaker();
  let breakerNoticeAt = 0;
  const touchedFiles: string[] = [];

  // The current dictation.
  let session: VoiceSession | null = null;
  let source: PcmSource | null = null;
  let editor: EditorLike | null = null;
  let anchor: { before: string; after: string } | null = null;
  let inserted = "";
  let lead: string | null = null;
  let diverged = false;
  let sendOnStop = false;
  let stopRequested = false;
  /** After an automatic stop while the key is still held: swallow its repeats. */
  let drainKey = false;
  let drainTimer: ReturnType<typeof setTimeout> | null = null;

  const notify = (text: string, level: "info" | "warning" | "error" = "info") => ctxRef?.ui?.notify?.(text, level);
  const requestRender = () => tui?.requestRender?.();

  const setUi = (next: UiState) => {
    ui = next;
    const active = next.kind === "recording" || next.kind === "finalizing" || next.kind === "starting";
    if (active && !renderTimer) renderTimer = setInterval(requestRender, 250);
    if (!active && renderTimer) {
      clearInterval(renderTimer);
      renderTimer = null;
    }
    requestRender();
  };

  const color = (name: string, text: string) => (theme?.fg ? theme.fg(name, text) : text);
  const keyLabel = () => (voiceKey?.id === "space" ? "Space" : voiceKey?.id ?? settings.key);

  const widget = {
    render(width: number): string[] {
      let line = "";
      switch (ui.kind) {
        case "idle":
          return [];
        case "warmup":
          line = `${color("warning", "●")} keep holding ${keyLabel()}…`;
          break;
        case "starting":
          line = `${color("warning", "●")} starting microphone…`;
          break;
        case "recording": {
          const snap = ui.snapshot;
          const pending = snap?.pending ? color("dim", `  · transcribing ${snap.pending}…`) : "";
          const hint = settings.mode === "hold" ? `release ${keyLabel()} to insert` : `${keyLabel()} to stop · Enter to send`;
          line = `${color("error", "● REC")} ${formatElapsed(snap?.elapsedMs ?? 0)} ${color("accent", bars(snap?.levels ?? []))}${pending}  ${color("dim", `${hint} · Esc cancels`)}`;
          break;
        }
        case "finalizing": {
          const n = ui.snapshot?.pending ?? 0;
          line = `${color("accent", "●")} processing…${n ? ` (${n} segment${n === 1 ? "" : "s"})` : ""}  ${color("dim", "Esc cancels")}`;
          break;
        }
      }
      const visible = line.replace(/\x1b\[[0-9;]*m/g, "");
      if (visible.length > width) {
        // Drop the hint first; never exceed the width.
        return [line.replace(/\x1b\[[0-9;]*m/g, "").slice(0, Math.max(0, width))];
      }
      return [line];
    },
    invalidate() {},
  };

  // ---- editor text ----------------------------------------------------------

  /** Keep the editor showing the committed text of the dictation so far. */
  const applyLive = (text: string) => {
    if (!editor || !anchor || diverged || !text) return;
    if (lead === null) lead = padInsertion(anchor.before, text, "").startsWith(" ") ? " " : "";
    const target = lead + text;
    if (target === inserted) return;
    const { before } = textAroundCursor(editor);
    if (!before.endsWith(inserted) || before.slice(0, before.length - inserted.length) !== anchor.before) {
      diverged = true;
      return;
    }
    if (target.startsWith(inserted)) {
      editor.insertTextAtCursor(target.slice(inserted.length));
    } else {
      if (!eraseBeforeCursor(editor, inserted)) {
        diverged = true;
        return;
      }
      editor.insertTextAtCursor(target);
    }
    inserted = target;
    requestRender();
  };

  const resetDictation = () => {
    session = null;
    source = null;
    editor = null;
    anchor = null;
    inserted = "";
    lead = null;
    diverged = false;
    sendOnStop = false;
    stopRequested = false;
  };

  const finish = (result: VoiceResult) => {
    const text = result.text;
    if (editor && anchor && text) {
      applyLive(text);
      if (!diverged) {
        const trail = padInsertion("", text, anchor.after).endsWith(" ") && anchor.after ? " " : "";
        if (trail) {
          editor.insertTextAtCursor(trail);
          inserted += trail;
        }
      } else {
        notify(`Voice text not inserted — the prompt changed while it was processing:\n  ${text}`, "warning");
      }
    }
    if (result.error) {
      const kind = result.error.kind;
      const level = text ? "warning" : kind === "no_speech" ? "info" : "error";
      notify(text ? `Part of the dictation was lost: ${voiceErrorMessage(result.error)}` : voiceErrorMessage(result.error), level);
    }
    const words = countWords(text);
    const ed = editor;
    const shouldSend = !diverged && !!text && ((sendOnStop && words >= 1) || (settings.autoSubmit && words >= 3));
    resetDictation();
    setUi({ kind: "idle" });
    if (shouldSend && ed) ed.handleInput("\r");
  };

  // ---- recording ------------------------------------------------------------

  const lazySource = (): AudioSource => ({
    async start(onFrames, onEnd) {
      try {
        const opened = await openRecorder(onFrames, (error) => onEnd?.(error));
        source = opened.source;
      } catch (error: any) {
        if (error instanceof RecorderUnavailableError) throw new VoiceError("no_device", error.message);
        throw isVoiceError(error) ? error : new VoiceError("capture_failed", String(error?.message ?? error));
      }
    },
    stop() {
      source?.stop();
    },
  });

  const startRecording = (strip: number) => {
    const focused = tui?.getFocusedComponent?.();
    if (!isEditorLike(focused)) {
      controller?.reset();
      return;
    }
    editor = focused;
    // The warm-up space that reached the editor is not part of the prompt.
    for (let i = 0; i < strip; i++) if (!eraseBeforeCursor(editor, " ")) eraseBeforeCursor(editor, "\u3000");
    anchor = textAroundCursor(editor);
    inserted = "";
    lead = null;
    diverged = false;
    const cwd = ctxRef?.cwd ?? process.cwd();
    const git = gitInfo(cwd);
    const keyterms = buildKeyterms({ repo: git.repo, branch: git.branch, files: [...touchedFiles.slice(-30), ...topLevelNames(cwd)] });
    let started = false;
    const current = new VoiceSession({
      source: lazySource(),
      transcriber: createGatewayTranscriber(),
      language: settings.language && settings.language !== "auto" ? settings.language : null,
      keyterms,
      maxRecordingMs: 120_000,
      silenceAutoStopMs: settings.mode === "tap" ? 15_000 : null,
      breaker,
      onChange: (snapshot) => {
        if (session !== current) return;
        if (snapshot.phase === "recording") {
          started = true;
          setUi({ kind: "recording", snapshot });
        } else if (snapshot.phase === "finalizing") setUi({ kind: "finalizing", snapshot });
        if (snapshot.phase === "recording" || snapshot.phase === "finalizing") applyLive(snapshot.text);
        // The session ended by itself (cap, silence, input ran dry, capture
        // failed): finish it like a release. Errors before the recording
        // started are reported by start() below.
        const ended = snapshot.phase === "finalizing" || snapshot.phase === "done" || (snapshot.phase === "error" && started);
        if (ended && controller?.recording) {
          if (settings.mode === "hold") startDrain();
          controller.reset();
        }
        // Deferred: stop() must not be re-entered from inside its own onChange.
        if (ended && !stopRequested) setTimeout(() => void stopRecording(false, current), 0);
      },
    });
    session = current;
    setUi({ kind: "starting" });
    current.start().catch((error) => {
      if (session !== current) return;
      controller?.reset();
      if (settings.mode === "hold") startDrain();
      resetDictation();
      setUi({ kind: "idle" });
      notify(isVoiceError(error) ? voiceErrorMessage(error) : `Voice: ${error?.message ?? error}`, "error");
    });
  };

  const stopRecording = async (send: boolean, which: VoiceSession | null = session) => {
    const current = which;
    if (!current || current !== session) return;
    stopRequested = true;
    sendOnStop = sendOnStop || send;
    setUi({ kind: "finalizing", snapshot: current.snapshot() });
    const result = await current.stop();
    if (session !== current) return; // cancelled meanwhile
    finish(result);
  };

  const cancelRecording = () => {
    const current = session;
    if (!current) return;
    current.cancel();
    if (editor && inserted && !diverged) eraseBeforeCursor(editor, inserted);
    resetDictation();
    controller?.reset();
    setUi({ kind: "idle" });
  };

  const startDrain = () => {
    drainKey = true;
    if (drainTimer) clearTimeout(drainTimer);
    drainTimer = setTimeout(() => (drainKey = false), 400);
  };

  // ---- key input ------------------------------------------------------------

  const canStart = (): boolean => {
    if (session || !tui) return false;
    if (tui.hasOverlay?.()) return false;
    const focused = tui.getFocusedComponent?.();
    if (!isEditorLike(focused)) return false;
    if (focused.isShowingAutocomplete?.()) return false;
    if (settings.mode === "tap" && voiceKey?.text && focused.getText().trim() !== "") return false;
    if (breaker.isOpen()) {
      if (Date.now() - breakerNoticeAt > 10_000) {
        breakerNoticeAt = Date.now();
        notify("Voice paused after repeated errors. Try again in a moment.", "warning");
      }
      return false;
    }
    return true;
  };

  const kittyAvailable = () => !!tui?.terminal?.kittyProtocolActive;
  const writeTerminal = (data: string) => {
    if (tui?.terminal?.write) tui.terminal.write(data);
    else process.stdout.write(data);
  };

  /** Hold-to-talk on Space: ask a Kitty terminal for release events of text keys (flag 8). */
  const syncKittyFlags = () => {
    const want = !!unsubscribe && settings.mode === "hold" && !!voiceKey?.text && kittyAvailable();
    if (want === kittyAllKeys) return;
    if (!want && !kittyAvailable()) {
      kittyAllKeys = false;
      return;
    }
    writeTerminal(setFlagsSequence(want ? ALL_KEYS_FLAGS : CORE_FLAGS));
    kittyAllKeys = want;
  };

  const onInput = (data: string): { consume?: boolean; data?: string } | undefined => {
    if (!controller || !voiceKey) return undefined;
    // A busy terminal can deliver several auto-repeated spaces in one read:
    // they are separate key events.
    if (voiceKey.text && data.length > 1 && /^[ \u3000]+$/.test(data)) {
      let passed = "";
      for (const ch of data) {
        const result = onInput(ch);
        if (!result) passed += ch;
        else if (!result.consume) passed += result.data ?? ch;
      }
      return passed === data ? undefined : passed ? { data: passed } : { consume: true };
    }
    if (!kittyAllKeys && settings.mode === "hold" && voiceKey.text && kittyAvailable()) syncKittyFlags();
    const legacy = kittyAllKeys ? kittyToLegacy(data) : undefined;
    const kittyEvent = parseKittyKey(data);
    const ours = kittyEvent ? kittyMatches(voiceKey, kittyEvent) : legacyMatches(voiceKey, data);

    if (drainKey) {
      if (ours) {
        if (kittyEvent?.event === "release") drainKey = false;
        else startDrain();
        return { consume: true };
      }
      drainKey = false;
    }

    // Between release and the last segment's text: Esc/Ctrl+C cancels.
    if (session && !controller.recording) {
      if (isCancelKey(data)) {
        cancelRecording();
        return { consume: true };
      }
      // Late repeats of a still-held key must not type spaces into the result.
      if (ours && settings.mode === "hold") return { consume: true };
    }

    const action = controller.handle(data);
    if (action.type === "consume") return { consume: true };
    if (legacy !== undefined) return legacy === "" ? { consume: true } : { data: legacy };
    return undefined;
  };

  const buildController = () => {
    voiceKey = parseVoiceKey(settings.key);
    controller = new KeyController({
      mode: settings.mode,
      key: voiceKey,
      kitty: () => kittyAllKeys || (!!voiceKey && !voiceKey.text && kittyAvailable()),
      canStart,
      callbacks: {
        onWarmup: () => setUi({ kind: "warmup" }),
        onWarmupAbort: (heldBack) => {
          const focused = tui?.getFocusedComponent?.();
          if (isEditorLike(focused)) focused.insertTextAtCursor(" ".repeat(heldBack));
          setUi({ kind: "idle" });
        },
        onStart: (strip) => startRecording(strip),
        onStop: (send) => void stopRecording(send),
        onCancel: () => cancelRecording(),
        typeKey: () => {
          const focused = tui?.getFocusedComponent?.();
          if (isEditorLike(focused)) focused.handleInput(" ");
        },
      },
    });
  };

  // ---- arming ---------------------------------------------------------------

  const arm = (ctx: any): string | null => {
    if (ctx?.mode && ctx.mode !== "tui") return "Voice works in the interactive terminal only.";
    try {
      buildController();
    } catch (error: any) {
      return `Voice key "${settings.key}": ${error?.message ?? error}`;
    }
    ctx.ui.setWidget(
      WIDGET,
      (t: any, th: any) => {
        tui = t;
        theme = th;
        return widget;
      },
      { placement: "belowEditor" },
    );
    if (!tui) {
      ctx.ui.setWidget(WIDGET, undefined);
      return "Voice works in the interactive terminal only.";
    }
    unsubscribe?.();
    unsubscribe = ctx.ui.onTerminalInput(onInput);
    // The Kitty negotiation may still be running at startup: check again shortly.
    syncKittyFlags();
    setTimeout(syncKittyFlags, 600).unref?.();
    const hint = settings.mode === "hold" ? `hold ${keyLabel()}` : `tap ${keyLabel()}`;
    ctx.ui.setStatus?.(STATUS, color("dim", `voice: ${hint}`));
    return null;
  };

  const disarm = (ctx: any) => {
    cancelRecording();
    unsubscribe?.();
    unsubscribe = null;
    syncKittyFlags();
    controller = null;
    ctx?.ui?.setWidget?.(WIDGET, undefined);
    ctx?.ui?.setStatus?.(STATUS, undefined);
    setUi({ kind: "idle" });
  };

  const save = () => {
    try {
      saveVoiceSettings(dir, settings);
    } catch (error: any) {
      notify(`Voice settings not saved (${error?.message ?? error})`, "warning");
    }
  };

  const enable = async (ctx: any, mode: VoiceMode) => {
    if (ctx?.mode && ctx.mode !== "tui") {
      notify("Voice works in the interactive terminal only.", "warning");
      return;
    }
    if (!currentToken()) {
      notify("Voice needs you signed in — run `omnirush login` first.", "error");
      return;
    }
    const probe = realProbe();
    const remote = remoteReason(probe);
    if (remote) {
      notify(`Voice can't use a microphone here: ${remote}.`, "error");
      return;
    }
    const availability = await checkAvailability();
    if (!availability.available) {
      notify(availabilityMessage(availability), "error");
      return;
    }
    let recorder = "";
    try {
      const check = await testCapture(300);
      recorder = check.recorder;
      if (check.recorder !== "file" && check.maxRms < 0.001) {
        notify("No audio detected from the microphone in a short test — check the input device (and the terminal's microphone permission).", "warning");
      }
    } catch (error: any) {
      notify(error?.message ?? String(error), "error");
      return;
    }
    const previous = { ...settings };
    settings = { ...settings, enabled: true, mode };
    const failure = arm(ctx);
    if (failure) {
      settings = previous;
      notify(failure, "error");
      return;
    }
    const firstTime = !settings.noticeShown;
    settings.noticeShown = true;
    save();
    const how = mode === "hold" ? `Hold ${keyLabel()} to talk, release to insert` : `Tap ${keyLabel()} on an empty prompt to talk, tap again to stop`;
    const lines = [`Voice on (${mode}). ${how} · Esc cancels · auto-send ${settings.autoSubmit ? "on" : "off"} · mic: ${recorder}`];
    if (firstTime) lines.push(PRIVACY_NOTICE);
    notify(lines.join("\n"));
  };

  const statusLines = async (): Promise<string[]> => {
    const availability = await checkAvailability();
    const remote = remoteReason(realProbe());
    return [
      "Voice:",
      `  state      ${settings.enabled ? `on (${settings.mode})` : "off"}`,
      `  key        ${settings.key}${kittyAllKeys ? " (Kitty key-release events)" : ""}`,
      `  auto-send  ${settings.autoSubmit ? "on (3+ words)" : "off"}`,
      `  language   ${settings.language}`,
      `  server     ${availability.available ? "available" : availabilityMessage(availability)}`,
      `  microphone ${remote ?? "local session"}`,
    ];
  };

  pi.registerCommand("voice", {
    description: "Dictate prompts: /voice [hold|tap|off|status|key <key>|auto-send on|off]",
    getArgumentCompletions: (prefix: string) => {
      const items = ["hold", "tap", "off", "status", "key", "auto-send"].map((value) => ({ value, label: value }));
      const typed = prefix.trim();
      // An exact match offers nothing, so Enter runs the command at once.
      const filtered = items.filter((item) => item.value.startsWith(typed) && item.value !== typed);
      return filtered.length ? filtered : null;
    },
    handler: async (args: string, ctx: any) => {
      ctxRef = ctx;
      const [sub = "", ...rest] = String(args ?? "").trim().split(/\s+/).filter(Boolean);
      const arg = rest.join(" ");
      switch (sub.toLowerCase()) {
        case "":
          if (settings.enabled && unsubscribe) {
            settings = { ...settings, enabled: false };
            disarm(ctx);
            save();
            notify("Voice off.");
          } else {
            await enable(ctx, settings.mode);
          }
          return;
        case "hold":
        case "tap":
          if (settings.enabled && unsubscribe) disarm(ctx);
          await enable(ctx, sub.toLowerCase() as VoiceMode);
          return;
        case "off":
          settings = { ...settings, enabled: false };
          disarm(ctx);
          save();
          notify("Voice off.");
          return;
        case "status":
          notify((await statusLines()).join("\n"));
          return;
        case "key": {
          if (!arg) {
            notify(`Voice key: ${settings.key}. Change with /voice key <space|alt+m|ctrl+space|…>`);
            return;
          }
          try {
            const parsed = parseVoiceKey(arg);
            settings = { ...settings, key: parsed.id };
          } catch (error: any) {
            notify(`Voice key not changed: ${error?.message ?? error}`, "error");
            return;
          }
          save();
          if (settings.enabled && unsubscribe) {
            disarm(ctx);
            arm(ctx);
          }
          notify(`Voice key: ${settings.key}${parseVoiceKey(settings.key).text ? "" : " (starts instantly)"}.`);
          return;
        }
        case "auto-send":
        case "autosend": {
          const value = arg.toLowerCase();
          if (value !== "on" && value !== "off") {
            notify(`Auto-send is ${settings.autoSubmit ? "on" : "off"}. Use /voice auto-send on|off.`);
            return;
          }
          settings = { ...settings, autoSubmit: value === "on" };
          save();
          notify(`Voice auto-send ${value} (sends on release when the text has at least 3 words).`);
          return;
        }
        default:
          notify("Usage: /voice [hold|tap|off|status|key <key>|auto-send on|off]", "warning");
      }
    },
  });

  pi.on("session_start", (_event: any, ctx: any) => {
    ctxRef = ctx;
    settings = loadVoiceSettings(dir);
    if (!settings.enabled || ctx?.mode !== "tui") return;
    if (remoteReason(realProbe())) return; // stays saved; a local session picks it up
    const failure = arm(ctx);
    if (failure) notify(failure, "warning");
  });

  pi.on("session_shutdown", (_event: any, ctx: any) => {
    disarm(ctx ?? ctxRef);
  });

  // Files the agent works on feed the keyterms (names only, never contents).
  pi.on("tool_call", (event: any) => {
    const input = event?.input ?? {};
    for (const key of ["path", "file_path", "filePath"]) {
      const value = input[key];
      if (typeof value === "string" && value) {
        touchedFiles.push(path.basename(value));
        if (touchedFiles.length > 60) touchedFiles.splice(0, touchedFiles.length - 60);
      }
    }
  });
}
