import { z } from "zod";
import { browser } from "$app/environment";
import type { Track } from "./types";

/**
 * Persistence for the global player.
 *
 * Persisted:
 * - queue
 * - currentIndex
 * - shuffleEnabled
 * - wrapEnabled
 * - shuffle traversal bookkeeping (order/history/cursor)
 * - volume
 * - uiOpen
 *
 * Not persisted by design:
 * - currentTime / playback position
 * - isPlaying (we always restore paused)
 */

const STORAGE_KEY = "amqtrain:player:v1";
const STORAGE_VERSION = 1;

const TrackSchema = z
	.object({
		id: z.number().int().nonnegative(),
		src: z.string().min(1),
		title: z.string().default(""),
		artist: z.string().default(""),
		album: z.string().default(""),

		animeName: z.string().optional(),
		type: z.number().optional(),
		number: z.number().optional(),
		fileName: z.string().nullable().optional(),
	})
	.strict();

const PersistedSnapshotSchema = z
	.object({
		version: z.literal(STORAGE_VERSION),

		queue: z.array(TrackSchema).default([]),
		currentIndex: z.number().int().nullable().default(null),

		shuffleEnabled: z.boolean().default(false),
		wrapEnabled: z.boolean().default(false),

		/**
		 * Shuffle traversal:
		 * - order: upcoming indices into `queue` in the order they will be visited
		 * - history: visited indices into `queue` in visit order
		 * - cursor: index into `history` pointing at the current item
		 */
		order: z.array(z.number().int().nonnegative()).default([]),
		history: z.array(z.number().int().nonnegative()).default([]),
		cursor: z.number().int().default(0),

		volume: z.number().min(0).max(1).default(1),
		uiOpen: z.boolean().default(false),
	})
	.strict();

export type PersistedSnapshot = z.infer<typeof PersistedSnapshotSchema>;

export type PersistablePlayerState = {
	queue: Track[];
	currentIndex: number | null;

	shuffleEnabled: boolean;
	wrapEnabled: boolean;

	order: number[];
	history: number[];
	cursor: number;

	volume: number;
	uiOpen: boolean;
};

export function loadPersistedPlayerState(): PersistablePlayerState | null {
	if (!browser) return null;

	try {
		const raw = localStorage.getItem(STORAGE_KEY);
		if (!raw) return null;

		const parsed = PersistedSnapshotSchema.safeParse(JSON.parse(raw));
		if (!parsed.success) return null;

		return sanitizePersistedState(parsed.data);
	} catch {
		return null;
	}
}

export function savePersistedPlayerState(state: PersistablePlayerState): void {
	if (!browser) return;

	const snapshot: PersistedSnapshot = {
		version: STORAGE_VERSION,

		queue: state.queue,
		currentIndex: state.currentIndex,

		shuffleEnabled: state.shuffleEnabled,
		wrapEnabled: state.wrapEnabled,

		order: state.order,
		history: state.history,
		cursor: state.cursor,

		volume: clamp01(state.volume),
		uiOpen: state.uiOpen,
	};

	try {
		localStorage.setItem(STORAGE_KEY, JSON.stringify(snapshot));
	} catch {
		// Ignore quota/security errors; persistence is a best-effort feature.
	}
}

/**
 * Throttled saver (simple debounce). Call this from reactive effects.
 */
export function createPersistScheduler(delayMs = 250) {
	let timeout: ReturnType<typeof setTimeout> | null = null;

	return (state: PersistablePlayerState) => {
		if (!browser) return;

		if (timeout) clearTimeout(timeout);
		timeout = setTimeout(() => {
			timeout = null;
			savePersistedPlayerState(state);
		}, delayMs);
	};
}

export function clearPersistedPlayerState(): void {
	if (!browser) return;
	try {
		localStorage.removeItem(STORAGE_KEY);
	} catch {
		// ignore
	}
}

function sanitizePersistedState(
	snapshot: PersistedSnapshot,
): PersistablePlayerState {
	const queue = dedupeById(snapshot.queue);

	const maxIndex = queue.length - 1;
	const currentIndex =
		snapshot.currentIndex == null
			? null
			: snapshot.currentIndex >= 0 && snapshot.currentIndex <= maxIndex
				? snapshot.currentIndex
				: null;

	const order = filterValidIndices(snapshot.order, queue.length);
	const history = filterValidIndices(snapshot.history, queue.length);

	// cursor points into history; if history is empty, cursor should be 0
	const cursor =
		history.length === 0
			? 0
			: Math.max(0, Math.min(snapshot.cursor, history.length - 1));

	// If we have a currentIndex but history doesn't reflect it, try to repair:
	// put currentIndex at end and point cursor there.
	let repairedHistory = history;
	let repairedCursor = cursor;

	if (currentIndex != null) {
		if (history.length === 0 || history[cursor] !== currentIndex) {
			repairedHistory = [...history, currentIndex];
			repairedCursor = repairedHistory.length - 1;
		}
	}

	// Ensure order doesn't contain items already visited up through cursor.
	const visitedSet =
		repairedHistory.length === 0
			? new Set<number>()
			: new Set(repairedHistory.slice(0, repairedCursor + 1));
	const repairedOrder = order.filter((i) => !visitedSet.has(i));

	return {
		queue,
		currentIndex,

		shuffleEnabled: snapshot.shuffleEnabled,
		wrapEnabled: snapshot.wrapEnabled,

		order: repairedOrder,
		history: repairedHistory,
		cursor: repairedCursor,

		volume: clamp01(snapshot.volume),
		uiOpen: snapshot.uiOpen,
	};
}

function filterValidIndices(indices: number[], length: number) {
	const out: number[] = [];
	for (const i of indices) {
		if (Number.isInteger(i) && i >= 0 && i < length) out.push(i);
	}
	return out;
}

function dedupeById(tracks: Track[]) {
	const seen = new Set<number>();
	const out: Track[] = [];
	for (const t of tracks) {
		const id = Number(t.id);
		if (!Number.isFinite(id)) continue;
		if (seen.has(id)) continue;
		seen.add(id);
		out.push(t);
	}
	return out;
}

function clamp01(n: number) {
	if (!Number.isFinite(n)) return 1;
	return Math.max(0, Math.min(1, n));
}
