import { fm } from '$lib/lexicons';
import type { Main as PlayRecord } from '$lib/lexicons/fm/teal/alpha/feed/play';
import type { AtUriString } from '@atproto/lex';
import type { NowPlaying, RecentPlay } from '$lib/types';
import { cache, MIN_MS } from '$lib/server/cache';
import { createClient, getRepoInfo, repoId, type RepoInfo } from './client';
import { resolveCoverArt } from './coverArt';

const RECENT_PLAYS_LIMIT = 50;
const COVER_ART_CONCURRENCY = 5;

/** Resolve `fn` over `items` with a bounded number of in-flight calls. */
async function mapWithConcurrency<T, R>(
	items: T[],
	limit: number,
	fn: (item: T, index: number) => Promise<R>
): Promise<R[]> {
	const results = new Array<R>(items.length);
	let cursor = 0;
	const workers = Array.from({ length: Math.min(limit, items.length) }, async () => {
		while (cursor < items.length) {
			const index = cursor++;
			results[index] = await fn(items[index], index);
		}
	});
	await Promise.all(workers);
	return results;
}

/**
 * A teal status is only "live" until its expiry — which defaults to 10 minutes
 * past the start time when the record omits one. Past that, the actor isn't
 * actively listening and we should fall back to their last play.
 */
function isStatusLive(status: { time: string; expiry?: string }): boolean {
	const expiryMs = status.expiry
		? Date.parse(status.expiry)
		: Date.parse(status.time) + 10 * MIN_MS;
	return Number.isFinite(expiryMs) && expiryMs > Date.now();
}

/** Most recently listened track, shaped as a NowPlaying fallback (playedTime set). */
async function fetchLastPlay(
	fetch: typeof globalThis.fetch,
	repo: RepoInfo
): Promise<NowPlaying | null> {
	const client = createClient(repo.pds);
	// Fetch a small batch and pick the newest defensively. (This PDS returns
	// records newest-first by default; passing `reverse: true` returns none.)
	const res = await client.list(fm.teal.alpha.feed.play, {
		repo: repoId(repo.did),
		limit: 5
	});
	const records = res.records as ReadonlyArray<{ value: PlayRecord }>;
	if (records.length === 0) return null;

	const value = [...records].sort((a, b) =>
		(b.value.playedTime ?? '').localeCompare(a.value.playedTime ?? '')
	)[0].value;
	// History uses the release MBID directly — no MusicBrainz ISRC lookup.
	const coverArt = await resolveCoverArt(fetch, {
		releaseMbId: value.releaseMbId ?? null,
		releaseName: value.releaseName ?? null
	});

	return {
		trackName: value.trackName,
		artists: value.artists.map((a) => a.artistName).join(', '),
		releaseName: value.releaseName ?? null,
		url: value.originUrl ?? null,
		coverArt,
		playedTime: value.playedTime ?? null
	};
}

export async function fetchNowPlaying(fetch: typeof globalThis.fetch): Promise<NowPlaying | null> {
	const cached = cache.get('nowPlaying') as { value: NowPlaying | null } | null;
	if (cached) return cached.value;

	const repo = getRepoInfo('Now playing');
	if (!repo) return null;

	try {
		const client = createClient(repo.pds);
		const status = await client
			.get(fm.teal.alpha.actor.status, { repo: repoId(repo.did) })
			.then((res) => res.value)
			.catch(() => null); // no status record yet — treat as not currently listening

		let nowPlaying: NowPlaying | null;
		if (status && isStatusLive(status)) {
			const item = status.item;
			const coverArt = await resolveCoverArt(fetch, {
				isrc: item.isrc ?? null,
				releaseMbId: item.releaseMbId ?? null,
				releaseName: item.releaseName ?? null,
				useIsrcLookup: true
			});
			nowPlaying = {
				trackName: item.trackName,
				artists: item.artists.map((a) => a.artistName).join(', '),
				releaseName: item.releaseName ?? null,
				url: item.originUrl ?? null,
				coverArt,
				playedTime: null
			};
		} else {
			// Nothing playing right now — show the most recently listened track instead.
			nowPlaying = await fetchLastPlay(fetch, repo);
		}

		cache.setWithExpiry('nowPlaying', { value: nowPlaying }, MIN_MS);
		return nowPlaying;
	} catch (error) {
		console.warn('Failed to fetch now playing status:', error);
		return null;
	}
}

export async function fetchRecentPlays(fetch: typeof globalThis.fetch): Promise<RecentPlay[]> {
	const cached = cache.get('recentPlays') as RecentPlay[] | null;
	if (cached) return cached;

	const repo = getRepoInfo('Music history');
	if (!repo) return [];

	try {
		const client = createClient(repo.pds);
		const res = await client.list(fm.teal.alpha.feed.play, {
			repo: repoId(repo.did),
			limit: RECENT_PLAYS_LIMIT
		});
		const records = res.records as ReadonlyArray<{ value: PlayRecord; uri: AtUriString }>;

		// TID rkeys are already time-ordered, but sort defensively (newest first).
		const sorted = [...records].sort((a, b) =>
			(b.value.playedTime ?? '').localeCompare(a.value.playedTime ?? '')
		);

		const plays = await mapWithConcurrency(sorted, COVER_ART_CONCURRENCY, async ({ value }) => {
			// History uses the release MBID directly — no MusicBrainz ISRC lookup.
			const coverArt = await resolveCoverArt(fetch, {
				releaseMbId: value.releaseMbId ?? null,
				releaseName: value.releaseName ?? null
			});

			return {
				trackName: value.trackName,
				artists: value.artists.map((a) => a.artistName).join(', '),
				releaseName: value.releaseName ?? null,
				url: value.originUrl ?? null,
				coverArt,
				playedTime: value.playedTime ?? null
			} satisfies RecentPlay;
		});

		cache.setWithExpiry('recentPlays', plays, MIN_MS * 5);
		return plays;
	} catch (error) {
		console.warn('Failed to fetch recent plays:', error);
		return [];
	}
}
