import { env } from '$env/dynamic/private';
import { Client, getBlobCidString, type AtIdentifierString, type AtUriString } from '@atproto/lex';
import { buzz, fm, site } from '$lib/lexicons';
import type { Main as PublicationRecord } from '$lib/lexicons/site/standard/publication';
import type { Main as BookRecord } from '$lib/lexicons/buzz/bookhive/book';
import type { CurrentlyReading, NowPlaying, Publication, Sponsor } from '$lib/types';
import type { PageServerLoad } from './$types';
import { SQLiteCache } from 'sqlite-cache';

const SPONSORS_LOGIN = 'fatfingers23';
const CACHE_TTL_MS = 86_400_000; // 1 day
const LIVE_CACHE_TTL_MS = 60_000; // 1 min — "now playing" changes often

const cache = new SQLiteCache({ path: './cache.db', ttl: CACHE_TTL_MS });
const liveCache = new SQLiteCache({ path: './cache-live.db', ttl: LIVE_CACHE_TTL_MS });

type SponsorNode = {
	login: string;
	name: string | null;
	avatarUrl: string;
	url: string;
};

const SPONSORS_QUERY = `
	query ($login: String!) {
		user(login: $login) {
			sponsorshipsAsMaintainer(first: 100, includePrivate: false, activeOnly: true) {
				nodes {
					sponsorEntity {
						... on User { login name avatarUrl(size: 160) url }
						... on Organization { login name avatarUrl(size: 160) url }
					}
				}
			}
		}
	}
`;

async function fetchSponsors(fetch: typeof globalThis.fetch): Promise<Sponsor[]> {
	// if (cache && Date.now() - cache.fetchedAt < CACHE_TTL_MS) {
	// 	return cache.sponsors;
	// }

	const cached = cache.get('sponsors');
	if (cached) {
		return cached as Sponsor[];
	}

	const token = env.GITHUB_TOKEN;
	if (!token) {
		console.warn('GITHUB_TOKEN is not set — Sponsors section will be hidden.');
		return [];
	}

	try {
		const res = await fetch('https://api.github.com/graphql', {
			method: 'POST',
			headers: {
				Authorization: `Bearer ${token}`,
				'Content-Type': 'application/json'
			},
			body: JSON.stringify({
				query: SPONSORS_QUERY,
				variables: { login: SPONSORS_LOGIN }
			})
		});

		if (!res.ok) {
			console.warn(`GitHub Sponsors request failed: ${res.status} ${res.statusText}`);
			return [];
		}

		const json = await res.json();
		if (json.errors) {
			console.warn('GitHub Sponsors GraphQL errors:', json.errors);
			return [];
		}

		const nodes: { sponsorEntity: SponsorNode | null }[] =
			json.data?.user?.sponsorshipsAsMaintainer?.nodes ?? [];
		const sponsors: Sponsor[] = nodes
			.map((node) => node.sponsorEntity)
			.filter((entity): entity is SponsorNode => Boolean(entity?.login))
			.map((entity) => ({
				login: entity.login,
				name: entity.name ?? entity.login,
				avatarUrl: entity.avatarUrl ?? `https://github.com/${entity.login}.png`,
				url: entity.url
			}));

		cache.set('sponsors', sponsors);
		return sponsors;
	} catch (error) {
		console.warn('Failed to fetch GitHub Sponsors:', error);
		return [];
	}
}

async function fetchPublications(): Promise<Publication[]> {
	const cached = cache.get('publications');
	if (cached) {
		return cached as Publication[];
	}

	const did = env.ATPROTO_DID;
	const pds = env.ATPROTO_PDS;
	if (!did || !pds) {
		console.warn('ATPROTO_DID / ATPROTO_PDS are not set — Writing section will be hidden.');
		return [];
	}

	try {
		const client = new Client(pds);
		const res = await client.list(site.standard.publication, {
			repo: did as AtIdentifierString,
			limit: 50
		});
		const records = res.records as ReadonlyArray<{ value: PublicationRecord; uri: AtUriString }>;

		const publications: Publication[] = records
			.filter((x) => !x.uri.includes('blento.self'))
			.map(({ value }) => ({
				name: value.name,
				description: value.description ?? '',
				href: value.url,
				image: value.icon
					? `${pds}/xrpc/com.atproto.sync.getBlob?did=${encodeURIComponent(did)}&cid=${getBlobCidString(value.icon)}`
					: null
			}));

		cache.set('publications', publications);
		return publications;
	} catch (error) {
		console.warn('Failed to fetch publications:', error);
		return [];
	}
}

