import { memo, useState, useEffect, useCallback, useRef } from 'react'
import {
  Star,
  Trash2,
  RefreshCw,
  Copy,
  AlertTriangle,
  Users,
  ShoppingBasket,
} from 'lucide-react'
import clsx from 'clsx'
import { photos as photosApi } from '../../services/api'
import type { Photo } from '../../types/photo'
import { usePhotoStore } from '../../store/photoStore'
import { useFilterStore } from '../../store/filterStore'
import { COLOR_LABEL_OPTIONS } from '../../constants/colorLabels'
import { findSearchMatch } from '../../lib/searchMatch'

/** Custom MIME used by HeapsPanel to recognise our drag payload. */
export const PHOTO_DRAG_MIME = 'application/x-mulita-photos'

// ── Thumbnail badge family ───────────────────────────────────────────────
// Every ornament that overlays a thumbnail — here and in other views that
// wrap PhotoThumbnail (e.g. DuplicatesView) — must compose these classes so
// the set reads as one coherent system. Shape, height, typography are
// fixed; variants set the fill + chiseled-pixel frame:
//   PRIMARY  → user-affirmed state (selection, rating, active-heap)
//   NEUTRAL  → informational metadata (duplicate flag, discard, file type, dims)
//   PICK     → auto-suggested "best" in duplicate groups
//
// Frame is a single box-shadow stack rather than a Tailwind `ring` so the
// 1px outline, the subtle inset top highlight, and the drop shadow all
// live in one paint, and variants can tint the inset highlight to suit
// their fill. Corners are `rounded-sm` (2px) to echo the pixel-art theme
// used in the TopBar and ActiveHeapCard.
export const THUMB_BADGE_BASE =
  'inline-flex h-5 items-center gap-1 rounded-sm px-1.5 text-[10px] font-semibold leading-none text-white'
/** Square icon-only variant — compose alongside THUMB_BADGE_BASE. */
export const THUMB_BADGE_SQUARE = 'w-5 justify-center !px-0'
/** Standard icon size for any lucide glyph inside a badge. */
export const THUMB_BADGE_ICON = 'h-3 w-3'
/** Chiseled-pixel frame: 1px outline + inset top highlight + drop shadow.
 *  Shared by every "user-affirmed" variant so any swatch (primary, pick,
 *  color label) reads as part of the same badge family. */
const THUMB_BADGE_FRAME =
  'shadow-[0_0_0_1px_rgba(0,0,0,0.55),inset_0_1px_0_rgba(255,255,255,0.28),0_1px_2px_rgba(0,0,0,0.5)]'
export const THUMB_BADGE_PRIMARY = `bg-primary ${THUMB_BADGE_FRAME}`
export const THUMB_BADGE_NEUTRAL =
  'bg-black/75 backdrop-blur-sm shadow-[0_0_0_1px_rgba(255,255,255,0.12),inset_0_1px_0_rgba(255,255,255,0.08),0_1px_2px_rgba(0,0,0,0.55)]'
export const THUMB_BADGE_PICK = `bg-pick ${THUMB_BADGE_FRAME}`

/** Tailwind bg-class for each color label, looked up at render time so the
 *  classnames are statically present in the source for the JIT to scan. */
const COLOR_LABEL_BG: Record<string, string> = Object.fromEntries(
  COLOR_LABEL_OPTIONS.map((o) => [o.value, o.className])
)

// Auto-retry schedule (ms). Backend generates thumbs on-demand via Celery, so
// the first hit on a freshly-scanned library returns 404 "not ready" until
// the worker catches up. RAW postprocess can take several seconds per file
// when the queue is deep, so the tail of the schedule is generous.
const AUTO_RETRY_DELAYS = [1500, 3500, 7000, 12000, 20000]

interface PhotoThumbnailProps {
  photo: Photo
  size: number
  /** When true, the cell stretches to fill its parent (100% width +
   *  100% height) and ignores `size` for the box dimensions. Used by
   *  the Timeline grid where the parent is a CSS grid track of 1fr —
   *  this is what guarantees the row fills the container without any
   *  rounding gap on the right. The heap sidebar leaves this off so
   *  thumbnails stay at the explicit `size`. */
  fill?: boolean
  isSelected: boolean
  /** True when the photo belongs to the currently active heap — drives
   *  the green tint overlay. */
  isInActiveHeap?: boolean
  /** Called with the photo + the native event. Pass a stable handler
   *  (useCallback with store-action deps) so React.memo can actually
   *  elide re-renders on unrelated store updates. */
  onClick: (photo: Photo, e: React.MouseEvent) => void
  onDoubleClick?: (photo: Photo, e: React.MouseEvent) => void
}

