/**
 * The Studio room chat-plugin.
 * Supports scrobbling and searching for music from last.fm.
 * Also supports storing and suggesting recommendations.
 * Written by dhelmise, loosely based on the concept from bumbadadabum.
 * @author dhelmise
 */

import { FS, Net, Utils } from '../../lib';
import { YouTube, type VideoData } from './youtube';

const LASTFM_DB = 'config/chat-plugins/lastfm.json';
const RECOMMENDATIONS = 'config/chat-plugins/the-studio.json';
const API_ROOT = 'http://ws.audioscrobbler.com/2.0/';
const DEFAULT_IMAGES = [
	'https://lastfm.freetls.fastly.net/i/u/34s/2a96cbd8b46e442fc41c2b86b821562f.png',
	'https://lastfm.freetls.fastly.net/i/u/64s/2a96cbd8b46e442fc41c2b86b821562f.png',
	'https://lastfm.freetls.fastly.net/i/u/174s/2a96cbd8b46e442fc41c2b86b821562f.png',
	'https://lastfm.freetls.fastly.net/i/u/300x300/2a96cbd8b46e442fc41c2b86b821562f.png',
];

interface Recommendation {
	artist: string;
	title: string;
	url: string;
	videoInfo: VideoData | null;
	description: string;
	tags: string[];
	userData: {
		name: string,
		avatar?: string,
	};
	likes: number;
	liked?: {
		ips: string[],
		userids: string[],
	};
}

interface Recommendations {
	suggested: Recommendation[];
	saved: Recommendation[];
	// Storing here so I don't need to rewrite the lastfm json
	youtubeSearchDisabled?: boolean;
}

const lastfm: { [userid: string]: string } = JSON.parse(FS(LASTFM_DB).readIfExistsSync() || "{}");
const recommendations: Recommendations = JSON.parse(FS(RECOMMENDATIONS).readIfExistsSync() || "{}");

if (!recommendations.saved) recommendations.saved = [];
if (!recommendations.suggested) recommendations.suggested = [];
saveRecommendations();

function updateRecTags() {
	for (const rec of recommendations.saved) {
		if (!rec.tags.map(toID).includes(toID(rec.artist))) rec.tags.push(rec.artist);
		if (!rec.tags.map(toID).includes(toID(rec.userData.name))) rec.tags.push(rec.userData.name);
	}
	for (const rec of recommendations.suggested) {
		if (!rec.tags.map(toID).includes(toID(rec.artist))) rec.tags.push(rec.artist);
		if (!rec.tags.map(toID).includes(toID(rec.userData.name))) rec.tags.push(rec.userData.name);
	}
	saveRecommendations();
}

updateRecTags();

function saveLastFM() {
	FS(LASTFM_DB).writeUpdate(() => JSON.stringify(lastfm));
}
function saveRecommendations() {
	FS(RECOMMENDATIONS).writeUpdate(() => JSON.stringify(recommendations));
}

