<script lang="ts">
	import { onDestroy } from 'svelte';
	import type { FeedUpload } from '$lib/types';
	import { dataMode, pickMediaUrl } from '$lib/data-mode-store';
	import { longpress } from '$lib/actions/longpress';
	import { doubletap } from '$lib/actions/doubletap';
	import { avatarPalette, initials } from '$lib/avatar';
	import { vibrate } from '$lib/haptics';
	import { now } from '$lib/now';
	import { commentsEnabled } from '$lib/event-config-store';
	import HeartBurst from './HeartBurst.svelte';

	interface Props {
		upload: FeedUpload;
		isOwn?: boolean;
		onlike: (id: string) => void;
		oncomment: (id: string) => void;
		onselect: (upload: FeedUpload) => void;
		oncontextmenu?: (upload: FeedUpload) => void;
	}

	let { upload, isOwn = false, onlike, oncomment, onselect, oncontextmenu }: Props = $props();

	function isVideo(mime: string): boolean {
		return mime.startsWith('video/');
	}

	const mediaSrc = $derived(pickMediaUrl($dataMode, upload));

	// Video cards show a poster if one exists; `pickMediaUrl` is not used here because it is
	// mime-agnostic and would hand back the raw MP4 for an <img>.
	const posterSrc = $derived(upload.thumbnail_url ?? upload.preview_url ?? '');

	// ── Media fallback ───────────────────────────────────────────────────────────────
	//
	// A failed <img> rendered as an empty grey box with `alt=""` — no icon, no message,
	// nothing to tap. And the failure is routine rather than exotic: `pickMediaUrl` falls
	// back to `/original` whenever preview AND thumbnail are still null, i.e. for everything
	// still compressing, which in a newest-first feed is the card at the top during every
	// burst. That also makes the cause usually TRANSIENT, so one delayed retry of the same
	// URL recovers most of them; the nonce exists because the browser would otherwise replay
	// its cached failure rather than re-request. Two strikes and we fall through to the
	// placeholder this card already draws for "no derivative yet".
	const MEDIA_RETRY_MS = 4000;
	let mediaRetryNonce = $state(0);
	let mediaFailed = $state(false);
	let mediaRetryTimer: ReturnType<typeof setTimeout> | null = null;

	function withNonce(url: string): string {
		if (!url || !mediaRetryNonce) return url;
		return `${url}${url.includes('?') ? '&' : '?'}r=${mediaRetryNonce}`;
	}

	const displaySrc = $derived(withNonce(isVideo(upload.mime_type) ? posterSrc : mediaSrc));

	// A url change — the SSE `upload-processed` swapping the original for a real preview is
	// the common one — deserves a clean attempt rather than inheriting the old verdict.
	$effect(() => {
		void mediaSrc;
		void posterSrc;
		mediaRetryNonce = 0;
		mediaFailed = false;
	});

	function handleMediaError() {
		if (mediaRetryNonce) {
			mediaFailed = true;
			return;
		}
		if (mediaRetryTimer) clearTimeout(mediaRetryTimer);
		mediaRetryTimer = setTimeout(() => {
			mediaRetryTimer = null;
			mediaRetryNonce = Date.now();
		}, MEDIA_RETRY_MS);
	}

	function relativeTime(iso: string, nowMs: number): string {
		const diff = nowMs - new Date(iso).getTime();
		const mins = Math.floor(diff / 60000);
		if (mins < 1) return 'gerade eben';
		if (mins < 60) return `vor ${mins} Min.`;
		const hrs = Math.floor(mins / 60);
		if (hrs < 24) return `vor ${hrs} Std.`;
		const days = Math.floor(hrs / 24);
		return `vor ${days} Tag${days === 1 ? '' : 'en'}`;
	}

	// Re-derives off the shared 60s clock so "vor 5 Min." actually advances.
	const relTime = $derived(relativeTime(upload.created_at, $now));

	function openContext() {
		oncontextmenu?.(upload);
	}

	// Inline heart-burst on double-tap (consistent with the lightbox).
	let heartBurst = $state(false);
	let burstTimer: ReturnType<typeof setTimeout> | null = null;

	function handleDoubleTap() {
		heartBurst = true;
		vibrate(10);
		onlike(upload.id);
		if (burstTimer) clearTimeout(burstTimer);
		burstTimer = setTimeout(() => (heartBurst = false), 700);
	}

	// Clear the burst timer on unmount (a feed-delta SSE event can remove this
	// card mid-animation) so it can't fire against a stale component.
	onDestroy(() => {
		if (burstTimer) clearTimeout(burstTimer);
		if (mediaRetryTimer) clearTimeout(mediaRetryTimer);
	});
</script>

<!-- Off-screen cards are removed from the DOM entirely by the parent VirtualFeed
     window-virtualizer, so this card no longer needs content-visibility — and must
     not use it, since the virtualizer measures each card's real rendered height. -->
