<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>

      <!-- C8: keyboard cheatsheet. '?' toggles; the corner hint advertises it. -->
      <button
        class="fc-viewer__help-hint" aria-label="Keyboard shortcuts (press ?)"
        @click="showHelp = !showHelp"
      >?</button>
      <div v-if="showHelp" class="fc-viewer__help" @click.self="showHelp = false">
        <div class="fc-viewer__help-card" role="dialog" aria-label="Keyboard shortcuts">
          <h3>Keyboard shortcuts</h3>
          <dl>
            <div><dt>← / →</dt><dd>Previous / next image</dd></div>
            <div><dt>T or /</dt><dd>Jump to the tag input</dd></div>
            <div><dt>↑ / ↓</dt><dd>Move through tag suggestions</dd></div>
            <div><dt>Enter / Tab</dt><dd>Accept the highlighted tag</dd></div>
            <div><dt>Esc</dt><dd>Close a dialog, then the viewer</dd></div>
            <div><dt>?</dt><dd>Toggle this help</dd></div>
          </dl>
        </div>
      </div>

      <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>

      <!-- Large centered loading spinner (operator-flagged 2026-06-07: the old
           size-36 one sat tiny in the top-left). Dual counter-rotating accent
           rings, centered over the whole modal. pointer-events:none so the
           close/nav controls underneath stay clickable. -->
      <div v-if="modal.loading" class="fc-viewer__loading">
        <div class="fc-viewer__spinner" />
      </div>

      <div class="fc-viewer__body">
        <div class="fc-viewer__media">
          <template v-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, isTextEntry } 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 showHelp = ref(false)

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' && showHelp.value) {
    // Close the cheatsheet first; a second Esc closes the modal.
    ev.preventDefault()
    showHelp.value = false
    return
  }
  if (ev.key === 'Escape') {
    // Escape closes the modal even from inside a text input — the universal
    // "get me out of here" expectation; the autofocused tag field would
    // otherwise trap focus (operator-flagged 2026-06-01). EXCEPTION: when the
    // keystroke originates INSIDE an open Vuetify overlay's content (a rename/
    // fandom/alias dialog, or a kebab menu), let that overlay handle its own
    // Esc and don't close the whole modal.
    //
    // #700 re-fix (2026-06-07): the prior guard queried for ANY active overlay
    // anywhere in the DOM and suppressed the close — so a lingering overlay
    // after accepting a suggestion (focus drops to <body>, not into any
    // overlay) wrongly blocked Esc. Keying off the event's origin instead means
    // a stray overlay no longer traps the modal: only an Esc pressed from
    // within overlay content defers to that overlay.
    if (ev.target?.closest?.('.v-overlay__content')) 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()
  } else if ((ev.key === '/' || ev.key === 't') && !isTextEntry(ev.target)) {
    // C9 (2026-06-07): jump focus to the tag input from anywhere in the modal.
    const input = document.querySelector('.fc-tag-autocomplete input')
    if (input) { ev.preventDefault(); input.focus() }
  } else if (ev.key === '?' && !isTextEntry(ev.target)) {
    // C8: toggle the keyboard cheatsheet.
    ev.preventDefault()
    showHelp.value = !showHelp.value
  }
}

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__help-hint {
  position: absolute; top: 16px; right: 64px; z-index: 2;
  width: 32px; height: 32px; border-radius: 50%;
  background: rgba(255, 255, 255, 0.08);
  color: rgb(var(--v-theme-on-surface));
  font-weight: 700; cursor: pointer; opacity: 0.6;
}
.fc-viewer__help-hint:hover { opacity: 1; }
.fc-viewer__help {
  position: absolute; inset: 0; z-index: 4;
  display: flex; align-items: center; justify-content: center;
  background: rgba(0, 0, 0, 0.4);
}
.fc-viewer__help-card {
  background: rgb(var(--v-theme-surface));
  border: 1px solid rgb(var(--v-theme-surface-light));
  border-radius: 10px; padding: 20px 24px; min-width: 320px;
  box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
}
.fc-viewer__help-card h3 {
  font-family: 'Fraunces', Georgia, serif;
  font-size: 18px; margin-bottom: 12px;
  color: rgb(var(--v-theme-on-surface));
}
.fc-viewer__help-card dl { display: flex; flex-direction: column; gap: 8px; }
.fc-viewer__help-card dl > div {
  display: flex; gap: 16px; align-items: baseline;
}
.fc-viewer__help-card dt {
  flex: 0 0 96px; text-align: right;
  font-family: 'JetBrains Mono', monospace; font-size: 12px;
  color: rgb(var(--v-theme-accent));
}
.fc-viewer__help-card dd {
  flex: 1; font-size: 14px;
  color: rgb(var(--v-theme-on-surface));
}
.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%);
}
/* Centered loading overlay — sits over the media band; non-interactive so the
   close/nav controls underneath keep working. */
.fc-viewer__loading {
  position: absolute; inset: 0; z-index: 2;
  display: flex; align-items: center; justify-content: center;
  pointer-events: none;
}
/* Two concentric accent rings counter-rotating, each lit on a different arc —
   reads as one orbiting object. ~108px so it's unmistakably the focal point. */
.fc-viewer__spinner {
  width: 108px; height: 108px; border-radius: 50%;
  border: 5px solid rgb(var(--v-theme-accent), 0.16);
  border-top-color: rgb(var(--v-theme-accent));
  animation: fc-viewer-spin 0.95s cubic-bezier(0.5, 0.1, 0.5, 0.9) infinite;
  position: relative;
  box-shadow: 0 0 24px rgb(var(--v-theme-accent), 0.18);
}
.fc-viewer__spinner::after {
  content: ''; position: absolute; inset: 14px; border-radius: 50%;
  border: 5px solid rgb(var(--v-theme-accent), 0.10);
  border-bottom-color: rgb(var(--v-theme-accent));
  animation: fc-viewer-spin 1.5s cubic-bezier(0.5, 0.1, 0.5, 0.9) infinite reverse;
}
@keyframes fc-viewer-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
  .fc-viewer__spinner, .fc-viewer__spinner::after { animation-duration: 3s; }
}

.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>
