import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { useState, useEffect, useRef, useMemo, useCallback } from 'react'
import { useSearchParams, useNavigate } from 'react-router-dom'
import { useBreadcrumb } from '../../hooks/useBreadcrumb'
import { breadcrumbConfig } from '../../config/breadcrumbConfig'
import {
  Download,
  Calendar,
  User,
  Image as ImageIcon,
  Video,
  File,
  X,
  Loader2,
  Edit,
  Save,
  Link,
  LayoutGrid,
  Tag,
  Check,
  Square,
  Columns,
  Pin,
  Lock,
  Settings,
  Upload,
  Trash2,
  Play,
  AlertTriangle,
  List,
  Search,
  ScanSearch,
  Globe,
  CheckSquare,
  Eye,
  EyeOff,
} from 'lucide-react'
import { FilterBar, FilterSection, ActiveFilter } from '../../components/FilterPopover'
import { api, PaidContentPost, PaidContentAttachment } from '../../lib/api'
import { formatRelativeTime, decodeHtmlEntities } from '../../lib/utils'
// PostDetail component defined locally - no shared PostDetailView needed
import PrivateGalleryBundleLightbox, { PrivateGalleryMediaItem } from '../../components/private-gallery/PrivateGalleryBundleLightbox'
import PrivateGalleryUploadModal from '../../components/private-gallery/PrivateGalleryUploadModal'
import TagSearchSelector from '../../components/private-gallery/TagSearchSelector'
import { usePrivateGalleryContext } from '../../components/private-gallery/PrivateGalleryProvider'

// Private gallery media item interface
interface PrivateMediaItem {
  id: number
  storage_id: string
  filename: string
  description?: string | null
  file_hash: string
  file_size: number
  file_type: 'image' | 'video' | 'other'
  mime_type: string
  width?: number | null
  height?: number | null
  duration?: number | null
  person_id?: number | null
  person?: {
    id: number
    name: string
    relationship: { id: number; name: string; color: string }
  } | null
  media_date: string
  post_date?: string
  thumbnail_url: string
  stream_url: string
  file_url: string
  tags: Array<{ id: number; name: string; color: string }>
  created_at?: string
}

interface FeedFilters {
  person_id?: number
  person_group_id?: number
  relationship_id?: number
  tag_id?: number
  file_type?: string  // 'image', 'video', 'all'
  date_from?: string
  date_to?: string
  sort_by: string
  sort_order: 'asc' | 'desc'
  search?: string
  has_tags?: boolean
  has_description?: boolean
  unread_only?: boolean
  min_resolution?: number
}

