/** @type {import("node-fetch").default} */
// @ts-ignore
const fetch = require("node-fetch")
const path = require("path")
const {render} = require("pinski/plugins")
const db = require("../utils/db")
const {getUser, getToken} = require("../utils/getuser")
// const pug = require("pug")
const converters = require("../utils/converters")
const constants = require("../utils/constants")

const cacheManager = require("../eirtubeMods/cache-manager")
const downloader = require("../eirtubeMods/downloader")
const dlQueue = require("../eirtubeMods/dl-queue")
const sb = require("../eirtubeMods/sb")
const comments = require("../eirtubeMods/comments")
const ratelimiting = require("../eirtubeMods/ratelimiting")
const qualitySort = require("../eirtubeMods/quality-sort")

class InstanceError extends Error {
	constructor(error, identifier) {
		super(error)
		this.identifier = identifier
	}
}

class MessageError extends Error {
}

module.exports = [
	{
		route: "/watch", methods: ["GET", "POST"], upload: true, code: async ({req, url, body}) => {
			// Prepare data needed to render video page
			const user = getUser(req)
			const settings = user.getSettingsOrDefaults()
			const id = url.searchParams.get("v")

			// Check if should watch on YouTube
			if (settings.local === 2) {
				const dest = `https://www.youtube.com${url.pathname}${url.search}` //#cloudtube`
				user.addWatchedVideoMaybe(id)
				return {
					statusCode: 302,
					contentType: "text/plain",
					headers: {
						"Location": dest
					},
					content: `Redirecting to ${dest}...`
				}
			}

			// Check if playback is allowed
			const videoTakedownInfo = db.prepare("SELECT id, org, url FROM TakedownVideos WHERE id = ?").get(id)
			if (videoTakedownInfo) {
				downloader.tryDeleteFromCache(id);
				return render(451, "pug/takedown-video.pug", Object.assign({req, settings}, videoTakedownInfo))
			}

			const responseHeaders = {
				Location: req.url
			}

			// Media fragment
			const t = url.searchParams.get("t")
			let mediaFragment = converters.tToMediaFragment(t)

			// Continuous mode
			const continuous = url.searchParams.get("continuous") === "1"
			const autoplay = url.searchParams.get("autoplay") != "0"
			const swp = url.searchParams.get("session-watched")
			const sessionWatched = swp ? swp.split(" ") : []
			const sessionWatchedNext = sessionWatched.concat([id]).join("+")
			// if (continuous) settings.quality = 0 // autoplay with synced streams does not work

			// Work out how to fetch the video
			let destName = `${id}-${url.searchParams.get("quality") || "360p"}`
			let willDownload = !downloader.videoExists(destName) && (downloader.downloadStatus(destName) == 0 || downloader.downloadStatus(destName) == 3)
			var videoFuture = cacheManager.read("video")[id];
			if (videoFuture == undefined  || Object.keys(videoFuture).length == 0 || !downloader.videoExists(id)) {
				if (req.method === "GET") {
					if (settings.local === 1) { // skip to the local fetching page, which will then POST video data in a moment
						return render(200, "pug/local-video.pug", {req, settings, id}, responseHeaders)
					}
					var instanceOrigin = settings.instance
				} else { // req.method === "POST"
					willDownload = true
					var instanceOrigin = "http://localhost:3000"
					videoFuture = JSON.parse(new URLSearchParams(body.toString()).get("video"))
					videoFuture = await downloader.formatVideoData(videoFuture, settings)
					cacheManager.write("video", id, videoFuture)
				}

				// Try fetch/download
				try {
					videoFuture = downloader.fetchVideoData(id, req)
				} catch(e) { willDownload = false }
			}

			try {
				// Fetch the video
				const video = await videoFuture

				// Error handling
				if (!video || Object.keys(video).length == 0) throw new MessageError("The instance returned null.")
				if (video.error) throw new InstanceError(video.error, video.identifier)
				if (video.errno) throw video
				if (video.liveNow || video.isUpcoming) throw new MessageError("Live playback is not supported.")
				if (video.isListed == false || video.paid || video.premium) throw new MessageError("Video is private or premium.")
				if ((!video.formatStreams || video.formatStreams.length == 0) && (!video.adaptiveFormats || video.adaptiveFormats.length == 0)) throw new MessageError("Video returned no streams.")

				// Set data to video cache
				if (video.descriptionHtml && video.recommendedVideos && video.recommendedVideos.length > 0)
					cacheManager.write("video", id, video)

				// Check if channel playback is allowed
				const channelTakedownInfo = db.prepare("SELECT ucid, org, url FROM TakedownChannels WHERE ucid = ?").get(video.authorId)
				if (channelTakedownInfo) {
					// automatically add the entry to the videos list, so it won't be fetched again
					const args = {id, ...channelTakedownInfo}
					db.prepare("INSERT INTO TakedownVideos (id, org, url) VALUES (@id, @org, @url)").run(args)
					return render(451, "pug/takedown-video.pug", Object.assign({req, settings}, channelTakedownInfo), responseHeaders)
				}

				// Fetch comments
				// TODO: allow disabling comments via setting
				if (!video.comments)
					video.comments = await comments.getVideoComments(id, req)

				// process stream list ordering
				let formats = await downloader.sortFormats(video, ([480, 720, 1080])[settings.quality])
				let startingFormat = formats[0]

				// Order formats like youtube.
				//   - Eir
				let uniqueFormats = []
				for (let i = 0; i < formats.length; i++) {
					if (!uniqueFormats.some(format => format.qualityLabel == formats[i].qualityLabel))
						uniqueFormats.push(formats[i])
				}
				uniqueFormats.sort((a, b) => b.second__height - a.second__height)

				// Any errors to display in toasts when the page actually loads
				let toasts = []

				// Apply SponsorBlock data
				if (settings.sponsorblock > 0) {
					const sbData = await sb.getSB(id)
					if (sbData && !sbData.error)
						video.sbData = sbData.data
				}
				// Apply DeArrow data
				// if (settings.dearrow > 0) {
				// 	const dearrowData = await sb.getDeArrow(id)
				// 	if (dearrowData && !dearrowData.error)
				// 		video.dearrowData = dearrowData.data
				// }

				// filter list
				const {videos, filteredCount} = converters.applyVideoFilters(video.recommendedVideos, user.getFilters())
				video.recommendedVideos = videos
				// Apply DeArrow data (recommended)
				if (settings.dearrow > 0) {
					if (settings.dearrow_preload == 1) {
						const dearrowOut = await sb.applyToAllDeArrow(video.recommendedVideos)
						if (Object.keys(dearrowOut.errors).length > 0)
							for (const deE of Object.keys(dearrowOut.errors))
								toasts.push({ color: "red", icon: "x", text: dearrowOut.errors[deE] })
					} else
						sb.getAllDeArrowNoBlock(video.recommendedVideos)
				}

				// get subscription data
				const subscribed = user.isSubscribed(video.authorId)

				// process watched videos
				user.addWatchedVideoMaybe(video.videoId)
				const watchedVideos = user.getWatchedVideos()
				if (watchedVideos.length) {
					for (const rec of video.recommendedVideos) {
						rec.watched = watchedVideos.includes(rec.videoId)
					}
				}

				let targetFormat = startingFormat // By default, same as video format. Will be set to another quality if using autoHD or &quality= in url
				let awaitingNewFormat = false

				// Allow specifying "&quality" in the URL to choose a specific quality
				// Needed to fetch a new quality from the same page
				let q = url.searchParams.get("quality")
				let qSet = false
				let largestSavedFormat
				let qualities = {} // When searching for a specific quality, if the specified one doesn't exist, retrieve the closest match

				let videoPath = `/getVideo?v=${id}&q=${q || "360p"}`

				for (const format of formats) {
					// apply media fragment to all sources + get largest format that is currently on disk
					format.url += mediaFragment

					// If a specific quality is chosen, find it
					if (!qSet && q != undefined) {
						if (format.type.startsWith("video")) {
							if (format.qualityLabel == q) {
								targetFormat = format
								qSet = true
								break
							}
							qualities[format.qualityLabel] = format
						} else
							continue
					}

					// Find the largest downloaded version of the video
					let fName = `${id}-${format.qualityLabel}`
					if (downloader.videoExists(fName) && (downloader.downloadStatus(fName) == 0 || downloader.downloadStatus(fName) == 3))
						if (largestSavedFormat == undefined || format.second__height + (format.fps / 100) > largestSavedFormat.second__height + (format.fps / 100))
							largestSavedFormat = format
				}

				// Still looking for a specific quality? match it here:
				if (!qSet && q) {
					q = qualitySort.sort(Object.keys(qualities), q)[0]
					targetFormat = qualities[q]
					qSet = true
				}

				// If a large format is already on disk, just use it instead
				if (largestSavedFormat) {
					startingFormat = largestSavedFormat
					videoPath = `/getVideo?v=${id}&q=${largestSavedFormat.qualityLabel}`
					willDownload = false

					// Highest available format <= target height
					if (largestSavedFormat.second__height > 360)
						formats = await downloader.sortFormats(video, largestSavedFormat.second__height)
				}

				if (settings.autoHD == 1 && !qSet) {
					// Get first adaptive format
					// Sorted already by user preferences to be their chosen quality
					for (const format of formats) {
						if (format.cloudtube__label.endsWith(" *") && format.type.startsWith("video") && format.second__height >= startingFormat.second__height) {
							targetFormat = format
							break
						}
					}
				}

				// Rate limiting
				const checkAuth = ratelimiting.authorize(req, willDownload, responseHeaders)
				if (checkAuth != true)
					return checkAuth

				// Download base video
				if (!qSet && willDownload && !largestSavedFormat) {
					console.log(`Starting download for ${id} (${startingFormat.qualityLabel})`);
					dlQueue.enqueue(id, startingFormat.qualityLabel)
					// Wait for a second to give the default noscript player at least a small portion of bytes to play
					await new Promise(resolve => setTimeout(resolve, 1000))
				}

				// Only preload if new quality is better than what we already have, unless using &quality
				// TODO: replace max video length with max file size
				if ((targetFormat.second__height > startingFormat.second__height && video.lengthSeconds < constants.server_setup.video_hq_preload_max_time) || qSet) {
					destName = `${id}-${targetFormat.qualityLabel}`

					// Check video already exists
					if (downloader.videoExists(destName)) {
						startingFormat = targetFormat
						videoPath = `/getVideo?v=${id}&q=${targetFormat.qualityLabel}`
					} else if (downloader.downloadStatus(destName) == 0 || downloader.downloadStatus(destName) == 3) {
						const checkAuth = ratelimiting.authorize(req, true, responseHeaders)
						if (checkAuth != true && checkAuth.statusCode == 429) {
							// Rate limited, don't preload higher quality
							awaitingNewFormat = true
							toasts.push({ color: "red", icon: "x", text: `Too many requests. You may download ${targetFormat.qualityLabel} in ${checkAuth.headers["Retry-After"]} seconds...` })
						} else {
							dlQueue.enqueue(id, targetFormat.qualityLabel)
							awaitingNewFormat = !downloader.videoExists(destName)
						}
					} else {
						awaitingNewFormat = downloader.downloadStatus(destName) < 3
					}
				}

				// Start preparing thumbnail stream if it doesn't exist.
				const sourceName = `${id}-${startingFormat.qualityLabel}`
				if (!downloader.videoExists(`${id}-thumb`) && downloader.downloadStatus(`${id}-thumb`) == 0) {
					cacheManager.write("download", `${id}-thumb`, { status: 1 }) // Makes /getVideo wait for thumb stream to finish
					const pathBase = path.join(path.join(__dirname, "../"), constants.server_setup.video_dl_path)

					// Wait until video finishes downloading
					const preloadForThumb = setInterval(() => {
						const status = downloader.downloadStatus(sourceName)
						if ((status == 0 || status == 3) && downloader.videoExists(sourceName)) {
							clearInterval(preloadForThumb)

							// Finally start creating thumb stream
							downloader.createThumbStream(`${id}-thumb`, path.join(pathBase, sourceName), path.join(pathBase, `${id}-thumb`), video.lengthSeconds)
						}
					}, 1000 * 2)
				}

				return render(200, "pug/video.pug", {
					req, url, video, formats: uniqueFormats, subscribed, mediaFragment, autoplay, continuous, sessionWatched,
					sessionWatchedNext, settings, videoPath, startingFormat, targetFormat, awaitingNewFormat, toasts,
					dlStatus: downloader.downloadStatus(sourceName), awaitingThumb: downloader.downloadStatus(`${id}-thumb`) > 0 && downloader.downloadStatus(`${id}-thumb`) < 3,
					timeToPastText: converters.timeToPastText, likeCountToText: converters.likeCountToText, rewriteVideoDescription: converters.rewriteVideoDescription
				}, responseHeaders)
			} catch (error) {
				// Something went wrong, somewhere! Find out where.

				let errorType = "unrecognised-error"
				const locals = {instanceOrigin, error}

				// Sort error category
				if (error instanceof fetch.FetchError) {
					errorType = "fetch-error"
				} else if (error instanceof MessageError) {
					errorType = "message-error"
				} else if (error instanceof InstanceError) {
					if (error.identifier === "RATE_LIMITED_BY_YOUTUBE" || error.message === "Could not extract video info. Instance is likely blocked.") {
						errorType = "rate-limited"
					} else {
						errorType = "instance-error"
					}
				}

				// Create appropriate formatted message
				const message = render(0, `pug/errors/${errorType}.pug`, locals).content

				return render(500, "pug/video.pug", {video: {videoId: id}, error: true, message, req, settings}, responseHeaders)
			}
		}
	}
]
