// ── Slash command autocomplete for the prompt input ─────────────────────
// Layer 1: type "/" → dropdown lists matching commands.
// Layer 2: pick a command → if its args[0] has `choices`, the dropdown
//          immediately re-opens showing those choices. Picking a value
//          inserts it and (if args[1] also has choices) chains to the
//          next arg. When all arg-choices are consumed (or an arg has
//          no choices), the dropdown closes and the user submits with
//          Enter (gated by app.js so Enter-while-open never submits).
//
// The arg metadata is the `args[i].choices` field already exposed by
// `chat.commands.list` — no server-side change needed for static enums.
// Dynamic completions (skill names, session ids) are a Phase 2 follow-up
// that will extend `getArgChoices` to call a per-arg RPC.
//
// Choices in the registry come in two shapes; both are normalised to
// `{ value, label }` here so the renderer doesn't care:
//   - "today"                          → { value: "today", label: "today" }
//   - { value: "today", label: "Today" } → { value: "today", label: "Today" }

(function () {
  const promptInput = document.getElementById("prompt-input");
  const promptRowMain = document.querySelector(".prompt-row-main");
  if (!promptInput || !promptRowMain) {
    return;
  }

  // Build the dropdown element once; show/hide via .open class.
  const dropdown = document.createElement("div");
  dropdown.className = "slash-autocomplete";
  dropdown.id = "slash-autocomplete";
  promptRowMain.parentElement.insertBefore(dropdown, promptRowMain);

  let commands = [];
  let filtered = [];
  let selectedIndex = 0;
  let isOpen = false;
  // When non-null we're filling an arg for this command at activeArgIndex;
  // when null we're picking a command. Set/cleared by `refilter()` based
  // on the parsed input, never by hand — keeps the state machine from
  // drifting from what the user actually typed.
  let activeCommand = null;
  let activeArgIndex = -1;

  // Fetch the command registry. The WebSocket handshake completes async, so
  // we must wait for `gateway:connected` (dispatched by app.js after the
  // gateway's connect event) before calling rpc — otherwise the rpc helper
  // rejects with "gateway not connected" and we silently end up with an
  // empty command list.
  let loaded = false;
  async function loadCommands() {
    if (loaded) {
      return;
    }
    if (!window.gateway?.rpc) {
      return;
    }
    if (!window.gateway.ws || window.gateway.ws.readyState !== WebSocket.OPEN) {
      return;
    }
    try {
      const result = await window.gateway.rpc("chat.commands.list", {});
      commands = Array.isArray(result?.commands) ? result.commands : [];
      loaded = true;
      console.log("[slash-autocomplete] loaded", commands.length, "commands");
    } catch (err) {
      console.warn("[slash-autocomplete] failed to load commands:", err);
    }
  }
  // Try once now in case we attached after the event already fired.
  void loadCommands();
  // And on every connect event (covers initial connect + reconnects).
  window.addEventListener("gateway:connected", () => {
    void loadCommands();
  });

  function escapeHtml(str) {
    return String(str)
      .replace(/&/g, "&amp;")
      .replace(/</g, "&lt;")
      .replace(/>/g, "&gt;")
      .replace(/"/g, "&quot;");
  }

  // 30s cache per RPC method name. Skill/session lists change rarely; this
  // keeps the dropdown snappy after the first hit and avoids hammering the
  // gateway on every keystroke.
  const rpcChoiceCache = new Map(); // method → { items, expiresAt }
  const RPC_CHOICE_TTL_MS = 30_000;
  // De-dupe in-flight requests per method so a fast typer doesn't issue
  // 5 parallel chat.skills.list calls.
  const rpcChoiceInflight = new Map(); // method → Promise

  function normalizeChoice(choice) {
    if (typeof choice === "string") {
      return { value: choice, label: choice };
    }
    if (choice && typeof choice === "object") {
      const value = String(choice.value ?? "");
      const label = String(choice.label ?? choice.value ?? "");
      return { value, label };
    }
    return null;
  }

  async function fetchRpcChoices(method) {
    if (!method) {
      return [];
    }
    const now = Date.now();
    const cached = rpcChoiceCache.get(method);
    if (cached && cached.expiresAt > now) {
      return cached.items;
    }
    const inflight = rpcChoiceInflight.get(method);
    if (inflight) {
      return inflight;
    }
    if (
      !window.gateway?.rpc ||
      !window.gateway.ws ||
      window.gateway.ws.readyState !== WebSocket.OPEN
    ) {
      return [];
    }
    const promise = (async () => {
      try {
        const result = await window.gateway.rpc(method, {});
        const raw = Array.isArray(result?.items) ? result.items : [];
        const items = raw.map(normalizeChoice).filter(Boolean);
        rpcChoiceCache.set(method, { items, expiresAt: Date.now() + RPC_CHOICE_TTL_MS });
        return items;
      } catch (err) {
        console.warn("[slash-autocomplete] rpc choices failed:", method, err);
        return [];
      } finally {
        rpcChoiceInflight.delete(method);
      }
    })();
    rpcChoiceInflight.set(method, promise);
    return promise;
  }

  // Resolve the autocomplete choices for a command's argIndex. Static
  // `arg.choices` always wins; if absent and `arg.argsRpc` is set, falls
  // back to the RPC (cached). Returns a Promise<{value,label}[]>.
  // captureRemaining args explicitly disable completion (free-form text).
  async function getArgChoices(cmd, argIndex) {
    const arg = cmd?.args?.[argIndex];
    if (!arg) {
      return [];
    }
    if (arg.captureRemaining) {
      return [];
    }
    if (Array.isArray(arg.choices) && arg.choices.length > 0) {
      return arg.choices.map(normalizeChoice).filter(Boolean);
    }
    if (typeof arg.argsRpc === "string" && arg.argsRpc) {
      return fetchRpcChoices(arg.argsRpc);
    }
    return [];
  }

  function findCommandByName(name) {
    const lower = String(name).toLowerCase();
    return commands.find((cmd) => (cmd.nativeName ?? cmd.key).toLowerCase() === lower);
  }

  // Parse the current input into either {mode:"command", commandQuery} or
  // {mode:"args", command, argIndex, argPartial}. Returns null if the input
  // doesn't start with "/" — caller closes the dropdown.
  function parseInput() {
    const value = promptInput.value;
    if (!value.startsWith("/")) {
      return null;
    }
    const afterSlash = value.slice(1);
    const endsWithSpace = afterSlash.length > 0 && /\s$/.test(value);
    const tokens = afterSlash.split(/\s+/).filter(Boolean);

    if (tokens.length === 0) {
      return { mode: "command", commandQuery: "" };
    }
    if (tokens.length === 1 && !endsWithSpace) {
      return { mode: "command", commandQuery: tokens[0] };
    }

    const cmd = findCommandByName(tokens[0]);
    if (!cmd) {
      // Typed a "/foo " prefix that doesn't match any registered command.
      // Nothing useful to complete — fall back to command-mode filter so
      // the dropdown shows close matches if any.
      return { mode: "command", commandQuery: tokens[0] };
    }

    const completedArgs = tokens.length - 1;
    const argIndex = endsWithSpace ? completedArgs : completedArgs - 1;
    const argPartial = endsWithSpace ? "" : (tokens[tokens.length - 1] ?? "");
    return { mode: "args", command: cmd, argIndex, argPartial };
  }

  // Monotonic token so a slow async refilter can't overwrite a faster one
  // that ran later. Common when an RPC fetch fires while the user is still
  // typing — we need the latest input's result, not the first one back.
  let refilterToken = 0;

  async function refilter() {
    const myToken = ++refilterToken;
    const parsed = parseInput();
    if (!parsed) {
      isOpen = false;
      activeCommand = null;
      activeArgIndex = -1;
      render();
      return;
    }

    if (parsed.mode === "command") {
      activeCommand = null;
      activeArgIndex = -1;
      const query = parsed.commandQuery.toLowerCase();
      filtered = commands.filter((cmd) => {
        const name = (cmd.nativeName ?? cmd.key).toLowerCase();
        if (name.startsWith(query)) {
          return true;
        }
        if (Array.isArray(cmd.textAliases)) {
          return cmd.textAliases.some((a) => a.toLowerCase().slice(1).startsWith(query));
        }
        return false;
      });
      selectedIndex = 0;
      isOpen = filtered.length > 0;
      render();
      return;
    }

    // args mode
    activeCommand = parsed.command;
    activeArgIndex = parsed.argIndex;
    const choicesPromise = getArgChoices(parsed.command, parsed.argIndex);
    // If choices come back synchronously (static), avoid a render flicker.
    let choices;
    if (typeof choicesPromise?.then === "function") {
      choices = await choicesPromise;
    } else {
      choices = choicesPromise;
    }
    if (myToken !== refilterToken) {
      // A later refilter superseded us; drop this result.
      return;
    }
    const partial = parsed.argPartial.toLowerCase();
    filtered = choices.filter((choice) => {
      if (choice.value.toLowerCase().startsWith(partial)) {
        return true;
      }
      return choice.label.toLowerCase().startsWith(partial);
    });
    selectedIndex = 0;
    isOpen = filtered.length > 0;
    render();
  }

  function render() {
    if (!isOpen || filtered.length === 0) {
      dropdown.classList.remove("open");
      dropdown.innerHTML = "";
      return;
    }

    const isArgsMode = activeCommand !== null && activeArgIndex >= 0;
    let html = "";

    if (isArgsMode) {
      const cmdName = activeCommand.nativeName ?? activeCommand.key;
      const arg = activeCommand.args?.[activeArgIndex];
      const argName = arg?.name ?? "value";
      html += `<div class="slash-ac-header">/${escapeHtml(cmdName)} · choose ${escapeHtml(argName)}</div>`;
    }

    html += filtered
      .slice(0, 8)
      .map((entry, i) => {
        const sel = i === selectedIndex ? " selected" : "";
        if (isArgsMode) {
          const value = entry.value;
          const label = entry.label;
          const showLabel = label && label !== value;
          return `<div class="slash-ac-item slash-ac-arg${sel}" data-index="${i}">
            <span class="slash-ac-name">${escapeHtml(value)}</span>
            ${showLabel ? `<span class="slash-ac-desc">${escapeHtml(label)}</span>` : ""}
          </div>`;
        }
        const cmd = entry;
        const name = cmd.nativeName ?? cmd.key;
        const aliasText =
          Array.isArray(cmd.textAliases) && cmd.textAliases.length > 0
            ? `<span class="slash-ac-alias">${escapeHtml(cmd.textAliases[0])}</span>`
            : "";
        return `<div class="slash-ac-item${sel}" data-index="${i}">
          <span class="slash-ac-name">/${escapeHtml(name)}</span>
          ${aliasText}
          <span class="slash-ac-desc">${escapeHtml(cmd.description ?? "")}</span>
        </div>`;
      })
      .join("");

    dropdown.innerHTML = html;
    dropdown.classList.add("open");
  }

  function applySelection() {
    const entry = filtered[selectedIndex];
    if (!entry) {
      return;
    }
    if (activeCommand !== null && activeArgIndex >= 0) {
      applyArgSelection(entry);
    } else {
      applyCommandSelection(entry);
    }
  }

  function applyCommandSelection(cmd) {
    const name = cmd.nativeName ?? cmd.key;
    // Preserve any existing args after the first space (e.g. user pressed
    // / late in a partially-typed command).
    const rest = promptInput.value.slice(1).split(/\s/).slice(1).join(" ");
    promptInput.value =
      cmd.acceptsArgs && !rest ? `/${name} ` : `/${name}${rest ? " " + rest : ""}`;
    promptInput.focus();
    promptInput.selectionStart = promptInput.selectionEnd = promptInput.value.length;
    // refilter() reparses: if cmd.args[0] has choices the dropdown chains
    // into args mode; otherwise it closes.
    void refilter();
  }

  function applyArgSelection(choice) {
    const value = promptInput.value;
    const endsWithSpace = /\s$/.test(value);
    if (endsWithSpace) {
      promptInput.value = value + choice.value + " ";
    } else {
      // Replace the partial at the end with the chosen value.
      const lastSpaceIdx = value.lastIndexOf(" ");
      promptInput.value = value.slice(0, lastSpaceIdx + 1) + choice.value + " ";
    }
    promptInput.focus();
    promptInput.selectionStart = promptInput.selectionEnd = promptInput.value.length;
    // refilter() reparses: if the next arg has choices, chain to it;
    // otherwise close.
    void refilter();
  }

  promptInput.addEventListener("input", () => {
    void refilter();
  });

  promptInput.addEventListener("keydown", (e) => {
    if (!isOpen) {
      return;
    }
    if (e.key === "ArrowDown") {
      e.preventDefault();
      selectedIndex = Math.min(selectedIndex + 1, Math.min(filtered.length, 8) - 1);
      render();
    } else if (e.key === "ArrowUp") {
      e.preventDefault();
      selectedIndex = Math.max(selectedIndex - 1, 0);
      render();
    } else if (e.key === "Enter" || e.key === "Tab") {
      e.preventDefault();
      applySelection();
    } else if (e.key === "Escape") {
      isOpen = false;
      activeCommand = null;
      activeArgIndex = -1;
      render();
    }
  });

  dropdown.addEventListener("mousedown", (e) => {
    const target = e.target.closest(".slash-ac-item");
    if (target) {
      e.preventDefault();
      selectedIndex = parseInt(target.getAttribute("data-index"), 10) || 0;
      applySelection();
    }
  });

  document.addEventListener("click", (e) => {
    if (!dropdown.contains(e.target) && e.target !== promptInput) {
      isOpen = false;
      activeCommand = null;
      activeArgIndex = -1;
      render();
    }
  });
})();