async function fetchCurrentlyReading(): Promise<CurrentlyReading | null> {
	const cached = cache.get('currentlyReading') as { value: CurrentlyReading | null } | null;
	if (cached) {
		return cached.value;
	}

	const did = env.ATPROTO_DID;
	const pds = env.ATPROTO_PDS;
	if (!did || !pds) {
		console.warn('ATPROTO_DID / ATPROTO_PDS are not set — Currently reading will be hidden.');
		return null;
	}

	try {
		const client = new Client(pds);
		const res = await client.list(buzz.bookhive.book, {
			repo: did as AtIdentifierString,
			limit: 100
		});
		const records = res.records as ReadonlyArray<{ value: BookRecord; uri: AtUriString }>;
		const reading = records.find((r) => r.value.status === 'buzz.bookhive.defs#reading');

		let currentlyReading: CurrentlyReading | null = null;
		if (reading) {
			const v = reading.value;
			currentlyReading = {
				title: v.title,
				bookUrl: `https://bookhive.buzz/books/${v.hiveId}`,
				authors: v.authors.split('\t').join(', '),
				cover: v.cover
					? `${pds}/xrpc/com.atproto.sync.getBlob?did=${encodeURIComponent(did)}&cid=${getBlobCidString(v.cover)}`
					: null
			};
		}

		cache.set('currentlyReading', { value: currentlyReading });
		return currentlyReading;
	} catch (error) {
		console.warn('Failed to fetch currently reading book:', error);
		return null;
	}
}

// MusicBrainz requires an identifying User-Agent or it will block requests.
const MB_USER_AGENT = 'baileys-website/1.0 (https://tangled.org/pds.dad/my-website)';

/// Got to go isrc to a mbid to get cover art
async function resolveCoverArt(
	fetch: typeof globalThis.fetch,
	opts: { isrc?: string | null; releaseMbId?: string | null; releaseName?: string | null }
): Promise<string | null> {
	const candidates: string[] = [];

	if (opts.isrc) {
		try {
			const isrcRes = await fetch(
				`https://musicbrainz.org/ws/2/isrc/${encodeURIComponent(opts.isrc)}?fmt=json`,
				{ headers: { 'User-Agent': MB_USER_AGENT } }
			);
			const recordingId = isrcRes.ok
				? ((await isrcRes.json()) as { recordings?: { id: string }[] }).recordings?.[0]?.id
				: undefined;

			if (recordingId) {
				const relRes = await fetch(
					`https://musicbrainz.org/ws/2/release?recording=${recordingId}&fmt=json`,
					{ headers: { 'User-Agent': MB_USER_AGENT } }
				);
				if (relRes.ok) {
					const releases =
						((await relRes.json()) as { releases?: { id: string; title?: string }[] }).releases ??
						[];
					// An ISRC's recording often appears on many releases (singles,
					// comps, regional editions). Prefer the one whose title matches
					// what's playing.
					const wanted = opts.releaseName?.toLowerCase();
					const seen = new Set<string>();
					for (const r of [
						...releases.filter((r) => wanted && r.title?.toLowerCase() === wanted),
						...releases
					]) {
						if (!seen.has(r.id)) {
							seen.add(r.id);
							candidates.push(r.id);
						}
					}
				}
			}
		} catch (error) {
			console.warn('MusicBrainz ISRC lookup failed:', error);
		}
	}

	// Fallback: release MBID supplied directly by the status record.
	if (opts.releaseMbId) candidates.push(opts.releaseMbId);

	// Not every release has art in the CAA — probe candidates and return the
	// first that resolves to an actual image.
	for (const mbid of candidates.slice(0, 5)) {
		const url = `https://coverartarchive.org/release/${mbid}/front-500`;
		try {
			const res = await fetch(url, { method: 'HEAD', redirect: 'follow' });
			if (res.ok) return url;
		} catch {
			// ignore and try the next candidate
		}
	}

	return null;
}

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

	const did = env.ATPROTO_DID;
	const pds = env.ATPROTO_PDS;
	if (!did || !pds) {
		console.warn('ATPROTO_DID / ATPROTO_PDS are not set — Now playing will be hidden.');
		return null;
	}

	try {
		const client = new Client(pds);
		const res = await client.get(fm.teal.alpha.actor.status, {
			repo: did as AtIdentifierString
		});
		const item = res.value.item;

		const coverArt = await resolveCoverArt(fetch, {
			isrc: item.isrc ?? null,
			releaseMbId: item.releaseMbId?.replace(/^mbid:/, '') ?? null,
			releaseName: item.releaseName ?? null
		});

		const nowPlaying: NowPlaying = {
			trackName: item.trackName,
			artists: item.artists.map((a) => a.artistName).join(', '),
			releaseName: item.releaseName ?? null,
			url: item.originUrl ?? null,
			coverArt
		};

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

export const load: PageServerLoad = async ({ request, fetch }) => {
	const host = request.headers.get('host') ?? '';
	const pumpkin = host.includes('pds.dad');

	const [sponsors, publications, currentlyReading, nowPlaying] = await Promise.all([
		fetchSponsors(fetch),
		fetchPublications(),
		fetchCurrentlyReading(),
		fetchNowPlaying(fetch)
	]);

	return { pumpkin, sponsors, publications, currentlyReading, nowPlaying };
};
