/* Customer Cloud Gallery — Service Worker für Streaming-ZIP-Downloads.
 *
 * Funktionsweise: Der Main-Thread postet via MessageChannel ein ReadableStream
 * an den Service Worker, registriert dazu einen Token. Sobald der Browser
 * eine GET-Anfrage auf <scope>/zip-stream/<token> macht, antwortet der SW
 * mit dem Stream + Content-Disposition: attachment, woraufhin der Browser
 * den Save-Dialog zeigt und Daten laufend in die Zieldatei schreibt — ohne
 * dass das ZIP jemals komplett im RAM gehalten werden muss.
 *
 * Architektur identisch zum bekannten StreamSaver-Pattern, aber selbst
 * implementiert: keine externen Dependencies, kein cross-origin mitm.html.
 *
 * skipWaiting() bewusst NICHT aufgerufen — sonst würde ein Plugin-Update
 * mid-stream den alten SW killen, der aktive Download bricht ab und der User
 * bekommt ein korruptes ZIP. Durch das Weglassen wartet ein neuer SW auf
 * Tab-Reload, was die saubere Variante ist.
 */
'use strict';

// token → { stream, filename, controller, port }
const channels = new Map();

self.addEventListener('install', function (/* event */) {
	// skipWaiting hier nötig: ohne bleibt ein neu installierter SW im
	// `waiting`-State stecken, solange ein älterer SW noch Clients hat —
	// und der Main-Thread hängt unbestimmt am state==='activated'-await.
	// Mid-Stream-Update-Kill (das war der Grund, warum ich's mal entfernt
	// hatte) ist hier kein Risiko, weil Plugin-Updates extrem selten
	// während eines aktiven Downloads passieren.
	self.skipWaiting();
});

self.addEventListener('activate', function (event) {
	event.waitUntil(self.clients.claim());
});

self.addEventListener('message', function (event) {
	const data = event.data || {};
	if (data.action !== 'ccgal-zip-init') return;

	const token    = String(data.token || '');
	const filename = String(data.filename || 'gallery.zip');
	if (!token) return;

	const port = event.ports && event.ports[0];
	if (!port) return;

	let controller = null;
	const stream = new ReadableStream({
		start: function (c) { controller = c; },
		cancel: function () {
			// Browser hat den Save-Stream beendet (User klickt im Save-Dialog
			// auf Abbrechen, Tab geschlossen, Disk voll). Wir sagen dem Main-
			// Thread Bescheid, damit er die Producer-Worker stoppt und den
			// Toast aufräumt — sonst pumpt er weiter Chunks ins Leere und der
			// Toast suggeriert weiterhin Aktivität.
			try { port.postMessage({ action: 'cancelled' }); } catch (_) {}
			channels.delete(token);
			try { port.close(); } catch (_) {}
		},
	});

	const channel = { stream: stream, filename: filename, controller: controller, port: port };
	channels.set(token, channel);

	port.onmessage = function (e) {
		const msg = e.data || {};
		const ctrl = channel.controller;
		if (!ctrl) return; // ReadableStream wurde vom Browser canceled
		if (msg.action === 'chunk' && msg.chunk) {
			try { ctrl.enqueue(new Uint8Array(msg.chunk)); } catch (_) {}
		} else if (msg.action === 'finish') {
			try { ctrl.close(); } catch (_) {}
			channels.delete(token);
			try { port.close(); } catch (_) {}
		} else if (msg.action === 'abort') {
			try { ctrl.error(new Error('aborted')); } catch (_) {}
			channels.delete(token);
			try { port.close(); } catch (_) {}
		}
	};

	// Acknowledge an den Main-Thread — der wartet darauf, bevor er den
	// fetch auf die Stream-URL anstößt. Damit kein Race zwischen init-Message
	// und fetch-Event möglich.
	try { port.postMessage({ action: 'ready' }); } catch (_) {}
});

self.addEventListener('fetch', function (event) {
	const url = new URL(event.request.url);

	// Probe-Endpoint: erlaubt dem Main-Thread vor dem eigentlichen Stream
	// zu prüfen, ob der SW den Tab tatsächlich kontrolliert. Im Inkognito-/
	// Privacy-Modus kann register() succeeden, ohne dass der SW respondWith
	// macht — dann gibt der Server hier 404, und Main weiß: Streaming-Pfad
	// klappt nicht, sauber auf Blob/Server-Fallback ausweichen.
	if (url.pathname.endsWith('/zip-stream-probe')) {
		event.respondWith(new Response('ok', {
			status: 200,
			headers: { 'Cache-Control': 'no-store', 'Content-Type': 'text/plain' },
		}));
		return;
	}

	const m = url.pathname.match(/\/zip-stream\/([A-Za-z0-9_\-]+)\/?$/);
	if (!m) return;
	const token = m[1];
	const channel = channels.get(token);
	if (!channel) return;

	event.respondWith(new Response(channel.stream, {
		status: 200,
		headers: {
			'Content-Type': 'application/zip',
			'Content-Disposition': 'attachment; filename="' + encodeFilename(channel.filename) + '"',
			'X-Content-Type-Options': 'nosniff',
			'Cache-Control': 'no-store',
		},
	}));
});

function encodeFilename(name) {
	return String(name).replace(/[^\x20-\x7E]/g, '_').replace(/"/g, '_');
}
