<template>
  <div class="fc-ex">
    <!-- Seeding the bare /explore nav entry with a random image. -->
    <div v-if="!anchorId && seeding" class="fc-ex__center-state">
      <div class="fc-ex__spinner" />
      <p class="fc-muted">Finding a random image to explore…</p>
    </div>

    <!-- Seed failed (e.g. empty library) — offer the gallery. -->
    <div v-else-if="!anchorId && seedError" class="fc-ex__center-state">
      <v-icon size="48" color="accent">mdi-compass-outline</v-icon>
      <h2 class="fc-ex__empty-title">Nothing to explore yet</h2>
      <p class="fc-ex__empty-body">{{ seedError }}</p>
      <v-btn color="accent" variant="flat" rounded="pill" :to="{ name: 'gallery' }">
        Browse the gallery
      </v-btn>
    </div>

    <template v-else>
      <!-- Breadcrumb of the walked path + reseed control. -->
      <nav class="fc-ex__trail" aria-label="Explore path">
        <button
          v-for="(c, i) in store.breadcrumb" :key="c.id"
          class="fc-ex__crumb"
          :class="{ 'fc-ex__crumb--current': i === store.cursor }"
          type="button"
          :aria-current="i === store.cursor ? 'page' : undefined"
          :title="`Step ${i + 1}`"
          @click="goTo(c.id)"
        >
          <img :src="c.thumbnail_url" alt="" loading="lazy" />
        </button>
        <div class="fc-ex__trail-actions">
          <!-- Reach (#1476): how far each step reaches past the anchor's cluster.
               Raise it to break out of a dense signature without hitting Random. -->
          <div
            class="fc-ex__reach"
            title="How far each step reaches — raise it to escape a dense cluster without going fully random"
          >
            <v-icon size="16" color="accent">mdi-map-marker-distance</v-icon>
            <v-slider
              :model-value="store.reach" @end="store.setReach"
              :min="0" :max="1" :step="0.2" hide-details density="compact"
              color="accent" class="fc-ex__reach-slider"
            />
            <span class="fc-muted fc-ex__reach-label">{{ reachLabel }}</span>
          </div>
          <!-- Active retrain right where you tag: fold the +/- you just gave
               into the heads without a trip to Settings (the nightly beat is the
               passive cadence). -->
          <v-btn
            size="small" variant="text" color="accent"
            prepend-icon="mdi-brain" :loading="headsBusy || headsRunning"
            title="Retrain the concept heads on your latest tags"
            @click="trainHeads"
          >{{ headsRunning ? 'Training…' : 'Retrain heads' }}</v-btn>
          <v-btn
            size="small" variant="text" color="accent"
            prepend-icon="mdi-shuffle-variant" :loading="seeding"
            @click="reseed"
          >Random image</v-btn>
        </div>
      </nav>

      <v-alert v-if="store.error" type="error" variant="tonal" class="ma-3">
        Failed to load: {{ store.error }}
      </v-alert>

      <!-- Three panes: neighbours | viewer | tag rail. The image modal,
           unfolded into a persistent surface so tagging while rabbit-holing is
           fast (operator concept 2026-06-26). -->
      <div class="fc-ex__panes">
        <!-- LEFT: visual neighbours; click to walk. -->
        <aside class="fc-ex__neighbors" aria-label="Visual neighbours">
          <h3 class="fc-ex__rail-title">
            Neighbours
            <span v-if="store.neighbors.length" class="fc-muted">({{ store.neighbors.length }})</span>
          </h3>
          <div v-if="store.loading && !store.neighbors.length" class="fc-ex__grid">
            <div v-for="n in 8" :key="n" class="fc-ex__skel" />
          </div>
          <p
            v-else-if="store.anchor && !store.anchor.has_embedding"
            class="fc-ex__note fc-muted"
          >
            No visual embedding yet (videos / not-yet-processed images), so
            there are no neighbours to walk.
          </p>
          <p v-else-if="!store.neighbors.length" class="fc-ex__note fc-muted">
            No visual neighbours found.
          </p>
          <div v-else class="fc-ex__grid">
            <button
              v-for="img in store.neighbors" :key="img.id"
              class="fc-ex__tile" type="button"
              :title="`Walk to image ${img.id}`"
              @click="goTo(img.id)"
            >
              <img :src="img.thumbnail_url" :alt="`Neighbour ${img.id}`" loading="lazy" />
            </button>
          </div>
        </aside>

        <!-- CENTER: the focused image (light viewer) + meta. -->
        <section class="fc-ex__viewer">
          <div class="fc-ex__canvas">
            <template v-if="store.anchor">
              <!-- Videos can't render in an <img> — branch to VideoCanvas like
                   the modal does (an MP4 in ImageCanvas just shows the alt). -->
              <ImageCanvas
                v-if="!isVideo"
                :key="store.anchor.id"
                :src="store.anchor.image_url"
                :alt="`Image ${store.anchor.id}`"
                :hover="hoverGrounding"
              />
              <VideoCanvas
                v-else
                :key="store.anchor.id"
                :src="store.anchor.image_url"
                :mime="store.anchor.mime"
              />
            </template>
          </div>
          <div v-if="store.anchor" class="fc-ex__viewer-foot">
            <div class="fc-ex__artist">{{ store.anchor.artist?.name || 'Unknown artist' }}</div>
            <ImageMetaBar :image="store.anchor" />
            <v-btn
              size="small" variant="text" color="accent"
              prepend-icon="mdi-arrow-expand-all" @click="openInViewer(store.anchor.id)"
            >Open full viewer</v-btn>
          </div>
        </section>

        <!-- RIGHT: provenance (post/source — often names the character) above
             the modal's tag rail, both hosted on the anchor. -->
        <aside class="fc-ex__rail" aria-label="Provenance and tags for the focused image">
          <template v-if="store.currentImageId != null">
            <ProvenancePanel :image-id="store.currentImageId" :image="store.anchor" />
            <TagPanel ref="tagPanelRef" :host="store" />
          </template>
        </aside>
      </div>
    </template>
  </div>
</template>

<script setup>
import { computed, nextTick, onMounted, onUnmounted, provide, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useApi } from '../composables/useApi.js'
import { useExploreStore } from '../stores/explore.js'
import { useModalStore } from '../stores/modal.js'
import { useHeadTraining } from '../composables/useHeadTraining.js'
import { isTextEntry } from '../utils/textEntry.js'
import ImageCanvas from '../components/modal/ImageCanvas.vue'
import VideoCanvas from '../components/modal/VideoCanvas.vue'
import ImageMetaBar from '../components/modal/ImageMetaBar.vue'
import ProvenancePanel from '../components/modal/ProvenancePanel.vue'
import TagPanel from '../components/modal/TagPanel.vue'

const route = useRoute()
const router = useRouter()
const api = useApi()
const store = useExploreStore()
const modal = useModalStore()

const anchorId = computed(() => route.params.imageId || null)
const isVideo = computed(() => !!store.anchor?.mime?.startsWith('video/'))
const reachLabel = computed(() => {
  const r = store.reach
  if (r <= 0.15) return 'Near'
  if (r <= 0.55) return 'Varied'
  return 'Far'
})

// #1206: hovering a suggestion in the rail highlights the crop it came from on
// the anchor image (same provide/inject as the modal viewer).
const hoverGrounding = ref(null)
provide('fcSuggestionHover', hoverGrounding)
const seeding = ref(false)
const seedError = ref(null)
const tagPanelRef = ref(null)

// Inline head-retrain (shared with the Settings card) so banking your latest
// +/- feedback is one click while you walk content.
const {
  running: headsRunning, busy: headsBusy, train: trainHeads,
  start: startHeads, stop: stopHeads,
} = useHeadTraining()

// Auto-focus the tag input after any action so tagging needs no extra click —
// the whole point of the workspace (operator-asked 2026-06-26). nextTick waits
// for the post-navigation re-render, then rAF lands the focus AFTER paint so a
// neighbour/breadcrumb click that re-renders the grid can't steal it back.
// Reuses TagPanel/TagAutocomplete's focus, which keeps its mobile guard (no
// soft-keyboard pop on touch).
function refocusTag () {
  nextTick(() => requestAnimationFrame(() => tagPanelRef.value?.focusTagInput?.()))
}
// Every focused-image change (seed + every walk: neighbour click, breadcrumb,
// Random image).
watch(() => store.currentImageId, (id) => { if (id != null) refocusTag() })

// The route is the source of truth. With an anchor, walk it; without one (the
// bare /explore nav entry) seed a RANDOM image so the tab kick-starts a rabbit
// hole instead of dead-ending on an empty state (operator-confirmed 2026-06-26).
watch(
  anchorId,
  (id) => {
    if (id) { store.anchorOn(id); return }
    store.reset()
    seedRandom()
  },
  { immediate: true },
)

async function seedRandom () {
  if (seeding.value) return
  seeding.value = true
  seedError.value = null
  try {
    const body = await api.get('/api/showcase', { params: { limit: 1 } })
    const img = body.images?.[0]
    if (!img) {
      seedError.value = 'Your library has no images to explore yet.'
      return
    }
    router.replace({ name: 'explore', params: { imageId: String(img.id) } })
  } catch (e) {
    seedError.value = e.message
  } finally {
    seeding.value = false
  }
}

// "Random image" jumps to a fresh seed from anywhere in the walk.
function reseed () {
  router.push({ name: 'explore' })
  // If already on bare /explore the param watch won't refire, so seed directly.
  if (!anchorId.value) seedRandom()
}

function goTo (id) {
  if (Number(id) === Number(anchorId.value)) return
  router.push({ name: 'explore', params: { imageId: String(id) } })
}

function openInViewer (id) { modal.open(id) }

// Keyboard: T or "/" jumps to the tag input (modal-parity); ←/→ walk the
// breadcrumb trail — ← steps back, → goes forward to an already-visited item
// or, with no forward history, jumps to a random neighbour (keep rabbit-holing).
function onKeyDown (ev) {
  if (ev.metaKey || ev.ctrlKey || ev.altKey) return
  const inText = isTextEntry(ev.target)
  if (ev.key === 't' || ev.key === '/') {
    if (!inText) {
      const input = document.querySelector('.fc-tag-autocomplete input')
      if (input) { ev.preventDefault(); input.focus() }
    }
    return
  }
  if (ev.key !== 'ArrowLeft' && ev.key !== 'ArrowRight') return
  // The tag input auto-focuses after every walk, so also allow navigation while
  // it's focused-but-EMPTY (the caret has nowhere to go) — otherwise arrow-nav
  // would dead-end after one step. Once a tag is being typed, arrows move the
  // caret instead.
  const t = ev.target
  const inEmptyTagInput =
    inText && t.value === '' && t.closest?.('.fc-tag-autocomplete')
  if (inText && !inEmptyTagInput) return
  const id = ev.key === 'ArrowLeft' ? store.backTarget() : store.forwardTarget()
  if (id != null) { ev.preventDefault(); goTo(id) }
}
onMounted(() => {
  document.addEventListener('keydown', onKeyDown)
  startHeads()   // reflect a run already in flight (e.g. the nightly one)
})
onUnmounted(() => {
  document.removeEventListener('keydown', onKeyDown)
  stopHeads()
})
</script>

<style scoped>

/* Full-height workspace under the sticky top nav. --fc-nav-h is the nav's REAL
   measured height (set by TopNav) — a hardcoded 64px here overflowed the
   viewport under Vuetify 4's taller nav and tucked the breadcrumb under it
   (#1481). Panes scroll internally, so an exact fit keeps everything on screen. */
.fc-ex {
  display: flex; flex-direction: column;
  height: calc(100vh - var(--fc-nav-h, 64px));
  min-height: 0;
}

.fc-ex__center-state {
  margin: auto; max-width: 520px; text-align: center;
  display: flex; flex-direction: column; align-items: center; gap: 14px;
  padding: 32px;
}
.fc-ex__empty-title { font-family: 'Fraunces', Georgia, serif; font-weight: 500; }
.fc-ex__empty-body { color: rgb(var(--v-theme-on-surface-variant)); }

.fc-ex__trail {
  display: flex; gap: 6px; align-items: center; flex-wrap: wrap;
  padding: 8px 12px; flex: 0 0 auto;
}
.fc-ex__crumb {
  width: 40px; height: 40px; border-radius: 8px; overflow: hidden;
  border: 2px solid transparent; cursor: pointer; padding: 0;
  background: rgb(var(--v-theme-surface-light)); flex: 0 0 auto;
}
.fc-ex__crumb img { width: 100%; height: 100%; object-fit: cover; }
.fc-ex__crumb--current { border-color: rgb(var(--v-theme-accent)); }
.fc-ex__crumb:focus-visible { outline: 2px solid rgb(var(--v-theme-accent)); outline-offset: 1px; }
.fc-ex__trail-actions {
  margin-left: auto; display: flex; align-items: center; gap: 4px; flex: 0 0 auto;
}
.fc-ex__reach {
  display: flex; align-items: center; gap: 6px;
  margin-right: 8px;
}
.fc-ex__reach-slider { width: 96px; }
.fc-ex__reach-label {
  font-size: 12px; min-width: 44px; text-align: left;
}

/* The three panes fill the remaining height; each scrolls on its own.
   grid-template-rows: minmax(0, 1fr) BOUNDS the single row to the container
   height — without it the row is `auto` and a tall pane (Provenance + Tags +
   a long Suggestions list) grows the row past the fixed-height view, spilling
   over and scrolling the whole page (the weird top gap). Bounded, the panes'
   own overflow-y:auto kicks in and they scroll internally instead. */
.fc-ex__panes {
  flex: 1 1 auto; min-height: 0;
  display: grid;
  grid-template-columns: 340px minmax(0, 1fr) var(--fc-side-w, 320px);
  grid-template-rows: minmax(0, 1fr);
  gap: 1px;
  background: rgb(var(--v-theme-surface-light));
}

.fc-ex__neighbors {
  background: rgb(var(--v-theme-background));
  overflow-y: auto; padding: 12px;
}
.fc-ex__rail-title {
  font-size: 14px; font-weight: 600; margin: 2px 0 10px;
}
.fc-ex__grid {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(148px, 1fr));
  gap: 8px;
}
.fc-ex__tile {
  aspect-ratio: 1; border-radius: 8px; overflow: hidden; padding: 0;
  border: 2px solid transparent; cursor: pointer;
  background: rgb(var(--v-theme-surface-light));
}
.fc-ex__tile img { width: 100%; height: 100%; object-fit: cover; display: block; }
.fc-ex__tile:hover { border-color: rgba(var(--v-theme-accent), 0.6); }
.fc-ex__tile:focus-visible { outline: 2px solid rgb(var(--v-theme-accent)); outline-offset: 1px; }
.fc-ex__skel {
  aspect-ratio: 1; border-radius: 8px;
  background: rgb(var(--v-theme-surface-light)); animation: fc-pulse 1.4s ease-in-out infinite;
}
.fc-ex__note { padding: 16px 4px; }

