'use client'

import {
  ChevronLeft, ChevronRight, Grid3x3, ZoomIn, ZoomOut, Maximize2,
} from 'lucide-react'
import { useCatalogStore } from '@/lib/store'
import { useEffect, useCallback, useState, memo, useRef } from 'react'
import { motion, AnimatePresence, useReducedMotion, type Variants } from 'framer-motion'
import { Hand } from 'lucide-react'
import { TOTAL_PAGES } from './header'
import { PageProductPins } from './page-product-pins'
import { useIsMobile } from '@/hooks/use-mobile'

// WebP-optimized sources — 78% smaller than original JPEGs
function pageSrc(page: number) {
  return `/catalog-pages-web/page-${String(page).padStart(3, '0')}.webp`
}

// Tiny preview (900px, ~46KB) for blur-up placeholder effect
function previewSrc(page: number) {
  return `/catalog-pages-preview/page-${String(page).padStart(3, '0')}.webp`
}

function thumbSrc(page: number) {
  return `/catalog-pages-thumbs/page-${String(page).padStart(3, '0')}.webp`
}

type FlipDir = 'next' | 'prev'

/**
 * MagazineViewer — the primary view (Boticário style).
 * - Single page per screen (always one page at a time)
 * - Side arrows flip pages
 * - Bottom: page slider + thumbnail toggle
 *
 * Optimizations:
 * - WebP images (78% smaller than JPEG)
 * - Blur-up placeholder using tiny preview image
 * - ALL pages preloaded in the background (requestIdleCallback) so
 *   page navigation is instant after the initial preload completes
 * - framer-motion AnimatePresence for proper page-turn animation
 *   (both enter AND exit transitions, direction-aware)
 * - Memoized PageImage and Thumbnail components
 * - Virtualized thumbnail strip (content-visibility: auto)
 */
