<script lang="ts">
	import type { Node, MixerChannel } from '$lib/realtime';
	import { edgeStore, nodeStore, mixerChannelStore } from '$lib/realtime';
	import {
		createMixerChannel as apiCreateMixerChannel,
		setMixerGain,
		setMixerMute,
		toggleMixerEffect,
		setMixerRole as apiSetMixerRole,
	} from '$lib/api';
	import TraitPanel from './TraitPanel.svelte';
	import SoundPadGrid from './SoundPadGrid.svelte';
	import {
		getStatus,
		getParticipants,
		getLocalIdentity,
		subscribe,
		type LiveKitParticipant,
	} from '$lib/livekit';
	import {
		getChannelLevels,
		getMasterLevels,
		setChannelGain,
		getChannelGain,
		muteChannel,
		unmuteChannel,
		setMasterGain,
		muteMaster,
		unmuteMaster,
		getChannelIdentities,
		setChannelEffect,
		applyActiveEffectsJson,
		applyEqPreset,
		setRobotVoice,
		setRobotFrequency,
		setRobotDepth,
		setMonsterVoice,
		setMonsterPitchFactor,
		getChannelVoiceState,
		EQ_PRESETS,
		type ChannelLevels,
		type EqEffectName,
		type EqState,
		type VoiceEffectName,
	} from '$lib/mixer';

	interface Props {
		collection?: Node;
		config: Record<string, unknown>;
		accessToken?: string;
	}

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

	// LiveKit state
	let participants: LiveKitParticipant[] = $state([]);
	let localIdentity: string = $state('');

	// Master state (local only — master is per-client, not shared)
	let masterState = $state({ gain: 1.0, muted: false });

	// VU meter levels (updated via animation frame)
	let channelLevels: Map<string, ChannelLevels> = $state(new Map());
	let masterLevels: ChannelLevels | null = $state(null);

	// Voice effect local params (per-client, not synced)
	let voiceParams = $state(new Map<string, {
		robotFrequency: number;
		robotDepth: number;
		monsterPitchFactor: number;
		showRobotParams: boolean;
		showMonsterParams: boolean;
	}>());

	function getVoiceParams(identity: string) {
		let p = voiceParams.get(identity);
		if (!p) {
			p = { robotFrequency: 80, robotDepth: 0.5, monsterPitchFactor: 0.7, showRobotParams: false, showMonsterParams: false };
			voiceParams.set(identity, p);
		}
		return p;
	}

	// Animation frame for VU meters
	let animFrameId: number | null = null;

	// Flag to suppress remote sync feedback for own changes
	let suppressRemoteSync = false;

	// Derive room_id from the collection's communication node
	// Pattern: "communication_{communication_node_id}"
	const roomId = $derived.by(() => {
		if (!collection) return null;
		for (const edge of edgeStore.byTarget(collection.id)) {
			if (edge.edgeType !== 'belongs_to') continue;
			const node = nodeStore.get(edge.sourceId);
			if (node && node.nodeKind === 'communication') {
				return `communication_${node.id}`;
			}
		}
		for (const edge of edgeStore.bySource(collection.id)) {
			if (edge.edgeType !== 'has_channel') continue;
			const node = nodeStore.get(edge.targetId);
			if (node && node.nodeKind === 'communication') {
				return `communication_${node.id}`;
			}
		}
		return null;
	});

	// Delte mixer-kanaler for dette rommet
	const sharedChannels = $derived(roomId ? mixerChannelStore.byRoom(roomId) : []);

	// Check if the local user is a viewer (read-only)
	const localRole = $derived.by(() => {
		if (!roomId || !localIdentity) return 'editor';
		const ch = mixerChannelStore.byParticipant(roomId, localIdentity);
		return ch?.role ?? 'editor';
	});

	const isViewer = $derived(localRole === 'viewer');

	// Hent kanaltilstand fra delt state, fallback til lokalt
	function getSharedState(identity: string): { gain: number; muted: boolean } {
		if (!roomId) return { gain: getChannelGain(identity), muted: false };
		const ch = mixerChannelStore.byParticipant(roomId, identity);
		if (ch) return { gain: ch.gain, muted: ch.isMuted };
		return { gain: getChannelGain(identity), muted: false };
	}

	$effect(() => {
		const unsub = subscribe(() => {
			participants = getParticipants();
			localIdentity = getLocalIdentity();

			// Opprett mixer-kanaler for nye deltakere via API
			const activeIdentities = getChannelIdentities();
			if (accessToken && roomId) {
				for (const id of activeIdentities) {
					const existing = mixerChannelStore.byParticipant(roomId, id);
					if (!existing) {
						apiCreateMixerChannel(accessToken, roomId, id).catch(console.error);
					}
				}
			}
		});
		return unsub;
	});

	// Synk Web Audio-graf når mixer-tilstand endres fra andre klienter
	$effect(() => {
		if (suppressRemoteSync) return;
		for (const ch of sharedChannels) {
			// Apply shared gain/mute to Web Audio graph
			if (ch.isMuted) {
				muteChannel(ch.targetUserId);
			} else {
				setChannelGain(ch.targetUserId, ch.gain);
				// Ensure unmuted in Web Audio if was previously muted
				unmuteChannel(ch.targetUserId, ch.gain);
			}
		}
	});

	// Start/stop VU meter animation loop based on connection
	$effect(() => {
		if (getStatus() === 'connected') {
			startVuLoop();
		} else {
			stopVuLoop();
		}
		return () => stopVuLoop();
	});

	function startVuLoop() {
		if (animFrameId !== null) return;
		function tick() {
			const newLevels = new Map<string, ChannelLevels>();
			for (const id of getChannelIdentities()) {
				const l = getChannelLevels(id);
				if (l) newLevels.set(id, l);
			}
			channelLevels = newLevels;
			masterLevels = getMasterLevels();
			animFrameId = requestAnimationFrame(tick);
		}
		animFrameId = requestAnimationFrame(tick);
	}

	function stopVuLoop() {
		if (animFrameId !== null) {
			cancelAnimationFrame(animFrameId);
			animFrameId = null;
		}
	}

	// Channel controls — update local Web Audio AND broadcast via API
	function handleGainChange(identity: string, value: number) {
		setChannelGain(identity, value);

		if (accessToken && roomId) {
			suppressRemoteSync = true;
			setMixerGain(accessToken, roomId, identity, value).catch(console.error);
			requestAnimationFrame(() => { suppressRemoteSync = false; });
		}
	}

	function handleEmergencyMute(identity: string) {
		const state = getSharedState(identity);
		const newMuted = !state.muted;

		if (newMuted) {
			muteChannel(identity);
		} else {
			unmuteChannel(identity, state.gain);
		}

		if (accessToken && roomId) {
			suppressRemoteSync = true;
			setMixerMute(accessToken, roomId, identity, newMuted).catch(console.error);
			requestAnimationFrame(() => { suppressRemoteSync = false; });
		}
	}

	// Master controls (local only — not shared across clients)
	function handleMasterGainChange(value: number) {
		setMasterGain(value);
		masterState.gain = value;
		masterState.muted = false;
	}

	function handleMasterMute() {
		if (masterState.muted) {
			unmuteMaster(masterState.gain);
			masterState.muted = false;
		} else {
			muteMaster();
			masterState.muted = true;
		}
	}

	// Access control: set a participant to viewer mode
	function handleSetRole(identity: string, role: 'editor' | 'viewer') {
		if (accessToken && roomId) {
			apiSetMixerRole(accessToken, roomId, identity, role).catch(console.error);
		}
	}

	// ─── EQ effect handling ─────────────────────────────────────────────────

	// Parse active_effects JSON into typed state
	function parseEffects(json: string | undefined): EqState & { robot: boolean; monster: boolean } {
		if (!json) return { fat_bottom: false, sparkle: false, exciter: false, robot: false, monster: false };
		try {
			const parsed = JSON.parse(json);
			return {
				fat_bottom: parsed.fat_bottom === true,
				sparkle: parsed.sparkle === true,
				exciter: parsed.exciter === true,
				robot: parsed.robot === true,
				monster: parsed.monster === true,
			};
		} catch {
			return { fat_bottom: false, sparkle: false, exciter: false, robot: false, monster: false };
		}
	}

	function getSharedEffects(identity: string): EqState & { robot: boolean; monster: boolean } {
		if (!roomId) return { fat_bottom: false, sparkle: false, exciter: false, robot: false, monster: false };
		const ch = mixerChannelStore.byParticipant(roomId, identity);
		return parseEffects(ch?.activeEffects);
	}

	function handleToggleEffect(identity: string, effect: EqEffectName) {
		const current = getSharedEffects(identity);
		const newEnabled = !current[effect];
		setChannelEffect(identity, effect, newEnabled);

		if (accessToken && roomId) {
			suppressRemoteSync = true;
			toggleMixerEffect(accessToken, roomId, identity, effect).catch(console.error);
			requestAnimationFrame(() => { suppressRemoteSync = false; });
		}
	}

	// ─── Voice effect handling ──────────────────────────────────────────────

	function handleToggleVoiceEffect(identity: string, effect: VoiceEffectName) {
		const current = getSharedEffects(identity);
		const newEnabled = !current[effect];
		const params = getVoiceParams(identity);

		if (effect === 'robot') {
			setRobotVoice(identity, newEnabled, params.robotFrequency, params.robotDepth);
		} else if (effect === 'monster') {
			setMonsterVoice(identity, newEnabled, params.monsterPitchFactor);
		}

		// Synk on/off toggle via API (params er lokale)
		if (accessToken && roomId) {
			suppressRemoteSync = true;
			toggleMixerEffect(accessToken, roomId, identity, effect).catch(console.error);
			requestAnimationFrame(() => { suppressRemoteSync = false; });
		}
	}

	function handleRobotFrequencyChange(identity: string, frequency: number) {
		const params = getVoiceParams(identity);
		params.robotFrequency = frequency;
		setRobotFrequency(identity, frequency);
	}

	function handleRobotDepthChange(identity: string, depth: number) {
		const params = getVoiceParams(identity);
		params.robotDepth = depth;
		setRobotDepth(identity, depth);
	}

	function handleMonsterPitchChange(identity: string, pitchFactor: number) {
		const params = getVoiceParams(identity);
		params.monsterPitchFactor = pitchFactor;
		setMonsterPitchFactor(identity, pitchFactor);
	}

	function handleApplyPreset(identity: string, presetName: string) {
		const preset = EQ_PRESETS.find(p => p.name === presetName);
		if (!preset) return;

		applyEqPreset(identity, preset);

		// Synk hver effekt via API
		if (accessToken && roomId) {
			suppressRemoteSync = true;
			const current = getSharedEffects(identity);
			for (const [effect, enabled] of Object.entries(preset.effects)) {
				if (current[effect as EqEffectName] !== enabled) {
					toggleMixerEffect(accessToken, roomId, identity, effect).catch(console.error);
				}
			}
			requestAnimationFrame(() => { suppressRemoteSync = false; });
		}
	}

	// Find which preset matches current effects (if any)
	function matchingPreset(effects: EqState): string {
		for (const preset of EQ_PRESETS) {
			if (preset.effects.fat_bottom === effects.fat_bottom &&
				preset.effects.sparkle === effects.sparkle &&
				preset.effects.exciter === effects.exciter) {
				return preset.name;
			}
		}
		return 'custom';
	}

	// Synk EQ-tilstand fra server til Web Audio ved fjernendringer
	$effect(() => {
		if (suppressRemoteSync) return;
		for (const ch of sharedChannels) {
			applyActiveEffectsJson(ch.targetUserId, ch.activeEffects);
		}
	});

	// VU meter helpers
	function vuPercent(levels: ChannelLevels | null | undefined): number {
		if (!levels) return 0;
		return Math.min(100, levels.rms * 100 * 3);
	}

	function vuColor(levels: ChannelLevels | null | undefined): string {
		if (!levels) return 'bg-gray-300';
		const peak = levels.peak;
		if (peak > 0.9) return 'bg-red-500';
		if (peak > 0.6) return 'bg-yellow-400';
		return 'bg-green-500';
	}

	function displayName(identity: string): string {
		const p = participants.find(p => p.identity === identity);
		if (!p) return identity;
		if (p.identity === localIdentity) return `${p.displayName} (deg)`;
		return p.displayName;
	}

	function gainToPercent(gain: number): number {
		return Math.round(gain * 100);
	}

	function channelRole(identity: string): string {
		if (!roomId) return 'editor';
		const ch = mixerChannelStore.byParticipant(roomId, identity);
		return ch?.role ?? 'editor';
	}

	// Channels that have actual audio (from mixer.ts)
	const activeChannels = $derived(getChannelIdentities());
	// Show participants that are in the room, with mixer channels where available
	const displayChannels = $derived.by(() => {
		const identities = new Set<string>();
		for (const id of activeChannels) identities.add(id);
		for (const p of participants) {
			if (p.identity !== localIdentity) identities.add(p.identity);
		}
		return Array.from(identities);
	});
