import { useState, useEffect, useCallback, useRef } from 'react'
import {
  Star,
  ShoppingBasket,
  Trash2,
  RefreshCw,
  Check,
  Copy,
  AlertTriangle,
} 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 { COLOR_LABEL_OPTIONS } from '../../constants/colorLabels'

/** 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. */
  isInActiveHeap?: boolean
  /** Name of the active heap. When set + isInActiveHeap, the basket
   *  badge expands into a name chip so the user knows which heap. */
  activeHeapName?: string | null
  onClick: (e: React.MouseEvent) => void
  onDoubleClick?: (e: React.MouseEvent) => void
}

export function PhotoThumbnail({
  photo,
  size,
  fill = false,
  isSelected,
  isInActiveHeap = false,
  activeHeapName = null,
  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 thumbnailUrl = retryCount > 0 ? `${baseUrl}?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

  // 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 transition-all duration-200',
        // Two-tone hover ring: bright primary inner + dark offset so it
        // pops on light AND dark photos.
        'hover:ring-2 hover:ring-primary/60 hover:ring-offset-1 hover:ring-offset-bg',
        isSelected &&
        'ring-2 ring-primary ring-offset-2 ring-offset-bg shadow-lg',
        !imageLoaded && 'bg-surface animate-pulse'
      )}
      style={
        fill
          ? { width: '100%', height: '100%' }
          : { width: size, height: displayHeight }
      }
      onClick={onClick}
      onDoubleClick={onDoubleClick}
      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 */}
          {!imageLoaded && (
            <div className="absolute inset-0 flex items-center justify-center bg-surface">
              <div className="text-text-muted">
                {isRetrying ? (
                  <div className="text-center">
                    <RefreshCw className="h-5 w-5 animate-spin mx-auto mb-1" />
                    <div className="text-xs">Retrying...</div>
                  </div>
                ) : (
                  <div className="h-8 w-8 border-2 border-primary/30 border-t-primary rounded-full animate-spin" />
                )}
              </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>
      )}

      {/* ── 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. */}

      {/* TL — selection */}
      {isSelected && (
        <div
          className={clsx(
            'absolute left-1 top-1',
            THUMB_BADGE_BASE,
            THUMB_BADGE_SQUARE,
            THUMB_BADGE_PRIMARY
          )}
        >
          <Check className={THUMB_BADGE_ICON} strokeWidth={3} />
        </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 — flags stack: heap (primary) · duplicate / discard (neutral).
       *  Heap is the only user-state flag here so it gets primary; the
       *  rest are metadata about the file, so they're neutral-dark. */}
      <div className="absolute bottom-1 right-1 flex items-center gap-1">
        {isInActiveHeap && (
          <div
            className={clsx(THUMB_BADGE_BASE, THUMB_BADGE_PRIMARY, 'max-w-[120px]')}
            title={activeHeapName ? `In heap: ${activeHeapName}` : 'In active heap'}
          >
            <ShoppingBasket
              className={clsx(THUMB_BADGE_ICON, 'flex-shrink-0')}
              strokeWidth={2.5}
            />
            {activeHeapName && <span className="truncate">{activeHeapName}</span>}
          </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>
  )
}