function PhotoThumbnailImpl({
  photo,
  size,
  fill = false,
  isSelected,
  isInActiveHeap = false,
  onClick,
  onDoubleClick,
}: PhotoThumbnailProps) {
  const [imageError, setImageError] = useState(false)
  const [imageLoaded, setImageLoaded] = useState(false)
  const [retryCount, setRetryCount] = useState(0)
  const [isRetrying, setIsRetrying] = useState(false)
  const retryTimerRef = useRef<number | null>(null)

  // Cache-bust on retry so the browser actually re-requests instead of
  // serving the cached 404.
  const baseUrl = photosApi.getThumbnailUrl(photo.id, 'medium')
  const sep = baseUrl.includes('?') ? '&' : '?'
  const thumbnailUrl = retryCount > 0 ? `${baseUrl}${sep}retry=${retryCount}` : baseUrl

  // "Capture date probably wrong" — read straight from the stored
  // `has_date_warning` flag rather than recomputing the heuristic
  // client-side. The backend sets this column at scan time and
  // refreshes it on any taken_at edit, so the UI, the filter, and the
  // thumbnail badge all read from one source of truth.
  const dateWarning = photo.has_date_warning === true

  // Active search query — subscribed with a focused selector so typing
  // in the search box only re-renders thumbnails, not every consumer
  // of the filter store. When a query is active we compute which field
  // on this photo matched, so the user can see *why* the photo came
  // back from the search instead of guessing.
  const searchQuery = useFilterStore((s) => s.q)
  const searchMatch = searchQuery.trim()
    ? findSearchMatch(photo, searchQuery)
    : null

  // Square cells (Lightroom-style grid). Variable-aspect cells previously
  // overflowed their row because TanStack Virtual estimates row height as a
  // single fixed value — portraits in a landscape row would overlap the row
  // below. With object-cover the image still fills the cell, just cropped.
  //
  // The cell stretches to whatever width the parent grid track gives it
  // (via width:100% + aspect-ratio:1) so the timeline's CSS grid can hand
  // out 1fr columns and we never leave horizontal space unused. `size`
  // remains the *minimum* track width and the fallback when there's no
  // parent grid (e.g. heap thumbnails).
  const displayHeight = size

  const clearRetryTimer = () => {
    if (retryTimerRef.current !== null) {
      window.clearTimeout(retryTimerRef.current)
      retryTimerRef.current = null
    }
  }

  const handleImageLoad = () => {
    setImageLoaded(true)
    setIsRetrying(false)
  }

  const handleImageError = () => {
    // Schedule next auto-retry if attempts remain.
    const nextDelay = AUTO_RETRY_DELAYS[retryCount]
    if (nextDelay !== undefined) {
      setIsRetrying(true)
      clearRetryTimer()
      retryTimerRef.current = window.setTimeout(() => {
        retryTimerRef.current = null
        setRetryCount(prev => prev + 1)
      }, nextDelay)
    } else {
      setImageError(true)
      setIsRetrying(false)
    }
  }

  const handleManualRetry = useCallback((e: React.MouseEvent) => {
    e.stopPropagation() // Prevent selection when clicking retry
    clearRetryTimer()
    setRetryCount(prev => prev + 1)
    setImageError(false)
    setImageLoaded(false)
    setIsRetrying(true)
  }, [])

  // Reset state when photo changes (component is reused across rows when virtualized)
  useEffect(() => {
    clearRetryTimer()
    setImageError(false)
    setImageLoaded(false)
    setRetryCount(0)
    setIsRetrying(false)
  }, [photo.id])

  // Clear pending timer on unmount to avoid setState-after-unmount.
  useEffect(() => {
    return () => clearRetryTimer()
  }, [])

  // Build the drag payload at fire time so multi-selection drags carry the
  // current selection. If the dragged photo isn't part of the selection,
  // drag just that one photo (matches Finder semantics).
  const handleDragStart = (e: React.DragEvent<HTMLDivElement>) => {
    const state = usePhotoStore.getState()
    const ids =
      state.selectedPhotos.includes(photo.id) && state.selectedPhotos.length > 0
        ? state.selectedPhotos
        : [photo.id]
    e.dataTransfer.effectAllowed = 'copy'
    e.dataTransfer.setData(PHOTO_DRAG_MIME, JSON.stringify(ids))
    // A plain text fallback so the OS shows something sensible if the user
    // drops outside the app.
    e.dataTransfer.setData('text/plain', `${ids.length} photo${ids.length > 1 ? 's' : ''}`)
  }

  return (
    <div
      className={clsx(
        'group relative cursor-pointer overflow-hidden rounded-sm will-change-transform',
        // Animate INTO selection (springy ease-in over 300ms); snap
        // back instantly when deselected by dropping the transition
        // entirely. Heap membership doesn't animate — it just paints
        // the green tint overlay.
        isSelected &&
        'transition-[transform,box-shadow] duration-300 ease-[cubic-bezier(0.34,1.56,0.64,1)]',
        // Hover is a subtle preview; selection/heap-membership are the
        // prominent ring + tint states. They must read differently
        // because keyboard nav drives selection while the mouse drives
        // hover — the two can land on different photos at the same
        // time, and the user needs to tell which one is actually
        // selected.
        !isSelected && !isInActiveHeap &&
        'hover:ring-1 hover:ring-text-muted/70 hover:ring-offset-1 hover:ring-offset-bg',
        isSelected &&
        'scale-90 ring-2 ring-blue-500 ring-offset-2 ring-offset-bg',
        // Active-heap membership signals via the green tint overlay
        // only — no ring, no scale, no ornament. The grid layout stays
        // intact and the photo just gets a green wash. When BOTH apply,
        // selection's blue ring takes over.
        !imageLoaded && 'bg-surface animate-pulse'
      )}
      style={
        fill
          ? { width: '100%', height: '100%' }
          : { width: size, height: displayHeight }
      }
      onClick={(e) => onClick(photo, e)}
      onDoubleClick={
        onDoubleClick ? (e) => onDoubleClick(photo, e) : undefined
      }
      draggable
      onDragStart={handleDragStart}
      title="Click to select • Double-click to open • Shift+Click for range • Ctrl+Click to add • Drag onto a heap to add"
    >
      {/* Thumbnail Image */}
      {!imageError ? (
        <>
          <img
            src={thumbnailUrl}
            alt={photo.filename}
            className={clsx(
              'h-full w-full object-cover transition-opacity duration-200',
              imageLoaded ? 'opacity-100' : 'opacity-0',
              // Discarded photos fade out + desaturate so the trash section
              // reads as a trash section, not just another grid view.
              photo.is_discarded && 'opacity-50 grayscale'
            )}
            onLoad={handleImageLoad}
            onError={handleImageError}
            loading="lazy"
          />
          {/* Loading indicator. The outer wrapper already pulses via
           *  `bg-surface animate-pulse` while !imageLoaded, which is the
           *  right default — hundreds of concurrent spinners churn the GPU
           *  on first paint. Only surface a visible indicator for the
           *  relatively rare retry case, where silence would look broken. */}
          {!imageLoaded && isRetrying && (
            <div className="absolute inset-0 flex items-center justify-center bg-surface">
              <div className="text-center text-text-muted">
                <RefreshCw className="mx-auto mb-1 h-4 w-4 animate-spin" />
                <div className="text-[10px]">Retrying…</div>
              </div>
            </div>
          )}
        </>
      ) : (
        <div className="flex h-full w-full items-center justify-center bg-surface text-text-muted">
          <div className="text-center text-xs">
            <button
              onClick={handleManualRetry}
              className="p-2 hover:bg-surface-light rounded transition-colors"
              title="Retry loading thumbnail"
            >
              <RefreshCw className="h-5 w-5 mb-1" />
            </button>
            <div>Unable to load</div>
            <div className="mt-1 font-mono text-[10px] px-2 break-all">{photo.filename}</div>
          </div>
        </div>
      )}

      {/* Tint overlays — heap membership (green) and selection (blue)
       *  composite on top of the image. They can stack: a photo that's
       *  both selected AND in the active heap shows both tints. Never
       *  tinted on hover so the keyboard-driven selection stays
       *  distinguishable from the mouse-driven hover. */}
      {isInActiveHeap && (
        <div className="pointer-events-none absolute inset-0 bg-emerald-500/40" />
      )}
      {isSelected && (
        <div className="pointer-events-none absolute inset-0 bg-blue-500/50" />
      )}

      {/* ── Ornaments ────────────────────────────────────────────────────
       *  All overlays compose the THUMB_BADGE_* classes so they share one
       *  shape/size/ring family. Colour signals semantics:
       *    PRIMARY → user-affirmed state     (selection, rating, heap)
       *    NEUTRAL → informational metadata  (duplicate, discard, file type)
       *  Corner ownership is fixed: TL=selection, TR=file-type,
       *  BL=rating, BR=flags. This keeps badges from stacking or colliding. */}

      {/* Top banner — shown only when a text search is active, explains
       *  which metadata field of this photo matched the user's query so
       *  they don't have to guess why the photo came back. The matched
       *  substring is wrapped in an amber highlight span so the user
       *  can see *exactly* what hit — e.g. "**Mul**ti-segment" when
       *  searching "mul". Sits above the TL/TR corner badges (lower
       *  z-index) so they still read on top of long excerpts. */}
      {searchMatch && (
        <div
          className="pointer-events-none absolute inset-x-0 top-0 z-[1] flex items-center gap-1 bg-black/75 px-1.5 py-0.5 text-[10px] font-medium text-white backdrop-blur-sm"
          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>
      )}

      {/* TL — owner badge for shared photos. Selection itself is
       *  conveyed by the ring/outline on the wrapper, no badge needed. */}
      {photo.owner_username && (
        <div
          className={clsx(
            'absolute left-1 top-1',
            THUMB_BADGE_BASE,
            THUMB_BADGE_NEUTRAL,
            'max-w-[90px]'
          )}
          title={`Photo owned by ${photo.owner_username}`}
        >
          <Users className={THUMB_BADGE_ICON} strokeWidth={2.5} />
          <span className="truncate">{photo.owner_username}</span>
        </div>
      )}

      {/* BL — color label + rating. Color comes first (left of rating)
       *  so the swatch reads as a "category dot" prefixing the stars. */}
      {(photo.color_label || photo.rating > 0) && (
        <div className="absolute bottom-1 left-1 flex items-center gap-1">
          {photo.color_label && COLOR_LABEL_BG[photo.color_label] && (
            <div
              className={clsx(
                THUMB_BADGE_BASE,
                THUMB_BADGE_SQUARE,
                COLOR_LABEL_BG[photo.color_label],
                THUMB_BADGE_FRAME
              )}
              title={`Color label: ${photo.color_label}`}
            />
          )}
          {photo.rating > 0 && (
            <div
              className={clsx('gap-0.5', THUMB_BADGE_BASE, THUMB_BADGE_PRIMARY)}
            >
              {Array.from({ length: photo.rating }).map((_, i) => (
                <Star key={i} className={clsx(THUMB_BADGE_ICON, 'fill-white')} />
              ))}
            </div>
          )}
        </div>
      )}

      {/* BR — duplicate / discard / date-warning (neutral info) plus a
       *  heap-membership chip. The green tint overlay above is the
       *  primary signal, but the icon makes membership readable in
       *  colorblind-safe terms too. */}
      <div className="absolute bottom-1 right-1 flex items-center gap-1">
        {isInActiveHeap && (
          <div
            className={clsx(
              THUMB_BADGE_BASE,
              THUMB_BADGE_SQUARE,
              THUMB_BADGE_PICK
            )}
            title="In active heap"
          >
            <ShoppingBasket className={THUMB_BADGE_ICON} strokeWidth={2.5} />
          </div>
        )}
        {photo.is_duplicate && (
          <div
            className={clsx(THUMB_BADGE_BASE, THUMB_BADGE_SQUARE, THUMB_BADGE_NEUTRAL)}
            title="Duplicate (matches another photo's hash)"
          >
            <Copy className={THUMB_BADGE_ICON} strokeWidth={2.5} />
          </div>
        )}
        {photo.is_discarded && (
          <div
            className={clsx(THUMB_BADGE_BASE, THUMB_BADGE_SQUARE, THUMB_BADGE_NEUTRAL)}
            title="Discarded"
          >
            <Trash2 className={THUMB_BADGE_ICON} strokeWidth={2.5} />
          </div>
        )}
        {dateWarning && (
          <div
            className={clsx(
              THUMB_BADGE_BASE,
              THUMB_BADGE_SQUARE,
              // Amber fill with the same chiseled frame as other affirmative
              // badges so it reads as a first-class warning rather than a
              // neutral info chip.
              'bg-amber-500 shadow-[0_0_0_1px_rgba(0,0,0,0.55),inset_0_1px_0_rgba(255,255,255,0.28),0_1px_2px_rgba(0,0,0,0.5)]'
            )}
            title="Capture date may be wrong — folder/filename suggests a different date"
          >
            <AlertTriangle className={THUMB_BADGE_ICON} strokeWidth={2.5} />
          </div>
        )}
      </div>

      {/* TR — file-type metadata (RAW / VIDEO) */}
      {(photo.filepath.toLowerCase().match(/\.(raw|arw|cr2|cr3|nef|orf|rw2|dng)$/i) ||
        photo.filepath.toLowerCase().match(/\.(mov|mp4|avi|mkv)$/i)) && (
          <div className={clsx('absolute right-1 top-1', THUMB_BADGE_BASE, THUMB_BADGE_NEUTRAL)}>
            {photo.filepath.toLowerCase().match(/\.(mov|mp4|avi|mkv)$/i) ? 'VIDEO' : 'RAW'}
          </div>
        )}
    </div>
  )
}

/**
 * React.memo with the default shallow comparison. Relies on the caller
 * passing stable `onClick`/`onDoubleClick` handlers (via useCallback)
 * so identity doesn't churn on every parent render — that's what lets
 * us skip re-render on unrelated store updates like heap invalidation.
 */
export const PhotoThumbnail = memo(PhotoThumbnailImpl)