<script lang="ts">
	/**
	 * AudioPlayer — controller component.
	 *
	 * Does NOT own an <audio> element.  Instead it reads/writes `audioStore`,
	 * which is shared with the layout's persistent <audio> element so audio
	 * survives SvelteKit navigations.
	 *
	 * ── Play flow ────────────────────────────────────────────────────────────
	 * On "Play narration" click / auto-start:
	 *   1. Populate store metadata (slug, chapter, titles, voice, speed).
	 *   2. If the pre-fetch already landed (nextStatus='prefetched' AND
	 *      nextChapterPrefetched === chapter), use the cached URL immediately.
	 *   3. Otherwise try GET /api/presign/audio — if 200, set audioUrl → layout plays.
	 *   4. If 404, POST /api/audio/:slug/:n to generate. Drive pseudo progress bar.
	 *      On success, presign again and set audioUrl.
	 *
	 * ── Voice selection ──────────────────────────────────────────────────────
	 * A "Change voice" panel lets users pick from the available Kokoro voices.
	 * Each voice shows a play button that streams a pre-generated sample from
	 * MinIO (GET /api/presign/voice-sample?voice=...). Samples are generated
	 * server-side via POST /api/audio/voice-samples.
	 *
	 * Changing voice updates audioStore.voice (saved to settings via layout).
	 * The currently loaded chapter audio is NOT re-generated automatically —
	 * the new voice takes effect on next "Play narration" click.
	 *
	 * ── Pre-fetch (immediate + 90% fallback) ────────────────────────────────
	 * When autoNext is on, prefetchNext() is called as soon as the current
	 * chapter starts playing (via maybeStartPrefetch() at the end of
	 * startPlayback()).  This gives the maximum lead time for Kokoro to
	 * generate the next chapter so the transition is seamless.
	 *
	 * A $effect also watches currentTime/duration and fires prefetchNext() at
	 * the 90% mark as a fallback — covering the case where autoNext was toggled
	 * on mid-playback after startPlayback() had already returned.
	 * The nextStatus !== 'none' guard prevents double-runs in all cases.
	 *
	 * prefetchNext():
	 *   • Calls POST /api/audio for next chapter (sets nextStatus='prefetching')
	 *   • On success, presigns and stores URL in audioStore.nextAudioUrl
	 *     (sets nextStatus='prefetched')
	 *   • On failure, sets nextStatus='failed'
	 *
	 * ── Auto-next ────────────────────────────────────────────────────────────
	 * layout.svelte onended → sets autoStartPending=true → navigates.
	 * New chapter's AudioPlayer mounts → sees autoStartPending → startPlayback()
	 * which uses the prefetched URL if available.
	 */

	import { audioStore } from '$lib/audio.svelte';

	interface Props {
		slug: string;
		chapter: number;
		chapterTitle?: string;
		bookTitle?: string;
		/** Cover image URL for the book (used in MediaSession for lock-screen art). */
		cover?: string;
		/** Next chapter number, or null/undefined if this is the last chapter. */
		nextChapter?: number | null;
		/** Full chapter list for the book (number + title). Written into the store. */
		chapters?: { number: number; title: string }[];
		/** List of available voices from the Kokoro API. */
		voices?: string[];
	}

	let {
		slug,
		chapter,
		chapterTitle = '',
		bookTitle = '',
		cover = '',
		nextChapter = null,
		chapters = [],
		voices = []
	}: Props = $props();

	// ── Voice selector state ────────────────────────────────────────────────
	let showVoicePanel = $state(false);
	/** Voice whose sample is currently being fetched or playing. */
	let samplePlayingVoice = $state<string | null>(null);
	/** Currently active sample <audio> element — one at a time. */
	let sampleAudio = $state<HTMLAudioElement | null>(null);

	/**
	 * Human-readable label for a voice ID.
	 * e.g. "af_bella" → "Bella (US F)" | "bm_george" → "George (UK M)"
	 */
	function voiceLabel(v: string): string {
		const langMap: Record<string, string> = {
			af: 'US', am: 'US',
			bf: 'UK', bm: 'UK',
			ef: 'ES', em: 'ES',
			ff: 'FR',
			hf: 'IN', hm: 'IN',
			'if': 'IT', im: 'IT',
			jf: 'JP', jm: 'JP',
			pf: 'PT', pm: 'PT',
			zf: 'ZH', zm: 'ZH',
		};
		const genderMap: Record<string, string> = {
			af: 'F', am: 'M',
			bf: 'F', bm: 'M',
			ef: 'F', em: 'M',
			ff: 'F',
			hf: 'F', hm: 'M',
			'if': 'F', im: 'M',
			jf: 'F', jm: 'M',
			pf: 'F', pm: 'M',
			zf: 'F', zm: 'M',
		};
		const prefix = v.slice(0, 2);
		const name = v.slice(3);
		// Capitalise and strip legacy v0 prefix.
		const displayName = name
			.replace(/^v0/, '')
			.replace(/^([a-z])/, (c: string) => c.toUpperCase());
		const lang = langMap[prefix] ?? prefix.toUpperCase();
		const gender = genderMap[prefix] ?? '?';
		return `${displayName} (${lang} ${gender})`;
	}

	/** Stop any currently playing sample. */
	function stopSample() {
		if (sampleAudio) {
			sampleAudio.pause();
			sampleAudio.src = '';
			sampleAudio = null;
		}
		samplePlayingVoice = null;
	}

	/** Play a voice sample from MinIO. */
	async function playSample(voice: string) {
		// If this voice is already playing, stop it.
		if (samplePlayingVoice === voice) {
			stopSample();
			return;
		}
		stopSample();

		samplePlayingVoice = voice;
		try {
			const res = await fetch(`/api/presign/voice-sample?voice=${encodeURIComponent(voice)}`);
			if (res.status === 404) {
				// Sample not generated yet — silently ignore
				samplePlayingVoice = null;
				return;
			}
			if (!res.ok) throw new Error(`presign failed: ${res.status}`);
			const data = (await res.json()) as { url: string };

			const audio = new Audio(data.url);
			sampleAudio = audio;
			audio.onended = () => {
				if (samplePlayingVoice === voice) stopSample();
			};
			audio.onerror = () => {
				if (samplePlayingVoice === voice) stopSample();
			};
			await audio.play();
		} catch {
			samplePlayingVoice = null;
		}
	}

	/** Select a voice and close the panel. */
	function selectVoice(voice: string) {
		stopSample();
		audioStore.voice = voice;
		showVoicePanel = false;
	}

	// Keep nextChapter in the store so the layout's onended can navigate.
	// NOTE: we do NOT clear on unmount here — the store retains the value so
	// onended (which may fire after {#key} unmounts this component) can still
	// read it.  The value is superseded when the new chapter mounts.
	$effect(() => {
		audioStore.nextChapter = nextChapter ?? null;
	});

	// Auto-start: if the layout navigated here via auto-next, kick off playback.
	// We match against the chapter prop so the outgoing chapter's AudioPlayer
	// (still mounted during the brief navigation window) never reacts to this.
	$effect(() => {
		if (audioStore.autoStartChapter === chapter) {
			audioStore.autoStartChapter = null;
			startPlayback();
		}
	});

	// Reset next-chapter prefetch state when this chapter changes (new page).
	// Only reset if the prefetch belongs to neither the current chapter
	// (about to be consumed by startPlayback) nor the next chapter (still valid).
	// Any other value means stale data from a previous page.
	$effect(() => {
		const prefetchedFor = audioStore.nextChapterPrefetched;
		if (
			prefetchedFor !== null &&
			prefetchedFor !== chapter &&
			prefetchedFor !== (nextChapter ?? null)
		) {
			audioStore.resetNextPrefetch();
		}
	});

	// Close voice panel when user clicks outside (escape key).
	function handleKeyDown(e: KeyboardEvent) {
		if (e.key === 'Escape') {
			stopSample();
			showVoicePanel = false;
		}
	}

	// ── 90% pre-fetch trigger ─────────────────────────────────────────────────
	// Watch playback progress; when >= 90% of current chapter, pre-generate
	// the next chapter's audio so it's ready when we navigate.
	$effect(() => {
		const ct = audioStore.currentTime;
		const dur = audioStore.duration;
		const isCurrentlyPlaying = audioStore.isCurrentChapter(slug, chapter);

		if (
			!isCurrentlyPlaying ||
			!audioStore.autoNext ||
			nextChapter === null ||
			nextChapter === undefined ||
			dur <= 0 ||
			ct / dur < 0.9 ||
			audioStore.nextStatus !== 'none'
		) {
			return;
		}

		// Trigger exactly once (nextStatus transitions away from 'none')
		prefetchNext();
	});

	// ── Pseudo progress helpers ────────────────────────────────────────────────
	let progressRafId = 0;

	function startProgress() {
		audioStore.progress = 0;
		let last = performance.now();

		function tick(now: number) {
			const dt = (now - last) / 1000;
			last = now;
			let rate: number;
			if (audioStore.progress < 30) rate = 4;
			else if (audioStore.progress < 60) rate = 12;
			else if (audioStore.progress < 80) rate = 4;
			else rate = 0.3;

			audioStore.progress = Math.min(audioStore.progress + rate * dt, 99);
			if (audioStore.progress < 99) {
				progressRafId = requestAnimationFrame(tick);
			}
		}
		progressRafId = requestAnimationFrame(tick);
	}

	function stopProgress() {
		if (progressRafId) {
			cancelAnimationFrame(progressRafId);
			progressRafId = 0;
		}
	}

	async function finishProgress() {
		stopProgress();
		const step = () => {
			audioStore.progress = Math.min(audioStore.progress + 8, 100);
			if (audioStore.progress < 100) {
				progressRafId = requestAnimationFrame(step);
			}
		};
		progressRafId = requestAnimationFrame(step);
		await new Promise((r) => setTimeout(r, 200));
		stopProgress();
	}

	// ── Next-chapter pseudo-progress helpers ──────────────────────────────────
	let nextProgressRafId = 0;

	function startNextProgress() {
		audioStore.nextProgress = 0;
		let last = performance.now();

		function tick(now: number) {
			const dt = (now - last) / 1000;
			last = now;
			let rate: number;
			if (audioStore.nextProgress < 30) rate = 4;
			else if (audioStore.nextProgress < 60) rate = 12;
			else if (audioStore.nextProgress < 80) rate = 4;
			else rate = 0.3;

			audioStore.nextProgress = Math.min(audioStore.nextProgress + rate * dt, 99);
			if (audioStore.nextProgress < 99) {
				nextProgressRafId = requestAnimationFrame(tick);
			}
		}
		nextProgressRafId = requestAnimationFrame(tick);
	}

	function stopNextProgress() {
		if (nextProgressRafId) {
			cancelAnimationFrame(nextProgressRafId);
			nextProgressRafId = 0;
		}
	}

	// ── API helpers ────────────────────────────────────────────────────────────

	async function tryPresign(
		targetSlug: string,
		targetChapter: number,
		targetVoice: string
	): Promise<string | null> {
		const params = new URLSearchParams({
			slug: targetSlug,
			n: String(targetChapter),
			voice: targetVoice
		});
		const res = await fetch(`/api/presign/audio?${params}`);
		if (res.status === 404) return null;
		if (!res.ok) throw new Error(`presign HTTP ${res.status}`);
		const data = (await res.json()) as { url: string };
		return data.url;
	}

	type AudioStatusResponse =
		| { status: 'done'; url: string; filename: string }
		| { status: 'pending' | 'generating'; job_id: string }
		| { status: 'idle' }
		| { status: 'failed'; error?: string };

	/**
	 * Poll GET /api/audio/status/[slug]/[n]?voice=... every `intervalMs` ms
	 * until status is "done" or "failed" (or the caller cancels via signal).
	 *
	 * Returns the final status response, or throws on network error / cancellation.
	 */
	async function pollAudioStatus(
		targetSlug: string,
		targetChapter: number,
		targetVoice: string,
		intervalMs = 2000,
		signal?: AbortSignal
	): Promise<AudioStatusResponse> {
		const qs = new URLSearchParams();
		if (targetVoice) qs.set('voice', targetVoice);
		const url = `/api/audio/status/${targetSlug}/${targetChapter}?${qs.toString()}`;

		while (true) {
			if (signal?.aborted) throw new DOMException('Aborted', 'AbortError');

			const res = await fetch(url, { signal });
			if (!res.ok) throw new Error(`Status poll HTTP ${res.status}`);
			const data = (await res.json()) as AudioStatusResponse;

			if (data.status === 'done' || data.status === 'failed') {
				return data;
			}

			// Still pending/generating — wait then retry.
			await new Promise<void>((resolve, reject) => {
				const timer = setTimeout(resolve, intervalMs);
				signal?.addEventListener('abort', () => {
					clearTimeout(timer);
					reject(new DOMException('Aborted', 'AbortError'));
				});
			});
		}
	}

	// ── Pre-fetch next chapter ─────────────────────────────────────────────────

	async function prefetchNext() {
		if (nextChapter === null || nextChapter === undefined) return;
		if (audioStore.nextStatus !== 'none') return; // already running or done

		const voice = audioStore.voice;

		audioStore.nextStatus = 'prefetching';
		audioStore.nextChapterPrefetched = nextChapter;
		startNextProgress();

		try {
			// Fast path: already generated
			const url = await tryPresign(slug, nextChapter, voice);
			if (url) {
				stopNextProgress();
				audioStore.nextProgress = 100;
				audioStore.nextAudioUrl = url;
				audioStore.nextStatus = 'prefetched';
				return;
			}

			// Slow path: trigger Kokoro generation (non-blocking POST), then poll.
			const res = await fetch(`/api/audio/${slug}/${nextChapter}`, {
				method: 'POST',
				headers: { 'Content-Type': 'application/json' },
				body: JSON.stringify({ voice })
			});
			if (!res.ok) throw new Error(`Prefetch generation failed: HTTP ${res.status}`);

			// If the scraper returned cached audio immediately (200), use the url.
			if (res.status === 200) {
				const cached = (await res.json()) as { url: string };
				stopNextProgress();
				audioStore.nextProgress = 100;
				audioStore.nextAudioUrl = cached.url;
				audioStore.nextStatus = 'prefetched';
				return;
			}

			// 202: poll until done.
			const final = await pollAudioStatus(slug, nextChapter, voice);
			stopNextProgress();
			audioStore.nextProgress = 100;

			if (final.status === 'failed') {
				throw new Error(`Prefetch failed: ${(final as { error?: string }).error ?? 'unknown'}`);
			}

			// Use the URL from the status response, or fall back to presign.
			const doneUrl =
				(final as { url?: string }).url ?? (await tryPresign(slug, nextChapter, voice));
			if (!doneUrl) throw new Error('Prefetch: audio done but no URL available');

			audioStore.nextAudioUrl = doneUrl;
			audioStore.nextStatus = 'prefetched';
		} catch {
			stopNextProgress();
			audioStore.nextStatus = 'failed';
		}
	}

	// ── Media Session ──────────────────────────────────────────────────────────
	// Sets the OS-level media metadata so the book cover, title, and chapter
	// appear on the phone lock screen / notification center.

	function setMediaSession() {
		if (typeof navigator === 'undefined' || !('mediaSession' in navigator)) return;

		const artwork: MediaImage[] = cover
			? [
					{ src: cover, sizes: '512x512', type: 'image/jpeg' },
					{ src: cover, sizes: '256x256', type: 'image/jpeg' }
			  ]
			: [];

		navigator.mediaSession.metadata = new MediaMetadata({
			title: chapterTitle || `Chapter ${chapter}`,
			artist: bookTitle,
			album: bookTitle,
			artwork
		});
	}

	// ── Core play flow ─────────────────────────────────────────────────────────

	async function startPlayback() {
		const voice = audioStore.voice;

		// Populate store metadata so layout + mini-bar have track info.
		audioStore.slug = slug;
		audioStore.chapter = chapter;
		audioStore.chapterTitle = chapterTitle;
		audioStore.bookTitle = bookTitle;
		audioStore.cover = cover;
		audioStore.chapters = chapters;

		// Update OS media session (lock screen / notification center).
		setMediaSession();

		audioStore.status = 'loading';
		audioStore.errorMsg = '';

		try {
			// Fast path A: pre-fetch already landed for THIS chapter.
			if (
				audioStore.nextStatus === 'prefetched' &&
				audioStore.nextChapterPrefetched === chapter &&
				audioStore.nextAudioUrl
			) {
				const url = audioStore.nextAudioUrl;
				// Consume the pre-fetch — reset so it doesn't carry over
				audioStore.resetNextPrefetch();
				audioStore.audioUrl = url;
				audioStore.status = 'ready';
				// Don't restore saved time for auto-next; position is 0
				// Immediately start pre-generating the chapter after this one.
				maybeStartPrefetch();
				return;
			}

			// Fast path B: audio already in MinIO (presign check).
			const url = await tryPresign(slug, chapter, voice);
			if (url) {
				audioStore.audioUrl = url;
				audioStore.status = 'ready';
				// Restore last saved position after the audio element loads
				restoreSavedAudioTime();
				// Immediately start pre-generating the next chapter in background.
				maybeStartPrefetch();
				return;
			}

			// Slow path: trigger Kokoro generation (non-blocking POST), then poll.
			audioStore.status = 'generating';
			startProgress();

			const res = await fetch(`/api/audio/${slug}/${chapter}`, {
				method: 'POST',
				headers: { 'Content-Type': 'application/json' },
				body: JSON.stringify({ voice })
			});
			if (!res.ok) throw new Error(`Generation failed: HTTP ${res.status}`);

			// If the scraper returned cached audio immediately (200), use the url.
			if (res.status === 200) {
				const cached = (await res.json()) as { url: string };
				await finishProgress();
				audioStore.audioUrl = cached.url;
				audioStore.status = 'ready';
				maybeStartPrefetch();
				return;
			}

			// 202: poll until done.
			const final = await pollAudioStatus(slug, chapter, voice);

			if (final.status === 'failed') {
				throw new Error(
					`Generation failed: ${(final as { error?: string }).error ?? 'unknown error'}`
				);
			}

			await finishProgress();

			// Use the URL from the status response, or fall back to presign.
			const doneUrl =
				(final as { url?: string }).url ?? (await tryPresign(slug, chapter, voice));
			if (!doneUrl) throw new Error('Audio generated but no URL available');
			audioStore.audioUrl = doneUrl;
			audioStore.status = 'ready';
			// Don't restore time for freshly generated audio — position is 0
			// Immediately start pre-generating the next chapter in background.
			maybeStartPrefetch();
		} catch (e) {
			stopProgress();
			audioStore.progress = 0;
			audioStore.status = 'error';
			audioStore.errorMsg = String(e);
		}
	}

	/**
	 * Start pre-fetching the next chapter if autoNext is on, there is a next
	 * chapter, and no prefetch is already running or completed.
	 * Called as soon as current-chapter playback begins so that the next
	 * chapter's audio is ready before we need it (seamless transition).
	 * The 90%-mark $effect acts as a fallback for cases where autoNext is
	 * toggled on mid-playback.
	 */
	function maybeStartPrefetch() {
		if (
			audioStore.autoNext &&
			nextChapter !== null &&
			nextChapter !== undefined &&
			audioStore.nextStatus === 'none'
		) {
			prefetchNext();
		}
	}

	/**
	 * Fetch the saved audio time for this chapter and seek to it after a short
	 * delay (to allow the audio element to load the source).
	 */
	async function restoreSavedAudioTime() {
		try {
			const params = new URLSearchParams({ slug, chapter: String(chapter) });
			const res = await fetch(`/api/progress/audio-time?${params}`);
			if (!res.ok) return;
			const data = (await res.json()) as { audioTime: number | null };
			if (data.audioTime && data.audioTime > 5) {
				// Small delay to let the <audio> element fully load the src before seeking
				setTimeout(() => {
					audioStore.seekRequest = data.audioTime as number;
				}, 300);
			}
		} catch {
			// Non-critical — silently ignore
		}
	}

	async function handlePlay() {
		const isCurrent = audioStore.isCurrentChapter(slug, chapter);

		// Already loaded this chapter: toggle play/pause.
		if (isCurrent && audioStore.status === 'ready') {
			audioStore.toggleRequest = (audioStore.toggleRequest ?? 0) + 1;
			return;
		}

		// Not yet loaded — start the full flow.
		await startPlayback();
	}

	function formatTime(s: number): string {
		if (!isFinite(s) || s < 0) return '0:00';
		const m = Math.floor(s / 60);
		const sec = Math.floor(s % 60);
		return `${m}:${sec.toString().padStart(2, '0')}`;
	}
