import mongoose from "mongoose";
import async from "async";
import config from "config";

import * as rax from "retry-axios";
import axios from "axios";
import url from "url";

import CoreClass from "../core";

let SpotifyModule;
let SoundcloudModule;
let DBModule;
let CacheModule;
let MediaModule;
let MusicBrainzModule;
let WikiDataModule;

const youtubeVideoUrlRegex =
	/^(https?:\/\/)?(www\.)?(m\.)?(music\.)?(youtube\.com|youtu\.be)\/(watch\?v=)?(?<youtubeId>[\w-]{11})((&([A-Za-z0-9]+)?)*)?$/;

const spotifyTrackObjectToMusareTrackObject = spotifyTrackObject => ({
	trackId: spotifyTrackObject.id,
	name: spotifyTrackObject.name,
	albumId: spotifyTrackObject.album.id,
	albumTitle: spotifyTrackObject.album.title,
	albumImageUrl: spotifyTrackObject.album.images[0].url,
	artists: spotifyTrackObject.artists.map(artist => artist.name),
	artistIds: spotifyTrackObject.artists.map(artist => artist.id),
	duration: spotifyTrackObject.duration_ms / 1000,
	explicit: spotifyTrackObject.explicit,
	externalIds: spotifyTrackObject.external_ids,
	popularity: spotifyTrackObject.popularity,
	isLocal: spotifyTrackObject.is_local
});

class RateLimitter {
	/**
	 * Constructor
	 * @param {number} timeBetween - The time between each allowed YouTube request
	 */
	constructor(timeBetween) {
		this.dateStarted = Date.now();
		this.timeBetween = timeBetween;
	}

	/**
	 * Returns a promise that resolves whenever the ratelimit of a YouTube request is done
	 * @returns {Promise} - promise that gets resolved when the rate limit allows it
	 */
	continue() {
		return new Promise(resolve => {
			if (Date.now() - this.dateStarted >= this.timeBetween) resolve();
			else setTimeout(resolve, this.dateStarted + this.timeBetween - Date.now());
		});
	}

	/**
	 * Restart the rate limit timer
	 */
	restart() {
		this.dateStarted = Date.now();
	}
}

class _SpotifyModule extends CoreClass {
	// eslint-disable-next-line require-jsdoc
	constructor() {
		super("spotify");

		SpotifyModule = this;
	}

	/**
	 * Initialises the spotify module
	 * @returns {Promise} - returns promise (reject, resolve)
	 */
	async initialize() {
		DBModule = this.moduleManager.modules.db;
		CacheModule = this.moduleManager.modules.cache;
		MediaModule = this.moduleManager.modules.media;
		MusicBrainzModule = this.moduleManager.modules.musicbrainz;
		SoundcloudModule = this.moduleManager.modules.soundcloud;
		WikiDataModule = this.moduleManager.modules.wikidata;

		this.spotifyTrackModel = this.SpotifyTrackModel = await DBModule.runJob("GET_MODEL", {
			modelName: "spotifyTrack"
		});
		this.spotifyAlbumModel = this.SpotifyAlbumModel = await DBModule.runJob("GET_MODEL", {
			modelName: "spotifyAlbum"
		});
		this.spotifyArtistModel = this.SpotifyArtistModel = await DBModule.runJob("GET_MODEL", {
			modelName: "spotifyArtist"
		});

		return new Promise((resolve, reject) => {
			if (!config.get("apis.spotify.enabled")) {
				reject(new Error("Spotify is not enabled."));
				return;
			}

			this.rateLimiter = new RateLimitter(config.get("apis.spotify.rateLimit"));
			this.requestTimeout = config.get("apis.spotify.requestTimeout");

			this.axios = axios.create();
			this.axios.defaults.raxConfig = {
				instance: this.axios,
				retry: config.get("apis.spotify.retryAmount"),
				noResponseRetries: config.get("apis.spotify.retryAmount")
			};
			rax.attach(this.axios);

			resolve();
		});
	}

	/**
	 * Fetches a Spotify API token from either the cache, or Spotify using the client id and secret from the config
	 * @returns {Promise} - returns promise (reject, resolve)
	 */
	GET_API_TOKEN() {
		return new Promise((resolve, reject) => {
			CacheModule.runJob("GET", { key: "spotifyApiKey" }, this).then(spotifyApiKey => {
				if (spotifyApiKey) {
					resolve(spotifyApiKey);
					return;
				}

				this.log("INFO", `No Spotify API token stored in cache, requesting new token.`);

				const clientId = config.get("apis.spotify.clientId");
				const clientSecret = config.get("apis.spotify.clientSecret");
				const unencoded = `${clientId}:${clientSecret}`;
				const encoded = Buffer.from(unencoded).toString("base64");

				const params = new url.URLSearchParams({ grant_type: "client_credentials" });

				SpotifyModule.axios
					.post("https://accounts.spotify.com/api/token", params.toString(), {
						headers: {
							Authorization: `Basic ${encoded}`,
							"Content-Type": "application/x-www-form-urlencoded"
						}
					})
					.then(res => {
						const { access_token: accessToken, expires_in: expiresIn } = res.data;

						// TODO TTL can be later if stuck in queue
						CacheModule.runJob(
							"SET",
							{ key: "spotifyApiKey", value: accessToken, ttl: expiresIn - 30 },
							this
						)
							.then(spotifyApiKey => {
								this.log(
									"SUCCESS",
									`Stored new Spotify API token in cache. Expires in ${expiresIn - 30}`
								);
								resolve(spotifyApiKey);
							})
							.catch(err => {
								this.log(
									"ERROR",
									`Failed to store new Spotify API token in cache.`,
									typeof err === "string" ? err : err.message
								);
								reject(err);
							});
					})
					.catch(err => {
						this.log(
							"ERROR",
							`Failed to get new Spotify API token.`,
							typeof err === "string" ? err : err.message
						);
						reject(err);
					});
			});
		});
	}

