// frontend\src\components\ui\FinishedDownloadsGalleryCard.tsx

'use client'

import { useState, useMemo, useCallback, useRef, memo, type ReactNode, type SyntheticEvent } from 'react'
import type {
  RecordJob,
  GalleryModelFlags,
  FinishedPostworkSummary,
} from '../../types'
import FinishedVideoPreview from './FinishedVideoPreview'
import RecordJobActions from './RecordJobActions'
import TagOverflowRow from './TagOverflowRow'
import { isHotName, stripHotPrefix } from './hotName'
import PreviewScrubber from './PreviewScrubber'
import Checkbox from './Checkbox'
import type { FinishedDownloadsTeaserState } from './teaserPlayback'
import {
  shouldAnimateTeaser,
  shouldEnableTeaserAudio,
} from './teaserPlayback'
import { SpeakerWaveIcon } from '@heroicons/react/24/solid'
import {
  buildSpriteFrameStyle,
  parseJobMeta,
  readPreviewSpriteInfo,
  scrubProgressRatioFromIndex,
} from './previewSprite'
import ModelGenderIcon from './ModelGenderIcon'
import { resolveFinishedModelFlags } from './finishedModelFlags'
import CountryFlag, { resolveFinishedCountry } from './CountryFlag'
import SpritePreloader from './SpritePreloader'
import PreviewRatingOverlay from './PreviewRatingOverlay'
import PreviewMetaOverlay from './PreviewMetaOverlay'

function normalizeDurationSeconds(value: unknown): number | undefined {
  if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) return undefined
  return value > 24 * 60 * 60 ? value / 1000 : value
}

type Props = {
  job: RecordJob
  selectedKeys: Set<string>
  selectionModeActive: boolean
  rowKey: string
  isPreviewActive: boolean
  forcePreviewMuted?: boolean
  blurPreviews?: boolean

  postworkByFile: Record<string, FinishedPostworkSummary>
  getPostworkSummaryForOutput: (
    output: string | undefined,
    summaries: Record<string, FinishedPostworkSummary>
  ) => FinishedPostworkSummary | undefined
  renderPostworkBadge?: (badge?: FinishedPostworkSummary) => ReactNode

  durationSeconds?: number
  teaserState: FinishedDownloadsTeaserState

  baseName: (p: string) => string
  modelNameFromOutput: (output?: string) => string
  runtimeOf: (job: RecordJob) => string
  sizeBytesOf: (job: RecordJob) => number | null
  formatBytes: (bytes?: number | null) => string
  lower: (s: string) => string

  modelsByKey: Record<string, GalleryModelFlags>
  activeTagSet: Set<string>

  deletingKeys: Set<string>
  keepingKeys: Set<string>
  removingKeys: Set<string>

  registerTeaserHost: (key: string) => (el: HTMLDivElement | null) => void

  onToggleSelected: (job: RecordJob) => void
  onSelectOnly: (job: RecordJob) => void
  onHoverPreviewKeyChange?: (key: string | null) => void
  onToggleTagFilter: (tag: string) => void
  onOpenPlayer: (job: RecordJob) => void
  handleDuration: (job: RecordJob, seconds: number) => void
  handleResolution: (job: RecordJob, w: number, h: number) => void
  resolutionLabelOf: (job: RecordJob) => string
  handleScrubberClickIndex: (job: RecordJob, segmentIndex: number, segmentCount: number) => void
  deleteVideo: (job: RecordJob) => Promise<boolean>
  keepVideo: (job: RecordJob) => Promise<boolean>
  onToggleHot: (job: RecordJob) => void | Promise<void>
  onToggleFavorite?: (job: RecordJob) => void | Promise<void>
  onToggleLike?: (job: RecordJob) => void | Promise<void>
  onToggleWatch?: (job: RecordJob) => void | Promise<void>
  onSplit?: (job: RecordJob) => void | Promise<void>
  onAddToDownloads?: (job: RecordJob) => void | Promise<boolean>
  jobForDetails: (job: RecordJob) => RecordJob
  renderIntegrityBadge?: (job: RecordJob) => ReactNode
  renderRatingOverlay?: (job: RecordJob) => ReactNode

  contentPaddingClass: string
  footerMinHeightClass: string
  titleClass: string
  sublineClass: string
  assetNonce?: number

  mobileTeaserAudioUnlocked?: boolean
  onUnlockMobileTeaserAudio?: () => void
}

