<script lang="ts">
	import { untrack } from 'svelte';
	import type { PageData } from './$types';
	import type { AudioJob, AudioCacheEntry } from '$lib/server/pocketbase';
	import * as m from '$lib/paraglide/messages.js';

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

	let entries = $state<AudioCacheEntry[]>(untrack(() => data.entries));
	let jobs = $state<AudioJob[]>(untrack(() => data.jobs));

	// Keep in sync on server reloads
	$effect(() => {
		entries = data.entries;
		jobs = data.jobs;
	});

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

	$effect(() => {
		if (!hasInFlight) return;
		const id = setInterval(async () => {
			const res = await fetch('/api/admin/audio-jobs').catch(() => null);
			if (res?.ok) {
				const body = await res.json().catch(() => null);
				if (body?.jobs) {
					jobs = body.jobs;
					entries = (body.jobs as AudioJob[])
						.filter((j) => j.status === 'done')
						.map((j) => ({ id: j.id, cache_key: j.cache_key, filename: `${j.cache_key}.mp3`, updated: j.finished }));
				}
			}
		}, 3000);
		return () => clearInterval(id);
	});

	// ── Tabs ─────────────────────────────────────────────────────────────────────
	type Tab = 'jobs' | 'cache';
	let activeTab = $state<Tab>('jobs');

	// ── Helpers ──────────────────────────────────────────────────────────────────
	function jobStatusColor(status: string) {
		if (status === 'done') return 'text-green-400';
		if (status === 'generating') return 'text-(--color-brand) animate-pulse';
		if (status === 'pending') return 'text-sky-400 animate-pulse';
		if (status === 'failed') return 'text-(--color-danger)';
		return 'text-(--color-text)';
	}

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

	function duration(started: string, finished: string) {
		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 engineLabel(voice: string): string {
		if (voice.startsWith('cfai:')) return 'CF AI';
		if (!voice.includes('_')) return 'Pocket TTS';
		return 'Kokoro';
	}

	// ── Audio jobs stats + filter ────────────────────────────────────────────────
	let jobsQ = $state('');
	let jobsStatusFilter = $state('all');
	const JOB_STATUS_OPTIONS = ['all', 'generating', 'pending', 'done', 'failed'] as const;

	let filteredJobs = $derived(
		jobs.filter((j: AudioJob) => {
			const qLower = jobsQ.trim().toLowerCase();
			const matchesQ =
				!qLower ||
				j.slug.toLowerCase().includes(qLower) ||
				j.voice.toLowerCase().includes(qLower) ||
				j.status.toLowerCase().includes(qLower);
			const matchesStatus = jobsStatusFilter === 'all' || j.status === jobsStatusFilter;
			return matchesQ && matchesStatus;
		})
	);

	let stats = $derived({
		total: jobs.length,
		done: jobs.filter((j: AudioJob) => j.status === 'done').length,
		failed: jobs.filter((j: AudioJob) => j.status === 'failed').length,
		pending: jobs.filter((j: AudioJob) => j.status === 'pending').length,
		generating: jobs.filter((j: AudioJob) => j.status === 'generating').length,
		inFlight: jobs.filter((j: AudioJob) => j.status === 'pending' || j.status === 'generating').length
	});

	// ── Cancel single job ────────────────────────────────────────────────────────
	let cancellingJobIds = $state(new Set<string>());
	let cancelJobErrors: Record<string, string> = $state({});

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

	// ── Retry failed job ─────────────────────────────────────────────────────────
	let retryingJobIds = $state(new Set<string>());
	let retryJobErrors: Record<string, string> = $state({});

	async function retryJob(job: AudioJob) {
		if (retryingJobIds.has(job.id)) return;
		retryingJobIds = new Set([...retryingJobIds, job.id]);
		delete retryJobErrors[job.id];
		try {
			const res = await fetch('/api/admin/audio/bulk', {
				method: 'POST',
				headers: { 'content-type': 'application/json' },
				body: JSON.stringify({ slug: job.slug, voice: job.voice, from: job.chapter, to: job.chapter, force: true })
			});
			if (!res.ok) {
				const body = await res.json().catch(() => ({}));
				retryJobErrors = { ...retryJobErrors, [job.id]: body.error ?? `Error ${res.status}` };
			} else {
				jobs = jobs.map((j: AudioJob) => j.id === job.id ? { ...j, status: 'pending', error_message: '' } : j);
			}
		} catch {
			retryJobErrors = { ...retryJobErrors, [job.id]: 'Network error.' };
		} finally {
			retryingJobIds = new Set([...retryingJobIds].filter((x) => x !== job.id));
		}
	}

	// ── Audio cache filter ───────────────────────────────────────────────────────
	function parseCacheKey(key: string) {
		const parts = key.split('/');
		if (parts.length >= 3) {
			return { slug: parts[0], chapter: parts[1], voice: parts.slice(2).join('/') };
		}
		return { slug: key, chapter: '—', voice: '—' };
	}

	let cacheQ = $state('');
	let filteredCache = $derived(
		cacheQ.trim()
			? entries.filter((e) => e.cache_key.toLowerCase().includes(cacheQ.toLowerCase().trim()))
			: entries
	);
</script>

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

<div class="space-y-6">
	<!-- Header -->
	<div>
		<h1 class="text-2xl font-bold text-(--color-text)">{m.admin_audio_heading()}</h1>
		<p class="text-(--color-muted) text-sm mt-1">
			{stats.total} job{stats.total !== 1 ? 's' : ''} &middot;
			<span class="text-green-400">{stats.done} done</span>
			{#if stats.failed > 0}
				&middot; <span class="text-(--color-danger)">{stats.failed} failed</span>
			{/if}
			{#if stats.inFlight > 0}
				&middot; <span class="text-(--color-brand) animate-pulse">{stats.inFlight} in-flight</span>
			{/if}
			&middot; {entries.length} cached file{entries.length !== 1 ? 's' : ''}
		</p>
	</div>

	<!-- Tabs -->
	<div class="flex gap-1 bg-(--color-surface-2) rounded-lg p-1 w-fit border border-(--color-border)">
		<button
			onclick={() => (activeTab = 'jobs')}
			class="px-4 py-1.5 rounded-md text-sm font-medium transition-colors
				{activeTab === 'jobs' ? 'bg-(--color-surface-3) text-(--color-text)' : 'text-(--color-muted) hover:text-(--color-text)'}"
		>
			Jobs
			{#if stats.inFlight > 0}
				<span class="ml-1.5 inline-flex items-center justify-center w-4 h-4 rounded-full bg-(--color-brand) text-(--color-surface) text-[10px] font-bold">
					{stats.inFlight}
				</span>
			{/if}
		</button>
		<button
			onclick={() => (activeTab = 'cache')}
			class="px-4 py-1.5 rounded-md text-sm font-medium transition-colors
				{activeTab === 'cache' ? 'bg-(--color-surface-3) text-(--color-text)' : 'text-(--color-muted) hover:text-(--color-text)'}"
		>
			Cache
		</button>
	</div>

	<!-- ── Audio Jobs tab ─────────────────────────────────────────────────────── -->
	{#if activeTab === 'jobs'}
		<div class="flex flex-wrap gap-3 items-center">
			<input
				type="search"
				bind:value={jobsQ}
				placeholder={m.admin_audio_filter_jobs()}
				class="flex-1 min-w-48 max-w-sm bg-(--color-surface-2) border border-(--color-border) rounded-lg px-3 py-2 text-(--color-text) text-sm placeholder-zinc-500 focus:outline-none focus:ring-2 focus:ring-(--color-brand)"
			/>
			<!-- Status pills -->
			<div class="flex gap-1 flex-wrap">
				{#each JOB_STATUS_OPTIONS as s}
					{@const count = s === 'all' ? stats.total : (s === 'generating' ? stats.generating : s === 'pending' ? stats.pending : s === 'done' ? stats.done : stats.failed)}
					<button
						onclick={() => (jobsStatusFilter = s)}
						class="px-2.5 py-1 rounded-md text-xs font-medium transition-colors capitalize
							{jobsStatusFilter === s
								? 'bg-(--color-brand) text-black'
								: 'bg-(--color-surface-2) text-(--color-muted) hover:text-(--color-text)'}"
					>
						{s}{count > 0 ? ` ${count}` : ''}
					</button>
				{/each}
			</div>
		</div>

		{#if filteredJobs.length === 0}
			<p class="text-(--color-muted) text-sm py-8 text-center">
				{jobsQ.trim() ? m.admin_audio_no_matching_jobs() : m.admin_audio_no_jobs()}
			</p>
		{:else}
			<!-- Desktop table -->
			<div class="hidden sm:block overflow-x-auto rounded-xl border border-(--color-border)">
				<table class="w-full text-sm">
				<thead class="bg-(--color-surface-2) text-(--color-muted) text-xs uppercase tracking-wide">
					<tr>
						<th class="px-4 py-3 text-left">Book</th>
						<th class="px-4 py-3 text-right">Ch.</th>
						<th class="px-4 py-3 text-left">Voice</th>
						<th class="px-4 py-3 text-left">Engine</th>
						<th class="px-4 py-3 text-left">Status</th>
						<th class="px-4 py-3 text-left">Started</th>
						<th class="px-4 py-3 text-left">Duration</th>
						<th class="px-4 py-3 text-left">Actions</th>
					</tr>
				</thead>
				<tbody class="divide-y divide-(--color-border)/50">
					{#each filteredJobs as job}
						<tr class="bg-(--color-surface) hover:bg-(--color-surface-2)/50 transition-colors">
							<td class="px-4 py-3 text-(--color-text) font-medium">
								<a href="/books/{job.slug}" class="hover:text-(--color-brand) transition-colors">{job.slug}</a>
							</td>
							<td class="px-4 py-3 text-right text-(--color-muted)">{job.chapter}</td>
							<td class="px-4 py-3 text-(--color-muted) font-mono text-xs">{job.voice}</td>
							<td class="px-4 py-3 text-(--color-muted) text-xs">{engineLabel(job.voice)}</td>
							<td class="px-4 py-3">
								<span class="font-medium {jobStatusColor(job.status)}">{job.status}</span>
							</td>
							<td class="px-4 py-3 text-(--color-muted) whitespace-nowrap">{fmtDate(job.started)}</td>
							<td class="px-4 py-3 text-(--color-muted) whitespace-nowrap">{duration(job.started, job.finished)}</td>
						<td class="px-4 py-3">
							{#if job.status === 'pending' || job.status === 'generating'}
								<button
									onclick={() => cancelJob(job.id)}
									disabled={cancellingJobIds.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"
								>
									{cancellingJobIds.has(job.id) ? 'Cancelling…' : 'Cancel'}
								</button>
							{/if}
							{#if job.status === 'failed'}
								<button
									onclick={() => retryJob(job)}
									disabled={retryingJobIds.has(job.id)}
									class="px-2 py-1 rounded text-xs font-medium bg-sky-400/10 text-sky-400 hover:bg-sky-400/20 disabled:opacity-50 transition-colors"
								>
									{retryingJobIds.has(job.id) ? 'Retrying…' : 'Retry ↺'}
								</button>
							{/if}
							{#if cancelJobErrors[job.id]}
								<p class="text-xs text-(--color-danger) mt-1">{cancelJobErrors[job.id]}</p>
							{/if}
							{#if retryJobErrors[job.id]}
								<p class="text-xs text-(--color-danger) mt-1">{retryJobErrors[job.id]}</p>
							{/if}
						</td>
						</tr>
						{#if job.error_message}
							<tr class="bg-(--color-danger)/10">
								<td colspan="8" class="px-4 py-2 text-xs text-(--color-danger) font-mono">{job.error_message}</td>
							</tr>
						{/if}
					{/each}
				</tbody>
				</table>
			</div>

		<!-- Mobile cards -->
		<div class="sm:hidden space-y-3">
			{#each filteredJobs as job}
				<div class="bg-(--color-surface) rounded-xl border border-(--color-border) p-4 space-y-2">
					<div class="flex items-start justify-between gap-2">
						<a href="/books/{job.slug}" class="text-(--color-text) font-medium hover:text-(--color-brand) transition-colors truncate">
							{job.slug}
						</a>
						<span class="shrink-0 text-xs font-semibold {jobStatusColor(job.status)}">{job.status}</span>
					</div>
					<div class="grid grid-cols-2 gap-1 text-xs">
						<span class="text-(--color-muted)">Chapter</span><span class="text-(--color-muted) text-right">{job.chapter}</span>
						<span class="text-(--color-muted)">Voice</span><span class="text-(--color-muted) font-mono text-right truncate">{job.voice}</span>
						<span class="text-(--color-muted)">Engine</span><span class="text-(--color-muted) text-right">{engineLabel(job.voice)}</span>
						<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">{duration(job.started, job.finished)}</span>
					</div>
					{#if job.error_message}
						<p class="text-xs text-(--color-danger) font-mono break-all">{job.error_message}</p>
					{/if}
				{#if job.status === 'pending' || job.status === 'generating'}
					<button
						onclick={() => cancelJob(job.id)}
						disabled={cancellingJobIds.has(job.id)}
						class="w-full px-3 py-1.5 rounded-lg text-xs font-medium bg-(--color-danger)/10 text-(--color-danger) hover:bg-(--color-danger)/20 disabled:opacity-50 transition-colors"
					>
						{cancellingJobIds.has(job.id) ? 'Cancelling…' : 'Cancel job'}
					</button>
				{/if}
				{#if job.status === 'failed'}
					<button
						onclick={() => retryJob(job)}
						disabled={retryingJobIds.has(job.id)}
						class="w-full px-3 py-1.5 rounded-lg text-xs font-medium bg-sky-400/10 text-sky-400 hover:bg-sky-400/20 disabled:opacity-50 transition-colors"
					>
						{retryingJobIds.has(job.id) ? 'Retrying…' : 'Retry ↺'}
					</button>
				{/if}
				{#if cancelJobErrors[job.id]}
					<p class="text-xs text-(--color-danger)">{cancelJobErrors[job.id]}</p>
				{/if}
				{#if retryJobErrors[job.id]}
					<p class="text-xs text-(--color-danger)">{retryJobErrors[job.id]}</p>
				{/if}
				</div>
			{/each}
		</div>
		{/if}
	{/if}

	<!-- ── Audio Cache tab ───────────────────────────────────────────────────── -->
	{#if activeTab === 'cache'}
		<input
			type="search"
			bind:value={cacheQ}
			placeholder={m.admin_audio_filter_cache()}
			class="w-full max-w-sm bg-(--color-surface-2) border border-(--color-border) rounded-lg px-3 py-2 text-(--color-text) text-sm placeholder-zinc-500 focus:outline-none focus:ring-2 focus:ring-(--color-brand)"
		/>

		{#if filteredCache.length === 0}
			<p class="text-(--color-muted) text-sm py-8 text-center">
				{cacheQ.trim() ? m.admin_audio_no_cache_results() : m.admin_audio_cache_empty()}
			</p>
		{:else}
			<!-- Desktop table -->
			<div class="hidden sm:block overflow-x-auto rounded-xl border border-(--color-border)">
				<table class="w-full text-sm">
					<thead class="bg-(--color-surface-2) text-(--color-muted) text-xs uppercase tracking-wide">
						<tr>
							<th class="px-4 py-3 text-left">Book</th>
							<th class="px-4 py-3 text-left">Chapter</th>
							<th class="px-4 py-3 text-left">Voice</th>
							<th class="px-4 py-3 text-left">Engine</th>
							<th class="px-4 py-3 text-left">Filename</th>
							<th class="px-4 py-3 text-left">Updated</th>
						</tr>
					</thead>
					<tbody class="divide-y divide-(--color-border)/50">
						{#each filteredCache as entry}
							{@const parts = parseCacheKey(entry.cache_key)}
							<tr class="bg-(--color-surface) hover:bg-(--color-surface-2)/50 transition-colors">
								<td class="px-4 py-3 text-(--color-text) font-medium">
									<a href="/books/{parts.slug}" class="hover:text-(--color-brand) transition-colors">{parts.slug}</a>
								</td>
								<td class="px-4 py-3 text-(--color-muted)">{parts.chapter}</td>
								<td class="px-4 py-3 text-(--color-muted) font-mono text-xs">{parts.voice}</td>
								<td class="px-4 py-3 text-(--color-muted) text-xs">{engineLabel(parts.voice)}</td>
								<td class="px-4 py-3 text-(--color-muted) font-mono text-xs truncate max-w-[14rem]" title={entry.filename}>
									{entry.filename}
								</td>
								<td class="px-4 py-3 text-(--color-muted) whitespace-nowrap">{fmtDate(entry.updated)}</td>
							</tr>
						{/each}
					</tbody>
				</table>
			</div>

			<!-- Mobile cards -->
			<div class="sm:hidden space-y-3">
				{#each filteredCache as entry}
					{@const parts = parseCacheKey(entry.cache_key)}
					<div class="bg-(--color-surface) rounded-xl border border-(--color-border) p-4 space-y-2">
						<a href="/books/{parts.slug}" class="text-(--color-text) font-medium hover:text-(--color-brand) transition-colors block truncate">
							{parts.slug}
						</a>
					<div class="grid grid-cols-2 gap-1 text-xs">
						<span class="text-(--color-muted)">Chapter</span><span class="text-(--color-muted) text-right">{parts.chapter}</span>
						<span class="text-(--color-muted)">Voice</span><span class="text-(--color-muted) font-mono text-right truncate">{parts.voice}</span>
						<span class="text-(--color-muted)">Engine</span><span class="text-(--color-muted) text-right">{engineLabel(parts.voice)}</span>
						<span class="text-(--color-muted)">Updated</span><span class="text-(--color-muted) text-right">{fmtDate(entry.updated)}</span>
					</div>
						{#if entry.filename}
							<p class="text-xs text-(--color-muted) font-mono truncate" title={entry.filename}>{entry.filename}</p>
						{/if}
					</div>
				{/each}
			</div>
		{/if}
	{/if}
</div>
