<!-- TRACES: UR-051, UR-052, UR-068 | DR-068, DR-078, DR-119 -->
<script lang="ts">
  import type { MediaItem, Library } from "$lib/api/types";
  import { truncateMiddle } from "$lib/utils/truncateMiddle";
  import { downloads } from "$lib/stores/downloads";
  import { isConnected } from "$lib/stores/connectivity";
  import { showServerCatalog } from "$lib/services/offlineCatalog";
  import { auth } from "$lib/stores/auth";
  import CachedImage from "$lib/components/common/CachedImage.svelte";
  import FavoriteButton from "$lib/components/FavoriteButton.svelte";
  import { favoriteOverrides, resolveIsFavorite } from "$lib/stores/favorites";

  interface Props {
    item: MediaItem | Library;
    size?: "small" | "medium" | "large";
    showProgress?: boolean;
    showDownloadStatus?: boolean;
    /**
     * Secondary on-disk size label (e.g. "1.2 GB"), shown under the subtitle.
     * Used by the Downloaded browse surface. TRACES: UR-056 | DR-085
     */
    sizeLabel?: string;
    /**
     * "full" | "partial" — badges a downloaded container on the artwork so a
     * fully-downloaded item reads differently from a partially-downloaded one.
     * TRACES: UR-055 | DR-083
     */
    downloadedBadge?: "full" | "partial";
    /**
     * When set, a hover/focus "remove from device" control appears on the card
     * (Downloaded surface only). TRACES: UR-055, UR-056 | DR-083
     */
    onRemove?: () => void;
    onclick?: () => void;
    /**
     * When set, a long press (touch hold / mouse hold) fires this instead of the
     * regular tap. The tap that would otherwise follow the release is suppressed.
     * Used on the home page: tap opens the detail page, long-press plays now.
     * TRACES: UR-058 | DR-087
     */
    onLongPress?: () => void;
    /**
     * Show the favourite heart on the artwork. On by default for media items;
     * surfaces that are not about the item itself can opt out.
     * TRACES: UR-068 | DR-119
     */
    showFavorite?: boolean;
    /**
     * Force the artwork box to a fixed aspect ratio instead of deriving one from
     * the item. Use on rows that mix item kinds (e.g. the home "Your Libraries"
     * strip, where square music art next to 16:9 video art would otherwise give
     * the cards different heights). Artwork still fills the box via object-cover.
     */
    aspect?: "square" | "video" | "poster";
  }

  let { item, size = "medium", showProgress = false, showDownloadStatus = true, sizeLabel, downloadedBadge, onRemove, onclick, onLongPress, showFavorite = true, aspect }: Props = $props();

  // Long-press detection. We arm a timer on pointerdown; if it fires before the
  // pointer is released (or moves too far), we treat it as a long press and set a
  // flag so the ensuing click is swallowed. Pointer events cover touch + mouse.
  const LONG_PRESS_MS = 500;
  const MOVE_CANCEL_PX = 10;
  let pressTimer: ReturnType<typeof setTimeout> | null = null;
  let longPressFired = false;
  let pressStartX = 0;
  let pressStartY = 0;

  function clearPressTimer() {
    if (pressTimer !== null) {
      clearTimeout(pressTimer);
      pressTimer = null;
    }
  }

  function handlePointerDown(e: PointerEvent) {
    if (!onLongPress || isServerOnly) return;
    longPressFired = false;
    pressStartX = e.clientX;
    pressStartY = e.clientY;
    clearPressTimer();
    pressTimer = setTimeout(() => {
      longPressFired = true;
      pressTimer = null;
      onLongPress?.();
    }, LONG_PRESS_MS);
  }

  function handlePointerMove(e: PointerEvent) {
    if (pressTimer === null) return;
    if (
      Math.abs(e.clientX - pressStartX) > MOVE_CANCEL_PX ||
      Math.abs(e.clientY - pressStartY) > MOVE_CANCEL_PX
    ) {
      clearPressTimer();
    }
  }

  function handlePointerUp() {
    clearPressTimer();
  }

  function handleClick() {
    // A long press already handled this interaction; swallow the trailing click.
    if (longPressFired) {
      longPressFired = false;
      return;
    }
    onclick?.();
  }

  // Check if this item is downloaded
  const downloadInfo = $derived(
    Object.values($downloads.downloads).find((d) => d.itemId === item.id)
  );

  const isDownloaded = $derived(downloadInfo?.status === "completed");
  const isDownloading = $derived(
    downloadInfo?.status === "downloading" || downloadInfo?.status === "pending"
  );
  const downloadProgress = $derived(downloadInfo?.progress || 0);

  // A media item (not a library) is "server only" when it exists in the cache
  // but isn't downloaded/downloading — and we're offline with the reveal toggle
  // on. Such cards render greyed out with a "queue for download" button and are
  // inert to tap (nothing to play offline).
  const isMediaItem = $derived("type" in item);
  const isQueued = $derived(downloadInfo?.status === "pending");
  // Actively transferring (as opposed to merely queued/pending for reconnect).
  const isActivelyDownloading = $derived(downloadInfo?.status === "downloading");
  // "Server only" = offline, reveal on, and not already downloaded or actively
  // transferring. A `pending` (queued-for-reconnect) item stays server-only so
  // it can show the Queued badge in place of the queue button.
  const isServerOnly = $derived(
    isMediaItem && !$isConnected && $showServerCatalog && !isDownloaded && !isActivelyDownloading
  );

  // The heart is about an item, so libraries never get one, and a greyed
  // server-only card has nothing actionable to offer. TRACES: UR-068 | DR-119
  const showHeart = $derived(showFavorite && isMediaItem && !isServerOnly);
  const isFavorited = $derived(
    isMediaItem ? resolveIsFavorite(item as MediaItem, $favoriteOverrides) : false
  );

  let queueError = $state<string | null>(null);

  // Queue this item for download on next reconnect. Offline, this just persists
  // a `pending` downloads row (no stream_url); the reconnect handler resolves
  // the URL and the pump starts it. See offlineCatalog service.
  async function queueForDownload(e: Event) {
    e.stopPropagation();
    if (!isMediaItem) return;
    const media = item as MediaItem;
    const userId = auth.getUserId();
    if (!userId) {
      queueError = "Not signed in";
      return;
    }
    try {
      queueError = null;
      // Derive a sensible on-disk path; the backend heals stream_url on reconnect.
      const filePath = `downloads/${media.id}`;
      await downloads.downloadItem(
        media.id,
        userId,
        filePath,
        undefined,
        undefined,
        media.name,
        media.artists?.join(", ") ?? undefined,
        media.albumName ?? undefined
      );
    } catch (err) {
      console.error("[MediaCard] Failed to queue download:", err);
      queueError = "Failed to queue";
    }
  }

  const sizeClasses = {
    small: "w-24",
    medium: "w-36",
    large: "w-48",
  };

  const isMusicType = $derived(
    "kind" in item && (item.kind === "track" || item.kind === "album" || item.kind === "artist" || item.kind === "playlist")
  );

  const FIXED_ASPECT = {
    square: "aspect-square",
    video: "aspect-video",
    poster: "aspect-[2/3]",
  } as const;

  const aspectRatio = $derived(() => {
    if (aspect) return FIXED_ASPECT[aspect];
    if ("kind" in item) {
      return isMusicType ? "aspect-square" : "aspect-[2/3]";
    }
    // Library
    return "collectionType" in item && item.collectionType === "music" ? "aspect-square" : "aspect-video";
  });

  const imageTag = $derived(
    "imageId" in item ? item.imageId : ("imageTag" in item ? item.imageTag : undefined)
  );

  const maxWidth = $derived(size === "large" ? 400 : size === "medium" ? 300 : 200);

  const progress = $derived(() => {
    if (!showProgress || !("userData" in item) || !item.userData || !item.durationMs) {
      return 0;
    }
    return ((item.userData.playbackPositionMs ?? 0) / item.durationMs) * 100;
  });

  const subtitle = $derived(() => {
    if (!("type" in item)) return "";

    switch (item.type) {
      case "Audio":
        return item.artists?.join(", ") || item.albumName || "";
      case "MusicAlbum":
        return item.artistItems?.map((a) => a.name).join(", ") || "";
      case "Episode":
        return item.seriesName ? `${item.seriesName} - S${item.parentIndexNumber}E${item.indexNumber}` : "";
      case "Movie":
        return item.productionYear?.toString() || "";
      case "Series":
        return item.productionYear?.toString() || "";
      default:
        return "";
    }
  });