</script>

<svelte:window onkeydown={handleKeyDown} />

<div class="mt-6 p-4 rounded-lg bg-zinc-800 border border-zinc-700">
	<div class="flex items-center justify-between gap-2 mb-3">
		<div class="flex items-center gap-2">
			<svg class="w-4 h-4 text-amber-400" fill="currentColor" viewBox="0 0 24 24">
				<path d="M12 3v10.55A4 4 0 1014 17V7h4V3h-6z"/>
			</svg>
			<span class="text-sm text-zinc-300 font-medium">Audio Narration</span>
		</div>

		<!-- Voice selector button -->
		{#if voices.length > 0}
			<button
				onclick={() => { stopSample(); showVoicePanel = !showVoicePanel; }}
				class="flex items-center gap-1.5 px-2 py-1 rounded text-xs font-medium transition-colors {showVoicePanel
					? 'text-amber-400 bg-amber-400/15 hover:bg-amber-400/25'
					: 'text-zinc-400 bg-zinc-700 hover:bg-zinc-600 hover:text-zinc-200'}"
				title="Change voice"
			>
				<svg class="w-3.5 h-3.5" fill="currentColor" viewBox="0 0 24 24">
					<path d="M12 14c1.66 0 3-1.34 3-3V5c0-1.66-1.34-3-3-3S9 3.34 9 5v6c0 1.66 1.34 3 3 3zm-1-9c0-.55.45-1 1-1s1 .45 1 1v6c0 .55-.45 1-1 1s-1-.45-1-1V5zm6 6c0 2.76-2.24 5-5 5s-5-2.24-5-5H5c0 3.53 2.61 6.43 6 6.92V21h2v-3.08c3.39-.49 6-3.39 6-6.92h-2z"/>
				</svg>
				<span class="max-w-[80px] truncate">{voiceLabel(audioStore.voice)}</span>
				<svg class="w-3 h-3 flex-shrink-0 transition-transform {showVoicePanel ? 'rotate-180' : ''}" fill="currentColor" viewBox="0 0 24 24">
					<path d="M7 10l5 5 5-5z"/>
				</svg>
			</button>
		{/if}
	</div>

	<!-- ── Voice selector panel ──────────────────────────────────────────── -->
	{#if showVoicePanel && voices.length > 0}
		<div class="mb-3 rounded-lg border border-zinc-600 bg-zinc-900 overflow-hidden">
			<div class="px-3 py-2 border-b border-zinc-700 flex items-center justify-between">
				<span class="text-xs font-semibold text-zinc-400 uppercase tracking-wider">Choose Voice</span>
				<button
					onclick={() => { stopSample(); showVoicePanel = false; }}
					class="text-zinc-500 hover:text-zinc-300 transition-colors"
					aria-label="Close voice selector"
				>
					<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
						<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
					</svg>
				</button>
			</div>
			<div class="max-h-64 overflow-y-auto">
				{#each voices as v (v)}
					<div
						class="flex items-center gap-2 px-3 py-2 hover:bg-zinc-800 transition-colors cursor-pointer {audioStore.voice === v ? 'bg-amber-400/10' : ''}"
						role="button"
						tabindex="0"
						onclick={() => selectVoice(v)}
						onkeydown={(e) => e.key === 'Enter' && selectVoice(v)}
					>
						<!-- Selected indicator -->
						<div class="w-4 flex-shrink-0">
							{#if audioStore.voice === v}
								<svg class="w-3.5 h-3.5 text-amber-400" fill="currentColor" viewBox="0 0 24 24">
									<path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41L9 16.17z"/>
								</svg>
							{/if}
						</div>

						<!-- Voice name -->
						<span class="flex-1 text-xs {audioStore.voice === v ? 'text-amber-400 font-medium' : 'text-zinc-300'}">
							{voiceLabel(v)}
						</span>
						<span class="text-zinc-600 text-xs font-mono">{v}</span>

						<!-- Sample play button (stop propagation so click doesn't select) -->
						<button
							onclick={(e) => { e.stopPropagation(); playSample(v); }}
							class="p-1 rounded transition-colors flex-shrink-0 {samplePlayingVoice === v
								? 'text-amber-400 bg-amber-400/15 hover:bg-amber-400/25'
								: 'text-zinc-500 hover:text-zinc-200 hover:bg-zinc-700'}"
							title={samplePlayingVoice === v ? 'Stop sample' : 'Play sample'}
							aria-label={samplePlayingVoice === v ? `Stop ${v} sample` : `Play ${v} sample`}
						>
							{#if samplePlayingVoice === v}
								<!-- Stop icon -->
								<svg class="w-3.5 h-3.5" fill="currentColor" viewBox="0 0 24 24">
									<path d="M6 6h12v12H6z"/>
								</svg>
							{:else}
								<!-- Play icon -->
								<svg class="w-3.5 h-3.5" fill="currentColor" viewBox="0 0 24 24">
									<path d="M8 5v14l11-7z"/>
								</svg>
							{/if}
						</button>
					</div>
				{/each}
			</div>
			<div class="px-3 py-2 border-t border-zinc-700 bg-zinc-800/50">
				<p class="text-xs text-zinc-500">
					New voice applies on next "Play narration".
					{#if voices.length > 0}
						<a
							href="/api/audio/voice-samples"
							class="text-zinc-400 hover:text-amber-400 transition-colors underline"
							onclick={(e) => {
								e.preventDefault();
								fetch('/api/audio/voice-samples', { method: 'POST' }).catch(() => {});
							}}
						>Generate missing samples</a>
					{/if}
				</p>
			</div>
		</div>
	{/if}

	{#if audioStore.isCurrentChapter(slug, chapter)}
		<!-- ── This chapter is the active one ── -->

		{#if audioStore.status === 'idle' || audioStore.status === 'error'}
			<!-- Should not normally reach here while current, but handle gracefully -->
			{#if audioStore.status === 'error'}
				<p class="text-red-400 text-sm mb-2">{audioStore.errorMsg || 'Failed to load audio.'}</p>
			{/if}
			<button
				onclick={handlePlay}
				class="px-4 py-1.5 rounded bg-amber-400 text-zinc-900 text-sm font-semibold hover:bg-amber-300 transition-colors flex items-center gap-2"
			>
				<svg class="w-3.5 h-3.5 ml-0.5" fill="currentColor" viewBox="0 0 24 24">
					<path d="M8 5v14l11-7z"/>
				</svg>
				Play narration
			</button>

		{:else if audioStore.status === 'loading'}
			<button
				disabled
				class="px-4 py-1.5 rounded bg-amber-400 text-zinc-900 text-sm font-semibold opacity-50 cursor-not-allowed flex items-center gap-2"
			>
				<svg class="w-3.5 h-3.5 animate-spin" fill="none" viewBox="0 0 24 24">
					<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
					<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
				</svg>
				Loading…
			</button>

		{:else if audioStore.status === 'generating'}
			<div class="space-y-2">
				<p class="text-xs text-zinc-400">Generating narration…</p>
				<div class="w-full h-1.5 bg-zinc-700 rounded-full overflow-hidden">
					<div
						class="h-full bg-amber-400 rounded-full transition-none"
						style="width: {audioStore.progress}%"
					></div>
				</div>
				<p class="text-xs text-zinc-500 tabular-nums">{Math.round(audioStore.progress)}%</p>
			</div>

	{:else if audioStore.status === 'ready'}
		<!-- Mini-bar is the canonical control surface — show a compact indicator here -->
		<div class="flex items-center justify-between gap-3">
			<div class="flex items-center gap-2 text-xs text-zinc-400">
				{#if audioStore.isPlaying}
					<svg class="w-3.5 h-3.5 text-amber-400 flex-shrink-0" fill="currentColor" viewBox="0 0 24 24">
						<path d="M6 4h4v16H6V4zm8 0h4v16h-4V4z"/>
					</svg>
					<span>Playing — controls below</span>
				{:else}
					<svg class="w-3.5 h-3.5 flex-shrink-0 ml-0.5" fill="currentColor" viewBox="0 0 24 24">
						<path d="M8 5v14l11-7z"/>
					</svg>
					<span>Paused — controls below</span>
				{/if}
				<span class="tabular-nums text-zinc-500">
					{formatTime(audioStore.currentTime)} / {formatTime(audioStore.duration)}
				</span>
			</div>

			<!-- Auto-next toggle (keep here as useful context) -->
			{#if nextChapter !== null && nextChapter !== undefined}
				<button
					onclick={() => (audioStore.autoNext = !audioStore.autoNext)}
					class="flex items-center gap-1.5 px-2 py-1 rounded text-xs font-medium transition-colors flex-shrink-0 {audioStore.autoNext
						? 'text-amber-400 bg-amber-400/15 hover:bg-amber-400/25'
						: 'text-zinc-500 bg-zinc-700 hover:bg-zinc-600 hover:text-zinc-200'}"
					title={audioStore.autoNext ? `Auto-next on — will play Ch.${nextChapter} automatically` : 'Auto-next off'}
					aria-pressed={audioStore.autoNext}
				>
					<svg class="w-3.5 h-3.5" fill="currentColor" viewBox="0 0 24 24">
						<path d="M6 18l8.5-6L6 6v12zm8.5-6L23 6v12l-8.5-6z"/>
					</svg>
					Auto
				</button>
			{/if}
		</div>

		<!-- Next chapter pre-fetch status (only when auto-next is on) -->
		{#if audioStore.autoNext && nextChapter !== null && nextChapter !== undefined}
			<div class="mt-2">
				{#if audioStore.nextStatus === 'prefetching'}
					<div class="flex items-center gap-2 text-xs text-zinc-500">
						<svg class="w-3 h-3 animate-spin flex-shrink-0" fill="none" viewBox="0 0 24 24">
							<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
							<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
						</svg>
						<span>Preparing Ch.{nextChapter}… {Math.round(audioStore.nextProgress)}%</span>
					</div>
				{:else if audioStore.nextStatus === 'prefetched'}
					<p class="text-xs text-zinc-500 flex items-center gap-1">
						<svg class="w-3 h-3 text-amber-400 flex-shrink-0" fill="currentColor" viewBox="0 0 24 24">
							<path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41L9 16.17z"/>
						</svg>
						Ch.{nextChapter} ready
					</p>
				{:else if audioStore.nextStatus === 'failed'}
					<p class="text-xs text-zinc-600">Ch.{nextChapter} will generate on navigate</p>
				{/if}
			</div>
		{/if}
	{/if}

	{:else if audioStore.active}
		<!-- ── A different chapter is currently playing ── -->
		<div class="flex items-center justify-between gap-3">
			<p class="text-xs text-zinc-400">
				Now playing: {audioStore.chapterTitle || `Ch.${audioStore.chapter}`}
			</p>
			<button
				onclick={startPlayback}
				class="px-3 py-1 rounded bg-zinc-700 text-zinc-200 text-xs font-medium hover:bg-zinc-600 transition-colors flex-shrink-0"
			>
				Load this chapter
			</button>
		</div>

	{:else}
		<!-- ── Idle — nothing playing ── -->
		<button
			onclick={handlePlay}
			class="px-4 py-1.5 rounded bg-amber-400 text-zinc-900 text-sm font-semibold hover:bg-amber-300 transition-colors flex items-center gap-2"
		>
			<svg class="w-3.5 h-3.5 ml-0.5" fill="currentColor" viewBox="0 0 24 24">
				<path d="M8 5v14l11-7z"/>
			</svg>
			Play narration
		</button>
	{/if}
</div>
