const introSkipper = {
	originalFetch: window.fetch.bind(window),
	originalXHROpen: XMLHttpRequest.prototype.open,
	d: (msg) => console.debug("[intro skipper] ", msg),
	setup() {
		const self = this;
		this.initializeState();
		this.initializeObserver();
		this.currentOption =
			localStorage.getItem("introskipperOption") || "Show Button";
		window.fetch = this.fetchWrapper.bind(this);
		XMLHttpRequest.prototype.open = function (...args) {
			self.xhrOpenWrapper(this, ...args);
		};
		document.addEventListener("viewshow", this.viewShow.bind(this));
		this.videoPositionChanged = this.videoPositionChanged.bind(this);
		this.handleEscapeKey = this.handleEscapeKey.bind(this);
		this.d("Registered hooks");
	},
	initializeState() {
		Object.assign(this, {
			allowEnter: true,
			skipSegments: {},
			videoPlayer: null,
			skipButton: null,
			osdElement: null,
			skipperData: null,
			currentEpisodeId: null,
			injectMetadata: false,
		});
	},
	initializeObserver() {
		this.observer = new MutationObserver((mutations) => {
			const actionSheet =
				mutations[mutations.length - 1].target.querySelector(".actionSheet");
			if (
				actionSheet &&
				!actionSheet.querySelector(`[data-id="${"introskipperMenu"}"]`)
			)
				this.injectIntroSkipperOptions(actionSheet);
		});
	},
	fetchWrapper(resource, options) {
		const response = this.originalFetch(resource, options);
		const url = new URL(resource);
		if (this.injectMetadata && url.pathname.includes("/MetadataEditor")) {
			this.processMetadata(url.pathname);
		}
		return response;
	},
	xhrOpenWrapper(xhr, method, url, ...rest) {
		url.includes("/PlaybackInfo") && this.processPlaybackInfo(url);
		return this.originalXHROpen.apply(xhr, [method, url, ...rest]);
	},
	async processPlaybackInfo(url) {
		const id = this.extractId(url);
		if (id) {
			try {
				this.skipSegments = await this.secureFetch(
					`Episode/${id}/IntroSkipperSegments`,
				);
			} catch (error) {
				this.d(`Error fetching skip segments: ${error.message}`);
			}
		}
	},
	async processMetadata(url) {
		const id = this.extractId(url);
		if (id) {
			try {
				this.skipperData = await this.secureFetch(`Episode/${id}/Timestamps`);
				if (this.skipperData) {
					this.currentEpisodeId = id;
					requestAnimationFrame(() => {
						const metadataFormFields = document.querySelector(
							".metadataFormFields",
						);
						metadataFormFields && this.injectSkipperFields(metadataFormFields);
					});
				}
			} catch (e) {
				console.error("Error processing", e);
			}
		}
	},
	extractId(searchString) {
		const startIndex = searchString.indexOf("Items/") + 6;
		const endIndex = searchString.indexOf("/", startIndex);
		return endIndex !== -1
			? searchString.substring(startIndex, endIndex)
			: searchString.substring(startIndex);
	},
	/**
	 * Event handler that runs whenever the current view changes.
	 * Used to detect the start of video playback.
	 */
	viewShow() {
		const location = window.location.hash;
		this.d(`Location changed to ${location}`);
		this.allowEnter = true;
		this.injectMetadata = /#\/(tv|details|home|search)/.test(location);
		if (location === "#/video") {
			this.injectCss();
			this.injectButton();
			this.videoPlayer = document.querySelector("video");
			if (this.videoPlayer) {
				this.d("Hooking video timeupdate");
				this.videoPlayer.addEventListener(
					"timeupdate",
					this.videoPositionChanged,
				);
				this.osdElement = document.querySelector("div.videoOsdBottom");
				this.observer.observe(document.body, {
					childList: true,
					subtree: false,
				});
			}
		} else {
			this.observer.disconnect();
		}
	},
	/**
	 * Injects the CSS used by the skip intro button.
	 * Calling this function is a no-op if the CSS has already been injected.
	 */
	injectCss() {
		if (document.querySelector("style#introSkipperCss")) {
			this.d("CSS already added");
			return;
		}
		this.d("Adding CSS");
		const styleElement = document.createElement("style");
		styleElement.id = "introSkipperCss";
		styleElement.textContent = `
            :root {
                --rounding: 4px;
                --accent: 0, 164, 220;
            }
            #skipIntro.upNextContainer {
                width: unset;
                margin: unset;
            }
            #skipIntro {
                position: absolute;
                bottom: 7.5em;
                right: 5em;
                background-color: transparent;
            }
            #skipIntro .emby-button {
                color: #ffffff;
                font-size: 110%;
                background: rgba(0, 0, 0, 0.7);
                border-radius: var(--rounding);
                box-shadow: 0 0 4px rgba(0, 0, 0, 0.6);
                transition: opacity 0.3s cubic-bezier(0.4,0,0.2,1),
                            transform 0.3s cubic-bezier(0.4,0,0.2,1),
                            background-color 0.2s ease-out,
                            box-shadow 0.2s ease-out;
                opacity: 0;
                transform: translateY(50%);
            }
            #skipIntro.show .emby-button {
                opacity: 1;
                transform: translateY(0);
            }
            #skipIntro .emby-button:hover {
                background: rgb(var(--accent));
                box-shadow: 0 0 8px rgba(var(--accent), 0.6);
                filter: brightness(1.2);
            }
            #skipIntro .emby-button:focus {
                background: rgb(var(--accent));
                box-shadow: 0 0 8px rgba(var(--accent), 0.6);
            }
            #btnSkipSegmentText {
                letter-spacing: 0.5px;
                padding: 0 5px 0 5px;
            }
        `;
		document.querySelector("head").appendChild(styleElement);
	},
	/**
	 * Inject the skip intro button into the video player.
	 * Calling this function is a no-op if the CSS has already been injected.
	 */
	async injectButton() {
		// Ensure the button we're about to inject into the page doesn't conflict with a pre-existing one
		const preExistingButton = document.querySelector("div.skipIntro");
		if (preExistingButton) {
			preExistingButton.style.display = "none";
		}
		if (document.querySelector(".btnSkipIntro.injected")) {
			this.d("Button already added");
			this.skipButton = document.querySelector("#skipIntro");
			return;
		}
		const config = await this.secureFetch("Intros/UserInterfaceConfiguration");
		if (!config.SkipButtonVisible) {
			this.d("Not adding button: not visible");
			return;
		}
		this.d("Adding button");
		this.skipButton = document.createElement("div");
		this.skipButton.id = "skipIntro";
		this.skipButton.classList.add("hide", "upNextContainer");
		this.skipButton.addEventListener("click", this.doSkip.bind(this));
		this.skipButton.addEventListener("keydown", this.eventHandler.bind(this));
		this.skipButton.innerHTML = `
            <button is="emby-button" type="button" class="btnSkipIntro injected">
                <span id="btnSkipSegmentText"></span>
                <span class="material-icons skip_next"></span>
            </button>
        `;
		this.skipButton.dataset.Introduction = config.SkipButtonIntroText;
		this.skipButton.dataset.Credits = config.SkipButtonEndCreditsText;
		const controls = document.querySelector("div#videoOsdPage");
		controls.appendChild(this.skipButton);
	},
	/** Tests if the OSD controls are visible. */
	osdVisible() {
		return this.osdElement
			? !this.osdElement.classList.contains("hide")
			: false;
	},
	/** Get the currently playing skippable segment. */
	getCurrentSegment(position) {
		for (const key in this.skipSegments) {
			const segment = this.skipSegments[key];
			if (
				(position > segment.ShowSkipPromptAt &&
					position < segment.HideSkipPromptAt - 1) ||
				(this.osdVisible() &&
					position > segment.IntroStart &&
					position < segment.IntroEnd - 1)
			) {
				return { ...segment, SegmentType: key };
			}
		}
		return { SegmentType: "None" };
	},
	overrideBlur(button) {
		if (!button.originalBlur) {
			button.originalBlur = button.blur;
			button.blur = function () {
				if (!this.contains(document.activeElement)) {
					this.originalBlur();
				}
			};
		}
	},
	/** Playback position changed, check if the skip button needs to be displayed. */
	videoPositionChanged() {
		if (!this.skipButton) return;
		const embyButton = this.skipButton.querySelector(".emby-button");
		const segmentType = this.getCurrentSegment(
			this.videoPlayer.currentTime,
		).SegmentType;
		if (
			segmentType === "None" ||
			this.currentOption === "Off" ||
			!this.allowEnter
		) {
			if (this.skipButton.classList.contains("show")) {
				this.skipButton.classList.remove("show");
				embyButton.addEventListener(
					"transitionend",
					() => {
						this.skipButton.classList.add("hide");
						if (this.osdVisible()) {
							this.osdElement.querySelector("button.btnPause").focus();
						} else {
							embyButton.originalBlur();
						}
					},
					{ once: true },
				);
			}
			return;
		}
		if (
			this.currentOption === "Automatically Skip" ||
			(this.currentOption === "Button w/ auto PiP" &&
				document.pictureInPictureElement)
		) {
			this.doSkip();
			return;
		}
		this.skipButton.querySelector("#btnSkipSegmentText").textContent =
			this.skipButton.dataset[segmentType];
		if (!this.skipButton.classList.contains("hide")) {
			if (!this.osdVisible() && !embyButton.contains(document.activeElement))
				embyButton.focus();
			return;
		}
		requestAnimationFrame(() => {
			this.skipButton.classList.remove("hide");
			requestAnimationFrame(() => {
				this.skipButton.classList.add("show");
				this.overrideBlur(embyButton);
				embyButton.focus();
			});
		});
	},
	/** Seeks to the end of the intro. */
	doSkip() {
		if (!this.allowEnter) return;
		const segment = this.getCurrentSegment(this.videoPlayer.currentTime);
		if (segment.SegmentType === "None") {
			console.warn("[intro skipper] doSkip() called without an active segment");
			return;
		}
		this.d(`Skipping ${segment.SegmentType}`);
		this.allowEnter = false;
		const seekedHandler = () => {
			this.videoPlayer.removeEventListener("seeked", seekedHandler);
			setTimeout(() => {
				this.allowEnter = true;
			}, 500);
		};
		this.videoPlayer.addEventListener("seeked", seekedHandler);
		this.videoPlayer.currentTime =
			segment.SegmentType === "Credits" &&
			this.videoPlayer.duration - segment.IntroEnd < 3
				? this.videoPlayer.duration + 10
				: segment.IntroEnd;
	},
	createButton(ref, id, innerHTML, clickHandler) {
		const button = ref.cloneNode(true);
		button.setAttribute("data-id", id);
		button.innerHTML = innerHTML;
		button.addEventListener("click", clickHandler);
		return button;
	},
	closeSubmenu(fullscreen) {
		document.querySelector(".dialogContainer").remove();
		document.querySelector(".dialogBackdrop").remove();
		document.dispatchEvent(new KeyboardEvent("keydown", { key: "Control" }));
		if (!fullscreen) return;
		document.removeEventListener("keydown", this.handleEscapeKey);
		document.querySelector(".btnVideoOsdSettings").focus();
	},
	openSubmenu(ref, menu) {
		const options = [
			"Show Button",
			"Button w/ auto PiP",
			"Automatically Skip",
			"Off",
		];
		const submenu = menu.cloneNode(true);
		const scroller = submenu.querySelector(".actionSheetScroller");
		scroller.innerHTML = "";
		for (const option of options) {
			if (option !== "Button w/ auto PiP" || document.pictureInPictureEnabled) {
				const button = this.createButton(
					ref,
					`introskipper-${option.toLowerCase().replace(" ", "-")}`,
					`<span class="actionsheetMenuItemIcon listItemIcon listItemIcon-transparent material-icons check" aria-hidden="true" style="visibility:${option === this.currentOption ? "visible" : "hidden"};"></span><div class="listItemBody actionsheetListItemBody"><div class="listItemBodyText actionSheetItemText">${option}</div></div>`,
					() => this.selectOption(option),
				);
				scroller.appendChild(button);
			}
		}
		const backdrop = document.createElement("div");
		backdrop.className = "dialogBackdrop dialogBackdropOpened";
		document.body.append(backdrop, submenu);
		const actionSheet = submenu.querySelector(".actionSheet");
		if (actionSheet.classList.contains("actionsheet-not-fullscreen")) {
			this.adjustPosition(
				actionSheet,
				document.querySelector(".btnVideoOsdSettings"),
			);
			submenu.addEventListener("click", () => this.closeSubmenu(false));
		} else {
			submenu
				.querySelector(".btnCloseActionSheet")
				.addEventListener("click", () => this.closeSubmenu(true));
			scroller.addEventListener("click", () => this.closeSubmenu(true));
			document.addEventListener("keydown", this.handleEscapeKey);
			setTimeout(() => scroller.firstElementChild.focus(), 240);
		}
	},
	selectOption(option) {
		this.currentOption = option;
		localStorage.setItem("introskipperOption", option);
		this.d(`Introskipper option selected and saved: ${option}`);
	},
	isAutoSkipLocked(config) {
		const isAutoSkip = config.AutoSkip && config.AutoSkipCredits;
		const isAutoSkipClient = new Set(config.ClientList.split(",")).has(
			ApiClient.appName(),
		);
		return isAutoSkip || (config.SkipButtonVisible && isAutoSkipClient);
	},
	async injectIntroSkipperOptions(actionSheet) {
		if (!this.skipButton) return;
		const config = await this.secureFetch("Intros/UserInterfaceConfiguration");
		if (this.isAutoSkipLocked(config)) {
			this.d("Auto skip enforced by server");
			return;
		}
		const statsButton = actionSheet.querySelector('[data-id="stats"]');
		if (!statsButton) return;
		const menuItem = this.createButton(
			statsButton,
			"introskipperMenu",
			`<div class="listItemBody actionsheetListItemBody"><div class="listItemBodyText actionSheetItemText">Intro Skipper</div></div><div class="listItemAside actionSheetItemAsideText">${this.currentOption}</div>`,
			() =>
				this.openSubmenu(statsButton, actionSheet.closest(".dialogContainer")),
		);
		const originalWidth = actionSheet.offsetWidth;
		statsButton.before(menuItem);
		if (actionSheet.classList.contains("actionsheet-not-fullscreen"))
			this.adjustPosition(actionSheet, menuItem, originalWidth);
	},
	adjustPosition(element, reference, originalWidth) {
		if (originalWidth) {
			const currentTop = Number.parseInt(element.style.top, 10) || 0;
			element.style.top = `${currentTop - reference.offsetHeight}px`;
			const newWidth = Math.max(reference.offsetWidth - originalWidth, 0);
			const originalLeft = Number.parseInt(element.style.left, 10) || 0;
			element.style.left = `${originalLeft - newWidth / 2}px`;
		} else {
			const rect = reference.getBoundingClientRect();
			element.style.left = `${Math.min(rect.left - (element.offsetWidth - rect.width) / 2, window.innerWidth - element.offsetWidth - 10)}px`;
			element.style.top = `${rect.top - element.offsetHeight + rect.height}px`;
		}
	},
	injectSkipperFields(metadataFormFields) {
		const skipperFields = document.createElement("div");
		skipperFields.className = "detailSection introskipperSection";
		skipperFields.innerHTML = `
            <h2>Intro Skipper</h2>
            <div class="inlineForm">
                <div class="inputContainer">
                    <label class="inputLabel inputLabelUnfocused" for="introStart">Intro Start</label>
                    <input type="text" id="introStartDisplay" class="emby-input custom-time-input" readonly>
                    <input type="number" id="introStartEdit" class="emby-input custom-time-input" style="display: none;" step="any" min="0">
                </div>
                <div class="inputContainer">
                    <label class="inputLabel inputLabelUnfocused" for="introEnd">Intro End</label>
                    <input type="text" id="introEndDisplay" class="emby-input custom-time-input" readonly>
                    <input type="number" id="introEndEdit" class="emby-input custom-time-input" style="display: none;" step="any" min="0">
                </div>
            </div>
            <div class="inlineForm">
                <div class="inputContainer">
                    <label class="inputLabel inputLabelUnfocused" for="creditsStart">Credits Start</label>
                    <input type="text" id="creditsStartDisplay" class="emby-input custom-time-input" readonly>
                    <input type="number" id="creditsStartEdit" class="emby-input custom-time-input" style="display: none;" step="any" min="0">
                </div>
                <div class="inputContainer">
                    <label class="inputLabel inputLabelUnfocused" for="creditsEnd">Credits End</label>
                    <input type="text" id="creditsEndDisplay" class="emby-input custom-time-input" readonly>
                    <input type="number" id="creditsEndEdit" class="emby-input custom-time-input" style="display: none;" step="any" min="0">
                </div>
            </div>
        `;
		metadataFormFields
			.querySelector("#metadataSettingsCollapsible")
			.insertAdjacentElement("afterend", skipperFields);
		this.attachSaveListener(metadataFormFields);
		this.updateSkipperFields(skipperFields);
		this.setTimeInputs(skipperFields);
	},
	updateSkipperFields(skipperFields) {
		const { Introduction = {}, Credits = {} } = this.skipperData;
		skipperFields.querySelector("#introStartEdit").value =
			Introduction.Start || 0;
		skipperFields.querySelector("#introEndEdit").value = Introduction.End || 0;
		skipperFields.querySelector("#creditsStartEdit").value = Credits.Start || 0;
		skipperFields.querySelector("#creditsEndEdit").value = Credits.End || 0;
	},
	attachSaveListener(metadataFormFields) {
		const saveButton = metadataFormFields.querySelector(
			".formDialogFooter .btnSave",
		);
		if (saveButton) {
			saveButton.addEventListener("click", this.saveSkipperData.bind(this));
		} else {
			console.error("Save button not found");
		}
	},
	setTimeInputs(skipperFields) {
		const inputContainers = skipperFields.querySelectorAll(".inputContainer");
		for (const container of inputContainers) {
			const displayInput = container.querySelector('[id$="Display"]');
			const editInput = container.querySelector('[id$="Edit"]');
			displayInput.addEventListener("pointerdown", (e) => {
				e.preventDefault();
				this.switchToEdit(displayInput, editInput);
			});
			editInput.addEventListener("blur", () =>
				this.switchToDisplay(displayInput, editInput),
			);
			displayInput.value = this.formatTime(
				Number.parseFloat(editInput.value) || 0,
			);
		}
	},
	formatTime(totalSeconds) {
		if (!totalSeconds) return "0 seconds";
		const totalRoundedSeconds = Math.round(totalSeconds);
		const hours = Math.floor(totalRoundedSeconds / 3600);
		const minutes = Math.floor((totalRoundedSeconds % 3600) / 60);
		const seconds = totalRoundedSeconds % 60;
		const result = [];
		if (hours) result.push(`${hours} hour${hours !== 1 ? "s" : ""}`);
		if (minutes) result.push(`${minutes} minute${minutes !== 1 ? "s" : ""}`);
		if (seconds || !result.length)
			result.push(`${seconds} second${seconds !== 1 ? "s" : ""}`);
		return result.join(" ");
	},
	switchToEdit(displayInput, editInput) {
		displayInput.style.display = "none";
		editInput.style.display = "";
		editInput.focus();
	},
	switchToDisplay(displayInput, editInput) {
		editInput.style.display = "none";
		displayInput.style.display = "";
		displayInput.value = this.formatTime(
			Number.parseFloat(editInput.value) || 0,
		);
	},
	async saveSkipperData() {
		const newTimestamps = {
			Introduction: {
				Start: Number.parseFloat(
					document.getElementById("introStartEdit").value || 0,
				),
				End: Number.parseFloat(
					document.getElementById("introEndEdit").value || 0,
				),
			},
			Credits: {
				Start: Number.parseFloat(
					document.getElementById("creditsStartEdit").value || 0,
				),
				End: Number.parseFloat(
					document.getElementById("creditsEndEdit").value || 0,
				),
			},
		};
		const { Introduction = {}, Credits = {} } = this.skipperData;
		if (
			newTimestamps.Introduction.Start !== (Introduction.Start || 0) ||
			newTimestamps.Introduction.End !== (Introduction.End || 0) ||
			newTimestamps.Credits.Start !== (Credits.Start || 0) ||
			newTimestamps.Credits.End !== (Credits.End || 0)
		) {
			const response = await this.secureFetch(
				`Episode/${this.currentEpisodeId}/Timestamps`,
				"POST",
				JSON.stringify(newTimestamps),
			);
			this.d(
				response.ok
					? "Timestamps updated successfully"
					: "Failed to update timestamps:",
				response.status,
			);
		} else {
			this.d("Timestamps have not changed, skipping update");
		}
	},
	/** Make an authenticated fetch to the Jellyfin server and parse the response body as JSON. */
	async secureFetch(url, method = "GET", body = null) {
		const response = await fetch(`${ApiClient.serverAddress()}/${url}`, {
			method,
			headers: Object.assign(
				{ Authorization: `MediaBrowser Token=${ApiClient.accessToken()}` },
				method === "POST" ? { "Content-Type": "application/json" } : {},
			),
			body,
		});
		return response.ok
			? method === "POST"
				? response
				: response.json()
			: response.status === 404
				? null
				: console.error(`Error ${response.status} from ${url}`) || null;
	},
	/** Handle keydown events. */
	eventHandler(e) {
		if (e.key !== "Enter") return;
		e.stopPropagation();
		e.preventDefault();
		this.doSkip();
	},
	handleEscapeKey(e) {
		if (e.key === "Escape" || e.keyCode === 461 || e.keyCode === 10009) {
			e.stopPropagation();
			this.closeSubmenu(true);
		}
	},
};
introSkipper.setup();