	/**
	 * Perform Spotify API get albums request
	 * @param {object} payload - object that contains the payload
	 * @param {Array} payload.albumIds - the album ids to get
	 * @returns {Promise} - returns promise (reject, resolve)
	 */
	API_GET_ALBUMS(payload) {
		return new Promise((resolve, reject) => {
			const { albumIds } = payload;

			SpotifyModule.runJob(
				"API_CALL",
				{
					url: `https://api.spotify.com/v1/albums`,
					params: {
						ids: albumIds.join(",")
					}
				},
				this
			)
				.then(response => {
					resolve(response);
				})
				.catch(err => {
					reject(err);
				});
		});
	}

	/**
	 * Perform Spotify API get artists request
	 * @param {object} payload - object that contains the payload
	 * @param {Array} payload.artistIds - the artist ids to get
	 * @returns {Promise} - returns promise (reject, resolve)
	 */
	API_GET_ARTISTS(payload) {
		return new Promise((resolve, reject) => {
			const { artistIds } = payload;

			SpotifyModule.runJob(
				"API_CALL",
				{
					url: `https://api.spotify.com/v1/artists`,
					params: {
						ids: artistIds.join(",")
					}
				},
				this
			)
				.then(response => {
					resolve(response);
				})
				.catch(err => {
					reject(err);
				});
		});
	}

	/**
	 * Perform Spotify API get track request
	 * @param {object} payload - object that contains the payload
	 * @param {string} payload.trackId - the Spotify track id to get
	 * @returns {Promise} - returns promise (reject, resolve)
	 */
	API_GET_TRACK(payload) {
		return new Promise((resolve, reject) => {
			const { trackId } = payload;

			SpotifyModule.runJob(
				"API_CALL",
				{
					url: `https://api.spotify.com/v1/tracks/${trackId}`
				},
				this
			)
				.then(response => {
					resolve(response);
				})
				.catch(err => {
					reject(err);
				});
		});
	}

	/**
	 * Perform Spotify API get playlist request
	 * @param {object} payload - object that contains the payload
	 * @param {string} payload.playlistId - the Spotify playlist id to get songs from
	 * @param {string} payload.nextUrl - the next URL to use
	 * @returns {Promise} - returns promise (reject, resolve)
	 */
	API_GET_PLAYLIST(payload) {
		return new Promise((resolve, reject) => {
			const { playlistId, nextUrl } = payload;

			SpotifyModule.runJob(
				"API_CALL",
				{
					url: nextUrl || `https://api.spotify.com/v1/playlists/${playlistId}/tracks`
				},
				this
			)
				.then(response => {
					resolve(response);
				})
				.catch(err => {
					reject(err);
				});
		});
	}

	/**
	 * Perform Spotify API call
	 * @param {object} payload - object that contains the payload
	 * @param {string} payload.url - request url
	 * @param {object} payload.params - request parameters
	 * @returns {Promise} - returns promise (reject, resolve)
	 */
	API_CALL(payload) {
		return new Promise((resolve, reject) => {
			const { url, params } = payload;

			SpotifyModule.runJob("GET_API_TOKEN", {}, this)
				.then(spotifyApiToken => {
					SpotifyModule.axios
						.get(url, {
							headers: {
								Authorization: `Bearer ${spotifyApiToken}`
							},
							timeout: SpotifyModule.requestTimeout,
							params
						})
						.then(response => {
							if (response.data.error) {
								reject(new Error(response.data.error));
							} else {
								resolve({ response });
							}
						})
						.catch(err => {
							reject(err);
						});
				})
				.catch(err => {
					this.log(
						"ERROR",
						`Spotify API call failed as an error occured whilst getting the API token`,
						typeof err === "string" ? err : err.message
					);
					resolve(err);
				});
		});
	}

	/**
	 * Create Spotify track
	 * @param {object} payload - an object containing the payload
	 * @param {Array} payload.spotifyTracks - the spotifyTracks
	 * @returns {Promise} - returns a promise (resolve, reject)
	 */
	CREATE_TRACKS(payload) {
		return new Promise((resolve, reject) => {
			async.waterfall(
				[
					next => {
						const { spotifyTracks } = payload;
						if (!Array.isArray(spotifyTracks)) next("Invalid spotifyTracks type");
						else {
							const trackIds = spotifyTracks.map(spotifyTrack => spotifyTrack.trackId);

							SpotifyModule.spotifyTrackModel.find({ trackId: trackIds }, (err, existingTracks) => {
								if (err) {
									next(err);
									return;
								}

								const existingTrackIds = existingTracks.map(existingTrack => existingTrack.trackId);

								const newSpotifyTracks = spotifyTracks.filter(
									spotifyTrack => existingTrackIds.indexOf(spotifyTrack.trackId) === -1
								);

								SpotifyModule.spotifyTrackModel.insertMany(newSpotifyTracks, next);
							});
						}
					},

					(spotifyTracks, next) => {
						const mediaSources = spotifyTracks.map(spotifyTrack => `spotify:${spotifyTrack.trackId}`);
						async.eachLimit(
							mediaSources,
							2,
							(mediaSource, next) => {
								MediaModule.runJob("RECALCULATE_RATINGS", { mediaSource }, this)
									.then(() => next())
									.catch(next);
							},
							err => {
								if (err) next(err);
								else next(null, spotifyTracks);
							}
						);
					}
				],
				(err, spotifyTracks) => {
					if (err) reject(new Error(err));
					else resolve({ spotifyTracks });
				}
			);
		});
	}

	/**
	 * Create Spotify albums
	 * @param {object} payload - an object containing the payload
	 * @param {Array} payload.spotifyAlbums - the Spotify albums
	 * @returns {Promise} - returns a promise (resolve, reject)
	 */
	async CREATE_ALBUMS(payload) {
		const { spotifyAlbums } = payload;

		if (!Array.isArray(spotifyAlbums)) throw new Error("Invalid spotifyAlbums type");

		const albumIds = spotifyAlbums.map(spotifyAlbum => spotifyAlbum.albumId);

		const existingAlbums = (await SpotifyModule.spotifyAlbumModel.find({ albumId: albumIds })).map(
			album => album._doc
		);
		const existingAlbumIds = existingAlbums.map(existingAlbum => existingAlbum.albumId);

		const newSpotifyAlbums = spotifyAlbums.filter(
			spotifyAlbum => existingAlbumIds.indexOf(spotifyAlbum.albumId) === -1
		);

		if (newSpotifyAlbums.length === 0) return existingAlbums;

		await SpotifyModule.spotifyAlbumModel.insertMany(newSpotifyAlbums);

		return existingAlbums.concat(newSpotifyAlbums);
	}

