<template>
  <Teleport to="body">
    <div
      class="fc-viewer" role="dialog" aria-modal="true"
      tabindex="-1" ref="rootEl"
    >
      <button class="fc-viewer__close" @click="$emit('close')" aria-label="Close">
        <v-icon>mdi-close</v-icon>
      </button>

      <v-chip
        v-if="integrityBadge"
        :color="integrityBadge.color"
        class="fc-viewer__integrity"
        size="x-small" variant="flat"
      >{{ integrityBadge.label }}</v-chip>

      <button
        class="fc-viewer__nav fc-viewer__nav--prev"
        :disabled="!modal.canPrev" @click="modal.goPrev()" aria-label="Previous"
      >
        <v-icon size="32">mdi-chevron-left</v-icon>
      </button>
      <button
        class="fc-viewer__nav fc-viewer__nav--next"
        :disabled="!modal.canNext" @click="modal.goNext()" aria-label="Next"
      >
        <v-icon size="32">mdi-chevron-right</v-icon>
      </button>

      <div class="fc-viewer__body">
        <div class="fc-viewer__media">
          <v-progress-circular
            v-if="modal.loading" indeterminate color="accent" size="36"
          />
          <template v-else-if="modal.current">
            <ImageCanvas
              v-if="!isVideo" :src="modal.current.image_url" :alt="`Image ${modal.current.id}`"
              @close-request="$emit('close')"
            />
            <VideoCanvas
              v-else :src="modal.current.image_url" :mime="modal.current.mime"
              @close-request="$emit('close')"
            />
          </template>
          <v-alert v-else-if="modal.error" type="error" variant="tonal">
            {{ modal.error }}
          </v-alert>
        </div>

        <aside v-if="modal.current" class="fc-viewer__side">
          <ProvenancePanel />
          <TagPanel />
          <!-- Non-blocking: fetches its own similar set after the modal is up;
               collapses silently if empty/slow/failed (see RelatedStrip). -->
          <RelatedStrip />
        </aside>
      </div>
    </div>
  </Teleport>
</template>

<script setup>
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
import { useModalStore } from '../../stores/modal.js'
import { arrowNavAllowed } from '../../utils/textEntry.js'
import ImageCanvas from './ImageCanvas.vue'
import VideoCanvas from './VideoCanvas.vue'
import TagPanel from './TagPanel.vue'
import ProvenancePanel from './ProvenancePanel.vue'
import RelatedStrip from './RelatedStrip.vue'

const emit = defineEmits(['close'])

const modal = useModalStore()
const rootEl = ref(null)

const isVideo = computed(() =>
  modal.current?.mime && modal.current.mime.startsWith('video/')
)

const integrityBadge = computed(() => {
  const s = modal.current?.integrity_status
  if (s === 'corrupt') return { color: 'error', label: 'Corrupt' }
  if (s === 'failed_verification')
    return { color: 'warning', label: 'Unverified' }
  return null
})

let prevBodyOverflow = null

// Document-level keyboard handler. Bound on document (not on the modal
// root) because <video> elements capture focus when the user interacts
// with their controls — when video has focus, Escape and arrow keys
// don't bubble reliably to ancestors. Listening on document side-steps
// that. Filtered via isTextEntry so tag/comment inputs still get their
// own keystrokes.
function onKeyDown(ev) {
  if (ev.key === 'Escape') {
    // Escape closes the modal even from inside a text input — that's
    // the universal "get me out of here" expectation, and the
    // autofocused tag-entry field would otherwise trap focus with no
    // visible escape (operator-flagged 2026-06-01). EXCEPTION: when a
    // nested Vuetify overlay is open (v-menu autocomplete dropdown,
    // FandomPicker v-dialog, per-suggestion 3-dot menu), let that
    // overlay's own Esc handling fire instead of closing the whole
    // modal mid-interaction. Vuetify marks open overlays with
    // `.v-overlay--active`.
    // EXCLUDE tooltips (`.v-tooltip`): they're also `.v-overlay--active` while
    // shown, so one lingering after a hover/click (e.g. just after accepting a
    // suggested tag) wrongly suppressed the close (#700). Only real interactive
    // overlays (menus/dialogs) should keep ESC from closing the modal.
    if (document.querySelector('.v-overlay--active:not(.v-tooltip)')) return
    ev.preventDefault()
    emit('close')
  } else if (ev.key === 'ArrowLeft') {
    // Navigate unless the caret is in a non-empty text field (then let it move
    // through the text). An empty tag-entry field still navigates.
    if (!arrowNavAllowed(ev.target)) return
    ev.preventDefault()
    modal.goPrev()
  } else if (ev.key === 'ArrowRight') {
    if (!arrowNavAllowed(ev.target)) return
    ev.preventDefault()
    modal.goNext()
  }
}

