<script lang="ts">
	/**
	 * Sound Pad Grid (4×2) for the mixer.
	 *
	 * Each pad plays a preloaded AudioBuffer from CAS on press.
	 * Pad triggers are synchronized to other participants via LiveKit Data Messages.
	 * Pad configuration (label, color, cas_hash) is stored in the collection node's
	 * metadata.mixer.pads array and persisted via the API.
	 */

	import type { Node } from '$lib/realtime';
	import { casUrl, updateNode, uploadMedia } from '$lib/api';
	import {
		loadPadAudio,
		isPadLoaded,
		playPad,
		stopPad,
		isPadPlaying,
		unloadAllPads,
		ensureAudioContext,
	} from '$lib/mixer';
	import { onDataMessage, sendDataMessage, getLocalIdentity } from '$lib/livekit';

	interface PadConfig {
		label: string;
		cas_hash: string;
		color: string;
	}

	interface Props {
		collection?: Node;
		accessToken?: string;
		isViewer?: boolean;
	}

	let { collection, accessToken, isViewer = false }: Props = $props();

	// Pad state
	let padConfigs: PadConfig[] = $state([]);
	let loadingPads = $state(new Set<number>());
	let playingPads = $state(new Set<number>());
	let configMode = $state(false);
	let playingTimeouts: Map<number, ReturnType<typeof setTimeout>> = new Map();

	// Edit state for config mode
	let editLabel = $state('');
	let editColor = $state('#6366f1');
	let editIndex: number | null = $state(null);

	const GRID_SIZE = 8; // 4×2

	const DEFAULT_COLORS = [
		'#EF4444', '#F97316', '#EAB308', '#22C55E',
		'#06B6D4', '#3B82F6', '#8B5CF6', '#EC4899',
	];

	// Parse pad config from collection metadata
	$effect(() => {
		const meta = collection?.metadata ? JSON.parse(collection.metadata) : {};
		const mixerMeta = meta?.mixer ?? {};
		const rawPads: PadConfig[] = mixerMeta?.pads ?? [];

		// Ensure we always have GRID_SIZE slots
		const configs: PadConfig[] = [];
		for (let i = 0; i < GRID_SIZE; i++) {
			configs.push(rawPads[i] ?? { label: '', cas_hash: '', color: DEFAULT_COLORS[i] });
		}
		padConfigs = configs;
	});

	// Load audio buffers when pad configs change
	$effect(() => {
		for (let i = 0; i < padConfigs.length; i++) {
			const pad = padConfigs[i];
			if (pad.cas_hash && !isPadLoaded(`pad_${i}`)) {
				loadPadBuffer(i, pad.cas_hash);
			}
		}

		return () => {
			unloadAllPads();
			// Clear visual feedback timeouts
			for (const t of playingTimeouts.values()) clearTimeout(t);
			playingTimeouts.clear();
		};
	});

	// Listen for remote pad triggers via LiveKit Data Messages
	$effect(() => {
		const unsub = onDataMessage((payload, senderIdentity, topic) => {
			if (topic !== 'pad_trigger') return;
			// Don't replay our own triggers
			if (senderIdentity === getLocalIdentity()) return;

			try {
				const msg = JSON.parse(new TextDecoder().decode(payload));
				if (msg.action === 'play' && typeof msg.padIndex === 'number') {
					triggerPadLocally(msg.padIndex);
				}
			} catch {
				// Invalid message — ignore
			}
		});
		return unsub;
	});

	async function loadPadBuffer(index: number, casHash: string) {
		const padId = `pad_${index}`;
		loadingPads = new Set([...loadingPads, index]);
		try {
			const url = casUrl(casHash);
			await loadPadAudio(padId, url);
		} catch (err) {
			console.error(`Failed to load pad ${index}:`, err);
		} finally {
			const next = new Set(loadingPads);
			next.delete(index);
			loadingPads = next;
		}
	}

	function triggerPadLocally(index: number) {
		const padId = `pad_${index}`;
		if (!isPadLoaded(padId)) return;

		ensureAudioContext();
		playPad(padId);

		// Visual feedback — track timeout for cleanup on destroy
		playingPads = new Set([...playingPads, index]);
		const prev = playingTimeouts.get(index);
		if (prev) clearTimeout(prev);
		playingTimeouts.set(index, setTimeout(() => {
			const next = new Set(playingPads);
			next.delete(index);
			playingPads = next;
			playingTimeouts.delete(index);
		}, 300));
	}

	async function handlePadPress(index: number) {
		if (isViewer) return;
		const pad = padConfigs[index];
		if (!pad.cas_hash) return;

		triggerPadLocally(index);

		// Broadcast to other participants
		const msg = JSON.stringify({ action: 'play', padIndex: index });
		const data = new TextEncoder().encode(msg);
		await sendDataMessage(data, 'pad_trigger');
	}

	// Config mode functions
	function startEditPad(index: number) {
		editIndex = index;
		editLabel = padConfigs[index].label;
		editColor = padConfigs[index].color;
	}

	function cancelEdit() {
		editIndex = null;
		editLabel = '';
		editColor = '#6366f1';
	}

	async function handleFileUpload(event: Event) {
		if (editIndex === null || !accessToken) return;
		const input = event.target as HTMLInputElement;
		const file = input.files?.[0];
		if (!file) return;

		try {
			const result = await uploadMedia(accessToken, { file, visibility: 'team' });
			const newConfigs = [...padConfigs];
			newConfigs[editIndex] = {
				label: editLabel || file.name.replace(/\.[^/.]+$/, ''),
				cas_hash: result.cas_hash,
				color: editColor,
			};
			await savePadConfigs(newConfigs);
			padConfigs = newConfigs;

			// Load the new audio immediately
			await loadPadBuffer(editIndex, result.cas_hash);
			cancelEdit();
		} catch (err) {
			console.error('Failed to upload pad audio:', err);
		}
	}

	async function saveEditedPad() {
		if (editIndex === null || !accessToken) return;
		const newConfigs = [...padConfigs];
		newConfigs[editIndex] = {
			...newConfigs[editIndex],
			label: editLabel,
			color: editColor,
		};
		await savePadConfigs(newConfigs);
		padConfigs = newConfigs;
		cancelEdit();
	}

	async function clearPad(index: number) {
		if (!accessToken) return;
		const newConfigs = [...padConfigs];
		newConfigs[index] = { label: '', cas_hash: '', color: DEFAULT_COLORS[index] };
		await savePadConfigs(newConfigs);
		padConfigs = newConfigs;
		stopPad(`pad_${index}`);
	}

	async function savePadConfigs(configs: PadConfig[]) {
		if (!accessToken || !collection) return;
		// Filter out empty pads for clean storage
		const padsToSave = configs.filter(p => p.cas_hash);
		const meta = collection?.metadata ? JSON.parse(collection.metadata) : {};
		const mixer = meta.mixer ?? {};
		mixer.pads = padsToSave.length > 0 ? configs : undefined;
		meta.mixer = mixer;

		await updateNode(accessToken, {
			node_id: collection.id,
			metadata: meta,
		});
	}