	/**
	 * Create Spotify artists
	 * @param {object} payload - an object containing the payload
	 * @param {Array} payload.spotifyArtists - the Spotify artists
	 * @returns {Promise} - returns a promise (resolve, reject)
	 */
	async CREATE_ARTISTS(payload) {
		const { spotifyArtists } = payload;

		if (!Array.isArray(spotifyArtists)) throw new Error("Invalid spotifyArtists type");

		const artistIds = spotifyArtists.map(spotifyArtist => spotifyArtist.artistId);

		const existingArtists = (await SpotifyModule.spotifyArtistModel.find({ artistId: artistIds })).map(
			artist => artist._doc
		);
		const existingArtistIds = existingArtists.map(existingArtist => existingArtist.artistId);

		const newSpotifyArtists = spotifyArtists.filter(
			spotifyArtist => existingArtistIds.indexOf(spotifyArtist.artistId) === -1
		);

		if (newSpotifyArtists.length === 0) return existingArtists;

		await SpotifyModule.spotifyArtistModel.insertMany(newSpotifyArtists);

		return existingArtists.concat(newSpotifyArtists);
	}

	/**
	 * Gets tracks from media sources
	 * @param {object} payload - object that contains the payload
	 * @param {Array} payload.mediaSources - the media sources to get tracks from
	 * @returns {Promise} - returns promise (reject, resolve)
	 */
	async GET_TRACKS_FROM_MEDIA_SOURCES(payload) {
		return new Promise((resolve, reject) => {
			const { mediaSources } = payload;

			const responses = {};

			const promises = [];

			mediaSources.forEach(mediaSource => {
				promises.push(
					new Promise(resolve => {
						const trackId = mediaSource.split(":")[1];
						SpotifyModule.runJob("GET_TRACK", { identifier: trackId, createMissing: true }, this)
							.then(({ track }) => {
								responses[mediaSource] = track;
							})
							.catch(err => {
								SpotifyModule.log(
									"ERROR",
									`Getting tracked with media source ${mediaSource} failed.`,
									typeof err === "string" ? err : err.message
								);
								responses[mediaSource] = typeof err === "string" ? err : err.message;
							})
							.finally(() => {
								resolve();
							});
					})
				);
			});

			Promise.all(promises)
				.then(() => {
					SpotifyModule.log("SUCCESS", `Got all tracks.`);
					resolve({ tracks: responses });
				})
				.catch(reject);
		});
	}

	/**
	 * Gets albums from Spotify album ids
	 * @param {object} payload - object that contains the payload
	 * @param {Array} payload.albumIds - the Spotify album ids
	 * @returns {Promise} - returns promise (reject, resolve)
	 */
	async GET_ALBUMS_FROM_IDS(payload) {
		const { albumIds } = payload;

		const existingAlbums = (await SpotifyModule.spotifyAlbumModel.find({ albumId: albumIds })).map(
			album => album._doc
		);
		const existingAlbumIds = existingAlbums.map(existingAlbum => existingAlbum.albumId);

		const missingAlbumIds = albumIds.filter(albumId => existingAlbumIds.indexOf(albumId) === -1);

		if (missingAlbumIds.length === 0) return existingAlbums;

		const jobsToRun = [];

		const chunkSize = 2;
		while (missingAlbumIds.length > 0) {
			const chunkedMissingAlbumIds = missingAlbumIds.splice(0, chunkSize);

			jobsToRun.push(SpotifyModule.runJob("API_GET_ALBUMS", { albumIds: chunkedMissingAlbumIds }, this));
		}

		const jobResponses = await Promise.all(jobsToRun);

		const newAlbums = jobResponses
			.map(jobResponse => jobResponse.response.data.albums)
			.flat()
			.map(album => ({
				albumId: album.id,
				rawData: album
			}));

		await SpotifyModule.runJob("CREATE_ALBUMS", { spotifyAlbums: newAlbums }, this);

		return existingAlbums.concat(newAlbums);
	}

	/**
	 * Gets Spotify artists from Spotify artist ids
	 * @param {object} payload - object that contains the payload
	 * @param {Array} payload.artistIds - the Spotify artist ids
	 * @returns {Promise} - returns promise (reject, resolve)
	 */
	async GET_ARTISTS_FROM_IDS(payload) {
		const { artistIds } = payload;

		const existingArtists = (await SpotifyModule.spotifyArtistModel.find({ artistId: artistIds })).map(
			artist => artist._doc
		);
		const existingArtistIds = existingArtists.map(existingArtist => existingArtist.artistId);

		const missingArtistIds = artistIds.filter(artistId => existingArtistIds.indexOf(artistId) === -1);

		if (missingArtistIds.length === 0) return existingArtists;

		const jobsToRun = [];

		const chunkSize = 50;
		while (missingArtistIds.length > 0) {
			const chunkedMissingArtistIds = missingArtistIds.splice(0, chunkSize);

			jobsToRun.push(SpotifyModule.runJob("API_GET_ARTISTS", { artistIds: chunkedMissingArtistIds }, this));
		}

		const jobResponses = await Promise.all(jobsToRun);

		const newArtists = jobResponses
			.map(jobResponse => jobResponse.response.data.artists)
			.flat()
			.map(artist => ({
				artistId: artist.id,
				rawData: artist
			}));

		await SpotifyModule.runJob("CREATE_ARTISTS", { spotifyArtists: newArtists }, this);

		return existingArtists.concat(newArtists);
	}

