'use client'

import {
	createContext,
	useCallback,
	useContext,
	useEffect,
	useMemo,
	useRef,
	useState,
	type ReactNode,
} from "react";
import { trpc } from "~/app/_trpc/Client";
import MusicMiniPlayer from "./MusicMiniPlayer";

export type PlayerTrack = {
	id: string;
	title: string;
	/** Streaming-friendly source actually played. */
	src: string;
	/** Original high-quality file for the download button. */
	downloadUrl: string;
	downloadName: string;
};

type MusicPlayerValue = {
	tracks: PlayerTrack[];
	currentId: string | null;
	currentTrack: PlayerTrack | null;
	isPlaying: boolean;
	shuffle: boolean;
	currentTime: number;
	duration: number;
	/** Play button on a track: start it, or toggle play/pause if already current. */
	toggle: (id: string) => void;
	/** Global play/pause — acts on the current track (or starts the first one). */
	togglePlayCurrent: () => void;
	next: () => void;
	previous: () => void;
	toggleShuffle: () => void;
	seek: (seconds: number) => void;
	/** Subscribe to playback time updates (for waveform cursors). */
	subscribeTime: (cb: (time: number) => void) => () => void;
};

const MusicPlayerContext = createContext<MusicPlayerValue | null>(null);

export function useMusicPlayer() {
	const ctx = useContext(MusicPlayerContext);
	if (!ctx) throw new Error("useMusicPlayer must be used within a MusicPlayerProvider");
	return ctx;
}

