<script lang="ts">
	import { invalidateAll } from '$app/navigation';
	import type { PageData } from './$types';
	import type { AIJob } from '$lib/server/pocketbase';
	import { cn } from '$lib/utils';
	import * as m from '$lib/paraglide/messages.js';

	let { data }: { data: PageData } = $props();

	let jobs = $state<AIJob[]>([]);

	// Resolve streamed promise on load and on server reloads (invalidateAll)
	$effect(() => {
		data.jobs.then((resolved) => { jobs = resolved; });
	});

	// ── Live-poll while any job is in-flight ─────────────────────────────────────
	let hasInFlight = $derived(jobs.some((j) => j.status === 'pending' || j.status === 'running'));

	$effect(() => {
		if (!hasInFlight) return;
		const id = setInterval(() => {
			invalidateAll();
		}, 3000);
		return () => clearInterval(id);
	});

	// ── Filter / search ───────────────────────────────────────────────────────────
	let query = $state('');
	let statusFilter = $state<string>('all');

	const STATUS_OPTIONS = ['all', 'pending', 'running', 'done', 'failed', 'cancelled'] as const;

	let filteredJobs = $derived(
		jobs.filter((j) => {
			const q = query.trim().toLowerCase();
			const matchesQ =
				!q ||
				j.slug.toLowerCase().includes(q) ||
				j.kind.toLowerCase().includes(q) ||
				j.model.toLowerCase().includes(q) ||
				(j.error_message ?? '').toLowerCase().includes(q);
			const matchesStatus = statusFilter === 'all' || j.status === statusFilter;
			return matchesQ && matchesStatus;
		})
	);

	// ── Stats ────────────────────────────────────────────────────────────────────
	let stats = $derived({
		total: jobs.length,
		running: jobs.filter((j) => j.status === 'running').length,
		pending: jobs.filter((j) => j.status === 'pending').length,
		done: jobs.filter((j) => j.status === 'done').length,
		failed: jobs.filter((j) => j.status === 'failed').length
	});

	// ── Cancel ────────────────────────────────────────────────────────────────────
	let cancellingIds = $state(new Set<string>());
	let cancelErrors: Record<string, string> = $state({});

	async function cancelJob(id: string) {
		if (cancellingIds.has(id)) return;
		cancellingIds = new Set([...cancellingIds, id]);
		delete cancelErrors[id];
		try {
			const res = await fetch(`/api/admin/ai-jobs/${id}/cancel`, { method: 'POST' });
			if (!res.ok) {
				const body = await res.json().catch(() => ({}));
				cancelErrors = { ...cancelErrors, [id]: body.error ?? `Error ${res.status}` };
			} else {
				jobs = jobs.map((j) => j.id === id ? { ...j, status: 'cancelled' as AIJob['status'] } : j);
			}
		} catch {
			cancelErrors = { ...cancelErrors, [id]: 'Network error.' };
		} finally {
			cancellingIds = new Set([...cancellingIds].filter((x) => x !== id));
		}
	}

	// ── Review & Apply (chapter-names jobs) ──────────────────────────────────────

	interface ProposedTitle {
		number: number;
		old_title: string;
		new_title: string;
	}

	interface ChapterNamesReview {
		kind: 'chapter-names';
		jobId: string;
		slug: string;
		pattern: string;
		titles: ProposedTitle[];
		loading: boolean;
		error: string;
		applying: boolean;
		applyError: string;
		applyDone: boolean;
	}

	// ── Review (image-gen jobs) ───────────────────────────────────────────────────

	interface ImageGenReview {
		kind: 'image-gen';
		jobId: string;
		slug: string;
		imageType: string;
		chapter: number;
		prompt: string;
		imageSrc: string;
		contentType: string;
		bytes: number;
		loading: boolean;
		error: string;
		saving: boolean;
		saveError: string;
		savedUrl: string;
	}

	// ── Review (description jobs) ─────────────────────────────────────────────────

	interface DescriptionReview {
		kind: 'description';
		jobId: string;
		slug: string;
		instructions: string;
		oldDescription: string;
		newDescription: string;
		loading: boolean;
		error: string;
		applying: boolean;
		applyError: string;
		applyDone: boolean;
	}

	type ReviewState = ChapterNamesReview | ImageGenReview | DescriptionReview;

	let review = $state<ReviewState | null>(null);

	// ── Open review ───────────────────────────────────────────────────────────────

	async function openReview(job: AIJob) {
		if (job.kind === 'chapter-names') {
			const r: ChapterNamesReview = {
				kind: 'chapter-names',
				jobId: job.id,
				slug: job.slug,
				pattern: '',
				titles: [],
				loading: true,
				error: '',
				applying: false,
				applyError: '',
				applyDone: false
			};
			review = r;

		try {
			const res = await fetch(`/api/admin/ai-jobs/${job.id}`);
			if (!res.ok) throw new Error(`HTTP ${res.status}`);
			const data = await res.json();

			let payload: { pattern?: string; slug?: string; results?: ProposedTitle[] } = {};
			try { payload = JSON.parse(data.payload ?? '{}'); } catch { /* ignore */ }

			review = {
				...r,
				pattern: payload.pattern ?? '',
				titles: (payload.results ?? []).map((t: ProposedTitle) => ({ ...t })),
				loading: false
			};
		} catch (e) {
			review = { ...r, loading: false, error: String(e) };
		}
	} else if (job.kind === 'image-gen') {
			const r: ImageGenReview = {
				kind: 'image-gen',
				jobId: job.id,
				slug: job.slug,
				imageType: '',
				chapter: 0,
				prompt: '',
				imageSrc: '',
				contentType: 'image/png',
				bytes: 0,
				loading: true,
				error: '',
				saving: false,
				saveError: '',
				savedUrl: ''
			};
			review = r;

		try {
			const res = await fetch(`/api/admin/ai-jobs/${job.id}`);
			if (!res.ok) throw new Error(`HTTP ${res.status}`);
			const data = await res.json();

			let payload: {
				prompt?: string;
				type?: string;
				chapter?: number;
				content_type?: string;
				image_b64?: string;
				bytes?: number;
			} = {};
			try { payload = JSON.parse(data.payload ?? '{}'); } catch { /* ignore */ }

			if (!payload.image_b64) {
				review = { ...r, error: 'No image in job payload.', loading: false };
				return;
			}
			const contentType = payload.content_type ?? 'image/png';
			review = {
				...r,
				imageType: payload.type ?? 'cover',
				chapter: payload.chapter ?? 0,
				prompt: payload.prompt ?? '',
				contentType,
				bytes: payload.bytes ?? 0,
				imageSrc: `data:${contentType};base64,${payload.image_b64}`,
				loading: false
			};
		} catch (e) {
			review = { ...r, loading: false, error: String(e) };
		}
	} else if (job.kind === 'description') {
			const r: DescriptionReview = {
				kind: 'description',
				jobId: job.id,
				slug: job.slug,
				instructions: '',
				oldDescription: '',
				newDescription: '',
				loading: true,
				error: '',
				applying: false,
				applyError: '',
				applyDone: false
			};
			review = r;

		try {
			const res = await fetch(`/api/admin/ai-jobs/${job.id}`);
			if (!res.ok) throw new Error(`HTTP ${res.status}`);
			const data = await res.json();

			let payload: {
				instructions?: string;
				old_description?: string;
				new_description?: string;
			} = {};
			try { payload = JSON.parse(data.payload ?? '{}'); } catch { /* ignore */ }

			review = {
				...r,
				instructions: payload.instructions ?? '',
				oldDescription: payload.old_description ?? '',
				newDescription: payload.new_description ?? '',
				loading: false
			};
		} catch (e) {
			review = { ...r, loading: false, error: String(e) };
		}
	}
}

	function closeReview() {
		review = null;
	}

	// ── Apply chapter names ───────────────────────────────────────────────────────

	async function applyChapterNames() {
		if (review?.kind !== 'chapter-names' || review.applying) return;
		review.applying = true;
		review.applyError = '';
		review.applyDone = false;

		const chapters = review.titles.map((t) => ({ number: t.number, title: t.new_title }));
		try {
			const res = await fetch('/api/admin/text-gen/chapter-names/apply', {
				method: 'POST',
				headers: { 'content-type': 'application/json' },
				body: JSON.stringify({ slug: review.slug, chapters })
			});
			if (!res.ok) {
				const body = await res.json().catch(() => ({}));
				review.applyError = body.error ?? `Error ${res.status}`;
			} else {
				review.applyDone = true;
			}
		} catch {
			review.applyError = 'Network error.';
		} finally {
			review.applying = false;
		}
	}

	// ── Save image as cover ───────────────────────────────────────────────────────

	async function saveImageAsCover() {
		if (review?.kind !== 'image-gen' || review.saving) return;
		review.saving = true;
		review.saveError = '';

		const b64 = review.imageSrc.split(',')[1];
		try {
			const res = await fetch('/api/admin/image-gen/save-cover', {
				method: 'POST',
				headers: { 'content-type': 'application/json' },
				body: JSON.stringify({ slug: review.slug, image_b64: b64 })
			});
			const body = await res.json().catch(() => ({}));
			if (!res.ok) {
				review.saveError = body.error ?? `Error ${res.status}`;
			} else {
				review.savedUrl = body.cover_url ?? `/api/cover/novelfire.net/${review.slug}`;
			}
		} catch {
			review.saveError = 'Network error.';
		} finally {
			review.saving = false;
		}
	}

	// ── Save image as chapter illustration ───────────────────────────────────────

	async function saveImageAsChapterImage() {
		if (review?.kind !== 'image-gen' || review.saving) return;
		review.saving = true;
		review.saveError = '';

		const b64 = review.imageSrc.split(',')[1];
		try {
			const res = await fetch('/api/admin/image-gen/save-chapter-image', {
				method: 'POST',
				headers: { 'content-type': 'application/json' },
				body: JSON.stringify({ slug: review.slug, chapter: review.chapter, image_b64: b64 })
			});
			const body = await res.json().catch(() => ({}));
			if (!res.ok) {
				review.saveError = body.error ?? `Error ${res.status}`;
			} else {
				review.savedUrl = body.image_url ?? `/api/chapter-image/novelfire.net/${review.slug}/${review.chapter}`;
			}
		} catch {
			review.saveError = 'Network error.';
		} finally {
			review.saving = false;
		}
	}

	function downloadImage() {
		if (review?.kind !== 'image-gen') return;
		const a = document.createElement('a');
		a.href = review.imageSrc;
		const ext = review.contentType === 'image/jpeg' ? 'jpg' : 'png';
		a.download = `${review.slug}-${review.imageType}.${ext}`;
		a.click();
	}

	// ── Apply description ─────────────────────────────────────────────────────────

	async function applyDescription() {
		if (review?.kind !== 'description' || review.applying) return;
		review.applying = true;
		review.applyError = '';
		review.applyDone = false;

		try {
			const res = await fetch('/api/admin/text-gen/description/apply', {
				method: 'POST',
				headers: { 'content-type': 'application/json' },
				body: JSON.stringify({ slug: review.slug, description: review.newDescription })
			});
			if (!res.ok) {
				const body = await res.json().catch(() => ({}));
				review.applyError = body.error ?? `Error ${res.status}`;
			} else {
				review.applyDone = true;
			}
		} catch {
			review.applyError = 'Network error.';
		} finally {
			review.applying = false;
		}
	}

	// ── Helpers ───────────────────────────────────────────────────────────────────
	function statusBg(status: string) {
		if (status === 'done') return 'bg-green-400/10 text-green-400';
		if (status === 'running') return 'bg-(--color-brand)/10 text-(--color-brand)';
		if (status === 'pending') return 'bg-sky-400/10 text-sky-400';
		if (status === 'failed') return 'bg-(--color-danger)/10 text-(--color-danger)';
		if (status === 'cancelled') return 'bg-zinc-700/50 text-(--color-muted)';
		return 'bg-zinc-700/50 text-(--color-text)';
	}

	function kindLabel(kind: string) {
		const labels: Record<string, string> = {
			'chapter-names': 'Chapter Names',
			'image-gen': 'Image Gen',
			'description': 'Description',
			'batch-covers': 'Batch Covers',
			'chapter-covers': 'Chapter Covers',
			'refresh-metadata': 'Refresh Metadata'
		};
		return labels[kind] ?? kind;
	}

	function fmtDate(s: string | undefined) {
		if (!s) return '—';
		return new Date(s).toLocaleString(undefined, {
			month: 'short',
			day: 'numeric',
			hour: '2-digit',
			minute: '2-digit'
		});
	}

	function duration(started: string | undefined, finished: string | undefined) {
		if (!started || !finished) return '—';
		const ms = new Date(finished).getTime() - new Date(started).getTime();
		if (ms < 0) return '—';
		const s = Math.floor(ms / 1000);
		if (s < 60) return `${s}s`;
		const m = Math.floor(s / 60);
		return `${m}m ${s % 60}s`;
	}

	function progress(job: AIJob) {
		if (!job.items_total) return null;
		return Math.round((job.items_done / job.items_total) * 100);
	}

	function fmtBytes(b: number) {
		if (b < 1024) return `${b} B`;
		if (b < 1024 * 1024) return `${(b / 1024).toFixed(1)} KB`;
		return `${(b / 1024 / 1024).toFixed(2)} MB`;
	}

	const REVIEWABLE_KINDS = new Set(['chapter-names', 'image-gen', 'description']);