<article class="px-3 py-2" use:longpress={{ duration: 500 }} onlongpress={openContext}>
	<div
		class="overflow-hidden rounded-2xl border border-gray-200/70 bg-white shadow-sm dark:border-gray-800 dark:bg-gray-900"
	>
		<!-- Uploader row -->
		<div class="flex items-center justify-between gap-3 px-4 py-3">
			<div class="flex min-w-0 items-center gap-3">
				<div
					class="flex h-9 w-9 shrink-0 items-center justify-center rounded-full text-sm font-bold
					{avatarPalette(upload.uploader_name)}"
				>
					{initials(upload.uploader_name)}
				</div>
				<div class="min-w-0">
					<p class="truncate text-sm font-semibold text-gray-900 dark:text-gray-100">
						{upload.uploader_name}
					</p>
					<p class="text-xs text-gray-400 dark:text-gray-500">{relTime}</p>
				</div>
			</div>

			{#if oncontextmenu}
				<!-- Desktop kebab — same actions as the mobile long-press context sheet. -->
				<button
					type="button"
					onclick={(e) => {
						e.stopPropagation();
						openContext();
					}}
					class="rounded-full p-1 text-gray-400 hover:bg-gray-100 active:bg-gray-200 hover:text-gray-700 dark:text-gray-500 dark:hover:bg-gray-800 dark:active:bg-gray-800 dark:hover:text-gray-200"
					aria-label="Mehr Aktionen"
				>
					<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 24 24"
						><circle cx="5" cy="12" r="2" /><circle cx="12" cy="12" r="2" /><circle
							cx="19"
							cy="12"
							r="2"
						/></svg
					>
				</button>
			{/if}
		</div>

		<!-- Media -->
		<button
			class="relative block w-full"
			use:doubletap
			onsingletap={() => onselect(upload)}
			ondoubletap={handleDoubleTap}
			onclick={(e) => {
				if (e.detail === 0) onselect(upload);
			}}
			aria-label="Bild vergrößern"
		>
			<HeartBurst active={heartBurst} />
			{#if isVideo(upload.mime_type)}
				<div class="relative aspect-video w-full bg-gray-900">
					{#if posterSrc && !mediaFailed}
						<img
							src={displaySrc}
							alt=""
							class="h-full w-full object-cover opacity-80"
							loading="lazy"
							decoding="async"
							onerror={handleMediaError}
						/>
					{/if}
					<div class="absolute inset-0 flex items-center justify-center">
						<span
							class="flex h-14 w-14 items-center justify-center rounded-full bg-black/50 text-white"
						>
							<svg class="h-7 w-7 pl-0.5" fill="currentColor" viewBox="0 0 24 24">
								<path d="M8 5v14l11-7z" />
							</svg>
						</span>
					</div>
				</div>
			{:else if mediaSrc && !mediaFailed}
				<!-- Reserve the same 4/5 box the skeleton uses so the card doesn't collapse
			     to height 0 and reflow as images stream in. The uncropped original is one
			     tap away in the lightbox. -->
				<div class="aspect-[4/5] w-full bg-gray-100 dark:bg-gray-800">
					<img
						src={displaySrc}
						alt=""
						class="h-full w-full object-cover"
						loading="lazy"
						decoding="async"
						onerror={handleMediaError}
					/>
				</div>
			{:else}
				<div
					class="flex aspect-square w-full items-center justify-center bg-gray-100 dark:bg-gray-800"
				>
					<svg
						class="h-12 w-12 text-gray-300 dark:text-gray-600"
						fill="none"
						viewBox="0 0 24 24"
						stroke="currentColor"
					>
						<path
							stroke-linecap="round"
							stroke-linejoin="round"
							stroke-width="1.5"
							d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"
						/>
					</svg>
				</div>
			{/if}
		</button>

		<!-- Actions row -->
		<div class="flex items-center gap-4 px-4 py-2">
			<button
				onclick={() => {
					vibrate(10);
					onlike(upload.id);
				}}
				aria-pressed={upload.liked_by_me}
				aria-label={upload.liked_by_me ? 'Gefällt mir nicht mehr' : 'Gefällt mir'}
				class="flex items-center gap-1.5 text-sm font-medium transition-colors
				{upload.liked_by_me
					? 'text-red-500 dark:text-red-400'
					: 'text-gray-500 hover:text-red-400 active:text-red-400 dark:text-gray-400 dark:hover:text-red-400 dark:active:text-red-400'}"
			>
				<svg
					class="h-5 w-5 {upload.liked_by_me ? 'fill-red-500' : ''}"
					fill="none"
					viewBox="0 0 24 24"
					stroke="currentColor"
					stroke-width="2"
				>
					<path
						stroke-linecap="round"
						stroke-linejoin="round"
						d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z"
					/>
				</svg>
				{upload.like_count}
			</button>
			{#if $commentsEnabled}
				<button
					onclick={() => oncomment(upload.id)}
					aria-label="Kommentare anzeigen"
					class="flex items-center gap-1.5 text-sm font-medium text-gray-500 transition-colors hover:text-blue-500 active:text-blue-500 dark:text-gray-400 dark:hover:text-blue-400 dark:active:text-blue-400"
				>
					<svg
						class="h-5 w-5"
						fill="none"
						viewBox="0 0 24 24"
						stroke="currentColor"
						stroke-width="2"
					>
						<path
							stroke-linecap="round"
							stroke-linejoin="round"
							d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"
						/>
					</svg>
					{upload.comment_count}
				</button>
			{/if}
			{#if isOwn}
				<span class="ml-auto text-xs text-gray-400 dark:text-gray-500">Eigener Beitrag</span>
			{/if}
		</div>

		<!-- Caption -->
		{#if upload.caption}
			<p class="px-4 pb-3 text-sm text-gray-800 [overflow-wrap:anywhere] dark:text-gray-200">
				{upload.caption}
			</p>
		{/if}
	</div>
</article>