export function MusicPlayerProvider({ children }: { children: ReactNode }) {
	// The provider owns the playlist so playback survives navigating away from
	// the music page. The query is cached, so the music page shares this request.
	const { data } = trpc.music.list.useQuery();
	const tracks = useMemo<PlayerTrack[]>(
		() =>
			(data ?? []).map((t) => ({
				id: t.id,
				title: t.title,
				src: t.streamUrl ?? t.fileUrl,
				downloadUrl: t.fileUrl,
				downloadName: t.fileName,
			})),
		[data],
	);

	const [currentId, setCurrentId] = useState<string | null>(null);
	const [isPlaying, setIsPlaying] = useState(false);
	const [shuffle, setShuffle] = useState(false);
	const [currentTime, setCurrentTime] = useState(0);
	const [duration, setDuration] = useState(0);

	const audioRef = useRef<HTMLAudioElement | null>(null);
	const tracksRef = useRef(tracks);
	tracksRef.current = tracks;

	// Lightweight pub/sub so each waveform can follow playback time without every
	// player re-rendering on the audio element's frequent timeupdate.
	const timeSubs = useRef<Set<(t: number) => void>>(new Set());
	const subscribeTime = useCallback((cb: (t: number) => void) => {
		timeSubs.current.add(cb);
		return () => {
			timeSubs.current.delete(cb);
		};
	}, []);
	const emitTime = useCallback((t: number) => {
		timeSubs.current.forEach((cb) => cb(t));
	}, []);

	const currentTrack = useMemo(
		() => tracks.find((t) => t.id === currentId) ?? null,
		[tracks, currentId],
	);

	const toggle = useCallback((id: string) => {
		setCurrentId((prev) => {
			if (prev === id) {
				setIsPlaying((p) => !p);
				return prev;
			}
			setIsPlaying(true);
			return id;
		});
	}, []);

	const togglePlayCurrent = useCallback(() => {
		setCurrentId((prev) => {
			if (prev) {
				setIsPlaying((p) => !p);
				return prev;
			}
			const first = tracksRef.current[0]?.id ?? null;
			if (first) setIsPlaying(true);
			return first;
		});
	}, []);

	const step = useCallback((dir: 1 | -1) => {
		const ids = tracksRef.current.map((t) => t.id);
		if (ids.length === 0) return;
		setCurrentId((prev) => {
			let nextId: string;
			if (shuffle && dir === 1 && ids.length > 1) {
				do {
					nextId = ids[Math.floor(Math.random() * ids.length)]!;
				} while (nextId === prev);
			} else {
				const idx = prev ? ids.indexOf(prev) : -1;
				nextId = ids[(idx + dir + ids.length) % ids.length]!;
			}
			setIsPlaying(true);
			return nextId;
		});
	}, [shuffle]);
	const next = useCallback(() => step(1), [step]);
	const previous = useCallback(() => step(-1), [step]);
	const toggleShuffle = useCallback(() => setShuffle((s) => !s), []);

	const stepRef = useRef(step);
	stepRef.current = step;

	const seek = useCallback(
		(s: number) => {
			if (audioRef.current) audioRef.current.currentTime = s;
			setCurrentTime(s);
			emitTime(s);
		},
		[emitTime],
	);

	// Persistent audio element + listeners, created once on the client.
	useEffect(() => {
		const audio = new Audio();
		audio.preload = "metadata";
		audioRef.current = audio;
		const onTime = () => {
			setCurrentTime(audio.currentTime);
			emitTime(audio.currentTime);
		};
		const onMeta = () => setDuration(audio.duration || 0);
		const onEnded = () => stepRef.current(1);
		audio.addEventListener("timeupdate", onTime);
		audio.addEventListener("loadedmetadata", onMeta);
		audio.addEventListener("ended", onEnded);
		return () => {
			audio.pause();
			audio.removeEventListener("timeupdate", onTime);
			audio.removeEventListener("loadedmetadata", onMeta);
			audio.removeEventListener("ended", onEnded);
			audioRef.current = null;
		};
	}, [emitTime]);

	// Swap the source when the current track changes.
	useEffect(() => {
		const audio = audioRef.current;
		if (!audio) return;
		if (!currentTrack) {
			audio.removeAttribute("src");
			audio.load();
			return;
		}
		if (audio.src !== currentTrack.src) {
			audio.src = currentTrack.src;
			audio.load();
			setCurrentTime(0);
			setDuration(0);
		}
	}, [currentTrack]);

	// Reflect the desired play state onto the element.
	useEffect(() => {
		const audio = audioRef.current;
		if (!audio || !currentTrack) return;
		if (isPlaying) audio.play().catch(() => {});
		else audio.pause();
	}, [isPlaying, currentTrack]);

	// OS-level media controls (lock screen, notification shade, media keys, etc.)
	// via the Media Session API. Wire the transport actions to our state.
	useEffect(() => {
		if (typeof navigator === "undefined" || !("mediaSession" in navigator)) return;
		const ms = navigator.mediaSession;
		const handlers: [MediaSessionAction, MediaSessionActionHandler][] = [
			["play", () => setIsPlaying(true)],
			["pause", () => setIsPlaying(false)],
			["previoustrack", () => stepRef.current(-1)],
			["nexttrack", () => stepRef.current(1)],
			[
				"seekto",
				(details) => {
					if (typeof details.seekTime === "number") seek(details.seekTime);
				},
			],
			[
				"seekbackward",
				(details) => seek((audioRef.current?.currentTime ?? 0) - (details.seekOffset ?? 10)),
			],
			[
				"seekforward",
				(details) => seek((audioRef.current?.currentTime ?? 0) + (details.seekOffset ?? 10)),
			],
		];
		for (const [action, handler] of handlers) {
			try {
				ms.setActionHandler(action, handler);
			} catch {
				// Action unsupported by this browser — ignore.
			}
		}
		return () => {
			for (const [action] of handlers) {
				try {
					ms.setActionHandler(action, null);
				} catch {
					// ignore
				}
			}
		};
	}, [seek]);

	// Keep the OS-visible metadata in sync with the current track.
	useEffect(() => {
		if (typeof navigator === "undefined" || !("mediaSession" in navigator)) return;
		navigator.mediaSession.metadata = currentTrack
			? new MediaMetadata({
					title: currentTrack.title,
					artist: "Gregor Lohaus",
				})
			: null;
	}, [currentTrack]);

	// Reflect play/pause state to the OS so the right button is shown.
	useEffect(() => {
		if (typeof navigator === "undefined" || !("mediaSession" in navigator)) return;
		navigator.mediaSession.playbackState = currentTrack
			? isPlaying
				? "playing"
				: "paused"
			: "none";
	}, [isPlaying, currentTrack]);

	// Keep the scrubber position on the OS controls in sync.
	useEffect(() => {
		if (typeof navigator === "undefined" || !("mediaSession" in navigator)) return;
		if (!("setPositionState" in navigator.mediaSession)) return;
		try {
			if (duration > 0 && Number.isFinite(duration)) {
				navigator.mediaSession.setPositionState({
					duration,
					position: Math.min(currentTime, duration),
					playbackRate: audioRef.current?.playbackRate ?? 1,
				});
			} else {
				navigator.mediaSession.setPositionState();
			}
		} catch {
			// Some browsers throw on invalid state — ignore.
		}
	}, [currentTime, duration]);

	const value = useMemo<MusicPlayerValue>(
		() => ({
			tracks,
			currentId,
			currentTrack,
			isPlaying,
			shuffle,
			currentTime,
			duration,
			toggle,
			togglePlayCurrent,
			next,
			previous,
			toggleShuffle,
			seek,
			subscribeTime,
		}),
		[
			tracks, currentId, currentTrack, isPlaying, shuffle, currentTime, duration,
			toggle, togglePlayCurrent, next, previous, toggleShuffle, seek, subscribeTime,
		],
	);

	return (
		<MusicPlayerContext.Provider value={value}>
			{children}
			<MusicMiniPlayer />
		</MusicPlayerContext.Provider>
	);
}