</script>

<svelte:head>
	<title>{m.admin_ai_jobs_page_title()}</title>
</svelte:head>

<div class="max-w-6xl mx-auto space-y-6">
	<!-- Header -->
	<div class="flex items-center justify-between gap-4">
		<div>
			<h1 class="text-2xl font-bold text-(--color-text)">{m.admin_ai_jobs_heading()}</h1>
			<p class="text-sm text-(--color-muted) mt-0.5">{m.admin_ai_jobs_subheading()}</p>
		</div>
		<button
			onclick={() => invalidateAll()}
			class="flex items-center gap-1.5 px-3 py-1.5 rounded-md text-sm text-(--color-muted) hover:text-(--color-text) hover:bg-(--color-surface-2) transition-colors"
		>
			<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="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
			</svg>
			Refresh
		</button>
	</div>

	<!-- Stats bar -->
	<div class="grid grid-cols-2 sm:grid-cols-5 gap-3">
		{#each [
			{ label: 'Total', value: stats.total, color: 'text-(--color-text)' },
			{ label: 'Running', value: stats.running, color: 'text-(--color-brand)' },
			{ label: 'Pending', value: stats.pending, color: 'text-sky-400' },
			{ label: 'Done', value: stats.done, color: 'text-green-400' },
			{ label: 'Failed', value: stats.failed, color: 'text-(--color-danger)' }
		] as stat}
			<div class="rounded-lg border border-(--color-border) bg-(--color-surface-2) px-4 py-3">
				<p class="text-xs text-(--color-muted) mb-1">{stat.label}</p>
				<p class={cn('text-xl font-bold tabular-nums', stat.color)}>{stat.value}</p>
			</div>
		{/each}
	</div>

	<!-- Filters -->
	<div class="flex flex-wrap gap-3 items-center">
		<input
			type="search"
			placeholder="Search slug, kind, model…"
			bind:value={query}
			class="flex-1 min-w-48 px-3 py-1.5 rounded-md bg-(--color-surface-2) border border-(--color-border) text-sm text-(--color-text) placeholder:text-(--color-muted) focus:outline-none focus:ring-1 focus:ring-(--color-brand)"
		/>
		<div class="flex gap-1 flex-wrap">
			{#each STATUS_OPTIONS as s}
				<button
					onclick={() => (statusFilter = s)}
					class={cn(
						'px-2.5 py-1 rounded-md text-xs font-medium transition-colors capitalize',
						statusFilter === s
							? 'bg-(--color-brand) text-black'
							: 'bg-(--color-surface-2) text-(--color-muted) hover:text-(--color-text)'
					)}
				>
					{s}
				</button>
			{/each}
		</div>
	</div>

	<!-- Jobs table -->
	{#if filteredJobs.length === 0}
		<div class="rounded-lg border border-(--color-border) bg-(--color-surface-2) px-6 py-12 text-center">
			<p class="text-(--color-muted) text-sm">
				{jobs.length === 0 ? 'No AI jobs found.' : 'No jobs match your filters.'}
			</p>
		</div>
	{:else}
		<!-- Desktop table -->
		<div class="hidden sm:block rounded-lg border border-(--color-border) overflow-hidden">
			<table class="w-full text-sm">
				<thead>
					<tr class="border-b border-(--color-border) bg-(--color-surface-2)">
						<th class="px-4 py-2.5 text-left text-xs font-semibold text-(--color-muted) uppercase tracking-wider">Status</th>
						<th class="px-4 py-2.5 text-left text-xs font-semibold text-(--color-muted) uppercase tracking-wider">Kind</th>
						<th class="px-4 py-2.5 text-left text-xs font-semibold text-(--color-muted) uppercase tracking-wider">Slug</th>
						<th class="px-4 py-2.5 text-left text-xs font-semibold text-(--color-muted) uppercase tracking-wider hidden sm:table-cell">Model</th>
						<th class="px-4 py-2.5 text-left text-xs font-semibold text-(--color-muted) uppercase tracking-wider hidden md:table-cell">Progress</th>
						<th class="px-4 py-2.5 text-left text-xs font-semibold text-(--color-muted) uppercase tracking-wider hidden lg:table-cell">Started</th>
						<th class="px-4 py-2.5 text-left text-xs font-semibold text-(--color-muted) uppercase tracking-wider hidden lg:table-cell">Duration</th>
						<th class="px-4 py-2.5 text-right text-xs font-semibold text-(--color-muted) uppercase tracking-wider">Actions</th>
					</tr>
				</thead>
				<tbody class="divide-y divide-(--color-border)">
					{#each filteredJobs as job (job.id)}
						<tr class="bg-(--color-surface) hover:bg-(--color-surface-2)/40 transition-colors">
							<!-- Status badge -->
							<td class="px-4 py-3">
								<span class={cn('inline-flex items-center px-2 py-0.5 rounded text-xs font-medium', statusBg(job.status))}>
									{job.status}
								</span>
							</td>

							<!-- Kind -->
							<td class="px-4 py-3 font-medium text-(--color-text)">
								{kindLabel(job.kind)}
							</td>

						<!-- Slug -->
						<td class="px-4 py-3 max-w-[12rem]">
							{#if job.slug}
								<div class="flex flex-col gap-0.5">
									<a
										href="/books/{job.slug}"
										class="text-(--color-brand) hover:underline truncate block font-mono text-xs"
									>
										{job.slug}
									</a>
									{#if job.kind === 'image-gen'}
										<a href="/admin/image-gen?slug={job.slug}" class="text-[10px] text-(--color-muted) hover:text-(--color-text) transition-colors">↗ image editor</a>
									{:else if job.kind === 'chapter-names' || job.kind === 'description'}
										<a href="/admin/text-gen" class="text-[10px] text-(--color-muted) hover:text-(--color-text) transition-colors">↗ text editor</a>
									{/if}
								</div>
							{:else}
								<span class="text-(--color-muted) text-xs">—</span>
							{/if}
						</td>

							<!-- Model -->
							<td class="px-4 py-3 hidden sm:table-cell">
								<span class="font-mono text-xs text-(--color-muted) truncate block max-w-[10rem]" title={job.model}>
									{job.model || '—'}
								</span>
							</td>

							<!-- Progress -->
							<td class="px-4 py-3 hidden md:table-cell">
								{#if job.items_total > 0}
									{@const pct = progress(job)}
									<div class="flex items-center gap-2">
										<div class="w-20 h-1.5 rounded-full bg-(--color-border) overflow-hidden">
											<div
												class={cn(
													'h-full rounded-full transition-all',
													job.status === 'done' ? 'bg-green-400' :
													job.status === 'failed' ? 'bg-(--color-danger)' :
													'bg-(--color-brand)'
												)}
												style="width: {pct}%"
											></div>
										</div>
										<span class="text-xs text-(--color-muted) tabular-nums whitespace-nowrap">
											{job.items_done}/{job.items_total}
										</span>
									</div>
								{:else}
									<span class="text-(--color-muted) text-xs">—</span>
								{/if}
							</td>

							<!-- Started -->
							<td class="px-4 py-3 hidden lg:table-cell">
								<span class="text-xs text-(--color-muted) whitespace-nowrap">{fmtDate(job.started)}</span>
							</td>

							<!-- Duration -->
							<td class="px-4 py-3 hidden lg:table-cell">
								<span class="text-xs text-(--color-muted) whitespace-nowrap tabular-nums">
									{duration(job.started, job.finished)}
								</span>
							</td>

							<!-- Actions -->
							<td class="px-4 py-3 text-right">
								<div class="flex items-center justify-end gap-2">
								{#if job.status === 'pending' || job.status === 'running'}
										<button
											onclick={() => cancelJob(job.id)}
											disabled={cancellingIds.has(job.id)}
											class="px-2 py-1 rounded text-xs font-medium bg-(--color-danger)/10 text-(--color-danger) hover:bg-(--color-danger)/20 disabled:opacity-50 transition-colors"
										>
											{cancellingIds.has(job.id) ? 'Cancelling…' : 'Cancel'}
										</button>
									{/if}
									{#if REVIEWABLE_KINDS.has(job.kind) && job.status === 'done'}
										<button
											onclick={() => openReview(job)}
											class="px-2 py-1 rounded text-xs font-medium bg-green-400/10 text-green-400 hover:bg-green-400/20 transition-colors"
										>
											Review
										</button>
									{/if}
									{#if job.error_message}
										<span
											class="text-xs text-(--color-danger) max-w-[12rem] truncate"
											title={job.error_message}
										>
											{job.error_message}
										</span>
									{/if}
									{#if cancelErrors[job.id]}
										<span class="text-xs text-(--color-danger)" title={cancelErrors[job.id]}>
											{cancelErrors[job.id]}
										</span>
									{/if}
								</div>
							</td>
						</tr>
					{/each}
				</tbody>
			</table>
		</div>

		<!-- Mobile cards -->
		<div class="sm:hidden space-y-3">
			{#each filteredJobs as job (job.id)}
				<div class="bg-(--color-surface) rounded-xl border border-(--color-border) p-4 space-y-3">
					<!-- Row 1: status + kind -->
					<div class="flex items-center justify-between gap-2">
						<span class={cn('inline-flex items-center px-2 py-0.5 rounded text-xs font-medium', statusBg(job.status))}>
							{job.status}
						</span>
						<span class="text-sm font-medium text-(--color-text)">{kindLabel(job.kind)}</span>
					</div>
					<!-- Slug + links -->
					{#if job.slug}
						<div class="flex flex-col gap-0.5">
							<a href="/books/{job.slug}" class="text-(--color-brand) hover:underline font-mono text-xs truncate">
								{job.slug}
							</a>
							{#if job.kind === 'image-gen'}
								<a href="/admin/image-gen?slug={job.slug}" class="text-[10px] text-(--color-muted) hover:text-(--color-text) transition-colors">↗ image editor</a>
							{:else if job.kind === 'chapter-names' || job.kind === 'description'}
								<a href="/admin/text-gen" class="text-[10px] text-(--color-muted) hover:text-(--color-text) transition-colors">↗ text editor</a>
							{/if}
						</div>
					{/if}
					<!-- Meta grid -->
					<div class="grid grid-cols-2 gap-1 text-xs">
						<span class="text-(--color-muted)">Model</span>
						<span class="text-(--color-muted) font-mono text-right truncate" title={job.model}>{job.model || '—'}</span>
						{#if job.items_total > 0}
							<span class="text-(--color-muted)">Progress</span>
							<span class="text-(--color-muted) text-right tabular-nums">{job.items_done}/{job.items_total}</span>
						{/if}
						<span class="text-(--color-muted)">Started</span>
						<span class="text-(--color-muted) text-right">{fmtDate(job.started)}</span>
						<span class="text-(--color-muted)">Duration</span>
						<span class="text-(--color-muted) text-right tabular-nums">{duration(job.started, job.finished)}</span>
					</div>
					<!-- Actions -->
					<div class="flex items-center gap-2 flex-wrap">
						{#if job.status === 'pending' || job.status === 'running'}
							<button
								onclick={() => cancelJob(job.id)}
								disabled={cancellingIds.has(job.id)}
								class="px-2.5 py-1 rounded text-xs font-medium bg-(--color-danger)/10 text-(--color-danger) hover:bg-(--color-danger)/20 disabled:opacity-50 transition-colors"
							>
								{cancellingIds.has(job.id) ? 'Cancelling…' : 'Cancel'}
							</button>
						{/if}
						{#if REVIEWABLE_KINDS.has(job.kind) && job.status === 'done'}
							<button
								onclick={() => openReview(job)}
								class="px-2.5 py-1 rounded text-xs font-medium bg-green-400/10 text-green-400 hover:bg-green-400/20 transition-colors"
							>
								Review
							</button>
						{/if}
					</div>
					{#if job.error_message}
						<p class="text-xs text-(--color-danger) font-mono break-all">{job.error_message}</p>
					{/if}
					{#if cancelErrors[job.id]}
						<p class="text-xs text-(--color-danger)">{cancelErrors[job.id]}</p>
					{/if}
				</div>
			{/each}
		</div>

		<p class="text-xs text-(--color-muted)">
			Showing {filteredJobs.length} of {jobs.length} jobs
		</p>
	{/if}
</div>

<!-- ── Review panel (shared backdrop + modal shell) ─────────────────────────── -->
{#if review}
	<!-- Backdrop -->
	<div
		class="fixed inset-0 z-40 bg-black/60 backdrop-blur-sm"
		onclick={closeReview}
		role="presentation"
	></div>

	<!-- Modal -->
	<div class="fixed inset-x-0 bottom-0 z-50 max-h-[90vh] overflow-hidden flex flex-col rounded-t-2xl bg-(--color-surface) border-t border-(--color-border) shadow-2xl sm:inset-auto sm:top-1/2 sm:left-1/2 sm:-translate-x-1/2 sm:-translate-y-1/2 sm:w-[min(960px,95vw)] sm:max-h-[88vh] sm:rounded-xl sm:border sm:shadow-2xl">

		<!-- ── Chapter names review ─────────────────────────────────────────────── -->
		{#if review.kind === 'chapter-names'}
			<!-- Header -->
			<div class="flex items-center justify-between gap-4 px-5 py-4 border-b border-(--color-border) shrink-0">
				<div>
					<h2 class="text-base font-semibold text-(--color-text)">Review Chapter Names</h2>
					<p class="text-xs text-(--color-muted) mt-0.5">
						<span class="font-mono">{review.slug}</span>
						{#if review.pattern}
							· pattern: <span class="font-mono">{review.pattern}</span>
						{/if}
					</p>
				</div>
				<button onclick={closeReview} class="p-1.5 rounded-md text-(--color-muted) hover:text-(--color-text) hover:bg-(--color-surface-2) transition-colors" aria-label="Close">
					<svg class="w-4 h-4" 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>

			<!-- Body -->
			<div class="flex-1 overflow-y-auto">
				{#if review.loading}
					<div class="flex items-center justify-center py-16 text-(--color-muted) text-sm">Loading results…</div>
				{:else if review.error}
					<div class="px-5 py-8 text-center"><p class="text-(--color-danger) text-sm">{review.error}</p></div>
				{:else if review.titles.length === 0}
					<div class="px-5 py-8 text-center"><p class="text-(--color-muted) text-sm">No results found in this job's payload.</p></div>
				{:else}
					<table class="w-full text-sm">
						<thead class="sticky top-0 bg-(--color-surface) border-b border-(--color-border)">
							<tr>
								<th class="px-4 py-2.5 text-left text-xs font-semibold text-(--color-muted) uppercase tracking-wider w-16">#</th>
								<th class="px-4 py-2.5 text-left text-xs font-semibold text-(--color-muted) uppercase tracking-wider w-1/2">Old Title</th>
								<th class="px-4 py-2.5 text-left text-xs font-semibold text-(--color-muted) uppercase tracking-wider">New Title (editable)</th>
							</tr>
						</thead>
						<tbody class="divide-y divide-(--color-border)">
							{#each review.titles as title (title.number)}
								<tr class="bg-(--color-surface) hover:bg-(--color-surface-2)/40 transition-colors">
									<td class="px-4 py-2 text-xs text-(--color-muted) tabular-nums">{title.number}</td>
									<td class="px-4 py-2 text-xs text-(--color-muted) max-w-0">
										<span class="block truncate" title={title.old_title}>{title.old_title || '—'}</span>
									</td>
									<td class="px-4 py-2">
										<input
											type="text"
											bind:value={title.new_title}
											class="w-full px-2 py-1 rounded bg-(--color-surface-2) border border-(--color-border) text-xs text-(--color-text) focus:outline-none focus:ring-1 focus:ring-(--color-brand)"
										/>
									</td>
								</tr>
							{/each}
						</tbody>
					</table>
				{/if}
			</div>

			<!-- Footer -->
			{#if !review.loading && !review.error && review.titles.length > 0}
				<div class="px-5 py-4 border-t border-(--color-border) shrink-0 flex items-center justify-between gap-4">
					<div class="text-xs text-(--color-muted)">{review.titles.length} chapters</div>
					<div class="flex items-center gap-3">
						{#if review.applyError}<p class="text-xs text-(--color-danger)">{review.applyError}</p>{/if}
						{#if review.applyDone}<p class="text-xs text-green-400">Applied successfully.</p>{/if}
						<button onclick={closeReview} class="px-3 py-1.5 rounded-md text-sm text-(--color-muted) hover:text-(--color-text) hover:bg-(--color-surface-2) transition-colors">Close</button>
						<button
							onclick={applyChapterNames}
							disabled={review.applying || review.applyDone}
							class="px-4 py-1.5 rounded-md text-sm font-medium bg-(--color-brand) text-black hover:bg-amber-300 disabled:opacity-50 transition-colors"
						>
							{review.applying ? 'Applying…' : review.applyDone ? 'Applied' : 'Apply All'}
						</button>
					</div>
				</div>
			{/if}

		<!-- ── Image-gen review ─────────────────────────────────────────────────── -->
		{:else if review.kind === 'image-gen'}
			<!-- Header -->
			<div class="flex items-center justify-between gap-4 px-5 py-4 border-b border-(--color-border) shrink-0">
				<div>
					<h2 class="text-base font-semibold text-(--color-text)">Review Generated Image</h2>
					<p class="text-xs text-(--color-muted) mt-0.5">
						<span class="font-mono">{review.slug}</span>
						{#if review.imageType} · {review.imageType}{/if}
					</p>
				</div>
				<button onclick={closeReview} class="p-1.5 rounded-md text-(--color-muted) hover:text-(--color-text) hover:bg-(--color-surface-2) transition-colors" aria-label="Close">
					<svg class="w-4 h-4" 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>

			<!-- Body -->
			<div class="flex-1 overflow-y-auto p-5">
				{#if review.loading}
					<div class="flex items-center justify-center py-16 text-(--color-muted) text-sm">Loading image…</div>
				{:else if review.error}
					<div class="text-center py-8"><p class="text-(--color-danger) text-sm">{review.error}</p></div>
				{:else if review.imageSrc}
					<div class="flex flex-col sm:flex-row gap-5 items-start">
						<!-- Image -->
						<div class="flex-shrink-0 w-full sm:w-64">
							<img
								src={review.imageSrc}
								alt="Generated"
								class="w-full rounded-lg border border-(--color-border) object-contain max-h-80 bg-zinc-950"
							/>
						</div>
						<!-- Meta -->
						<div class="flex-1 space-y-3 text-sm">
							{#if review.prompt}
								<div>
									<p class="text-xs text-(--color-muted) mb-1">Prompt</p>
									<p class="text-(--color-text) leading-relaxed">{review.prompt}</p>
								</div>
							{/if}
							{#if review.bytes > 0}
								<div>
									<p class="text-xs text-(--color-muted)">Size</p>
									<p class="text-(--color-text)">{fmtBytes(review.bytes)}</p>
								</div>
							{/if}
						{#if review.savedUrl}
							<p class="text-xs text-green-400">
								Saved →
								<a href={review.savedUrl} target="_blank" rel="noopener noreferrer" class="underline hover:text-green-300">{review.savedUrl}</a>
							</p>
						{/if}
						</div>
					</div>
				{/if}
			</div>

		<!-- Footer -->
		{#if !review.loading && !review.error && review.imageSrc}
			<div class="px-5 py-4 border-t border-(--color-border) shrink-0 flex items-center justify-between gap-4 flex-wrap">
				<button onclick={closeReview} class="px-3 py-1.5 rounded-md text-sm text-(--color-muted) hover:text-(--color-text) hover:bg-(--color-surface-2) transition-colors">Discard</button>
				<div class="flex items-center gap-3">
					<button
						onclick={downloadImage}
						class="px-3 py-1.5 rounded-md text-sm bg-(--color-surface-3) text-(--color-text) hover:bg-zinc-600 transition-colors"
					>
						Download
					</button>
					{#if review.savedUrl}
						<span class="text-sm text-green-400 font-medium">Saved ✓</span>
					{:else if review.imageType === 'cover'}
						<button
							onclick={saveImageAsCover}
							disabled={review.saving}
							class="px-4 py-1.5 rounded-md text-sm font-medium bg-(--color-brand) text-black hover:bg-amber-300 disabled:opacity-50 transition-colors"
						>
							{review.saving ? 'Saving…' : 'Save as cover'}
						</button>
					{:else if review.imageType === 'chapter' && review.chapter > 0}
						<button
							onclick={saveImageAsChapterImage}
							disabled={review.saving}
							class="px-4 py-1.5 rounded-md text-sm font-medium bg-(--color-brand) text-black hover:bg-amber-300 disabled:opacity-50 transition-colors"
						>
							{review.saving ? 'Saving…' : `Save as chapter ${review.chapter} image`}
						</button>
					{/if}
					{#if review.saveError}
						<p class="text-xs text-(--color-danger)">{review.saveError}</p>
					{/if}
				</div>
			</div>
		{/if}

		<!-- ── Description review ──────────────────────────────────────────────── -->
		{:else if review.kind === 'description'}
			<!-- Header -->
			<div class="flex items-center justify-between gap-4 px-5 py-4 border-b border-(--color-border) shrink-0">
				<div>
					<h2 class="text-base font-semibold text-(--color-text)">Review Description</h2>
					<p class="text-xs text-(--color-muted) mt-0.5">
						<span class="font-mono">{review.slug}</span>
					</p>
				</div>
				<button onclick={closeReview} class="p-1.5 rounded-md text-(--color-muted) hover:text-(--color-text) hover:bg-(--color-surface-2) transition-colors" aria-label="Close">
					<svg class="w-4 h-4" 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>

			<!-- Body -->
			<div class="flex-1 overflow-y-auto p-5 space-y-5">
				{#if review.loading}
					<div class="flex items-center justify-center py-16 text-(--color-muted) text-sm">Loading…</div>
				{:else if review.error}
					<div class="text-center py-8"><p class="text-(--color-danger) text-sm">{review.error}</p></div>
				{:else}
					<!-- Old description -->
					<div class="space-y-1">
						<p class="text-xs font-semibold text-(--color-muted) uppercase tracking-wide">Current description</p>
						<p class="text-sm text-(--color-muted) leading-relaxed bg-(--color-surface-2) rounded-lg px-4 py-3 border border-(--color-border)">
							{review.oldDescription || '—'}
						</p>
					</div>

					<!-- New description (editable) -->
					<div class="space-y-1">
						<p class="text-xs font-semibold text-(--color-text) uppercase tracking-wide">Proposed description <span class="normal-case font-normal text-(--color-muted)">(editable)</span></p>
						<textarea
							bind:value={review.newDescription}
							rows="6"
							class="w-full px-4 py-3 rounded-lg bg-(--color-surface-2) border border-(--color-brand)/50 text-sm text-(--color-text) leading-relaxed resize-y focus:outline-none focus:ring-1 focus:ring-(--color-brand)"
						></textarea>
					</div>

					{#if review.instructions}
						<p class="text-xs text-(--color-muted)">
							<span class="font-medium text-(--color-text)">Instructions:</span> {review.instructions}
						</p>
					{/if}
				{/if}
			</div>

			<!-- Footer -->
			{#if !review.loading && !review.error}
				<div class="px-5 py-4 border-t border-(--color-border) shrink-0 flex items-center justify-between gap-4">
					<button onclick={closeReview} class="px-3 py-1.5 rounded-md text-sm text-(--color-muted) hover:text-(--color-text) hover:bg-(--color-surface-2) transition-colors">Discard</button>
					<div class="flex items-center gap-3">
						{#if review.applyError}<p class="text-xs text-(--color-danger)">{review.applyError}</p>{/if}
						{#if review.applyDone}<p class="text-xs text-green-400">Applied successfully.</p>{/if}
						<button
							onclick={applyDescription}
							disabled={review.applying || review.applyDone || !review.newDescription.trim()}
							class="px-4 py-1.5 rounded-md text-sm font-medium bg-(--color-brand) text-black hover:bg-amber-300 disabled:opacity-50 transition-colors"
						>
							{review.applying ? 'Applying…' : review.applyDone ? 'Applied' : 'Apply'}
						</button>
					</div>
				</div>
			{/if}
		{/if}
	</div>
{/if}
