<template>
  <div class="fc-tag-autocomplete">
    <v-text-field
      ref="inputRef"
      v-model="query"
      placeholder="Add tag (or kind:name — character/fandom/series)"
      density="compact" hide-details
      @keydown.down.prevent="moveHighlight(1)"
      @keydown.up.prevent="moveHighlight(-1)"
      @keydown.enter.prevent="onEnter"
      @keydown.tab="onTab"
      @keydown.esc="$emit('cancel')"
    />
    <v-list
      v-if="rows.length"
      ref="listRef"
      density="compact" class="fc-tag-autocomplete__list"
    >
      <template v-for="(row, idx) in rows" :key="row.key">
        <!-- Existing tag match (server autocomplete). -->
        <v-list-item
          v-if="row.type === 'hit'"
          :active="idx === highlight" @click="onPickRow(row)"
        >
          <template #prepend>
            <v-icon size="small" :color="store.colorFor(row.hit.kind)">
              {{ iconFor(row.hit.kind) }}
            </v-icon>
          </template>
          <v-list-item-title>
            {{ row.hit.name }}
            <span v-if="row.hit.fandom_name" class="text-caption">— {{ row.hit.fandom_name }}</span>
          </v-list-item-title>
          <template #append>
            <span class="text-caption">{{ row.hit.kind }}</span>
          </template>
        </v-list-item>

        <!-- ML suggestion for THIS image that matches the typed query. Picking
             it routes through the same accept path as the Suggestions panel, so
             it's recorded + drops out of the panel (operator-asked 2026-06-07). -->
        <v-list-item
          v-else-if="row.type === 'suggestion'"
          :active="idx === highlight" @click="onPickRow(row)"
          class="fc-tag-autocomplete__sugg"
        >
          <template #prepend>
            <v-icon size="small" :color="store.colorFor(row.sugg.category)">
              {{ iconFor(row.sugg.category) }}
            </v-icon>
          </template>
          <v-list-item-title>
            {{ row.sugg.display_name }}
            <span v-if="row.sugg.creates_new_tag" class="text-caption">— new</span>
          </v-list-item-title>
          <template #append>
            <span class="fc-tag-autocomplete__sugg-tag">
              <v-icon size="x-small">mdi-auto-fix</v-icon>
              {{ scorePct(row.sugg) }}
            </span>
          </template>
        </v-list-item>

        <!-- Create-new row. -->
        <v-list-item
          v-else
          :active="idx === highlight" @click="onPickRow(row)"
        >
          <template #prepend>
            <v-icon size="small" :color="store.colorFor(parsedKind)">
              {{ iconFor(parsedKind) }}
            </v-icon>
          </template>
          <v-list-item-title>
            Create "{{ parsedName }}" as {{ parsedKind }}
          </v-list-item-title>
        </v-list-item>
      </template>
    </v-list>

    <!-- @after-enter is the reliable moment to focus the picker: the dialog's
         open transition + focus-trap have settled, so focusSearch lands in the
         fandom field instead of fighting the trap (operator-flagged 2026-06-07). -->
    <v-dialog
      v-model="fandomDialog" max-width="480"
      @after-enter="fandomPickerRef?.focusSearch?.()"
    >
      <FandomPicker
        ref="fandomPickerRef"
        @confirm="onFandomChosen" @cancel="onFandomCancel"
      />
    </v-dialog>
  </div>
</template>

<script setup>
import { computed, nextTick, onMounted, ref, watch } from 'vue'
import { useTagStore } from '../../stores/tags.js'
import { useSuggestionsStore } from '../../stores/suggestions.js'
import FandomPicker from './FandomPicker.vue'

const emit = defineEmits(['pick-existing', 'pick-new', 'accept-suggestion', 'cancel'])
const store = useTagStore()
// The image's ML (Camie) suggestions, surfaced inline in this dropdown so the
// operator can pick a suggestion while typing instead of hunting for it in the
// Suggestions panel below (operator-asked 2026-06-07).
const suggestions = useSuggestionsStore()