</script>

<div class="space-y-2">
	<div class="flex items-center justify-between">
		<h4 class="text-xs font-semibold text-gray-600 uppercase tracking-wide">Sound Pads</h4>
		{#if !isViewer}
			<button
				onclick={() => { configMode = !configMode; cancelEdit(); }}
				class="text-xs text-gray-400 hover:text-gray-600 transition-colors"
			>
				{configMode ? 'Ferdig' : 'Rediger'}
			</button>
		{/if}
	</div>

	<!-- 4×2 pad grid -->
	<div class="grid grid-cols-4 gap-1.5 sm:gap-2">
		{#each padConfigs as pad, index (index)}
			{@const isPlaying = playingPads.has(index)}
			{@const isLoading = loadingPads.has(index)}
			{@const isEmpty = !pad.cas_hash}
			{@const isEditing = configMode && editIndex === index}

			{#if isEditing}
				<!-- Edit mode for this pad -->
				<div class="col-span-4 rounded-lg border border-indigo-200 bg-indigo-50 p-3 space-y-2">
					<div class="flex items-center gap-2">
						<input
							type="text"
							bind:value={editLabel}
							placeholder="Label"
							class="flex-1 rounded border border-gray-300 px-2 py-1 text-xs"
						/>
						<input
							type="color"
							bind:value={editColor}
							class="h-7 w-7 rounded border border-gray-300 cursor-pointer"
						/>
					</div>
					<div class="flex items-center gap-2">
						<label class="flex-1 cursor-pointer rounded bg-indigo-600 px-3 py-1.5 text-center text-xs font-medium text-white hover:bg-indigo-700 transition-colors">
							{pad.cas_hash ? 'Bytt lydfil' : 'Last opp lydfil'}
							<input type="file" accept="audio/*" onchange={handleFileUpload} class="hidden" />
						</label>
						{#if pad.cas_hash}
							<button
								onclick={saveEditedPad}
								class="rounded bg-green-600 px-3 py-1.5 text-xs font-medium text-white hover:bg-green-700 transition-colors"
							>
								Lagre
							</button>
						{/if}
						<button
							onclick={cancelEdit}
							class="rounded bg-gray-200 px-3 py-1.5 text-xs font-medium text-gray-600 hover:bg-gray-300 transition-colors"
						>
							Avbryt
						</button>
					</div>
				</div>
			{:else}
				<!-- Pad button with optional clear badge -->
				<div class="relative">
					<button
						onclick={() => configMode ? startEditPad(index) : handlePadPress(index)}
						disabled={!configMode && (isEmpty || isLoading || isViewer)}
						class="w-full aspect-square rounded-lg border-2 transition-all duration-100 flex flex-col items-center justify-center gap-0.5
							{isEmpty
								? 'border-dashed border-gray-300 bg-gray-50 text-gray-400'
								: isPlaying
									? 'border-white shadow-lg scale-95'
									: 'border-gray-200 hover:border-gray-300 shadow-sm hover:shadow'}
							{isLoading ? 'animate-pulse' : ''}
							disabled:opacity-40 disabled:cursor-not-allowed"
						style={isEmpty ? '' : `background-color: ${pad.color}; border-color: ${pad.color}`}
						title={isEmpty ? (configMode ? 'Klikk for å legge til lyd' : 'Tom pad') : pad.label}
					>
						{#if isEmpty}
							{#if configMode}
								<span class="text-lg leading-none">+</span>
								<span class="text-[10px]">Legg til</span>
							{:else}
								<span class="text-[10px]">Tom</span>
							{/if}
						{:else}
							<span class="text-xs font-bold text-white drop-shadow-sm truncate px-1 max-w-full leading-tight">
								{pad.label || '?'}
							</span>
							{#if isLoading}
								<span class="text-[9px] text-white/70">Laster...</span>
							{/if}
						{/if}
					</button>
					{#if configMode && !isEmpty}
						<button
							onclick={() => clearPad(index)}
							class="absolute -top-1 -right-1 h-4 w-4 rounded-full bg-red-500 text-white text-[10px] leading-none flex items-center justify-center shadow hover:bg-red-600 z-10"
							title="Fjern pad"
						>
							×
						</button>
					{/if}
				</div>
			{/if}
		{/each}
	</div>
</div>
