<template>
  <section class="fc-suggestions" aria-label="Tag suggestions">
    <h3 class="fc-suggestions__title">Suggestions</h3>

    <div v-if="store.loading" class="fc-suggestions__skeleton">
      <div v-for="i in 4" :key="i" class="fc-suggestions__skel-row" />
    </div>
    <v-alert v-else-if="store.error" type="error" variant="tonal" density="compact">
      {{ store.error }}
    </v-alert>
    <div v-else-if="isEmpty" class="text-caption">
      No suggestions above threshold.
    </div>

    <!-- Scroll-capped so a long suggestion set (the General bucket can run to
         dozens) scrolls WITHIN this box instead of stretching the right rail
         and shoving the Related strip below the fold. Mirrors the
         ProvenancePanel cards/attachments treatment. -->
    <div v-else class="fc-suggestions__list">
      <SuggestionsCategoryGroup
        v-for="cat in peopleCats" :key="cat"
        v-show="store.byCategory[cat] && store.byCategory[cat].length"
        :label="labelFor(cat)" :items="store.byCategory[cat] || []"
        @accept="onAccept" @alias="onAlias" @remove-alias="onRemoveAlias"
        @dismiss="store.dismiss"
      />
      <SuggestionsCategoryGroup
        v-if="store.byCategory.general && store.byCategory.general.length"
        label="General" :items="store.byCategory.general"
        collapsible :default-open="true"
        @accept="onAccept" @alias="onAlias" @remove-alias="onRemoveAlias"
        @dismiss="store.dismiss"
      />
    </div>

    <v-dialog v-model="aliasDialog" max-width="480">
      <AliasPickerDialog
        v-if="aliasTarget"
        :category="aliasTarget.category"
        @confirm="onAliasConfirm" @cancel="aliasDialog = false"
      />
    </v-dialog>
  </section>
</template>

<script setup>
import { toast } from '../../utils/toast.js'
import { computed, ref, watch } from 'vue'
import { useSuggestionsStore, CATEGORY_LABELS } from '../../stores/suggestions.js'
import { useModalStore } from '../../stores/modal.js'
import SuggestionsCategoryGroup from './SuggestionsCategoryGroup.vue'
import AliasPickerDialog from './AliasPickerDialog.vue'

const props = defineProps({
  imageId: { type: Number, required: true },
  // The tagging host whose chip rail to refresh after an accept. Defaults to
  // the modal store (image modal); the Explore workspace passes its anchor host
  // so the same panel refreshes the right surface. See TagPanel.
  host: { type: Object, default: null },
})
// 'accepted' lets the parent return focus to the tag input after a suggestion is
// applied (operator-asked 2026-06-08).
const emit = defineEmits(['accepted'])
const store = useSuggestionsStore()
const modalStore = useModalStore()
const host = props.host || modalStore

// 'artist' (FC-2d-vii-c) and 'copyright' (2026-06-01) retired as
// suggestion categories. Only 'character' remains as a people-style
// category alongside the general bucket.
const peopleCats = ['character']
function labelFor(c) { return CATEGORY_LABELS[c] || c }

const isEmpty = computed(() =>
  Object.values(store.byCategory).every(list => !list || list.length === 0)
)

watch(() => props.imageId, (id) => {
  if (id == null) return
  store.load(id)        // panel: curated, ≥ threshold
  store.loadAll(id)     // dropdown: full prediction set (low-confidence included)
}, { immediate: true })

// After a successful accept/alias-accept, refresh the modal's current
// tag list so TagPanel's chip rail reflects the newly-attached tag.
// Operator-flagged 2026-06-01: the suggestion store dropped the
// suggestion (correct) but didn't propagate the new tag back into the
// modal store, so the chip rail looked unchanged even though the
// backend had recorded the application. Mirrors the addExistingTag /
// createAndAdd flows which already call reloadTags() after applying.
async function onAccept(s) {
  try {
    await store.accept(s)
    await host.reloadTags()
    emit('accepted')
  } catch (e) {
    toast({ text: `Accept failed: ${e.message}`, type: 'error' })
  }
}

const aliasDialog = ref(false)
const aliasTarget = ref(null)
function onAlias(s) { aliasTarget.value = s; aliasDialog.value = true }
async function onAliasConfirm(canonicalTagId) {
  try {
    await store.aliasAccept(aliasTarget.value, canonicalTagId)
    aliasDialog.value = false
    await host.reloadTags()
    emit('accepted')
  } catch (e) {
    toast({ text: `Alias failed: ${e.message}`, type: 'error' })
  }
}

// Undo the model-key→tag mapping behind an aliased suggestion. The store
// reloads suggestions so the prediction reverts to its raw form; the applied
// canonical tag (if any) stays, so no tag-rail reload is needed.
async function onRemoveAlias(s) {
  try {
    await store.removeAlias(s)
  } catch (e) {
    toast({ text: `Remove alias failed: ${e.message}`, type: 'error' })
  }
}
</script>

<style scoped>
.fc-suggestions {
  margin-top: 16px;
  padding-top: 12px;
  border-top: 1px solid rgb(var(--v-theme-surface-light));
}
.fc-suggestions__title {
  font-family: 'Fraunces', Georgia, serif;
  font-size: 16px; font-weight: 500;
  color: rgb(var(--v-theme-on-surface));
  margin-bottom: 8px;
}
.fc-suggestions__list {
  /* Cap the suggestion run so it scrolls internally rather than pushing the
     Related strip off the bottom of the rail. Hairline scrollbar matching
     ProvenancePanel's capped regions. */
  max-height: 320px;
  overflow-y: auto;
  scrollbar-width: thin;
  scrollbar-color: rgb(var(--v-theme-surface-light)) transparent;
  padding-right: 4px;
}
.fc-suggestions__skeleton { display: flex; flex-direction: column; gap: 8px; }
.fc-suggestions__skel-row {
  height: 18px; border-radius: 4px;
  background: linear-gradient(
    90deg,
    rgb(var(--v-theme-surface)) 0%,
    rgb(var(--v-theme-surface-light)) 50%,
    rgb(var(--v-theme-surface)) 100%
  );
  background-size: 200% 100%;
  animation: fc-shimmer 1.4s infinite;
}
@keyframes fc-shimmer {
  0% { background-position: 200% 0; }
  100% { background-position: -200% 0; }
}
</style>