// Autofocus on modal open so the operator can type the moment the view
// modal renders, no extra click required (operator-asked 2026-06-01).
// Vuetify's v-text-field exposes .focus() on the component instance;
// nextTick waits for the modal's mount to finish so the inner <input>
// element exists.
//
// EXCEPTION — not on mobile (operator-flagged 2026-06-05): focusing the
// field pops the soft keyboard, which shrinks the visual viewport and
// shoves the pinned image + its nav/close controls out of view. 900px is
// the modal's stacked-layout breakpoint (ImageViewer.vue). matchMedia is
// safe on plain HTTP (not a secure-context API).
const inputRef = ref(null)
function focusInput () {
  if (window.matchMedia?.('(max-width: 900px)')?.matches) return
  nextTick(() => inputRef.value?.focus?.())
}
onMounted(focusInput)
// Exposed so the parent (TagPanel) can hand focus back to this field after an
// auto-suggestion is accepted, keeping the keyboard flow on the input instead
// of dropping to <body> (operator-asked 2026-06-08). Mobile guard preserved.
defineExpose({ focus: focusInput })

// Single text input; no kind dropdown. Client-side mirror of the
// backend's parse_kind_prefix lives below — kept in sync with
// KNOWN_KINDS in backend/app/utils/tag_prefix.py. The backend is the
// canonical parser; this mirror just powers the live Create-label
// preview ("Create 'Eric' as artist") and the autocomplete query.
const query = ref('')
const hits = ref([])
const highlight = ref(0)
const listRef = ref(null)
const fandomDialog = ref(false)
const fandomPickerRef = ref(null)
let pendingNewName = null

const KNOWN_KINDS = new Set([
  'character', 'fandom', 'series',
])
const KIND_ICONS = {
  general: 'mdi-tag', character: 'mdi-account-circle',
  fandom: 'mdi-book-open-page-variant', series: 'mdi-bookshelf',
}
function iconFor (k) { return KIND_ICONS[k] || 'mdi-tag' }

const parsed = computed(() => {
  const raw = query.value.trim()
  if (raw.includes(':')) {
    const idx = raw.indexOf(':')
    const prefix = raw.slice(0, idx).toLowerCase()
    if (KNOWN_KINDS.has(prefix)) {
      return { kind: prefix, name: raw.slice(idx + 1).trim() }
    }
  }
  return { kind: 'general', name: raw }
})
const parsedKind = computed(() => parsed.value.kind)
const parsedName = computed(() => parsed.value.name)

let debounceId = null
watch(query, () => {
  highlight.value = 0
  if (debounceId) clearTimeout(debounceId)
  debounceId = setTimeout(async () => {
    const q = parsedName.value
    if (!q) { hits.value = []; return }
    // Autocomplete across ALL kinds. When the user typed a prefix the
    // matches list is naturally narrower because the parsed name is
    // shorter; we don't filter server-side by kind.
    hits.value = await store.autocomplete(q, null, 10)
  }, 200)
})

const allowCreate = computed(() => {
  const q = parsedName.value
  if (!q) return false
  return !hits.value.some(h =>
    h.name.toLowerCase() === q.toLowerCase() && h.kind === parsedKind.value,
  )
})

function scorePct (s) { return `${Math.round(s.score * 100)}%` }

// This image's suggestions that match the typed query, minus any the server
// autocomplete already returned (same name+kind) so a tag never shows twice.
// Sources the FULL prediction set (allByCategory, down to the store floor) — NOT
// the threshold-filtered panel list — so a low-confidence action/feature the
// model saw can be typed and accepted in canonical formatting instead of being
// hand-entered as a custom tag (operator-asked 2026-06-09). The typed query is
// the only filter; the threshold no longer hides anything here.
const suggestionHits = computed(() => {
  const q = parsedName.value.toLowerCase()
  if (!q) return []
  const seen = new Set(hits.value.map(h => `${h.kind}:${h.name.toLowerCase()}`))
  const out = []
  for (const list of Object.values(suggestions.allByCategory)) {
    for (const s of list || []) {
      const key = `${s.category}:${s.display_name.toLowerCase()}`
      if (!s.display_name.toLowerCase().includes(q)) continue
      if (seen.has(key)) continue
      seen.add(key)
      out.push(s)
    }
  }
  // Best matches first; cap generously so a specific typed query surfaces its
  // matches even when many predictions exist, while the list stays scrollable.
  out.sort((a, b) => b.score - a.score)
  return out.slice(0, 20)
})

