import { forwardRef, memo, useCallback, useEffect, useMemo, useRef, useState, type HTMLAttributes } from 'react'
import { Calendar, Image as ImageIcon, Info, Loader2, PlayCircle, RefreshCw, Trash2, UserRound } from 'lucide-react'
import { VirtuosoGrid } from 'react-virtuoso'
import { finishBackgroundTask, registerBackgroundTask, updateBackgroundTask } from '../services/backgroundTaskMonitor'
import './ResourcesPage.scss'

type MediaTab = 'image' | 'video'

interface MediaStreamItem {
  sessionId: string
  sessionDisplayName?: string
  mediaType: 'image' | 'video'
  localId: number
  serverId?: string
  createTime: number
  localType: number
  senderUsername?: string
  isSend?: number | null
  imageMd5?: string
  imageDatName?: string
  videoMd5?: string
  content?: string
}

interface ContactOption {
  id: string
  name: string
}

type DialogState = {
  mode: 'alert' | 'confirm' | 'info'
  title: string
  message?: string
  infoRows?: Array<{ label: string; value: string }>
  confirmText?: string
  cancelText?: string
  onConfirm?: (() => void) | null
}

const PAGE_SIZE = 96
const MAX_IMAGE_CACHE_RESOLVE_PER_TICK = 12
const MAX_IMAGE_CACHE_PRELOAD_PER_TICK = 24
const MAX_VIDEO_POSTER_RESOLVE_PER_TICK = 3
const INITIAL_IMAGE_PRELOAD_END = 48
const INITIAL_IMAGE_RESOLVE_END = 12
const TASK_PROGRESS_UPDATE_MIN_INTERVAL_MS = 250
const TASK_PROGRESS_UPDATE_MAX_STEPS = 100
const BATCH_IMAGE_DECRYPT_CONCURRENCY = 8

const GridList = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(function GridList(props, ref) {
  const { className = '', ...rest } = props
  const mergedClassName = ['stream-grid-list', className].filter(Boolean).join(' ')
  return <div ref={ref} className={mergedClassName} {...rest} />
})

function GridItem(props: HTMLAttributes<HTMLDivElement>) {
  const { className = '', ...rest } = props
  const mergedClassName = ['stream-grid-item', className].filter(Boolean).join(' ')
  return <div className={mergedClassName} {...rest} />
}

function getRangeTimestampStart(date: string): number | undefined {
  if (!date) return undefined
  const parsed = new Date(`${date}T00:00:00`)
  const n = Math.floor(parsed.getTime() / 1000)
  return Number.isFinite(n) ? n : undefined
}

function getRangeTimestampEnd(date: string): number | undefined {
  if (!date) return undefined
  const parsed = new Date(`${date}T23:59:59`)
  const n = Math.floor(parsed.getTime() / 1000)
  return Number.isFinite(n) ? n : undefined
}

function normalizeMediaToken(value?: string): string {
  return String(value || '').trim().toLowerCase()
}

function getSafeImageDatName(item: Pick<MediaStreamItem, 'imageDatName' | 'imageMd5'>): string {
  const datName = normalizeMediaToken(item.imageDatName)
  if (!datName) return ''
  return datName
}

function hasImageLocator(item: Pick<MediaStreamItem, 'imageDatName' | 'imageMd5'>): boolean {
  return Boolean(normalizeMediaToken(item.imageMd5) || getSafeImageDatName(item))
}

function getItemKey(item: MediaStreamItem): string {
  const sessionId = String(item.sessionId || '').trim().toLowerCase()
  const localId = Number(item.localId || 0)
  if (sessionId && Number.isFinite(localId) && localId > 0) {
    return `${sessionId}|${localId}`
  }

  const serverId = String(item.serverId || '').trim().toLowerCase()
  const createTime = Number(item.createTime || 0)
  const localType = Number(item.localType || 0)
  const mediaId = String(
    item.mediaType === 'video'
      ? (item.videoMd5 || '')
      : (item.imageMd5 || getSafeImageDatName(item) || '')
  ).trim().toLowerCase()
  return `${sessionId}|${createTime}|${localType}|${serverId}|${mediaId}`
}

function formatTimeLabel(timestampSec: number): string {
  if (!timestampSec) return '--:--'
  return new Date(timestampSec * 1000).toLocaleString('zh-CN', {
    year: 'numeric',
    month: '2-digit',
    day: '2-digit',
    hour: '2-digit',
    minute: '2-digit'
  })
}

function formatInfoValue(value: unknown): string {
  if (value === null || value === undefined) return '-'
  const text = String(value).trim()
  return text || '-'
}

function extractVideoTitle(content?: string): string {
  const xml = String(content || '')
  if (!xml) return '视频'
  const match = /<title>([\s\S]*?)<\/title>/i.exec(xml)
  const text = String(match?.[1] || '').replace(/<!\[CDATA\[/g, '').replace(/\]\]>/g, '').trim()
  return text || '视频'
}