	/**
	 * Get Spotify track
	 * @param {object} payload - an object containing the payload
	 * @param {string} payload.identifier - the spotify track ObjectId or track id
	 * @param {boolean} payload.createMissing - attempt to fetch and create track if not in db
	 * @returns {Promise} - returns a promise (resolve, reject)
	 */
	GET_TRACK(payload) {
		return new Promise((resolve, reject) => {
			async.waterfall(
				[
					next => {
						const query = mongoose.isObjectIdOrHexString(payload.identifier)
							? { _id: payload.identifier }
							: { trackId: payload.identifier };

						return SpotifyModule.spotifyTrackModel.findOne(query, next);
					},

					(track, next) => {
						if (track) return next(null, track, false);
						if (mongoose.isObjectIdOrHexString(payload.identifier) || !payload.createMissing)
							return next("Spotify track not found.");
						return SpotifyModule.runJob("API_GET_TRACK", { trackId: payload.identifier }, this)
							.then(({ response }) => {
								const { data } = response;

								if (!data || !data.id)
									return next("The specified track does not exist or cannot be publicly accessed.");

								const spotifyTrack = spotifyTrackObjectToMusareTrackObject(data);

								return next(null, false, spotifyTrack);
							})
							.catch(next);
					},
					(track, spotifyTrack, next) => {
						if (track) return next(null, track, true);
						return SpotifyModule.runJob("CREATE_TRACKS", { spotifyTracks: [spotifyTrack] }, this)
							.then(res => {
								if (res.spotifyTracks.length === 1) next(null, res.spotifyTracks[0], false);
								else next("Spotify track not found.");
							})
							.catch(next);
					}
				],
				(err, track, existing) => {
					if (err) reject(new Error(err));
					else if (track.isLocal) reject(new Error("Track is local."));
					else resolve({ track, existing });
				}
			);
		});
	}

	/**
	 * Get Spotify album
	 * @param {object} payload - an object containing the payload
	 * @param {string} payload.identifier - the spotify album ObjectId or track id
	 * @returns {Promise} - returns a promise (resolve, reject)
	 */
	async GET_ALBUM(payload) {
		const query = mongoose.isObjectIdOrHexString(payload.identifier)
			? { _id: payload.identifier }
			: { albumId: payload.identifier };

		const album = await SpotifyModule.spotifyAlbumModel.findOne(query);

		if (album) return album._doc;

		return null;
	}

	/**
	 * Returns an array of songs taken from a Spotify playlist
	 * @param {object} payload - object that contains the payload
	 * @param {string} payload.url - the id of the Spotify playlist
	 * @returns {Promise} - returns promise (reject, resolve)
	 */
	GET_PLAYLIST(payload) {
		return new Promise((resolve, reject) => {
			const spotifyPlaylistUrlRegex = /.+open\.spotify\.com\/playlist\/(?<playlistId>[A-Za-z0-9]+)/;
			const match = spotifyPlaylistUrlRegex.exec(payload.url);

			if (!match || !match.groups) {
				SpotifyModule.log("ERROR", "GET_PLAYLIST", "Invalid Spotify playlist URL query.");
				reject(new Error("Invalid playlist URL."));
				return;
			}

			const { playlistId } = match.groups;

			async.waterfall(
				[
					next => {
						let spotifyTracks = [];
						let total = -1;
						let nextUrl = "";

						async.whilst(
							next => {
								SpotifyModule.log(
									"INFO",
									`Getting playlist progress for job (${this.toString()}): ${
										spotifyTracks.length
									} tracks gotten so far. Total tracks: ${total}.`
								);
								next(null, nextUrl !== null);
							},
							next => {
								// Add 250ms delay between each job request
								setTimeout(() => {
									SpotifyModule.runJob("API_GET_PLAYLIST", { playlistId, nextUrl }, this)
										.then(({ response }) => {
											const { data } = response;

											if (!data) {
												next("The provided URL does not exist or cannot be accessed.");
												return;
											}

											total = data.total;
											nextUrl = data.next;

											const { items } = data;
											const trackObjects = items.map(item => item.track);
											const newSpotifyTracks = trackObjects.map(trackObject =>
												spotifyTrackObjectToMusareTrackObject(trackObject)
											);

											spotifyTracks = spotifyTracks.concat(newSpotifyTracks);
											next();
										})
										.catch(err => next(err));
								}, 1000);
							},
							err => {
								if (err) next(err);
								else
									SpotifyModule.runJob("CREATE_TRACKS", { spotifyTracks }, this)
										.then(() => {
											next(
												null,
												spotifyTracks.map(spotifyTrack => spotifyTrack.trackId)
											);
										})
										.catch(next);
							}
						);
					}
				],
				(err, soundcloudTrackIds) => {
					if (err && err !== true) {
						SpotifyModule.log(
							"ERROR",
							"GET_PLAYLIST",
							"Some error has occurred.",
							typeof err === "string" ? err : err.message
						);
						reject(new Error(typeof err === "string" ? err : err.message));
					} else {
						resolve({ songs: soundcloudTrackIds });
					}
				}
			);
		});
	}

	/**
	 * Tries to get alternative artists sources for a list of Spotify artist ids
	 * @param {object} payload - object that contains the payload
	 * @param {string} payload.artistIds - the Spotify artist ids to try and get alternative artist sources for
	 * @param {boolean} payload.collectAlternativeArtistSourcesOrigins - whether to collect the origin of any alternative artist sources found
	 * @returns {Promise} - returns promise (reject, resolve)
	 */
	async GET_ALTERNATIVE_ARTIST_SOURCES_FOR_ARTISTS(payload) {
		const { artistIds, collectAlternativeArtistSourcesOrigins } = payload;

		await async.eachLimit(artistIds, 1, async artistId => {
			try {
				const result = await SpotifyModule.runJob(
					"GET_ALTERNATIVE_ARTIST_SOURCES_FOR_ARTIST",
					{ artistId, collectAlternativeArtistSourcesOrigins },
					this
				);
				this.publishProgress({
					status: "working",
					message: `Got alternative artist source for ${artistId}`,
					data: {
						artistId,
						status: "success",
						result
					}
				});
			} catch (err) {
				this.publishProgress({
					status: "working",
					message: `Failed to get alternative artist source for ${artistId}`,
					data: {
						artistId,
						status: "error"
					}
				});
			}
		});

		this.publishProgress({
			status: "finished",
			message: `Finished getting alternative artist sources`
		});
	}