export function MagazineViewer() {
  const {
    selectedPage, goToPage, showThumbnails, setShowThumbnails,
    zoom, setZoom,
  } = useCatalogStore()

  // Single page — always show the selected page
  const currentPage = selectedPage
  const prefersReduced = useReducedMotion()
  const isMobile = useIsMobile()

  // Default zoom 75% on mobile — set ONCE on first mobile detection. With
  // the slot filling 100% of the viewing area and the image centered, 75%
  // shows the FULL page centered with symmetric margins top & bottom (a
  // "fit to screen" view) — no bottom-only gap. The user can zoom in via
  // the controls to fill the height. Guard ref ensures it only runs once
  // (doesn't override manual changes on later re-renders).
  const defaultZoomSetRef = useRef(false)
  useEffect(() => {
    if (defaultZoomSetRef.current) return
    if (isMobile) {
      setZoom(0.75)
      defaultZoomSetRef.current = true
    }
  }, [isMobile, setZoom])

  // Page-flip direction: set synchronously when navigating so the
  // AnimatePresence variants know which way to slide
  // (next = new page enters from right, old page exits to left)
  const [flipDir, setFlipDir] = useState<FlipDir | null>(null)
  const goToPageWithDir = useCallback((target: number) => {
    if (target === selectedPage) return
    setFlipDir(target > selectedPage ? 'next' : 'prev')
    goToPage(target)
  }, [selectedPage, goToPage])

  const nextPage = useCallback(() => {
    if (selectedPage < TOTAL_PAGES) goToPageWithDir(selectedPage + 1)
  }, [selectedPage, goToPageWithDir])

  const prevPage = useCallback(() => {
    if (selectedPage > 1) goToPageWithDir(selectedPage - 1)
  }, [selectedPage, goToPageWithDir])

  // Keyboard navigation
  useEffect(() => {
    const handler = (e: KeyboardEvent) => {
      const tag = (e.target as HTMLElement)?.tagName
      if (tag === 'INPUT' || tag === 'TEXTAREA') return
      const s = useCatalogStore.getState()
      if (s.selectedProduct || s.searchOpen || s.sidebarOpen || s.favoritesOpen || s.adminOpen) return
      if (e.key === 'ArrowRight') nextPage()
      if (e.key === 'ArrowLeft') prevPage()
      if (e.key === '+' || e.key === '=') setZoom(Math.min(zoom + 0.25, 2.5))
      if (e.key === '-') setZoom(Math.max(zoom - 0.25, 0.5))
    }
    window.addEventListener('keydown', handler)
    return () => window.removeEventListener('keydown', handler)
  }, [nextPage, prevPage, zoom, setZoom])

  // Eagerly preload ±3 neighbors (high priority — most likely to be
  // visited next). Runs on every page change so the immediate next/prev
  // pages are always ready even before the full background preload finishes.
  useEffect(() => {
    const from = Math.max(1, selectedPage - 3)
    const to = Math.min(TOTAL_PAGES, selectedPage + 3)
    for (let p = from; p <= to; p++) {
      if (p === selectedPage) continue
      const img = new Image()
      img.src = pageSrc(p)
      img.decoding = 'async'
    }
  }, [selectedPage])

  // Preload ALL catalog pages in the background (once on mount) so that
  // page navigation is instant — no waiting for images to load.
  // Uses requestIdleCallback (with setTimeout fallback) to avoid blocking
  // the initial render or the current page's decode. The browser caches
  // each image, so subsequent navigations read from cache (instant).
  // Total payload: ~16MB across 91 WebP pages — acceptable for a catalog
  // browsing experience where the user is expected to flip through pages.
  useEffect(() => {
    const preloadAll = () => {
      for (let p = 1; p <= TOTAL_PAGES; p++) {
        const img = new Image()
        img.src = pageSrc(p)
        img.decoding = 'async'
      }
    }
    const w = window as Window & {
      requestIdleCallback?: (cb: () => void, opts?: { timeout?: number }) => number
      cancelIdleCallback?: (handle: number) => void
    }
    if (typeof w.requestIdleCallback === 'function') {
      const handle = w.requestIdleCallback(preloadAll, { timeout: 5000 })
      return () => w.cancelIdleCallback?.(handle)
    }
    const t = setTimeout(preloadAll, 2000)
    return () => clearTimeout(t)
  }, [])

  const canPrev = selectedPage > 1
  const canNext = selectedPage < TOTAL_PAGES

  // Ref callback for the page stage — on mobile, centers the horizontal
  // scroll so the MIDDLE of the (wider-than-viewport) image is visible
  // by default. The user can then swipe left/right to see the edges.
  //
  // The image is preloaded (cached), but the browser may still decode it
  // asynchronously after the <img> element mounts. So we center:
  //  1. Synchronously (for already-laid-out images)
  //  2. On the next animation frame (for cached images being decoded)
  //  3. Via ResizeObserver (fires when the image's layout size settles)
  // The ResizeObserver is cleaned up when the stage unmounts (page change),
  // since motion.div has key={currentPage} and is re-created each navigation.
  const setStageRef = useCallback((el: HTMLDivElement | null) => {
    if (!el) return
    const center = () => {
      if (el.scrollWidth > el.clientWidth) {
        el.scrollLeft = (el.scrollWidth - el.clientWidth) / 2
      }
    }
    center()
    requestAnimationFrame(center)
    const child = el.firstElementChild as HTMLElement | null
    if (child && typeof ResizeObserver !== 'undefined') {
      const ro = new ResizeObserver(center)
      ro.observe(child)
      return () => ro.disconnect()
    }
  }, [])

  // Fit to screen — calculates the zoom that makes the FULL page visible
  // (no cropping). On mobile the default (zoom=1) fills the height and
  // crops the horizontal edges; this zooms out just enough to show the
  // entire page. On desktop, zoom=1 is already contained, so this resets
  // to 100%.
  const fitToScreen = useCallback(() => {
    if (typeof document === 'undefined') return
    if (isMobile) {
      // Mobile: calculate fit zoom from the strip viewport + current image rect
      const strip = stripScrollRef.current
      const img = strip?.querySelector('img[src*="catalog-pages-web"]') as HTMLImageElement | null
      if (!strip || !img) return
      const imgRect = img.getBoundingClientRect()
      if (!imgRect.width || !imgRect.height) return
      const fitZoom = zoom * Math.min(
        strip.clientWidth / imgRect.width,
        strip.clientHeight / imgRect.height
      )
      setZoom(Math.max(0.2, Math.min(fitZoom, 1)))
      return
    }
    // Desktop
    const stage = document.querySelector('[style*="transform-style"]')
    const img = stage?.querySelector('img[src*="catalog-pages-web"]') as HTMLImageElement | null
    if (!stage || !img || !img.width || !img.height) return
    const fitZoom = Math.min(
      stage.clientWidth / img.width,
      stage.clientHeight / img.height
    )
    setZoom(Math.max(0.2, Math.min(fitZoom, 1)))
  }, [setZoom, isMobile, zoom])

  // ── MOBILE HORIZONTAL STRIP ────────────────────────────────────────
  // All 91 pages are laid out in a single horizontal row (touching, no gap
  // = "ligadas uma do lado da outra"). The user scrolls/swipes sideways to
  // move through the catalog continuously — like ONE long single page. The
  // strip NEVER snaps back to a page edge: the user can stop anywhere and
  // the scroll stays exactly where they left it (no "pulling" / yank).
  //
  // Page tracking (rAF-throttled) updates selectedPage so the slider /
  // control bar reflect the currently-visible page, but does NOT move the
  // scroll. External navigation (arrows / slider / thumbnails) DOES smooth-
  // scroll to center the target page via alignPageCenter.
  //
  // PERFORMANCE OPTIMIZATIONS for fluid scroll:
  //  • Slot position CACHE — slot offsets read once per scroll session, not
  //    per frame (avoids layout thrashing / forced reflow).
  //  • No CSS scroll-snap and no JS snap — nothing fights the user's touch.
  //  • content-visibility:auto on slots — browser skips paint/layout for
  //    off-screen pages.
  //  • PageProductPins only rendered for visible pages (±1 of current).
  //  • Lighter shadow on mobile (shadow-md instead of shadow-xl).
  //  • will-change: scroll-position on the strip container.
  const stripScrollRef = useRef<HTMLDivElement>(null)
  const pageSlotRefs = useRef<Array<HTMLDivElement | null>>([])
  const programmaticScrollRef = useRef(false)
  const scrollRafRef = useRef<number | null>(null)
  // Tracks page changes that originated from the user's swipe (via the
  // scroll handler's rAF tracking). When true, the selectedPage useEffect
  // SKIPS alignPageCenter — so the programmatic smooth-scroll does NOT fight
  // the user's active touch swipe. The strip scrolls freely (no snap); the
  // flag is consumed (reset) on the next external navigation.
  const fromSwipeRef = useRef(false)

  // Cache of slot positions keyed by page number: { left, center }.
  // Populated lazily on first scroll, rebuilt when zoom changes. Eliminates
  // 91 DOM layout reads per scroll frame (the #1 cause of scroll jank).
  const slotCacheRef = useRef<Map<number, { left: number; center: number }>>(new Map())
  // Last measured strip scrollWidth — when images lazy-load, slots expand and
  // scrollWidth grows. We detect that change (1 cheap read per rAF) and
  // invalidate the slot cache so positions stay accurate. Without this, the
  // cache built early (before off-screen images loaded) has WRONG offsetLeft
  // for distant pages, corrupting visible-range computation → pins vanish.
  const lastScrollWidthRef = useRef(0)

  // Actual visible page range — tracks which pages are on screen so pins
  // NEVER disappear mid-swipe. (The old ±1-of-selectedPage window lagged
  // behind the real scroll position by ~1 rAF frame and missed pages during
  // fast swipes, especially at zoom < 100% where slots are narrower than
  // the viewport and 2-3 pages are visible at once.)
  const [visibleRange, setVisibleRange] = useState<{ min: number; max: number }>(
    { min: Math.max(1, selectedPage - 1), max: Math.min(TOTAL_PAGES, selectedPage + 1) }
  )
  // Ref mirror of visibleRange so the scroll handler (which runs in rAF) can
  // read the current value without stale-closure issues and avoid redundant
  // setState calls (only updates when the range actually changes).
  const visibleRangeRef = useRef(visibleRange)
  useEffect(() => { visibleRangeRef.current = visibleRange }, [visibleRange])

  // Invalidate the slot cache when zoom changes (slot widths change).
  useEffect(() => {
    slotCacheRef.current.clear()
    const c = stripScrollRef.current
    if (c) lastScrollWidthRef.current = c.scrollWidth
  }, [zoom])

  // Rebuild the cache from live DOM. Called once per scroll session (when
  // cache is empty) — subsequent scroll frames in the same session read from
  // the cache (pure JS, no layout reads).
  const rebuildSlotCache = useCallback(() => {
    const cache = slotCacheRef.current
    cache.clear()
    for (let p = 1; p <= TOTAL_PAGES; p++) {
      const el = pageSlotRefs.current[p]
      if (el) {
        const left = el.offsetLeft
        cache.set(p, { left, center: left + el.offsetWidth / 2 })
      }
    }
  }, [])

  // Ensure the slot cache reflects the CURRENT layout. Lazy-loaded images
  // expand slots as they decode, so a cache built early (before off-screen
  // images loaded) has wrong offsetLeft for distant pages. We detect this by
  // comparing the strip's scrollWidth to the value captured when the cache
  // was last built — 1 cheap read, rebuild only when layout actually changed.
  const ensureFreshCache = useCallback(() => {
    const container = stripScrollRef.current
    if (!container) return
    const sw = container.scrollWidth
    if (sw !== lastScrollWidthRef.current || slotCacheRef.current.size === 0) {
      lastScrollWidthRef.current = sw
      rebuildSlotCache()
    }
  }, [rebuildSlotCache])

  // Helper: find the page whose slot-center is closest to the viewport center.
  // Used for page tracking (indicator / slider). Uses cache (no DOM reads).
  const findClosestPage = useCallback(() => {
    const container = stripScrollRef.current
    if (!container) return { page: 1 }
    ensureFreshCache()
    const centerX = container.scrollLeft + container.clientWidth / 2
    let closest = 1, closestDist = Infinity
    for (const [p, { center }] of slotCacheRef.current) {
      const dist = Math.abs(center - centerX)
      if (dist < closestDist) { closestDist = dist; closest = p }
    }
    return { page: closest }
  }, [ensureFreshCache])

  // Helper: compute the actual range of pages visible in the viewport right
  // now (slots that intersect [scrollLeft, scrollLeft + clientWidth]), with a
  // ±1 buffer for partial entry / fast swipes. This is what drives `showPins`
  // so pins are mounted BEFORE a page finishes sliding in and stay mounted
  // until it's fully gone — no flicker / disappearance mid-swipe.
  const computeVisibleRange = useCallback(() => {
    const container = stripScrollRef.current
    if (!container) return null
    ensureFreshCache()
    const cache = slotCacheRef.current
    const viewLeft = container.scrollLeft
    const viewRight = viewLeft + container.clientWidth
    let min = TOTAL_PAGES, max = 1
    for (const [p, { left: slotLeft, center }] of cache) {
      const slotWidth = (center - slotLeft) * 2
      const slotRight = slotLeft + slotWidth
      // Slot intersects the viewport?
      if (slotRight >= viewLeft && slotLeft <= viewRight) {
        if (p < min) min = p
        if (p > max) max = p
      }
    }
    if (min > max) return null
    // ±1 buffer so pins mount just before a page enters and unmount just
    // after it leaves — never visible-flicker.
    return {
      min: Math.max(1, min - 1),
      max: Math.min(TOTAL_PAGES, max + 1),
    }
  }, [ensureFreshCache])

  // Sync visibleRange from the current scroll position. No-op if the range
  // hasn't changed (avoids needless re-renders — PageImage is memo'd so only
  // boundary slots re-render anyway, but we still skip the setState).
  const syncVisibleRange = useCallback(() => {
    const range = computeVisibleRange()
    if (!range) return
    const prev = visibleRangeRef.current
    if (prev.min === range.min && prev.max === range.max) return
    visibleRangeRef.current = range
    setVisibleRange(range)
  }, [computeVisibleRange])

  // Helper: smooth-scroll the strip so the given page's CENTER aligns with
  // the viewport's CENTER ("centralized"). Used for initial load and for
  // external navigation (arrows / slider / thumbnails) — the target page
  // lands centered on screen. Sets programmaticScrollRef to suppress onScroll
  // during the smooth scroll. Reads the slot's offsetLeft + offsetWidth
  // directly from the DOM (1 read, not 91) for accuracy — content-visibility:
  // auto can give off-screen slots wrong sizes in the cache, but reading a
  // single slot forces it to render.
  const alignPageCenter = useCallback((page: number) => {
    const container = stripScrollRef.current
    const el = pageSlotRefs.current[page]
    if (!container || !el) return
    // Center the slot in the viewport: scroll so the slot's midpoint lands
    // at the viewport's midpoint. Clamped to >= 0 (first page can't scroll
    // left of 0).
    const target = Math.max(0, el.offsetLeft + el.offsetWidth / 2 - container.clientWidth / 2)
    if (Math.abs(target - container.scrollLeft) < 8) return
    programmaticScrollRef.current = true
    container.scrollTo({ left: target, behavior: 'smooth' })
    window.setTimeout(() => { programmaticScrollRef.current = false }, 600)
  }, [])

  // During scroll: throttled page detection ONLY (updates selectedPage so
  // the slider / control bar track the visible page). NO snap — the catalog
  // scrolls freely like one continuous long page. The user can stop anywhere
  // and the strip stays exactly where they left it (no "pulling back" / yank
  // to a page edge).
  //
  // Page changes from a swipe set fromSwipeRef so the selectedPage useEffect
  // does NOT fire alignPageCenter and fight the user's touch. External
  // navigation (arrows / slider / thumbnails) still centers the target page
  // via alignPageCenter — that's an explicit user action.
  const triggerSwipeAnimation = useCatalogStore(s => s.triggerSwipeAnimation)

  const handleStripScroll = useCallback(() => {
    if (programmaticScrollRef.current) return
    if (scrollRafRef.current) cancelAnimationFrame(scrollRafRef.current)
    scrollRafRef.current = requestAnimationFrame(() => {
      // Update the visible page range FIRST so pins are mounted for any page
      // currently on screen (prevents pins disappearing mid-swipe). The cache
      // freshness check (lazy-image load detection) runs inside.
      syncVisibleRange()
      const { page: closest } = findClosestPage()
      if (closest !== useCatalogStore.getState().selectedPage) {
        fromSwipeRef.current = true
        goToPage(closest)
        // Dispara a animação dos pins ao mudar de página via swipe no mobile
        triggerSwipeAnimation()
      }
    })
  }, [goToPage, findClosestPage, syncVisibleRange, triggerSwipeAnimation])

  // When selectedPage changes externally (arrows / slider / thumbnails),
  // smooth-scroll the strip so that page's CENTER aligns with the
  // viewport's CENTER ("catalog more centralized on mobile").
  //
  // SKIPS the alignment when the change originated from the user's swipe
  // (fromSwipeRef) — in that case the user is freely scrolling the strip
  // (treated as one continuous long page) and we must NOT fight their touch
  // with a programmatic smooth-scroll. There is NO snap after the swipe
  // ends: the strip stays exactly where the user left it. The flag is
  // consumed (reset) here so the next external change realigns normally.
  useEffect(() => {
    if (!isMobile) return
    if (fromSwipeRef.current) {
      fromSwipeRef.current = false
      return
    }
    alignPageCenter(selectedPage)
    // After the smooth-scroll settles, recompute the visible range so pins
    // reflect the new position (the scroll handler's rAF covers active
    // scrolling, but programmatic smooth-scroll fires onScroll events too).
    const t = window.setTimeout(syncVisibleRange, 650)
    return () => window.clearTimeout(t)
  }, [selectedPage, isMobile, alignPageCenter, syncVisibleRange])

  // Initial alignment on mobile mount — center the current page in the
  // viewport. Re-runs when zoom changes (slot positions shift with zoom).
  // Uses rAF + timeout because slot widths may not be final until preview
  // images load.
  useEffect(() => {
    if (!isMobile) return
    const align = () => {
      alignPageCenter(useCatalogStore.getState().selectedPage)
      syncVisibleRange()
    }
    const raf = requestAnimationFrame(() => requestAnimationFrame(align))
    const t = window.setTimeout(align, 500)
    return () => { cancelAnimationFrame(raf); window.clearTimeout(t) }
  }, [isMobile, zoom, alignPageCenter, syncVisibleRange])

  // Page-flip animation variants — direction-aware.
  // enter: new page slides in from the side it's coming from (right for "next")
  // exit:  old page slides out to the opposite side (left for "next")
  // The exiting page uses the SAME flipDir (via AnimatePresence custom prop)
  // so the exit matches the navigation direction.
  //
  // MOBILE: fade-only (no slide/rotate) + mode="wait" so only ONE page is
  // visible at a time — no dual-page sliding animation. This is cleaner on
  // small screens where two sliding pages would feel cramped/confusing.
  //
  // DESKTOP: 3D page-turn (slide + rotateY + scale).
  //
  // REDUCED MOTION: fade only.
  const pageVariants: Variants = prefersReduced || isMobile
    ? {
        enter: { opacity: 0 },
        center: { opacity: 1 },
        exit: { opacity: 0 },
      }
    : {
        enter: (dir: FlipDir | null) => ({
          opacity: 0,
          x: dir === 'prev' ? -100 : 100,
          rotateY: dir === 'prev' ? 18 : -18,
          scale: 0.9,
        }),
        center: { opacity: 1, x: 0, rotateY: 0, scale: 1 },
        exit: (dir: FlipDir | null) => ({
          opacity: 0,
          x: dir === 'prev' ? 100 : -100,
          rotateY: dir === 'prev' ? -18 : 18,
          scale: 0.9,
        }),
      }

  return (
    <div className="flex-1 min-h-0 flex flex-col bg-neutral-100 relative">
        {/* Page viewing area — no horizontal padding on mobile so the image
            fills the full viewport. Arrows overlay the image edges. */}
      <div
        className="flex-1 min-h-0 relative flex items-center justify-center overflow-hidden px-0 sm:px-4 py-0 sm:py-4"
        style={{ perspective: '1800px' }}
      >
        {/* Left arrow — hidden on mobile, visible on desktop */}
        <button
          onClick={prevPage}
          disabled={!canPrev}
          className="absolute left-1 sm:left-4 z-10 w-9 h-9 sm:w-12 sm:h-12 rounded-full bg-white shadow-lg hover:bg-amber-50 hover:scale-105 active:scale-95 hidden sm:flex items-center justify-center text-neutral-800 disabled:opacity-0 disabled:pointer-events-none transition-all"
          aria-label="Página anterior"
        >
          <ChevronLeft className="w-5 h-5 sm:w-6 sm:h-6" />
        </button>

        {isMobile ? (
          /* MOBILE: horizontal continuous strip — all pages side by side,
             touching ("ligadas"). Performance: no scroll-snap (avoids
             stutter), content-visibility:auto on slots (skip off-screen
             paint), will-change:scroll-position (GPU hint), pins only on
             visible pages. */
          <div
            ref={stripScrollRef}
            onScroll={handleStripScroll}
            className="absolute inset-0 overflow-x-auto overflow-y-auto flex"
            style={{
              WebkitOverflowScrolling: 'touch',
              willChange: 'scroll-position',
            }}
          >
            {Array.from({ length: TOTAL_PAGES }, (_, i) => i + 1).map(page => (
              <div
                key={page}
                ref={el => { pageSlotRefs.current[page] = el }}
                className="flex-shrink-0 flex items-center justify-center"
                style={{
                  // Slot fills the strip height exactly (100%) so there is
                  // NEVER a vertical gap between the page image and the
                  // bottom control bar, regardless of zoom or chrome height.
                  // The image inside scales with zoom (height = 100% * zoom):
                  //  • zoom = 1 → image fills the slot (no gap, no overflow)
                  //  • zoom > 1 → image overflows (vertical pan)
                  //  • zoom < 1 → image shrinks, centered (zoomed-out view)
                  height: '100%',
                }}
              >
                <PageImage
                  page={page}
                  lazy
                  mobileZoom={zoom}
                  showPins={page >= visibleRange.min && page <= visibleRange.max}
                />
              </div>
            ))}
          </div>
        ) : (
          /* DESKTOP: 3D page-turn stage (AnimatePresence, one page at a time). */
          <AnimatePresence custom={flipDir} mode="sync" initial={false}>
            <motion.div
              key={currentPage}
              ref={setStageRef}
              className="absolute inset-0 flex items-center overflow-x-auto overflow-y-hidden sm:overflow-x-hidden sm:justify-center"
              style={{
                transformStyle: 'preserve-3d',
                WebkitOverflowScrolling: 'touch',
              }}
              custom={flipDir}
              variants={pageVariants}
              initial="enter"
              animate="center"
              exit="exit"
              transition={{
                duration: 0.45,
                ease: [0.22, 1, 0.36, 1],
              }}
            >
              {/* Zoom wrapper — separated from the flip animation transform
                  so zoom changes don't interfere with the page-turn animation.
                  margin:auto centers the image horizontally when it fits
                  (zoomed out) and allows scroll-pan when it overflows. */}
              <div
                className="relative inline-flex"
                style={{
                  transform: `scale(${zoom})`,
                  transformOrigin: 'center center',
                  transition: 'transform 0.2s ease',
                  margin: 'auto',
                }}
              >
                <PageImage page={currentPage} />
                {/* Sweeping shadow overlay during page flip — adds depth realism.
                    Runs on enter (page landing) via CSS animation. On exit, the
                    page fades out with the motion.div transition. */}
                {!prefersReduced && (
                  <div
                    aria-hidden
                    className="pointer-events-none absolute inset-0 z-20"
                    style={{
                      background: 'linear-gradient(90deg, rgba(0,0,0,0) 0%, rgba(0,0,0,0.18) 50%, rgba(0,0,0,0) 100%)',
                      animation: flipDir === 'prev'
                        ? `shadowSweepPrev 0.45s cubic-bezier(0.22, 1, 0.36, 1)`
                        : `shadowSweepNext 0.45s cubic-bezier(0.22, 1, 0.36, 1)`,
                      mixBlendMode: 'multiply',
                    }}
                  />
                )}
              </div>
            </motion.div>
          </AnimatePresence>
        )}

        {/* Right arrow — hidden on mobile, visible on desktop */}
        <button
          onClick={nextPage}
          disabled={!canNext}
          className="absolute right-1 sm:right-4 z-10 w-9 h-9 sm:w-12 sm:h-12 rounded-full bg-white shadow-lg hover:bg-amber-50 hover:scale-105 active:scale-95 hidden sm:flex items-center justify-center text-neutral-800 disabled:opacity-0 disabled:pointer-events-none transition-all"
          aria-label="Próxima página"
        >
          <ChevronRight className="w-5 h-5 sm:w-6 sm:h-6" />
        </button>

        {/* Zoom controls — visible on BOTH mobile and desktop. On mobile
            (strip mode) zoom scales the image height; on desktop zoom
            scales the image via transform. Touch-friendly (w-9 h-9) on
            mobile, compact (w-7 h-7) on desktop. */}
        <div className="flex absolute top-2 right-2 sm:top-3 sm:right-3 z-30 items-center gap-1 bg-white/90 backdrop-blur rounded-full shadow px-2 py-1.5 sm:px-1.5 sm:py-1">
          <button
            onClick={() => setZoom(Math.max(zoom - 0.25, 0.2))}
            disabled={zoom <= 0.2}
            className="w-9 h-9 sm:w-7 sm:h-7 rounded-full hover:bg-neutral-100 active:bg-neutral-200 flex items-center justify-center disabled:opacity-30 transition-colors"
            aria-label="Diminuir zoom"
          >
            <ZoomOut className="w-4 h-4" />
          </button>
          <span className="text-[11px] font-mono text-neutral-600 w-9 text-center tabular-nums">{Math.round(zoom * 100)}%</span>
          <button
            onClick={() => setZoom(Math.min(zoom + 0.25, 2.5))}
            disabled={zoom >= 2.5}
            className="w-9 h-9 sm:w-7 sm:h-7 rounded-full hover:bg-neutral-100 active:bg-neutral-200 flex items-center justify-center disabled:opacity-30 transition-colors"
            aria-label="Aumentar zoom"
          >
            <ZoomIn className="w-4 h-4" />
          </button>
          <button
            onClick={fitToScreen}
            className="ml-0.5 w-9 h-9 sm:w-7 sm:h-7 rounded-full hover:bg-neutral-100 active:bg-neutral-200 flex items-center justify-center transition-colors"
            aria-label="Ajustar à tela"
            title="Ajustar à tela"
          >
            <Maximize2 className="w-3.5 h-3.5" />
          </button>
          {zoom !== 1 && (
            <button
              onClick={() => setZoom(1)}
              className="ml-0.5 px-2 h-9 sm:h-7 rounded-full bg-neutral-100 hover:bg-neutral-200 active:bg-neutral-300 text-[10px] font-bold text-neutral-600 transition-colors"
              aria-label="Zoom 100%"
            >
              100%
            </button>
          )}
        </div>
      </div>

      {/* Bottom control bar: slider + thumbnail toggle */}
      <div className="flex-shrink-0 bg-white border-t border-neutral-200">
        <div className="px-3 sm:px-5 py-2.5 flex items-center gap-3">
          <span className="text-[11px] sm:text-xs font-mono text-neutral-500 whitespace-nowrap tabular-nums">
            {currentPage} <span className="text-neutral-300">/</span> {TOTAL_PAGES}
          </span>
          <input
            type="range"
            min="1"
            max={TOTAL_PAGES}
            value={selectedPage}
            onChange={(e) => goToPageWithDir(parseInt(e.target.value, 10))}
            className="flex-1 accent-amber-400 h-1.5"
            aria-label="Navegar páginas"
          />
          {/* O modo de edição de pins foi desativado — os pins ficam fixos
              nas posições definidas pelo admin. O botão "Editar pins" e o
              banner de hint foram removidos do control bar. */}
          <button
            onClick={() => setShowThumbnails(!showThumbnails)}
            className={`flex items-center gap-1.5 px-2.5 sm:px-3 py-1.5 rounded-full text-xs font-semibold transition-colors flex-shrink-0 ${
              showThumbnails ? 'bg-amber-400 text-black' : 'bg-neutral-100 text-neutral-600 hover:bg-neutral-200'
            }`}
            aria-label="Ver todas as páginas"
          >
            <Grid3x3 className="w-3.5 h-3.5" />
            <span className="hidden sm:inline">Páginas</span>
          </button>
        </div>

        {/* Thumbnail strip — virtualized via content-visibility */}
        {showThumbnails && (
          <div className="overflow-hidden border-t border-neutral-100" style={{ animation: 'fadeIn 0.2s ease-out' }}>
            <div className="overflow-x-auto overflow-y-hidden">
              <div className="flex gap-2 p-3">
                {Array.from({ length: TOTAL_PAGES }, (_, i) => i + 1).map(page => (
                  <Thumbnail
                    key={page}
                    page={page}
                    active={page === selectedPage}
                    onClick={() => goToPageWithDir(page)}
                  />
                ))}
              </div>
            </div>
          </div>
        )}
      </div>

      <style jsx global>{`
        /* Shadow sweep — a soft gradient that moves across during the flip,
           simulating page thickness. Only runs on enter (page landing). */
        @keyframes shadowSweepNext {
          0% { opacity: 0; transform: translateX(-30%); }
          40% { opacity: 1; }
          100% { opacity: 0; transform: translateX(30%); }
        }
        @keyframes shadowSweepPrev {
          0% { opacity: 0; transform: translateX(30%); }
          40% { opacity: 1; }
          100% { opacity: 0; transform: translateX(-30%); }
        }
        @keyframes fadeIn {
          from { opacity: 0; }
          to { opacity: 1; }
        }
        @keyframes pinPulse {
          0% { transform: scale(1); opacity: 0.9; }
          70% { transform: scale(1.8); opacity: 0; }
          100% { transform: scale(1.8); opacity: 0; }
        }
      `}</style>
    </div>
  )
}