export class LastFMInterface {
	async getScrobbleData(username: string, displayName?: string) {
		this.checkHasKey();
		const accountName = this.getAccountName(username);
		let raw;
		try {
			raw = await Net(API_ROOT).get({
				query: {
					method: 'user.getRecentTracks', user: accountName,
					limit: 1, api_key: Config.lastfmkey, format: 'json',
				},
			});
		} catch {
			throw new Chat.ErrorMessage(`No scrobble data found.`);
		}
		const res = JSON.parse(raw);
		if (res.error) {
			throw new Chat.ErrorMessage(`${res.message}.`);
		}
		if (!res?.recenttracks?.track?.length) throw new Chat.ErrorMessage(`last.fm account not found.`);
		const track = res.recenttracks.track[0];
		let buf = `<table><tr>`;
		if (track.image?.length) {
			const imageIndex = track.image.length >= 3 ? 2 : track.image.length - 1;
			if (track.image[imageIndex]['#text']) {
				buf += `<td style="padding-right:5px"><img src="${track.image[imageIndex]['#text']}" width="75" height="75" /></td>`;
			}
			buf += `<td><strong><a href="https://www.last.fm/user/${accountName}">${Utils.escapeHTML(displayName || accountName)}</a></strong>`;
			if (track['@attr']?.nowplaying) {
				buf += ` is currently listening to:`;
			} else {
				buf += ` was last seen listening to:`;
			}
			buf += `<br />`;
			const trackName = `${track.artist?.['#text'] ? `${track.artist['#text']} - ` : ''}${track.name}`;
			let videoIDs: string[] | undefined;
			try {
				videoIDs = await YouTube.searchVideo(trackName, 1);
			} catch (e: any) {
				if (!recommendations.youtubeSearchDisabled) {
					throw new Chat.ErrorMessage(`Error while fetching video data: ${e.message}`);
				}
			}
			if (!videoIDs?.length && !recommendations.youtubeSearchDisabled) {
				throw new Chat.ErrorMessage(`Something went wrong with the YouTube API.`);
			}
			if (recommendations.youtubeSearchDisabled) {
				buf += Utils.escapeHTML(trackName);
			} else {
				buf += `<a href="https://youtu.be/${videoIDs![0]}">${Utils.escapeHTML(trackName)}</a>`;
			}
			buf += `</td></tr></table>${this.getScrobbleBadge()}`;
		}
		return buf;
	}

	addAccountName(userid: ID, accountName: string) {
		this.checkHasKey();
		accountName = accountName.trim();
		if (lastfm[userid]) {
			const oldName = lastfm[userid];
			lastfm[userid] = accountName;
			saveLastFM();
			return `last.fm account name changed from '${oldName}' to '${accountName}'.`;
		}
		lastfm[userid] = accountName;
		saveLastFM();
		return `Registered last.fm account '${accountName}'.`;
	}

	validateAccountName(accountName: string) {
		accountName = accountName.trim();
		const sanitizedName = accountName.replace(/[^-_a-zA-Z0-9]+/g, '');
		if (!(!accountName.includes(' ') && accountName === sanitizedName && /^[a-zA-Z]/.test(sanitizedName) &&
			sanitizedName.length > 1 && sanitizedName.length < 16)) {
			throw new Chat.ErrorMessage(`The provided account name (${sanitizedName}) is invalid. Valid last.fm usernames are between 2-15 characters, start with a letter, and only contain letters, numbers, hyphens, and underscores.`);
		}
		return true;
	}

	getAccountName(username: string) {
		if (lastfm[toID(username)]) return lastfm[toID(username)];
		return username.trim().replace(/ /g, '_').replace(/[^-_a-zA-Z0-9]/g, '');
	}

	async tryGetTrackData(track: string, artist?: string) {
		this.checkHasKey();
		const query: { [k: string]: any } = {
			method: 'track.search', limit: 1, api_key: Config.lastfmkey, track, format: 'json',
		};
		if (artist) query.artist = artist;
		let raw;
		try {
			raw = await Net(API_ROOT).get({ query });
		} catch {
			throw new Chat.ErrorMessage(`No track data found.`);
		}
		const req = JSON.parse(raw);
		let buf = ``;
		if (req.results?.trackmatches?.track?.length) {
			buf += `<table><tr><td style="padding-right:5px">`;
			const obj = req.results.trackmatches.track[0];
			const trackName = obj.name || "Untitled";
			const artistName = obj.artist || "Unknown Artist";
			const searchName = `${artistName} - ${trackName}`;
			if (obj.image?.length) {
				const img = obj.image;
				const imageIndex = img.length >= 3 ? 2 : img.length - 1;
				if (img[imageIndex]['#text'] && !DEFAULT_IMAGES.includes(img[imageIndex]['#text'])) {
					buf += `<img src="${img[imageIndex]['#text']}" width="75" height="75" />`;
				}
			}
			buf += `</td><td>`;
			const artistUrl = obj.url.split('_/')[0];
			buf += `<strong><a href="${artistUrl}">${artistName}</a> - <a href="${obj.url}">${trackName}</a></strong><br />`;
			let videoIDs: string[] | undefined;
			try {
				videoIDs = await YouTube.searchVideo(searchName, 1);
			} catch (e: any) {
				if (!recommendations.youtubeSearchDisabled) {
					throw new Chat.ErrorMessage(`Error while fetching video data: ${e.message}`);
				}
			}
			if (!videoIDs?.length || recommendations.youtubeSearchDisabled) {
				buf += searchName;
			} else {
				buf += `<a href="https://youtu.be/${videoIDs[0]}">YouTube link</a>`;
			}
			buf += `</td></tr></table>${this.getScrobbleBadge()}`;
		}
		if (req.error) {
			throw new Chat.ErrorMessage(`${req.message}.`);
		}
		if (!buf) {
			throw new Chat.ErrorMessage(`No results for '${artist ? `${artist} - ` : ``}${track}' found. Check spelling?`);
		}
		return buf;
	}
	checkHasKey() {
		if (!Config.lastfmkey) {
			throw new Chat.ErrorMessage(`This server does not support last.fm commands. If you're the owner, you can enable them by setting up Config.lastfmkey.`);
		}
	}
	getScrobbleBadge() {
		return `<div class="gray" style="float:right;font-size:8pt">[powered by AudioScrobbler]</div><div style="clear:both"></div>`;
	}
}