/* Center viewer. */
.fc-ex__viewer {
  background: rgb(20, 23, 26);
  display: flex; flex-direction: column; min-width: 0; min-height: 0;
}
.fc-ex__canvas { flex: 1 1 auto; display: flex; min-height: 0; min-width: 0; }
.fc-ex__viewer-foot {
  flex: 0 0 auto;
  border-top: 1px solid rgb(var(--v-theme-surface-light));
  padding-bottom: 10px;
}
.fc-ex__artist { padding: 10px 16px 0; font-weight: 600; }

/* Right rail = the modal's tag panel, hosted on the anchor. The bottom padding
   keeps the scrollable content from reaching the viewport floor, so the
   bottom-right snackbar floats over empty space instead of covering the last
   suggestions / their accept-reject controls (operator-flagged 2026-07-06). */
.fc-ex__rail {
  background: rgb(var(--v-theme-surface));
  overflow-y: auto;
  padding-bottom: 88px;
}

.fc-ex__spinner {
  width: 64px; height: 64px; border-radius: 50%;
  border: 5px solid rgb(var(--v-theme-accent), 0.16);
  border-top-color: rgb(var(--v-theme-accent));
  animation: fc-ex-spin 0.95s cubic-bezier(0.5, 0.1, 0.5, 0.9) infinite;
}
@keyframes fc-ex-spin { to { transform: rotate(360deg); } }
@keyframes fc-pulse { 0%, 100% { opacity: 0.5; } 50% { opacity: 0.9; } }
@media (prefers-reduced-motion: reduce) { .fc-ex__spinner { animation-duration: 3s; } }

/* Narrow: stack viewer → tags → neighbours, page scrolls. */
@media (max-width: 1100px) {
  .fc-ex { height: auto; }
  .fc-ex__panes {
    grid-template-columns: 1fr;
    /* Stacked: let each pane take its natural height and the page scroll. */
    grid-template-rows: none;
    background: transparent;
  }
  .fc-ex__viewer { order: -1; min-height: 60vh; }
  .fc-ex__neighbors { order: 1; }
}
</style>