// Format file size helper
function formatFileSize(bytes: number | null): string {
  if (!bytes) return ''
  if (bytes < 1024) return `${bytes} B`
  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
  if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
  return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`
}

// Format duration helper
function formatDuration(seconds: number | null): string {
  if (!seconds) return ''
  const mins = Math.floor(seconds / 60)
  const secs = Math.floor(seconds % 60)
  if (mins > 0) return `${mins}:${secs.toString().padStart(2, '0')}`
  return `0:${secs.toString().padStart(2, '0')}`
}

// Generate display title for posts without titles
function getPostDisplayTitle(post: PaidContentPost): string {
  // Use actual title if available
  if (post.title && post.title.trim()) {
    return post.title.trim()
  }

  // Build a descriptive title from available data
  const parts: string[] = []

  // Add date if available
  if (post.published_at) {
    const normalized = post.published_at.includes('T') ? post.published_at : `${post.published_at}T00:00:00`
    const date = new Date(normalized)
    parts.push(date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }))
  }

  // Add attachment info — use backend-provided type counts (accurate for all sizes)
  if (post.attachment_count && post.attachment_count > 0) {
    const imageCount = (post as any).image_count ?? 0
    const videoCount = (post as any).video_count ?? 0
    const totalFromTypes = imageCount + videoCount

    // Use type counts if they're available and match the total
    if (totalFromTypes > 0 && totalFromTypes >= post.attachment_count) {
      if (videoCount > 0 && imageCount > 0) {
        parts.push(`${imageCount} images, ${videoCount} videos`)
      } else if (videoCount > 0) {
        parts.push(`${videoCount} ${videoCount === 1 ? 'video' : 'videos'}`)
      } else {
        parts.push(`${imageCount} ${imageCount === 1 ? 'image' : 'images'}`)
      }
    } else {
      // Fallback: just show total count
      parts.push(`${post.attachment_count} ${post.attachment_count === 1 ? 'file' : 'files'}`)
    }
  }

  // If we have parts, join them
  if (parts.length > 0) {
    return parts.join(' — ')
  }

  // Last resort: use post_id
  return `Post ${post.post_id?.slice(-8) || post.id}`
}

function AttachmentThumbnail({
  attachment,
  onClick,
  onImport
}: {
  attachment: PaidContentAttachment
  onClick?: (e: React.MouseEvent) => void
  onImport?: (attachment: PaidContentAttachment) => void
}) {
  const [thumbnailError, setThumbnailError] = useState(false)
  const isImage = attachment.file_type === 'image'
  const isVideo = attachment.file_type === 'video'

  // Get private gallery token for authenticated URLs
  const token = api.getPrivateGalleryToken()
  const tokenParam = token ? `?_token=${encodeURIComponent(token)}` : ''

  // Use private gallery API endpoints
  const thumbnailUrl = `/api/private-gallery/thumbnail/${attachment.id}${tokenParam}`
  const fileUrl = isVideo
    ? `/api/private-gallery/stream/${attachment.id}${tokenParam}`
    : `/api/private-gallery/file/${attachment.id}${tokenParam}`

  const isMissing = attachment.status === 'failed' || attachment.status === 'pending'

  const handleClick = (e: React.MouseEvent) => {
    e.stopPropagation() // Prevent click from bubbling to parent (e.g., mobile post wrapper)
    if (onClick) {
      onClick(e)
    } else if (attachment.status === 'completed') {
      window.open(fileUrl, '_blank')
    }
  }

  // Build metadata string for tooltip
  const metadata: string[] = []
  if (attachment.name) metadata.push(attachment.name)
  if (attachment.file_size) metadata.push(formatFileSize(attachment.file_size))
  if (attachment.width && attachment.height) metadata.push(`${attachment.width}×${attachment.height}`)
  if (attachment.duration) metadata.push(formatDuration(attachment.duration))

  const canShowThumbnail = (isImage || isVideo) && attachment.status === 'completed' && attachment.id && !thumbnailError

  return (
    <div
      className={`aspect-square bg-secondary rounded-lg overflow-hidden relative group ${
        attachment.status === 'completed' ? 'cursor-pointer hover:ring-2 hover:ring-primary' : ''
      }`}
      onClick={handleClick}
      title={metadata.join(' • ')}
    >
      {canShowThumbnail ? (
        // Use private gallery thumbnail endpoint
        <img
          src={thumbnailUrl}
          alt={attachment.name}
          className="w-full h-full object-cover"
          loading="lazy"
          onError={() => setThumbnailError(true)}
        />
      ) : (
        <div className="w-full h-full flex items-center justify-center">
          {isImage && <ImageIcon className="w-8 h-8 text-muted-foreground" />}
          {isVideo && <Video className="w-8 h-8 text-muted-foreground" />}
          {!isImage && !isVideo && <File className="w-8 h-8 text-muted-foreground" />}
        </div>
      )}

      {/* Metadata overlay on hover */}
      <div className="absolute inset-x-0 bottom-0 bg-gradient-to-t from-black/80 to-transparent p-2 opacity-0 group-hover:opacity-100 transition-opacity">
        <div className="text-white text-xs truncate">
          {attachment.name || `File ${attachment.attachment_index + 1}`}
        </div>
        <div className="text-white/70 text-xs flex items-center space-x-2">
          {attachment.file_size && <span>{formatFileSize(attachment.file_size)}</span>}
          {attachment.width && attachment.height && <span>{attachment.width}×{attachment.height}</span>}
          {isVideo && attachment.duration && <span>{formatDuration(attachment.duration)}</span>}
        </div>
      </div>

      {/* Status indicators */}
      {attachment.status === 'completed' && (
        <div className="absolute top-1 right-1 bg-emerald-500 text-white rounded-full p-1">
          <Download className="w-3 h-3" />
        </div>
      )}
      {attachment.status === 'pending' && (
        <div className="absolute inset-0 bg-black/50 flex items-center justify-center">
          <span className="text-xs text-white">Pending</span>
        </div>
      )}
      {attachment.status === 'failed' && (
        <div className="absolute inset-0 bg-red-500/50 flex items-center justify-center">
          <span className="text-xs text-white">Failed</span>
        </div>
      )}
      {isVideo && attachment.status === 'completed' && (
        <div className="absolute top-1 left-1 bg-black/60 text-white rounded px-1.5 py-0.5 text-xs">
          <Video className="w-3 h-3 inline mr-1" />
          {attachment.duration ? formatDuration(attachment.duration) : 'Video'}
        </div>
      )}

      {/* Import button for failed/pending attachments */}
      {isMissing && onImport && (
        <button
          onClick={(e) => {
            e.stopPropagation()
            onImport(attachment)
          }}
          className="absolute top-1 right-1 bg-blue-500 hover:bg-blue-600 text-white rounded-full p-1.5 opacity-0 group-hover:opacity-100 transition-opacity z-10"
          title="Import from URL"
        >
          <Link className="w-3 h-3" />
        </button>
      )}
    </div>
  )
}

function PostCard({
  post,
  isSelected,
  onClick,
  onOpenLightbox,
  onToggleRead,
}: {
  post: PaidContentPost
  isSelected: boolean
  onClick: () => void
  onOpenLightbox?: (attachmentIndex: number) => void
  onToggleRead?: () => void
}) {
  const touchStartRef = useRef<{ x: number; y: number; time: number } | null>(null)

  // Get completed attachments for display
  const completedAttachments = post.attachments?.filter(a => a.status === 'completed') || []
  const hasMoreAttachments = (post.attachment_count || 0) > 4

  // Handle touch to distinguish between tap and scroll
  const handleTouchStart = (e: React.TouchEvent) => {
    touchStartRef.current = { x: e.touches[0].clientX, y: e.touches[0].clientY, time: Date.now() }
  }

  const handleTouchEnd = (e: React.TouchEvent) => {
    if (!touchStartRef.current) return
    const touch = e.changedTouches[0]
    const dx = Math.abs(touch.clientX - touchStartRef.current.x)
    const dy = Math.abs(touch.clientY - touchStartRef.current.y)
    const duration = Date.now() - touchStartRef.current.time
    // Only trigger click if movement was less than 20px and touch lasted at least 50ms (deliberate tap)
    if (dx < 20 && dy < 20 && duration >= 50) {
      onClick()
    }
    touchStartRef.current = null
  }

  return (
    <div
      data-post-id={post.id}
      className={`p-4 border-b border-border cursor-pointer hover:bg-muted/50 transition-colors ${isSelected ? 'bg-primary/10' : ''}`}
      onClick={() => {
        // Only handle click on desktop (no touch events)
        if (!('ontouchstart' in window)) {
          onClick()
        }
      }}
      onTouchStart={handleTouchStart}
      onTouchEnd={handleTouchEnd}
    >
      {/* Header row */}
      <div className="flex items-start justify-between">
        <div className="flex items-center space-x-3">
          <div className="w-10 h-10 rounded-full bg-gradient-to-br from-pink-500 to-violet-500 p-0.5 flex-shrink-0">
            <div className="w-full h-full rounded-full overflow-hidden bg-card">
              {post.profile_image_url ? (
                <img src={post.profile_image_url} alt="" className="w-full h-full object-cover" />
              ) : (
                <div className="w-full h-full flex items-center justify-center bg-secondary">
                  <User className="w-5 h-5 text-muted-foreground" />
                </div>
              )}
            </div>
          </div>
          <div>
            <div className="flex items-center gap-2">
              <p className="font-medium text-foreground">{post.display_name || 'Unknown'}</p>
              {(post as any)._relationship && (
                <span
                  className="px-1.5 py-0.5 rounded-full text-xs font-medium text-white"
                  style={{ backgroundColor: (post as any)._relationship.color }}
                >
                  {(post as any)._relationship.name}
                </span>
              )}
            </div>
            <p className="text-xs text-muted-foreground">
              {post.published_at ? new Date(post.published_at.includes('T') ? post.published_at : `${post.published_at}T00:00:00`).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }) : 'Unknown date'}
            </p>
          </div>
        </div>
        <div className="flex items-center space-x-2">
          {post.is_pinned === 1 && (
            <span className="text-amber-500" title="Pinned">
              <Pin className="w-4 h-4" />
            </span>
          )}
          {!post.is_viewed && (
            <span className="w-2 h-2 rounded-full bg-primary"></span>
          )}
          {onToggleRead && (
            <button
              onClick={(e) => { e.stopPropagation(); onToggleRead(); }}
              className="p-1 rounded hover:bg-muted transition-colors text-muted-foreground hover:text-foreground"
              title={post.is_viewed ? 'Mark as unread' : 'Mark as read'}
            >
              {post.is_viewed ? <EyeOff className="w-3.5 h-3.5" /> : <Eye className="w-3.5 h-3.5" />}
            </button>
          )}
        </div>
      </div>

      {/* Tags row */}
      {post.tags && post.tags.length > 0 && (
        <div className="mt-2 flex flex-wrap gap-1">
          {post.tags.map((tag) => (
            <span
              key={tag.id}
              className="px-2 py-0.5 rounded-full text-xs font-medium"
              style={{ backgroundColor: tag.color, color: '#fff' }}
            >
              {tag.name}
            </span>
          ))}
        </div>
      )}

      {/* Content - For YouTube/Twitch, use title instead of content */}
      {(() => {
        const isVideoService = ['youtube', 'twitch', 'pornhub'].includes(post.platform?.toLowerCase() || '')
        const displayText = isVideoService && post.title ? post.title : (post.content || post.title)
        return displayText ? (
          <div className="mt-3">
            <p className="text-sm text-muted-foreground line-clamp-3">{decodeHtmlEntities(displayText)}</p>
          </div>
        ) : null
      })()}

      {/* Attachment Grid - Inline thumbnails (mobile only, desktop uses detail panel) */}
      {completedAttachments.length > 0 && (
        <div className="mt-3 lg:hidden">
          <div className={`grid gap-1 rounded-lg overflow-hidden ${
            completedAttachments.length === 1 ? 'grid-cols-1' :
            completedAttachments.length === 2 ? 'grid-cols-2' :
            'grid-cols-2'
          }`}>
            {completedAttachments.slice(0, 4).map((att, idx) => {
              const isImage = att.file_type === 'image'
              const isVideo = att.file_type === 'video'

              return (
                <button
                  key={att.id}
                  type="button"
                  className={`relative bg-secondary overflow-hidden cursor-pointer active:opacity-70 ${
                    completedAttachments.length === 1 ? 'aspect-video' : 'aspect-square'
                  }`}
                  style={{ touchAction: 'manipulation', WebkitTapHighlightColor: 'transparent' }}
                  onTouchStart={(e) => e.stopPropagation()}
                  onTouchEnd={(e) => e.stopPropagation()}
                  onClick={(e) => {
                    e.stopPropagation()
                    if (onOpenLightbox) {
                      onOpenLightbox(idx)
                    }
                  }}
                >
                  {(isImage || isVideo) && att.id ? (
                    // Use cached thumbnail from database
                    <div className="w-full h-full bg-slate-800 relative pointer-events-none">
                      <img
                        src={`/api/private-gallery/thumbnail/${att.id}${api.getPrivateGalleryToken() ? `?_token=${encodeURIComponent(api.getPrivateGalleryToken()!)}` : ''}`}
                        alt=""
                        className="w-full h-full object-cover"
                        loading="lazy"
                        onError={(e) => {
                          // Hide broken image, show fallback
                          (e.target as HTMLImageElement).style.display = 'none'
                        }}
                      />
                      {/* Play button overlay for videos */}
                      {isVideo && (
                        <div className="absolute inset-0 flex items-center justify-center">
                          <div className="w-12 h-12 rounded-full bg-black/50 flex items-center justify-center">
                            <Video className="w-6 h-6 text-white" />
                          </div>
                        </div>
                      )}
                      {/* Duration badge for videos */}
                      {isVideo && att.duration && (
                        <div className="absolute bottom-1 right-1 bg-black/70 text-white text-xs px-1.5 py-0.5 rounded">
                          {Math.floor(att.duration / 60)}:{Math.floor(att.duration % 60).toString().padStart(2, '0')}
                        </div>
                      )}
                    </div>
                  ) : (
                    <div className="w-full h-full flex items-center justify-center pointer-events-none">
                      <File className="w-8 h-8 text-muted-foreground" />
                    </div>
                  )}

                  {/* Show +N overlay on the 4th item if there are more */}
                  {idx === 3 && hasMoreAttachments && (
                    <div className="absolute inset-0 bg-black/60 flex items-center justify-center pointer-events-none">
                      <span className="text-white text-xl font-bold">
                        +{(post.attachment_count || 0) - 4}
                      </span>
                    </div>
                  )}
                </button>
              )
            })}
          </div>
        </div>
      )}

      {/* Footer */}
      <div className="mt-3 flex items-center justify-between text-xs text-muted-foreground">
        <span className="flex items-center space-x-1">
          <Calendar className="w-3 h-3" />
          <span>{post.published_at ? formatRelativeTime(post.published_at) : 'Unknown date'}</span>
        </span>
        <div className="flex items-center space-x-2">
          {/* Content type tags — use backend-provided counts for accuracy */}
          {(() => {
            const imageCount = (post as any).image_count ?? post.attachments?.filter(a => a.file_type === 'image').length ?? 0
            const videoCount = (post as any).video_count ?? post.attachments?.filter(a => a.file_type === 'video').length ?? 0
            return (
              <>
                {imageCount > 0 && (
                  <span className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300">
                    <ImageIcon className="w-3 h-3" />
                    <span>{imageCount}</span>
                  </span>
                )}
                {videoCount > 0 && (
                  <span className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-300">
                    <Video className="w-3 h-3" />
                    <span>{videoCount}</span>
                  </span>
                )}
              </>
            )
          })()}
          {post.downloaded && (
            <span className="flex items-center space-x-1 text-emerald-600 dark:text-emerald-400">
              <Download className="w-3 h-3" />
            </span>
          )}
        </div>
      </div>
    </div>
  )
}

function PostDetail({ post, onClose, onPostUpdated, attachmentViewMode, onToggleAttachmentView, duplicateAutoSelectDistance = 2 }: { post: PaidContentPost; onClose: () => void; onPostUpdated?: (updatedPost?: Partial<PaidContentPost>) => void; attachmentViewMode?: 'grid' | 'list'; onToggleAttachmentView?: () => void; duplicateAutoSelectDistance?: number }) {
  const [lightboxIndex, setLightboxIndex] = useState<number | null>(null)
  const [isEditing, setIsEditing] = useState(false)
  const [editContent, setEditContent] = useState(post.content || '')
  const [editDate, setEditDate] = useState(post.published_at?.split('T')[0] || '')
  const [editTags, setEditTags] = useState<number[]>(post.tags?.filter(t => t.id > 0).map(t => t.id) || [])
  const queryClient = useQueryClient()

  // Attachment deletion state
  type ExtendedAttachment = PaidContentAttachment & { thumbnail_url?: string }
  const [attachments, setAttachments] = useState<ExtendedAttachment[]>(
    (post.attachments || []) as ExtendedAttachment[]
  )
  const [attachmentTotal, setAttachmentTotal] = useState<number>((post as any).attachment_count ?? post.attachments?.length ?? 0)
  const [isLoadingMore, setIsLoadingMore] = useState(false)
  const loadMoreSentinelRef = useRef<HTMLDivElement>(null)
  const isLoadingMoreRef = useRef(false)
  const [deletingAttachmentId, setDeletingAttachmentId] = useState<number | null>(null)
  const [confirmDeleteId, setConfirmDeleteId] = useState<number | null>(null)
  const [fileSearch, setFileSearch] = useState('')

  // Selection mode for batch delete
  const [selectionMode, setSelectionMode] = useState(false)
  const [selectedIds, setSelectedIds] = useState<Set<number>>(new Set())
  const [isBatchDeleting, setIsBatchDeleting] = useState(false)
  const lastSelectedAttIdx = useRef<number | null>(null)

  // Duplicate scanning state
  const [isScanning, setIsScanning] = useState(false)
  const [, setScanJobId] = useState<string | null>(null)
  const [scanProgress, setScanProgress] = useState<{ processed: number; total: number; phase: string; comparedPairs?: number; totalPairs?: number } | null>(null)
  const [selectedDupes, setSelectedDupes] = useState<Set<number>>(new Set())
  const lastSelectedDupeIdx = useRef<string | null>(null)
  const [isDeletingDupes, setIsDeletingDupes] = useState(false)
  const [scanResult, setScanResult] = useState<{
    results: Array<{
      items: Array<{
        media_id: number
        post_id: number
        storage_id: string
        filename: string
        file_type: string
        width: number | null
        height: number | null
        distances: Record<string, number>
      }>
    }>
    total_scanned: number
    total_groups: number
    person_id: number | null
    message?: string
  } | null>(null)

  // Fetch available tags
  const { data: availableTags = [] } = useQuery({
    queryKey: ['private-gallery-tags'],
    queryFn: () => api.privateGallery.getTags(),
  })

  // Update edit state when post changes
  useEffect(() => {
    setEditContent(post.content || '')
    setEditDate(post.published_at?.split('T')[0] || '')
    setEditTags(post.tags?.filter(t => t.id > 0).map(t => t.id) || [])
    setAttachments((post.attachments || []) as ExtendedAttachment[])
    setAttachmentTotal((post as any).attachment_count ?? post.attachments?.length ?? 0)
    setIsEditing(false)
    setConfirmDeleteId(null)
    setDeletingAttachmentId(null)
    setScanResult(null)
    setScanJobId(null)
    setScanProgress(null)
    setIsScanning(false)
    setSelectedDupes(new Set())
    setIsDeletingDupes(false)
    setSelectionMode(false)
    setSelectedIds(new Set())
    setIsBatchDeleting(false)
  }, [post.id])

  // Infinite scroll: load more attachments when sentinel is visible
  useEffect(() => {
    const sentinel = loadMoreSentinelRef.current
    if (!sentinel) return
    if (attachments.length >= attachmentTotal) return

    const observer = new IntersectionObserver(
      async (entries) => {
        if (!entries[0].isIntersecting) return
        if (isLoadingMoreRef.current) return
        isLoadingMoreRef.current = true
        setIsLoadingMore(true)
        try {
          const result = await api.privateGallery.getPostAttachments(post.id, attachments.length, 200)
          if (result.items.length > 0) {
            const newItems = result.items.map(item => ({
              ...item,
              id: item.id,
              name: item.filename,
              local_filename: item.filename,
              status: 'completed' as const,
            })) as unknown as ExtendedAttachment[]
            setAttachments(prev => {
              const existingIds = new Set(prev.map(a => a.id))
              const deduped = newItems.filter(a => !existingIds.has(a.id))
              return deduped.length > 0 ? [...prev, ...deduped] : prev
            })
          }
          setAttachmentTotal(result.total)
        } catch (error) {
          console.error('Failed to load more attachments:', error)
        } finally {
          isLoadingMoreRef.current = false
          setIsLoadingMore(false)
        }
      },
      { rootMargin: '600px' }
    )
    observer.observe(sentinel)
    return () => observer.disconnect()
  }, [attachments.length, attachmentTotal, post.id])

  // Mutation for updating post (uses private gallery API)
  const updateMutation = useMutation({
    mutationFn: (data: { description?: string; media_date?: string; tag_ids?: number[] }) =>
      api.privateGallery.updatePost(post.id, data).then(() => data),
    onSuccess: (data) => {
      queryClient.invalidateQueries({ queryKey: ['private-gallery-posts'] })
      setIsEditing(false)
      // Pass back updated fields so parent can merge into selectedPost state
      const updates: Partial<PaidContentPost> = {}
      if (data.description !== undefined) updates.content = data.description
      if (data.media_date !== undefined) updates.published_at = data.media_date
      if (data.tag_ids !== undefined) {
        updates.tags = data.tag_ids.map(id => {
          const existing = availableTags.find(t => t.id === id)
          return existing ? { id: existing.id, name: existing.name, slug: '', color: (existing as { color?: string }).color || '#6b7280', description: null } : { id, name: '', slug: '', color: '#6b7280', description: null }
        })
      }
      onPostUpdated?.(updates)
    },
    onError: (error) => {
      console.error('Failed to update post:', error)
    },
  })

  const handleSave = async () => {
    const updates: { description?: string; media_date?: string; tag_ids?: number[] } = {}
    // Normalize null/undefined to empty string for comparison
    const originalContent = post.content || ''
    const originalDate = post.published_at?.split('T')[0] || ''

    if (editContent !== originalContent) updates.description = editContent
    if (editDate !== originalDate) updates.media_date = editDate || undefined

    // Check if tags changed (exclude relationship pseudo-tag with negative ID)
    const currentTagIds = (post.tags?.filter(t => t.id > 0).map(t => t.id)) || []
    const filteredEditTags = editTags.filter(id => id > 0)
    const tagsChanged = filteredEditTags.length !== currentTagIds.length ||
      filteredEditTags.some(id => !currentTagIds.includes(id))

    if (tagsChanged) {
      updates.tag_ids = filteredEditTags
    }

    if (Object.keys(updates).length > 0) {
      updateMutation.mutate(updates)
    } else {
      setIsEditing(false)
    }
  }

  // Delete attachment handler
  const handleDeleteAttachment = async (attachmentId: number) => {
    // If this is the last attachment, offer to delete the entire post
    if (attachments.length === 1) {
      setConfirmDeleteId(null)
      if (confirm('This is the last attachment. Delete the entire post?')) {
        try {
          await api.privateGallery.deletePost(post.id)
          queryClient.invalidateQueries({ queryKey: ['private-gallery-posts'] })
          queryClient.invalidateQueries({ queryKey: ['private-gallery-media'] })
          queryClient.invalidateQueries({ queryKey: ['private-gallery-stats'] })
          onClose()
        } catch (error) {
          console.error('Failed to delete post:', error)
          alert('Failed to delete post')
        }
      }
      return
    }

    setDeletingAttachmentId(attachmentId)
    setConfirmDeleteId(null)

    try {
      await api.privateGallery.deleteMedia(attachmentId)
      setAttachments(prev => prev.filter(a => a.id !== attachmentId))
      // Refresh the post list
      queryClient.invalidateQueries({ queryKey: ['private-gallery-posts'] })
    } catch (error) {
      console.error('Failed to delete attachment:', error)
      alert('Failed to delete attachment')
    } finally {
      setDeletingAttachmentId(null)
    }
  }

  // Batch delete handler
  const handleBatchDelete = async () => {
    if (selectedIds.size === 0) return

    // If deleting all attachments, offer to delete the post
    if (selectedIds.size === attachments.length) {
      if (confirm(`Delete all ${attachments.length} attachments and the entire post? This cannot be undone.`)) {
        try {
          setIsBatchDeleting(true)
          await api.privateGallery.deletePost(post.id)
          queryClient.invalidateQueries({ queryKey: ['private-gallery-posts'] })
          queryClient.invalidateQueries({ queryKey: ['private-gallery-media'] })
          queryClient.invalidateQueries({ queryKey: ['private-gallery-stats'] })
          onClose()
        } catch (error) {
          console.error('Failed to delete post:', error)
          alert('Failed to delete post')
        } finally {
          setIsBatchDeleting(false)
        }
      }
      return
    }

    if (!confirm(`Delete ${selectedIds.size} selected attachment${selectedIds.size > 1 ? 's' : ''}?`)) return

    setIsBatchDeleting(true)
    try {
      const idsToDelete = Array.from(selectedIds)
      // Chunk into batches of 500 to avoid overwhelming the server
      for (let i = 0; i < idsToDelete.length; i += 500) {
        const chunk = idsToDelete.slice(i, i + 500)
        await api.privateGallery.batchDeleteMedia(chunk)
      }
      setAttachments(prev => prev.filter(a => !selectedIds.has(a.id)))
      setSelectedIds(new Set())
      setSelectionMode(false)
      queryClient.invalidateQueries({ queryKey: ['private-gallery-posts'] })
      queryClient.invalidateQueries({ queryKey: ['private-gallery-media'] })
      queryClient.invalidateQueries({ queryKey: ['private-gallery-stats'] })
    } catch (error) {
      console.error('Failed to batch delete:', error)
      alert('Some attachments failed to delete')
    } finally {
      setIsBatchDeleting(false)
    }
  }

  // Lightbox delete handler (deletes current item and adjusts index)
  const handleLightboxDelete = async (item: PrivateGalleryMediaItem) => {
    if (!confirm('Delete this attachment?')) return
    try {
      await api.privateGallery.deleteMedia(item.id)
      const newAttachments = attachments.filter(a => a.id !== item.id)
      setAttachments(newAttachments)
      queryClient.invalidateQueries({ queryKey: ['private-gallery-posts'] })

      // If no attachments left, close lightbox
      if (newAttachments.length === 0 || newAttachments.filter(a => a.status === 'completed' && (a.file_type === 'image' || a.file_type === 'video')).length === 0) {
        setLightboxIndex(null)
        return
      }

      // Adjust index if needed
      const newCompleted = newAttachments.filter(a => a.status === 'completed' && (a.file_type === 'image' || a.file_type === 'video'))
      if (lightboxIndex !== null && lightboxIndex >= newCompleted.length) {
        setLightboxIndex(newCompleted.length - 1)
      }
    } catch (error) {
      console.error('Failed to delete attachment:', error)
      alert('Failed to delete attachment')
    }
  }

  // Filter to only completed media attachments for lightbox navigation
  const completedAttachments = attachments.filter(
    (a) => a.status === 'completed' && (a.file_type === 'image' || a.file_type === 'video')
  )

  const openLightbox = (attachment: PaidContentAttachment) => {
    if (attachment.status !== 'completed') return
    const index = completedAttachments.findIndex((a) => a.id === attachment.id)
    if (index !== -1) {
      setLightboxIndex(index)
    }
  }

  // Convert post's attachment to PrivateGalleryMediaItem for lightbox
  // NOTE: Do NOT include token in URLs here - the lightbox component adds it via getPreviewUrl/getThumbnailUrl
  const detailLightboxItems: PrivateGalleryMediaItem[] = completedAttachments.map(att => {
    const relationship = (post as any)._relationship || undefined
    return {
      id: att.id,
      storage_id: att.id.toString(),
      filename: att.name || att.local_filename || 'file',
      description: post.content || null,
      file_type: att.file_type as 'image' | 'video' | 'other',
      file_size: att.file_size || 0,
      width: att.width,
      height: att.height,
      duration: att.duration,
      person: post.display_name ? { id: post.creator_id, name: post.display_name, relationship } : null,
      media_date: (att as any).media_date || post.published_at || new Date().toISOString(),
      post_date: post.published_at || undefined,
      tags: post.tags?.filter(t => t.id > 0).map(t => ({ ...t, slug: '' })) || [],
      thumbnail_url: `/api/private-gallery/thumbnail/${att.id}`,
      file_url: `/api/private-gallery/file/${att.id}`,
      stream_url: `/api/private-gallery/stream/${att.id}`,
    }
  })

  const renderActionButtons = () => (
    <>
      {isEditing ? (
        <>
          <button
            type="button"
            onClick={(e) => { e.stopPropagation(); setIsEditing(false); setSelectionMode(false); setSelectedIds(new Set()); }}
            onTouchEnd={(e) => { e.preventDefault(); e.stopPropagation(); setIsEditing(false); setSelectionMode(false); setSelectedIds(new Set()); }}
            className="p-2 rounded-lg hover:bg-secondary active:bg-secondary/80 transition-colors text-muted-foreground min-w-[40px] min-h-[40px] flex items-center justify-center"
            style={{ touchAction: 'manipulation', WebkitTapHighlightColor: 'transparent' }}
            title="Cancel"
          >
            <X className="w-5 h-5" />
          </button>
          <button
            type="button"
            onClick={(e) => { e.stopPropagation(); handleSave(); }}
            onTouchEnd={(e) => { e.preventDefault(); e.stopPropagation(); handleSave(); }}
            disabled={updateMutation.isPending}
            className="p-2 rounded-lg hover:bg-secondary active:bg-secondary/80 transition-colors text-primary min-w-[40px] min-h-[40px] flex items-center justify-center"
            style={{ touchAction: 'manipulation', WebkitTapHighlightColor: 'transparent' }}
            title="Save"
          >
            {updateMutation.isPending ? (
              <Loader2 className="w-5 h-5 animate-spin" />
            ) : (
              <Save className="w-5 h-5" />
            )}
          </button>
          {/* Select mode toggle - available in edit mode for bulk delete */}
          <button
            type="button"
            onClick={(e) => {
              e.stopPropagation()
              if (selectionMode) {
                setSelectionMode(false)
                setSelectedIds(new Set())
              } else {
                setSelectionMode(true)
              }
            }}
            className={`p-2 rounded-lg hover:bg-secondary active:bg-secondary/80 transition-colors min-w-[40px] min-h-[40px] flex items-center justify-center ${
              selectionMode ? 'text-primary bg-primary/10' : 'text-muted-foreground'
            }`}
            style={{ touchAction: 'manipulation', WebkitTapHighlightColor: 'transparent' }}
            title={selectionMode ? 'Cancel selection' : 'Select items to delete'}
          >
            <CheckSquare className="w-5 h-5" />
          </button>
        </>
      ) : (
        <>
          <button
            type="button"
            onClick={(e) => { e.stopPropagation(); setIsEditing(true); }}
            onTouchEnd={(e) => { e.preventDefault(); e.stopPropagation(); setIsEditing(true); }}
            className="p-2 rounded-lg hover:bg-secondary active:bg-secondary/80 transition-colors text-muted-foreground min-w-[40px] min-h-[40px] flex items-center justify-center"
            style={{ touchAction: 'manipulation', WebkitTapHighlightColor: 'transparent' }}
            title="Edit"
          >
            <Edit className="w-5 h-5" />
          </button>
          {/* Select mode toggle */}
          <button
            type="button"
            onClick={(e) => {
              e.stopPropagation()
              if (selectionMode) {
                setSelectionMode(false)
                setSelectedIds(new Set())
              } else {
                setSelectionMode(true)
              }
            }}
            className={`p-2 rounded-lg hover:bg-secondary active:bg-secondary/80 transition-colors min-w-[40px] min-h-[40px] flex items-center justify-center ${
              selectionMode ? 'text-primary bg-primary/10' : 'text-muted-foreground'
            }`}
            style={{ touchAction: 'manipulation', WebkitTapHighlightColor: 'transparent' }}
            title={selectionMode ? 'Cancel selection' : 'Select items'}
          >
            <CheckSquare className="w-5 h-5" />
          </button>
          {/* Scan duplicates button */}
          <button
            type="button"
            onClick={async (e) => {
              e.stopPropagation()
              if (isScanning) return
              setIsScanning(true)
              setScanResult(null)
              setScanProgress(null)
              try {
                const resp = await api.privateGallery.scanDuplicates(post.id) as any
                if (!resp.job_id) {
                  setScanResult({
                    results: [],
                    total_scanned: 0,
                    total_groups: 0,
                    person_id: null,
                    message: resp.message || 'Post has no person assigned',
                  })
                  setIsScanning(false)
                  return
                }
                setScanJobId(resp.job_id)
                // Poll for progress
                let pollFailures = 0
                const poll = async () => {
                  try {
                    const status = await api.privateGallery.getJobStatus(resp.job_id)
                    pollFailures = 0
                    const s = status as any
                    setScanProgress({
                      processed: s.processed_files || 0,
                      total: s.total_files || 0,
                      phase: s.current_phase || 'processing',
                      ...(s.compared_pairs !== undefined && s.total_pairs ? {
                        comparedPairs: s.compared_pairs,
                        totalPairs: s.total_pairs,
                      } : {}),
                    })
                    if (status.status === 'completed') {
                      setScanResult({
                        results: s.scan_results || [],
                        total_scanned: s.total_scanned || 0,
                        total_groups: s.total_groups || 0,
                        person_id: s.person_id || null,
                        message: s.message,
                      })
                      setIsScanning(false)
                      setScanJobId(null)
                      setScanProgress(null)
                    } else {
                      setTimeout(poll, 1000)
                    }
                  } catch {
                    pollFailures++
                    if (pollFailures >= 5) {
                      setIsScanning(false)
                      setScanJobId(null)
                      setScanProgress(null)
                    } else {
                      // Retry with backoff — don't give up immediately
                      setTimeout(poll, 2000)
                    }
                  }
                }
                poll()
              } catch (error) {
                console.error('Failed to scan duplicates:', error)
                alert('Failed to scan for duplicates')
                setIsScanning(false)
              }
            }}
            className="p-2 rounded-lg hover:bg-secondary active:bg-secondary/80 transition-colors text-muted-foreground min-w-[40px] min-h-[40px] flex items-center justify-center"
            style={{ touchAction: 'manipulation', WebkitTapHighlightColor: 'transparent' }}
            title="Scan for duplicates"
            disabled={isScanning}
          >
            {isScanning ? (
              <Loader2 className="w-5 h-5 animate-spin" />
            ) : (
              <ScanSearch className="w-5 h-5" />
            )}
          </button>
          {/* Delete post button */}
          <button
            type="button"
            onClick={async (e) => {
              e.stopPropagation()
              if (confirm('Delete this post and all its media? This cannot be undone.')) {
                try {
                  await api.privateGallery.deletePost(post.id)
                  await queryClient.invalidateQueries({ queryKey: ['private-gallery-posts'] })
                  queryClient.invalidateQueries({ queryKey: ['private-gallery-media'] })
                  queryClient.invalidateQueries({ queryKey: ['private-gallery-stats'] })
                  onClose()
                } catch (error) {
                  console.error('Failed to delete post:', error)
                  alert('Failed to delete post')
                }
              }
            }}
            className="flex p-2 rounded-lg hover:bg-destructive/20 transition-colors min-w-[40px] min-h-[40px] items-center justify-center"
            title="Delete post"
          >
            <Trash2 className="w-5 h-5 text-destructive" />
          </button>
        </>
      )}
    </>
  )

  return (
    <div className="h-full flex flex-col">
      {/* PrivateGalleryBundleLightbox */}
      {lightboxIndex !== null && detailLightboxItems.length > 0 && (
        <PrivateGalleryBundleLightbox
          items={detailLightboxItems}
          currentIndex={lightboxIndex}
          onClose={() => setLightboxIndex(null)}
          onNavigate={setLightboxIndex}
          onDelete={handleLightboxDelete}
        />
      )}

      {/* Header */}
      <div className="p-4 border-b border-border sticky top-0 bg-card z-10">
        <div className="flex items-center justify-between">
          {/* Left side: Avatar + Info */}
          <div className="flex items-center gap-3 flex-1 min-w-0">
            {/* Avatar */}
            <div className="w-10 h-10 rounded-full bg-gradient-to-br from-pink-500 to-violet-500 p-0.5 flex-shrink-0">
              <div className="w-full h-full rounded-full overflow-hidden bg-card">
                {post.profile_image_url ? (
                  <img src={post.profile_image_url} alt="" className="w-full h-full object-cover" />
                ) : (
                  <div className="w-full h-full flex items-center justify-center bg-secondary">
                    <User className="w-5 h-5 text-muted-foreground" />
                  </div>
                )}
              </div>
            </div>

            {/* Person info - visible on all screen sizes */}
            <div className="min-w-0 flex-1">
              <div className="flex items-center gap-2 flex-wrap">
                <p className="font-medium text-foreground truncate">{post.display_name || post.username}</p>
                {(post as any)._relationship && (
                  <span
                    className="px-2 py-0.5 rounded-full text-xs font-medium text-white flex-shrink-0"
                    style={{ backgroundColor: (post as any)._relationship.color }}
                  >
                    {(post as any)._relationship.name}
                  </span>
                )}
              </div>
              <p className="text-xs text-muted-foreground">
                {post.published_at ? formatRelativeTime(post.published_at) : 'Unknown date'}
              </p>
            </div>
          </div>

          {/* Desktop: Action buttons inline */}
          <div className="hidden lg:flex items-center gap-1 flex-shrink-0">
            {renderActionButtons()}
          </div>
        </div>
        {/* Mobile: Action buttons on separate row */}
        <div className="lg:hidden flex items-center gap-1 mt-2 ml-[3.25rem]">
          {renderActionButtons()}
        </div>
      </div>

      {/* Duplicate scan progress */}
      {isScanning && scanProgress && (
        <div className="mx-4 mt-2 p-3 bg-secondary/50 border border-border rounded-lg">
          <div className="flex items-center gap-2 mb-2">
            <Loader2 className="w-4 h-4 animate-spin text-primary" />
            <span className="text-sm font-medium text-foreground">
              Scanning for duplicates...
            </span>
          </div>
          <div className="text-xs text-muted-foreground">
            {scanProgress.phase === 'hashing' ? 'Computing hashes' : scanProgress.phase === 'comparing' ? 'Comparing hashes' : 'Starting'}
            {scanProgress.phase === 'hashing' && scanProgress.total > 0 && ` — ${scanProgress.processed}/${scanProgress.total} files`}
            {scanProgress.phase === 'comparing' && scanProgress.totalPairs ? ` — ${Math.round(((scanProgress.comparedPairs || 0) / scanProgress.totalPairs) * 100)}%` : ''}
          </div>
          {scanProgress.total > 0 && (
            <div className="mt-2 w-full bg-secondary rounded-full h-1.5">
              <div
                className="bg-primary h-1.5 rounded-full transition-all duration-300"
                style={{ width: `${scanProgress.phase === 'comparing' && scanProgress.totalPairs
                  ? Math.round(((scanProgress.comparedPairs || 0) / scanProgress.totalPairs) * 100)
                  : Math.round((scanProgress.processed / scanProgress.total) * 100)}%` }}
              />
            </div>
          )}
        </div>
      )}

      {/* Duplicate scan results */}
      {scanResult && (
        <div className="mx-4 mt-2 p-3 bg-secondary/50 border border-border rounded-lg max-h-[60vh] overflow-y-auto">
          <div className="flex items-center justify-between mb-2 sticky top-0 bg-secondary/80 backdrop-blur-sm py-1 z-10 -mx-3 px-3">
            <h4 className="text-sm font-medium text-foreground">
              Duplicate Scan Results
              <span className="ml-2 text-muted-foreground font-normal">
                ({scanResult.total_scanned} scanned)
              </span>
            </h4>
            <div className="flex items-center gap-1">
              {scanResult.results.length > 0 && (
                <button
                  onClick={() => {
                    const toSelect = new Set<number>()
                    for (const group of scanResult.results) {
                      if (group.items.length < 2) continue
                      const adj = new Map<number, Set<number>>()
                      for (const item of group.items) {
                        for (const [otherId, dist] of Object.entries(item.distances)) {
                          if (dist <= duplicateAutoSelectDistance) {
                            if (!adj.has(item.media_id)) adj.set(item.media_id, new Set())
                            adj.get(item.media_id)!.add(Number(otherId))
                          }
                        }
                      }
                      if (adj.size === 0) continue
                      // BFS to find clusters within threshold
                      const visited = new Set<number>()
                      for (const startId of adj.keys()) {
                        if (visited.has(startId)) continue
                        const cluster: number[] = []
                        const queue = [startId]
                        while (queue.length > 0) {
                          const id = queue.pop()!
                          if (visited.has(id)) continue
                          visited.add(id)
                          cluster.push(id)
                          for (const nb of adj.get(id) || []) {
                            if (!visited.has(nb)) queue.push(nb)
                          }
                        }
                        if (cluster.length < 2) continue
                        // Keep highest resolution, select all others
                        const items = cluster.map(id => group.items.find(i => i.media_id === id)!).filter(Boolean)
                        let best = items[0]
                        for (const item of items) {
                          if ((item.width || 0) * (item.height || 0) > (best.width || 0) * (best.height || 0)) best = item
                        }
                        for (const item of items) {
                          if (item.media_id !== best.media_id) toSelect.add(item.media_id)
                        }
                      }
                    }
                    if (toSelect.size === 0) return
                    setSelectedDupes(toSelect)
                  }}
                  className="flex items-center gap-1 px-2 py-1 rounded text-xs bg-amber-500/20 text-amber-400 hover:bg-amber-500/30 transition-colors"
                  title={`Auto-select lowest resolution items from duplicate pairs (distance<=${duplicateAutoSelectDistance})`}
                >
                  <Check className="w-3 h-3" />
                  Auto-select
                </button>
              )}
              {selectedDupes.size > 0 && (
                <button
                  onClick={async () => {
                    if (isDeletingDupes) return
                    const count = selectedDupes.size
                    if (!confirm(`Delete ${count} selected media item${count !== 1 ? 's' : ''}? This cannot be undone.`)) return
                    setIsDeletingDupes(true)
                    try {
                      const ids = Array.from(selectedDupes)
                      // Collect affected post_ids before deleting
                      const affectedPostIds = new Set<number>()
                      for (const group of scanResult.results) {
                        for (const item of group.items) {
                          if (selectedDupes.has(item.media_id)) {
                            affectedPostIds.add(item.post_id)
                          }
                        }
                      }
                      for (let i = 0; i < ids.length; i += 500) {
                        await api.privateGallery.batchDeleteMedia(ids.slice(i, i + 500))
                      }
                      // Remove deleted items from scan results
                      const newResults = scanResult.results
                        .map(group => ({
                          ...group,
                          items: group.items.filter(item => !selectedDupes.has(item.media_id))
                        }))
                        .filter(group => group.items.length > 1)
                      setScanResult({
                        ...scanResult,
                        results: newResults,
                        total_groups: newResults.length,
                      })
                      setSelectedDupes(new Set())
                      // Update current post's attachments if any were deleted from it
                      if (affectedPostIds.has(post.id)) {
                        setAttachments(prev => prev.filter(a => !ids.includes(a.id)))
                      }
                      // Cleanup empty posts (no media + no description)
                      const postIdsToCheck = Array.from(affectedPostIds)
                      if (postIdsToCheck.length > 0) {
                        try {
                          const cleanup = await api.privateGallery.cleanupEmptyPosts(postIdsToCheck)
                          if (cleanup.deleted_post_ids.includes(post.id)) {
                            // Current post was deleted — close the detail view
                            queryClient.invalidateQueries({ queryKey: ['private-gallery-posts'] })
                            queryClient.invalidateQueries({ queryKey: ['private-gallery-media'] })
                            queryClient.invalidateQueries({ queryKey: ['private-gallery-stats'] })
                            onClose()
                            return
                          }
                        } catch {
                          // Cleanup is best-effort
                        }
                      }
                      // Refresh gallery data
                      queryClient.invalidateQueries({ queryKey: ['private-gallery-posts'] })
                      queryClient.invalidateQueries({ queryKey: ['private-gallery-media'] })
                      queryClient.invalidateQueries({ queryKey: ['private-gallery-stats'] })
                    } catch (error) {
                      console.error('Failed to delete:', error)
                      alert('Failed to delete selected media')
                    } finally {
                      setIsDeletingDupes(false)
                    }
                  }}
                  disabled={isDeletingDupes}
                  className="flex items-center gap-1 px-2 py-1 rounded text-xs bg-destructive/20 text-destructive hover:bg-destructive/30 transition-colors"
                >
                  {isDeletingDupes ? <Loader2 className="w-3 h-3 animate-spin" /> : <Trash2 className="w-3 h-3" />}
                  Delete {selectedDupes.size}
                </button>
              )}
              <button
                onClick={() => { setScanResult(null); setSelectedDupes(new Set()) }}
                className="p-1 rounded hover:bg-secondary text-muted-foreground"
                title="Dismiss"
              >
                <X className="w-4 h-4" />
              </button>
            </div>
          </div>
          {scanResult.results.length === 0 ? (
            <p className="text-sm text-muted-foreground">{scanResult.message || 'No perceptual duplicates found.'}</p>
          ) : (
            <div className="space-y-4">
              <p className="text-sm text-amber-500">{scanResult.total_groups} duplicate group{scanResult.total_groups !== 1 ? 's' : ''} found — select items to delete</p>
              {(() => {
                // Build flat list of all dupe items for shift+click range selection
                const allDupeItems: Array<{ media_id: number; gi: number; ii: number }> = []
                scanResult.results.forEach((group, gi) => {
                  group.items.forEach((item, ii) => {
                    allDupeItems.push({ media_id: item.media_id, gi, ii })
                  })
                })
                return scanResult.results.map((group, gi) => (
                <div key={gi} className="p-2 bg-background border border-border rounded-lg">
                  <p className="text-xs font-medium text-muted-foreground mb-2">Group {gi + 1} ({group.items.length} items)</p>
                  <div className="grid grid-cols-3 sm:grid-cols-3 lg:grid-cols-4 gap-2">
                    {group.items.map((item, ii) => {
                      const token = api.getPrivateGalleryToken()
                      const tokenParam = token ? `?_token=${encodeURIComponent(token)}` : ''
                      const thumbUrl = `/api/private-gallery/thumbnail/${item.media_id}${tokenParam}`
                      const isSelected = selectedDupes.has(item.media_id)
                      const minDist = Object.values(item.distances).length > 0
                        ? Math.min(...Object.values(item.distances))
                        : null
                      const flatKey = `${gi}-${ii}`
                      return (
                        <button
                          key={item.media_id}
                          type="button"
                          className={`relative aspect-square bg-secondary rounded-lg overflow-hidden cursor-pointer transition-all ${
                            isSelected ? 'ring-2 ring-destructive ring-offset-1 ring-offset-background' : 'hover:ring-2 hover:ring-primary'
                          }`}
                          onClick={(e) => {
                            e.preventDefault()
                            setSelectedDupes(prev => {
                              const next = new Set(prev)
                              if (e.shiftKey && lastSelectedDupeIdx.current !== null && lastSelectedDupeIdx.current !== flatKey) {
                                const curFlat = allDupeItems.findIndex(d => d.gi === gi && d.ii === ii)
                                const lastParts = lastSelectedDupeIdx.current.split('-')
                                const lastFlat = allDupeItems.findIndex(d => d.gi === parseInt(lastParts[0]) && d.ii === parseInt(lastParts[1]))
                                if (curFlat >= 0 && lastFlat >= 0) {
                                  const start = Math.min(curFlat, lastFlat)
                                  const end = Math.max(curFlat, lastFlat)
                                  for (let i = start; i <= end; i++) {
                                    next.add(allDupeItems[i].media_id)
                                  }
                                }
                              } else {
                                if (next.has(item.media_id)) {
                                  next.delete(item.media_id)
                                } else {
                                  next.add(item.media_id)
                                }
                              }
                              return next
                            })
                            if (!e.shiftKey) lastSelectedDupeIdx.current = flatKey
                          }}
                        >
                          <img
                            src={thumbUrl}
                            alt={item.filename}
                            className={`w-full h-full object-cover ${isSelected ? 'opacity-60' : ''}`}
                            loading="lazy"
                            onError={(e) => { (e.target as HTMLImageElement).style.display = 'none' }}
                          />
                          {/* Checkmark overlay */}
                          <div className={`absolute top-1.5 left-1.5 w-5 h-5 rounded flex items-center justify-center transition-all ${
                            isSelected
                              ? 'bg-destructive text-white'
                              : 'bg-black/40 text-white/70 opacity-0 group-hover:opacity-100'
                          }`} style={{ opacity: isSelected ? 1 : undefined }}>
                            <Check className="w-3.5 h-3.5" />
                          </div>
                          {/* Info overlay at bottom */}
                          <div className="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/70 to-transparent p-1.5 pt-4">
                            <p className="text-[10px] text-white truncate" title={item.filename}>{item.filename}</p>
                            {item.width && item.height && (
                              <p className="text-[10px] text-white/80">{item.width}x{item.height}</p>
                            )}
                            <div className="flex justify-between items-center">
                              <span className="text-[10px] text-white/70">Post #{item.post_id}</span>
                              {minDist !== null && <span className="text-[10px] text-amber-300">d={minDist}</span>}
                            </div>
                          </div>
                        </button>
                      )
                    })}
                  </div>
                </div>
              ))
              })()}
            </div>
          )}
        </div>
      )}

      {/* Content */}
      <div className="flex-1 overflow-y-auto p-4 space-y-4">
        {isEditing ? (
          <>
            <div className="space-y-2">
              <label className="text-sm font-medium text-foreground">Date</label>
              <input
                type="date"
                value={editDate}
                onChange={(e) => setEditDate(e.target.value)}
                className="w-full px-3 py-2 bg-secondary border border-border rounded-lg text-foreground focus:outline-none focus:ring-2 focus:ring-primary"
              />
            </div>
            <div className="space-y-2">
              <label className="text-sm font-medium text-foreground">Description</label>
              <textarea
                value={editContent}
                onChange={(e) => setEditContent(e.target.value)}
                className="w-full px-3 py-2 bg-secondary border border-border rounded-lg text-foreground focus:outline-none focus:ring-2 focus:ring-primary min-h-[200px] resize-y"
                placeholder="Post description"
              />
            </div>
            <div className="space-y-2">
              <TagSearchSelector
                tags={availableTags}
                selectedTagIds={editTags}
                onChange={setEditTags}
                label="Tags"
              />
            </div>
          </>
        ) : (
          <>
            <h2 className="text-lg font-semibold text-foreground">{getPostDisplayTitle(post)}</h2>

            {/* Relationship & Tags in view mode */}
            <div className="flex flex-wrap gap-2">
              {(post as any)._relationship && (
                <span
                  className="px-2.5 py-1 rounded-full text-xs font-medium text-white"
                  style={{ backgroundColor: (post as any)._relationship.color }}
                >
                  {(post as any)._relationship.name}
                </span>
              )}
              {post.tags?.map((tag) => (
                <span
                  key={tag.id}
                  className="px-2.5 py-1 rounded-full text-xs font-medium"
                  style={{ backgroundColor: tag.color, color: '#fff' }}
                >
                  {tag.name}
                </span>
              ))}
            </div>

            {post.content && (
              <p className="text-muted-foreground whitespace-pre-wrap">{decodeHtmlEntities(post.content)}</p>
            )}
          </>
        )}

        {/* Batch delete bar - shown when items are selected */}
        {selectionMode && selectedIds.size > 0 && (
          <div className="p-3 bg-secondary/50 border border-border rounded-lg flex items-center justify-between gap-2">
            <div className="flex items-center gap-2">
              <span className="text-sm font-medium">
                {selectedIds.size} selected
              </span>
              <button
                onClick={() => {
                  if (selectedIds.size === attachments.length) {
                    setSelectedIds(new Set())
                  } else {
                    setSelectedIds(new Set(attachments.map(a => a.id)))
                  }
                }}
                className="text-xs text-primary hover:underline"
              >
                {selectedIds.size === attachments.length ? 'Deselect all' : 'Select all'}
              </button>
            </div>
            <button
              onClick={handleBatchDelete}
              disabled={isBatchDeleting}
              className="flex items-center gap-1.5 px-3 py-1.5 bg-red-500 text-white text-sm font-medium rounded-lg hover:bg-red-600 transition-colors disabled:opacity-50"
            >
              {isBatchDeleting ? (
                <Loader2 className="w-4 h-4 animate-spin" />
              ) : (
                <Trash2 className="w-4 h-4" />
              )}
              Delete
            </button>
          </div>
        )}

        {/* Attachments Grid / List */}
        <div className="space-y-3">
          <div className="flex items-center justify-between">
            <h3 className="font-medium text-foreground">
              Attachments ({attachmentTotal > attachments.length ? `${attachments.length} / ${attachmentTotal}` : attachments.length})
              {isEditing && !selectionMode && (
                <span className="text-xs text-muted-foreground ml-2">(click to delete)</span>
              )}
            </h3>
            {attachments.length > 0 && onToggleAttachmentView && (
              <div className="flex items-center gap-2">
                {attachmentViewMode === 'list' && (
                  <div className="relative">
                    <Search className="absolute left-2 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-muted-foreground pointer-events-none" />
                    <input
                      type="text"
                      value={fileSearch}
                      onChange={(e) => setFileSearch(e.target.value)}
                      placeholder="Filter files..."
                      className="h-7 w-40 pl-7 pr-7 text-xs rounded-md border border-border bg-background text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring"
                    />
                    {fileSearch && (
                      <button
                        onClick={() => setFileSearch('')}
                        className="absolute right-1.5 top-1/2 -translate-y-1/2 p-0.5 rounded-sm text-muted-foreground hover:text-foreground"
                      >
                        <X className="w-3 h-3" />
                      </button>
                    )}
                  </div>
                )}
                <div className="flex items-center bg-secondary rounded-lg p-0.5">
                  <button
                    onClick={onToggleAttachmentView}
                    className={`p-1.5 rounded-md transition-colors ${
                      (!attachmentViewMode || attachmentViewMode === 'grid')
                        ? 'bg-primary text-primary-foreground'
                        : 'text-muted-foreground hover:text-foreground'
                    }`}
                    title="Grid view"
                  >
                    <LayoutGrid className="w-3.5 h-3.5" />
                  </button>
                  <button
                    onClick={onToggleAttachmentView}
                    className={`p-1.5 rounded-md transition-colors ${
                      attachmentViewMode === 'list'
                        ? 'bg-primary text-primary-foreground'
                        : 'text-muted-foreground hover:text-foreground'
                    }`}
                    title="List view"
                  >
                    <List className="w-3.5 h-3.5" />
                  </button>
                </div>
              </div>
            )}
          </div>
          {attachments.length > 0 ? (
            attachmentViewMode === 'list' ? (
              <div className="border border-border rounded-lg overflow-hidden divide-y divide-border">
                {[...attachments]
                  .sort((a, b) => {
                    const nameA = (a.name || a.local_filename || '').toLowerCase()
                    const nameB = (b.name || b.local_filename || '').toLowerCase()
                    return nameA.localeCompare(nameB)
                  })
                  .filter((attachment) => {
                    if (!fileSearch) return true
                    const name = (attachment.name || attachment.local_filename || '').toLowerCase()
                    return name.includes(fileSearch.toLowerCase())
                  })
                  .map((attachment, index) => {
                  const isDeleting = deletingAttachmentId === attachment.id
                  const isConfirming = confirmDeleteId === attachment.id
                  const isVideo = attachment.file_type === 'video'
                  const filename = attachment.name || attachment.local_filename || 'file'

                  return (
                    <div
                      key={attachment.id}
                      className={`relative flex items-center gap-3 px-3 py-2.5 transition-colors cursor-pointer group ${
                        index % 2 === 0 ? 'bg-background' : 'bg-muted/30'
                      } hover:bg-accent/50`}
                      onClick={isEditing ? undefined : () => openLightbox(attachment as PaidContentAttachment)}
                    >
                      {/* Thumbnail */}
                      <div className="w-9 h-9 rounded overflow-hidden bg-secondary flex-shrink-0 relative ring-1 ring-border">
                        {attachment.id ? (
                          <img
                            src={`/api/private-gallery/thumbnail/${attachment.id}${api.getPrivateGalleryToken() ? `?_token=${encodeURIComponent(api.getPrivateGalleryToken()!)}` : ''}`}
                            alt=""
                            className="w-full h-full object-cover"
                            loading="lazy"
                          />
                        ) : (
                          <div className="w-full h-full flex items-center justify-center">
                            <File className="w-4 h-4 text-muted-foreground" />
                          </div>
                        )}
                        {isVideo && (
                          <div className="absolute inset-0 flex items-center justify-center bg-black/20">
                            <Play className="w-3 h-3 text-white drop-shadow" />
                          </div>
                        )}
                      </div>

                      {/* File info */}
                      <div className="flex-1 min-w-0">
                        <p className="text-sm font-medium text-foreground truncate">{filename}</p>
                        <div className="flex items-center gap-1.5 text-[11px] text-muted-foreground mt-0.5">
                          <span className="uppercase font-medium tracking-wide">{attachment.file_type}</span>
                          {attachment.file_size ? (
                            <>
                              <span className="text-border">·</span>
                              <span>
                                {attachment.file_size < 1024 * 1024
                                  ? `${(attachment.file_size / 1024).toFixed(0)} KB`
                                  : `${(attachment.file_size / (1024 * 1024)).toFixed(1)} MB`}
                              </span>
                            </>
                          ) : null}
                          {isVideo && attachment.duration ? (
                            <>
                              <span className="text-border">·</span>
                              <span>{Math.floor(attachment.duration / 60)}:{Math.floor(attachment.duration % 60).toString().padStart(2, '0')}</span>
                            </>
                          ) : null}
                          {attachment.width && attachment.height ? (
                            <>
                              <span className="text-border">·</span>
                              <span>{attachment.width}×{attachment.height}</span>
                            </>
                          ) : null}
                        </div>
                      </div>

                      {/* Delete button - only in edit mode */}
                      {isEditing && !isConfirming && !isDeleting && (
                        <button
                          onClick={(e) => {
                            e.stopPropagation()
                            setConfirmDeleteId(attachment.id)
                          }}
                          className="p-1.5 rounded-md text-muted-foreground hover:text-red-500 hover:bg-red-500/10 transition-colors flex-shrink-0 opacity-0 group-hover:opacity-100"
                          title="Delete attachment"
                        >
                          <Trash2 className="w-3.5 h-3.5" />
                        </button>
                      )}

                      {/* Confirmation overlay */}
                      {isConfirming && (
                        <div className="flex items-center gap-1.5 flex-shrink-0">
                          <button
                            onClick={(e) => { e.stopPropagation(); handleDeleteAttachment(attachment.id); }}
                            className="px-2.5 py-1 text-xs font-medium bg-red-500 text-white rounded-md hover:bg-red-600 transition-colors"
                          >
                            Delete
                          </button>
                          <button
                            onClick={(e) => { e.stopPropagation(); setConfirmDeleteId(null); }}
                            className="px-2.5 py-1 text-xs font-medium bg-secondary text-foreground rounded-md hover:bg-secondary/80 transition-colors"
                          >
                            Cancel
                          </button>
                        </div>
                      )}

                      {/* Deleting spinner */}
                      {isDeleting && (
                        <Loader2 className="w-4 h-4 text-muted-foreground animate-spin flex-shrink-0" />
                      )}
                    </div>
                  )
                })}
              </div>
            ) : (
              <div className="grid grid-cols-3 sm:grid-cols-3 lg:grid-cols-4 gap-2" style={selectionMode ? { userSelect: 'none' } : undefined}>
                {attachments.map((attachment, attIdx) => {
                  const isDeleting = deletingAttachmentId === attachment.id
                  const isConfirming = confirmDeleteId === attachment.id
                  const isSelected = selectedIds.has(attachment.id)

                  const handleAttSelection = (e: React.MouseEvent) => {
                    e.preventDefault()
                    setSelectedIds(prev => {
                      const next = new Set(prev)
                      if (e.shiftKey && lastSelectedAttIdx.current !== null && lastSelectedAttIdx.current !== attIdx) {
                        const start = Math.min(lastSelectedAttIdx.current, attIdx)
                        const end = Math.max(lastSelectedAttIdx.current, attIdx)
                        for (let i = start; i <= end; i++) {
                          next.add(attachments[i].id)
                        }
                      } else {
                        if (next.has(attachment.id)) {
                          next.delete(attachment.id)
                        } else {
                          next.add(attachment.id)
                        }
                      }
                      return next
                    })
                    if (!e.shiftKey) lastSelectedAttIdx.current = attIdx
                  }

                  return (
                    <div key={attachment.id} className={`relative group aspect-square ${selectionMode && isSelected ? 'ring-2 ring-primary ring-offset-1 ring-offset-background rounded-lg' : ''}`}>
                      {/* Thumbnail */}
                      <div className="w-full h-full">
                        <AttachmentThumbnail
                          attachment={attachment as PaidContentAttachment}
                          onClick={selectionMode ? (e: React.MouseEvent) => handleAttSelection(e) : isEditing ? undefined : () => openLightbox(attachment as PaidContentAttachment)}
                        />
                      </div>

                      {/* Selection checkbox */}
                      {selectionMode && (
                        <button
                          onClick={(e) => {
                            e.stopPropagation()
                            handleAttSelection(e)
                          }}
                          className={`absolute top-1 left-1 w-6 h-6 rounded-md border-2 flex items-center justify-center transition-colors z-10 ${
                            isSelected
                              ? 'bg-primary border-primary text-primary-foreground'
                              : 'bg-black/50 border-white/70 text-transparent hover:border-white'
                          }`}
                        >
                          {isSelected && <Check className="w-4 h-4" />}
                        </button>
                      )}

                      {/* Delete button - only in edit mode */}
                      {isEditing && !selectionMode && !isConfirming && !isDeleting && (
                        <button
                          onClick={(e) => {
                            e.stopPropagation()
                            setConfirmDeleteId(attachment.id)
                          }}
                          className="absolute top-1 right-1 p-1.5 rounded-full bg-red-500 text-white shadow-md hover:bg-red-600 transition-colors z-10"
                          title="Delete attachment"
                        >
                          <Trash2 className="w-3.5 h-3.5" />
                        </button>
                      )}

                      {/* Confirmation overlay */}
                      {isConfirming && (
                        <div className="absolute inset-0 bg-black/80 rounded-lg flex flex-col items-center justify-center gap-2 p-2 z-10">
                          <AlertTriangle className="w-5 h-5 text-yellow-500" />
                          <span className="text-xs text-white text-center">Delete?</span>
                          <div className="flex gap-1">
                            <button
                              onClick={() => handleDeleteAttachment(attachment.id)}
                              className="px-2 py-1 text-xs bg-red-500 text-white rounded hover:bg-red-600"
                            >
                              Yes
                            </button>
                            <button
                              onClick={() => setConfirmDeleteId(null)}
                              className="px-2 py-1 text-xs bg-secondary text-foreground rounded hover:bg-secondary/80"
                            >
                              No
                            </button>
                          </div>
                        </div>
                      )}

                      {/* Deleting spinner overlay */}
                      {isDeleting && (
                        <div className="absolute inset-0 bg-black/80 rounded-lg flex items-center justify-center z-10">
                          <Loader2 className="w-6 h-6 text-white animate-spin" />
                        </div>
                      )}
                    </div>
                  )
                })}
              </div>
            )
          ) : (
            <div className="text-center py-8 text-muted-foreground">
              <File className="w-8 h-8 mx-auto mb-2 opacity-50" />
              <p className="text-sm">No attachments</p>
            </div>
          )}
          {/* Infinite scroll sentinel for large posts */}
          {attachments.length < attachmentTotal && (
            <div
              ref={loadMoreSentinelRef}
              className="flex items-center justify-center py-4 text-sm text-muted-foreground"
            >
              {isLoadingMore && <Loader2 className="w-4 h-4 animate-spin mr-2" />}
              {isLoadingMore ? 'Loading more...' : `${attachmentTotal - attachments.length} more`}
            </div>
          )}

          {/* Batch delete bar (bottom) */}
          {selectionMode && selectedIds.size > 0 && (
            <div className="p-3 bg-secondary/50 border border-border rounded-lg flex items-center justify-between gap-2">
              <div className="flex items-center gap-2">
                <span className="text-sm font-medium">
                  {selectedIds.size} selected
                </span>
                <button
                  onClick={() => {
                    if (selectedIds.size === attachments.length) {
                      setSelectedIds(new Set())
                    } else {
                      setSelectedIds(new Set(attachments.map(a => a.id)))
                    }
                  }}
                  className="text-xs text-primary hover:underline"
                >
                  {selectedIds.size === attachments.length ? 'Deselect all' : 'Select all'}
                </button>
              </div>
              <button
                onClick={handleBatchDelete}
                disabled={isBatchDeleting}
                className="flex items-center gap-1.5 px-3 py-1.5 bg-red-500 text-white text-sm font-medium rounded-lg hover:bg-red-600 transition-colors disabled:opacity-50"
              >
                {isBatchDeleting ? (
                  <Loader2 className="w-4 h-4 animate-spin" />
                ) : (
                  <Trash2 className="w-4 h-4" />
                )}
                Delete
              </button>
            </div>
          )}
        </div>
      </div>

    </div>
  )
}

// Parse filters from URL search params
function parseFiltersFromParams(searchParams: URLSearchParams): FeedFilters {
  // Parse has_tags and has_description - undefined means show all, true/false means filter
  const hasTagsParam = searchParams.get('has_tags')
  const hasDescParam = searchParams.get('has_desc')

  return {
    person_id: searchParams.get('person') ? parseInt(searchParams.get('person')!) : undefined,
    person_group_id: searchParams.get('person_group') ? parseInt(searchParams.get('person_group')!) : undefined,
    relationship_id: searchParams.get('relationship') ? parseInt(searchParams.get('relationship')!) : undefined,
    tag_id: searchParams.get('tag') ? parseInt(searchParams.get('tag')!) : undefined,
    file_type: searchParams.get('type') || undefined,
    date_from: searchParams.get('from') || undefined,
    date_to: searchParams.get('to') || undefined,
    sort_by: searchParams.get('sort') || 'created_at',
    sort_order: (searchParams.get('order') as 'asc' | 'desc') || 'desc',
    search: searchParams.get('q') || undefined,
    has_tags: hasTagsParam === 'true' ? true : hasTagsParam === 'false' ? false : undefined,
    has_description: hasDescParam === 'true' ? true : hasDescParam === 'false' ? false : undefined,
    unread_only: searchParams.get('unread_only') === 'true' ? true : undefined,
    min_resolution: searchParams.get('min_res') ? parseInt(searchParams.get('min_res')!) : undefined,
  }
}

// Convert filters to URL search params
function filtersToParams(filters: FeedFilters): URLSearchParams {
  const params = new URLSearchParams()
  if (filters.person_id) params.set('person', filters.person_id.toString())
  if (filters.person_group_id) params.set('person_group', filters.person_group_id.toString())
  if (filters.relationship_id) params.set('relationship', filters.relationship_id.toString())
  if (filters.tag_id) params.set('tag', filters.tag_id.toString())
  if (filters.file_type) params.set('type', filters.file_type)
  if (filters.date_from) params.set('from', filters.date_from)
  if (filters.date_to) params.set('to', filters.date_to)
  if (filters.sort_by && filters.sort_by !== 'created_at') params.set('sort', filters.sort_by)
  if (filters.sort_order && filters.sort_order !== 'desc') params.set('order', filters.sort_order)
  if (filters.search) params.set('q', filters.search)
  if (filters.has_tags) params.set('has_tags', 'true')
  if (filters.has_description) params.set('has_desc', 'true')
  if (filters.unread_only) params.set('unread_only', 'true')
  if (filters.min_resolution) params.set('min_res', filters.min_resolution.toString())
  return params
}

export default function PrivateGalleryFeed() {
  const navigate = useNavigate()
  const { isUnlocked, isLoading: isAuthLoading, lock } = usePrivateGalleryContext()

  useBreadcrumb(breadcrumbConfig['/private-gallery'])

  // Redirect if not authenticated (wait for status check to complete)
  useEffect(() => {
    if (!isAuthLoading && !isUnlocked) {
      navigate('/private-gallery/unlock', { replace: true })
    }
  }, [isAuthLoading, isUnlocked, navigate])

  const [searchParams, setSearchParams] = useSearchParams()
  const [selectedPost, setSelectedPost] = useState<PaidContentPost | null>(null)
  const [pendingSelectPostId, setPendingSelectPostId] = useState<number | null>(null)
  // Default to 'social' view on all devices
  const [viewMode, setViewMode] = useState<'feed' | 'gallery' | 'social'>('social')
  const [attachmentViewMode, setAttachmentViewMode] = useState<'grid' | 'list'>('grid')

  // Social view lightbox state - use index instead of post for correct attachment navigation
  const [socialLightboxIndex, setSocialLightboxIndex] = useState<number | null>(null)

  // Mobile full post modal state
  const [mobileSelectedPost, setMobileSelectedPost] = useState<PaidContentPost | null>(null)

  // Edit post modal state
  const [editingPost, setEditingPost] = useState<PaidContentPost | null>(null)

  // Initialize filters from URL params
  const [filters, setFilters] = useState<FeedFilters>(() => parseFiltersFromParams(searchParams))

  // Sync filters to URL when they change (separate from state update to avoid render-time setState)
  useEffect(() => {
    const params = filtersToParams(filters)
    setSearchParams(params, { replace: true })
  }, [filters, setSearchParams])

  const [showFilters, setShowFilters] = useState(false)
  const scrollContainerRef = useRef<HTMLDivElement>(null)

  // Helper to scroll to top of the list
  const scrollToTop = useCallback(() => {
    if (scrollContainerRef.current) {
      scrollContainerRef.current.scrollTo({ top: 0, behavior: 'smooth' })
    } else {
      window.scrollTo({ top: 0, behavior: 'smooth' })
    }
  }, [])

  // Scroll to top when filters change to make refresh visible
  const prevFilterKeyRef = useRef<string | null>(null)
  useEffect(() => {
    const currentFilterKey = JSON.stringify(filters)
    if (prevFilterKeyRef.current !== null && prevFilterKeyRef.current !== currentFilterKey) {
      // Filters changed, scroll to top
      scrollToTop()
    }
    prevFilterKeyRef.current = currentFilterKey
  }, [filters, scrollToTop])

  // Refs to avoid stale closures in scroll callback
  const hasNextPageRef = useRef(false)
  const isFetchingNextPageRef = useRef(false)
  const fetchNextPageRef = useRef<() => void>(() => {})

  // Mobile detection for conditional ref attachment
  const [isDesktop, setIsDesktop] = useState(() => typeof window !== 'undefined' && window.innerWidth >= 1024)
  useEffect(() => {
    const checkDesktop = () => setIsDesktop(window.innerWidth >= 1024)
    window.addEventListener('resize', checkDesktop)
    return () => window.removeEventListener('resize', checkDesktop)
  }, [])

  // Force feed view to social on mobile (feed needs split pane)
  useEffect(() => {
    if (!isDesktop && viewMode === 'feed') {
      setViewMode('social')
    }
  }, [isDesktop, viewMode])

  // Lightbox state for inline viewing from feed
  const [lightboxPost, setLightboxPost] = useState<PaidContentPost | null>(null)

  // Gallery lightbox state - simple index-based (like socialLightboxIndex)
  const [galleryLightboxIndex, setGalleryLightboxIndex] = useState<number | null>(null)

  // Slideshow state
  const [slideshowActive, setSlideshowActive] = useState(false)
  const [slideshowPostItems, setSlideshowPostItems] = useState<PrivateGalleryMediaItem[] | null>(null)
  const [slideshowPostId, setSlideshowPostId] = useState<number | null>(null)
  const [slideshowPostTotal, setSlideshowPostTotal] = useState(0)
  const [slideshowPostLoading, setSlideshowPostLoading] = useState(false)

  // Gallery shuffle state (backend-driven random order)
  const [galleryShuffleEnabled, setGalleryShuffleEnabled] = useState(false)
  const [galleryShuffleSeed, setGalleryShuffleSeed] = useState<number>(0)
  const [galleryShuffleItems, setGalleryShuffleItems] = useState<PrivateGalleryMediaItem[]>([])
  const [galleryShuffleHasMore, setGalleryShuffleHasMore] = useState(false)
  const [galleryShuffleTotal, setGalleryShuffleTotal] = useState(0)
  const [galleryShuffleLoading, setGalleryShuffleLoading] = useState(false)
  const galleryShuffleOffsetRef = useRef(0)

  const fetchShufflePage = useCallback(async (seed: number, offset: number, reset: boolean) => {
    setGalleryShuffleLoading(true)
    try {
      const response = await api.privateGallery.getMedia({
        offset,
        limit: 200,
        person_id: filters.person_id,
        person_group_id: filters.person_group_id,
        relationship_id: filters.relationship_id,
        tag_ids: filters.tag_id ? [filters.tag_id] : undefined,
        file_type: filters.file_type as 'image' | 'video' | 'all' | undefined,
        date_from: filters.date_from,
        date_to: filters.date_to,
        search: filters.search || undefined,
        has_tags: filters.has_tags,
        has_description: filters.has_description,
        min_resolution: filters.min_resolution,
        shuffle: true,
        shuffle_seed: seed,
      })
      const newItems: PrivateGalleryMediaItem[] = ((response.items || []) as any[])
        .filter((item: any) => item.file_type === 'image' || item.file_type === 'video')
        .map((item: any) => ({
          id: item.id,
          storage_id: item.storage_id || item.id.toString(),
          filename: item.filename || '',
          description: item.description || null,
          file_type: item.file_type as 'image' | 'video' | 'other',
          file_size: item.file_size || 0,
          width: item.width, height: item.height, duration: item.duration,
          person: item.person || null,
          media_date: item.media_date || '',
          tags: item.tags || [],
          thumbnail_url: item.thumbnail_url || '',
          file_url: item.file_url || '',
          stream_url: item.stream_url || '',
          created_at: item.created_at,
        }))
      if (reset) {
        setGalleryShuffleItems(newItems)
      } else {
        setGalleryShuffleItems(prev => [...prev, ...newItems])
      }
      setGalleryShuffleTotal(response.total)
      setGalleryShuffleHasMore(response.has_more)
      galleryShuffleOffsetRef.current = offset + 200
    } catch (e) {
      console.error('Failed to fetch shuffle page:', e)
    } finally {
      setGalleryShuffleLoading(false)
    }
  }, [filters])

  // Tag mode state for bulk tagging in gallery view
  const [tagMode, setTagMode] = useState(false)
  const [selectedPostIds, setSelectedPostIds] = useState<Set<number>>(new Set())
  const [selectedTagIds, setSelectedTagIds] = useState<Set<number>>(new Set())

  // Upload modal state
  const [showUploadModal, setShowUploadModal] = useState(false)

  // Simpcity monitored threads count
  const { data: simpcityThreadsData } = useQuery({
    queryKey: ['private-gallery-simpcity-threads-count'],
    queryFn: () => api.privateGallery.getSimpcityThreads(),
    staleTime: 60000,
  })
  const monitoredThreadCount = simpcityThreadsData?.threads?.filter((t: { monitoring_enabled: boolean }) => t.monitoring_enabled)?.length ?? 0

  // Gallery-level duplicate scan state
  const [isGalleryScanActive, setIsGalleryScanActive] = useState(false)
  const [, setGalleryScanJobId] = useState<string | null>(null)
  const [galleryScanProgress, setGalleryScanProgress] = useState<{
    processed: number; total: number; phase: string;
    comparedPairs?: number; totalPairs?: number; currentPerson?: string
  } | null>(null)
  const [galleryScanResult, setGalleryScanResult] = useState<{
    results: Array<{
      person_name: string
      items: Array<{
        media_id: number; post_id: number; person_id: number; person_name: string
        storage_id: string; filename: string; file_type: string
        width: number | null; height: number | null
        distances: Record<string, number>
      }>
    }>
    total_scanned: number; total_groups: number; message?: string
  } | null>(null)
  const [gallerySelectedDupes, setGallerySelectedDupes] = useState<Set<number>>(new Set())
  const [galleryIsDeletingDupes, setGalleryIsDeletingDupes] = useState(false)
  const lastGalleryDupeIdx = useRef<string | null>(null)

  const limit = 10

  // Stable filter key for query - ensures proper cache invalidation
  const filterKey = JSON.stringify(filters)

  // Infinite query for endless scrolling - fetches private gallery media items
  const {
    data,
    isLoading,
    isFetchingNextPage,
    hasNextPage,
    fetchNextPage,
  } = useInfiniteQuery({
    queryKey: ['private-gallery-posts', filterKey],
    queryFn: async ({ pageParam = 0 }) => {
      const response = await api.privateGallery.getPosts({
        offset: pageParam * limit,
        limit,
        person_id: filters.person_id,
        person_group_id: filters.person_group_id,
        relationship_id: filters.relationship_id,
        tag_ids: filters.tag_id ? [filters.tag_id] : undefined,
        file_type: filters.file_type as 'image' | 'video' | 'all' | undefined,
        date_from: filters.date_from,
        date_to: filters.date_to,
        sort_by: (filters.sort_by || 'created_at') as 'media_date' | 'created_at',
        sort_order: filters.sort_order || 'desc',
        search: filters.search || undefined,
        has_tags: filters.has_tags,
        has_description: filters.has_description,
        unread_only: filters.unread_only,
      })
      return response
    },
    getNextPageParam: (lastPage, allPages) => {
      if (!lastPage) return undefined
      if (!lastPage.has_more) return undefined
      return allPages?.length ?? 0
    },
    initialPageParam: 0,
    refetchOnMount: true,
    staleTime: 30000,
    enabled: isUnlocked,
  })

  // Separate infinite query for gallery view — fetches flat media items via /media endpoint
  // (not capped per-post like the posts query)
  const galleryMediaLimit = 100
  const {
    data: galleryMediaData,
    isLoading: isGalleryMediaLoading,
    isFetchingNextPage: isGalleryMediaFetchingNext,
    hasNextPage: hasGalleryMediaNextPage,
    fetchNextPage: fetchGalleryMediaNextPage,
  } = useInfiniteQuery({
    queryKey: ['private-gallery-media', filterKey],
    queryFn: async ({ pageParam = 0 }) => {
      const response = await api.privateGallery.getMedia({
        offset: pageParam * galleryMediaLimit,
        limit: galleryMediaLimit,
        person_id: filters.person_id,
        person_group_id: filters.person_group_id,
        relationship_id: filters.relationship_id,
        tag_ids: filters.tag_id ? [filters.tag_id] : undefined,
        file_type: filters.file_type as 'image' | 'video' | 'all' | undefined,
        date_from: filters.date_from,
        date_to: filters.date_to,
        sort_by: (filters.sort_by || 'created_at') as 'media_date' | 'created_at' | 'filename',
        sort_order: filters.sort_order || 'desc',
        search: filters.search || undefined,
        has_tags: filters.has_tags,
        has_description: filters.has_description,
        unread_only: filters.unread_only,
        min_resolution: filters.min_resolution,
      })
      return response
    },
    getNextPageParam: (lastPage, allPages) => {
      if (!lastPage?.has_more) return undefined
      return allPages?.length ?? 0
    },
    initialPageParam: 0,
    staleTime: 30000,
    enabled: isUnlocked && viewMode === 'gallery',
  })

  // Convert posts from API to PaidContentPost format for component reuse
  const token = api.getPrivateGalleryToken()
  const tokenParam = token ? `?_token=${encodeURIComponent(token)}` : ''

  const apiPosts = data?.pages?.flatMap(page => page.posts || []) || []
  const posts: PaidContentPost[] = apiPosts.map((post) => {
    const allTags = [...(post.tags || [])]

    return {
      id: post.id,
      creator_id: post.person_id || 0,
      post_id: `private-post-${post.id}`,
      title: '',
      content: post.description || '',
      published_at: post.media_date,
      added_at: post.created_at,
      has_attachments: post.attachment_count > 0 ? 1 : 0,
      attachment_count: post.attachment_count,
      downloaded: 1,
      download_date: post.created_at,
      is_favorited: 0,
      is_viewed: post.is_read ? 1 : 0,
      is_pinned: 0,
      pinned_at: null,
      username: post.person?.name || 'private',
      platform: 'private-gallery',
      service_id: 'private',
      display_name: post.person?.name || null,
      profile_image_url: null,
      identity_id: post.person_id,
      attachments: post.attachments.map((att, idx) => ({
        id: att.id,
        post_id: post.id,
        attachment_index: idx,
        name: att.filename,
        file_type: att.file_type as 'image' | 'video' | 'other',
        extension: att.filename.split('.').pop() || '',
        server_path: att.file_url,
        download_url: att.file_url + tokenParam,
        file_size: att.file_size,
        file_hash: '',
        status: 'completed' as const,
        error_message: null,
        download_attempts: 0,
        local_path: att.file_type === 'video' ? att.stream_url + tokenParam : att.file_url + tokenParam,
        local_filename: att.filename,
        downloaded_at: post.created_at,
        created_at: post.created_at,
        width: att.width,
        height: att.height,
        duration: att.duration,
        media_date: att.media_date,
        thumbnail_url: att.thumbnail_url + tokenParam,
      })),
      embeds: [],
      tags: allTags.map(tag => ({ ...tag, description: null })),
      _relationship: post.person?.relationship || null,
      image_count: post.attachments.filter(a => a.file_type === 'image').length,
      video_count: post.attachments.filter(a => a.file_type === 'video').length,
    } as PaidContentPost
  })

  // Auto-select post after upload completes
  useEffect(() => {
    if (pendingSelectPostId && posts.length > 0) {
      const newPost = posts.find(p => p.id === pendingSelectPostId)
      if (newPost) {
        setSelectedPost(newPost)
        setPendingSelectPostId(null)
      }
    }
  }, [pendingSelectPostId, posts])

  // Also create mediaItems array for lightbox (flattened from all posts)
  const mediaItems: PrivateMediaItem[] = apiPosts.flatMap(post =>
    post.attachments.map(att => ({
      id: att.id,
      storage_id: att.id.toString(),
      filename: att.filename,
      description: post.description,
      file_type: att.file_type as 'image' | 'video' | 'other',
      file_size: att.file_size,
      file_hash: '',
      mime_type: '',
      width: att.width,
      height: att.height,
      duration: att.duration,
      person_id: post.person_id,
      person: post.person ? {
        id: post.person.id,
        name: post.person.name,
        relationship: post.person.relationship
      } : null,
      media_date: att.media_date || post.media_date,
      post_date: post.media_date,
      thumbnail_url: att.thumbnail_url,
      stream_url: att.stream_url,
      file_url: att.file_url,
      tags: post.tags,
      created_at: post.created_at,
    }))
  )

  const totalItems = data?.pages?.[0]?.total || 0
  const totalMediaItems = data?.pages?.[0]?.total_media || 0

  // Gallery view: flat media items from the /media endpoint (not capped per-post)
  const galleryMediaItems: PrivateMediaItem[] = useMemo(() => {
    if (!galleryMediaData?.pages) return []
    const tkn = api.getPrivateGalleryToken()
    const tkp = tkn ? `?_token=${encodeURIComponent(tkn)}` : ''
    return galleryMediaData.pages.flatMap(page =>
      (page.items || []).map((item: any) => ({
        id: item.id,
        storage_id: item.storage_id || item.id.toString(),
        filename: item.filename,
        description: item.description || null,
        file_hash: item.file_hash || '',
        file_size: item.file_size || 0,
        file_type: item.file_type as 'image' | 'video' | 'other',
        mime_type: item.mime_type || '',
        width: item.width,
        height: item.height,
        duration: item.duration,
        person_id: item.person_id,
        person: item.person || null,
        media_date: item.media_date || '',
        thumbnail_url: item.thumbnail_url + tkp,
        stream_url: item.stream_url + tkp,
        file_url: item.file_url + tkp,
        tags: item.tags || [],
        created_at: item.created_at,
        post_id: item.post_id,
      } as PrivateMediaItem & { post_id: number }))
    )
  }, [galleryMediaData])

  const totalGalleryMedia = galleryMediaData?.pages?.[0]?.total || 0

  // Keep refs in sync for IntersectionObserver callback (avoids stale closures)
  useEffect(() => {
    if (viewMode === 'gallery') {
      hasNextPageRef.current = !!hasGalleryMediaNextPage
      isFetchingNextPageRef.current = isGalleryMediaFetchingNext
      fetchNextPageRef.current = fetchGalleryMediaNextPage
    } else {
      hasNextPageRef.current = !!hasNextPage
      isFetchingNextPageRef.current = isFetchingNextPage
      fetchNextPageRef.current = fetchNextPage
    }
  }, [viewMode, hasNextPage, isFetchingNextPage, fetchNextPage, hasGalleryMediaNextPage, isGalleryMediaFetchingNext, fetchGalleryMediaNextPage])

  // Keyboard navigation for feed and social views
  useEffect(() => {
    if (viewMode !== 'social' && viewMode !== 'feed') return

    const handleKeyDown = (e: KeyboardEvent) => {
      // Don't handle if user is typing in an input
      if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) return
      // Don't handle if lightbox is open
      if (socialLightboxIndex !== null || lightboxPost || galleryLightboxIndex !== null) return

      if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
        e.preventDefault()

        if (!posts || posts.length === 0) return

        const currentIndex = selectedPost
          ? posts.findIndex((p: PaidContentPost) => p.id === selectedPost.id)
          : -1

        let newIndex: number
        if (e.key === 'ArrowDown') {
          newIndex = currentIndex < posts.length - 1 ? currentIndex + 1 : currentIndex
        } else {
          newIndex = currentIndex > 0 ? currentIndex - 1 : 0
        }

        if (newIndex !== currentIndex && posts[newIndex]) {
          const newPost = posts[newIndex]
          setSelectedPost(newPost)

          // Scroll the post into view in the list
          const postElement = scrollContainerRef.current?.querySelector(`[data-post-id="${newPost.id}"]`)
          if (postElement) {
            postElement.scrollIntoView({ behavior: 'smooth', block: 'nearest' })
          }
        }
      }
    }

    window.addEventListener('keydown', handleKeyDown)
    return () => window.removeEventListener('keydown', handleKeyDown)
  }, [viewMode, posts, selectedPost, socialLightboxIndex, lightboxPost, galleryLightboxIndex])

  // Get total count from first page response
  const totalFilteredPosts = totalItems

  // Fetch persons for filtering
  const { data: persons = [] } = useQuery({
    queryKey: ['private-gallery-persons'],
    queryFn: () => api.privateGallery.getPersons(),
    enabled: isUnlocked,
  })

  // Fetch person groups for filtering
  const { data: personGroups = [] } = useQuery({
    queryKey: ['private-gallery-person-groups'],
    queryFn: () => api.privateGallery.getPersonGroups(),
    enabled: isUnlocked,
  })

  // Fetch selected group detail for scoping other filters
  const { data: selectedGroupDetail } = useQuery({
    queryKey: ['private-gallery-person-group', filters.person_group_id],
    queryFn: () => api.privateGallery.getPersonGroup(filters.person_group_id!),
    enabled: isUnlocked && !!filters.person_group_id,
  })

  // Sync group settings into filters when group detail loads
  useEffect(() => {
    if (selectedGroupDetail) {
      setFilters((prev) => ({
        ...prev,
        min_resolution: selectedGroupDetail.min_resolution || undefined,
      }))
    }
  }, [selectedGroupDetail])

  // Fetch relationships for filtering
  const { data: relationships = [] } = useQuery({
    queryKey: ['private-gallery-relationships'],
    queryFn: () => api.privateGallery.getRelationships(),
    enabled: isUnlocked,
  })

  // Assigned-only relationships for filter dropdown
  const { data: assignedRelationships = [] } = useQuery({
    queryKey: ['private-gallery-relationships-assigned'],
    queryFn: () => api.privateGallery.getRelationships({ assigned_only: true }),
    enabled: isUnlocked,
  })

  // Tags for filtering and bulk tagging
  const { data: availableTags = [] } = useQuery({
    queryKey: ['private-gallery-tags'],
    queryFn: () => api.privateGallery.getTags(),
    enabled: isUnlocked,
  })

  // Assigned-only tags for filter dropdown
  const { data: assignedTags = [] } = useQuery({
    queryKey: ['private-gallery-tags-assigned'],
    queryFn: () => api.privateGallery.getTags({ assigned_only: true }),
    enabled: isUnlocked,
  })

  // Config for duplicate auto-select distance
  const { data: galleryConfig } = useQuery({
    queryKey: ['private-gallery-config'],
    queryFn: () => api.privateGallery.getConfig(),
    enabled: isUnlocked,
  })
  const duplicateAutoSelectDistance = Number((galleryConfig as Record<string, unknown>)?.duplicate_auto_select_distance) || 2

  const queryClient = useQueryClient()

  // Query for new posts count since last seen
  const { data: newPostsData } = useQuery({
    queryKey: ['private-gallery-new-posts-count'],
    queryFn: () => api.privateGallery.getNewPostsCount(),
    staleTime: 60000,
    refetchInterval: 60000,
    enabled: isUnlocked,
  })
  const newPostsCount = newPostsData?.count ?? 0

  // Mutation to mark all posts as read
  const markSeenMutation = useMutation({
    mutationFn: () => api.privateGallery.markPostsSeen(),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['private-gallery-new-posts-count'] })
      queryClient.resetQueries({ queryKey: ['private-gallery-posts'] })
      queryClient.resetQueries({ queryKey: ['private-gallery-media'] })
      if (filters.unread_only) {
        handleFilterChange('unread_only', undefined)
      }
    },
  })

  // Mutation to toggle read status on a single post
  const toggleReadMutation = useMutation({
    mutationFn: ({ postId, is_read }: { postId: number; is_read: boolean }) =>
      api.privateGallery.updatePostReadStatus(postId, is_read),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['private-gallery-new-posts-count'] })
      queryClient.invalidateQueries({ queryKey: ['private-gallery-posts'] })
      queryClient.invalidateQueries({ queryKey: ['private-gallery-media'] })
    },
  })

  // Mutation for applying tags to multiple media items
  const applyTagsMutation = useMutation({
    mutationFn: ({ postIds, tagIds }: { postIds: number[]; tagIds: number[] }) =>
      api.privateGallery.batchUpdateTags({ media_ids: postIds, add_tag_ids: tagIds }),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['private-gallery-posts'] })
      setSelectedPostIds(new Set())
      setSelectedTagIds(new Set())
      setTagMode(false)
    },
  })

  // Toggle post selection in tag mode
  const togglePostSelection = (postId: number) => {
    setSelectedPostIds(prev => {
      const newSet = new Set(prev)
      if (newSet.has(postId)) {
        newSet.delete(postId)
      } else {
        newSet.add(postId)
      }
      return newSet
    })
  }

  // Toggle tag selection
  const toggleTagSelection = (tagId: number) => {
    setSelectedTagIds(prev => {
      const newSet = new Set(prev)
      if (newSet.has(tagId)) {
        newSet.delete(tagId)
      } else {
        newSet.add(tagId)
      }
      return newSet
    })
  }

  // Select all visible posts
  const selectAllPosts = () => {
    if (viewMode === 'gallery') {
      const allPostIds = new Set(galleryMediaItems.map((m: any) => m.post_id).filter(Boolean))
      setSelectedPostIds(allPostIds)
    } else {
      const allPostIds = new Set(posts.map(p => p.id))
      setSelectedPostIds(allPostIds)
    }
  }

  // Clear selections and exit tag mode
  const exitTagMode = () => {
    setTagMode(false)
    setSelectedPostIds(new Set())
    setSelectedTagIds(new Set())
  }

  // Apply selected tags to selected posts
  const applyTags = () => {
    if (selectedPostIds.size === 0 || selectedTagIds.size === 0) return
    applyTagsMutation.mutate({
      postIds: Array.from(selectedPostIds),
      tagIds: Array.from(selectedTagIds),
    })
  }

  // Infinite scroll using scroll event
  useEffect(() => {
    // On mobile, always use window scroll. On desktop, use scroll container if available.
    const scrollContainer = isDesktop ? scrollContainerRef.current : null

    const handleScroll = () => {
      if (!hasNextPageRef.current || isFetchingNextPageRef.current) return

      let shouldFetch = false

      if (scrollContainer) {
        // Desktop: check scroll container
        const { scrollTop, scrollHeight, clientHeight } = scrollContainer
        shouldFetch = scrollHeight - scrollTop - clientHeight < 500
      } else {
        // Mobile: check window/document scroll
        const scrollTop = window.scrollY || document.documentElement.scrollTop
        const scrollHeight = document.documentElement.scrollHeight
        const clientHeight = window.innerHeight
        shouldFetch = scrollHeight - scrollTop - clientHeight < 500
      }

      if (shouldFetch) {
        fetchNextPageRef.current()
      }
    }

    // Attach to appropriate scroll target
    const target = scrollContainer || window
    target.addEventListener('scroll', handleScroll, { passive: true })

    // Check immediately and after a short delay (for initial render)
    handleScroll()
    const timeoutId = setTimeout(handleScroll, 500)

    return () => {
      target.removeEventListener('scroll', handleScroll)
      clearTimeout(timeoutId)
    }
  }, [viewMode, isDesktop])

  // Auto-select first post on desktop only (feed and social views)
  useEffect(() => {
    if (posts && posts.length > 0 && !selectedPost && window.innerWidth >= 1024 && (viewMode === 'feed' || viewMode === 'social')) {
      setSelectedPost(posts[0])
    }
  }, [posts, selectedPost, viewMode])

  const handleFilterChange = useCallback((key: keyof FeedFilters, value: any) => {
    setFilters((prev) => {
      const updated = { ...prev, [key]: value }

      // Clear person filter when relationship changes (optional: person may not be in that relationship)
      if (key === 'relationship_id' && value) {
        const person = persons.find((p: any) => p.id === updated.person_id)
        if (person && person.relationship_id !== value) {
          updated.person_id = undefined
        }
      }

      return updated
    })
    setSelectedPost(null)
  }, [persons, setFilters, setSelectedPost])

  const clearFilters = () => {
    setFilters({
      sort_by: 'created_at',
      sort_order: 'desc',
      date_from: undefined,
      date_to: undefined,
      person_id: undefined,
      person_group_id: undefined,
      relationship_id: undefined,
      tag_id: undefined,
      file_type: undefined,
      search: undefined,
      has_tags: undefined,
      has_description: undefined,
      unread_only: undefined,
      min_resolution: undefined,
    })
  }

  const hasActiveFilters =
    filters.unread_only ||
    filters.person_id ||
    filters.person_group_id ||
    filters.relationship_id ||
    filters.tag_id ||
    filters.file_type ||
    filters.search ||
    filters.date_from ||
    filters.date_to ||
    filters.has_tags !== undefined ||
    filters.has_description !== undefined ||
    filters.min_resolution

  // When a group is selected, scope persons/relationships/tags to group members
  const groupPersonIds = useMemo(() => selectedGroupDetail?.members ? new Set(selectedGroupDetail.members.map((m: any) => m.id)) : null, [selectedGroupDetail])
  const groupTagIds = useMemo(() => selectedGroupDetail?.tag_members ? new Set(selectedGroupDetail.tag_members.map((t: any) => t.id)) : null, [selectedGroupDetail])
  const groupExcludedTagIds = useMemo(() => selectedGroupDetail?.excluded_tags ? new Set(selectedGroupDetail.excluded_tags.map((t: any) => t.id)) : null, [selectedGroupDetail])

  // Collect all relationship IDs from group persons + explicit relationship members
  const groupAllRelIds = useMemo(() => {
    if (!selectedGroupDetail) return null
    const ids = new Set<number>()
    if (selectedGroupDetail.relationship_members) {
      selectedGroupDetail.relationship_members.forEach((r: any) => ids.add(r.id))
    }
    if (selectedGroupDetail.members) {
      selectedGroupDetail.members.forEach((m: any) => ids.add(m.relationship_id))
    }
    return ids
  }, [selectedGroupDetail])

  const scopedPersons = groupPersonIds
    ? persons.filter((p: any) => groupPersonIds.has(p.id) || (groupAllRelIds && groupAllRelIds.has(p.relationship_id)))
    : persons
  const scopedRelationships = groupAllRelIds
    ? assignedRelationships.filter((r: any) => groupAllRelIds.has(r.id))
    : assignedRelationships
  const scopedTags = useMemo(() => {
    let filtered = assignedTags
    if (groupTagIds && groupTagIds.size > 0) {
      filtered = filtered.filter((t: any) => groupTagIds.has(t.id))
    }
    if (groupExcludedTagIds && groupExcludedTagIds.size > 0) {
      filtered = filtered.filter((t: any) => !groupExcludedTagIds.has(t.id))
    }
    return filtered
  }, [assignedTags, groupTagIds, groupExcludedTagIds])

  // Build filter sections for FilterBar component
  const filterSections: FilterSection[] = useMemo(() => [
    {
      id: 'person_group',
      label: 'Person Group',
      type: 'select' as const,
      options: [
        { value: '', label: 'All Groups' },
        ...personGroups.map((group: any) => ({
          value: group.id.toString(),
          label: group.name,
        })),
      ],
      value: filters.person_group_id?.toString() || '',
      onChange: (value) => {
        const groupId = value ? Number(value as string) : undefined
        const group = groupId ? personGroups.find((g: any) => g.id === groupId) : null
        setFilters((prev) => ({
          ...prev,
          person_group_id: groupId,
          // Clear sub-filters that may no longer be valid when changing group
          ...(value ? { person_id: undefined, relationship_id: undefined, tag_id: undefined } : {}),
          // Apply group's min_resolution setting
          min_resolution: group?.min_resolution || undefined,
        }))
        setSelectedPost(null)
      },
    },
    {
      id: 'person',
      label: 'Person',
      type: 'select' as const,
      options: [
        { value: '', label: 'All Persons' },
        ...scopedPersons.map((person: any) => ({
          value: person.id.toString(),
          label: person.name,
        })),
      ],
      value: filters.person_id?.toString() || '',
      onChange: (value) => handleFilterChange('person_id', value ? Number(value as string) : undefined),
    },
    {
      id: 'relationship',
      label: 'Relationship',
      type: 'select' as const,
      options: [
        { value: '', label: 'All Relationships' },
        ...scopedRelationships.map((rel: any) => ({
          value: rel.id.toString(),
          label: rel.name,
        })),
      ],
      value: filters.relationship_id?.toString() || '',
      onChange: (value) => handleFilterChange('relationship_id', value ? Number(value as string) : undefined),
    },
    {
      id: 'tag',
      label: 'Tag',
      type: 'select' as const,
      options: [
        { value: '', label: 'All Tags' },
        ...scopedTags.map((tag: { id: number; name: string; color: string }) => ({
          value: tag.id.toString(),
          label: tag.name,
        })),
      ],
      value: filters.tag_id?.toString() || '',
      onChange: (value) => handleFilterChange('tag_id', value ? parseInt(value as string) : undefined),
    },
    {
      id: 'file_type',
      label: 'Type',
      type: 'select' as const,
      options: [
        { value: '', label: 'All Types' },
        { value: 'image', label: 'Images' },
        { value: 'video', label: 'Videos' },
      ],
      value: filters.file_type || '',
      onChange: (value) => handleFilterChange('file_type', (value as string) || undefined),
    },
    {
      id: 'min_resolution',
      label: 'Min Resolution',
      type: 'select' as const,
      options: [
        { value: '', label: 'Any Resolution' },
        { value: '300', label: '300px+' },
        { value: '500', label: '500px+' },
        { value: '720', label: '720px+' },
        { value: '1080', label: '1080px+' },
      ],
      value: filters.min_resolution?.toString() || '',
      onChange: (value) => handleFilterChange('min_resolution', value ? parseInt(value as string) : undefined),
    },
    {
      id: 'sort',
      label: 'Sort By',
      type: 'select' as const,
      options: [
        { value: 'media_date-desc', label: 'Newest First' },
        { value: 'media_date-asc', label: 'Oldest First' },
        { value: 'created_at-desc', label: 'Recently Added' },
        { value: 'filename-asc', label: 'Filename A-Z' },
      ],
      value: `${filters.sort_by}-${filters.sort_order}`,
      onChange: (value) => {
        const [sortBy, sortOrder] = (value as string).split('-')
        setFilters((prev) => ({ ...prev, sort_by: sortBy, sort_order: sortOrder as 'asc' | 'desc' }))
        setSelectedPost(null)
      },
    },
    {
      id: 'has_tags',
      label: 'Has Tags',
      type: 'radio' as const,
      options: [
        { value: '', label: 'Any' },
        { value: 'true', label: 'Yes' },
        { value: 'false', label: 'No' },
      ],
      value: filters.has_tags === true ? 'true' : filters.has_tags === false ? 'false' : '',
      onChange: (value) => handleFilterChange('has_tags', value === 'true' ? true : value === 'false' ? false : undefined),
    },
    {
      id: 'has_description',
      label: 'Has Description',
      type: 'radio' as const,
      options: [
        { value: '', label: 'Any' },
        { value: 'true', label: 'Yes' },
        { value: 'false', label: 'No' },
      ],
      value: filters.has_description === true ? 'true' : filters.has_description === false ? 'false' : '',
      onChange: (value) => handleFilterChange('has_description', value === 'true' ? true : value === 'false' ? false : undefined),
    },
  ], [filters, persons, personGroups, scopedPersons, scopedRelationships, scopedTags, relationships, availableTags, handleFilterChange, setFilters, setSelectedPost])

  // Build active filters array for FilterBar
  const activeFilters: ActiveFilter[] = useMemo(() => {
    const result: ActiveFilter[] = []

    if (filters.person_group_id) {
      const group = personGroups.find((g: any) => g.id === filters.person_group_id)
      result.push({
        id: 'person_group',
        label: 'Person Group',
        value: filters.person_group_id.toString(),
        displayValue: group?.name || filters.person_group_id.toString(),
        onRemove: () => handleFilterChange('person_group_id', undefined),
      })
    }
    if (filters.person_id) {
      const person = persons.find((p: any) => p.id === filters.person_id)
      result.push({
        id: 'person',
        label: 'Person',
        value: filters.person_id.toString(),
        displayValue: person?.name || filters.person_id.toString(),
        onRemove: () => handleFilterChange('person_id', undefined),
      })
    }
    if (filters.relationship_id) {
      const relationship = relationships.find((r: any) => r.id === filters.relationship_id)
      result.push({
        id: 'relationship',
        label: 'Relationship',
        value: filters.relationship_id.toString(),
        displayValue: relationship?.name || filters.relationship_id.toString(),
        onRemove: () => handleFilterChange('relationship_id', undefined),
      })
    }
    if (filters.tag_id) {
      const tag = availableTags.find((t: { id: number; name: string }) => t.id === filters.tag_id)
      result.push({
        id: 'tag',
        label: 'Tag',
        value: filters.tag_id.toString(),
        displayValue: tag?.name || filters.tag_id.toString(),
        onRemove: () => handleFilterChange('tag_id', undefined),
      })
    }
    if (filters.file_type && filters.file_type !== 'all') {
      result.push({
        id: 'file_type',
        label: 'Type',
        value: filters.file_type,
        displayValue: filters.file_type.charAt(0).toUpperCase() + filters.file_type.slice(1) + 's',
        onRemove: () => handleFilterChange('file_type', undefined),
      })
    }
    if (filters.min_resolution) {
      result.push({
        id: 'min_resolution',
        label: 'Min Resolution',
        value: filters.min_resolution.toString(),
        displayValue: `${filters.min_resolution}px+`,
        onRemove: () => handleFilterChange('min_resolution', undefined),
      })
    }
    if (filters.date_from || filters.date_to) {
      let displayValue = ''
      if (filters.date_from && filters.date_to) {
        displayValue = `${filters.date_from} to ${filters.date_to}`
      } else if (filters.date_from) {
        displayValue = `From ${filters.date_from}`
      } else if (filters.date_to) {
        displayValue = `Until ${filters.date_to}`
      }
      result.push({
        id: 'date',
        label: 'Date',
        value: filters.date_from || filters.date_to || '',
        displayValue,
        onRemove: () => {
          handleFilterChange('date_from', undefined)
          handleFilterChange('date_to', undefined)
        },
      })
    }
    if (filters.has_tags !== undefined) {
      result.push({
        id: 'has_tags',
        label: 'Tags',
        value: filters.has_tags.toString(),
        displayValue: filters.has_tags ? 'Has Tags' : 'No Tags',
        onRemove: () => handleFilterChange('has_tags', undefined),
      })
    }
    if (filters.has_description !== undefined) {
      result.push({
        id: 'has_description',
        label: 'Description',
        value: filters.has_description.toString(),
        displayValue: filters.has_description ? 'Has Description' : 'No Description',
        onRemove: () => handleFilterChange('has_description', undefined),
      })
    }
    if (filters.unread_only) {
      result.push({
        id: 'unread_only',
        label: 'Unread',
        value: 'true',
        displayValue: 'Unread Only',
        onRemove: () => handleFilterChange('unread_only', undefined),
      })
    }

    return result
  }, [filters, persons, personGroups, relationships, availableTags, handleFilterChange])

  // Convert PrivateMediaItem to PrivateGalleryMediaItem for lightbox (social view - API order)
  const lightboxItems: PrivateGalleryMediaItem[] = mediaItems.map(item => ({
    ...item,
    file_type: item.file_type as 'image' | 'video' | 'other',
  }))

  // Gallery view lightbox items - built directly from galleryMediaItems (flat /media endpoint)
  // IMPORTANT: Strip tokens from URLs because the lightbox component adds its own token
  // via getPreviewUrl/getThumbnailUrl. galleryMediaItems already has tokens for the grid thumbnails.
  const galleryLightboxItems: PrivateGalleryMediaItem[] = useMemo(() => {
    return galleryMediaItems
      .filter(item => item.file_type === 'image' || item.file_type === 'video')
      .map(item => ({
        ...item,
        thumbnail_url: item.thumbnail_url.split('?')[0],
        stream_url: item.stream_url.split('?')[0],
        file_url: item.file_url.split('?')[0],
        file_type: item.file_type as 'image' | 'video' | 'other',
      }))
  }, [galleryMediaItems])

  // Delete handler for gallery-view lightbox (works for both normal and shuffle modes)
  const handleGalleryLightboxDelete = async (item: PrivateGalleryMediaItem) => {
    if (!confirm('Delete this item?')) return
    try {
      await api.privateGallery.deleteMedia(item.id)
      queryClient.invalidateQueries({ queryKey: ['private-gallery-posts'] })
      queryClient.invalidateQueries({ queryKey: ['private-gallery-media'] })
      queryClient.invalidateQueries({ queryKey: ['private-gallery-stats'] })

      if (galleryShuffleEnabled) {
        const newItems = galleryShuffleItems.filter(i => i.id !== item.id)
        setGalleryShuffleItems(newItems)
        setGalleryShuffleTotal(prev => Math.max(0, prev - 1))
        if (newItems.length === 0) {
          setGalleryLightboxIndex(null)
        } else if (galleryLightboxIndex !== null && galleryLightboxIndex >= newItems.length) {
          setGalleryLightboxIndex(newItems.length - 1)
        }
      } else {
        // Normal mode: items come from galleryLightboxItems (derived from query data)
        // Query invalidation will refresh the list; adjust index if needed
        const remaining = galleryLightboxItems.filter(i => i.id !== item.id)
        if (remaining.length === 0) {
          setGalleryLightboxIndex(null)
        } else if (galleryLightboxIndex !== null && galleryLightboxIndex >= remaining.length) {
          setGalleryLightboxIndex(remaining.length - 1)
        }
      }
    } catch (error) {
      console.error('Failed to delete media:', error)
      alert('Failed to delete item')
    }
  }

  // Auth is handled by RequirePrivateGalleryAuth wrapper in App.tsx

  return (
    <div className="space-y-4">
      {/* PrivateGalleryBundleLightbox for gallery view — uses simple index like social view */}
      {viewMode === 'gallery' && galleryLightboxIndex !== null && galleryLightboxIndex >= 0 && (galleryShuffleEnabled || galleryLightboxItems.length > 0) && (
        <PrivateGalleryBundleLightbox
          items={galleryShuffleEnabled && galleryShuffleItems.length > 0 ? galleryShuffleItems : galleryLightboxItems}
          currentIndex={galleryLightboxIndex}
          onClose={() => {
            setGalleryLightboxIndex(null)
            setSlideshowActive(false)
            setGalleryShuffleEnabled(false)
            setGalleryShuffleItems([])
          }}
          onNavigate={(idx) => {
            setGalleryLightboxIndex(idx)
          }}
          onDelete={handleGalleryLightboxDelete}
          initialSlideshow={slideshowActive}
          initialInterval={slideshowActive ? 3000 : undefined}
          initialShuffle={slideshowActive ? true : undefined}
          totalCount={galleryShuffleEnabled ? galleryShuffleTotal : totalGalleryMedia}
          hasMore={galleryShuffleEnabled ? galleryShuffleHasMore : !!hasGalleryMediaNextPage}
          onLoadMore={galleryShuffleEnabled
            ? () => { if (!galleryShuffleLoading && galleryShuffleHasMore) fetchShufflePage(galleryShuffleSeed, galleryShuffleOffsetRef.current, false) }
            : () => { if (hasGalleryMediaNextPage && !isGalleryMediaFetchingNext) fetchGalleryMediaNextPage() }
          }
          onShuffleChange={(enabled) => {
            if (enabled) {
              const seed = Math.floor(Math.random() * 1000000)
              setGalleryShuffleSeed(seed)
              setGalleryShuffleEnabled(true)
              setGalleryShuffleTotal(totalGalleryMedia || 0)
              setGalleryLightboxIndex(0)
              galleryShuffleOffsetRef.current = 0
              fetchShufflePage(seed, 0, true)
            } else {
              setGalleryShuffleEnabled(false)
              setGalleryShuffleItems([])
              setGalleryLightboxIndex(0)
            }
          }}
          isShuffled={galleryShuffleEnabled}
        />
      )}

      {/* Header */}
      <div className="flex items-center justify-between">
        <div>
          <h1 className="text-2xl sm:text-3xl font-bold flex items-center gap-2 sm:gap-3">
            <Lock className="w-8 h-8 text-violet-500" />
            Private Gallery
          </h1>
          <p className="text-slate-600 dark:text-slate-400 mt-1 hidden sm:block">
            Your private media collection
          </p>
        </div>
        <div className="flex items-center gap-2">
          {/* Upload button */}
          <button
            onClick={() => setShowUploadModal(true)}
            className="flex items-center gap-2 px-3 py-2 rounded-lg bg-blue-600 text-white hover:bg-blue-700 transition-colors"
          >
            <Upload className="w-4 h-4" />
            <span className="text-sm hidden sm:inline">Upload</span>
          </button>
          {/* Monitored threads indicator */}
          {monitoredThreadCount > 0 && (
            <button
              onClick={() => navigate('/private-gallery/config')}
              className="flex items-center gap-1.5 px-2.5 py-2 rounded-lg bg-emerald-600/10 text-emerald-600 hover:bg-emerald-600/20 transition-colors"
              title={`${monitoredThreadCount} monitored thread${monitoredThreadCount !== 1 ? 's' : ''}`}
            >
              <Globe className="w-4 h-4" />
              <span className="text-xs font-medium">{monitoredThreadCount}</span>
            </button>
          )}
          {/* Settings button */}
          <button
            onClick={() => navigate('/private-gallery/config')}
            className="flex items-center gap-2 px-3 py-2 rounded-lg bg-secondary text-foreground hover:bg-secondary/80 transition-colors"
          >
            <Settings className="w-4 h-4" />
            <span className="text-sm hidden sm:inline">Settings</span>
          </button>
          {/* Lock button */}
          <button
            onClick={() => { lock(); navigate('/private-gallery/unlock'); }}
            className="flex items-center gap-2 px-3 py-2 rounded-lg bg-secondary text-foreground hover:bg-secondary/80 transition-colors"
            title="Lock gallery"
          >
            <Lock className="w-4 h-4" />
            <span className="text-sm hidden sm:inline">Lock</span>
          </button>
          {/* Scan All Duplicates button */}
          <button
            onClick={async () => {
              if (isGalleryScanActive) return
              setIsGalleryScanActive(true)
              setGalleryScanResult(null)
              setGalleryScanProgress(null)
              setGallerySelectedDupes(new Set())
              try {
                const resp = await api.privateGallery.scanAllDuplicates() as any
                if (!resp.job_id) {
                  setIsGalleryScanActive(false)
                  return
                }
                setGalleryScanJobId(resp.job_id)
                let pollFailures = 0
                const poll = async () => {
                  try {
                    const status = await api.privateGallery.getJobStatus(resp.job_id)
                    pollFailures = 0
                    const s = status as any
                    setGalleryScanProgress({
                      processed: s.processed_files || 0,
                      total: s.total_files || 0,
                      phase: s.current_phase || 'processing',
                      ...(s.compared_pairs !== undefined && s.total_pairs ? {
                        comparedPairs: s.compared_pairs,
                        totalPairs: s.total_pairs,
                      } : {}),
                      currentPerson: s.current_person,
                    })
                    if (status.status === 'completed') {
                      setGalleryScanResult({
                        results: s.scan_results || [],
                        total_scanned: s.total_scanned || 0,
                        total_groups: s.total_groups || 0,
                        message: s.message,
                      })
                      setIsGalleryScanActive(false)
                      setGalleryScanJobId(null)
                      setGalleryScanProgress(null)
                    } else {
                      setTimeout(poll, 1000)
                    }
                  } catch {
                    pollFailures++
                    if (pollFailures >= 5) {
                      setIsGalleryScanActive(false)
                      setGalleryScanJobId(null)
                      setGalleryScanProgress(null)
                    } else {
                      setTimeout(poll, 2000)
                    }
                  }
                }
                poll()
              } catch (error) {
                console.error('Failed to scan all duplicates:', error)
                setIsGalleryScanActive(false)
              }
            }}
            disabled={isGalleryScanActive}
            className="hidden lg:flex items-center gap-2 px-3 py-2 rounded-lg bg-secondary text-foreground hover:bg-secondary/80 transition-colors"
            title="Scan all persons for duplicates"
          >
            {isGalleryScanActive ? (
              <Loader2 className="w-4 h-4 animate-spin" />
            ) : (
              <ScanSearch className="w-4 h-4" />
            )}
            <span className="text-sm hidden xl:inline">Duplicates</span>
          </button>
          {/* Slideshow button - gallery view: all items, social view: selected post only */}
          {(() => {
            if (viewMode === 'gallery') return galleryLightboxItems.length > 1
            const post = selectedPost || (posts.length > 0 ? posts[0] : null)
            const mediaCount = post?.attachments?.filter(a => a.status === 'completed' && (a.file_type === 'image' || a.file_type === 'video')).length || 0
            return mediaCount > 1
          })() && (
            <button
              onClick={() => {
                if (viewMode === 'gallery') {
                  // Gallery view: slideshow all items with shuffle and 3s interval
                  if (galleryLightboxItems.length > 0) {
                    // Trigger backend shuffle
                    const seed = Math.floor(Math.random() * 1000000)
                    setGalleryShuffleSeed(seed)
                    setGalleryShuffleEnabled(true)
                    setGalleryShuffleTotal(totalGalleryMedia || 0)
                    galleryShuffleOffsetRef.current = 0
                    fetchShufflePage(seed, 0, true)
                    setGalleryLightboxIndex(0)
                    setSlideshowActive(true)
                  }
                } else {
                  // Social/feed view: slideshow the currently selected post only
                  // Use the post's already-loaded attachments (same order as the post grid)
                  const post = selectedPost || (posts.length > 0 ? posts[0] : null)
                  if (!post) return
                  const postMediaAtts = post.attachments?.filter(
                    a => a.status === 'completed' && (a.file_type === 'image' || a.file_type === 'video')
                  ) || []
                  if (postMediaAtts.length === 0) return
                  const relationship = (post as any)._relationship || undefined
                  const postItems: PrivateGalleryMediaItem[] = postMediaAtts.map(att => ({
                    id: att.id,
                    storage_id: att.id.toString(),
                    filename: att.name || att.local_filename || '',
                    description: post.content || null,
                    file_type: att.file_type as 'image' | 'video' | 'other',
                    file_size: att.file_size || 0,
                    width: att.width,
                    height: att.height,
                    duration: att.duration,
                    person: post.display_name ? { id: post.identity_id || 0, name: post.display_name, relationship } : null,
                    media_date: (att as any).media_date || post.published_at || '',
                    tags: post.tags?.filter(t => t.id > 0).map(t => ({ ...t, slug: '' })) || [],
                    thumbnail_url: `/api/private-gallery/thumbnail/${att.id}`,
                    file_url: `/api/private-gallery/file/${att.id}`,
                    stream_url: `/api/private-gallery/stream/${att.id}`,
                    created_at: att.created_at || undefined,
                  }))
                  setSlideshowPostItems(postItems)
                  setSlideshowPostId(post.id)
                  setSlideshowPostTotal(post.attachment_count || postItems.length)
                  setSocialLightboxIndex(0)
                  setSlideshowActive(true)
                }
              }}
              className="hidden lg:flex items-center gap-2 px-3 py-2 rounded-lg bg-secondary text-foreground hover:bg-secondary/80 transition-colors"
              title="Start Slideshow"
            >
              <Play className="w-4 h-4" />
              <span className="text-sm">Slideshow</span>
            </button>
          )}
          {/* Tag Mode button - only show in gallery view */}
          {viewMode === 'gallery' && !tagMode && (
            <button
              onClick={() => setTagMode(true)}
              className="hidden lg:flex items-center gap-2 px-3 py-2 rounded-lg bg-secondary text-foreground hover:bg-secondary/80 transition-colors"
            >
              <Tag className="w-4 h-4" />
              <span className="text-sm">Tag Mode</span>
            </button>
          )}
          {/* View toggle - Social + Gallery */}
          <div className="flex items-center bg-secondary rounded-lg p-1">
            <button
              onClick={() => { setViewMode('social'); exitTagMode(); }}
              className={`p-2 rounded-md transition-colors ${
                viewMode === 'social'
                  ? 'bg-primary text-primary-foreground'
                  : 'text-muted-foreground hover:text-foreground'
              }`}
              title="Social view"
            >
              <Columns className="w-4 h-4" />
            </button>
            <button
              onClick={() => setViewMode('gallery')}
              className={`p-2 rounded-md transition-colors ${
                viewMode === 'gallery'
                  ? 'bg-primary text-primary-foreground'
                  : 'text-muted-foreground hover:text-foreground'
              }`}
              title="Gallery view"
            >
              <LayoutGrid className="w-4 h-4" />
            </button>
          </div>
          {/* Mobile filter toggle */}
          <button
            onClick={() => setShowFilters(!showFilters)}
            className="lg:hidden flex items-center gap-2 px-3 py-2 rounded-lg bg-secondary text-foreground"
          >
            <span className="text-sm">Filters</span>
            {activeFilters.length > 0 && (
              <span className="w-2 h-2 rounded-full bg-blue-500"></span>
            )}
          </button>
        </div>
      </div>

      {/* Filters - Using FilterBar component */}
      <div className={`card-glass-hover rounded-xl p-4 relative z-30 ${showFilters ? 'block' : 'hidden lg:block'}`}>
        <FilterBar
          searchValue={filters.search || ''}
          onSearchChange={(value) => handleFilterChange('search', value || undefined)}
          searchPlaceholder="Search media..."
          filterSections={filterSections}
          activeFilters={activeFilters}
          onClearAll={clearFilters}
          totalCount={viewMode === 'gallery' ? (totalGalleryMedia || totalFilteredPosts) : totalFilteredPosts}
          countLabel={viewMode === 'gallery' ? ((totalGalleryMedia || totalFilteredPosts) === 1 ? 'item' : 'items') : (totalFilteredPosts === 1 ? 'post' : 'posts')}
          advancedFilters={{
            dateFrom: {
              value: filters.date_from ? (() => {
                const fromDate = new Date(filters.date_from)
                fromDate.setDate(fromDate.getDate() + 7)
                return fromDate.toISOString().split('T')[0]
              })() : '',
              onChange: (value) => {
                if (value) {
                  const centerDate = new Date(value)
                  const fromDate = new Date(centerDate)
                  const toDate = new Date(centerDate)
                  fromDate.setDate(centerDate.getDate() - 7)
                  toDate.setDate(centerDate.getDate() + 7)
                  handleFilterChange('date_from', fromDate.toISOString().split('T')[0])
                  handleFilterChange('date_to', toDate.toISOString().split('T')[0])
                } else {
                  handleFilterChange('date_from', undefined)
                  handleFilterChange('date_to', undefined)
                }
              },
              label: 'Date (+/-1 week)',
            },
          }}
        />
      </div>

      {/* Unread posts banner */}
      {newPostsCount > 0 && !filters.unread_only && (
        <div className="flex items-center justify-between gap-3 bg-gradient-to-r from-blue-50 to-indigo-50 dark:from-blue-900/20 dark:to-indigo-900/20 rounded-lg shadow-sm border border-blue-200 dark:border-blue-800 px-4 py-3">
          <div className="flex items-center gap-3">
            <div className="flex items-center justify-center w-8 h-8 rounded-full bg-blue-600 text-white text-sm font-bold shrink-0">
              {newPostsCount}
            </div>
            <span className="text-sm font-medium text-blue-700 dark:text-blue-300">
              {newPostsCount === 1 ? '1 unread post' : `${newPostsCount} unread posts`}
            </span>
          </div>
          <div className="flex items-center gap-2 shrink-0">
            <button
              onClick={() => {
                setFilters(prev => ({ ...prev, sort_by: 'created_at', sort_order: 'desc', unread_only: true }))
              }}
              className="px-3 py-1.5 rounded-lg bg-blue-600 text-white text-sm font-medium hover:bg-blue-700 transition-colors"
            >
              View unread
            </button>
            <button
              onClick={() => markSeenMutation.mutate()}
              disabled={markSeenMutation.isPending}
              className="px-3 py-1.5 rounded-md text-blue-700 dark:text-blue-300 text-sm hover:bg-blue-100 dark:hover:bg-blue-900/30 transition-colors"
            >
              {markSeenMutation.isPending ? 'Marking...' : 'Mark all read'}
            </button>
          </div>
        </div>
      )}

      {/* Active unread filter banner */}
      {filters.unread_only && (
        <div className="flex items-center justify-between gap-3 bg-gradient-to-r from-blue-50 to-indigo-50 dark:from-blue-900/20 dark:to-indigo-900/20 rounded-lg shadow-sm border border-blue-200 dark:border-blue-800 px-4 py-3">
          <span className="text-sm font-medium text-blue-700 dark:text-blue-300">
            Showing unread posts
          </span>
          <div className="flex items-center gap-2 shrink-0">
            <button
              onClick={() => markSeenMutation.mutate()}
              disabled={markSeenMutation.isPending}
              className="px-3 py-1.5 rounded-md text-blue-700 dark:text-blue-300 text-sm font-medium hover:bg-blue-100 dark:hover:bg-blue-900/30 transition-colors"
            >
              {markSeenMutation.isPending ? 'Marking...' : 'Mark all read'}
            </button>
            <button
              onClick={() => handleFilterChange('unread_only', undefined)}
              className="px-3 py-1.5 rounded-md text-blue-700 dark:text-blue-300 text-sm hover:bg-blue-100 dark:hover:bg-blue-900/30 transition-colors"
            >
              Show all
            </button>
          </div>
        </div>
      )}

      {/* Gallery-level duplicate scan progress */}
      {isGalleryScanActive && galleryScanProgress && (
        <div className="card-glass-hover rounded-xl p-4">
          <div className="flex items-center gap-2 mb-2">
            <Loader2 className="w-4 h-4 animate-spin text-primary" />
            <span className="text-sm font-medium text-foreground">
              Scanning all persons for duplicates...
            </span>
          </div>
          <div className="text-xs text-muted-foreground">
            {galleryScanProgress.phase === 'hashing' ? 'Computing hashes' : galleryScanProgress.phase === 'comparing' ? 'Comparing hashes' : 'Starting'}
            {galleryScanProgress.phase === 'hashing' && galleryScanProgress.total > 0 && ` — ${galleryScanProgress.processed}/${galleryScanProgress.total} files`}
            {galleryScanProgress.phase === 'comparing' && galleryScanProgress.totalPairs ? ` — ${Math.round(((galleryScanProgress.comparedPairs || 0) / galleryScanProgress.totalPairs) * 100)}%` : ''}
            {galleryScanProgress.currentPerson && ` — ${galleryScanProgress.currentPerson}`}
          </div>
          {galleryScanProgress.total > 0 && (
            <div className="mt-2 w-full bg-secondary rounded-full h-1.5">
              <div
                className="bg-primary h-1.5 rounded-full transition-all duration-300"
                style={{ width: `${galleryScanProgress.phase === 'comparing' && galleryScanProgress.totalPairs
                  ? Math.round(((galleryScanProgress.comparedPairs || 0) / galleryScanProgress.totalPairs) * 100)
                  : Math.round((galleryScanProgress.processed / galleryScanProgress.total) * 100)}%` }}
              />
            </div>
          )}
        </div>
      )}

      {/* Gallery-level duplicate scan results */}
      {galleryScanResult && (
        <div className="card-glass-hover rounded-xl p-4 max-h-[70vh] overflow-y-auto">
          <div className="flex items-center justify-between mb-2 sticky top-0 bg-card/80 backdrop-blur-sm py-1 z-10 -mx-4 px-4">
            <h4 className="text-sm font-medium text-foreground">
              Duplicate Scan Results
              <span className="ml-2 text-muted-foreground font-normal">
                ({galleryScanResult.total_scanned} scanned)
              </span>
            </h4>
            <div className="flex items-center gap-1">
              {galleryScanResult.results.length > 0 && (
                <button
                  onClick={() => {
                    const toSelect = new Set<number>()
                    for (const group of galleryScanResult.results) {
                      if (group.items.length < 2) continue
                      const adj = new Map<number, Set<number>>()
                      for (const item of group.items) {
                        for (const [otherId, dist] of Object.entries(item.distances)) {
                          if (dist <= duplicateAutoSelectDistance) {
                            if (!adj.has(item.media_id)) adj.set(item.media_id, new Set())
                            adj.get(item.media_id)!.add(Number(otherId))
                          }
                        }
                      }
                      if (adj.size === 0) continue
                      const visited = new Set<number>()
                      for (const startId of adj.keys()) {
                        if (visited.has(startId)) continue
                        const cluster: number[] = []
                        const queue = [startId]
                        while (queue.length > 0) {
                          const id = queue.pop()!
                          if (visited.has(id)) continue
                          visited.add(id)
                          cluster.push(id)
                          for (const nb of adj.get(id) || []) {
                            if (!visited.has(nb)) queue.push(nb)
                          }
                        }
                        if (cluster.length < 2) continue
                        const items = cluster.map(id => group.items.find(i => i.media_id === id)!).filter(Boolean)
                        let best = items[0]
                        for (const item of items) {
                          if ((item.width || 0) * (item.height || 0) > (best.width || 0) * (best.height || 0)) best = item
                        }
                        for (const item of items) {
                          if (item.media_id !== best.media_id) toSelect.add(item.media_id)
                        }
                      }
                    }
                    if (toSelect.size === 0) return
                    setGallerySelectedDupes(toSelect)
                  }}
                  className="flex items-center gap-1 px-2 py-1 rounded text-xs bg-amber-500/20 text-amber-400 hover:bg-amber-500/30 transition-colors"
                  title={`Auto-select lowest resolution items from near-duplicate pairs (distance<=${duplicateAutoSelectDistance})`}
                >
                  <Check className="w-3 h-3" />
                  Auto-select
                </button>
              )}
              {gallerySelectedDupes.size > 0 && (
                <button
                  onClick={async () => {
                    if (galleryIsDeletingDupes) return
                    const count = gallerySelectedDupes.size
                    if (!confirm(`Delete ${count} selected media item${count !== 1 ? 's' : ''}? This cannot be undone.`)) return
                    setGalleryIsDeletingDupes(true)
                    try {
                      const ids = Array.from(gallerySelectedDupes)
                      const affectedPostIds = new Set<number>()
                      for (const group of galleryScanResult.results) {
                        for (const item of group.items) {
                          if (gallerySelectedDupes.has(item.media_id)) {
                            affectedPostIds.add(item.post_id)
                          }
                        }
                      }
                      for (let i = 0; i < ids.length; i += 500) {
                        await api.privateGallery.batchDeleteMedia(ids.slice(i, i + 500))
                      }
                      const newResults = galleryScanResult.results
                        .map(group => ({
                          ...group,
                          items: group.items.filter(item => !gallerySelectedDupes.has(item.media_id))
                        }))
                        .filter(group => group.items.length > 1)
                      setGalleryScanResult({
                        ...galleryScanResult,
                        results: newResults,
                        total_groups: newResults.length,
                      })
                      setGallerySelectedDupes(new Set())
                      // Cleanup empty posts
                      const postIdsToCheck = Array.from(affectedPostIds)
                      if (postIdsToCheck.length > 0) {
                        try {
                          await api.privateGallery.cleanupEmptyPosts(postIdsToCheck)
                        } catch {
                          // Cleanup is best-effort
                        }
                      }
                      queryClient.invalidateQueries({ queryKey: ['private-gallery-posts'] })
                      queryClient.invalidateQueries({ queryKey: ['private-gallery-media'] })
                      queryClient.invalidateQueries({ queryKey: ['private-gallery-stats'] })
                    } catch (error) {
                      console.error('Failed to delete:', error)
                      alert('Failed to delete selected media')
                    } finally {
                      setGalleryIsDeletingDupes(false)
                    }
                  }}
                  disabled={galleryIsDeletingDupes}
                  className="flex items-center gap-1 px-2 py-1 rounded text-xs bg-destructive/20 text-destructive hover:bg-destructive/30 transition-colors"
                >
                  {galleryIsDeletingDupes ? <Loader2 className="w-3 h-3 animate-spin" /> : <Trash2 className="w-3 h-3" />}
                  Delete {gallerySelectedDupes.size}
                </button>
              )}
              <button
                onClick={() => { setGalleryScanResult(null); setGallerySelectedDupes(new Set()) }}
                className="p-1 rounded hover:bg-secondary text-muted-foreground"
                title="Dismiss"
              >
                <X className="w-4 h-4" />
              </button>
            </div>
          </div>
          {galleryScanResult.results.length === 0 ? (
            <p className="text-sm text-muted-foreground">{galleryScanResult.message || 'No perceptual duplicates found.'}</p>
          ) : (
            <div className="space-y-4">
              <p className="text-sm text-amber-500">{galleryScanResult.total_groups} duplicate group{galleryScanResult.total_groups !== 1 ? 's' : ''} found — select items to delete</p>
              {(() => {
                const allGalleryDupeItems: Array<{ media_id: number; gi: number; ii: number }> = []
                galleryScanResult.results.forEach((group, gi) => {
                  group.items.forEach((item, ii) => {
                    allGalleryDupeItems.push({ media_id: item.media_id, gi, ii })
                  })
                })
                return galleryScanResult.results.map((group, gi) => (
                <div key={gi} className="p-2 bg-background border border-border rounded-lg">
                  <p className="text-xs font-medium mb-2">
                    <span className="text-violet-400">{group.person_name}</span>
                    <span className="text-muted-foreground ml-2">Group {gi + 1} ({group.items.length} items)</span>
                  </p>
                  <div className="grid grid-cols-3 sm:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 gap-2">
                    {group.items.map((item, ii) => {
                      const token = api.getPrivateGalleryToken()
                      const tokenParam = token ? `?_token=${encodeURIComponent(token)}` : ''
                      const thumbUrl = `/api/private-gallery/thumbnail/${item.media_id}${tokenParam}`
                      const isSelected = gallerySelectedDupes.has(item.media_id)
                      const minDist = Object.values(item.distances).length > 0
                        ? Math.min(...Object.values(item.distances))
                        : null
                      const flatKey = `${gi}-${ii}`
                      return (
                        <button
                          key={item.media_id}
                          type="button"
                          className={`relative aspect-square bg-secondary rounded-lg overflow-hidden cursor-pointer transition-all ${
                            isSelected ? 'ring-2 ring-destructive ring-offset-1 ring-offset-background' : 'hover:ring-2 hover:ring-primary'
                          }`}
                          onClick={(e) => {
                            e.preventDefault()
                            setGallerySelectedDupes(prev => {
                              const next = new Set(prev)
                              if (e.shiftKey && lastGalleryDupeIdx.current !== null && lastGalleryDupeIdx.current !== flatKey) {
                                const curFlat = allGalleryDupeItems.findIndex(d => d.gi === gi && d.ii === ii)
                                const lastParts = lastGalleryDupeIdx.current.split('-')
                                const lastFlat = allGalleryDupeItems.findIndex(d => d.gi === parseInt(lastParts[0]) && d.ii === parseInt(lastParts[1]))
                                if (curFlat >= 0 && lastFlat >= 0) {
                                  const start = Math.min(curFlat, lastFlat)
                                  const end = Math.max(curFlat, lastFlat)
                                  for (let i = start; i <= end; i++) {
                                    next.add(allGalleryDupeItems[i].media_id)
                                  }
                                }
                              } else {
                                if (next.has(item.media_id)) {
                                  next.delete(item.media_id)
                                } else {
                                  next.add(item.media_id)
                                }
                              }
                              return next
                            })
                            if (!e.shiftKey) lastGalleryDupeIdx.current = flatKey
                          }}
                        >
                          <img
                            src={thumbUrl}
                            alt={item.filename}
                            className={`w-full h-full object-cover ${isSelected ? 'opacity-60' : ''}`}
                            loading="lazy"
                            onError={(e) => { (e.target as HTMLImageElement).style.display = 'none' }}
                          />
                          <div className={`absolute top-1.5 left-1.5 w-5 h-5 rounded flex items-center justify-center transition-all ${
                            isSelected
                              ? 'bg-destructive text-white'
                              : 'bg-black/40 text-white/70 opacity-0 group-hover:opacity-100'
                          }`} style={{ opacity: isSelected ? 1 : undefined }}>
                            <Check className="w-3.5 h-3.5" />
                          </div>
                          <div className="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/70 to-transparent p-1.5 pt-4">
                            <p className="text-[10px] text-white truncate" title={item.filename}>{item.filename}</p>
                            {item.width && item.height && (
                              <p className="text-[10px] text-white/80">{item.width}x{item.height}</p>
                            )}
                            <div className="flex justify-between items-center">
                              <span className="text-[10px] text-violet-300">{item.person_name}</span>
                              <span className="text-[10px] text-white/70">#{item.post_id}</span>
                            </div>
                            {minDist !== null && <span className="text-[10px] text-amber-300">d={minDist}</span>}
                          </div>
                        </button>
                      )
                    })}
                  </div>
                </div>
              ))
              })()}
            </div>
          )}
        </div>
      )}

      {/* Mobile: Full-screen post detail modal */}
      {selectedPost && (
        <div
          className="lg:hidden fixed inset-0 z-50 bg-background overflow-y-auto"
          style={{ paddingTop: 'env(safe-area-inset-top)', overscrollBehavior: 'contain' }}
        >
          <PostDetail
            key={selectedPost.id}
            post={selectedPost}
            onClose={() => setSelectedPost(null)}
            onPostUpdated={(updates) => {
              if (updates) {
                setSelectedPost(prev => prev ? { ...prev, ...updates } : null)
              }
            }}
            attachmentViewMode={attachmentViewMode}
            onToggleAttachmentView={() => setAttachmentViewMode(v => v === 'grid' ? 'list' : 'grid')}
            duplicateAutoSelectDistance={duplicateAutoSelectDistance}
          />
        </div>
      )}

      {/* Gallery View */}
      {viewMode === 'gallery' && (
        <div className="bg-card rounded-xl border border-border overflow-hidden relative">
          {/* Tag Mode Header Bar */}
          {tagMode && (
            <div className="sticky top-0 z-10 bg-primary/10 border-b border-primary/20 px-4 py-3 flex items-center justify-between">
              <div className="flex items-center gap-4">
                <span className="text-sm font-medium text-foreground">
                  {selectedPostIds.size} post{selectedPostIds.size !== 1 ? 's' : ''} selected
                </span>
                <button
                  onClick={selectAllPosts}
                  className="text-sm text-primary hover:underline"
                >
                  Select All ({viewMode === 'gallery' ? new Set(galleryMediaItems.map((m: any) => m.post_id).filter(Boolean)).size : posts.length})
                </button>
                {selectedPostIds.size > 0 && (
                  <button
                    onClick={() => setSelectedPostIds(new Set())}
                    className="text-sm text-muted-foreground hover:text-foreground"
                  >
                    Clear Selection
                  </button>
                )}
              </div>
              <button
                onClick={exitTagMode}
                className="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-secondary text-foreground hover:bg-secondary/80 transition-colors"
              >
                <X className="w-4 h-4" />
                <span className="text-sm">Exit Tag Mode</span>
              </button>
            </div>
          )}

          <div
            ref={viewMode === 'gallery' ? scrollContainerRef : undefined}
            className="p-4 overflow-y-auto"
            style={{ maxHeight: tagMode ? 'calc(100vh - 400px)' : 'calc(100vh - 280px)', minHeight: '400px' }}
          >
            {(isGalleryMediaLoading && galleryMediaItems.length === 0) ? (
              <div className="flex items-center justify-center h-64">
                <div className="animate-spin rounded-full h-8 w-8 border-2 border-primary border-t-transparent"></div>
              </div>
            ) : (galleryMediaItems.length === 0) ? (
              <div className="flex flex-col items-center justify-center h-64 text-muted-foreground">
                <LayoutGrid className="w-12 h-12 mb-2 opacity-50" />
                <p>No media found</p>
                {hasActiveFilters && (
                  <button onClick={clearFilters} className="mt-2 text-sm text-primary hover:underline">
                    Clear filters
                  </button>
                )}
              </div>
            ) : (
              <>
                {/* Thumbnail grid - flat media items from /media endpoint */}
                <div className="grid grid-cols-4 xl:grid-cols-6 2xl:grid-cols-8 gap-2">
                  {galleryMediaItems.map((item) => {
                    const isVideo = item.file_type === 'video'
                    const itemPostId = (item as any).post_id
                    const isPostSelected = itemPostId ? selectedPostIds.has(itemPostId) : false
                    return (
                      <button
                        key={item.id}
                        type="button"
                        className={`relative aspect-square bg-secondary rounded-lg overflow-hidden cursor-pointer transition-all group ${
                          tagMode && isPostSelected
                            ? 'ring-2 ring-primary ring-offset-2 ring-offset-background'
                            : 'hover:ring-2 hover:ring-primary'
                        }`}
                        onClick={() => {
                          if (tagMode) {
                            if (itemPostId) togglePostSelection(itemPostId)
                          } else {
                            // Find item's index in galleryLightboxItems and open lightbox
                            const galleryIdx = galleryLightboxItems.findIndex(m => m.id === item.id)
                            if (galleryIdx >= 0) {
                              setGalleryLightboxIndex(galleryIdx)
                            }
                          }
                        }}
                      >
                        <img
                          src={item.thumbnail_url}
                          alt=""
                          className={`w-full h-full object-cover transition-opacity ${
                            tagMode && isPostSelected ? 'opacity-80' : ''
                          }`}
                          loading="lazy"
                          onError={(e) => {
                            (e.target as HTMLImageElement).style.display = 'none'
                          }}
                        />
                        {/* Selection checkbox overlay in tag mode */}
                        {tagMode && (
                          <div className={`absolute top-2 left-2 w-6 h-6 rounded-md flex items-center justify-center transition-all ${
                            isPostSelected
                              ? 'bg-primary text-primary-foreground'
                              : 'bg-black/50 text-white'
                          }`}>
                            {isPostSelected ? (
                              <Check className="w-4 h-4" />
                            ) : (
                              <Square className="w-4 h-4" />
                            )}
                          </div>
                        )}
                        {/* Video indicator */}
                        {isVideo && !tagMode && (
                          <div className="absolute inset-0 flex items-center justify-center">
                            <div className="w-10 h-10 rounded-full bg-black/50 flex items-center justify-center">
                              <Video className="w-5 h-5 text-white" />
                            </div>
                          </div>
                        )}
                        {/* Duration badge for videos */}
                        {isVideo && item.duration && (
                          <div className="absolute bottom-1 right-1 bg-black/70 text-white text-xs px-1.5 py-0.5 rounded">
                            {Math.floor(item.duration / 60)}:{Math.floor(item.duration % 60).toString().padStart(2, '0')}
                          </div>
                        )}
                        {/* Creator/date info on hover - hide in tag mode */}
                        {!tagMode && (
                          <div className="absolute inset-x-0 bottom-0 bg-gradient-to-t from-black/80 to-transparent p-2 opacity-0 group-hover:opacity-100 transition-opacity">
                            <div className="text-white text-xs truncate font-medium">{item.person?.name || 'Unknown'}</div>
                            <div className="text-white/70 text-xs">
                              {item.media_date ? new Date(item.media_date.includes('T') ? item.media_date : `${item.media_date}T00:00:00`).toLocaleDateString() : 'Unknown'}
                            </div>
                          </div>
                        )}
                      </button>
                    )
                  })}
                </div>

                {/* Infinite scroll trigger */}
                <div className="p-4 flex items-center justify-center">
                  {isGalleryMediaFetchingNext ? (
                    <div className="flex items-center gap-2 text-muted-foreground">
                      <Loader2 className="w-5 h-5 animate-spin" />
                      <span className="text-sm">Loading more...</span>
                    </div>
                  ) : hasGalleryMediaNextPage ? (
                    <span className="text-sm text-muted-foreground">Scroll for more</span>
                  ) : (galleryMediaItems.length > 0) ? (
                    <span className="text-sm text-muted-foreground">End of gallery</span>
                  ) : null}
                </div>
              </>
            )}
          </div>

          {/* Tag Selection Bar - Bottom of gallery in tag mode */}
          {tagMode && (
            <div className="sticky bottom-0 z-10 bg-card border-t border-border p-4">
              <div className="flex flex-col gap-3">
                <div className="flex items-center justify-between">
                  <span className="text-sm font-medium text-foreground flex items-center gap-2">
                    <Tag className="w-4 h-4" />
                    Select tags to apply:
                  </span>
                  {selectedTagIds.size > 0 && (
                    <span className="text-sm text-muted-foreground">
                      {selectedTagIds.size} tag{selectedTagIds.size !== 1 ? 's' : ''} selected
                    </span>
                  )}
                </div>
                <div className="flex flex-wrap gap-2 max-h-32 overflow-y-auto">
                  {availableTags.map((tag) => {
                    const isSelected = selectedTagIds.has(tag.id)
                    return (
                      <button
                        key={tag.id}
                        type="button"
                        onClick={() => toggleTagSelection(tag.id)}
                        className={`px-3 py-1.5 rounded-full text-sm font-medium transition-all border-2 ${
                          isSelected
                            ? 'shadow-md'
                            : 'opacity-60 hover:opacity-100'
                        }`}
                        style={{
                          backgroundColor: isSelected ? tag.color : `${tag.color}20`,
                          color: isSelected ? '#fff' : tag.color,
                          borderColor: tag.color,
                        }}
                      >
                        {tag.name}
                      </button>
                    )
                  })}
                </div>
                <div className="flex items-center justify-end gap-3 pt-2 border-t border-border">
                  <button
                    onClick={exitTagMode}
                    className="px-4 py-2 rounded-lg bg-secondary text-foreground hover:bg-secondary/80 transition-colors"
                  >
                    Cancel
                  </button>
                  <button
                    onClick={applyTags}
                    disabled={selectedPostIds.size === 0 || selectedTagIds.size === 0 || applyTagsMutation.isPending}
                    className="px-4 py-2 rounded-lg bg-primary text-primary-foreground hover:bg-primary/90 transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
                  >
                    {applyTagsMutation.isPending ? (
                      <>
                        <Loader2 className="w-4 h-4 animate-spin" />
                        Applying...
                      </>
                    ) : (
                      <>
                        <Check className="w-4 h-4" />
                        Apply Tags ({selectedPostIds.size} item{selectedPostIds.size !== 1 ? 's' : ''})
                      </>
                    )}
                  </button>
                </div>
              </div>
            </div>
          )}
        </div>
      )}

      {/* Social View - Desktop only (Fansly-style) */}
      {viewMode === 'social' && (
        <>
          {/* PrivateGalleryBundleLightbox for social view */}
          {socialLightboxIndex !== null && socialLightboxIndex >= 0 && (
            <PrivateGalleryBundleLightbox
              items={slideshowPostItems || lightboxItems}
              currentIndex={socialLightboxIndex}
              onClose={() => {
                setSocialLightboxIndex(null)
                setSlideshowActive(false)
                setSlideshowPostItems(null)
                setSlideshowPostId(null)
                setSlideshowPostTotal(0)
              }}
              onNavigate={(idx) => {
                setSocialLightboxIndex(idx)
              }}
              initialSlideshow={slideshowActive}
              totalCount={slideshowPostItems ? slideshowPostTotal : totalMediaItems}
              hasMore={slideshowPostItems ? (slideshowPostItems.length < slideshowPostTotal) : !!hasNextPage}
              onLoadMore={slideshowPostItems ? async () => {
                if (slideshowPostLoading || !slideshowPostId || slideshowPostItems.length >= slideshowPostTotal) return
                setSlideshowPostLoading(true)
                try {
                  const post = selectedPost || (posts.length > 0 ? posts[0] : null)
                  const resp = await api.privateGallery.getPostAttachments(slideshowPostId, slideshowPostItems.length, 500)
                  const newAtts = (resp.items || []).filter(
                    (a: any) => a.file_type === 'image' || a.file_type === 'video'
                  )
                  if (newAtts.length > 0) {
                    const newItems: PrivateGalleryMediaItem[] = newAtts.map((att: any) => ({
                      id: att.id,
                      storage_id: att.id.toString(),
                      filename: att.filename || '',
                      description: att.description || post?.content || null,
                      file_type: att.file_type as 'image' | 'video' | 'other',
                      file_size: att.file_size,
                      width: att.width,
                      height: att.height,
                      duration: att.duration,
                      person: post?.display_name ? {
                        id: post.identity_id || 0,
                        name: post.display_name,
                        relationship: (post as any)._relationship || undefined,
                      } : null,
                      media_date: att.media_date || post?.published_at || '',
                      tags: post?.tags?.map(t => ({ id: t.id, name: t.name, slug: (t as any).slug, color: t.color })),
                      thumbnail_url: att.thumbnail_url || '',
                      file_url: att.file_url || '',
                      stream_url: att.stream_url || '',
                      created_at: att.created_at,
                    }))
                    setSlideshowPostItems(prev => prev ? [...prev, ...newItems] : newItems)
                  }
                } catch (e) {
                  console.error('Failed to load more post attachments:', e)
                } finally {
                  setSlideshowPostLoading(false)
                }
              } : () => { if (hasNextPage && !isFetchingNextPage) fetchNextPage() }}
            />
          )}

          <div className="hidden lg:block bg-card rounded-xl border border-border overflow-hidden">
            <div className="flex h-[calc(100vh-280px)] min-h-[500px]">
              {/* Post List - Left sidebar (narrower in social view) */}
              <div
                ref={viewMode === 'social' ? scrollContainerRef : undefined}
                className="w-80 xl:w-96 border-r border-border overflow-y-auto flex-shrink-0"
              >
                {isLoading && (!posts || posts.length === 0) ? (
                  <div className="flex items-center justify-center h-64">
                    <div className="animate-spin rounded-full h-8 w-8 border-2 border-primary border-t-transparent"></div>
                  </div>
                ) : (!posts || posts.length === 0) ? (
                  <div className="flex flex-col items-center justify-center h-64 text-muted-foreground">
                    <Lock className="w-12 h-12 mb-2 opacity-50" />
                    <p>No posts found</p>
                    {hasActiveFilters && (
                      <button onClick={clearFilters} className="mt-2 text-sm text-primary hover:underline">
                        Clear filters
                      </button>
                    )}
                  </div>
                ) : (
                  <>
                    {posts.map((post) => (
                      <PostCard
                        key={post.id}
                        post={post}
                        isSelected={selectedPost?.id === post.id}
                        onClick={() => setSelectedPost(post)}
                        onOpenLightbox={(attachmentIndex) => {
                          // Find the global index for this post's attachment
                          const attachment = post.attachments?.[attachmentIndex || 0]
                          if (attachment) {
                            const globalIndex = mediaItems.findIndex(m => m.id === attachment.id)
                            if (globalIndex >= 0) {
                              setSocialLightboxIndex(globalIndex)
                            }
                          }
                        }}
                        onToggleRead={() => toggleReadMutation.mutate({ postId: post.id, is_read: !post.is_viewed })}
                      />
                    ))}

                    {/* Infinite scroll trigger */}
                    <div className="p-4 flex items-center justify-center">
                      {isFetchingNextPage ? (
                        <div className="flex items-center gap-2 text-muted-foreground">
                          <Loader2 className="w-5 h-5 animate-spin" />
                          <span className="text-sm">Loading more...</span>
                        </div>
                      ) : hasNextPage ? (
                        <span className="text-sm text-muted-foreground">Scroll for more</span>
                      ) : (posts && posts.length > 0) ? (
                        <span className="text-sm text-muted-foreground">No more posts</span>
                      ) : null}
                    </div>
                  </>
                )}
              </div>

              {/* Post Detail - Right panel */}
              {selectedPost ? (
                <div className="flex-1 overflow-hidden">
                  <PostDetail
                    key={selectedPost.id}
                    post={selectedPost}
                    onClose={() => setSelectedPost(null)}
                    onPostUpdated={(updates) => {
                      if (updates) {
                        setSelectedPost(prev => prev ? { ...prev, ...updates } : null)
                      }
                      queryClient.invalidateQueries({ queryKey: ['private-gallery-posts'] })
                    }}
                    attachmentViewMode={attachmentViewMode}
                    onToggleAttachmentView={() => setAttachmentViewMode(v => v === 'grid' ? 'list' : 'grid')}
                    duplicateAutoSelectDistance={duplicateAutoSelectDistance}
                  />
                </div>
              ) : (
                <div className="flex-1 flex items-center justify-center text-muted-foreground">
                  <div className="text-center">
                    <Columns className="w-12 h-12 mx-auto mb-2 opacity-50" />
                    <p>Select a post to view details</p>
                  </div>
                </div>
              )}
            </div>
          </div>

          {/* Mobile - Continuous scroll of full posts (like Instagram) */}
          <div className="lg:hidden space-y-4">
            {isLoading && (!posts || posts.length === 0) ? (
              <div className="flex items-center justify-center h-64">
                <div className="animate-spin rounded-full h-8 w-8 border-2 border-primary border-t-transparent"></div>
              </div>
            ) : (!posts || posts.length === 0) ? (
              <div className="flex flex-col items-center justify-center h-64 text-muted-foreground">
                <Lock className="w-12 h-12 mb-2 opacity-50" />
                <p>No posts found</p>
              </div>
            ) : (
              <>
                {posts.map((post) => (
                  <div
                    key={post.id}
                    className="bg-card rounded-xl border border-border overflow-hidden cursor-pointer"
                    onClick={() => setMobileSelectedPost(post)}
                  >
                    <PostDetail
                      post={post}
                      onClose={() => {
                        // Post was deleted — refresh the list
                        queryClient.resetQueries({ queryKey: ['private-gallery-posts'] })
                      }}
                      onPostUpdated={(updates) => {
                        if (updates) {
                          // Social view has inline posts, just invalidate to refresh
                        }
                        queryClient.invalidateQueries({ queryKey: ['private-gallery-posts'] })
                      }}
                      attachmentViewMode={attachmentViewMode}
                      onToggleAttachmentView={() => setAttachmentViewMode(v => v === 'grid' ? 'list' : 'grid')}
                      duplicateAutoSelectDistance={duplicateAutoSelectDistance}
                    />
                  </div>
                ))}

                {/* Infinite scroll trigger - mobile only (always social view) */}
                <div className="p-4 flex items-center justify-center">
                  {isFetchingNextPage ? (
                    <div className="flex items-center gap-2 text-muted-foreground">
                      <Loader2 className="w-5 h-5 animate-spin" />
                      <span className="text-sm">Loading more...</span>
                    </div>
                  ) : hasNextPage ? (
                    <span className="text-sm text-muted-foreground">Scroll for more</span>
                  ) : (posts && posts.length > 0) ? (
                    <span className="text-sm text-muted-foreground">No more posts</span>
                  ) : null}
                </div>
              </>
            )}
          </div>

          {/* Mobile Full Post Modal */}
          {mobileSelectedPost && (
            <div className="fixed inset-0 z-50 bg-background overflow-y-auto" style={{ paddingTop: 'env(safe-area-inset-top, 0px)' }}>
              {/* Header with back button */}
              <div className="sticky top-0 z-10 bg-background/95 backdrop-blur border-b border-border px-4 py-3 flex items-center gap-3">
                <button
                  onClick={() => setMobileSelectedPost(null)}
                  className="p-2 -ml-2 hover:bg-secondary rounded-lg"
                >
                  <X className="w-5 h-5" />
                </button>
                <span className="font-medium">Post</span>
              </div>
              {/* Full post view */}
              <div className="p-4">
                <PostDetail
                  post={mobileSelectedPost}
                  onClose={() => setMobileSelectedPost(null)}
                  onPostUpdated={() => {
                    queryClient.invalidateQueries({ queryKey: ['private-gallery-posts'] })
                  }}
                  attachmentViewMode={attachmentViewMode}
                  onToggleAttachmentView={() => setAttachmentViewMode(v => v === 'grid' ? 'list' : 'grid')}
                  duplicateAutoSelectDistance={duplicateAutoSelectDistance}
                />
              </div>
            </div>
          )}
        </>
      )}

      {/* Desktop: Split View / Mobile: Full-width post list (Feed view) */}
      {/* Only show when feed view is active */}
      {viewMode === 'feed' && (
      <div className="bg-card rounded-xl border border-border overflow-hidden">
          <div className="flex lg:h-[calc(100vh-280px)] lg:min-h-[500px]">
            {/* Post List - Full width on mobile, sidebar on desktop */}
            <div
              ref={viewMode === 'feed' ? scrollContainerRef : undefined}
              className={`w-full lg:w-1/3 lg:border-r lg:border-border overflow-y-auto ${
                selectedPost ? 'lg:block' : ''
              }`}
            >
              {isLoading && (!posts || posts.length === 0) ? (
                <div className="flex items-center justify-center h-64">
                  <div className="animate-spin rounded-full h-8 w-8 border-2 border-primary border-t-transparent"></div>
                </div>
              ) : (!posts || posts.length === 0) ? (
                <div className="flex flex-col items-center justify-center h-64 text-muted-foreground">
                  <Lock className="w-12 h-12 mb-2 opacity-50" />
                  <p>No posts found</p>
                  {hasActiveFilters && (
                    <button onClick={clearFilters} className="mt-2 text-sm text-primary hover:underline">
                      Clear filters
                    </button>
                  )}
                </div>
              ) : (
                <>
                  {posts.map((post) => (
                    <PostCard
                      key={post.id}
                      post={post}
                      isSelected={selectedPost?.id === post.id}
                      onClick={() => setSelectedPost(post)}
                      onOpenLightbox={() => {
                        setLightboxPost(post)
                      }}
                      onToggleRead={() => toggleReadMutation.mutate({ postId: post.id, is_read: !post.is_viewed })}
                    />
                  ))}

                  {/* Infinite scroll trigger */}
                  <div className="p-4 flex items-center justify-center">
                    {isFetchingNextPage ? (
                      <div className="flex items-center gap-2 text-muted-foreground">
                        <Loader2 className="w-5 h-5 animate-spin" />
                        <span className="text-sm">Loading more...</span>
                      </div>
                    ) : hasNextPage ? (
                      <span className="text-sm text-muted-foreground">Scroll for more</span>
                    ) : (posts && posts.length > 0) ? (
                      <span className="text-sm text-muted-foreground">No more posts</span>
                    ) : null}
                  </div>
                </>
              )}
            </div>

            {/* Desktop Post Detail - hidden on mobile (uses modal above) */}
            {selectedPost && (
              <div className="hidden lg:block lg:w-2/3">
                <PostDetail
                  post={selectedPost}
                  onClose={() => setSelectedPost(null)}
                  onPostUpdated={(updates) => {
                    if (updates) {
                      setSelectedPost(prev => prev ? { ...prev, ...updates } : null)
                    }
                  }}
                  attachmentViewMode={attachmentViewMode}
                  onToggleAttachmentView={() => setAttachmentViewMode(v => v === 'grid' ? 'list' : 'grid')}
                  duplicateAutoSelectDistance={duplicateAutoSelectDistance}
                />
              </div>
            )}
          </div>
        </div>
      )}

      {/* Upload Modal */}
      <PrivateGalleryUploadModal
        open={showUploadModal}
        onClose={() => setShowUploadModal(false)}
        onSuccess={(postId) => {
          setShowUploadModal(false)
          // Reset query to fetch fresh data from page 0 (better than refetch for infinite queries)
          queryClient.resetQueries({ queryKey: ['private-gallery-posts'] })
          if (postId) setPendingSelectPostId(postId)
          scrollToTop()
        }}
      />

      {/* Edit Post Modal */}
      {editingPost && (
        <EditPostModal
          post={editingPost}
          onClose={() => setEditingPost(null)}
          onSave={() => {
            setEditingPost(null)
            // Clear selected post so it refreshes from the updated data
            setSelectedPost(null)
            // Reset query to fetch fresh data
            queryClient.resetQueries({ queryKey: ['private-gallery-posts'] })
          }}
        />
      )}
    </div>
  )
}

// Edit Post Modal Component
function EditPostModal({
  post,
  onClose,
  onSave,
}: {
  post: PaidContentPost
  onClose: () => void
  onSave: () => void
}) {
  const [description, setDescription] = useState(post.content || '')
  const [date, setDate] = useState(post.published_at?.split('T')[0] || '')
  const [selectedTags, setSelectedTags] = useState<number[]>(
    post.tags?.filter(t => t.id > 0).map(t => t.id) || []
  )
  const [isSaving, setIsSaving] = useState(false)

  // Attachment state for deletion (with extended properties from private gallery mapping)
  type ExtendedAttachment = PaidContentAttachment & { thumbnail_url?: string }
  const [attachments, setAttachments] = useState<ExtendedAttachment[]>(
    (post.attachments || []) as ExtendedAttachment[]
  )
  const [deletingAttachmentId, setDeletingAttachmentId] = useState<number | null>(null)
  const [confirmDeleteId, setConfirmDeleteId] = useState<number | null>(null)
  const [deletedCount, setDeletedCount] = useState(0)

  // Fetch available tags
  const { data: availableTags = [] } = useQuery({
    queryKey: ['private-gallery-tags'],
    queryFn: () => api.privateGallery.getTags(),
  })

  const handleDeleteAttachment = async (attachmentId: number) => {
    // If this is the last attachment, offer to delete the entire post
    if (attachments.length === 1) {
      setConfirmDeleteId(null)
      if (confirm('This is the last attachment. Delete the entire post?')) {
        try {
          await api.privateGallery.deletePost(post.id)
          onSave() // triggers refresh and close
        } catch (error) {
          console.error('Failed to delete post:', error)
          alert('Failed to delete post')
        }
      }
      return
    }

    setDeletingAttachmentId(attachmentId)
    setConfirmDeleteId(null)

    try {
      await api.privateGallery.deleteMedia(attachmentId)
      setAttachments(prev => prev.filter(a => a.id !== attachmentId))
      setDeletedCount(prev => prev + 1)
    } catch (error) {
      console.error('Failed to delete attachment:', error)
      alert('Failed to delete attachment')
    } finally {
      setDeletingAttachmentId(null)
    }
  }

  const handleSave = async () => {
    setIsSaving(true)
    try {
      await api.privateGallery.updatePost(post.id, {
        description,
        media_date: date || undefined,
        tag_ids: selectedTags,
      })
      onSave()
    } catch (error) {
      console.error('Failed to update post:', error)
      alert('Failed to save changes')
    } finally {
      setIsSaving(false)
    }
  }

  const handleClose = () => {
    // If attachments were deleted, trigger a refresh even if cancel is clicked
    if (deletedCount > 0) {
      onSave()
    } else {
      onClose()
    }
  }

  return (
    <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60" onClick={handleClose}>
      <div
        className="bg-card border border-border rounded-xl shadow-xl w-full max-w-lg mx-4 max-h-[90vh] overflow-y-auto"
        onClick={(e) => e.stopPropagation()}
      >
        <div className="sticky top-0 bg-card border-b border-border px-4 py-3 flex items-center justify-between z-10">
          <h3 className="text-lg font-semibold">Edit Post</h3>
          <button onClick={handleClose} className="p-1 rounded-lg hover:bg-secondary">
            <X className="w-5 h-5" />
          </button>
        </div>

        <div className="p-4 space-y-4">
          {/* Attachments */}
          {attachments.length > 0 && (
            <div className="space-y-2">
              <label className="text-sm font-medium flex items-center gap-2">
                <ImageIcon className="w-4 h-4" />
                Attachments ({attachments.length})
              </label>
              <div className="grid grid-cols-3 gap-2">
                {attachments.map((attachment) => {
                  const isDeleting = deletingAttachmentId === attachment.id
                  const isConfirming = confirmDeleteId === attachment.id

                  return (
                    <div key={attachment.id} className="relative group aspect-square">
                      {/* Thumbnail */}
                      <div className="w-full h-full rounded-lg overflow-hidden bg-secondary">
                        {attachment.thumbnail_url ? (
                          <img
                            src={attachment.thumbnail_url}
                            alt=""
                            className="w-full h-full object-cover"
                          />
                        ) : (
                          <div className="w-full h-full flex items-center justify-center">
                            {attachment.file_type === 'video' ? (
                              <Video className="w-8 h-8 text-muted-foreground" />
                            ) : (
                              <ImageIcon className="w-8 h-8 text-muted-foreground" />
                            )}
                          </div>
                        )}

                        {/* Video indicator */}
                        {attachment.file_type === 'video' && (
                          <div className="absolute bottom-1 right-1 bg-black/70 rounded px-1 py-0.5">
                            <Play className="w-3 h-3 text-white" />
                          </div>
                        )}
                      </div>

                      {/* Delete button overlay - always visible */}
                      {!isConfirming && !isDeleting && (
                        <button
                          onClick={() => setConfirmDeleteId(attachment.id)}
                          className="absolute top-1 right-1 p-1.5 rounded-full bg-red-500 text-white shadow-md hover:bg-red-600 transition-colors"
                          title="Delete attachment"
                        >
                          <Trash2 className="w-3.5 h-3.5" />
                        </button>
                      )}

                      {/* Confirmation overlay */}
                      {isConfirming && (
                        <div className="absolute inset-0 bg-black/80 rounded-lg flex flex-col items-center justify-center gap-2 p-2">
                          <AlertTriangle className="w-5 h-5 text-yellow-500" />
                          <span className="text-xs text-white text-center">Delete?</span>
                          <div className="flex gap-1">
                            <button
                              onClick={() => handleDeleteAttachment(attachment.id)}
                              className="px-2 py-1 text-xs bg-red-500 text-white rounded hover:bg-red-600"
                            >
                              Yes
                            </button>
                            <button
                              onClick={() => setConfirmDeleteId(null)}
                              className="px-2 py-1 text-xs bg-secondary text-foreground rounded hover:bg-secondary/80"
                            >
                              No
                            </button>
                          </div>
                        </div>
                      )}

                      {/* Deleting spinner overlay */}
                      {isDeleting && (
                        <div className="absolute inset-0 bg-black/80 rounded-lg flex items-center justify-center">
                          <Loader2 className="w-6 h-6 text-white animate-spin" />
                        </div>
                      )}
                    </div>
                  )
                })}
              </div>
              {deletedCount > 0 && (
                <p className="text-xs text-muted-foreground">
                  {deletedCount} attachment{deletedCount > 1 ? 's' : ''} deleted
                </p>
              )}
            </div>
          )}

          {/* Date */}
          <div className="space-y-2">
            <label className="text-sm font-medium">Date</label>
            <input
              type="date"
              value={date}
              onChange={(e) => setDate(e.target.value)}
              className="w-full px-3 py-2 bg-secondary border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary"
            />
          </div>

          {/* Description */}
          <div className="space-y-2">
            <label className="text-sm font-medium">Description</label>
            <textarea
              value={description}
              onChange={(e) => setDescription(e.target.value)}
              className="w-full px-3 py-2 bg-secondary border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary min-h-[120px] resize-y"
              placeholder="Add a description..."
            />
          </div>

          {/* Tags */}
          <div className="space-y-2">
            <TagSearchSelector
              tags={availableTags}
              selectedTagIds={selectedTags}
              onChange={setSelectedTags}
              label="Tags"
            />
          </div>

          {/* Actions */}
          <div className="flex justify-end gap-2 pt-4 border-t border-border">
            <button
              onClick={handleClose}
              className="px-4 py-2 rounded-lg border border-border hover:bg-secondary"
            >
              Cancel
            </button>
            <button
              onClick={handleSave}
              disabled={isSaving}
              className="px-4 py-2 rounded-lg bg-primary text-primary-foreground hover:bg-primary/90 disabled:opacity-50 flex items-center gap-2"
            >
              {isSaving ? (
                <>
                  <Loader2 className="w-4 h-4 animate-spin" />
                  Saving...
                </>
              ) : (
                <>
                  <Save className="w-4 h-4" />
                  Save
                </>
              )}
            </button>
          </div>
        </div>
      </div>
    </div>
  )
}