// Memoized PageImage with blur-up placeholder.
// `lazy`: when true (mobile strip), the full-res image uses loading="lazy" +
// low fetchPriority so off-screen pages don't all decode at once. The tiny
// preview stays eager so every slot gets its correct width immediately
// (prevents the strip layout from collapsing before images load).
const PageImage = memo(function PageImage({ page, lazy = false, mobileZoom = 1, showPins = true }: { page: number; lazy?: boolean; mobileZoom?: number; showPins?: boolean }) {
  const [loaded, setLoaded] = useState(false)

  // Cached / already-decoded images can be `complete` BEFORE React attaches
  // the onLoad handler — in that case onLoad never fires, `loaded` stays
  // false, and the blurred preview placeholder is shown forever (even
  // though the sharp full-res image is ready in the DOM at opacity-0).
  //
  // We catch this by checking `img.complete && naturalWidth > 0` in a ref
  // callback, which runs at commit time (after the browser has had a
  // chance to decode cached images). For images still loading from the
  // network, the ref callback sees `complete === false` and the onLoad
  // handler picks up the job when the image finishes.
  const setImgRef = useCallback((el: HTMLImageElement | null) => {
    if (el && el.complete && el.naturalWidth > 0) {
      setLoaded(true)
    }
  }, [])

  return (
    <div
      className="relative inline-flex h-full items-center justify-center shadow-md sm:shadow-xl sm:h-auto bg-white overflow-hidden"
      // On mobile, the wrapper HEIGHT scales with zoom so the wrapper
      // always matches the image. The pin layer (absolute inset-0) then
      // covers exactly the image — pins stay fixed on the image at any
      // zoom, matching desktop. At zoom > 100% the wrapper grows beyond
      // the slot (vertical scroll via the strip's overflow-y-auto), so
      // nothing is clipped. At zoom < 100% the wrapper shrinks (centered
      // in the slot by items-center).
      // Only applied when mobileZoom !== 1 (desktop passes mobileZoom=1 →
      // no inline style → sm:h-auto takes over on desktop).
      style={mobileZoom !== 1 ? { height: `calc(100% * ${mobileZoom})` } : undefined}
    >
      {/* Blur-up placeholder: tiny preview image stretched + blurred.
          Reduced from blur(12px) to blur(6px) + scale(1.03) so the
          placeholder is recognizable during the brief loading window
          rather than an opaque smudge.
          MOBILE: h-full = 100% of the wrapper (= slot * zoom). w-auto =
          intrinsic width (maintains aspect ratio).
          Desktop: sm:h-auto + sm:max-h cap the intrinsic size. */}
      {!loaded && (
        <img
          src={previewSrc(page)}
          alt=""
          aria-hidden
          className="block object-contain h-full w-auto max-w-none sm:h-auto sm:max-h-[calc(100vh-8.5rem)] sm:max-w-[calc(100vw-6rem)]"
          style={{
            filter: 'blur(6px)',
            transform: 'scale(1.03)',
          }}
          draggable={false}
        />
      )}
      {/* Full-resolution WebP image */}
      <img
        ref={setImgRef}
        src={pageSrc(page)}
        alt={`Página ${page} do catálogo Abelha Rainha`}
        className={`block object-contain h-full w-auto max-w-none sm:h-auto sm:max-h-[calc(100vh-8.5rem)] sm:max-w-[calc(100vw-6rem)] transition-opacity duration-200 ${loaded ? 'opacity-100' : 'absolute inset-0 opacity-0'}`}
        draggable={false}
        decoding="async"
        loading={lazy ? 'lazy' : undefined}
        fetchPriority={lazy ? 'low' : 'high'}
        onLoad={() => setLoaded(true)}
      />
      {/* page number badge */}
      <span className="absolute bottom-1.5 right-1.5 text-[10px] font-mono font-bold text-white bg-black/60 px-1.5 py-0.5 rounded pointer-events-none">
        {page}
      </span>
      {/* Buy pins — one per product on this page, over the image.
          Only rendered for visible pages (showPins) to avoid mounting 91
          pin layers that each subscribe to store state.
          The pin layer (absolute inset-0) covers the wrapper, which matches
          the image size (wrapper height = slot * zoom on mobile = image
          height). So pins stay fixed on the image at any zoom. */}
      {showPins && <PageProductPins page={page} />}

      {/* Swipe tutorial — only on mobile, page 1, and only if not zoomed in too much */}
      {page === 1 && mobileZoom <= 1.2 && (
        <div className="sm:hidden absolute inset-0 flex items-center justify-center pointer-events-none z-30">
          <div className="flex flex-col items-center gap-2">
            <div className="w-12 h-12 rounded-full bg-white/20 backdrop-blur-sm border border-white/30 flex items-center justify-center animate-swipe-tutorial">
              <Hand className="w-6 h-6 text-white drop-shadow-lg" />
            </div>
            <div className="bg-black/40 backdrop-blur-md px-3 py-1 rounded-full border border-white/20 text-[10px] font-bold text-white uppercase tracking-widest animate-pulse">
              Deslize para ver
            </div>
          </div>
        </div>
      )}
    </div>
  )
})

// Memoized Thumbnail — uses content-visibility to skip rendering off-screen items
const Thumbnail = memo(function Thumbnail({
  page,
  active,
  onClick,
}: {
  page: number
  active: boolean
  onClick: () => void
}) {
  return (
    <button
      onClick={onClick}
      className={`relative flex-shrink-0 w-16 sm:w-20 aspect-[3/4] rounded-md overflow-hidden border-2 transition-all ${
        active ? 'border-amber-400 ring-2 ring-amber-200 scale-105' : 'border-transparent opacity-70 hover:opacity-100'
      }`}
      style={{
        // Skip rendering work for off-screen thumbnails
        contentVisibility: 'auto',
        containIntrinsicSize: '64px 85px',
      }}
    >
      <img
        src={thumbSrc(page)}
        alt={`Página ${page}`}
        className="w-full h-full object-cover"
        loading="lazy"
        decoding="async"
      />
      <span className="absolute bottom-0 left-0 right-0 text-[9px] text-white bg-black/70 text-center py-0.5 font-mono">
        {page}
      </span>
    </button>
  )
})