	/**
	 * Tries to get alternative artist sources for a Spotify artist id
	 * @param {object} payload - object that contains the payload
	 * @param {string} payload.artistId - the Spotify artist id to try and get alternative artist sources for
	 * @param {boolean} payload.collectAlternativeArtistSourcesOrigins - whether to collect the origin of any alternative artist sources found
	 * @returns {Promise} - returns promise (reject, resolve)
	 */
	async GET_ALTERNATIVE_ARTIST_SOURCES_FOR_ARTIST(payload) {
		const { artistId /* , collectAlternativeArtistSourcesOrigins */ } = payload;

		if (!artistId) throw new Error("Artist id provided is not valid.");

		const wikiDataResponse = await WikiDataModule.runJob(
			"API_GET_DATA_FROM_SPOTIFY_ARTIST",
			{ spotifyArtistId: artistId },
			this
		);

		const youtubeChannelIds = Array.from(
			new Set(
				wikiDataResponse.results.bindings
					.filter(binding => !!binding.YouTube_channel_ID)
					.map(binding => binding.YouTube_channel_ID.value)
			)
		);

		// const soundcloudIds = Array.from(
		// 	new Set(
		// 		wikiDataResponse.results.bindings
		// 			.filter(binding => !!binding.SoundCloud_ID)
		// 			.map(binding => binding.SoundCloud_ID.value)
		// 	)
		// );

		// const musicbrainzArtistIds = Array.from(
		// 	new Set(
		// 		wikiDataResponse.results.bindings
		// 			.filter(binding => !!binding.MusicBrainz_artist_ID)
		// 			.map(binding => binding.MusicBrainz_artist_ID.value)
		// 	)
		// );

		return youtubeChannelIds;
	}

	/**
	 * Tries to get alternative album sources for a list of Spotify album ids
	 * @param {object} payload - object that contains the payload
	 * @param {string} payload.albumIds - the Spotify album ids to try and get alternative album sources for
	 * @param {boolean} payload.collectAlternativeAlbumSourcesOrigins - whether to collect the origin of any alternative album sources found
	 * @returns {Promise} - returns promise (reject, resolve)
	 */
	async GET_ALTERNATIVE_ALBUM_SOURCES_FOR_ALBUMS(payload) {
		const { albumIds, collectAlternativeAlbumSourcesOrigins } = payload;

		await async.eachLimit(albumIds, 1, async albumId => {
			try {
				const result = await SpotifyModule.runJob(
					"GET_ALTERNATIVE_ALBUM_SOURCES_FOR_ALBUM",
					{ albumId, collectAlternativeAlbumSourcesOrigins },
					this
				);
				this.publishProgress({
					status: "working",
					message: `Got alternative album source for ${albumId}`,
					data: {
						albumId,
						status: "success",
						result
					}
				});
			} catch (err) {
				this.publishProgress({
					status: "working",
					message: `Failed to get alternative album source for ${albumId}`,
					data: {
						albumId,
						status: "error"
					}
				});
			}
		});

		this.publishProgress({
			status: "finished",
			message: `Finished getting alternative album sources`
		});
	}

	/**
	 * Tries to get alternative album sources for a Spotify album id
	 * @param {object} payload - object that contains the payload
	 * @param {string} payload.albumId - the Spotify album id to try and get alternative album sources for
	 * @param {boolean} payload.collectAlternativeAlbumSourcesOrigins - whether to collect the origin of any alternative album sources found
	 * @returns {Promise} - returns promise (reject, resolve)
	 */
	async GET_ALTERNATIVE_ALBUM_SOURCES_FOR_ALBUM(payload) {
		const { albumId /* , collectAlternativeAlbumSourcesOrigins */ } = payload;

		if (!albumId) throw new Error("Album id provided is not valid.");

		const wikiDataResponse = await WikiDataModule.runJob(
			"API_GET_DATA_FROM_SPOTIFY_ALBUM",
			{ spotifyAlbumId: albumId },
			this
		);

		const youtubePlaylistIds = Array.from(
			new Set(
				wikiDataResponse.results.bindings
					.filter(binding => !!binding.YouTube_playlist_ID)
					.map(binding => binding.YouTube_playlist_ID.value)
			)
		);

		return youtubePlaylistIds;
	}

	/**
	 * Tries to get alternative track sources for a list of Spotify track media sources
	 * @param {object} payload - object that contains the payload
	 * @param {string} payload.mediaSources - the Spotify media sources to try and get alternative track sources for
	 * @param {boolean} payload.collectAlternativeMediaSourcesOrigins - whether to collect the origin of any alternative track sources found
	 * @returns {Promise} - returns promise (reject, resolve)
	 */
	async GET_ALTERNATIVE_MEDIA_SOURCES_FOR_TRACKS(payload) {
		const { mediaSources, collectAlternativeMediaSourcesOrigins } = payload;

		await async.eachLimit(mediaSources, 1, async mediaSource => {
			try {
				const result = await SpotifyModule.runJob(
					"GET_ALTERNATIVE_MEDIA_SOURCES_FOR_TRACK",
					{ mediaSource, collectAlternativeMediaSourcesOrigins },
					this
				);
				this.publishProgress({
					status: "working",
					message: `Got alternative media for ${mediaSource}`,
					data: {
						mediaSource,
						status: "success",
						result
					}
				});
			} catch (err) {
				this.publishProgress({
					status: "working",
					message: `Failed to get alternative media for ${mediaSource}`,
					data: {
						mediaSource,
						status: "error"
					}
				});
			}
		});

		this.publishProgress({
			status: "finished",
			message: `Finished getting alternative media`
		});
	}