onMounted(async () => {
  prevBodyOverflow = document.body.style.overflow
  document.body.style.overflow = 'hidden'
  document.addEventListener('keydown', onKeyDown, true)
  await nextFrame()
  rootEl.value?.focus()
})
onUnmounted(() => {
  document.body.style.overflow = prevBodyOverflow
  document.removeEventListener('keydown', onKeyDown, true)
})

watch(() => modal.currentImageId, async () => {
  await nextFrame()
  rootEl.value?.focus()
})

function nextFrame() {
  return new Promise(resolve => requestAnimationFrame(resolve))
}
</script>

<style scoped>
.fc-viewer {
  position: fixed; inset: 0; z-index: 2000;
  /* Single source of truth for the metadata side-panel width — the next
     arrow offsets off it so it never overlaps the panel. */
  --fc-side-w: 320px;
  /* Obsidian haze (#14171A = 20,23,26) — same palette as TopNav,
     mid-opacity + blur so the page behind shows through faintly. */
  background: rgba(20, 23, 26, 0.65);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  outline: none;
  display: flex; flex-direction: column;
}
.fc-viewer__close, .fc-viewer__nav {
  position: absolute; top: 50%;
  background: rgba(20, 23, 26, 0.7);
  color: rgb(var(--v-theme-parchment, 232 228 216));
  border: 1px solid rgb(var(--v-theme-surface-light));
  border-radius: 50%;
  width: 48px; height: 48px;
  display: grid; place-items: center;
  cursor: pointer; z-index: 2;
  transition: background 100ms ease;
}
.fc-viewer__close:hover, .fc-viewer__nav:hover:not(:disabled) {
  background: rgb(var(--v-theme-surface));
}
.fc-viewer__nav:disabled { opacity: 0.3; cursor: not-allowed; }
.fc-viewer__close { top: 16px; right: 16px; transform: none; }
.fc-viewer__integrity {
  position: absolute; top: 72px; right: 16px; z-index: 3;
}
.fc-viewer__nav--prev { left: 16px; transform: translateY(-50%); }
/* Sit just inside the image area, clear of the metadata side panel —
   not floating over it (operator-flagged 2026-06-04). */
.fc-viewer__nav--next {
  right: calc(var(--fc-side-w) + 16px);
  transform: translateY(-50%);
}
.fc-viewer__body {
  flex: 1; display: flex; min-height: 0;
}
.fc-viewer__media {
  /* Let the canvas fill us; the canvas does its own centering. Both
     min-* are needed so this child can shrink inside its flex parents
     (without them, max-height/max-width on the <img> have nothing to
     bound against and the image overflows the viewport). */
  flex: 1; display: flex;
  min-width: 0; min-height: 0;
}
.fc-viewer__side {
  width: var(--fc-side-w); flex-shrink: 0;
  background: rgb(var(--v-theme-surface));
  border-left: 1px solid rgb(var(--v-theme-surface-light));
  overflow-y: auto;
}

@media (max-width: 900px) {
  /* Stacked layout (operator-flagged 2026-06-05): pin the image pane and
     its controls at the top while the metadata panel scrolls beneath it.
     Previously the image + a 40vh panel shared one fixed viewport and the
     controls could be pushed out of view; now the body scrolls and the
     media is sticky, so the image + prev/next/close (and the integrity
     badge) stay visible no matter how far the panel is scrolled. */
  .fc-viewer__body {
    flex-direction: column;
    overflow-y: auto;
  }
  .fc-viewer__media {
    flex: none;
    height: 55vh;
    position: sticky;
    top: 0;
    z-index: 1;
    /* Opaque obsidian so the scrolling panel never bleeds through the
       haze behind the pinned image. */
    background: rgb(20, 23, 26);
  }
  .fc-viewer__side {
    width: 100%;
    max-height: none;
    border-left: none;
    border-top: 1px solid rgb(var(--v-theme-surface-light));
  }
  /* Re-center the prev/next arrows over the 55vh image band (their base
     top:50% would land on the scrolling panel); next uses the full width
     now that the panel is below, not beside. Close + integrity badge keep
     their top:16px/72px — already within the pinned band. */
  .fc-viewer__nav { top: 27.5vh; }
  .fc-viewer__nav--next { right: 16px; }
}
</style>