</script>

<svelte:element
  this={isServerOnly ? "div" : "button"}
  type={isServerOnly ? undefined : "button"}
  role={isServerOnly ? "group" : undefined}
  class="group/card flex flex-col text-left {sizeClasses[size]} flex-shrink-0 transition-transform duration-200 {isServerOnly ? '' : 'hover:scale-105'}"
  style={onLongPress ? "touch-action: manipulation; -webkit-touch-callout: none;" : undefined}
  onclick={isServerOnly ? undefined : handleClick}
  onpointerdown={isServerOnly ? undefined : handlePointerDown}
  onpointermove={isServerOnly ? undefined : handlePointerMove}
  onpointerup={isServerOnly ? undefined : handlePointerUp}
  onpointercancel={isServerOnly ? undefined : handlePointerUp}
  oncontextmenu={onLongPress ? (e: Event) => e.preventDefault() : undefined}
>
  <div class="relative {aspectRatio()} w-full rounded-lg overflow-hidden bg-[var(--color-surface)] shadow-md group-hover/card:shadow-2xl transition-shadow duration-200">
    <CachedImage
      itemId={item.id}
      imageType="Primary"
      tag={imageTag}
      maxWidth={maxWidth}
      alt={item.name}
      class="w-full h-full object-cover transition-transform duration-300 group-hover/card:scale-110 {isServerOnly ? 'opacity-40 grayscale' : ''}"
    />

    <!-- Hover overlay with smooth gradient (play affordance; hidden for
         server-only cards, which can't be played offline) -->
    <div class="absolute inset-0 bg-gradient-to-t from-black/60 via-black/0 to-black/0 opacity-0 {isServerOnly ? '' : 'group-hover/card:opacity-100'} transition-opacity duration-300 flex items-center justify-center">
      <div class="transform scale-90 group-hover/card:scale-100 opacity-0 group-hover/card:opacity-100 transition-all duration-300">
        <div class="w-14 h-14 rounded-full bg-[var(--color-jellyfin)] hover:bg-[var(--color-jellyfin)]/90 flex items-center justify-center shadow-2xl">
          <svg class="w-7 h-7 text-white ml-1" fill="currentColor" viewBox="0 0 24 24">
            <path d="M8 5v14l11-7z"/>
          </svg>
        </div>
      </div>
    </div>

    <!-- Progress bar -->
    {#if progress() > 0}
      <div class="absolute bottom-0 left-0 right-0 h-1 bg-gray-800">
        <div
          class="h-full bg-[var(--color-jellyfin)]"
          style="width: {progress()}%"
        ></div>
      </div>
    {/if}

    <!-- Top-right status stack: played tick, then the favourite heart. Grouped
         so the two never land on the same pixels when both apply. -->
    {#if ("userData" in item && item.userData?.isPlayed) || showHeart}
      <div class="absolute top-2 right-2 flex flex-col items-end gap-1">
        {#if "userData" in item && item.userData?.isPlayed}
          <svg class="w-5 h-5 text-[var(--color-jellyfin)]" fill="currentColor" viewBox="0 0 24 24">
            <path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/>
          </svg>
        {/if}
        {#if showHeart}
          <!-- Always visible on touch (no hover to reveal it); on pointer
               devices an unfavourited heart stays out of the way until the card
               is hovered or focused. A favourited one is always shown — it is
               state, not an affordance. TRACES: UR-068 | DR-119 -->
          <div
            class="transition-opacity {isFavorited
              ? ''
              : 'opacity-100 [@media(hover:hover)]:opacity-0 [@media(hover:hover)]:group-hover/card:opacity-100 [@media(hover:hover)]:group-focus-within/card:opacity-100'}"
          >
            <FavoriteButton
              itemId={item.id}
              isFavorite={isFavorited}
              size="sm"
              variant="overlay"
              stopPropagation
            />
          </div>
        {/if}
      </div>
    {/if}

    <!-- Download indicator -->
    {#if showDownloadStatus && (isDownloaded || isDownloading)}
      <div class="absolute bottom-2 right-2" title={isDownloaded ? "Downloaded" : "Downloading..."}>
        {#if isDownloaded}
          <!-- Downloaded badge -->
          <div class="w-6 h-6 rounded-full bg-green-600 flex items-center justify-center shadow-lg">
            <svg class="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
              <path stroke-linecap="round" stroke-linejoin="round" d="M12 4v12m0 0l-4-4m4 4l4-4" />
            </svg>
          </div>
        {:else if isDownloading}
          <!-- Downloading progress -->
          <div class="w-6 h-6 relative">
            <svg class="w-6 h-6 -rotate-90" viewBox="0 0 24 24">
              <circle
                cx="12"
                cy="12"
                r="10"
                fill="rgba(0,0,0,0.6)"
                stroke="rgba(255,255,255,0.3)"
                stroke-width="2"
              />
              <circle
                cx="12"
                cy="12"
                r="10"
                fill="none"
                stroke="#3b82f6"
                stroke-width="2"
                stroke-dasharray={2 * Math.PI * 10}
                stroke-dashoffset={2 * Math.PI * 10 * (1 - downloadProgress)}
                stroke-linecap="round"
                class="transition-all duration-300"
              />
            </svg>
            <svg class="absolute inset-0 m-auto w-3 h-3 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
              <path stroke-linecap="round" stroke-linejoin="round" d="M12 4v12m0 0l-4-4m4 4l4-4" />
            </svg>
          </div>
        {/if}
      </div>
    {/if}

    <!-- Remove-from-device control (Downloaded surface), shown on hover/focus -->
    {#if onRemove}
      <button
        type="button"
        onclick={(e) => { e.stopPropagation(); onRemove?.(); }}
        class="absolute top-2 left-2 w-7 h-7 rounded-full bg-black/70 hover:bg-red-600 text-white flex items-center justify-center opacity-0 group-hover/card:opacity-100 focus:opacity-100 transition-opacity shadow-lg"
        title="Remove from device"
        aria-label="Remove {item.name} from device"
      >
        <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
          <path stroke-linecap="round" stroke-linejoin="round" d="M6 7h12M9 7V5a1 1 0 011-1h4a1 1 0 011 1v2m-7 0v12a1 1 0 001 1h6a1 1 0 001-1V7" />
        </svg>
      </button>
    {/if}

    <!-- Container downloaded badge (Downloaded surface): full vs partial -->
    {#if downloadedBadge}
      <div
        class="absolute bottom-2 right-2"
        title={downloadedBadge === "full" ? "Fully downloaded" : "Partially downloaded"}
      >
        {#if downloadedBadge === "full"}
          <div class="w-6 h-6 rounded-full bg-green-600 flex items-center justify-center shadow-lg">
            <svg class="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
              <path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
            </svg>
          </div>
        {:else}
          <div class="w-6 h-6 rounded-full bg-amber-500 flex items-center justify-center shadow-lg" aria-label="Partially downloaded">
            <svg class="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
              <path stroke-linecap="round" stroke-linejoin="round" d="M12 4v12m0 0l-4-4m4 4l4-4" />
            </svg>
          </div>
        {/if}
      </div>
    {/if}

    <!-- Server-only: queue-for-download control (kept at full opacity over the
         greyed artwork). Queued items show a "queued" badge instead. -->
    {#if isServerOnly}
      <div class="absolute inset-0 flex items-center justify-center">
        {#if isQueued}
          <div class="flex flex-col items-center gap-1 text-white" title="Queued — will download on reconnect">
            <div class="w-11 h-11 rounded-full bg-black/60 flex items-center justify-center shadow-lg">
              <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
                <path stroke-linecap="round" stroke-linejoin="round" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
              </svg>
            </div>
            <span class="text-[10px] font-medium bg-black/60 px-1.5 py-0.5 rounded-full">Queued</span>
          </div>
        {:else}
          <button
            type="button"
            onclick={queueForDownload}
            class="w-11 h-11 rounded-full bg-[var(--color-jellyfin)] hover:bg-[var(--color-jellyfin)]/90 flex items-center justify-center shadow-lg transition-colors"
            title="Queue download for next connection"
            aria-label="Queue download for {item.name}"
          >
            <svg class="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
              <path stroke-linecap="round" stroke-linejoin="round" d="M12 4v12m0 0l-4-4m4 4l4-4M4 20h16" />
            </svg>
          </button>
        {/if}
      </div>
      {#if queueError}
        <div class="absolute bottom-1 left-1 right-1 text-center text-[10px] text-red-200 bg-black/70 rounded px-1 py-0.5">
          {queueError}
        </div>
      {/if}
    {/if}
  </div>

  <div class="mt-2 space-y-0.5 {isServerOnly ? 'opacity-60' : ''}">
    <p class="text-sm font-medium text-white truncate group-hover/card:text-[var(--color-jellyfin)] transition-colors">
      {truncateMiddle(item.name, 40)}
    </p>
    {#if subtitle()}
      <p class="text-xs text-gray-400 truncate">{subtitle()}</p>
    {/if}
    {#if sizeLabel}
      <p class="text-xs text-gray-500 truncate">{sizeLabel}</p>
    {/if}
  </div>
</svelte:element>