function FinishedDownloadsGalleryCardInner({
  job: j,
  selectedKeys,
  selectionModeActive,
  rowKey,
  isPreviewActive,
  forcePreviewMuted,
  blurPreviews,
  postworkByFile,
  getPostworkSummaryForOutput,
  renderPostworkBadge,
  durationSeconds,
  teaserState,
  baseName,
  modelNameFromOutput,
  runtimeOf,
  sizeBytesOf,
  formatBytes,
  lower,
  modelsByKey,
  activeTagSet,
  deletingKeys,
  keepingKeys,
  removingKeys,
  registerTeaserHost,
  onToggleSelected,
  onHoverPreviewKeyChange,
  onToggleTagFilter,
  onOpenPlayer,
  handleDuration,
  handleResolution,
  resolutionLabelOf,
  handleScrubberClickIndex,
  deleteVideo,
  keepVideo,
  onToggleHot,
  onToggleFavorite,
  onToggleLike,
  onToggleWatch,
  onSplit,
  onAddToDownloads,
  jobForDetails,
  renderIntegrityBadge,
  renderRatingOverlay,
  mobileTeaserAudioUnlocked,
  onUnlockMobileTeaserAudio,
  contentPaddingClass,
  footerMinHeightClass,
  titleClass,
  sublineClass,
  assetNonce
}: Props) {
  const k = rowKey
  const checked = selectedKeys.has(rowKey)
  const longPressTimerRef = useRef<number | null>(null)
  const longPressTriggeredRef = useRef(false)

  const [scrubIndexByKey, setScrubIndexByKey] = useState<Record<string, number | undefined>>({})
  const [hoveredThumbKey, setHoveredThumbKey] = useState<string | null>(null)

  const teaserPlayFnsRef = useRef<Record<string, (() => void) | null>>({})

  const [audioButtonDismissed, setAudioButtonDismissed] = useState(false)
  const [teaserAudioEnabled, setTeaserAudioEnabled] = useState(false)

  const registerTeaserPlayForKey = useCallback(
    (key: string) => (fn: (() => void) | null) => {
      if (fn) teaserPlayFnsRef.current[key] = fn
      else delete teaserPlayFnsRef.current[key]
    },
    []
  )

  const handlePrimaryClick = useCallback(
    (e?: SyntheticEvent) => {
      e?.preventDefault()
      e?.stopPropagation()

      if (longPressTriggeredRef.current) {
        longPressTriggeredRef.current = false
        return
      }

      if (selectionModeActive) {
        onToggleSelected(j)
        return
      }

      onOpenPlayer(j)
    },
    [selectionModeActive, onToggleSelected, onOpenPlayer, j]
  )

  const clearLongPressTimer = useCallback(() => {
    if (longPressTimerRef.current !== null) {
      window.clearTimeout(longPressTimerRef.current)
      longPressTimerRef.current = null
    }
  }, [])

  const startLongPressSelect = useCallback(() => {
    clearLongPressTimer()

    if (selectionModeActive) return

    longPressTimerRef.current = window.setTimeout(() => {
      longPressTimerRef.current = null
      longPressTriggeredRef.current = true
      onToggleSelected(j)
    }, 450)
  }, [clearLongPressTimer, selectionModeActive, onToggleSelected, j])

  const setScrubIndexForKey = useCallback((key: string, index: number | undefined) => {
    setScrubIndexByKey((prev) => {
      if (index === undefined) {
        if (!(key in prev)) return prev
        const next = { ...prev }
        delete next[key]
        return next
      }
      if (prev[key] === index) return prev
      return { ...prev, [key]: index }
    })
  }, [])

  const clearScrubIndex = useCallback((key: string) => {
    setScrubIndexForKey(key, undefined)
  }, [setScrubIndexForKey])

  const allowSound = shouldEnableTeaserAudio({
    state: teaserState,
    itemKey: k,
  })
  const previewMuted = forcePreviewMuted
    ? true
    : !(allowSound || mobileTeaserAudioUnlocked || teaserAudioEnabled)

  const showMobileAudioButton =
    Boolean(onUnlockMobileTeaserAudio) &&
    !mobileTeaserAudioUnlocked &&
    !teaserAudioEnabled &&
    !audioButtonDismissed

  const model = modelNameFromOutput(j.output)

  const { flags } = resolveFinishedModelFlags(
    j,
    modelsByKey,
    modelNameFromOutput,
    lower
  )

  const tags = useMemo(() => {
    const s = String(flags?.tags ?? '').trim()
    if (!s) return [] as string[]
    const parts = s.split(/[\n,;|]+/g).map((p) => p.trim()).filter(Boolean)
    const seen = new Set<string>()
    const out: string[] = []
    for (const p of parts) {
      const kk = p.toLowerCase()
      if (seen.has(kk)) continue
      seen.add(kk)
      out.push(p)
    }
    return out
  }, [flags?.tags])

  const isFav = Boolean(flags?.favorite)
  const isLiked = flags?.liked === true
  const isWatching = Boolean(flags?.watching)

  const country = resolveFinishedCountry(flags, j)

  const fileRaw = baseName(j.output || '')
  const isHot = isHotName(fileRaw)
  const file = stripHotPrefix(fileRaw)

  const postworkBadge = getPostworkSummaryForOutput(j.output, postworkByFile)
  const showPostworkBadge = !!postworkBadge

  const dur = runtimeOf(j)
  const size = formatBytes(sizeBytesOf(j))

  const meta = useMemo(() => parseJobMeta((j as any)?.meta), [(j as any)?.meta])

  const resObj = useMemo(() => {
    const w = meta?.media?.video?.width ?? meta?.file?.video?.width
    const h = meta?.media?.video?.height ?? meta?.file?.video?.height
    if (
      typeof w === 'number' && Number.isFinite(w) && w > 0 &&
      typeof h === 'number' && Number.isFinite(h) && h > 0
    ) {
      return { w, h }
    }
    return null
  }, [meta])

  const resLabelRaw = resolutionLabelOf(j)
  const resLabel = resLabelRaw && resLabelRaw !== '—' ? resLabelRaw : ''

  const isRemoving = removingKeys.has(k)
  const isDeleting = deletingKeys.has(k)
  const isKeeping = keepingKeys.has(k)
  const busy = isDeleting || isKeeping || isRemoving

  const sprite = useMemo(
    () =>
      readPreviewSpriteInfo((j as any)?.meta, {
        fallbackPath: flags?.previewScrubberPath,
        fallbackCount: flags?.previewScrubberCount,
        versionFallback: assetNonce ?? 0,
      }),
    [(j as any)?.meta, flags?.previewScrubberPath, flags?.previewScrubberCount, assetNonce]
  )

  const spriteUrl = sprite.url
  const hasScrubberUi = sprite.hasScrubberUi
  const hasSpriteScrubber = sprite.hasSpriteScrubber
  const scrubberCount = sprite.count
  const scrubberStepSeconds = sprite.stepSeconds
  const hasScrubber = hasScrubberUi
  const shouldPreloadSprite = Boolean(hasSpriteScrubber && spriteUrl)

  const activeScrubIndex = scrubIndexByKey[k]
  const scrubProgressRatio = scrubProgressRatioFromIndex(activeScrubIndex, scrubberCount)

  const spriteFrameStyle = useMemo(
    () =>
      buildSpriteFrameStyle({
        spriteUrl,
        spriteCols: sprite.cols,
        spriteRows: sprite.rows,
        spriteCount: sprite.count,
        activeIndex: activeScrubIndex,
      }),
    [spriteUrl, sprite.cols, sprite.rows, sprite.count, activeScrubIndex]
  )

  const showScrubberSpriteInThumb = Boolean(spriteFrameStyle)

  const previewDurationSeconds =
    durationSeconds ??
    normalizeDurationSeconds(meta?.media?.durationSeconds) ??
    normalizeDurationSeconds(meta?.file?.durationSeconds)

  const spriteScrubbingActive =
    hasSpriteScrubber && typeof activeScrubIndex === 'number'

  return (
    <div className="relative">
      <div
        role="button"
        tabIndex={0}
        data-finished-hover-key={k}
        className="group relative focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 dark:focus-visible:outline-indigo-500"
        onClick={(e) => {
          handlePrimaryClick(e)
        }}
        onPointerDown={(e) => {
          if (e.pointerType === 'mouse') return
          startLongPressSelect()
        }}
        onPointerUp={clearLongPressTimer}
        onPointerCancel={clearLongPressTimer}
        onPointerLeave={clearLongPressTimer}
        onContextMenu={(e) => {
          if (longPressTriggeredRef.current) {
            e.preventDefault()
          }
        }}
        onMouseEnter={() => onHoverPreviewKeyChange?.(k)}
        onMouseLeave={() => {
          clearLongPressTimer()
          onHoverPreviewKeyChange?.(null)
          setHoveredThumbKey((prev) => (prev === k ? null : prev))
          clearScrubIndex(k)
        }}
        onKeyDown={(e) => {
          if (e.key !== 'Enter' && e.key !== ' ') return
          handlePrimaryClick(e)
        }}
      >
        <div
          className={[
            'relative rounded-lg overflow-visible outline-1 outline-black/5 dark:-outline-offset-1 dark:outline-white/10',
            'bg-white dark:bg-gray-900/40',
            'transition-[transform,opacity,box-shadow,background-color] duration-180 ease-out',
            'will-change-[transform,opacity]',
            !busy && 'hover:-translate-y-0.5 hover:shadow-md dark:hover:shadow-none',
            checked && 'ring-2 ring-indigo-500 bg-indigo-50/60 dark:bg-indigo-500/10 dark:ring-indigo-400',
            busy && !isRemoving && 'pointer-events-none opacity-70',
            isDeleting && 'ring-1 ring-red-300 bg-red-50/60 dark:bg-red-500/10 dark:ring-red-500/30',
            isKeeping && 'ring-1 ring-emerald-300 bg-emerald-50/60 dark:bg-emerald-500/10 dark:ring-emerald-500/30',
            isRemoving ? 'pointer-events-none opacity-0 translate-y-2 scale-[0.98]' : 'opacity-100 translate-y-0 scale-100',
          ].filter(Boolean).join(' ')}
        >
          <div
            className={[
              'absolute left-3 top-3 z-40 hidden transition-opacity duration-150 lg:block',
              checked ? 'opacity-100' : 'opacity-0 group-hover:opacity-100 group-focus-within:opacity-100',
            ].join(' ')}
            onClick={(e) => e.stopPropagation()}
            onMouseDown={(e) => e.stopPropagation()}
            onPointerDown={(e) => e.stopPropagation()}
          >
            <Checkbox
              checked={checked}
              ariaLabel={`${checked ? 'Abwählen' : 'Auswählen'}: ${baseName(j.output || '') || 'Download'}`}
              onChange={() => onToggleSelected(j)}
            />
          </div>
          <div
            className="group/thumb relative aspect-video rounded-t-lg bg-black/5 dark:bg-white/5"
            ref={registerTeaserHost(k)}
            onMouseEnter={() => setHoveredThumbKey(k)}
            onMouseLeave={() => {
              setHoveredThumbKey((prev) => (prev === k ? null : prev))
              clearScrubIndex(k)
            }}
          >
            <div className="absolute inset-0 overflow-hidden rounded-t-lg">
              <div className="absolute inset-0">
                <FinishedVideoPreview
                  job={j}
                  getFileName={(p) => stripHotPrefix(baseName(p))}
                  durationSeconds={previewDurationSeconds}
                  onDuration={handleDuration}
                  onResolution={handleResolution}
                  variant="fill"
                  showPopover={false}
                  blur={blurPreviews}
                  animated={shouldAnimateTeaser({
                    state: teaserState,
                    itemKey: k,
                    disabled: spriteScrubbingActive,
                  })}
                  animatedMode="teaser"
                  animatedTrigger="always"
                  muted={previewMuted}
                  popoverMuted={previewMuted}
                  assetNonce={assetNonce ?? 0}
                  scrubProgressRatio={hasSpriteScrubber ? undefined : scrubProgressRatio}
                  preferScrubProgress={!hasSpriteScrubber && typeof activeScrubIndex === 'number'}
                  forceActive={isPreviewActive}
                  teaserPreloadEnabled
                  registerTeaserPlay={registerTeaserPlayForKey(k)}
                />
              </div>

              {/* Desktop/Tablet: wie bisher */}
              <div className="hidden sm:block">
                <PreviewRatingOverlay>
                  {renderRatingOverlay ? renderRatingOverlay(j) : null}
                </PreviewRatingOverlay>
              </div>

              {/* Mobile: Stern unten links */}
              {renderRatingOverlay ? (
                <div
                  className="pointer-events-auto absolute bottom-2 left-2 z-[45] sm:hidden"
                  onClick={(e) => {
                    e.preventDefault()
                    e.stopPropagation()
                  }}
                  onPointerDown={(e) => {
                    e.stopPropagation()
                  }}
                  onTouchStart={(e) => {
                    e.stopPropagation()
                  }}
                >
                  {renderRatingOverlay(j)}
                </div>
              ) : null}

              {showMobileAudioButton ? (
                <button
                  type="button"
                  className="
                    absolute top-2 right-2 z-[35] inline-flex items-center gap-1.5 rounded-full
                    px-2.5 py-1.5 text-[11px] font-medium backdrop-blur transition sm:hidden
                    bg-white/95 text-gray-900 ring-1 ring-gray-200 shadow-sm hover:bg-white
                    dark:bg-black/70 dark:text-white dark:ring-white/10 dark:hover:bg-black/80
                    focus:outline-none focus:ring-2 focus:ring-indigo-500
                  "
                  onClick={(e) => {
                    e.preventDefault()
                    e.stopPropagation()

                    setAudioButtonDismissed(true)
                    setTeaserAudioEnabled(true)
                    onUnlockMobileTeaserAudio?.()

                    requestAnimationFrame(() => {
                      teaserPlayFnsRef.current[k]?.()
                    })
                  }}
                  onMouseDown={(e) => e.stopPropagation()}
                  onTouchStart={(e) => e.stopPropagation()}
                  aria-label="Teaser mit Ton abspielen"
                  title="Ton aktivieren"
                >
                  <SpeakerWaveIcon className="size-3.5 shrink-0" />
                  <span>Ton aktivieren</span>
                </button>
              ) : null}

              <PreviewMetaOverlay
                dur={dur}
                resObj={resObj}
                resLabel={resLabel}
                size={size}
              />

              <SpritePreloader
                src={spriteUrl}
                enabled={shouldPreloadSprite}
              />

              {showScrubberSpriteInThumb && spriteFrameStyle ? (
                <div className="absolute inset-x-0 top-0 bottom-[6px] z-[5]" aria-hidden="true">
                  <div className="h-full w-full" style={spriteFrameStyle} />
                </div>
              ) : null}

              {hasScrubber && hoveredThumbKey === k ? (
                <div
                  className="absolute inset-x-0 bottom-0 z-30 pointer-events-none opacity-100 transition-opacity duration-150"
                  onClick={(e) => e.stopPropagation()}
                  onMouseDown={(e) => e.stopPropagation()}
                >
                  <PreviewScrubber
                    className="pointer-events-auto px-1"
                    imageCount={scrubberCount}
                    activeIndex={activeScrubIndex}
                    onActiveIndexChange={(idx) => setScrubIndexForKey(k, idx)}
                    onIndexClick={(index) => {
                      setScrubIndexForKey(k, index)
                      handleScrubberClickIndex(j, index, scrubberCount)
                    }}
                    stepSeconds={scrubberStepSeconds}
                  />
                </div>
              ) : null}
            </div>
          </div>

          <div
            className={[
              'relative flex flex-col rounded-b-lg border-t transition-colors',
              checked
                ? [
                    'border-indigo-200 bg-indigo-50/90',
                    'dark:border-indigo-400/30 dark:bg-indigo-500/15',
                  ].join(' ')
                : [
                    'border-black/5 bg-white',
                    'dark:border-white/10 dark:bg-gray-900',
                  ].join(' '),
              footerMinHeightClass,
              contentPaddingClass,
            ].join(' ')}
            onClick={(e) => {
              handlePrimaryClick(e)
            }}
          >
            <div className="min-w-0">
              <div className="mt-0.5 flex min-h-6 items-center justify-between gap-2 min-w-0">
                <div className="min-w-0 flex items-center gap-1.5">
                  <div className={['min-w-0 truncate font-semibold text-gray-900 dark:text-white', titleClass].join(' ')}>
                    {model}
                  </div>

                  <ModelGenderIcon
                    gender={flags?.gender}
                    className="shrink-0"
                  />

                  <CountryFlag
                    country={country}
                    size="sm"
                    className="shrink-0"
                  />
                </div>

                {(renderIntegrityBadge || (showPostworkBadge && renderPostworkBadge)) ? (
                  <div className="relative z-20 shrink-0 flex h-6 items-center gap-2">
                    {renderIntegrityBadge ? renderIntegrityBadge(j) : null}
                    {showPostworkBadge && renderPostworkBadge ? renderPostworkBadge(postworkBadge) : null}
                  </div>
                ) : null}
              </div>

              <div className="mt-0.5 flex min-h-[18px] items-start gap-2 min-w-0">
                {isHot ? (
                  <span className="shrink-0 self-start rounded bg-amber-500/15 px-1.5 py-0.5 text-[11px] leading-none font-semibold text-amber-800 dark:text-amber-300">
                    HOT
                  </span>
                ) : null}

                <span
                  className={['min-w-0 truncate text-gray-500 dark:text-gray-400', sublineClass].join(' ')}
                  title={stripHotPrefix(file) || '—'}
                >
                  {stripHotPrefix(file) || '—'}
                </span>
              </div>
            </div>

            <div
              className="mt-2 shrink-0"
              onClick={(e) => e.stopPropagation()}
              onMouseDown={(e) => e.stopPropagation()}
            >
              <div className="w-full">
                <div className="w-full rounded-md bg-gray-50/70 p-1 ring-1 ring-black/5 dark:bg-white/5 dark:ring-white/10">
                  <RecordJobActions
                    job={jobForDetails(j)}
                    variant="table"
                    busy={busy}
                    collapseToMenu
                    compact={true}
                    isHot={isHot}
                    isFavorite={isFav}
                    isLiked={isLiked}
                    isWatching={isWatching}
                    onToggleWatch={onToggleWatch}
                    onToggleFavorite={onToggleFavorite}
                    onToggleLike={onToggleLike}
                    onToggleHot={onToggleHot}
                    onKeep={keepVideo}
                    onDelete={deleteVideo}
                    onSplit={onSplit}
                    onAddToDownloads={onAddToDownloads}
                    order={['watch', 'favorite', 'like', 'hot', 'keep', 'delete', 'split', 'details', 'add', 'training']}
                    className="w-full gap-1.5"
                  />
                </div>
              </div>
            </div>

            <div className="mt-2" onClick={(e) => e.stopPropagation()} onMouseDown={(e) => e.stopPropagation()}>
              <TagOverflowRow
                rowKey={k}
                tags={tags}
                activeTagSet={activeTagSet}
                lower={lower}
                onToggleTagFilter={onToggleTagFilter}
              />
            </div>
          </div>
        </div>
      </div>
    </div>
  )
}

const FinishedDownloadsGalleryCard = memo(FinishedDownloadsGalleryCardInner)
export default FinishedDownloadsGalleryCard