import { useEffect, useMemo, useRef, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { motion, AnimatePresence } from 'framer-motion'
import {
  Play,
  Shuffle,
  Heart,
  Disc3,
} from '../../lib/icons'
import {
  usePlaylistItems,
  usePlaylistMove,
  usePlaylistRemove,
  useBulkMarkPlayed,
  useBulkToggleFavorite,
} from '../../hooks/use-jellyfin'
import { getBestImage } from '../../api/jellyfin'
import { formatRuntime } from '../../lib/format'
import type { BaseItemDto } from '../../api/types'
import { useQueueStore } from '../../stores/queue-store'
import PlaylistRow from './PlaylistRow'
import { DragGhost, SelectionToolbar, PosterMosaic } from './PlaylistChrome'

/**
 * PlaylistItemId is the per-row identifier the Jellyfin Playlists endpoint
 * returns so the server knows *which* instance of a duplicated track to
 * move or remove. The SDK's BaseItemDto doesn't include this field, but
 * the playlist-items response always has it. Extracted here so the cast
 * lives in one place.
 */
function playlistItemId(item: BaseItemDto | undefined): string | undefined {
  return (item as (BaseItemDto & { PlaylistItemId?: string }) | undefined)?.PlaylistItemId
}

/**
 * Editable playlist view. The list supports:
 *   - Plain click on row body  -> /item/{id}  (detail)
 *   - Click on thumbnail       -> /play/{id}
 *   - Ctrl/Cmd-click           -> toggle selection (no nav)
 *   - Shift-click              -> range-select from the last anchor
 *   - Drag a row               -> reorder
 *   - Drag a row in selection  -> reorder the whole selection as a unit
 *   - Selection toolbar        -> remove, mark played/unplayed, favorite
 *
 * Reorders are optimistic: the local copy reflows instantly and Jellyfin's
 * moveItem call is fired in the background. On settle, the cached query
 * invalidates so the next read confirms server order.
 */

const PRIMARY_BTN =
  'inline-flex items-center gap-2 h-11 px-6 rounded-full text-[13px] font-semibold tracking-tight transition-[transform,background] duration-200 ease-out focus-ring active:scale-[0.97]'
const SECONDARY_BTN =
  'inline-flex items-center gap-2 h-11 px-5 rounded-full bg-white/8 hover:bg-white/14 backdrop-blur text-white text-[13px] font-medium tracking-tight border border-white/14 hover:border-white/25 transition-all duration-200 ease-out focus-ring active:scale-[0.97]'
const ICON_BTN =
  'w-11 h-11 rounded-full bg-white/8 hover:bg-white/14 backdrop-blur border border-white/14 hover:border-white/25 transition-all duration-200 ease-out focus-ring active:scale-[0.97] grid place-items-center'

interface Props {
  item: BaseItemDto
  serverUrl: string
}

export default function PlaylistView({ item, serverUrl }: Props) {
  const playlistId = item.Id || ''
  const { data: rawItems = [], isLoading } = usePlaylistItems(playlistId || undefined)
  const navigate = useNavigate()
  const moveMut = usePlaylistMove(playlistId || undefined)
  const removeMut = usePlaylistRemove(playlistId || undefined)
  const markMut = useBulkMarkPlayed()
  const favMut = useBulkToggleFavorite()

  // Local optimistic copy. Stays in sync with the server until the user
  // does something edit-y, then the local copy leads.
  const [localItems, setLocalItems] = useState<BaseItemDto[]>([])
  useEffect(() => {
    setLocalItems(rawItems as BaseItemDto[])
  }, [rawItems])
  const items = localItems

  /* â”€â”€ Selection â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€ */
  const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set())
  const [anchorIndex, setAnchorIndex] = useState<number | null>(null)

  // Map of PlaylistItemId -> index for quick lookups
  const indexByEntry = useMemo(() => {
    const m = new Map<string, number>()
    items.forEach((it, i) => {
      const eid = playlistItemId(it)
      if (eid) m.set(eid, i)
    })
    return m
  }, [items])

  // Drop selection if the underlying list lost some entries (e.g. delete)
  useEffect(() => {
    setSelectedIds(prev => {
      const next = new Set<string>()
      for (const eid of prev) if (indexByEntry.has(eid)) next.add(eid)
      return next.size === prev.size ? prev : next
    })
  }, [indexByEntry])

  // Esc clears selection
  useEffect(() => {
    function onKey(e: KeyboardEvent) {
      if (e.key === 'Escape' && selectedIds.size > 0) {
        e.stopPropagation()
        setSelectedIds(new Set())
      }
    }
    window.addEventListener('keydown', onKey)
    return () => window.removeEventListener('keydown', onKey)
  }, [selectedIds.size])

  function toggleOne(entryId: string, index: number) {
    setSelectedIds(prev => {
      const next = new Set(prev)
      if (next.has(entryId)) next.delete(entryId)
      else next.add(entryId)
      return next
    })
    setAnchorIndex(index)
  }
  function selectRange(toIndex: number) {
    if (anchorIndex == null) {
      const eid = playlistItemId(items[toIndex])
      if (eid) {
        setSelectedIds(new Set([eid]))
        setAnchorIndex(toIndex)
      }
      return
    }
    const lo = Math.min(anchorIndex, toIndex)
    const hi = Math.max(anchorIndex, toIndex)
    const next = new Set<string>()
    for (let i = lo; i <= hi; i++) {
      const eid = playlistItemId(items[i])
      if (eid) next.add(eid)
    }
    setSelectedIds(next)
  }
  function clearSelection() {
    setSelectedIds(new Set())
    setAnchorIndex(null)
  }

  /* â”€â”€ Drag-and-drop â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€ */
  type DragState = {
    pointerId: number
    indices: number[] // indices being moved (1 or many for multi-drag)
    startY: number
    currentY: number
    dropIndex: number // 0..items.length, where the drop will land
  }
  const [drag, setDrag] = useState<DragState | null>(null)
  const listRef = useRef<HTMLOListElement | null>(null)
  const rowRefs = useRef<Map<number, HTMLLIElement>>(new Map())
  // Tracks small movements before commit so a click doesn't accidentally start a drag
  const pendingDragRef = useRef<{
    pointerId: number
    rowIndex: number
    startX: number
    startY: number
    startedDrag: boolean
  } | null>(null)
  const DRAG_THRESHOLD = 5

  function rowMidY(idx: number): number {
    const el = rowRefs.current.get(idx)
    if (!el) return 0
    const r = el.getBoundingClientRect()
    return r.top + r.height / 2
  }

  function computeDropIndex(pointerY: number, draggingIndices: number[]): number {
    // Skip the dragged rows themselves when computing drop position so the
    // pointer doesn't snap onto a row that's about to vanish.
    const draggingSet = new Set(draggingIndices)
    for (let i = 0; i < items.length; i++) {
      if (draggingSet.has(i)) continue
      if (pointerY < rowMidY(i)) return i
    }
    return items.length
  }

  function startMaybeDrag(rowIndex: number, e: React.PointerEvent) {
    // Only left-button mouse / single-touch, and not on inner buttons
    if (e.button !== 0) return
    pendingDragRef.current = {
      pointerId: e.pointerId,
      rowIndex,
      startX: e.clientX,
      startY: e.clientY,
      startedDrag: false,
    }
  }

  function handlePointerMove(e: React.PointerEvent) {
    const pending = pendingDragRef.current
    if (!pending || pending.pointerId !== e.pointerId) return
    if (!pending.startedDrag) {
      const dx = e.clientX - pending.startX
      const dy = e.clientY - pending.startY
      if (Math.abs(dx) < DRAG_THRESHOLD && Math.abs(dy) < DRAG_THRESHOLD) return
      pending.startedDrag = true
      // Decide what's being dragged: the row, or the whole selection
      const rowEntry = playlistItemId(items[pending.rowIndex])
      const draggingSet = new Set<number>()
      if (rowEntry && selectedIds.has(rowEntry)) {
        for (const eid of selectedIds) {
          const idx = indexByEntry.get(eid)
          if (idx != null) draggingSet.add(idx)
        }
      } else {
        draggingSet.add(pending.rowIndex)
      }
      const indices = Array.from(draggingSet).sort((a, b) => a - b)
      ;(e.target as Element).setPointerCapture?.(e.pointerId)
      setDrag({
        pointerId: e.pointerId,
        indices,
        startY: e.clientY,
        currentY: e.clientY,
        dropIndex: computeDropIndex(e.clientY, indices),
      })
      return
    }
    setDrag(prev => {
      if (!prev || prev.pointerId !== e.pointerId) return prev
      const dropIndex = computeDropIndex(e.clientY, prev.indices)
      return { ...prev, currentY: e.clientY, dropIndex }
    })
  }

  function handlePointerUp(e: React.PointerEvent) {
    const pending = pendingDragRef.current
    pendingDragRef.current = null
    if (drag && drag.pointerId === e.pointerId) {
      commitDrop(drag)
      setDrag(null)
      return
    }
    // No drag started -> treat as click
    if (pending && pending.pointerId === e.pointerId && !pending.startedDrag) {
      handleRowClick(pending.rowIndex, e)
    }
  }

  function commitDrop(d: DragState) {
    const moved = d.indices
    if (moved.length === 0) return
    const moving = moved.map(i => items[i]).filter(Boolean) as BaseItemDto[]
    const remaining = items.filter((_, i) => !moved.includes(i))
    // Translate the drop index from "original space" to "remaining-array space"
    // by subtracting the count of moved indices that come before the drop
    const beforeDrop = moved.filter(i => i < d.dropIndex).length
    const insertAt = Math.max(0, Math.min(remaining.length, d.dropIndex - beforeDrop))
    const next = [
      ...remaining.slice(0, insertAt),
      ...moving,
      ...remaining.slice(insertAt),
    ]
    setLocalItems(next)
    // Fire the API call sequentially so each move sees the server state from
    // the previous one. Final indices come from the new array.
    ;(async () => {
      for (let k = 0; k < moving.length; k++) {
        const targetIndex = insertAt + k
        const movingPlaylistItemId = playlistItemId(moving[k])
        if (!movingPlaylistItemId) continue
        try {
          await moveMut.mutateAsync({ playlistItemId: movingPlaylistItemId, newIndex: targetIndex })
        } catch {
          /* invalidate-on-settle will reconcile */
        }
      }
    })()
  }

  function handleRowClick(rowIndex: number, e: React.PointerEvent) {
    const it = items[rowIndex]
    if (!it) return
    const eid = playlistItemId(it)
    if (e.shiftKey) {
      e.preventDefault()
      selectRange(rowIndex)
      return
    }
    if (e.ctrlKey || e.metaKey) {
      e.preventDefault()
      if (eid) toggleOne(eid, rowIndex)
      return
    }
    // Plain click - clear selection and navigate
    clearSelection()
    if (it.Id) navigate(`/item/${it.Id}`)
  }

  /* â”€â”€ Bulk actions â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€ */
  function selectedItemIds(): string[] {
    return Array.from(selectedIds)
      .map(eid => items[indexByEntry.get(eid) ?? -1]?.Id)
      .filter(Boolean) as string[]
  }
  function bulkRemove() {
    const ids = Array.from(selectedIds)
    if (ids.length === 0) return
    setLocalItems(prev => prev.filter(it => !selectedIds.has(playlistItemId(it) ?? '')))
    clearSelection()
    removeMut.mutate(ids)
  }
  function bulkMark(played: boolean) {
    const ids = selectedItemIds()
    if (ids.length === 0) return
    markMut.mutate({ itemIds: ids, played })
  }
  function bulkFavorite() {
    const ids = selectedItemIds()
    if (ids.length === 0) return
    // If any in the selection isn't favorited, favorite them all.
    // Otherwise unfavorite them all.
    const items_ = ids
      .map(id => items.find(it => it.Id === id))
      .filter(Boolean) as BaseItemDto[]
    const anyMissing = items_.some(it => !it.UserData?.IsFavorite)
    favMut.mutate({ itemIds: ids, favorite: anyMissing })
  }

  /* â”€â”€ Hero data â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€ */
  const totalRuntimeTicks = useMemo(
    () => items.reduce((sum, it) => sum + Number(it.RunTimeTicks ?? 0), 0),
    [items],
  )
  const totalRuntime = totalRuntimeTicks > 0 ? formatRuntime(totalRuntimeTicks) : null
  const watchedCount = useMemo(
    () => items.filter(it => it.UserData?.Played).length,
    [items],
  )
  const playlistPoster = getBestImage(serverUrl, item, 'primary', 600)
  const playlistBackdrop =
    getBestImage(serverUrl, item, 'backdrop', 1920) ||
    (items[0] ? getBestImage(serverUrl, items[0], 'backdrop', 1920) : '') ||
    playlistPoster

  function playFirst() {
    const playable = items.filter(it => !!it.Id) as BaseItemDto[]
    if (playable.length === 0) return
    useQueueStore.getState().setQueue({
      items: playable,
      originalOrder: playable,
      index: 0,
      source: playlistId ? { type: 'playlist', id: playlistId } : null,
      shuffled: false,
    })
    if (playable[0].Id) navigate(`/play/${playable[0].Id}`)
  }
  function shufflePlay() {
    const playable = items.filter(it => !!it.Id) as BaseItemDto[]
    if (playable.length === 0) return
    // Fisher-Yates - the `.sort(() => Math.random() - 0.5)` trick produces
    // biased orderings because TimSort calls the comparator inconsistently.
    const shuffled = [...playable]
    for (let i = shuffled.length - 1; i > 0; i--) {
      const j = Math.floor(Math.random() * (i + 1))
      ;[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]]
    }
    useQueueStore.getState().setQueue({
      items: shuffled,
      originalOrder: playable,
      index: 0,
      source: playlistId ? { type: 'playlist', id: playlistId } : null,
      shuffled: true,
    })
    if (shuffled[0].Id) navigate(`/play/${shuffled[0].Id}`)
  }
  function togglePlaylistFavorite() {
    if (!item.Id) return
    favMut.mutate({ itemIds: [item.Id], favorite: !item.UserData?.IsFavorite })
  }

  return (
    <div onPointerMove={handlePointerMove} onPointerUp={handlePointerUp} onPointerCancel={() => setDrag(null)}>
      {/* â”€â”€ Hero â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€ */}
      <div className="relative isolate overflow-hidden">
        {playlistBackdrop && (
          <div className="absolute inset-0 -z-10">
            <img
              src={playlistBackdrop}
              alt=""
              className="w-full h-full object-cover scale-110 blur-[6px] opacity-55"
            />
            <div className="absolute inset-0 bg-gradient-to-b from-void/30 via-void/70 to-void" />
          </div>
        )}

        <div className="px-7 pt-12 pb-10 flex flex-col md:flex-row gap-7 items-end">
          <motion.div
            initial={{ opacity: 0, scale: 0.96, y: 12 }}
            animate={{ opacity: 1, scale: 1, y: 0 }}
            transition={{ duration: 0.55, ease: [0.16, 1, 0.3, 1] }}
            className="shrink-0"
          >
            {playlistPoster ? (
              <img
                src={playlistPoster}
                alt=""
                className="w-[200px] aspect-square object-cover rounded-xl ring-1 ring-white/10 shadow-[0_24px_60px_-12px_rgba(0,0,0,0.7)]"
              />
            ) : (
              <PosterMosaic items={items} serverUrl={serverUrl} />
            )}
          </motion.div>

          <motion.div
            initial={{ opacity: 0, y: 18 }}
            animate={{ opacity: 1, y: 0 }}
            transition={{ duration: 0.55, ease: [0.16, 1, 0.3, 1], delay: 0.1 }}
            className="flex-1 min-w-0"
          >
            <span className="inline-flex items-center gap-1.5 h-6 px-2.5 rounded-full bg-white/8 border border-white/12 backdrop-blur text-[10px] font-semibold uppercase tracking-[0.16em] text-white/80 mb-3">
              <Disc3 size={10} className="text-accent" />
              Playlist
            </span>

            <h1 className="font-display text-4xl md:text-5xl font-bold text-white leading-[1] tracking-tight mb-3 drop-shadow-[0_4px_16px_rgba(0,0,0,0.55)]">
              {item.Name || 'Untitled'}
            </h1>

            <p className="text-[12.5px] text-white/65 font-medium tabular-nums mb-5">
              {items.length} {items.length === 1 ? 'item' : 'items'}
              {totalRuntime && <> · {totalRuntime}</>}
              {watchedCount > 0 && <> · {watchedCount} watched</>}
            </p>

            <div className="flex items-center gap-2.5 flex-wrap mb-4">
              <button
                onClick={playFirst}
                disabled={items.length === 0}
                className={`${PRIMARY_BTN} bg-accent hover:bg-accent-hover text-void shadow-[0_8px_24px_-8px_rgba(245,182,66,0.5)] hover:scale-[1.03] disabled:opacity-50 disabled:hover:scale-100`}
              >
                <Play size={14} className="-ml-0.5" fill="currentColor" />
                Play
              </button>
              <button
                onClick={shufflePlay}
                disabled={items.length === 0}
                className={`${SECONDARY_BTN} disabled:opacity-50`}
              >
                <Shuffle size={14} stroke={2} />
                Shuffle
              </button>
              <button
                onClick={togglePlaylistFavorite}
                aria-label="Like playlist"
                aria-pressed={!!item.UserData?.IsFavorite}
                className={`${ICON_BTN} ${item.UserData?.IsFavorite ? 'text-accent' : 'text-white'}`}
              >
                <Heart
                  size={16}
                  stroke={item.UserData?.IsFavorite ? 0 : 2}
                  fill={item.UserData?.IsFavorite ? 'currentColor' : 'none'}
                />
              </button>
            </div>

            {item.Overview && (
              <p className="text-[13px] text-white/75 leading-[1.7] max-w-[68ch] tracking-[-0.005em]">
                {item.Overview}
              </p>
            )}
          </motion.div>
        </div>
      </div>

      {/* â”€â”€ Item list â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€ */}
      <div className="px-7 pt-2 pb-10">
        {isLoading ? (
          <div className="flex flex-col gap-2 mt-4">
            {Array.from({ length: 6 }).map((_, i) => (
              <div key={i} className="h-[68px] rounded-lg skeleton" />
            ))}
          </div>
        ) : items.length === 0 ? (
          <div className="text-center py-16">
            <p className="text-[14px] text-text-2 font-medium mb-1">This playlist is empty</p>
            <p className="text-[12px] text-text-4">Add items to it from any movie or show on your server.</p>
          </div>
        ) : (
          <div className="relative">
            {/* Tip strip - shown only when nothing is selected */}
            {selectedIds.size === 0 && (
              <p className="text-[10.5px] text-text-4 mb-2 mt-1 px-1">
                Drag rows to reorder. Ctrl-click to multi-select, Shift-click for a range.
              </p>
            )}
            <ol ref={listRef} className="flex flex-col gap-1 mt-2 select-none">
              {items.map((it, i) => {
                const eid = playlistItemId(it)
                const isSelected = !!eid && selectedIds.has(eid)
                const isDragging = drag?.indices.includes(i) || false
                return (
                  <PlaylistRow
                    key={`${eid || it.Id || i}-${i}`}
                    item={it}
                    index={i}
                    serverUrl={serverUrl}
                    selected={isSelected}
                    dragging={isDragging}
                    showDropIndicatorBefore={drag?.dropIndex === i}
                    onPointerDownBody={(ev) => startMaybeDrag(i, ev)}
                    refSetter={(el) => {
                      if (el) rowRefs.current.set(i, el)
                      else rowRefs.current.delete(i)
                    }}
                  />
                )
              })}
              {/* Drop indicator at the very end of the list */}
              {drag && drag.dropIndex === items.length && (
                <li className="relative h-1 -my-px">
                  <span className="absolute inset-x-2 top-1/2 -translate-y-1/2 h-px bg-accent shadow-[0_0_8px_rgba(245,182,66,0.6)]" />
                </li>
              )}
            </ol>

            {/* Floating drag preview */}
            <AnimatePresence>
              {drag && (
                <DragGhost
                  count={drag.indices.length}
                  pointerY={drag.currentY}
                  label={items[drag.indices[0]]?.Name || ''}
                />
              )}
            </AnimatePresence>
          </div>
        )}
      </div>

      {/* â”€â”€ Selection toolbar â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€ */}
      <AnimatePresence>
        {selectedIds.size > 0 && (
          <SelectionToolbar
            count={selectedIds.size}
            onClear={clearSelection}
            onRemove={bulkRemove}
            onMarkPlayed={() => bulkMark(true)}
            onMarkUnplayed={() => bulkMark(false)}
            onToggleFavorite={bulkFavorite}
          />
        )}
      </AnimatePresence>
    </div>
  )
}