// One ordered list backing both the rendered dropdown and keyboard nav, so the
// highlight index maps 1:1 to a row regardless of which section it's in.
const rows = computed(() => {
  const r = hits.value.map(h => ({ type: 'hit', key: `h${h.id}`, hit: h }))
  for (const s of suggestionHits.value) {
    r.push({ type: 'suggestion', key: `s${s.category}:${s.display_name}`, sugg: s })
  }
  if (allowCreate.value) r.push({ type: 'create', key: 'create' })
  return r
})

function moveHighlight (delta) {
  const total = rows.value.length
  if (total === 0) return
  highlight.value = (highlight.value + delta + total) % total
  // Keep the highlighted row visible — the list is capped at 240px and arrowing
  // past the fold otherwise left the active item off-screen (operator-flagged
  // 2026-06-07). block:'nearest' scrolls the minimum needed.
  nextTick(() => {
    listRef.value?.$el
      ?.querySelector('.v-list-item--active')
      ?.scrollIntoView({ block: 'nearest' })
  })
}

// Dispatch a chosen dropdown row by its type.
function onPickRow (row) {
  if (row.type === 'hit') { emit('pick-existing', row.hit); reset() }
  else if (row.type === 'suggestion') { onPickSuggestion(row.sugg) }
  else { onCreate() }
}

// Picking an ML suggestion hands it to the parent, which runs the same accept
// flow as the Suggestions panel (creates the tag if raw, records acceptance,
// drops it from the panel). reset() clears the query; the panel-drop makes it
// fall out of suggestionHits reactively.
function onPickSuggestion (s) { emit('accept-suggestion', s); reset() }

function onCreate () {
  const name = parsedName.value
  const kind = parsedKind.value
  if (kind === 'character') {
    pendingNewName = name
    fandomDialog.value = true
    return
  }
  // Pass explicit kind here; the backend accepts both shapes. Passing
  // it makes the parsed kind preview match the actual server outcome
  // for users who didn't use a prefix (general goes through cleanly).
  emit('pick-new', { name, kind, fandom_id: null })
  reset()
}

// fandom is null when the user picked "No fandom" — characters don't all
// belong to a fandom. The backend already accepts fandom_id: null for the
// character kind (tag.kind check + nullable fandom_id), and a fandom can be
// assigned later from the chip kebab's "Set fandom…".
function onFandomChosen (fandom) {
  fandomDialog.value = false
  emit('pick-new', {
    name: pendingNewName, kind: 'character', fandom_id: fandom ? fandom.id : null,
  })
  pendingNewName = null
  reset()
  // Return focus to the tag input so the keyboard flow continues straight into
  // the next tag instead of dropping to <body> (operator-flagged 2026-06-07).
  focusInput()
}

// Cancelling the fandom dialog drops the pending character and hands focus back
// to the tag input, same as a successful pick.
function onFandomCancel () {
  fandomDialog.value = false
  pendingNewName = null
  focusInput()
}

function onEnter () {
  const row = rows.value[highlight.value]
  if (row) onPickRow(row)
}

// B5 (2026-06-07): when the dropdown is open, Tab accepts the highlighted row
// (standard autocomplete convention) instead of leaving the field. With the
// list closed it falls through to normal focus traversal.
function onTab (e) {
  if (rows.value.length) {
    e.preventDefault()
    onEnter()
  }
}

function reset () { query.value = ''; hits.value = []; highlight.value = 0 }
</script>

<style scoped>
.fc-tag-autocomplete__list {
  margin-top: 4px;
  max-height: 240px;
  overflow-y: auto;
  background: rgb(var(--v-theme-surface));
  border: 1px solid rgb(var(--v-theme-surface-light));
  border-radius: 6px;
}
/* Mark ML-suggestion rows so they read as distinct from typed/known matches. */
.fc-tag-autocomplete__sugg {
  border-left: 2px solid rgb(var(--v-theme-accent), 0.5);
}
.fc-tag-autocomplete__sugg-tag {
  display: inline-flex; align-items: center; gap: 2px;
  font-size: 11px;
  font-family: 'JetBrains Mono', monospace;
  color: rgb(var(--v-theme-accent));
}
</style>