	/**
	 * Tries to get alternative track sources for a Spotify track media source
	 * @param {object} payload - object that contains the payload
	 * @param {string} payload.mediaSource - the Spotify media source to try and get alternative track sources for
	 * @param {boolean} payload.collectAlternativeMediaSourcesOrigins - whether to collect the origin of any alternative track sources found
	 * @returns {Promise} - returns promise (reject, resolve)
	 */
	async GET_ALTERNATIVE_MEDIA_SOURCES_FOR_TRACK(payload) {
		const { mediaSource, collectAlternativeMediaSourcesOrigins } = payload;

		if (!mediaSource || !mediaSource.startsWith("spotify:"))
			throw new Error("Media source provided is not a valid Spotify media source.");

		const spotifyTrackId = mediaSource.split(":")[1];

		const { track: spotifyTrack } = await SpotifyModule.runJob(
			"GET_TRACK",
			{
				identifier: spotifyTrackId,
				createMissing: true
			},
			this
		);

		const ISRC = spotifyTrack.externalIds.isrc;
		if (!ISRC) throw new Error(`ISRC not found for Spotify track ${mediaSource}.`);

		const mediaSources = new Set();
		const mediaSourcesOrigins = {};

		const jobsToRun = [];

		try {
			const ISRCApiResponse = await MusicBrainzModule.runJob(
				"API_CALL",
				{
					url: `https://musicbrainz.org/ws/2/isrc/${ISRC}`,
					params: {
						fmt: "json",
						inc: "url-rels+work-rels"
					}
				},
				this
			);

			ISRCApiResponse.recordings.forEach(recording => {
				recording.relations.forEach(relation => {
					if (relation["target-type"] === "url" && relation.url) {
						// relation["type-id"] === "7e41ef12-a124-4324-afdb-fdbae687a89c"
						const { resource } = relation.url;

						if (config.get("experimental.soundcloud") && resource.indexOf("soundcloud.com") !== -1) {
							const promise = new Promise(resolve => {
								SoundcloudModule.runJob(
									"GET_TRACK_FROM_URL",
									{ identifier: resource, createMissing: true },
									this
								)
									.then(response => {
										const { trackId } = response.track;
										const mediaSource = `soundcloud:${trackId}`;

										mediaSources.add(mediaSource);

										if (collectAlternativeMediaSourcesOrigins) {
											const mediaSourceOrigins = [
												`Spotify track ${spotifyTrackId}`,
												`ISRC ${ISRC}`,
												`MusicBrainz recordings`,
												`MusicBrainz recording ${recording.id}`,
												`MusicBrainz relations`,
												`MusicBrainz relation target-type url`,
												`MusicBrainz relation resource ${resource}`,
												`SoundCloud ID ${trackId}`
											];

											if (!mediaSourcesOrigins[mediaSource])
												mediaSourcesOrigins[mediaSource] = [];

											mediaSourcesOrigins[mediaSource].push(mediaSourceOrigins);
										}

										resolve();
									})
									.catch(() => {
										resolve();
									});
							});

							jobsToRun.push(promise);

							return;
						}

						if (resource.indexOf("youtube.com") !== -1 || resource.indexOf("youtu.be") !== -1) {
							const match = youtubeVideoUrlRegex.exec(resource);
							if (!match) throw new Error(`Unable to parse YouTube resource ${resource}.`);

							const { youtubeId } = match.groups;
							if (!youtubeId) throw new Error(`Unable to parse YouTube resource ${resource}.`);

							const mediaSource = `youtube:${youtubeId}`;

							mediaSources.add(mediaSource);

							if (collectAlternativeMediaSourcesOrigins) {
								const mediaSourceOrigins = [
									`Spotify track ${spotifyTrackId}`,
									`ISRC ${ISRC}`,
									`MusicBrainz recordings`,
									`MusicBrainz recording ${recording.id}`,
									`MusicBrainz relations`,
									`MusicBrainz relation target-type url`,
									`MusicBrainz relation resource ${resource}`,
									`YouTube ID ${youtubeId}`
								];

								if (!mediaSourcesOrigins[mediaSource]) mediaSourcesOrigins[mediaSource] = [];

								mediaSourcesOrigins[mediaSource].push(mediaSourceOrigins);
							}

							return;
						}

						return;
					}

					if (relation["target-type"] === "work") {
						const promise = new Promise(resolve => {
							WikiDataModule.runJob(
								"API_GET_DATA_FROM_MUSICBRAINZ_WORK",
								{ workId: relation.work.id },
								this
							)
								.then(resultBody => {
									const youtubeIds = Array.from(
										new Set(
											resultBody.results.bindings
												.filter(binding => !!binding.YouTube_video_ID)
												.map(binding => binding.YouTube_video_ID.value)
										)
									);
									// const soundcloudIds = Array.from(
									// 	new Set(
									// 		resultBody.results.bindings
									// 			.filter(binding => !!binding["SoundCloud_track_ID"])
									// 			.map(binding => binding["SoundCloud_track_ID"].value)
									// 	)
									// );
									const musicVideoEntityUrls = Array.from(
										new Set(
											resultBody.results.bindings
												.filter(binding => !!binding.Music_video_entity_URL)
												.map(binding => binding.Music_video_entity_URL.value)
										)
									);

									youtubeIds.forEach(youtubeId => {
										const mediaSource = `youtube:${youtubeId}`;

										mediaSources.add(mediaSource);

										if (collectAlternativeMediaSourcesOrigins) {
											const mediaSourceOrigins = [
												`Spotify track ${spotifyTrackId}`,
												`ISRC ${ISRC}`,
												`MusicBrainz recordings`,
												`MusicBrainz recording ${recording.id}`,
												`MusicBrainz relations`,
												`MusicBrainz relation target-type work`,
												`MusicBrainz relation work id ${relation.work.id}`,
												`WikiData select from MusicBrainz work id ${relation.work.id}`,
												`YouTube ID ${youtubeId}`
											];

											if (!mediaSourcesOrigins[mediaSource])
												mediaSourcesOrigins[mediaSource] = [];

											mediaSourcesOrigins[mediaSource].push(mediaSourceOrigins);
										}
									});

									// soundcloudIds.forEach(soundcloudId => {
									// 	const mediaSource = `soundcloud:${soundcloudId}`;

									// 	mediaSources.add(mediaSource);

									// 	if (collectAlternativeMediaSourcesOrigins) {
									// 		const mediaSourceOrigins = [
									// 			`Spotify track ${spotifyTrackId}`,
									// 			`ISRC ${ISRC}`,
									// 			`MusicBrainz recordings`,
									// 			`MusicBrainz recording ${recording.id}`,
									// 			`MusicBrainz relations`,
									// 			`MusicBrainz relation target-type work`,
									// 			`MusicBrainz relation work id ${relation.work.id}`,
									// 			`WikiData select from MusicBrainz work id ${relation.work.id}`,
									// 			`SoundCloud ID ${soundcloudId}`
									// 		];

									// 		if (!mediaSourcesOrigins[mediaSource]) mediaSourcesOrigins[mediaSource] = [];

									// 		mediaSourcesOrigins[mediaSource].push(mediaSourceOrigins);
									// 	}
									// });

									const promisesToRun2 = [];

									musicVideoEntityUrls.forEach(musicVideoEntityUrl => {
										promisesToRun2.push(
											new Promise(resolve => {
												WikiDataModule.runJob(
													"API_GET_DATA_FROM_ENTITY_URL",
													{ entityUrl: musicVideoEntityUrl },
													this
												).then(resultBody => {
													const youtubeIds = Array.from(
														new Set(
															resultBody.results.bindings
																.filter(binding => !!binding.YouTube_video_ID)
																.map(binding => binding.YouTube_video_ID.value)
														)
													);
													// const soundcloudIds = Array.from(
													// 	new Set(
													// 		resultBody.results.bindings
													// 			.filter(binding => !!binding["SoundCloud_track_ID"])
													// 			.map(binding => binding["SoundCloud_track_ID"].value)
													// 	)
													// );

													youtubeIds.forEach(youtubeId => {
														const mediaSource = `youtube:${youtubeId}`;

														mediaSources.add(mediaSource);

														// if (collectAlternativeMediaSourcesOrigins) {
														// 	const mediaSourceOrigins = [
														// 		`Spotify track ${spotifyTrackId}`,
														// 		`ISRC ${ISRC}`,
														// 		`MusicBrainz recordings`,
														// 		`MusicBrainz recording ${recording.id}`,
														// 		`MusicBrainz relations`,
														// 		`MusicBrainz relation target-type work`,
														// 		`MusicBrainz relation work id ${relation.work.id}`,
														// 		`WikiData select from MusicBrainz work id ${relation.work.id}`,
														// 		`YouTube ID ${youtubeId}`
														// 	];

														// 	if (!mediaSourcesOrigins[mediaSource]) mediaSourcesOrigins[mediaSource] = [];

														// 	mediaSourcesOrigins[mediaSource].push(mediaSourceOrigins);
														// }
													});

													// soundcloudIds.forEach(soundcloudId => {
													// 	const mediaSource = `soundcloud:${soundcloudId}`;

													// 	mediaSources.add(mediaSource);

													// 	// if (collectAlternativeMediaSourcesOrigins) {
													// 	// 	const mediaSourceOrigins = [
													// 	// 		`Spotify track ${spotifyTrackId}`,
													// 	// 		`ISRC ${ISRC}`,
													// 	// 		`MusicBrainz recordings`,
													// 	// 		`MusicBrainz recording ${recording.id}`,
													// 	// 		`MusicBrainz relations`,
													// 	// 		`MusicBrainz relation target-type work`,
													// 	// 		`MusicBrainz relation work id ${relation.work.id}`,
													// 	// 		`WikiData select from MusicBrainz work id ${relation.work.id}`,
													// 	// 		`SoundCloud ID ${soundcloudId}`
													// 	// 	];

													// 	// 	if (!mediaSourcesOrigins[mediaSource]) mediaSourcesOrigins[mediaSource] = [];

													// 	// 	mediaSourcesOrigins[mediaSource].push(mediaSourceOrigins);
													// 	// }
													// });

													resolve();
												});
											})
										);
									});

									Promise.allSettled(promisesToRun2).then(resolve);
								})
								.catch(err => {
									console.log(err);
									resolve();
								});
						});

						jobsToRun.push(promise);

						// WikiDataModule.runJob("API_GET_DATA_FROM_MUSICBRAINZ_WORK", { workId: relation.work.id }, this));
					}
				});
			});
		} catch (err) {
			console.log("Error during initial ISRC getting/parsing", err);
		}

		try {
			const RecordingApiResponse = await MusicBrainzModule.runJob(
				"API_CALL",
				{
					url: `https://musicbrainz.org/ws/2/recording/`,
					params: {
						fmt: "json",
						query: `isrc:${ISRC}`
					}
				},
				this
			);

			const releaseIds = new Set();
			const releaseGroupIds = new Set();

			RecordingApiResponse.recordings.forEach(recording => {
				// const recordingId = recording.id;
				// console.log("Recording:", recording.id);

				recording.releases.forEach(release => {
					const releaseId = release.id;
					// console.log("Release:", releaseId);

					const releaseGroupId = release["release-group"].id;
					// console.log("Release group:", release["release-group"]);
					// console.log("Release group id:", release["release-group"].id);
					// console.log("Release group type id:", release["release-group"]["type-id"]);
					// console.log("Release group primary type id:", release["release-group"]["primary-type-id"]);
					// console.log("Release group primary type:", release["release-group"]["primary-type"]);

					// d6038452-8ee0-3f68-affc-2de9a1ede0b9 = single
					// 6d0c5bf6-7a33-3420-a519-44fc63eedebf = EP
					if (
						release["release-group"]["type-id"] === "d6038452-8ee0-3f68-affc-2de9a1ede0b9" ||
						release["release-group"]["type-id"] === "6d0c5bf6-7a33-3420-a519-44fc63eedebf"
					) {
						releaseIds.add(releaseId);
						releaseGroupIds.add(releaseGroupId);
					}
				});
			});

			Array.from(releaseGroupIds).forEach(releaseGroupId => {
				const promise = new Promise(resolve => {
					WikiDataModule.runJob("API_GET_DATA_FROM_MUSICBRAINZ_RELEASE_GROUP", { releaseGroupId }, this)
						.then(resultBody => {
							const youtubeIds = Array.from(
								new Set(
									resultBody.results.bindings
										.filter(binding => !!binding.YouTube_video_ID)
										.map(binding => binding.YouTube_video_ID.value)
								)
							);
							// const soundcloudIds = Array.from(
							// 	new Set(
							// 		resultBody.results.bindings
							// 			.filter(binding => !!binding["SoundCloud_track_ID"])
							// 			.map(binding => binding["SoundCloud_track_ID"].value)
							// 	)
							// );
							const musicVideoEntityUrls = Array.from(
								new Set(
									resultBody.results.bindings
										.filter(binding => !!binding.Music_video_entity_URL)
										.map(binding => binding.Music_video_entity_URL.value)
								)
							);

							youtubeIds.forEach(youtubeId => {
								const mediaSource = `youtube:${youtubeId}`;

								mediaSources.add(mediaSource);

								// if (collectAlternativeMediaSourcesOrigins) {
								// 	const mediaSourceOrigins = [
								// 		`Spotify track ${spotifyTrackId}`,
								// 		`ISRC ${ISRC}`,
								// 		`MusicBrainz recordings`,
								// 		`MusicBrainz recording ${recording.id}`,
								// 		`MusicBrainz relations`,
								// 		`MusicBrainz relation target-type work`,
								// 		`MusicBrainz relation work id ${relation.work.id}`,
								// 		`WikiData select from MusicBrainz work id ${relation.work.id}`,
								// 		`YouTube ID ${youtubeId}`
								// 	];

								// 	if (!mediaSourcesOrigins[mediaSource]) mediaSourcesOrigins[mediaSource] = [];

								// 	mediaSourcesOrigins[mediaSource].push(mediaSourceOrigins);
								// }
							});

							// soundcloudIds.forEach(soundcloudId => {
							// 	const mediaSource = `soundcloud:${soundcloudId}`;

							// 	mediaSources.add(mediaSource);

							// 	// if (collectAlternativeMediaSourcesOrigins) {
							// 	// 	const mediaSourceOrigins = [
							// 	// 		`Spotify track ${spotifyTrackId}`,
							// 	// 		`ISRC ${ISRC}`,
							// 	// 		`MusicBrainz recordings`,
							// 	// 		`MusicBrainz recording ${recording.id}`,
							// 	// 		`MusicBrainz relations`,
							// 	// 		`MusicBrainz relation target-type work`,
							// 	// 		`MusicBrainz relation work id ${relation.work.id}`,
							// 	// 		`WikiData select from MusicBrainz work id ${relation.work.id}`,
							// 	// 		`SoundCloud ID ${soundcloudId}`
							// 	// 	];

							// 	// 	if (!mediaSourcesOrigins[mediaSource]) mediaSourcesOrigins[mediaSource] = [];

							// 	// 	mediaSourcesOrigins[mediaSource].push(mediaSourceOrigins);
							// 	// }
							// });

							const promisesToRun2 = [];

							musicVideoEntityUrls.forEach(musicVideoEntityUrl => {
								promisesToRun2.push(
									new Promise(resolve => {
										WikiDataModule.runJob(
											"API_GET_DATA_FROM_ENTITY_URL",
											{ entityUrl: musicVideoEntityUrl },
											this
										).then(resultBody => {
											const youtubeIds = Array.from(
												new Set(
													resultBody.results.bindings
														.filter(binding => !!binding.YouTube_video_ID)
														.map(binding => binding.YouTube_video_ID.value)
												)
											);
											// const soundcloudIds = Array.from(
											// 	new Set(
											// 		resultBody.results.bindings
											// 			.filter(binding => !!binding["SoundCloud_track_ID"])
											// 			.map(binding => binding["SoundCloud_track_ID"].value)
											// 	)
											// );

											youtubeIds.forEach(youtubeId => {
												const mediaSource = `youtube:${youtubeId}`;

												mediaSources.add(mediaSource);

												// if (collectAlternativeMediaSourcesOrigins) {
												// 	const mediaSourceOrigins = [
												// 		`Spotify track ${spotifyTrackId}`,
												// 		`ISRC ${ISRC}`,
												// 		`MusicBrainz recordings`,
												// 		`MusicBrainz recording ${recording.id}`,
												// 		`MusicBrainz relations`,
												// 		`MusicBrainz relation target-type work`,
												// 		`MusicBrainz relation work id ${relation.work.id}`,
												// 		`WikiData select from MusicBrainz work id ${relation.work.id}`,
												// 		`YouTube ID ${youtubeId}`
												// 	];

												// 	if (!mediaSourcesOrigins[mediaSource]) mediaSourcesOrigins[mediaSource] = [];

												// 	mediaSourcesOrigins[mediaSource].push(mediaSourceOrigins);
												// }
											});

											// soundcloudIds.forEach(soundcloudId => {
											// 	const mediaSource = `soundcloud:${soundcloudId}`;

											// 	mediaSources.add(mediaSource);

											// 	// if (collectAlternativeMediaSourcesOrigins) {
											// 	// 	const mediaSourceOrigins = [
											// 	// 		`Spotify track ${spotifyTrackId}`,
											// 	// 		`ISRC ${ISRC}`,
											// 	// 		`MusicBrainz recordings`,
											// 	// 		`MusicBrainz recording ${recording.id}`,
											// 	// 		`MusicBrainz relations`,
											// 	// 		`MusicBrainz relation target-type work`,
											// 	// 		`MusicBrainz relation work id ${relation.work.id}`,
											// 	// 		`WikiData select from MusicBrainz work id ${relation.work.id}`,
											// 	// 		`SoundCloud ID ${soundcloudId}`
											// 	// 	];

											// 	// 	if (!mediaSourcesOrigins[mediaSource]) mediaSourcesOrigins[mediaSource] = [];

											// 	// 	mediaSourcesOrigins[mediaSource].push(mediaSourceOrigins);
											// 	// }
											// });

											resolve();
										});
									})
								);
							});

							Promise.allSettled(promisesToRun2).then(resolve);
						})
						.catch(err => {
							console.log(err);
							resolve();
						});
				});

				jobsToRun.push(promise);
			});
		} catch (err) {
			console.log("Error during getting releases from ISRC", err);
		}

		await Promise.allSettled(jobsToRun);

		return {
			mediaSources: Array.from(mediaSources),
			mediaSourcesOrigins
		};
	}
}

export default new _SpotifyModule();