<script setup lang="ts">
/**
 * EditorSuggestionMenu
 *
 * Self-contained suggestion listbox for slash commands (`/`) and mention
 * completions (`[[`, `((`, `#`).  Driven entirely by props — no internal
 * state machine, no external UI library dependency for navigation.
 *
 * Keyboard navigation is exposed via `forwardKey(key)` so that the
 * capture-phase handler in `useKeyboard.ts` can route ArrowDown/ArrowUp/
 * Enter/Escape without dispatching synthetic DOM events.
 *
 * Items are grouped by `groups` (matching Nuxt UI's `CommandPaletteGroup`
 * shape).  Each item can have an icon, label, suffix, kbd hints, and an
 * `onSelect` callback.  The actively highlighted item is tracked by
 * `activeIndex` (flat across all groups) and rendered via
 * `data-highlighted` attribute — styled with CSS tokens matching the
 * existing editor theme.
 */
import type { CommandPaletteGroup, CommandPaletteItem } from "@nuxt/ui"
import { computed, ref, watch } from "vue"

const props = defineProps<{
  /** Grouped items following Nuxt UI's CommandPaletteGroup shape. */
  groups: CommandPaletteGroup[]
  /** Screen-space position (top-left of the trigger character). */
  position: { x: number; y: number }
  /** Current search/filter text (shown in the empty-state message). */
  query: string
  /** Whether the menu is visible.  Resets activeIndex to 0 on transition. */
  active: boolean
}>()

const emit = defineEmits<{
  /**
   * Fired when an item is selected via click or Enter, or when
   * Escape is pressed.  The parent should clear the pattern session.
   */
  close: []
}>()

const menuRef = ref<HTMLElement | null>(null)

/** Filter groups by query (case-insensitive match against item label). */
const filteredGroups = computed(() => {
  const q = props.query.toLowerCase()
  if (!q) return props.groups
  return props.groups
    .map((g) => ({
      ...g,
      items: g.items?.filter((item) => item.label?.toLowerCase().includes(q)),
    }))
    .filter((g) => g.items && g.items.length > 0)
})

/** Index of the currently highlighted item (flat across all filtered groups). */
const activeIndex = ref(0)
const total = computed(() =>
  filteredGroups.value.reduce((sum, g) => sum + (g.items?.length ?? 0), 0),
)

watch(
  () => props.active,
  (v) => {
    if (v) activeIndex.value = 0
  },
)

watch(
  () => props.query,
  () => {
    activeIndex.value = 0
  },
)

// Scroll the highlighted item into view
watch(activeIndex, () => {
  if (!menuRef.value) return
  const highlighted = menuRef.value.querySelector("[data-highlighted]")
  if (highlighted && "scrollIntoView" in highlighted) {
    ;(highlighted as HTMLElement).scrollIntoView({ block: "center" })
  }
})

/** Convert (group index, item-within-group index) to a flat global index. */
function getGlobalIndex(groupIdx: number, itemIdx: number): number {
  let offset = 0
  for (let g = 0; g < groupIdx; g++) {
    offset += filteredGroups.value[g]?.items?.length ?? 0
  }
  return offset + itemIdx
}

/** Select an item by group + item index.  Calls onSelect and emits close. */
function selectItem(groupIdx: number, itemIdx: number) {
  const group = filteredGroups.value[groupIdx]
  const item = group?.items?.[itemIdx]
  if (item && !item.disabled) {
    item.onSelect?.(new Event("select"))
    emit("close")
  }
}

/**
 * Drive keyboard navigation externally.
 *
 * Called by the capture-phase handler in `useKeyboard.ts` when a pattern
 * session is active.  Updates `activeIndex` for ArrowUp/Down, selects
 * the highlighted item on Enter, and closes on Escape.
 */
function forwardKey(key: string) {
  if (key === "Escape") {
    emit("close")
    return
  }
  if (total.value === 0) return
  if (key === "ArrowDown") {
    activeIndex.value = (activeIndex.value + 1) % total.value
  } else if (key === "ArrowUp") {
    activeIndex.value = (activeIndex.value - 1 + total.value) % total.value
  } else if (key === "Enter") {
    const item = findItemByGlobalIndex(activeIndex.value)
    if (item && !item.disabled) {
      item.onSelect?.(new Event("select"))
    }
    emit("close")
  }
}

/** Resolve a flat global index (on filtered items) back to the original item. */
function findItemByGlobalIndex(index: number): CommandPaletteItem | null {
  let offset = 0
  for (const g of filteredGroups.value) {
    if (!g.items) continue
    if (index < offset + g.items.length) {
      return g.items[index - offset] ?? null
    }
    offset += g.items.length
  }
  return null
}

defineExpose({ forwardKey })
</script>

<template>
  <div
    v-if="active"
    ref="menuRef"
    role="listbox"
    aria-label="Suggestion menu"
    data-command-palette
    class="flex flex-col fixed z-50 w-80 shadow-lg rounded-lg ring ring-default bg-(--ui-bg) max-h-64 overflow-y-auto"
    :style="{ top: `${props.position.y + 24}px`, left: `${props.position.x}px` }"
  >
        <template v-for="(group, gIdx) in filteredGroups" :key="group.id">
          <div
            v-if="group.label && group.items?.length"
            role="presentation"
            class="px-3 py-1.5 text-xs font-semibold text-(--ui-text-muted) uppercase tracking-wider select-none"
          >
            {{ group.label }}
          </div>

          <div
            v-for="(item, iIdx) in group.items"
            :key="`${group.id}-${iIdx}`"
            :role="'option'"
            :aria-selected="getGlobalIndex(gIdx, iIdx) === activeIndex ? 'true' : 'false'"
            :data-highlighted="getGlobalIndex(gIdx, iIdx) === activeIndex ? '' : undefined"
            :data-menu-item-index="getGlobalIndex(gIdx, iIdx)"
            :class="[
              'flex items-center gap-2.5 px-3 py-2 cursor-pointer text-sm transition-colors',
              item.disabled ? 'opacity-50 cursor-not-allowed' : 'hover:bg-(--ui-bg-elevated)/60',
              getGlobalIndex(gIdx, iIdx) === activeIndex ? 'bg-(--ui-bg-elevated)' : '',
            ]"
            @click="selectItem(gIdx, iIdx)"
            @mouseenter="activeIndex = getGlobalIndex(gIdx, iIdx)"
          >
            <UIcon
              v-if="item.icon"
              :name="item.icon"
              class="w-5 h-5 shrink-0 text-(--ui-text-muted)"
            />

            <span class="flex-1 truncate text-default">{{ item.label }}</span>

            <span
              v-if="item.suffix"
              class="text-xs text-(--ui-text-muted) font-mono ml-auto"
            >{{ item.suffix }}</span>

            <span
              v-else-if="item.kbds?.length"
              class="flex items-center gap-1 ml-auto"
            >
              <kbd
                v-for="(k, kIdx) in item.kbds"
                :key="kIdx"
                class="px-1 py-0.5 text-[10px] font-mono rounded bg-(--ui-bg-elevated)/50 text-(--ui-text-muted) border border-(--ui-border)"
              >{{ k }}</kbd>
            </span>
          </div>
        </template>

        <div
          v-if="!total"
          class="px-3 py-4 text-sm text-(--ui-text-muted) text-center select-none"
        >
          {{ query ? `No results for "${query}"` : "No items" }}
        </div>
    </div>
</template>