function toRenderableMediaSrc(rawPath?: string): string {
  const src = String(rawPath || '').trim()
  if (!src) return ''
  if (/^(data:image\/|blob:|https?:\/\/)/i.test(src)) {
    return src
  }
  if (/^file:\/\//i.test(src)) {
    return src.replace(/#/g, '%23')
  }
  if (src.startsWith('/')) {
    return encodeURI(`file://${src}`).replace(/#/g, '%23')
  }
  if (/^[a-zA-Z]:[\\/]/.test(src)) {
    return encodeURI(`file:///${src.replace(/\\/g, '/')}`).replace(/#/g, '%23')
  }
  return encodeURI(`file://${src.startsWith('/') ? '' : '/'}${src.replace(/\\/g, '/')}`).replace(/#/g, '%23')
}

const MediaCard = memo(function MediaCard({
  item,
  sessionName,
  previewPath,
  videoPosterPath,
  imageIsLong,
  hasPreviewUpdate,
  selected,
  decrypting,
  onToggleSelect,
  onDelete,
  onShowInfo,
  onImagePreviewAction,
  onUpdateImageQuality,
  onOpenVideo,
  onImageLoaded
}: {
  item: MediaStreamItem
  sessionName: string
  previewPath: string
  videoPosterPath: string
  imageIsLong: boolean
  hasPreviewUpdate: boolean
  selected: boolean
  decrypting: boolean
  onToggleSelect: (item: MediaStreamItem) => void
  onDelete: (item: MediaStreamItem) => void
  onShowInfo: (item: MediaStreamItem) => void
  onImagePreviewAction: (item: MediaStreamItem) => void
  onUpdateImageQuality: (item: MediaStreamItem) => void
  onOpenVideo: (item: MediaStreamItem) => void
  onImageLoaded: (item: MediaStreamItem, width: number, height: number) => void
}) {
  const isImage = item.mediaType === 'image'
  const isDecryptingVisual = decrypting
  const showDecryptOverlay = isImage && isDecryptingVisual

  return (
    <article className={`media-card ${selected ? 'selected' : ''} ${isDecryptingVisual ? 'decrypting' : ''}`}>
      <button type="button" className="floating-info" onClick={() => onShowInfo(item)} aria-label="查看资源信息">
        <Info size={14} />
      </button>
      <button type="button" className="floating-delete" onClick={() => onDelete(item)} aria-label="删除资源">
        <Trash2 size={14} />
      </button>

      {isImage && hasPreviewUpdate && (
        <button
          type="button"
          className="floating-update"
          disabled={decrypting}
          onClick={() => onUpdateImageQuality(item)}
          title="已扫描到高清图，点击更新画质"
          aria-label="更新画质"
        >
          <RefreshCw size={13} />
          更新
        </button>
      )}

      <button
        type="button"
        className={`card-visual ${isImage ? 'image' : 'video'}`}
        disabled={isImage && isDecryptingVisual}
        onClick={() => {
          if (isImage) {
            onImagePreviewAction(item)
            return
          }
          onOpenVideo(item)
        }}
      >
        {isImage ? (
          previewPath
            ? <img
              src={toRenderableMediaSrc(previewPath)}
              alt="图片资源"
              className={imageIsLong ? 'long-image' : ''}
              loading="lazy"
              decoding="async"
              onLoad={(event) => {
                const width = event.currentTarget.naturalWidth || 0
                const height = event.currentTarget.naturalHeight || 0
                onImageLoaded(item, width, height)
              }}
            />
            : <div className="placeholder"><ImageIcon size={30} /></div>
        ) : (
          videoPosterPath
            ? <img src={toRenderableMediaSrc(videoPosterPath)} alt="视频封面" loading="lazy" decoding="async" />
            : <div className="placeholder">
              <PlayCircle size={34} />
              <span>{extractVideoTitle(item.content)}</span>
            </div>
        )}
        {showDecryptOverlay && (
          <div className="decrypting-overlay" aria-hidden="true">
            <div className="decrypting-spinner" />
          </div>
        )}
      </button>

      <div className="card-meta" onClick={() => onToggleSelect(item)}>
        <div className="title-row">
          <span className="session" title={sessionName}>{sessionName}</span>
          <span className="time">{formatTimeLabel(item.createTime)}</span>
        </div>
        <div className="sub-row">
          <span>{item.mediaType === 'image' ? '图片' : '视频'}</span>
          {item.senderUsername && <span>{item.senderUsername}</span>}
        </div>
      </div>
    </article>
  )
})

function ResourcesPage() {
  const [tab, setTab] = useState<MediaTab>('image')
  const [contacts, setContacts] = useState<ContactOption[]>([{ id: 'all', name: '全部联系人' }])
  const [selectedContact, setSelectedContact] = useState('all')
  const [dateStart, setDateStart] = useState('')
  const [dateEnd, setDateEnd] = useState('')

  const [items, setItems] = useState<MediaStreamItem[]>([])
  const [selectedKeys, setSelectedKeys] = useState<Set<string>>(new Set())
  const [nextOffset, setNextOffset] = useState(0)
  const [hasMore, setHasMore] = useState(false)
  const [loading, setLoading] = useState(false)
  const [loadingMore, setLoadingMore] = useState(false)
  const [error, setError] = useState('')
  const [batchBusy, setBatchBusy] = useState(false)
  const [decryptingKeys, setDecryptingKeys] = useState<Set<string>>(new Set())
  const [actionMessage, setActionMessage] = useState('')
  const [sessionNameMap, setSessionNameMap] = useState<Record<string, string>>({})
  const [previewPathMap, setPreviewPathMap] = useState<Record<string, string>>({})
  const [previewUpdateMap, setPreviewUpdateMap] = useState<Record<string, boolean>>({})
  const [videoPosterMap, setVideoPosterMap] = useState<Record<string, string>>({})
  const [imageAspectMap, setImageAspectMap] = useState<Record<string, number>>({})
  const [dialog, setDialog] = useState<DialogState | null>(null)

  const previewPathMapRef = useRef<Record<string, string>>({})
  const previewUpdateMapRef = useRef<Record<string, boolean>>({})
  const videoPosterMapRef = useRef<Record<string, string>>({})
  const imageAspectMapRef = useRef<Record<string, number>>({})
  const resolvingImageCacheBatchRef = useRef(false)
  const pendingImageResolveRangeRef = useRef<{ start: number; end: number } | null>(null)
  const imagePreloadUntilRef = useRef<Record<string, number>>({})
  const imageCacheMissUntilRef = useRef<Record<string, number>>({})
  const resolvingVideoPosterKeysRef = useRef<Set<string>>(new Set())
  const attemptedVideoPosterKeysRef = useRef<Set<string>>(new Set())
  const resolvedVideoMd5Ref = useRef<Record<string, string>>({})
  const previewPatchRef = useRef<Record<string, string>>({})
  const updatePatchRef = useRef<Record<string, boolean>>({})
  const previewPatchTimerRef = useRef<number | null>(null)
  const posterPatchRef = useRef<Record<string, string>>({})
  const posterPatchTimerRef = useRef<number | null>(null)
  const aspectPatchRef = useRef<Record<string, number>>({})
  const aspectPatchTimerRef = useRef<number | null>(null)
  const pendingRangeRef = useRef<{ start: number; end: number } | null>(null)
  const rangeTimerRef = useRef<number | null>(null)

  useEffect(() => {
    previewPathMapRef.current = previewPathMap
  }, [previewPathMap])

  useEffect(() => {
    previewUpdateMapRef.current = previewUpdateMap
  }, [previewUpdateMap])

  useEffect(() => {
    videoPosterMapRef.current = videoPosterMap
  }, [videoPosterMap])

  useEffect(() => {
    imageAspectMapRef.current = imageAspectMap
  }, [imageAspectMap])

  useEffect(() => () => {
    if (previewPatchTimerRef.current !== null) {
      window.clearTimeout(previewPatchTimerRef.current)
      previewPatchTimerRef.current = null
    }
    if (posterPatchTimerRef.current !== null) {
      window.clearTimeout(posterPatchTimerRef.current)
      posterPatchTimerRef.current = null
    }
    if (aspectPatchTimerRef.current !== null) {
      window.clearTimeout(aspectPatchTimerRef.current)
      aspectPatchTimerRef.current = null
    }
    if (rangeTimerRef.current !== null) {
      window.clearTimeout(rangeTimerRef.current)
      rangeTimerRef.current = null
    }
  }, [])

  const showAlert = useCallback((message: string, title: string = '提示') => {
    setDialog({
      mode: 'alert',
      title,
      message,
      confirmText: '确定',
      onConfirm: null
    })
  }, [])

  const showConfirm = useCallback((message: string, onConfirm: () => void, title: string = '确认操作') => {
    setDialog({
      mode: 'confirm',
      title,
      message,
      confirmText: '确定',
      cancelText: '取消',
      onConfirm
    })
  }, [])

  const closeDialog = useCallback(() => {
    setDialog(null)
  }, [])

  const isLikelyThumbnailPreview = useCallback((path: string): boolean => {
    const lower = String(path || '').toLowerCase()
    if (!lower) return false
    return lower.includes('_thumb') || lower.includes('_t.') || lower.includes('.t.')
  }, [])

  const flushPreviewPatch = useCallback(() => {
    const pathPatch = previewPatchRef.current
    const updatePatch = updatePatchRef.current
    previewPatchRef.current = {}
    updatePatchRef.current = {}
    previewPatchTimerRef.current = null
    const hasPathPatch = Object.keys(pathPatch).length > 0
    const hasUpdatePatch = Object.keys(updatePatch).length > 0
    if (hasPathPatch) {
      setPreviewPathMap((prev) => ({ ...prev, ...pathPatch }))
    }
    if (hasUpdatePatch) {
      setPreviewUpdateMap((prev) => ({ ...prev, ...updatePatch }))
    }
  }, [])

  const queuePreviewPatch = useCallback((itemKey: string, localPath: string, hasUpdate: boolean) => {
    if (!itemKey || !localPath) return
    if (previewPathMapRef.current[itemKey] === localPath && previewUpdateMapRef.current[itemKey] === hasUpdate) return
    previewPatchRef.current[itemKey] = localPath
    updatePatchRef.current[itemKey] = hasUpdate
    if (previewPatchTimerRef.current !== null) return
    previewPatchTimerRef.current = window.setTimeout(flushPreviewPatch, 16)
  }, [flushPreviewPatch])

  const flushPosterPatch = useCallback(() => {
    const patch = posterPatchRef.current
    posterPatchRef.current = {}
    posterPatchTimerRef.current = null
    if (Object.keys(patch).length === 0) return
    setVideoPosterMap((prev) => ({ ...prev, ...patch }))
  }, [])

  const queuePosterPatch = useCallback((itemKey: string, posterPath: string) => {
    if (!itemKey || !posterPath) return
    if (videoPosterMapRef.current[itemKey] === posterPath) return
    posterPatchRef.current[itemKey] = posterPath
    if (posterPatchTimerRef.current !== null) return
    posterPatchTimerRef.current = window.setTimeout(flushPosterPatch, 16)
  }, [flushPosterPatch])

  const flushAspectPatch = useCallback(() => {
    const patch = aspectPatchRef.current
    aspectPatchRef.current = {}
    aspectPatchTimerRef.current = null
    if (Object.keys(patch).length === 0) return
    setImageAspectMap((prev) => ({ ...prev, ...patch }))
  }, [])

  const queueAspectPatch = useCallback((itemKey: string, ratio: number) => {
    const old = imageAspectMapRef.current[itemKey]
    if (typeof old === 'number' && Math.abs(old - ratio) < 0.01) return
    aspectPatchRef.current[itemKey] = ratio
    if (aspectPatchTimerRef.current !== null) return
    aspectPatchTimerRef.current = window.setTimeout(flushAspectPatch, 24)
  }, [flushAspectPatch])

  const loadStream = useCallback(async (reset: boolean) => {
    if (reset) setLoading(true)
    else setLoadingMore(true)
    if (reset) {
      setError('')
      setActionMessage('')
    }

    try {
      if (reset) {
        const connectResult = await window.electronAPI.chat.connect()
        if (!connectResult.success) {
          setError(connectResult.error || '连接数据库失败')
          return
        }
      }
      const requestOffset = reset ? 0 : nextOffset
      const streamResult = await window.electronAPI.chat.getMediaStream({
        sessionId: selectedContact === 'all' ? undefined : selectedContact,
        mediaType: tab,
        beginTimestamp: getRangeTimestampStart(dateStart),
        endTimestamp: getRangeTimestampEnd(dateEnd),
        offset: requestOffset,
        limit: PAGE_SIZE
      })

      if (!streamResult.success) {
        setError(streamResult.error || '加载失败')
        if (reset) {
          previewPatchRef.current = {}
          updatePatchRef.current = {}
          posterPatchRef.current = {}
          aspectPatchRef.current = {}
          if (previewPatchTimerRef.current !== null) {
            window.clearTimeout(previewPatchTimerRef.current)
            previewPatchTimerRef.current = null
          }
          if (posterPatchTimerRef.current !== null) {
            window.clearTimeout(posterPatchTimerRef.current)
            posterPatchTimerRef.current = null
          }
          if (aspectPatchTimerRef.current !== null) {
            window.clearTimeout(aspectPatchTimerRef.current)
            aspectPatchTimerRef.current = null
          }
          if (rangeTimerRef.current !== null) {
            window.clearTimeout(rangeTimerRef.current)
            rangeTimerRef.current = null
          }
          pendingRangeRef.current = null
          resolvingImageCacheBatchRef.current = false
          pendingImageResolveRangeRef.current = null
          imagePreloadUntilRef.current = {}
          imageCacheMissUntilRef.current = {}
          resolvingVideoPosterKeysRef.current.clear()
          attemptedVideoPosterKeysRef.current.clear()
          resolvedVideoMd5Ref.current = {}
          setItems([])
          setNextOffset(0)
          setHasMore(false)
          setSelectedKeys(new Set())
          setPreviewPathMap({})
          setPreviewUpdateMap({})
          setVideoPosterMap({})
          setImageAspectMap({})
        }
        return
      }

      const incoming = (streamResult.items || []) as MediaStreamItem[]
      if (reset) {
        previewPatchRef.current = {}
        updatePatchRef.current = {}
        posterPatchRef.current = {}
        aspectPatchRef.current = {}
        if (previewPatchTimerRef.current !== null) {
          window.clearTimeout(previewPatchTimerRef.current)
          previewPatchTimerRef.current = null
        }
        if (posterPatchTimerRef.current !== null) {
          window.clearTimeout(posterPatchTimerRef.current)
          posterPatchTimerRef.current = null
        }
        if (aspectPatchTimerRef.current !== null) {
          window.clearTimeout(aspectPatchTimerRef.current)
          aspectPatchTimerRef.current = null
        }
        if (rangeTimerRef.current !== null) {
          window.clearTimeout(rangeTimerRef.current)
          rangeTimerRef.current = null
        }
        pendingRangeRef.current = null
        resolvingImageCacheBatchRef.current = false
        pendingImageResolveRangeRef.current = null
        imagePreloadUntilRef.current = {}
        imageCacheMissUntilRef.current = {}
        resolvingVideoPosterKeysRef.current.clear()
        attemptedVideoPosterKeysRef.current.clear()
        resolvedVideoMd5Ref.current = {}
        setItems(incoming)
        setSelectedKeys(new Set())
        setPreviewPathMap({})
        setPreviewUpdateMap({})
        setVideoPosterMap({})
        setImageAspectMap({})
      } else {
        setItems((prev) => {
          const map = new Map(prev.map((row) => [getItemKey(row), row]))
          incoming.forEach((row) => map.set(getItemKey(row), row))
          return Array.from(map.values())
        })
      }
      setNextOffset(Number(streamResult.nextOffset || requestOffset + incoming.length))
      setHasMore(Boolean(streamResult.hasMore))
    } catch (e) {
      setError(String(e))
    } finally {
      setLoading(false)
      setLoadingMore(false)
    }
  }, [dateEnd, dateStart, nextOffset, selectedContact, tab])

  useEffect(() => {
    void loadStream(true)
  }, [tab, selectedContact, dateStart, dateEnd])

  useEffect(() => {
    let cancelled = false
    const run = async () => {
      try {
        const sessionResult = await window.electronAPI.chat.getSessions()
        if (!cancelled && sessionResult.success && Array.isArray(sessionResult.sessions)) {
          const initialNameMap: Record<string, string> = {}
          sessionResult.sessions.forEach((session) => {
            initialNameMap[session.username] = session.displayName || session.username
          })
          setSessionNameMap(initialNameMap)
          setContacts([
            { id: 'all', name: '全部联系人' },
            ...sessionResult.sessions.map((session) => ({
              id: session.username,
              name: session.displayName || session.username
            }))
          ])
        }
      } catch {
        // ignore
      }
    }
    void run()
    return () => {
      cancelled = true
    }
  }, [])

  const displayItems = useMemo(() => (
    items.map((item) => ({
      ...item,
      sessionDisplayName: item.sessionDisplayName || sessionNameMap[item.sessionId] || item.sessionId
    }))
  ), [items, sessionNameMap])

  useEffect(() => {
    const imageKeySet = new Set(
      displayItems
        .filter((item) => item.mediaType === 'image')
        .map((item) => getItemKey(item))
    )
    const videoKeySet = new Set(
      displayItems
        .filter((item) => item.mediaType === 'video')
        .map((item) => getItemKey(item))
    )

    setPreviewPathMap((prev) => {
      let changed = false
      const next: Record<string, string> = {}
      for (const [key, value] of Object.entries(prev)) {
        if (!imageKeySet.has(key)) continue
        next[key] = value
      }
      if (Object.keys(next).length !== Object.keys(prev).length) changed = true
      return changed ? next : prev
    })

    setPreviewUpdateMap((prev) => {
      let changed = false
      const next: Record<string, boolean> = {}
      for (const [key, value] of Object.entries(prev)) {
        if (!imageKeySet.has(key)) continue
        next[key] = value
      }
      if (Object.keys(next).length !== Object.keys(prev).length) changed = true
      return changed ? next : prev
    })

    setImageAspectMap((prev) => {
      let changed = false
      const next: Record<string, number> = {}
      for (const [key, value] of Object.entries(prev)) {
        if (!imageKeySet.has(key)) continue
        next[key] = value
      }
      if (Object.keys(next).length !== Object.keys(prev).length) changed = true
      return changed ? next : prev
    })

    setVideoPosterMap((prev) => {
      let changed = false
      const next: Record<string, string> = {}
      for (const [key, value] of Object.entries(prev)) {
        if (!videoKeySet.has(key)) continue
        next[key] = value
      }
      if (Object.keys(next).length !== Object.keys(prev).length) changed = true
      return changed ? next : prev
    })

    const validKeys = new Set<string>(displayItems.map((item) => getItemKey(item)))
    const nextResolvedVideoMd5: Record<string, string> = {}
    for (const [key, value] of Object.entries(resolvedVideoMd5Ref.current)) {
      if (!validKeys.has(key)) continue
      nextResolvedVideoMd5[key] = value
    }
    resolvedVideoMd5Ref.current = nextResolvedVideoMd5

    const nextAttempted = new Set<string>()
    attemptedVideoPosterKeysRef.current.forEach((key) => {
      if (validKeys.has(key)) nextAttempted.add(key)
    })
    attemptedVideoPosterKeysRef.current = nextAttempted

    const nextImageMissUntil: Record<string, number> = {}
    for (const [key, value] of Object.entries(imageCacheMissUntilRef.current)) {
      if (!validKeys.has(key)) continue
      nextImageMissUntil[key] = value
    }
    imageCacheMissUntilRef.current = nextImageMissUntil

    const nextImagePreloadUntil: Record<string, number> = {}
    for (const [key, value] of Object.entries(imagePreloadUntilRef.current)) {
      if (!validKeys.has(key)) continue
      nextImagePreloadUntil[key] = value
    }
    imagePreloadUntilRef.current = nextImagePreloadUntil

  }, [displayItems])

  const resolveImageCacheRange = useCallback((start: number, end: number) => {
    const from = Math.max(0, start)
    const to = Math.min(displayItems.length - 1, end)
    if (to < from) return
    const now = Date.now()
    const payloads: Array<{ sessionId?: string; imageMd5?: string; imageDatName?: string; createTime?: number }> = []
    const itemKeys: string[] = []
    for (let i = from; i <= to; i += 1) {
      const item = displayItems[i]
      if (!item || item.mediaType !== 'image') continue
      const itemKey = getItemKey(item)
      if (previewPathMapRef.current[itemKey] || previewPatchRef.current[itemKey]) continue
      if (!hasImageLocator(item)) continue
      if ((imageCacheMissUntilRef.current[itemKey] || 0) > now) continue
      payloads.push({
        sessionId: item.sessionId,
        imageMd5: normalizeMediaToken(item.imageMd5) || undefined,
        imageDatName: getSafeImageDatName(item) || undefined,
        createTime: Number(item.createTime || 0) || undefined
      })
      itemKeys.push(itemKey)
      if (payloads.length >= MAX_IMAGE_CACHE_RESOLVE_PER_TICK) break
    }
    if (payloads.length === 0) return
    if (resolvingImageCacheBatchRef.current) {
      pendingImageResolveRangeRef.current = { start: from, end: to }
      return
    }

    resolvingImageCacheBatchRef.current = true
    void (async () => {
      try {
        const result = await window.electronAPI.image.resolveCacheBatch(payloads, {
          disableUpdateCheck: true,
          allowCacheIndex: true,
          preferFilePath: true,
          hardlinkOnly: true,
          suppressEvents: true
        })
        const rows = Array.isArray(result?.rows) ? result.rows : []
        const pathPatch: Record<string, string> = {}
        const updatePatch: Record<string, boolean> = {}
        const missUntil = Date.now() + 4500

        for (let i = 0; i < itemKeys.length; i += 1) {
          const itemKey = itemKeys[i]
          const row = rows[i]
          if (row?.success && row.localPath) {
            delete imageCacheMissUntilRef.current[itemKey]
            pathPatch[itemKey] = row.localPath
            updatePatch[itemKey] = Boolean(row.hasUpdate)
          } else {
            imageCacheMissUntilRef.current[itemKey] = missUntil
          }
        }

        if (Object.keys(pathPatch).length > 0) {
          setPreviewPathMap((prev) => ({ ...prev, ...pathPatch }))
        }
        if (Object.keys(updatePatch).length > 0) {
          setPreviewUpdateMap((prev) => ({ ...prev, ...updatePatch }))
        }
      } catch {
        const missUntil = Date.now() + 4500
        itemKeys.forEach((itemKey) => {
          imageCacheMissUntilRef.current[itemKey] = missUntil
        })
      } finally {
        resolvingImageCacheBatchRef.current = false
        const pending = pendingImageResolveRangeRef.current
        pendingImageResolveRangeRef.current = null
        if (pending) {
          resolveImageCacheRange(pending.start, pending.end)
        }
      }
    })()
  }, [displayItems])

  const preloadImageCacheRange = useCallback((start: number, end: number) => {
    const from = Math.max(0, start)
    const to = Math.min(displayItems.length - 1, end)
    if (to < from) return

    const now = Date.now()
    const payloads: Array<{ sessionId?: string; imageMd5?: string; imageDatName?: string; createTime?: number }> = []
    const dedup = new Set<string>()
    for (let i = from; i <= to; i += 1) {
      const item = displayItems[i]
      if (!item || item.mediaType !== 'image') continue
      const itemKey = getItemKey(item)
      if (previewPathMapRef.current[itemKey] || previewPatchRef.current[itemKey]) continue
      if (!hasImageLocator(item)) continue
      if ((imagePreloadUntilRef.current[itemKey] || 0) > now) continue
      const dedupKey = `${item.sessionId || ''}|${normalizeMediaToken(item.imageMd5)}|${getSafeImageDatName(item)}`
      if (dedup.has(dedupKey)) continue
      dedup.add(dedupKey)
      imagePreloadUntilRef.current[itemKey] = now + 12000
      payloads.push({
        sessionId: item.sessionId,
        imageMd5: normalizeMediaToken(item.imageMd5) || undefined,
        imageDatName: getSafeImageDatName(item) || undefined,
        createTime: Number(item.createTime || 0) || undefined
      })
      if (payloads.length >= MAX_IMAGE_CACHE_PRELOAD_PER_TICK) break
    }
    if (payloads.length === 0) return
    void window.electronAPI.image.preload(payloads, {
      allowDecrypt: false,
      allowCacheIndex: true
    })
  }, [displayItems])

  const resolveItemVideoMd5 = useCallback(async (item: MediaStreamItem): Promise<string> => {
    const itemKey = getItemKey(item)
    const cached = resolvedVideoMd5Ref.current[itemKey]
    if (cached) return cached

    let md5 = String(item.videoMd5 || '').trim().toLowerCase()
    if (md5) {
      resolvedVideoMd5Ref.current[itemKey] = md5
      return md5
    }
    const parsed = await window.electronAPI.video.parseVideoMd5(String(item.content || ''))
    if (parsed.success && parsed.md5) md5 = String(parsed.md5).trim().toLowerCase()
    if (md5) resolvedVideoMd5Ref.current[itemKey] = md5
    return md5
  }, [])

  const showMediaInfo = useCallback(async (item: MediaStreamItem) => {
    const itemKey = getItemKey(item)
    const mediaLabel = item.mediaType === 'image' ? '图片' : '视频'
    const baseRows: Array<{ label: string; value: string }> = [
      { label: '资源类型', value: mediaLabel },
      { label: '会话 ID', value: formatInfoValue(item.sessionId) },
      { label: '消息 LocalId', value: formatInfoValue(item.localId) },
      { label: '消息时间', value: formatTimeLabel(item.createTime) },
      { label: '发送方', value: formatInfoValue(item.senderUsername) },
      { label: '是否我发送', value: item.isSend === 1 ? '是' : (item.isSend === 0 ? '否' : '-') }
    ]

    setDialog({
      mode: 'info',
      title: `${mediaLabel}信息`,
      infoRows: [...baseRows, { label: '状态', value: '正在读取缓存信息...' }],
      confirmText: '关闭',
      onConfirm: null
    })

    try {
      if (item.mediaType === 'image') {
        const resolved = await window.electronAPI.image.resolveCache({
          sessionId: item.sessionId,
          imageMd5: normalizeMediaToken(item.imageMd5) || undefined,
          imageDatName: getSafeImageDatName(item) || undefined,
          createTime: Number(item.createTime || 0) || undefined,
          preferFilePath: true,
          hardlinkOnly: true,
          allowCacheIndex: true,
          suppressEvents: true
        })
        const previewPath = previewPathMapRef.current[itemKey] || previewPatchRef.current[itemKey] || ''
        const cachePath = String(resolved?.localPath || previewPath || '').trim()
        const rows: Array<{ label: string; value: string }> = [
          ...baseRows,
          { label: 'imageMd5', value: formatInfoValue(normalizeMediaToken(item.imageMd5)) },
          { label: 'imageDatName', value: formatInfoValue(getSafeImageDatName(item)) },
          { label: '列表预览路径', value: formatInfoValue(previewPath) },
          { label: '缓存命中', value: resolved?.success && cachePath ? '是' : '否' },
          { label: '缓存路径', value: formatInfoValue(cachePath) },
          { label: '缓存可更新', value: resolved?.hasUpdate ? '是' : '否' },
          { label: '缓存状态', value: resolved?.success ? '可用' : formatInfoValue(resolved?.error || resolved?.failureKind || '未命中') }
        ]
        setDialog({
          mode: 'info',
          title: '图片信息',
          infoRows: rows,
          confirmText: '关闭',
          onConfirm: null
        })
        return
      }

      const resolvedMd5 = await resolveItemVideoMd5(item)
      const videoInfo = resolvedMd5
        ? await window.electronAPI.video.getVideoInfo(resolvedMd5, { includePoster: true, posterFormat: 'fileUrl' })
        : null
      const posterPath = videoPosterMapRef.current[itemKey] || posterPatchRef.current[itemKey] || ''
      const rows: Array<{ label: string; value: string }> = [
        ...baseRows,
        { label: 'videoMd5(消息)', value: formatInfoValue(normalizeMediaToken(item.videoMd5)) },
        { label: 'videoMd5(解析)', value: formatInfoValue(resolvedMd5) },
        { label: '视频文件存在', value: videoInfo?.success && videoInfo.exists ? '是' : '否' },
        { label: '视频路径', value: formatInfoValue(videoInfo?.videoUrl) },
        { label: '同名封面路径', value: formatInfoValue(videoInfo?.coverUrl) },
        { label: '列表封面路径', value: formatInfoValue(posterPath) },
        { label: '视频状态', value: videoInfo?.success ? '可用' : formatInfoValue(videoInfo?.error || '未找到') }
      ]
      setDialog({
        mode: 'info',
        title: '视频信息',
        infoRows: rows,
        confirmText: '关闭',
        onConfirm: null
      })
    } catch (e) {
      setDialog({
        mode: 'info',
        title: `${mediaLabel}信息`,
        infoRows: [...baseRows, { label: '读取失败', value: formatInfoValue(String(e)) }],
        confirmText: '关闭',
        onConfirm: null
      })
    }
  }, [resolveItemVideoMd5])

  const resolveVideoPoster = useCallback(async (item: MediaStreamItem) => {
    if (item.mediaType !== 'video') return
    const itemKey = getItemKey(item)
    if (videoPosterMapRef.current[itemKey]) return
    if (attemptedVideoPosterKeysRef.current.has(itemKey)) return
    if (resolvingVideoPosterKeysRef.current.has(itemKey)) return

    resolvingVideoPosterKeysRef.current.add(itemKey)
    try {
      const md5 = await resolveItemVideoMd5(item)
      if (!md5) {
        attemptedVideoPosterKeysRef.current.add(itemKey)
        return
      }
      const info = await window.electronAPI.video.getVideoInfo(md5, { includePoster: true, posterFormat: 'fileUrl' })
      if (!info.success || !info.exists) {
        attemptedVideoPosterKeysRef.current.add(itemKey)
        return
      }
      const poster = String(info.coverUrl || '')
      if (!poster) {
        attemptedVideoPosterKeysRef.current.add(itemKey)
        return
      }
      queuePosterPatch(itemKey, poster)
      attemptedVideoPosterKeysRef.current.add(itemKey)
    } catch {
      attemptedVideoPosterKeysRef.current.add(itemKey)
    } finally {
      resolvingVideoPosterKeysRef.current.delete(itemKey)
    }
  }, [queuePosterPatch, resolveItemVideoMd5])

  const resolvePosterRange = useCallback((start: number, end: number) => {
    const from = Math.max(0, start)
    const to = Math.min(displayItems.length - 1, end)
    if (to < from) return
    let resolvedCount = 0
    for (let i = from; i <= to; i += 1) {
      const item = displayItems[i]
      if (!item || item.mediaType !== 'video') continue
      void resolveVideoPoster(item)
      resolvedCount += 1
      if (resolvedCount >= MAX_VIDEO_POSTER_RESOLVE_PER_TICK) break
    }
  }, [displayItems, resolveVideoPoster])

  const flushRangeResolve = useCallback(() => {
    rangeTimerRef.current = null
    const pending = pendingRangeRef.current
    if (!pending) return
    pendingRangeRef.current = null
    if (tab === 'image') {
      preloadImageCacheRange(pending.start - 4, pending.end + 20)
      resolveImageCacheRange(pending.start - 1, pending.end + 6)
      return
    }
    resolvePosterRange(pending.start, pending.end)
  }, [preloadImageCacheRange, resolveImageCacheRange, resolvePosterRange, tab])

  const scheduleRangeResolve = useCallback((start: number, end: number) => {
    const previous = pendingRangeRef.current
    if (previous && start >= previous.start && end <= previous.end) {
      return
    }
    pendingRangeRef.current = { start, end }
    if (rangeTimerRef.current !== null) {
      window.clearTimeout(rangeTimerRef.current)
      rangeTimerRef.current = null
    }
    rangeTimerRef.current = window.setTimeout(flushRangeResolve, 120)
  }, [flushRangeResolve])

  useEffect(() => {
    if (displayItems.length === 0) return
    if (tab === 'image') {
      preloadImageCacheRange(0, Math.min(displayItems.length - 1, INITIAL_IMAGE_PRELOAD_END))
      resolveImageCacheRange(0, Math.min(displayItems.length - 1, INITIAL_IMAGE_RESOLVE_END))
      return
    }
    resolvePosterRange(0, Math.min(displayItems.length - 1, 12))
  }, [displayItems, preloadImageCacheRange, resolveImageCacheRange, resolvePosterRange, tab])

  const selectedItems = useMemo(() => {
    if (selectedKeys.size === 0) return []
    return displayItems.filter((item) => selectedKeys.has(getItemKey(item)))
  }, [displayItems, selectedKeys])

  const toggleSelect = useCallback((item: MediaStreamItem) => {
    const key = getItemKey(item)
    setSelectedKeys((prev) => {
      const next = new Set(prev)
      if (next.has(key)) next.delete(key)
      else next.add(key)
      return next
    })
  }, [])

  const onImageLoaded = useCallback((item: MediaStreamItem, width: number, height: number) => {
    if (item.mediaType !== 'image') return
    if (!width || !height) return
    const ratio = height / width
    if (!Number.isFinite(ratio) || ratio <= 0) return
    const itemKey = getItemKey(item)
    queueAspectPatch(itemKey, ratio)
  }, [queueAspectPatch])

  const deleteOne = useCallback((item: MediaStreamItem) => {
    showConfirm('确认删除该原始记录？此操作不可恢复。', () => {
      void (async () => {
        const result = await window.electronAPI.chat.deleteMessage(item.sessionId, item.localId, item.createTime)
        if (!result.success) {
          showAlert(`删除失败：${result.error || '未知错误'}`, '删除失败')
          return
        }

        const key = getItemKey(item)
        setItems((prev) => prev.filter((row) => getItemKey(row) !== key))
        setSelectedKeys((prev) => {
          const next = new Set(prev)
          next.delete(key)
          return next
        })
        setPreviewPathMap((prev) => {
          if (!prev[key]) return prev
          const next = { ...prev }
          delete next[key]
          return next
        })
        setPreviewUpdateMap((prev) => {
          if (prev[key] === undefined) return prev
          const next = { ...prev }
          delete next[key]
          return next
        })
        setActionMessage('删除成功')
      })()
    }, '删除确认')
  }, [showAlert, showConfirm])

  const batchDelete = useCallback(() => {
    if (selectedItems.length === 0 || batchBusy) return

    showConfirm(`确认删除选中 ${selectedItems.length} 条记录？此操作不可恢复。`, () => {
      void (async () => {
        setBatchBusy(true)
        let success = 0
        const deletedKeys = new Set<string>()
        try {
          for (const item of selectedItems) {
            const result = await window.electronAPI.chat.deleteMessage(item.sessionId, item.localId, item.createTime)
            if (result.success) {
              success += 1
              deletedKeys.add(getItemKey(item))
            }
          }

          setItems((prev) => prev.filter((item) => !deletedKeys.has(getItemKey(item))))
          setSelectedKeys(new Set())
          setPreviewPathMap((prev) => {
            const next = { ...prev }
            deletedKeys.forEach((key) => { delete next[key] })
            return next
          })
          setPreviewUpdateMap((prev) => {
            const next = { ...prev }
            deletedKeys.forEach((key) => { delete next[key] })
            return next
          })
          setActionMessage(`批量删除完成：成功 ${success}，失败 ${selectedItems.length - success}`)
          showAlert(`批量删除完成：成功 ${success}，失败 ${selectedItems.length - success}`, '批量删除完成')
        } finally {
          setBatchBusy(false)
        }
      })()
    }, '批量删除确认')
  }, [batchBusy, selectedItems, showAlert, showConfirm])

  const decryptImage = useCallback(async (
    item: MediaStreamItem,
    options?: { allowCacheIndex?: boolean }
  ): Promise<string | undefined> => {
    if (item.mediaType !== 'image') return

    const key = getItemKey(item)
    if (!hasImageLocator(item)) {
      showAlert('当前图片缺少解密所需字段（imageMd5/imageDatName）', '无法解密')
      return
    }

    setDecryptingKeys((prev) => {
      const next = new Set(prev)
      next.add(key)
      return next
    })

    try {
      const result = await window.electronAPI.image.decrypt({
        sessionId: item.sessionId,
        imageMd5: normalizeMediaToken(item.imageMd5) || undefined,
        imageDatName: getSafeImageDatName(item) || undefined,
        createTime: Number(item.createTime || 0) || undefined,
        force: true,
        preferFilePath: true,
        hardlinkOnly: true,
        allowCacheIndex: options?.allowCacheIndex ?? true,
        suppressEvents: true
      })
      if (!result?.success) {
        if (result?.failureKind === 'decrypt_failed') {
          showAlert(`解密失败：${result?.error || '解密后不是有效图片'}`, '解密失败')
        } else {
          showAlert(`本地无数据：${result?.error || '未找到原始 DAT 文件'}`, '未找到本地数据')
        }
        return undefined
      }

      if (result.localPath) {
        const localPath = result.localPath as string
        setPreviewPathMap((prev) => ({ ...prev, [key]: localPath }))
        setPreviewUpdateMap((prev) => ({ ...prev, [key]: isLikelyThumbnailPreview(localPath) }))
        setActionMessage('图片解密完成')
        return localPath
      }
      try {
        const resolved = await window.electronAPI.image.resolveCache({
          sessionId: item.sessionId,
          imageMd5: normalizeMediaToken(item.imageMd5) || undefined,
          imageDatName: getSafeImageDatName(item) || undefined,
          createTime: Number(item.createTime || 0) || undefined,
          preferFilePath: true,
          hardlinkOnly: true,
          allowCacheIndex: true,
          suppressEvents: true
        })
        if (resolved?.success && resolved.localPath) {
          const localPath = resolved.localPath
          setPreviewPathMap((prev) => ({ ...prev, [key]: localPath }))
          setPreviewUpdateMap((prev) => ({ ...prev, [key]: Boolean(resolved.hasUpdate) }))
          setActionMessage('图片解密完成')
          return localPath
        }
      } catch {
        // ignore
      }
      setActionMessage('图片解密完成')
      return undefined
    } catch (e) {
      showAlert(`本地无数据：${String(e)}`, '未找到本地数据')
      return undefined
    } finally {
      setDecryptingKeys((prev) => {
        const next = new Set(prev)
        next.delete(key)
        return next
      })
    }
  }, [isLikelyThumbnailPreview, showAlert])

  const onImagePreviewAction = useCallback(async (item: MediaStreamItem) => {
    if (item.mediaType !== 'image') return
    const key = getItemKey(item)
    let localPath = previewPathMapRef.current[key] || previewPatchRef.current[key] || ''

    if (localPath) {
      try {
        const resolved = await window.electronAPI.image.resolveCache({
          sessionId: item.sessionId,
          imageMd5: normalizeMediaToken(item.imageMd5) || undefined,
          imageDatName: getSafeImageDatName(item) || undefined,
          createTime: Number(item.createTime || 0) || undefined,
          preferFilePath: true,
          hardlinkOnly: true,
          allowCacheIndex: true,
          suppressEvents: true
        })
        if (resolved?.success && resolved.localPath) {
          localPath = resolved.localPath
          queuePreviewPatch(key, localPath, Boolean(resolved.hasUpdate))
        }
      } catch {
        // ignore
      }
      if (localPath) {
        await window.electronAPI.window.openImageViewerWindow(localPath)
        return
      }
    }

    try {
      const resolved = await window.electronAPI.image.resolveCache({
        sessionId: item.sessionId,
        imageMd5: normalizeMediaToken(item.imageMd5) || undefined,
        imageDatName: getSafeImageDatName(item) || undefined,
        createTime: Number(item.createTime || 0) || undefined,
        preferFilePath: true,
        hardlinkOnly: true,
        allowCacheIndex: true,
        suppressEvents: true
      })
      if (resolved?.success && resolved.localPath) {
        localPath = resolved.localPath
        queuePreviewPatch(key, localPath, Boolean(resolved.hasUpdate))
        await window.electronAPI.window.openImageViewerWindow(localPath)
        return
      }
    } catch {
      // ignore
    }

    await decryptImage(item)
  }, [decryptImage, queuePreviewPatch])

  const updateImageQuality = useCallback(async (item: MediaStreamItem) => {
    await decryptImage(item)
  }, [decryptImage])

  const batchDecryptImage = useCallback(async () => {
    if (batchBusy) return

    const imageItems = selectedItems.filter((item) => item.mediaType === 'image')
    if (imageItems.length === 0) {
      showAlert('当前选中中没有图片资源', '无法批量解密')
      return
    }

    setBatchBusy(true)
    let success = 0
    let notFound = 0
    let decryptFailed = 0
    const previewPatch: Record<string, string> = {}
    const updatePatch: Record<string, boolean> = {}
    const taskId = registerBackgroundTask({
      sourcePage: 'other',
      title: '资源页图片批量解密',
      detail: `正在解密图片（0/${imageItems.length}）`,
      progressText: `0 / ${imageItems.length}`,
      cancelable: false
    })
    try {
      let completed = 0
      const progressStep = Math.max(1, Math.floor(imageItems.length / TASK_PROGRESS_UPDATE_MAX_STEPS))
      let lastProgressBucket = 0
      let lastProgressUpdateAt = Date.now()
      const updateTaskProgress = (force: boolean = false) => {
        const now = Date.now()
        const bucket = Math.floor(completed / progressStep)
        const crossedBucket = bucket !== lastProgressBucket
        const intervalReached = now - lastProgressUpdateAt >= TASK_PROGRESS_UPDATE_MIN_INTERVAL_MS
        if (!force && !crossedBucket && !intervalReached) return
        updateBackgroundTask(taskId, {
          detail: `正在解密图片（${completed}/${imageItems.length}）`,
          progressText: `${completed} / ${imageItems.length}`
        })
        lastProgressBucket = bucket
        lastProgressUpdateAt = now
      }
      const hardlinkMd5Set = new Set<string>()
      for (const item of imageItems) {
        if (!hasImageLocator(item)) continue
        const imageMd5 = normalizeMediaToken(item.imageMd5)
        if (imageMd5) {
          hardlinkMd5Set.add(imageMd5)
          continue
        }
        const imageDatName = getSafeImageDatName(item)
        if (/^[a-f0-9]{32}$/i.test(imageDatName)) {
          hardlinkMd5Set.add(imageDatName)
        }
      }
      if (hardlinkMd5Set.size > 0) {
        try {
          await window.electronAPI.image.preloadHardlinkMd5s(Array.from(hardlinkMd5Set))
        } catch {
          // ignore preload failures and continue decrypt
        }
      }

      const concurrency = Math.max(1, Math.min(BATCH_IMAGE_DECRYPT_CONCURRENCY, imageItems.length))
      let cursor = 0
      const worker = async () => {
        while (true) {
          const index = cursor
          cursor += 1
          if (index >= imageItems.length) return
          const item = imageItems[index]
          try {
            if (!hasImageLocator(item)) {
              notFound += 1
              continue
            }
            const result = await window.electronAPI.image.decrypt({
              sessionId: item.sessionId,
              imageMd5: normalizeMediaToken(item.imageMd5) || undefined,
              imageDatName: getSafeImageDatName(item) || undefined,
              createTime: Number(item.createTime || 0) || undefined,
              force: true,
              preferFilePath: true,
              hardlinkOnly: true,
              allowCacheIndex: true,
              suppressEvents: true
            })
            if (!result?.success) {
              if (result?.failureKind === 'decrypt_failed') decryptFailed += 1
              else notFound += 1
            } else {
              success += 1
              if (result.localPath) {
                const key = getItemKey(item)
                previewPatch[key] = result.localPath
                updatePatch[key] = isLikelyThumbnailPreview(result.localPath)
              }
            }
          } catch {
            notFound += 1
          } finally {
            completed += 1
            updateTaskProgress()
          }
        }
      }
      await Promise.all(Array.from({ length: concurrency }, () => worker()))
      updateTaskProgress(true)

      if (Object.keys(previewPatch).length > 0) {
        setPreviewPathMap((prev) => ({ ...prev, ...previewPatch }))
      }
      if (Object.keys(updatePatch).length > 0) {
        setPreviewUpdateMap((prev) => ({ ...prev, ...updatePatch }))
      }
      setActionMessage(`批量解密完成：成功 ${success}，未找到 ${notFound}，解密失败 ${decryptFailed}`)
      showAlert(`批量解密完成：成功 ${success}，未找到 ${notFound}，解密失败 ${decryptFailed}`, '批量解密完成')
      finishBackgroundTask(taskId, decryptFailed > 0 ? 'failed' : 'completed', {
        detail: `资源页图片批量解密完成：成功 ${success}，未找到 ${notFound}，解密失败 ${decryptFailed}`,
        progressText: `成功 ${success} / 未找到 ${notFound} / 解密失败 ${decryptFailed}`
      })
    } catch (e) {
      finishBackgroundTask(taskId, 'failed', {
        detail: `资源页图片批量解密失败：${String(e)}`
      })
      showAlert(`批量解密失败：${String(e)}`, '批量解密失败')
    } finally {
      setBatchBusy(false)
    }
  }, [batchBusy, isLikelyThumbnailPreview, selectedItems, showAlert])

  const openVideo = useCallback(async (item: MediaStreamItem) => {
    if (item.mediaType !== 'video') return

    const md5 = await resolveItemVideoMd5(item)
    if (!md5) {
      showAlert('未解析到视频资源标识', '无法播放')
      return
    }

    const info = await window.electronAPI.video.getVideoInfo(md5, { includePoster: false })
    if (!info.success || !info.exists || !info.videoUrl) {
      showAlert(info.error || '未找到视频文件', '无法播放')
      return
    }

    await window.electronAPI.window.openVideoPlayerWindow(info.videoUrl)
  }, [resolveItemVideoMd5, showAlert])

  return (
    <div className="resources-page stream-rebuild">
      <header className="stream-toolbar">
        <div className="toolbar-left">
          <div className="media-tabs">
            <button type="button" className={tab === 'image' ? 'active' : ''} onClick={() => setTab('image')}>图片</button>
            <button type="button" className={tab === 'video' ? 'active' : ''} onClick={() => setTab('video')}>视频</button>
          </div>
          <div className="filters">
            <label className="filter-field filter-select">
              <UserRound size={14} />
              <select
                className="contact-select"
                value={selectedContact}
                onChange={(event) => setSelectedContact(event.target.value)}
              >
                {contacts.map((contact) => (
                  <option key={contact.id} value={contact.id}>{contact.name}</option>
                ))}
              </select>
            </label>
            <label className="filter-field filter-date">
              <Calendar size={14} />
              <input
                className="date-input"
                type="date"
                value={dateStart}
                onChange={(event) => setDateStart(event.target.value)}
              />
            </label>
            <span className="sep">至</span>
            <label className="filter-field filter-date">
              <Calendar size={14} />
              <input
                className="date-input"
                type="date"
                value={dateEnd}
                onChange={(event) => setDateEnd(event.target.value)}
              />
            </label>
            <button type="button" className="ghost reset-btn" onClick={() => { setDateStart(''); setDateEnd('') }}>重置时间</button>
          </div>
        </div>
        <div className="toolbar-right">
          <button type="button" onClick={() => void loadStream(true)} disabled={loading || loadingMore}>
            {loading ? <Loader2 size={14} className="spin" /> : <RefreshCw size={14} />}
            刷新
          </button>
          {tab === 'image' && (
            <button type="button" onClick={() => void batchDecryptImage()} disabled={selectedKeys.size === 0 || batchBusy}>
              批量解密
            </button>
          )}
          <button type="button" className="danger" onClick={() => void batchDelete()} disabled={selectedKeys.size === 0 || batchBusy}>
            批量删除
          </button>
        </div>
      </header>

      <div className="stream-summary">
        <span>已加载 {items.length} 条</span>
        <span>已选 {selectedKeys.size} 条</span>
        <span>{tab === 'image' ? '图片按时间倒序流式展示' : '视频按时间倒序流式展示'}</span>
        {actionMessage && <span className="action-message">{actionMessage}</span>}
      </div>

      {error && (
        <div className="stream-state error">{error}</div>
      )}

      {!error && items.length === 0 && (loading || loadingMore) && (
        <div className="stream-state"><Loader2 size={18} className="spin" /> 正在加载...</div>
      )}

      {!error && items.length === 0 && !loading && !loadingMore && (
        <div className="stream-state">当前筛选条件下没有内容</div>
      )}

      {!error && items.length > 0 && (
        <div className="stream-grid-wrap">
          <VirtuosoGrid
            className="stream-grid"
            overscan={48}
            components={{
              List: GridList,
              Item: GridItem
            }}
            data={displayItems}
            computeItemKey={(_, item) => getItemKey(item)}
            rangeChanged={(range) => {
              scheduleRangeResolve(range.startIndex - 3, range.endIndex + 6)
            }}
            endReached={() => {
              if (!hasMore || loading || loadingMore) return
              void loadStream(false)
            }}
            itemContent={(_, item) => {
              const itemKey = getItemKey(item)
              const aspect = imageAspectMap[itemKey] || 0
              return (
                <MediaCard
                  item={item}
                  sessionName={item.sessionDisplayName || item.sessionId}
                  previewPath={previewPathMap[itemKey] || ''}
                  videoPosterPath={videoPosterMap[itemKey] || ''}
                  imageIsLong={aspect >= 2.8}
                  hasPreviewUpdate={Boolean(previewUpdateMap[itemKey])}
                  selected={selectedKeys.has(itemKey)}
                  decrypting={decryptingKeys.has(itemKey)}
                  onToggleSelect={toggleSelect}
                  onDelete={deleteOne}
                  onShowInfo={showMediaInfo}
                  onImagePreviewAction={onImagePreviewAction}
                  onUpdateImageQuality={updateImageQuality}
                  onOpenVideo={openVideo}
                  onImageLoaded={onImageLoaded}
                />
              )
            }}
          />
          {loadingMore && <div className="grid-loading-more"><Loader2 size={16} className="spin" /> 加载更多中...</div>}
          {!hasMore && <div className="grid-end">已加载到底</div>}
        </div>
      )}

      {dialog && (
        <div className="resource-dialog-mask">
          <div className="resource-dialog" role="dialog" aria-modal="true" aria-label={dialog.title}>
            <header className="dialog-header">{dialog.title}</header>
            <div className="dialog-body">
              {dialog.mode === 'info' ? (
                <div className="dialog-info-list">
                  {(dialog.infoRows || []).map((row, idx) => (
                    <div className="dialog-info-row" key={`${row.label}-${idx}`}>
                      <span className="info-label">{row.label}</span>
                      <span className="info-value" title={row.value}>{row.value}</span>
                    </div>
                  ))}
                </div>
              ) : (
                dialog.message
              )}
            </div>
            <footer className="dialog-actions">
              {dialog.mode === 'confirm' && (
                <button type="button" className="dialog-btn ghost" onClick={closeDialog}>
                  {dialog.cancelText || '取消'}
                </button>
              )}
              <button
                type="button"
                className="dialog-btn solid"
                onClick={() => {
                  const callback = dialog.onConfirm
                  closeDialog()
                  callback?.()
                }}
              >
                {dialog.confirmText || '确定'}
              </button>
            </footer>
          </div>
        </div>
      )}
    </div>
  )
}

export default ResourcesPage
