/**
 * Slash-command state machine — pure functions, no React.
 *
 * A small model for the composer's `/` surface: derive `SlashState` from
 * the raw editor value, narrow the verb list by a filter, and produce
 * the next editor value when a verb is picked.
 *
 * Triggering rule: a slash verb only counts when it is the first token
 * of the buffer (`"/clear"` matches, `"hello /clear"` does not). The
 * caret is irrelevant — the machine is buffer-shape driven, matching
 * the Warp `slash_command_model.rs` design the cmdop port came from.
 *
 * Layering: this file imports types only — keep it React-free so it
 * stays trivially unit-testable.
 */
import type { SlashCommand, SlashState } from './types';

/** A `/verb` only counts when it is the very first token of the input. */
const SLASH_RE = /^\/([a-zA-Z][\w-]*)?(?:[ \n]([\s\S]*))?$/;

/**
 * Derive the slash state from the raw editor value.
 *
 * - Buffer empty or not starting with `/` → `none`.
 * - `/`, `/c`, `/cle` (no space yet) → `composing` with `filter`.
 * - `/clear`, `/clear stuff` (verb followed by space) → `command` if
 *   `verb` matches a known id, else `none`.
 */
export function parseSlashState(
  value: string,
  commands: readonly SlashCommand[],
): SlashState {
  if (!value.startsWith('/')) return { kind: 'none' };
  const m = SLASH_RE.exec(value);
  if (!m) return { kind: 'none' };
  const verb = (m[1] ?? '').toLowerCase();
  const rest = m[2];
  // The buffer has advanced past the verb (a space/newline follows).
  if (rest !== undefined) {
    const command = commands.find((c) => c.id === verb);
    if (!command) return { kind: 'none' };
    return { kind: 'command', command, argument: rest };
  }
  // Still typing the verb — `/` alone or `/cle`.
  return { kind: 'composing', filter: verb };
}

/**
 * Narrow a verb list by the `composing` filter.
 *
 * Empty filter returns every command in input order. Otherwise the
 * filter is matched (case-insensitive) against `id`, `token`, `label`
 * and any `keywords`: prefix matches rank above substring matches.
 */
export function filterCommands(
  commands: readonly SlashCommand[],
  filter: string,
): SlashCommand[] {
  const q = filter.trim().toLowerCase();
  if (q.length === 0) return [...commands];
  const scored: { cmd: SlashCommand; score: number; idx: number }[] = [];
  commands.forEach((cmd, idx) => {
    const terms = [cmd.id, cmd.token, cmd.label, ...(cmd.keywords ?? [])];
    let best = -1;
    for (const term of terms) {
      const t = term.toLowerCase();
      if (t.startsWith(q) || t.startsWith(`/${q}`)) best = Math.max(best, 2);
      else if (t.includes(q)) best = Math.max(best, 1);
    }
    if (best >= 0) scored.push({ cmd, score: best, idx });
  });
  scored.sort((a, b) => (b.score - a.score) || (a.idx - b.idx));
  return scored.map((s) => s.cmd);
}

/**
 * What should happen when the user picks a verb in the menu.
 *
 * - `execute` — the command explicitly opted in via `autoExecute: true`.
 *   The host should call `command.onExecute?.()` and clear the editor
 *   buffer. Use for action commands that produce no chat message.
 * - `insert`  — default. The host should replace the leading `/partial`
 *   with `text` (which is `<token> `) so the user can keep typing and
 *   submit normally.
 */
export type SlashCommandAction =
  | { kind: 'execute'; command: SlashCommand }
  | { kind: 'insert'; command: SlashCommand; text: string };

/**
 * Decide whether a picked verb runs immediately or just inserts a token.
 *
 * Pure — does not touch the editor value. The React layer in
 * `useSlashCommands` consumes this to branch between `onExecute`
 * + clear-buffer and `applyCommand` + keep-buffer.
 *
 * Default is `insert`. Only `autoExecute === true` triggers `execute`
 * — `argHint` is display-only and does NOT influence this decision.
 */
export function resolveCommandAction(
  value: string,
  command: SlashCommand,
): SlashCommandAction {
  const shouldAutoExecute = command.autoExecute === true;
  if (shouldAutoExecute) return { kind: 'execute', command };
  return { kind: 'insert', command, text: applyCommand(value, command) };
}

/**
 * Find the `/verb` slice at the start of the buffer (if any). Returns
 * the matched token + its end offset so an overlay-mirror highlighter
 * can wrap the exact same characters the parser considers a slash verb.
 *
 * - `/`, `/cle` (still composing)         → returns the partial slice.
 * - `/clear`, `/clear stuff` (resolved)   → returns the bare `/verb`.
 * - `hello /clear` (slash not at start)   → returns `null`.
 */
export function extractSlashToken(
  value: string,
): { token: string; end: number } | null {
  if (!value.startsWith('/')) return null;
  const m = SLASH_RE.exec(value);
  if (!m) return null;
  const verb = m[1] ?? '';
  const token = `/${verb}`;
  return { token, end: token.length };
}

/**
 * Whether the current buffer is a slash command that may be submitted.
 *
 * Returns `true` when the buffer is NOT a resolved slash command — the
 * host decides on its own emptiness gates for plain messages. Returns
 * `true` for resolved commands whose `argHint` is unset (the command is
 * self-contained) and whose `autoExecute` is not `true`. Returns
 * `false` when:
 *
 *  - the resolved command has `autoExecute: true` (action commands are
 *    picked from the menu, not submitted as a text message),
 *  - or the resolved command declares an `argHint` and the trimmed
 *    argument is empty.
 *
 * Pure — no React, no DOM. The composer uses this to gate the Send
 * button + Enter keypress so half-typed `/note` etc. cannot be
 * submitted as a message.
 */
export function isSubmittableSlash(
  value: string,
  commands: readonly SlashCommand[],
): boolean {
  const state = parseSlashState(value, commands);
  if (state.kind !== 'command') return true;
  if (state.command.autoExecute === true) return false;
  if (!state.command.argHint) return true;
  return state.argument.trim().length > 0;
}

/**
 * Apply a picked command to the editor value — replaces the leading
 * `/partial` with `"<token> "` so the user can immediately type the
 * argument. Any existing argument text is preserved.
 */
export function applyCommand(value: string, command: SlashCommand): string {
  const m = SLASH_RE.exec(value);
  const rest = m?.[2];
  const tail = rest !== undefined && rest.length > 0 ? rest : '';
  return `${command.token} ${tail}`;
}
