import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useQuery } from '@tanstack/react-query'
import { useHotkeys } from 'react-hotkeys-hook'
import { X, Info } from 'lucide-react'
import { usePhotoStore } from '../../store/photoStore'
import { useFilterStore } from '../../store/filterStore'
import { usePhotosQuery } from '../../hooks/usePhotosQuery'
import { photos as photosApi } from '../../services/api'
import { findSearchMatch } from '../../lib/searchMatch'
import type { Photo } from '../../types/photo'
import { PreviewImage } from './PreviewImage'
import { PreviewFilmstrip } from './PreviewFilmstrip'
import { getPreviewImageSrc, isVideo } from './previewSrc'
import { PhotoInfoPanel } from '../sidebar/PhotoInfoPanel'
import { KeyboardHints } from '../KeyboardHints'
import { Button } from '@/components/ui/button'
import { cn } from '@/lib/utils'

export function PreviewView() {
  const activePhotoId = usePhotoStore((s) => s.activePhotoId)
  const setActivePhoto = usePhotoStore((s) => s.setActivePhoto)
  const visiblePhotoIds = usePhotoStore((s) => s.visiblePhotoIds)

  const containerRef = useRef<HTMLDivElement>(null)
  const previouslyFocusedRef = useRef<HTMLElement | null>(null)
  const [infoPanelOpen, setInfoPanelOpen] = useState(true)

  // Track the photo we OPENED preview on as a defensive fallback only.
  // On close we restore the LAST viewed photo so the grid focus matches
  // what the user just saw (filmstrip/arrow nav can land them anywhere).
  const openOriginRef = useRef<string | null>(
    activePhotoId ?? usePhotoStore.getState().activePhotoId
  )
  const closePreview = useCallback(() => {
    const id =
      usePhotoStore.getState().activePhotoId ?? openOriginRef.current
    usePhotoStore.setState({
      viewMode: 'grid',
      activePhotoId: id,
      rangeStartId: id,
      selectedPhotos: id ? [id] : [],
      previewOriginPhotoId: null,
    })
  }, [])

  // Same hook Timeline uses, so we share one cache entry rather than looking
  // it up by key (which broke when the key gained the filter params).
  const { data: rawPhotos = [] } = usePhotosQuery()

  // Walk the timeline's visible-order sequence (published by Timeline
  // into the photo store), which respects tag-grouping and any other
  // grid-layout rearrangement. Falls back to the raw photos list when
  // the sequence isn't populated yet — relevant on a fresh page load
  // where the user opened preview before the timeline mounted.
  const photos: Photo[] = useMemo(() => {
    if (visiblePhotoIds.length === 0) return rawPhotos
    const byId = new Map(rawPhotos.map((p) => [p.id, p]))
    const out: Photo[] = []
    for (const id of visiblePhotoIds) {
      const p = byId.get(id)
      if (p) out.push(p)
    }
    return out
  }, [visiblePhotoIds, rawPhotos])

  const currentIndex = activePhotoId
    ? photos.findIndex((p) => p.id === activePhotoId)
    : 0
  const safeIndex = currentIndex < 0 ? 0 : currentIndex

  // Fallback fetch: when the preview is opened for a photo that isn't in
  // the timeline query result (e.g. clicked from the Map view, where the
  // active section's filter excludes it), look it up by id directly.
  // PhotoInfoPanel runs the same query under the same key, so they share
  // one cache entry — no extra request.
  const photoInListById = activePhotoId
    ? photos.find((p) => p.id === activePhotoId)
    : undefined
  const { data: standalonePhoto } = useQuery<Photo>({
    queryKey: ['photo', activePhotoId],
    queryFn: () => photosApi.get(activePhotoId as string),
    enabled: !!activePhotoId && !photoInListById,
    staleTime: 60_000,
  })

  const currentPhoto: Photo | undefined =
    photoInListById ?? photos[safeIndex] ?? standalonePhoto

  // Carry the timeline's search-match chip into preview so the user
  // doesn't lose the "why did this photo come back" context when they
  // zoom in. Pure recompute — same helper the thumbnail uses.
  const searchQuery = useFilterStore((s) => s.q)
  const searchMatch =
    currentPhoto && searchQuery.trim()
      ? findSearchMatch(currentPhoto, searchQuery)
      : null

  // Keep the latest photos array + active id in a ref so the keyboard
  // handlers ALWAYS read the freshest state. Without this, react-hotkeys-
  // hook can fire a closure that captured an older photos array (e.g.
  // the empty initial render before visiblePhotoIds was applied) and
  // arrow nav lands on the wrong photo or no-ops.
  const navRef = useRef({ photos, activePhotoId })
  navRef.current = { photos, activePhotoId }

  const goPrev = useCallback(() => {
    const { photos: ps, activePhotoId: aid } = navRef.current
    if (ps.length === 0) return
    const idx = aid ? ps.findIndex((p) => p.id === aid) : 0
    const safe = idx < 0 ? 0 : idx
    const next = Math.max(0, safe - 1)
    setActivePhoto(ps[next].id)
  }, [setActivePhoto])

  const goNext = useCallback(() => {
    const { photos: ps, activePhotoId: aid } = navRef.current
    if (ps.length === 0) return
    const idx = aid ? ps.findIndex((p) => p.id === aid) : 0
    const safe = idx < 0 ? 0 : idx
    const next = Math.min(ps.length - 1, safe + 1)
    setActivePhoto(ps[next].id)
  }, [setActivePhoto])

  // Preview-scoped hotkeys: only mounted while PreviewView is rendered.
  // The handlers themselves are stable (refs internally) so the deps
  // array stays empty — useHotkeys won't have to re-bind on every render.
  //
  // Escape is intentionally NOT bound through useHotkeys here. The
  // grid-level Timeline component listens for Escape on `window` to
  // clear the current selection, and react-hotkeys-hook binds at
  // document level — so a single Esc keypress would land in BOTH
  // handlers. The grid handler would then wipe the selection we just
  // restored from the preview origin. Instead, escape is handled by
  // the dialog's onKeyDown below, which runs first (lower in the
  // bubble chain) and calls stopPropagation so the keypress never
  // reaches window.
  useHotkeys('left', goPrev, { preventDefault: true })
  useHotkeys('right', goNext, { preventDefault: true })
  useHotkeys('i', () => setInfoPanelOpen((v) => !v), { preventDefault: true })

  // Preload the immediate neighbors so arrow nav feels instant. Skip videos
  // (browsers can't preload them via Image()) and skip when at the edges.
  useEffect(() => {
    const neighbors: Photo[] = []
    if (safeIndex > 0) neighbors.push(photos[safeIndex - 1])
    if (safeIndex < photos.length - 1) neighbors.push(photos[safeIndex + 1])
    for (const p of neighbors) {
      if (isVideo(p)) continue
      const img = new Image()
      img.src = getPreviewImageSrc(p)
    }
  }, [safeIndex, photos])

  // Focus trap: focus the preview container on mount, restore focus on
  // unmount. The container is keyboard-focusable (tabIndex=-1) so screen
  // readers and tab navigation stay scoped here.
  useEffect(() => {
    previouslyFocusedRef.current = document.activeElement as HTMLElement | null
    containerRef.current?.focus()
    return () => {
      previouslyFocusedRef.current?.focus?.()
    }
  }, [])

  // Trap Tab inside the dialog so users can't accidentally tab into the
  // hidden grid behind. Simple cycle implementation. Also intercepts
  // Escape and stops propagation before the keypress reaches the
  // window-level handler in Timeline (which would otherwise wipe the
  // selection we just restored to the entry photo).
  const handleKeyDown = (e: React.KeyboardEvent) => {
    if (e.key === 'Escape') {
      e.preventDefault()
      e.stopPropagation()
      closePreview()
      return
    }
    if (e.key !== 'Tab') return
    const root = containerRef.current
    if (!root) return
    const focusable = root.querySelectorAll<HTMLElement>(
      'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
    )
    if (focusable.length === 0) {
      e.preventDefault()
      root.focus()
      return
    }
    const first = focusable[0]
    const last = focusable[focusable.length - 1]
    const active = document.activeElement as HTMLElement | null
    if (e.shiftKey && active === first) {
      e.preventDefault()
      last.focus()
    } else if (!e.shiftKey && active === last) {
      e.preventDefault()
      first.focus()
    }
  }

  if (!currentPhoto) {
    return (
      <div
        ref={containerRef}
        role="dialog"
        aria-modal="true"
        aria-label="Photo preview"
        tabIndex={-1}
        className="fixed inset-0 z-[1000] flex flex-col items-center justify-center bg-black text-text-muted outline-none"
      >
        <div>No photo to display</div>
        <Button variant="outline" size="sm" className="mt-4" onClick={closePreview}>
          Close
        </Button>
      </div>
    )
  }

  return (
    <div
      ref={containerRef}
      role="dialog"
      aria-modal="true"
      aria-label={`Photo preview: ${currentPhoto.filename}`}
      tabIndex={-1}
      onKeyDown={handleKeyDown}
      className="fixed inset-0 z-[1000] flex bg-black outline-none"
    >
      {/* Main column — image + filmstrip */}
      <div className="relative flex min-w-0 flex-1 flex-col">
        {/* Filename + counter + (optional) search match chip */}
        <div className="absolute left-3 top-3 z-10 max-w-[60%] rounded bg-black/60 px-3 py-1.5 text-xs text-white">
          <div className="truncate font-mono">{currentPhoto.filename}</div>
          <div className="text-text-muted">
            {safeIndex + 1} / {photos.length}
          </div>
          {searchMatch && (
            <div
              className="mt-1 flex items-center gap-1.5 text-[11px]"
              title={`Matched on ${searchMatch.label.toLowerCase()}: ${searchMatch.excerpt}`}
            >
              <span className="shrink-0 rounded-sm bg-primary/80 px-1 text-[9px] font-semibold uppercase tracking-wider">
                {searchMatch.label}
              </span>
              <span className="truncate">
                {searchMatch.matchLength > 0 ? (
                  <>
                    {searchMatch.excerpt.slice(0, searchMatch.matchStart)}
                    <mark className="rounded-sm bg-amber-400/90 px-0.5 font-semibold text-black">
                      {searchMatch.excerpt.slice(
                        searchMatch.matchStart,
                        searchMatch.matchStart + searchMatch.matchLength,
                      )}
                    </mark>
                    {searchMatch.excerpt.slice(
                      searchMatch.matchStart + searchMatch.matchLength,
                    )}
                  </>
                ) : (
                  searchMatch.excerpt
                )}
              </span>
            </div>
          )}
        </div>

        {/* Top-right action buttons */}
        <div className="absolute right-3 top-3 z-10 flex items-center gap-2">
          <Button
            size="icon"
            onClick={() => setInfoPanelOpen((v) => !v)}
            className={cn(
              'h-9 w-9 rounded-full bg-black/60 text-white hover:bg-black/80',
              infoPanelOpen && 'ring-2 ring-primary'
            )}
            title="Toggle info panel (I)"
            aria-label="Toggle info panel"
            aria-pressed={infoPanelOpen}
          >
            <Info className="h-5 w-5" />
          </Button>
          <Button
            size="icon"
            onClick={closePreview}
            className="h-9 w-9 rounded-full bg-black/60 text-white hover:bg-black/80"
            title="Close (Esc)"
            aria-label="Close preview"
          >
            <X className="h-5 w-5" />
          </Button>
        </div>

        <PreviewImage photo={currentPhoto} />

        {/* Shortcut hints — sits above the filmstrip and centers to the
         *  image column (not the viewport), so the optional info panel
         *  on the right doesn't push it off-axis. */}
        <div className="pointer-events-none absolute inset-x-0 bottom-24 z-10">
          <KeyboardHints />
        </div>

        <PreviewFilmstrip
          photos={photos}
          currentIndex={safeIndex}
          onSelect={setActivePhoto}
        />
      </div>

      {/* Right info panel — slides in/out, mirrors the grid right sidebar
       *  but lives inside the preview overlay so it isn't covered by it. */}
      {infoPanelOpen && (
        <aside className="w-80 shrink-0 overflow-hidden border-l border-border bg-surface">
          <PhotoInfoPanel photoId={currentPhoto.id} />
        </aside>
      )}
    </div>
  )
}
