'use client';

/**
 * `useSlashCommands` — React glue over the pure slash machine.
 *
 * Given the live editor value, exposes:
 *   - the current `SlashState`,
 *   - the filtered verb list while composing,
 *   - a `highlight` index + keyboard handler for the menu,
 *   - `pick(cmd)` which either executes the verb (`autoExecute: true`)
 *     or inserts `"<token> "` so the user can type arguments (default),
 *   - `clear()` which strips the leading slash to close the menu.
 *
 * The hook owns no editor state — the host passes `value` in and
 * applies the strings `pick` / `onKeyDown` hand back to its editor's
 * setter. This keeps it usable from the chat composer (driven by
 * `useChatComposer`) and from any plain `useState` driven input.
 */
import { useCallback, useMemo, useState, type KeyboardEvent } from 'react';

import {
  filterCommands,
  isSubmittableSlash,
  parseSlashState,
  resolveCommandAction,
} from './state';
import type { SlashCommand, SlashState } from './types';

export interface UseSlashCommandsOptions {
  /** Live editor value (the full draft). */
  value: string;
  /** Verb set surfaced to the user. */
  commands: readonly SlashCommand[];
  /**
   * Apply a new editor value. Called by `pick` (after an `insert`
   * action), `clear` (Escape), and the auto-execute branch (clears
   * the buffer after running the command).
   */
  onApply: (next: string) => void;
}

export interface UseSlashCommandsReturn {
  /** Current machine state — `none | composing | command`. */
  state: SlashState;
  /** True while the dropdown should be visible (including empty-match state). */
  isOpen: boolean;
  /** Filtered verbs (empty list is possible when `isOpen` is true). */
  matches: SlashCommand[];
  /** Current filter string (the partial verb after the leading slash). */
  query: string;
  /** Index of the highlighted row in `matches`. */
  highlight: number;
  /** Set the highlight index (pointer-move sync). */
  setHighlight: (i: number) => void;
  /** The resolved verb once `state.kind === "command"`, else null. */
  active: SlashCommand | null;
  /**
   * Whether the current buffer may be submitted as a chat message.
   *
   * Mirrors `isSubmittableSlash(value, commands)`. The composer ANDs
   * this with `composer.canSubmit` to disable Send and turn Enter into
   * a no-op while a slash command is missing its required argument or
   * is an `autoExecute` action (which must be picked from the menu,
   * not dispatched as a text message).
   *
   * When `commands` is empty (slash effectively disabled) this is
   * always `true` — the gate is a no-op.
   */
  canSubmit: boolean;
  /**
   * Apply a verb. Commands with `autoExecute: true` invoke
   * `command.onExecute?.('')` and clear the buffer. All other commands
   * (the default) replace the leading `/partial` with `"<token> "` so
   * the caret lands on the argument, and the user submits normally.
   */
  pick: (command: SlashCommand) => void;
  /** Close the menu by dropping the leading slash. */
  clear: () => void;
  /**
   * Keydown handler for the host editor. Consumes ↑/↓/Enter/Tab/Esc
   * while the menu is open (calling `e.preventDefault()`); otherwise
   * the event passes through untouched.
   */
  onKeyDown: (e: KeyboardEvent<HTMLElement>) => void;
}

export function useSlashCommands({
  value,
  commands,
  onApply,
}: UseSlashCommandsOptions): UseSlashCommandsReturn {
  const [highlight, setHighlightState] = useState(0);

  const state = useMemo(
    () => parseSlashState(value, commands),
    [value, commands],
  );

  const matches = useMemo(
    () =>
      state.kind === 'composing'
        ? filterCommands(commands, state.filter)
        : [],
    [state, commands],
  );

  // Menu stays open for the whole `composing` state — even when the
  // filter narrows to nothing, so we can render a "No commands match"
  // line instead of silently closing on the user.
  const isOpen = state.kind === 'composing';
  const query = state.kind === 'composing' ? state.filter : '';
  const active = state.kind === 'command' ? state.command : null;
  // Empty `commands` means slash is effectively off — never block submit
  // (the composer instantiates the hook unconditionally, see Composer.tsx).
  const canSubmit = useMemo(
    () => (commands.length === 0 ? true : isSubmittableSlash(value, commands)),
    [value, commands],
  );

  // Clamp the highlight whenever the match list shrinks.
  const safeHighlight =
    matches.length > 0 ? Math.min(highlight, matches.length - 1) : 0;

  const setHighlight = useCallback((i: number) => {
    setHighlightState(i);
  }, []);

  const pick = useCallback(
    (command: SlashCommand) => {
      const action = resolveCommandAction(value, command);
      if (action.kind === 'execute') {
        // Fire-and-forget; the host owns error handling. Clear the
        // buffer so the dropdown closes and the next draft starts fresh.
        try {
          void command.onExecute?.('');
        } finally {
          onApply('');
        }
      } else {
        onApply(action.text);
      }
      setHighlightState(0);
    },
    [value, onApply],
  );

  const clear = useCallback(() => {
    onApply(value.replace(/^\//, ''));
    setHighlightState(0);
  }, [value, onApply]);

  const onKeyDown = useCallback(
    (e: KeyboardEvent<HTMLElement>) => {
      if (!isOpen) return;
      switch (e.key) {
        case 'ArrowDown':
          if (matches.length === 0) return;
          e.preventDefault();
          setHighlightState((h) => (h + 1) % matches.length);
          break;
        case 'ArrowUp':
          if (matches.length === 0) return;
          e.preventDefault();
          setHighlightState((h) => (h - 1 + matches.length) % matches.length);
          break;
        case 'Enter':
        case 'Tab': {
          // When there are no matches, swallow Enter so the composer
          // doesn't submit a `/xyzzy` literal — but leave Tab to the
          // browser so focus management still works.
          if (matches.length === 0) {
            if (e.key === 'Enter') e.preventDefault();
            return;
          }
          e.preventDefault();
          const sel = matches[safeHighlight];
          if (sel) pick(sel);
          break;
        }
        case 'Escape':
          e.preventDefault();
          clear();
          break;
        default:
          break;
      }
    },
    [isOpen, matches, safeHighlight, pick, clear],
  );

  return {
    state,
    isOpen,
    matches,
    query,
    highlight: safeHighlight,
    setHighlight,
    active,
    canSubmit,
    pick,
    clear,
    onKeyDown,
  };
}