</script>

<TraitPanel name="mixer" label="Mixer" icon="🎚️">
	{#snippet children()}
		{#if getStatus() !== 'connected'}
			<p class="text-sm text-gray-400">Koble til opptak-rommet for å bruke mixeren.</p>
		{:else if displayChannels.length === 0}
			<p class="text-sm text-gray-400">Ingen lydkanaler aktive.</p>
		{:else}
			<!-- Channel strips -->
			<div class="space-y-2 sm:space-y-3">
				{#each displayChannels as identity (identity)}
					{@const state = getSharedState(identity)}
					{@const levels = channelLevels.get(identity)}
					{@const isMuted = state.muted}
					{@const gain = state.gain}
					{@const role = channelRole(identity)}
					{@const isChannelViewer = role === 'viewer'}
					{@const effects = getSharedEffects(identity)}
					{@const currentPreset = matchingPreset(effects)}
					{@const vParams = getVoiceParams(identity)}

					<div class="rounded-lg border border-gray-100 bg-gray-50 p-2 sm:p-3
						{isChannelViewer ? 'opacity-60' : ''}">
						<!-- Mobile: stacked / Desktop: horizontal -->
						<div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:gap-3">
							<!-- Name label with role indicator -->
							<span class="text-xs font-medium text-gray-700 truncate sm:w-28 sm:flex-shrink-0 flex items-center gap-1">
								{displayName(identity)}
								{#if isChannelViewer}
									<span class="text-[10px] text-gray-400 uppercase" title="Kun visning">(V)</span>
								{/if}
							</span>

							<!-- VU meter -->
							<div class="h-2 flex-shrink-0 rounded-full bg-gray-200 overflow-hidden sm:w-20 sm:flex-shrink-0">
								<div
									class="h-full rounded-full transition-none {isMuted ? 'bg-gray-300' : vuColor(levels)}"
									style="width: {isMuted ? 0 : vuPercent(levels)}%"
								></div>
							</div>

							<!-- Volume slider -->
							<div class="flex items-center gap-2 flex-1 min-w-0">
								<input
									type="range"
									min="0"
									max="1.5"
									step="0.01"
									value={gain}
									oninput={(e) => handleGainChange(identity, parseFloat((e.target as HTMLInputElement).value))}
									disabled={isMuted || (isViewer && identity !== localIdentity)}
									class="flex-1 h-2 accent-indigo-600 disabled:opacity-40"
								/>
								<span class="text-xs text-gray-500 w-10 text-right tabular-nums flex-shrink-0">
									{gainToPercent(gain)}%
								</span>
							</div>

							<!-- Emergency mute button -->
							<button
								onclick={() => handleEmergencyMute(identity)}
								disabled={isViewer && identity !== localIdentity}
								class="flex-shrink-0 rounded-lg px-3 py-2 text-xs font-bold uppercase tracking-wide transition-colors
									{isMuted
										? 'bg-red-600 text-white shadow-md hover:bg-red-700'
										: 'bg-red-100 text-red-700 hover:bg-red-200'}
									disabled:opacity-40 disabled:cursor-not-allowed"
								title={isMuted ? 'Slå på lyd' : 'Nøddemp'}
							>
								{isMuted ? 'DEMPET' : 'DEMP'}
							</button>
						</div>

						<!-- Effect toggles row -->
						<div class="mt-2 flex flex-wrap items-center gap-1.5">
							<!-- EQ effects -->
							<span class="text-[10px] text-gray-400 uppercase tracking-wider mr-1">EQ</span>
							<button
								onclick={() => handleToggleEffect(identity, 'fat_bottom')}
								disabled={isViewer && identity !== localIdentity}
								class="rounded px-2 py-0.5 text-[11px] font-medium transition-colors
									{effects.fat_bottom
										? 'bg-amber-500 text-white'
										: 'bg-gray-200 text-gray-500 hover:bg-gray-300'}
									disabled:opacity-40 disabled:cursor-not-allowed"
								title="Fat bottom: lavfrekvent fylde (+8dB @ 200Hz)"
							>
								Bass
							</button>
							<button
								onclick={() => handleToggleEffect(identity, 'sparkle')}
								disabled={isViewer && identity !== localIdentity}
								class="rounded px-2 py-0.5 text-[11px] font-medium transition-colors
									{effects.sparkle
										? 'bg-sky-500 text-white'
										: 'bg-gray-200 text-gray-500 hover:bg-gray-300'}
									disabled:opacity-40 disabled:cursor-not-allowed"
								title="Sparkle: høyfrekvent luft (+4dB @ 10kHz)"
							>
								Luft
							</button>
							<button
								onclick={() => handleToggleEffect(identity, 'exciter')}
								disabled={isViewer && identity !== localIdentity}
								class="rounded px-2 py-0.5 text-[11px] font-medium transition-colors
									{effects.exciter
										? 'bg-purple-500 text-white'
										: 'bg-gray-200 text-gray-500 hover:bg-gray-300'}
									disabled:opacity-40 disabled:cursor-not-allowed"
								title="Exciter: harmonisk tilstedeværelse (3.5kHz)"
							>
								Exciter
							</button>

							<!-- Separator -->
							<span class="text-gray-300 mx-0.5">|</span>

							<!-- Voice effects -->
							<span class="text-[10px] text-gray-400 uppercase tracking-wider mr-1">FX</span>
							<button
								onclick={() => handleToggleVoiceEffect(identity, 'robot')}
								disabled={isViewer && identity !== localIdentity}
								class="rounded px-2 py-0.5 text-[11px] font-medium transition-colors
									{effects.robot
										? 'bg-emerald-500 text-white'
										: 'bg-gray-200 text-gray-500 hover:bg-gray-300'}
									disabled:opacity-40 disabled:cursor-not-allowed"
								title="Robotstemme: ring-modulasjon (metallisk, Dalek-aktig)"
							>
								Robot
							</button>
							<button
								onclick={() => handleToggleVoiceEffect(identity, 'monster')}
								disabled={isViewer && identity !== localIdentity}
								class="rounded px-2 py-0.5 text-[11px] font-medium transition-colors
									{effects.monster
										? 'bg-rose-500 text-white'
										: 'bg-gray-200 text-gray-500 hover:bg-gray-300'}
									disabled:opacity-40 disabled:cursor-not-allowed"
								title="Monsterstemme: pitch shift ned (dyp, mørk stemme)"
							>
								Monster
							</button>

							<!-- Preset selector -->
							<select
								onchange={(e) => handleApplyPreset(identity, (e.target as HTMLSelectElement).value)}
								disabled={isViewer && identity !== localIdentity}
								class="ml-auto rounded border border-gray-200 bg-white px-1.5 py-0.5 text-[11px] text-gray-600
									disabled:opacity-40 disabled:cursor-not-allowed"
								title="EQ-preset"
							>
								{#each EQ_PRESETS as preset (preset.name)}
									<option value={preset.name} selected={currentPreset === preset.name}>
										{preset.label}
									</option>
								{/each}
								{#if currentPreset === 'custom'}
									<option value="custom" selected disabled>Egendefinert</option>
								{/if}
							</select>
						</div>

						<!-- Robot voice parameter sliders (shown when robot is active) -->
						{#if effects.robot}
							<div class="mt-2 rounded bg-emerald-50 border border-emerald-100 p-2 space-y-1.5">
								<div class="flex items-center gap-2">
									<span class="text-[10px] text-emerald-600 font-medium w-14 flex-shrink-0">Frekvens</span>
									<input
										type="range"
										min="30"
										max="300"
										step="1"
										value={vParams.robotFrequency}
										oninput={(e) => handleRobotFrequencyChange(identity, parseFloat((e.target as HTMLInputElement).value))}
										disabled={isViewer && identity !== localIdentity}
										class="flex-1 h-1.5 accent-emerald-600 disabled:opacity-40"
									/>
									<span class="text-[10px] text-emerald-600 tabular-nums w-12 text-right flex-shrink-0">
										{vParams.robotFrequency} Hz
									</span>
								</div>
								<div class="flex items-center gap-2">
									<span class="text-[10px] text-emerald-600 font-medium w-14 flex-shrink-0">Dybde</span>
									<input
										type="range"
										min="0"
										max="1"
										step="0.05"
										value={vParams.robotDepth}
										oninput={(e) => handleRobotDepthChange(identity, parseFloat((e.target as HTMLInputElement).value))}
										disabled={isViewer && identity !== localIdentity}
										class="flex-1 h-1.5 accent-emerald-600 disabled:opacity-40"
									/>
									<span class="text-[10px] text-emerald-600 tabular-nums w-12 text-right flex-shrink-0">
										{Math.round(vParams.robotDepth * 100)}%
									</span>
								</div>
							</div>
						{/if}

						<!-- Monster voice parameter slider (shown when monster is active) -->
						{#if effects.monster}
							<div class="mt-2 rounded bg-rose-50 border border-rose-100 p-2">
								<div class="flex items-center gap-2">
									<span class="text-[10px] text-rose-600 font-medium w-14 flex-shrink-0">Pitch</span>
									<input
										type="range"
										min="0.5"
										max="2.0"
										step="0.05"
										value={vParams.monsterPitchFactor}
										oninput={(e) => handleMonsterPitchChange(identity, parseFloat((e.target as HTMLInputElement).value))}
										disabled={isViewer && identity !== localIdentity}
										class="flex-1 h-1.5 accent-rose-600 disabled:opacity-40"
									/>
									<span class="text-[10px] text-rose-600 tabular-nums w-12 text-right flex-shrink-0">
										{vParams.monsterPitchFactor.toFixed(2)}x
									</span>
								</div>
								<div class="flex justify-between text-[9px] text-rose-400 mt-0.5 px-1">
									<span>Dyp</span>
									<span>Normal</span>
									<span>Høy</span>
								</div>
							</div>
						{/if}
					</div>
				{/each}
			</div>

			<!-- Sound Pads -->
			<div class="mt-4">
				<SoundPadGrid {collection} {accessToken} {isViewer} />
			</div>

			<!-- Master section -->
			<div class="mt-4 rounded-lg border-2 border-gray-200 bg-white p-3">
				<div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:gap-3">
					<span class="text-xs font-bold text-gray-800 uppercase tracking-wide sm:w-28 sm:flex-shrink-0">
						Master
					</span>

					<!-- Master VU meter -->
					<div class="h-2.5 flex-shrink-0 rounded-full bg-gray-200 overflow-hidden sm:w-20 sm:flex-shrink-0">
						<div
							class="h-full rounded-full transition-none {masterState.muted ? 'bg-gray-300' : vuColor(masterLevels)}"
							style="width: {masterState.muted ? 0 : vuPercent(masterLevels)}%"
						></div>
					</div>

					<!-- Master slider -->
					<div class="flex items-center gap-2 flex-1 min-w-0">
						<input
							type="range"
							min="0"
							max="1.5"
							step="0.01"
							value={masterState.gain}
							oninput={(e) => handleMasterGainChange(parseFloat((e.target as HTMLInputElement).value))}
							disabled={masterState.muted}
							class="flex-1 h-2 accent-indigo-600 disabled:opacity-40"
						/>
						<span class="text-xs text-gray-500 w-10 text-right tabular-nums flex-shrink-0">
							{gainToPercent(masterState.gain)}%
						</span>
					</div>

					<!-- Master mute -->
					<button
						onclick={handleMasterMute}
						class="flex-shrink-0 rounded-lg px-4 py-2.5 text-sm font-bold uppercase tracking-wide transition-colors
							{masterState.muted
								? 'bg-red-600 text-white shadow-lg hover:bg-red-700 ring-2 ring-red-300'
								: 'bg-red-100 text-red-700 hover:bg-red-200'}"
						title={masterState.muted ? 'Slå på master' : 'Demp alt'}
					>
						{masterState.muted ? 'DEMPET' : 'DEMP'}
					</button>
				</div>
			</div>
		{/if}
	{/snippet}
</TraitPanel>