class RecommendationsInterface {
	getRandomRecommendation() {
		const recs = recommendations.saved;
		return recs[Math.floor(Math.random() * recs.length)];
	}

	async add(
		artist: string, title: string, url: string, description: string,
		username: string, tags: string[], avatar?: string
	) {
		artist = artist.trim();
		title = title.trim();
		if (this.get(artist, title)) {
			throw new Chat.ErrorMessage(`The song titled '${title}' by ${artist} is already recommended.`);
		}
		if (!/^https?:\/\//.test(url)) url = `https://${url}`;
		if (!YouTube.linkRegex.test(url)) {
			throw new Chat.ErrorMessage(`Please provide a valid YouTube link.`);
		}
		url = url.split('~')[0];
		const videoInfo = await YouTube.getVideoData(url);
		this.checkTags(tags);
		// JUST in case
		if (!recommendations.saved) recommendations.saved = [];
		const rec: Recommendation = {
			artist, title, videoInfo, url, description, tags, userData: { name: username }, likes: 0,
		};
		if (!rec.tags.map(toID).includes(toID(username))) rec.tags.push(username);
		if (!rec.tags.map(toID).includes(toID(artist))) rec.tags.push(artist);
		if (avatar) rec.userData.avatar = avatar;
		recommendations.saved.push(rec);
		saveRecommendations();
	}

	delete(artist: string, title: string) {
		artist = artist.trim();
		title = title.trim();
		if (!recommendations.saved?.length) {
			throw new Chat.ErrorMessage(`The song titled '${title}' by ${artist} isn't recommended.`);
		}
		const recIndex = this.getIndex(artist, title);
		if (recIndex < 0) {
			throw new Chat.ErrorMessage(`The song titled '${title}' by ${artist} isn't recommended.`);
		}
		recommendations.saved.splice(recIndex, 1);
		saveRecommendations();
	}

	async suggest(
		artist: string, title: string, url: string, description: string,
		username: string, tags: string[], avatar?: string
	) {
		artist = artist.trim();
		title = title.trim();
		if (this.get(artist, title)) {
			throw new Chat.ErrorMessage(`The song titled '${title}' by ${artist} is already recommended.`);
		}
		if (this.get(artist, title, null, true)) {
			throw new Chat.ErrorMessage(`The song titled '${title}' by ${artist} is already suggested.`);
		}
		if (!/^https?:\/\//.test(url)) url = `https://${url}`;
		if (!YouTube.linkRegex.test(url)) {
			throw new Chat.ErrorMessage(`Please provide a valid YouTube link.`);
		}
		url = url.split('~')[0];
		const videoInfo = await YouTube.getVideoData(url);
		this.checkTags(tags);
		const rec: Recommendation = {
			artist, title, videoInfo, url, description, tags, userData: { name: username }, likes: 0,
		};
		if (!rec.tags.map(toID).includes(toID(username))) rec.tags.push(username);
		if (!rec.tags.map(toID).includes(toID(artist))) rec.tags.push(artist);
		if (avatar) rec.userData.avatar = avatar;
		recommendations.suggested.push(rec);
		saveRecommendations();
	}

	approveSuggestion(submitter: string, artist: string, title: string) {
		artist = artist.trim();
		title = title.trim();
		const rec = this.get(artist, title, submitter, true);
		if (!rec) {
			throw new Chat.ErrorMessage(`There is no song titled '${title}' by ${artist} suggested from ${submitter.trim()}.`);
		}
		if (!recommendations.saved) recommendations.saved = [];
		recommendations.saved.push(rec);
		recommendations.suggested.splice(recommendations.suggested.indexOf(rec), 1);
		saveRecommendations();
	}

	denySuggestion(submitter: string, artist: string, title: string) {
		artist = artist.trim();
		title = title.trim();
		const index = this.getIndex(artist, title, submitter, true);
		if (index < 0) {
			throw new Chat.ErrorMessage(`There is no song titled '${title}' by ${artist} suggested from ${submitter.trim()}.`);
		}
		recommendations.suggested.splice(index, 1);
		saveRecommendations();
	}

	async render(rec: Recommendation, suggested = false) {
		let buf = ``;
		buf += `<div style="color:#000;background:linear-gradient(rgba(210,210,210),rgba(225,225,225))">`;
		buf += `<table style="margin:auto;background:rgba(255,255,255,0.25);padding:3px;"><tbody><tr>`;
		if (!rec.videoInfo) {
			// eslint-disable-next-line require-atomic-updates
			rec.videoInfo = await YouTube.getVideoData(YouTube.getId(rec.url));
			saveRecommendations();
		}
		if (rec.videoInfo) {
			buf += `<td style="text-align:center;"><img src="${rec.videoInfo.thumbnail}" width="120" height="67" /><br />`;
			buf += `<small><em>${!suggested ? `${Chat.count(rec.likes, "points")} | ` : ``}${rec.videoInfo.views} views</em></small></td>`;
		}
		buf += Utils.html`<td style="max-width:300px"><a href="${rec.url}" style="color:#000;font-weight:bold;">${rec.artist} - ${rec.title}</a>`;
		const tags = rec.tags.map(x => Utils.escapeHTML(x))
			.filter(x => toID(x) !== toID(rec.userData.name) && toID(x) !== toID(rec.artist));
		if (tags.length) {
			buf += `<br /><strong>Tags:</strong> <em>${tags.join(', ')}</em>`;
		}
		if (rec.description) {
			buf += `<br /><span style="display:inline-block;line-height:1.15em;"><strong>Description:</strong> ${Utils.escapeHTML(rec.description)}</span>`;
		}
		if (!rec.videoInfo && !suggested) {
			buf += `<br /><strong>Score:</strong> ${Chat.count(rec.likes, "points")}`;
		}
		if (!rec.userData.avatar) {
			buf += `<br /><strong>Recommended by:</strong> ${rec.userData.name}`;
		}
		buf += `<hr />`;
		if (suggested) {
			buf += Utils.html`<button class="button" name="send" value="/msgroom thestudio,/approvesuggestion ${rec.userData.name}|${rec.artist}|${rec.title}">Approve</button> | `;
			buf += Utils.html`<button class="button" name="send" value="/msgroom thestudio,/denysuggestion ${rec.userData.name}|${rec.artist}|${rec.title}">Deny</button>`;
		} else {
			buf += Utils.html`<button class="button" name="send" value="/msgroom thestudio,/likerec ${rec.artist}|${rec.title}" style="float:right;display:inline;padding:3px 5px;font-size:8pt;">`;
			buf += `<img src="https://${Config.routes.client}/sprites/bwicons/441.png" style="margin:-9px 0 -6px -7px;" width="32" height="32" />`;
			buf += `<span style="position:relative;bottom:2.6px;">Upvote</span></button>`;
		}
		buf += `</td>`;
		if (rec.userData.avatar) {
			buf += `<td style="text-align:center;width:110px;background:rgba(255,255,255,0.4);border-radius:15px;">`;
			const isCustom = rec.userData.avatar.startsWith('#');
			buf += `<img style="margin-bottom:-38px;" src="https://${Config.routes.client}/sprites/trainers${isCustom ? '-custom' : ''}/${isCustom ? rec.userData.avatar.slice(1) : rec.userData.avatar}.png" width="80" height="80" />`;
			buf += `<br /><span style="background:rgba(0,0,0,0.5);padding:1.5px 4px;color:white;font-size:7pt;">Recommended by:`;
			buf += `<br /><strong>${rec.userData.name}</strong></span></td>`;
		} else {
			buf += `<td><span style="background:rgba(0,0,0,0.5);padding:1.5px 4px;color:white;font-size:7pt;">Recommended by: <strong>${rec.userData.name}</strong></span></td>`;
		}
		buf += `</tbody></table>`;
		buf += `</div>`;
		return buf;
	}

	likeRecommendation(artist: string, title: string, liker: User) {
		const rec = this.get(artist, title);
		if (!rec) {
			throw new Chat.ErrorMessage(`The song titled '${title}' by ${artist} isn't recommended.`);
		}
		if (!rec.liked) {
			rec.liked = { ips: [], userids: [] };
		}
		if ((!Config.noipchecks && rec.liked.ips.includes(liker.latestIp)) || rec.liked.userids.includes(liker.id)) {
			throw new Chat.ErrorMessage(`You've already liked this recommendation.`);
		}
		rec.likes++;
		rec.liked.ips.push(liker.latestIp);
		rec.liked.userids.push(liker.id);
		saveRecommendations();
	}

	get(artist: string, title: string, submitter: string | null = null, fromSuggestions = false) {
		let recs = recommendations.saved;
		if (fromSuggestions) recs = recommendations.suggested;
		return recs.find(x => (
			toID(x.artist) === toID(artist) &&
			toID(x.title) === toID(title) &&
			(!submitter || toID(x.userData.name) === toID(submitter))
		));
	}

	getIndex(artist: string, title: string, submitter: string | null = null, fromSuggestions = false) {
		let recs = recommendations.saved;
		if (fromSuggestions) recs = recommendations.suggested;
		return recs.findIndex(x => (
			toID(x.artist) === toID(artist) &&
			toID(x.title) === toID(title) &&
			(!submitter || toID(x.userData.name) === toID(submitter))
		));
	}

	checkTags(tags: string[]) {
		const cleansedTags = new Set<ID>();
		for (const tag of tags) {
			if (!toID(tag)) throw new Chat.ErrorMessage(`Empty tag detected.`);
			if (cleansedTags.has(toID(tag))) {
				throw new Chat.ErrorMessage(`Duplicate tag: ${tag.trim()}`);
			}
			cleansedTags.add(toID(tag));
		}
	}
}

export const LastFM = new LastFMInterface();
export const Recs = new RecommendationsInterface();

export const commands: Chat.ChatCommands = {
	lastfmyoutubesearch(target, room, user) {
		this.checkCan('gdeclare');
		target = toID(target);
		if (!target || !['enable', 'disable'].includes(target)) {
			return this.parse('/help lastfmyoutubesearch');
		}
		if (target === 'enable') {
			if (!recommendations.youtubeSearchDisabled) {
				throw new Chat.ErrorMessage(`The YouTube API is already enabled for Last.fm commands.`);
			}
			delete recommendations.youtubeSearchDisabled;
			saveRecommendations();
			this.sendReply(`YouTube API enabled for Last.fm commands.`);
			this.globalModlog(`LASTFM YOUTUBE API`, null, 'enabled');
		} else {
			if (recommendations.youtubeSearchDisabled) {
				throw new Chat.ErrorMessage(`The YouTube API is already disabled for Last.fm commands.`);
			}
			recommendations.youtubeSearchDisabled = true;
			saveRecommendations();
			this.sendReply(`YouTube API disabled for Last.fm commands.`);
			this.globalModlog(`LASTFM YOUTUBE API`, null, 'disabled');
		}
	},
	lastfmyoutubesearchhelp: [
		'/lastfmyoutubesearch [enable|disable] - Enables/disables the YouTube API for Last.fm commands. Requires: ~',
	],
	registerlastfm(target, room, user) {
		if (!target) return this.parse(`/help registerlastfm`);
		this.checkChat(target);
		target = this.filter(target) || '';
		if (!target) {
			throw new Chat.ErrorMessage(`The provided account name has phrases that PS doesn't allow.`);
		}
		LastFM.validateAccountName(target);
		this.sendReply(LastFM.addAccountName(user.id, target.trim()));
	},
	registerlastfmhelp: [
		`/registerlastfm [username] - Adds the provided [username] to the last.fm database for scrobbling.`,
		`Usernames can only be 2-15 characters long, must start with a letter, and can only contain letters, numbers, hyphens, and underscores.`,
	],

	async lastfm(target, room, user) {
		this.checkChat();
		if (!user.autoconfirmed) throw new Chat.ErrorMessage(`You cannot use this command while not autoconfirmed.`);
		this.runBroadcast(true);
		const targetUsername = this.splitUser(target).targetUsername || (user.named ? user.name : '');
		const username = LastFM.getAccountName(targetUsername);
		this.sendReplyBox(await LastFM.getScrobbleData(username, targetUsername));
	},
	lastfmhelp: [
		`/lastfm [username] - Displays the last scrobbled song for the person using the command or for [username] if provided.`,
		`To link up your last.fm account, check out "/help registerlastfm".`,
	],

	async track(target, room, user) {
		if (!target) return this.parse('/help track');
		this.checkChat();
		if (!user.autoconfirmed) throw new Chat.ErrorMessage(`You cannot use this command while not autoconfirmed.`);
		const [track, artist] = this.splitOne(target);
		if (!track) return this.parse('/help track');
		this.runBroadcast(true);
		this.sendReplyBox(await LastFM.tryGetTrackData(track, artist || undefined));
	},
	trackhelp: [
		`/track [song name], [artist] - Displays the most relevant search result to the song name (and artist if specified) provided.`,
	],

	addrec: 'addrecommendation',
	async addrecommendation(target, room, user) {
		room = this.requireRoom('thestudio' as RoomID);
		this.checkCan('show', null, room);
		const [artist, title, url, description, ...tags] = target.split('|').map(x => x.trim());
		if (!(artist && title && url && description && tags?.length)) {
			return this.parse(`/help addrecommendation`);
		}

		const cleansedTags = tags.map(x => x.trim());
		await Recs.add(artist, title, url, description, user.name, cleansedTags, String(user.avatar));

		this.privateModAction(`${user.name} added a recommendation for '${title}' by ${artist}.`);
		this.modlog(`RECOMMENDATION`, null, `add: '${toID(title)}' by ${toID(artist)}`);
	},
	addrecommendationhelp: [
		`/addrecommendation artist | song title | url | description | tag1 | tag2 | ... - Adds a song recommendation. Requires: + % @ * # ~`,
	],

	delrec: 'removerecommendation',
	removerecommendation(target, room, user) {
		room = this.requireRoom('thestudio' as RoomID);
		const [artist, title] = target.split(`|`).map(x => x.trim());
		if (!(artist && title)) return this.parse(`/help removerecommendation`);
		const rec = Recs.get(artist, title);
		if (!rec) throw new Chat.ErrorMessage(`Recommendation not found.`);
		if (toID(rec.userData.name) !== user.id) {
			this.checkCan('mute', null, room);
		}

		Recs.delete(artist, title);

		this.privateModAction(`${user.name} removed a recommendation for '${title}' by ${artist}.`);
		this.modlog(`RECOMMENDATION`, null, `remove: '${toID(title)}' by ${toID(artist)}`);
	},
	removerecommendationhelp: [
		`/removerecommendation artist | song title - Removes a song recommendation. Requires: % @ * # ~`,
		`If you added a recommendation, you can remove it on your own without being one of the required ranks.`,
	],

	suggestrec: 'suggestrecommendation',
	async suggestrecommendation(target, room, user) {
		room = this.requireRoom('thestudio' as RoomID);
		if (!target) {
			return this.parse('/help suggestrecommendation');
		}
		this.checkChat(target);
		if (!user.autoconfirmed) throw new Chat.ErrorMessage(`You cannot use this command while not autoconfirmed.`);
		const [artist, title, url, description, ...tags] = target.split('|').map(x => x.trim());
		if (!(artist && title && url && description && tags?.length)) {
			return this.parse(`/help suggestrecommendation`);
		}

		const cleansedTags = tags.map(x => x.trim());
		await Recs.suggest(artist, title, url, description, user.name, cleansedTags, String(user.avatar));
		this.sendReply(`Your suggestion for '${title}' by ${artist} has been submitted.`);
		const html = await Recs.render({
			artist, title, url, description,
			userData: { name: user.name, avatar: String(user.avatar) },
			tags: cleansedTags, likes: 0, videoInfo: null,
		}, true);
		room.sendRankedUsers(`|html|${html}`, '%');
	},
	suggestrecommendationhelp: [
		`/suggestrecommendation artist | song title | url | description | tag1 | tag2 | ... - Suggest a song recommendation.`,
	],

	approvesuggestion(target, room, user) {
		room = this.requireRoom('thestudio' as RoomID);
		this.checkCan('mute', null, room);
		const [submitter, artist, title] = target.split('|').map(x => x.trim());
		if (!(submitter && artist && title)) return this.parse(`/help approvesuggestion`);

		Recs.approveSuggestion(submitter, artist, title);

		this.privateModAction(`${user.name} approved a suggested recommendation from ${submitter} for '${title}' by ${artist}.`);
		this.modlog(`RECOMMENDATION`, null, `approve: '${toID(title)}' by ${toID(artist)} from ${submitter}`);
	},
	approvesuggestionhelp: [
		`/approvesuggestion submitter | artist | strong title - Approve a submitted song recommendation. Requires: % @ * # ~`,
	],

	denysuggestion(target, room, user) {
		room = this.requireRoom('thestudio' as RoomID);
		this.checkCan('mute', null, room);
		const [submitter, artist, title] = target.split('|').map(x => x.trim());
		if (!(submitter && artist && title)) return this.parse(`/help approvesuggestion`);

		Recs.denySuggestion(submitter, artist, title);

		this.privateModAction(`${user.name} denied a suggested recommendation from ${submitter} for '${title}' by ${artist}.`);
		this.modlog(`RECOMMENDATION`, null, `deny: '${toID(title)}' by ${toID(artist)} from ${submitter}`);
	},
	denysuggestionhelp: [
		`/denysuggestion submitter | artist | strong title - Deny a submitted song recommendation. Requires: % @ * # ~`,
	],

	rec: 'recommendation',
	searchrec: 'recommendation',
	viewrec: 'recommendation',
	searchrecommendation: 'recommendation',
	viewrecommendation: 'recommendation',
	randrec: 'recommendation',
	randomrecommendation: 'recommendation',
	async recommendation(target, room, user) {
		if (!recommendations.saved.length) {
			throw new Chat.ErrorMessage(`There are no recommendations saved.`);
		}
		room = this.requireRoom('thestudio' as RoomID);
		this.runBroadcast();
		if (!target) {
			return this.sendReply(`|html|${await Recs.render(Recs.getRandomRecommendation())}`);
		}
		const matches: Recommendation[] = [];
		target = target.slice(0, 300);
		const args = target.split(',');
		for (const rec of recommendations.saved) {
			if (!args.every(x => rec.tags.map(toID).includes(toID(x)))) continue;
			matches.push(rec);
		}
		if (!matches.length) {
			throw new Chat.ErrorMessage(`No matches found.`);
		}
		const sample = Utils.shuffle(matches)[0];
		this.sendReply(`|html|${await Recs.render(sample)}`);
	},
	recommendationhelp: [
		`/recommendation [key1, key2, key3, ...] - Displays a random recommendation that matches all keys, if one exists.`,
		`If no arguments are provided, a random recommendation is shown.`,
		`/addrecommendation artist | song title | url | description | tag1 | tag2 | ... - Adds a song recommendation. Requires: + % @ * # ~`,
		`/removerecommendation artist | song title - Removes a song recommendation. Requires: % @ * # ~`,
		`If you added a recommendation, you can remove it on your own without being one of the required ranks.`,
		`/suggestrecommendation artist | song title | url | description | tag1 | tag2 | ... - Suggest a song recommendation.`,
	],

	likerec: 'likerecommendation',
	likerecommendation(target, room, user, connection) {
		room = this.requireRoom('thestudio' as RoomID);
		const [artist, title] = target.split('|').map(x => x.trim());
		if (!(artist && title)) return this.parse(`/help likerecommendation`);
		Recs.likeRecommendation(artist, title, user);
		this.sendReply(`You liked '${title}' by ${artist}.`);
	},
	likerecommendationhelp: [
		`/likerecommendation artist | title - Upvotes a recommendation for the provided artist and title.`,
	],

	viewrecs: 'viewrecommendations',
	viewrecommendations(target, room, user) {
		room = this.requireRoom('thestudio' as RoomID);
		this.parse(`/j view-recommendations-${room.roomid}`);
	},
	viewrecommendationshelp: [
		`/viewrecommendations OR /viewrecs - View all recommended songs.`,
	],

	viewsuggestions: 'viewsuggestedrecommendations',
	viewsuggestedrecs: 'viewsuggestedrecommendations',
	viewsuggestedrecommendations(target, room, user) {
		room = this.requireRoom('thestudio' as RoomID);
		this.parse(`/j view-suggestedrecommendations-${room.roomid}`);
	},
	viewsuggestedrecommendationshelp: [
		`/viewsuggestedrecommendations OR /viewsuggestions - View all suggested recommended songs. Requires: % @ * # ~`,
	],
};

export const pages: Chat.PageTable = {
	async recommendations(query, user, connection) {
		const room = this.requireRoom();
		this.checkCan('mute', null, room);
		if (!user.inRooms.has(room.roomid)) throw new Chat.ErrorMessage(`You must be in ${room.title} to view this page.`);
		this.title = 'Recommendations';
		let buf = `<div class="pad">`;
		buf += `<button style="float:right" class="button" name="send" value="/j view-recommendations-${room.roomid}"><i class="fa fa-refresh"></i> Refresh</button>`;
		const recs = recommendations.saved;
		if (!recs?.length) {
			return `${buf}<h2>There are currently no recommendations.</h2></div>`;
		}
		buf += `<h2>Recommendations (${recs.length}):</h2>`;
		for (const rec of recs) {
			buf += `<div class="infobox">`;
			buf += await Recs.render(rec);
			if (user.can('mute', null, room) || toID(rec.userData.name) === user.id) {
				buf += `<hr /><button class="button" name="send" value="/msgroom thestudio,/removerecommendation ${rec.artist}|${rec.title}">Delete</button>`;
			}
			buf += `</div>`;
		}
		return buf;
	},
	async suggestedrecommendations(query, user, connection) {
		const room = this.requireRoom();
		this.checkCan('mute', null, room);
		if (!user.inRooms.has(room.roomid)) throw new Chat.ErrorMessage(`You must be in ${room.title} to view this page.`);
		this.title = 'Suggested Recommendations';
		let buf = `<div class="pad">`;
		buf += `<button style="float:right" class="button" name="send" value="/j view-suggestedrecommendations-${room.roomid}"><i class="fa fa-refresh"></i> Refresh</button>`;
		const recs = recommendations.suggested;
		if (!recs?.length) {
			return `${buf}<h2>There are currently no suggested recommendations.</h2></div>`;
		}
		buf += `<h2>Suggested Recommendations (${recs.length}):</h2>`;
		for (const rec of recs) {
			buf += `<div class="infobox">`;
			buf += await Recs.render(rec, true);
			buf += `</div>`;
		}
		return buf;
	},
};
