(() => {
  const BOX_ID = "__shift_middle_link_copy_box__";
  const TOAST_ID = "__shift_middle_link_copy_toast__";
  const BORDER_COLOR = "#22c55e";
  const LINK_BORDER_COLOR = "#ef4444";
  const YOUTUBE_DISLIKE_ID = "__tablinker_youtube_dislike__";
  const YOUTUBE_DISLIKE_STYLE_ID = "__tablinker_youtube_dislike_style__";
  const YOUTUBE_DISLIKE_ATTRIBUTION_URL = "https://returnyoutubedislike.com";
  const YOUTUBE_LOOP_ID = "__tablinker_youtube_loop__";
  const YOUTUBE_LOOP_STYLE_ID = "__tablinker_youtube_loop_style__";
  const BASE64_DECODE_CLASS = "__one_base64_decode__";
  const BASE64_DECODE_STYLE_ID = "__one_base64_decode_style__";
  const BASE64_TEXT_PATTERN = /[A-Za-z0-9+/_-]{16,}={0,2}/g;
  const DOWNLOAD_FILE_EXTENSION_PATTERN = /\.(?:7z|zip|rar|tar|gz|bz2|xz|exe|msi|dmg|pkg|deb|rpm|apk|crx|iso|pdf|docx?|xlsx?|pptx?|hwp|hwpx|csv|tsv|txt|json|xml|mp[34]|m4a|mkv|avi|mov|webm|wav|flac|jpe?g|png|gif|webp|svg)(?:[?#]|$)/i;
  const SPONSOR_BLOCK_SKIP_OFFSET = 0.03;
  const EXTENSION_CONTEXT_ERROR = "확장 컨텍스트가 만료되었습니다. 탭을 새로고침하세요.";
  const FEATURE_DEFAULTS = {
    linkCopyEnabled: true,
    closedTabsEnabled: true,
    gmailUnreadEnabled: true,
    windowSessionEnabled: true,
    youtubeDislikeEnabled: true,
    sponsorBlockEnabled: true,
    youtubeLoopEnabled: true,
    passwordUnmaskEnabled: false,
    singleFileSaveEnabled: true,
    base64AutoDecodeEnabled: true,
    downloadAcceleratorEnabled: true
  };

  let drag = null;
  let suppressAuxClickUntil = 0;
  let featureSettings = { ...FEATURE_DEFAULTS };
  let youtubeObserver = null;
  let youtubeUpdateTimer = 0;
  let youtubeRequestId = 0;
  let sponsorBlockObserver = null;
  let sponsorBlockUpdateTimer = 0;
  let sponsorBlockRequestId = 0;
  let sponsorBlockVideo = null;
  let sponsorBlockVideoId = "";
  let sponsorBlockSegments = [];
  let sponsorBlockLastSkipAt = 0;
  let youtubeLoopObserver = null;
  let youtubeLoopUpdateTimer = 0;
  let youtubeLoopVideo = null;
  let youtubeLoopVideoId = "";
  let youtubeLoopActive = false;
  let youtubeLoopStart = 0;
  let youtubeLoopEnd = null;
  let youtubeLoopLimit = Infinity;
  let youtubeLoopCompleted = 0;
  let passwordObserver = null;
  let base64DecodeObserver = null;
  let base64DecodeTimer = 0;
  let base64DecodePendingRoots = new Set();
  let gmailTitleObserver = null;
  let lastReportedGmailUnread = null;

  loadFeatureSettings();
  document.addEventListener("mousedown", onMouseDown, true);
  document.addEventListener("keydown", onKeyDown, true);
  document.addEventListener("click", onDownloadLinkClick, true);
  document.addEventListener("auxclick", onDownloadLinkAuxClick, true);
  document.addEventListener("auxclick", onAuxClick, true);
  document.addEventListener("contextmenu", onContextMenu, true);
  addStorageChangedListener(onStorageChanged);
  addRuntimeMessageListener(onRuntimeMessage);

  function loadFeatureSettings() {
    readSyncStorage(FEATURE_DEFAULTS, (settings) => {
      featureSettings = { ...FEATURE_DEFAULTS, ...settings };
      syncFeatureState();
    });
  }

  function readSyncStorage(defaults, callback) {
    const syncStorage = globalThis.chrome?.storage?.sync;
    if (typeof syncStorage?.get !== "function") {
      callback(defaults);
      return;
    }

    try {
      syncStorage.get(defaults, (settings) => callback(settings || defaults));
    } catch {
      callback(defaults);
    }
  }

  function addStorageChangedListener(listener) {
    const onChanged = globalThis.chrome?.storage?.onChanged;
    if (typeof onChanged?.addListener !== "function") {
      return;
    }

    try {
      onChanged.addListener(listener);
    } catch {
      // The extension can be reloaded while old content scripts are still alive.
    }
  }

  function addRuntimeMessageListener(listener) {
    const onMessage = globalThis.chrome?.runtime?.onMessage;
    if (typeof onMessage?.addListener !== "function") {
      return;
    }

    try {
      onMessage.addListener(listener);
    } catch {
      // The extension can be reloaded while old content scripts are still alive.
    }
  }

  function removeRuntimeMessageListener(listener) {
    const onMessage = globalThis.chrome?.runtime?.onMessage;
    if (typeof onMessage?.removeListener !== "function") {
      return;
    }

    try {
      onMessage.removeListener(listener);
    } catch {
      // The extension can be reloaded while old content scripts are still alive.
    }
  }

  function getRuntimeLastError() {
    try {
      return globalThis.chrome?.runtime?.lastError || null;
    } catch {
      return { message: EXTENSION_CONTEXT_ERROR };
    }
  }

  function sendRuntimeMessage(message, callback) {
    const runtime = globalThis.chrome?.runtime;
    if (typeof runtime?.sendMessage !== "function") {
      callback?.({ ok: false, error: EXTENSION_CONTEXT_ERROR }, { message: EXTENSION_CONTEXT_ERROR });
      return false;
    }

    try {
      runtime.sendMessage(message, (response) => callback?.(response, getRuntimeLastError()));
      return true;
    } catch (error) {
      const messageText = error?.message || EXTENSION_CONTEXT_ERROR;
      callback?.({ ok: false, error: messageText }, { message: messageText });
      return false;
    }
  }

  function onStorageChanged(changes, areaName) {
    if (areaName !== "sync") {
      return;
    }

    let changed = false;
    for (const key of Object.keys(FEATURE_DEFAULTS)) {
      if (changes[key]) {
        featureSettings[key] = changes[key].newValue !== false;
        changed = true;
      }
    }

    if (changed) {
      syncFeatureState();
    }
  }

  function syncFeatureState() {
    if (!featureSettings.linkCopyEnabled && drag) {
      cancelDrag();
    }

    if (featureSettings.youtubeDislikeEnabled) {
      initYoutubeDislike();
    } else {
      stopYoutubeDislike();
    }

    if (featureSettings.sponsorBlockEnabled) {
      initSponsorBlock();
    } else {
      stopSponsorBlock();
    }

    if (featureSettings.youtubeLoopEnabled) {
      initYoutubeLoop();
    } else {
      stopYoutubeLoop();
    }

    if (featureSettings.passwordUnmaskEnabled) {
      initPasswordUnmask();
    } else {
      stopPasswordUnmask();
    }

    if (featureSettings.base64AutoDecodeEnabled) {
      initBase64AutoDecode();
    } else {
      stopBase64AutoDecode();
    }

    if (featureSettings.gmailUnreadEnabled) {
      initGmailUnreadObserver();
    } else {
      stopGmailUnreadObserver();
    }
  }

  function onRuntimeMessage(message, _sender, sendResponse) {
    if (message?.type !== "save-single-file-page" || window.top !== window) {
      return false;
    }

    saveSingleFilePage()
      .then((data) => sendResponse({ ok: true, ...data }))
      .catch((error) => sendResponse({ ok: false, error: error.message }));
    return true;
  }

  function onKeyDown(event) {
    const key = normalizeShortcutKey(event);
    if (!key) {
      return;
    }

    const isModifierShortcut = (event.ctrlKey || event.metaKey) && event.shiftKey && !event.altKey;
    if (!isModifierShortcut) {
      return;
    }

    if (key === "t" && featureSettings.closedTabsEnabled) {
      event.preventDefault();
      event.stopPropagation();
      sendRuntimeMessage({ type: "restore-last-closed-tab" });
      return;
    }

    if (key === "s" && featureSettings.windowSessionEnabled) {
      event.preventDefault();
      event.stopPropagation();
      sendRuntimeMessage({ type: "toggle-window-session" });
      return;
    }

    if (key === "q" && featureSettings.gmailUnreadEnabled) {
      event.preventDefault();
      event.stopPropagation();
      sendRuntimeMessage({ type: "open-gmail" });
    }
  }

  function normalizeShortcutKey(event) {
    if (event.code === "KeyT" || event.key === "t" || event.key === "T") {
      return "t";
    }
    if (event.code === "KeyS" || event.key === "s" || event.key === "S") {
      return "s";
    }
    if (event.code === "KeyQ" || event.key === "q" || event.key === "Q") {
      return "q";
    }

    return "";
  }

  function onMouseDown(event) {
    if (!featureSettings.linkCopyEnabled || !event.shiftKey || event.button !== 1 || drag) {
      return;
    }

    event.preventDefault();
    event.stopPropagation();

    drag = {
      startX: event.clientX,
      startY: event.clientY,
      lastX: event.clientX,
      lastY: event.clientY,
      box: createBox(),
      highlights: []
    };

    updateBox();
    updateLinkHighlights();

    window.addEventListener("mousemove", onMouseMove, true);
    window.addEventListener("mouseup", onMouseUp, true);
    window.addEventListener("blur", cancelDrag, true);
  }

  function onMouseMove(event) {
    if (!drag) {
      return;
    }

    event.preventDefault();
    event.stopPropagation();

    drag.lastX = event.clientX;
    drag.lastY = event.clientY;
    updateBox();
    updateLinkHighlights();
  }

  async function onMouseUp(event) {
    if (!drag || event.button !== 1) {
      return;
    }

    event.preventDefault();
    event.stopPropagation();
    suppressAuxClickUntil = Date.now() + 750;

    const rect = selectionRect();
    cleanupDrag();

    if (rect.width < 3 || rect.height < 3) {
      showToast("선택 영역 없음");
      return;
    }

    const links = collectLinks(rect);
    if (!links.length) {
      showToast("링크 없음");
      return;
    }

    const text = links.join("\n");
    const copied = await copyText(text);
    showToast(copied ? `링크 ${links.length}개 복사됨` : "복사 실패");
  }

  function onAuxClick(event) {
    if (event.button === 1 && Date.now() < suppressAuxClickUntil) {
      event.preventDefault();
      event.stopPropagation();
    }
  }

  function onDownloadLinkClick(event) {
    if (event.button !== 0) {
      return;
    }

    handleDownloadLinkEvent(event);
  }

  function onDownloadLinkAuxClick(event) {
    if (event.button !== 1 || Date.now() < suppressAuxClickUntil) {
      return;
    }

    handleDownloadLinkEvent(event);
  }

  function handleDownloadLinkEvent(event) {
    const link = getAccelerableDownloadLink(event);
    if (!link) {
      return;
    }

    const request = {
      url: link.href,
      referrer: document.referrer || location.href,
      tabUrl: location.href,
      filename: link.getAttribute("download") || ""
    };
    const fallback = {
      url: link.href,
      target: link.target || "",
      button: event.button,
      ctrlKey: event.ctrlKey,
      metaKey: event.metaKey,
      shiftKey: event.shiftKey
    };

    event.preventDefault();
    event.stopPropagation();
    accelerateDownloadLink(request, fallback);
  }

  async function accelerateDownloadLink(request, fallback) {
    const response = await sendRuntimeRequest({ type: "accelerate-download-link", ...request });
    if (response?.ok) {
      showToast("다운로드 시작 창 열림");
      return;
    }

    showToast("가속기 실패: Chrome으로 열기");
    openDownloadLinkFallback(fallback);
  }

  function getAccelerableDownloadLink(event) {
    if (!featureSettings.downloadAcceleratorEnabled || event.defaultPrevented || drag || event.shiftKey) {
      return null;
    }

    const link = event.target?.closest?.("a[href]");
    if (!link || !isAccelerableDownloadHref(link.href) || !isLikelyDownloadLink(link)) {
      return null;
    }

    return link;
  }

  function isLikelyDownloadLink(link) {
    if (link.hasAttribute("download")) {
      return true;
    }

    const type = (link.getAttribute("type") || "").toLowerCase();
    if (type && !type.startsWith("text/html")) {
      return true;
    }

    return isLikelyDownloadUrl(link.href);
  }

  function isLikelyDownloadUrl(value) {
    try {
      const url = new URL(value);
      if (DOWNLOAD_FILE_EXTENSION_PATTERN.test(`${url.pathname}${url.search}`)) {
        return true;
      }

      const pathParts = url.pathname.toLowerCase().split("/").filter(Boolean);
      if (pathParts.includes("download") || pathParts.includes("downloads")) {
        return true;
      }

      for (const key of ["download", "filename", "file", "attachment"]) {
        if (url.searchParams.has(key)) {
          return true;
        }
      }
    } catch (_) {
      return false;
    }

    return false;
  }

  function isAccelerableDownloadHref(value) {
    return /^https?:\/\//i.test(value || "");
  }

  function openDownloadLinkFallback({ url, target, button, ctrlKey, metaKey, shiftKey }) {
    const opensNewContext = button === 1 || ctrlKey || metaKey || shiftKey || target === "_blank";
    if (opensNewContext) {
      const opened = window.open(url, target || "_blank", "noopener");
      if (opened) {
        return;
      }
    }

    window.location.href = url;
  }

  function onContextMenu(event) {
    if (drag) {
      event.preventDefault();
      event.stopPropagation();
    }
  }

  function cancelDrag() {
    if (!drag) {
      return;
    }

    cleanupDrag();
  }

  function cleanupDrag() {
    clearLinkHighlights();

    if (drag?.box) {
      drag.box.remove();
    }

    drag = null;
    window.removeEventListener("mousemove", onMouseMove, true);
    window.removeEventListener("mouseup", onMouseUp, true);
    window.removeEventListener("blur", cancelDrag, true);
  }

  function createBox() {
    const oldBox = document.getElementById(BOX_ID);
    if (oldBox) {
      oldBox.remove();
    }

    const box = document.createElement("div");
    box.id = BOX_ID;
    Object.assign(box.style, {
      position: "fixed",
      left: "0",
      top: "0",
      width: "0",
      height: "0",
      border: `2px dashed ${BORDER_COLOR}`,
      background: "transparent",
      backgroundColor: "transparent",
      boxSizing: "border-box",
      pointerEvents: "none",
      zIndex: "2147483647"
    });
    box.style.setProperty("background", "transparent", "important");
    box.style.setProperty("background-color", "transparent", "important");
    document.documentElement.appendChild(box);
    return box;
  }

  function updateBox() {
    const rect = selectionRect();
    Object.assign(drag.box.style, {
      left: `${rect.left}px`,
      top: `${rect.top}px`,
      width: `${rect.width}px`,
      height: `${rect.height}px`
    });
  }

  function updateLinkHighlights() {
    clearLinkHighlights();

    const selection = selectionRect();
    if (selection.width < 3 || selection.height < 3) {
      return;
    }

    for (const link of document.querySelectorAll("a[href]")) {
      if (!link.href || !isVisible(link)) {
        continue;
      }

      for (const rect of matchingClientRects(link, selection)) {
        const highlight = document.createElement("div");
        Object.assign(highlight.style, {
          position: "fixed",
          left: `${rect.left}px`,
          top: `${rect.top}px`,
          width: `${rect.width}px`,
          height: `${rect.height}px`,
          border: `1px dashed ${LINK_BORDER_COLOR}`,
          background: "transparent",
          backgroundColor: "transparent",
          boxSizing: "border-box",
          pointerEvents: "none",
          zIndex: "2147483646"
        });
        highlight.style.setProperty("background", "transparent", "important");
        highlight.style.setProperty("background-color", "transparent", "important");
        document.documentElement.appendChild(highlight);
        drag.highlights.push(highlight);
      }
    }
  }

  function clearLinkHighlights() {
    if (!drag?.highlights) {
      return;
    }

    for (const highlight of drag.highlights) {
      highlight.remove();
    }
    drag.highlights = [];
  }

  function selectionRect() {
    const left = Math.min(drag.startX, drag.lastX);
    const top = Math.min(drag.startY, drag.lastY);
    const right = Math.max(drag.startX, drag.lastX);
    const bottom = Math.max(drag.startY, drag.lastY);

    return {
      left,
      top,
      right,
      bottom,
      width: right - left,
      height: bottom - top
    };
  }

  function collectLinks(selection) {
    const urls = [];
    const seen = new Set();

    for (const link of document.querySelectorAll("a[href]")) {
      const href = link.href;
      if (!href || seen.has(href) || !isVisible(link)) {
        continue;
      }

      if (matchingClientRects(link, selection).length) {
        seen.add(href);
        urls.push(href);
      }
    }

    return urls;
  }

  function isVisible(element) {
    const style = window.getComputedStyle(element);
    return style.visibility !== "hidden" && style.display !== "none" && Number(style.opacity) !== 0;
  }

  function matchingClientRects(element, selection) {
    return Array.from(element.getClientRects()).filter((rect) => intersects(selection, rect));
  }

  function intersects(a, b) {
    if (b.width <= 0 || b.height <= 0) {
      return false;
    }

    return a.left <= b.right && a.right >= b.left && a.top <= b.bottom && a.bottom >= b.top;
  }

  async function copyText(text) {
    try {
      await navigator.clipboard.writeText(text);
      return true;
    } catch (_) {
      return fallbackCopyText(text);
    }
  }

  function fallbackCopyText(text) {
    const textarea = document.createElement("textarea");
    textarea.value = text;
    textarea.setAttribute("readonly", "");
    Object.assign(textarea.style, {
      position: "fixed",
      left: "-9999px",
      top: "-9999px"
    });

    document.documentElement.appendChild(textarea);
    textarea.select();

    try {
      return document.execCommand("copy");
    } finally {
      textarea.remove();
    }
  }

  function initYoutubeDislike() {
    if (!isTopLevelYoutubePage()) {
      return;
    }
    ensureYoutubeDislikeStyle();

    if (!youtubeObserver) {
      youtubeObserver = new MutationObserver(scheduleYoutubeDislikeUpdate);
      youtubeObserver.observe(document.documentElement, { childList: true, subtree: true });
      window.addEventListener("yt-navigate-finish", scheduleYoutubeDislikeUpdate);
      window.addEventListener("popstate", scheduleYoutubeDislikeUpdate);
    }

    scheduleYoutubeDislikeUpdate();
  }

  function stopYoutubeDislike() {
    youtubeRequestId += 1;
    window.clearTimeout(youtubeUpdateTimer);
    youtubeUpdateTimer = 0;

    if (youtubeObserver) {
      youtubeObserver.disconnect();
      youtubeObserver = null;
      window.removeEventListener("yt-navigate-finish", scheduleYoutubeDislikeUpdate);
      window.removeEventListener("popstate", scheduleYoutubeDislikeUpdate);
    }

    document.getElementById(YOUTUBE_DISLIKE_ID)?.remove();
  }

  function scheduleYoutubeDislikeUpdate() {
    if (youtubeUpdateTimer) {
      return;
    }
    youtubeUpdateTimer = window.setTimeout(updateYoutubeDislike, 250);
  }

  function updateYoutubeDislike() {
    youtubeUpdateTimer = 0;
    if (!featureSettings.youtubeDislikeEnabled || !isTopLevelYoutubePage()) {
      stopYoutubeDislike();
      return;
    }

    const videoId = getYoutubeVideoId();
    if (!videoId) {
      document.getElementById(YOUTUBE_DISLIKE_ID)?.remove();
      return;
    }

    const container = findYoutubeActionContainer();
    if (!container) {
      return;
    }

    const badge = ensureYoutubeDislikeBadge(container);
    if (badge.dataset.videoId === videoId && badge.dataset.loaded === "true") {
      return;
    }

    const requestId = ++youtubeRequestId;
    badge.dataset.videoId = videoId;
    badge.dataset.loaded = "false";
    badge.textContent = "싫어요 불러오는 중";
    badge.title = "Return YouTube Dislike에서 싫어요 수를 가져오는 중입니다.";

    sendRuntimeMessage({ type: "get-youtube-dislike", videoId }, (response) => {
      if (requestId !== youtubeRequestId || badge.dataset.videoId !== videoId) {
        return;
      }

      if (!response?.ok) {
        badge.dataset.loaded = "true";
        badge.textContent = "싫어요 없음";
        badge.title = response?.error || "싫어요 수를 가져오지 못했습니다.";
        return;
      }

      const dislikes = Number(response.data?.dislikes);
      badge.dataset.loaded = "true";
      badge.textContent = Number.isFinite(dislikes) ? `싫어요 ${formatCount(dislikes)}` : "싫어요 없음";
      badge.title = `Return YouTube Dislike 제공 추정값. ${YOUTUBE_DISLIKE_ATTRIBUTION_URL}`;
    });
  }

  function ensureYoutubeDislikeBadge(container) {
    let badge = document.getElementById(YOUTUBE_DISLIKE_ID);
    if (!badge) {
      badge = document.createElement("span");
      badge.id = YOUTUBE_DISLIKE_ID;
      badge.setAttribute("role", "status");
    }

    if (badge.parentElement !== container) {
      container.appendChild(badge);
    }

    return badge;
  }

  function ensureYoutubeDislikeStyle() {
    if (document.getElementById(YOUTUBE_DISLIKE_STYLE_ID)) {
      return;
    }

    const style = document.createElement("style");
    style.id = YOUTUBE_DISLIKE_STYLE_ID;
    style.textContent = `
      #${YOUTUBE_DISLIKE_ID} {
        display: inline-flex;
        align-items: center;
        min-height: 36px;
        margin-left: 8px;
        padding: 0 12px;
        border-radius: 18px;
        background: rgba(255, 255, 255, 0.1);
        color: var(--yt-spec-text-primary, #f1f1f1);
        font: 500 14px/1.2 Roboto, Arial, sans-serif;
        white-space: nowrap;
      }
    `;
    document.documentElement.appendChild(style);
  }

  function findYoutubeActionContainer() {
    return (
      document.querySelector("ytd-watch-metadata #top-level-buttons-computed") ||
      document.querySelector("#top-level-buttons-computed") ||
      document.querySelector("ytd-watch-metadata #actions") ||
      document.querySelector("#actions-inner")
    );
  }

  function getYoutubeVideoId() {
    try {
      return new URL(window.location.href).searchParams.get("v") || "";
    } catch (_) {
      return "";
    }
  }

  function isTopLevelYoutubePage() {
    return window.top === window && /(^|\.)youtube\.com$/i.test(window.location.hostname);
  }

  function formatCount(value) {
    return new Intl.NumberFormat("ko-KR").format(value);
  }

  function initSponsorBlock() {
    if (!isTopLevelYoutubePage()) {
      return;
    }

    if (!sponsorBlockObserver) {
      sponsorBlockObserver = new MutationObserver(scheduleSponsorBlockUpdate);
      sponsorBlockObserver.observe(document.documentElement, { childList: true, subtree: true });
      window.addEventListener("yt-navigate-finish", scheduleSponsorBlockUpdate);
      window.addEventListener("popstate", scheduleSponsorBlockUpdate);
    }

    scheduleSponsorBlockUpdate();
  }

  function stopSponsorBlock() {
    sponsorBlockRequestId += 1;
    window.clearTimeout(sponsorBlockUpdateTimer);
    sponsorBlockUpdateTimer = 0;
    sponsorBlockVideoId = "";
    sponsorBlockSegments = [];

    if (sponsorBlockObserver) {
      sponsorBlockObserver.disconnect();
      sponsorBlockObserver = null;
      window.removeEventListener("yt-navigate-finish", scheduleSponsorBlockUpdate);
      window.removeEventListener("popstate", scheduleSponsorBlockUpdate);
    }

    detachSponsorBlockVideo();
  }

  function scheduleSponsorBlockUpdate() {
    if (sponsorBlockUpdateTimer) {
      return;
    }
    sponsorBlockUpdateTimer = window.setTimeout(updateSponsorBlock, 250);
  }

  function updateSponsorBlock() {
    sponsorBlockUpdateTimer = 0;
    if (!featureSettings.sponsorBlockEnabled || !isTopLevelYoutubePage()) {
      stopSponsorBlock();
      return;
    }

    attachSponsorBlockVideo();

    const videoId = getYoutubeVideoId();
    if (!videoId) {
      sponsorBlockVideoId = "";
      sponsorBlockSegments = [];
      return;
    }
    if (sponsorBlockVideoId === videoId) {
      return;
    }

    sponsorBlockVideoId = videoId;
    sponsorBlockSegments = [];
    const requestId = ++sponsorBlockRequestId;

    sendRuntimeMessage({ type: "get-sponsorblock-segments", videoId }, (response, error) => {
      if (requestId !== sponsorBlockRequestId || sponsorBlockVideoId !== videoId) {
        return;
      }
      if (error) {
        sponsorBlockSegments = [];
        return;
      }

      sponsorBlockSegments = response?.ok && Array.isArray(response.data) ? response.data : [];
      onSponsorBlockTimeUpdate();
    });
  }

  function attachSponsorBlockVideo() {
    const video = document.querySelector("video.html5-main-video") || document.querySelector("video");
    if (!video || video === sponsorBlockVideo) {
      return;
    }

    detachSponsorBlockVideo();
    sponsorBlockVideo = video;
    sponsorBlockVideo.addEventListener("timeupdate", onSponsorBlockTimeUpdate, true);
    sponsorBlockVideo.addEventListener("seeking", onSponsorBlockTimeUpdate, true);
    sponsorBlockVideo.addEventListener("play", onSponsorBlockTimeUpdate, true);
  }

  function detachSponsorBlockVideo() {
    if (!sponsorBlockVideo) {
      return;
    }

    sponsorBlockVideo.removeEventListener("timeupdate", onSponsorBlockTimeUpdate, true);
    sponsorBlockVideo.removeEventListener("seeking", onSponsorBlockTimeUpdate, true);
    sponsorBlockVideo.removeEventListener("play", onSponsorBlockTimeUpdate, true);
    sponsorBlockVideo = null;
  }

  function onSponsorBlockTimeUpdate() {
    if (!featureSettings.sponsorBlockEnabled || !sponsorBlockVideo || !sponsorBlockSegments.length) {
      return;
    }

    const currentTime = sponsorBlockVideo.currentTime;
    const segment = sponsorBlockSegments.find((entry) => currentTime + SPONSOR_BLOCK_SKIP_OFFSET >= entry.start && currentTime < entry.end - SPONSOR_BLOCK_SKIP_OFFSET);
    if (!segment || Date.now() - sponsorBlockLastSkipAt < 300) {
      return;
    }

    sponsorBlockLastSkipAt = Date.now();
    const skipTarget = segment.end + SPONSOR_BLOCK_SKIP_OFFSET;
    const duration = Number.isFinite(sponsorBlockVideo.duration) ? sponsorBlockVideo.duration : skipTarget;
    sponsorBlockVideo.currentTime = Math.min(skipTarget, duration);
    if (!sponsorBlockVideo.paused) {
      sponsorBlockVideo.play().catch(() => {});
    }
    showToast(`SponsorBlock 스킵: ${sponsorBlockCategoryLabel(segment.category)}`);
  }

  function sponsorBlockCategoryLabel(category) {
    const labels = {
      sponsor: "광고",
      interaction: "상호작용",
      selfpromo: "자체 홍보",
      intro: "인트로",
      preview: "미리보기",
      outro: "아웃트로",
      hook: "훅"
    };
    return labels[category] || category || "구간";
  }

  function initYoutubeLoop() {
    if (!isTopLevelYoutubePage()) {
      return;
    }
    ensureYoutubeLoopStyle();

    if (!youtubeLoopObserver) {
      youtubeLoopObserver = new MutationObserver(scheduleYoutubeLoopUpdate);
      youtubeLoopObserver.observe(document.documentElement, { childList: true, subtree: true });
      window.addEventListener("yt-navigate-finish", scheduleYoutubeLoopUpdate);
      window.addEventListener("popstate", scheduleYoutubeLoopUpdate);
    }

    scheduleYoutubeLoopUpdate();
  }

  function stopYoutubeLoop() {
    window.clearTimeout(youtubeLoopUpdateTimer);
    youtubeLoopUpdateTimer = 0;
    youtubeLoopVideoId = "";
    youtubeLoopActive = false;

    if (youtubeLoopObserver) {
      youtubeLoopObserver.disconnect();
      youtubeLoopObserver = null;
      window.removeEventListener("yt-navigate-finish", scheduleYoutubeLoopUpdate);
      window.removeEventListener("popstate", scheduleYoutubeLoopUpdate);
    }

    detachYoutubeLoopVideo();
    document.getElementById(YOUTUBE_LOOP_ID)?.remove();
  }

  function scheduleYoutubeLoopUpdate() {
    if (youtubeLoopUpdateTimer) {
      return;
    }
    youtubeLoopUpdateTimer = window.setTimeout(updateYoutubeLoop, 250);
  }

  function updateYoutubeLoop() {
    youtubeLoopUpdateTimer = 0;
    if (!featureSettings.youtubeLoopEnabled || !isTopLevelYoutubePage()) {
      stopYoutubeLoop();
      return;
    }

    attachYoutubeLoopVideo();

    const videoId = getYoutubeVideoId();
    if (!videoId) {
      youtubeLoopVideoId = "";
      youtubeLoopActive = false;
      updateYoutubeLoopButton();
      return;
    }

    if (youtubeLoopVideoId !== videoId) {
      youtubeLoopVideoId = videoId;
      applyYoutubeLoopUrlParams();
    }

    const container = findYoutubeActionContainer();
    if (!container) {
      return;
    }

    ensureYoutubeLoopButton(container);
    updateYoutubeLoopButton();
  }

  function ensureYoutubeLoopButton(container) {
    let button = document.getElementById(YOUTUBE_LOOP_ID);
    if (!button) {
      button = document.createElement("button");
      button.id = YOUTUBE_LOOP_ID;
      button.type = "button";
      button.addEventListener("click", toggleYoutubeLoop);
    }

    if (button.parentElement !== container) {
      container.appendChild(button);
    }

    return button;
  }

  function updateYoutubeLoopButton() {
    const button = document.getElementById(YOUTUBE_LOOP_ID);
    if (!button) {
      return;
    }

    button.classList.toggle("is-active", youtubeLoopActive);
    button.textContent = youtubeLoopActive ? "반복 켜짐" : "반복";
    button.title = youtubeLoopActive ? "현재 영상 반복 끄기" : "현재 영상 반복 켜기";
  }

  function toggleYoutubeLoop() {
    if (!featureSettings.youtubeLoopEnabled) {
      return;
    }

    attachYoutubeLoopVideo();
    if (!youtubeLoopVideo) {
      return;
    }

    youtubeLoopActive = !youtubeLoopActive;
    youtubeLoopStart = 0;
    youtubeLoopEnd = null;
    youtubeLoopLimit = Infinity;
    youtubeLoopCompleted = 0;
    updateYoutubeLoopButton();
    showToast(youtubeLoopActive ? "YouTube 반복 켜짐" : "YouTube 반복 꺼짐");
  }

  function applyYoutubeLoopUrlParams() {
    const params = new URL(window.location.href).searchParams;
    const loopParam = params.get("loop");
    const start = parseYoutubeLoopTime(params.get("start"));
    const end = parseYoutubeLoopTime(params.get("end"));
    const hasLoopParam = loopParam !== null;
    const hasRangeParam = start !== null || end !== null;

    youtubeLoopActive = (hasLoopParam && loopParam !== "0" && loopParam !== "false") || hasRangeParam;
    youtubeLoopStart = start || 0;
    youtubeLoopEnd = end;
    youtubeLoopLimit = parseYoutubeLoopLimit(loopParam);
    youtubeLoopCompleted = 0;

    if (youtubeLoopActive && youtubeLoopVideo && youtubeLoopStart > 0 && youtubeLoopVideo.currentTime < youtubeLoopStart) {
      youtubeLoopVideo.currentTime = youtubeLoopStart;
    }
  }

  function parseYoutubeLoopLimit(value) {
    if (value === null || value === "" || value === "true") {
      return Infinity;
    }

    const number = Number.parseInt(value, 10);
    return Number.isFinite(number) && number > 0 ? number : Infinity;
  }

  function parseYoutubeLoopTime(value) {
    if (!value) {
      return null;
    }

    const trimmed = value.trim();
    if (/^\d+(\.\d+)?$/.test(trimmed)) {
      return Number.parseFloat(trimmed);
    }

    const parts = trimmed.split(":").map((part) => Number.parseFloat(part));
    if (!parts.length || parts.some((part) => !Number.isFinite(part))) {
      return null;
    }

    return parts.reduce((total, part) => total * 60 + part, 0);
  }

  function attachYoutubeLoopVideo() {
    const video = document.querySelector("video.html5-main-video") || document.querySelector("video");
    if (!video || video === youtubeLoopVideo) {
      return;
    }

    detachYoutubeLoopVideo();
    youtubeLoopVideo = video;
    youtubeLoopVideo.addEventListener("timeupdate", onYoutubeLoopTimeUpdate, true);
    youtubeLoopVideo.addEventListener("ended", onYoutubeLoopTimeUpdate, true);
    youtubeLoopVideo.addEventListener("loadedmetadata", onYoutubeLoopTimeUpdate, true);
  }

  function detachYoutubeLoopVideo() {
    if (!youtubeLoopVideo) {
      return;
    }

    youtubeLoopVideo.removeEventListener("timeupdate", onYoutubeLoopTimeUpdate, true);
    youtubeLoopVideo.removeEventListener("ended", onYoutubeLoopTimeUpdate, true);
    youtubeLoopVideo.removeEventListener("loadedmetadata", onYoutubeLoopTimeUpdate, true);
    youtubeLoopVideo = null;
  }

  function onYoutubeLoopTimeUpdate() {
    if (!featureSettings.youtubeLoopEnabled || !youtubeLoopActive || !youtubeLoopVideo) {
      return;
    }

    const end = Number.isFinite(youtubeLoopEnd)
      ? youtubeLoopEnd
      : Number.isFinite(youtubeLoopVideo.duration)
        ? youtubeLoopVideo.duration
        : null;
    if (!Number.isFinite(end)) {
      return;
    }

    if (youtubeLoopVideo.currentTime >= end - 0.08 || youtubeLoopVideo.ended) {
      loopYoutubeVideo();
    }
  }

  function loopYoutubeVideo() {
    if (Number.isFinite(youtubeLoopLimit) && youtubeLoopCompleted >= youtubeLoopLimit) {
      youtubeLoopActive = false;
      updateYoutubeLoopButton();
      showToast("YouTube 반복 완료");
      return;
    }

    youtubeLoopCompleted += 1;
    youtubeLoopVideo.currentTime = youtubeLoopStart;
    youtubeLoopVideo.play().catch(() => {});
  }

  function ensureYoutubeLoopStyle() {
    if (document.getElementById(YOUTUBE_LOOP_STYLE_ID)) {
      return;
    }

    const style = document.createElement("style");
    style.id = YOUTUBE_LOOP_STYLE_ID;
    style.textContent = `
      #${YOUTUBE_LOOP_ID} {
        display: inline-flex;
        align-items: center;
        min-height: 36px;
        margin-left: 8px;
        padding: 0 12px;
        border: 1px solid rgba(255, 255, 255, 0.18);
        border-radius: 18px;
        background: rgba(255, 255, 255, 0.1);
        color: var(--yt-spec-text-primary, #f1f1f1);
        font: 500 14px/1.2 Roboto, Arial, sans-serif;
        white-space: nowrap;
        cursor: pointer;
      }

      #${YOUTUBE_LOOP_ID}.is-active {
        border-color: #22c55e;
        color: #22c55e;
      }
    `;
    document.documentElement.appendChild(style);
  }

  function initPasswordUnmask() {
    unmaskPasswordInputs();

    if (!passwordObserver) {
      passwordObserver = new MutationObserver(unmaskPasswordInputs);
      passwordObserver.observe(document.documentElement, { childList: true, subtree: true });
    }
  }

  function stopPasswordUnmask() {
    if (passwordObserver) {
      passwordObserver.disconnect();
      passwordObserver = null;
    }

    for (const input of document.querySelectorAll('input[data-tablinker-password-unmasked="true"]')) {
      try {
        input.type = "password";
        delete input.dataset.tablinkerPasswordUnmasked;
      } catch (_) {
        // 입력칸이 페이지에 의해 잠겨 있으면 건너뛴다.
      }
    }
  }

  function unmaskPasswordInputs() {
    if (!featureSettings.passwordUnmaskEnabled) {
      return;
    }

    for (const input of document.querySelectorAll('input[type="password"]')) {
      try {
        input.type = "text";
        input.dataset.tablinkerPasswordUnmasked = "true";
      } catch (_) {
        // 일부 사이트의 특수 입력칸은 type 변경이 막힐 수 있다.
      }
    }
  }

  function initBase64AutoDecode() {
    ensureBase64DecodeStyle();
    document.addEventListener("click", onBase64DecodeClick, true);
    scanBase64Text(document.body || document.documentElement);

    if (!base64DecodeObserver) {
      base64DecodeObserver = new MutationObserver(scheduleBase64DecodeScan);
      base64DecodeObserver.observe(document.documentElement, {
        childList: true,
        characterData: true,
        subtree: true
      });
    }
  }

  function stopBase64AutoDecode() {
    document.removeEventListener("click", onBase64DecodeClick, true);
    window.clearTimeout(base64DecodeTimer);
    base64DecodeTimer = 0;
    base64DecodePendingRoots.clear();

    if (base64DecodeObserver) {
      base64DecodeObserver.disconnect();
      base64DecodeObserver = null;
    }

    for (const element of document.querySelectorAll(`.${BASE64_DECODE_CLASS}`)) {
      element.replaceWith(document.createTextNode(element.dataset.originalBase64 || element.textContent || ""));
    }
    document.getElementById(BASE64_DECODE_STYLE_ID)?.remove();
  }

  function scheduleBase64DecodeScan(mutations = []) {
    if (!featureSettings.base64AutoDecodeEnabled) {
      return;
    }

    queueBase64DecodeRoots(mutations);
    if (base64DecodeTimer) {
      return;
    }

    base64DecodeTimer = window.setTimeout(() => {
      base64DecodeTimer = 0;
      const roots = base64DecodePendingRoots.size
        ? Array.from(base64DecodePendingRoots)
        : [document.body || document.documentElement];
      base64DecodePendingRoots.clear();

      for (const root of roots) {
        if (root.isConnected !== false) {
          scanBase64Text(root);
        }
      }
    }, 200);
  }

  function queueBase64DecodeRoots(mutations) {
    for (const mutation of mutations || []) {
      if (mutation.type === "characterData") {
        base64DecodePendingRoots.add(mutation.target);
        continue;
      }

      for (const node of mutation.addedNodes || []) {
        if (node.nodeType === Node.TEXT_NODE || node.nodeType === Node.ELEMENT_NODE) {
          base64DecodePendingRoots.add(node);
        }
      }
    }
  }

  function scanBase64Text(root) {
    if (!root || !featureSettings.base64AutoDecodeEnabled) {
      return;
    }
    if (root.nodeType === Node.TEXT_NODE) {
      if (!shouldSkipBase64TextNode(root) && hasBase64Candidate(root.nodeValue || "")) {
        replaceBase64TextNode(root);
      }
      return;
    }

    const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
      acceptNode(node) {
        if (!hasBase64Candidate(node.nodeValue || "")) {
          return NodeFilter.FILTER_REJECT;
        }
        return shouldSkipBase64TextNode(node) ? NodeFilter.FILTER_REJECT : NodeFilter.FILTER_ACCEPT;
      }
    });
    const nodes = [];
    while (walker.nextNode()) {
      nodes.push(walker.currentNode);
    }

    for (const node of nodes) {
      replaceBase64TextNode(node);
    }
  }

  function hasBase64Candidate(value) {
    BASE64_TEXT_PATTERN.lastIndex = 0;
    const matched = BASE64_TEXT_PATTERN.test(value);
    BASE64_TEXT_PATTERN.lastIndex = 0;
    return matched;
  }

  function shouldSkipBase64TextNode(node) {
    const parent = node.parentElement;
    if (!parent || parent.closest(`.${BASE64_DECODE_CLASS}`)) {
      return true;
    }

    return Boolean(parent.closest("script, style, textarea, input, select, option, [contenteditable='true']"));
  }

  function replaceBase64TextNode(node) {
    const text = node.nodeValue || "";
    BASE64_TEXT_PATTERN.lastIndex = 0;

    const fragment = document.createDocumentFragment();
    let lastIndex = 0;
    let changed = false;
    let match;
    while ((match = BASE64_TEXT_PATTERN.exec(text))) {
      const token = match[0];
      const decoded = decodeBase64Token(token);
      if (!decoded) {
        continue;
      }

      if (match.index > lastIndex) {
        fragment.append(document.createTextNode(text.slice(lastIndex, match.index)));
      }
      fragment.append(createBase64DecodeElement(token, decoded));
      lastIndex = match.index + token.length;
      changed = true;
    }

    if (!changed) {
      return;
    }
    if (lastIndex < text.length) {
      fragment.append(document.createTextNode(text.slice(lastIndex)));
    }
    node.replaceWith(fragment);
  }

  function decodeBase64Token(token) {
    const normalized = token.replace(/-/g, "+").replace(/_/g, "/");
    if (normalized.length % 4 === 1 || !/^[A-Za-z0-9+/]+={0,2}$/.test(normalized)) {
      return "";
    }

    const padded = normalized.padEnd(Math.ceil(normalized.length / 4) * 4, "=");
    try {
      const binary = atob(padded);
      const bytes = new Uint8Array(binary.length);
      for (let index = 0; index < binary.length; index += 1) {
        bytes[index] = binary.charCodeAt(index);
      }

      const decoded = new TextDecoder("utf-8", { fatal: false }).decode(bytes).trim();
      return isUsefulDecodedText(decoded, token) ? decoded : "";
    } catch (_) {
      return "";
    }
  }

  function isUsefulDecodedText(decoded, original) {
    if (!decoded || decoded === original || decoded.length < 4 || decoded.includes("\uFFFD")) {
      return false;
    }
    if (/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/.test(decoded)) {
      return false;
    }
    if (/^(https?|ftp|magnet):\/\//i.test(decoded)) {
      return true;
    }

    const compact = decoded.replace(/\s+/g, "");
    if (compact.length < 4) {
      return false;
    }

    const printable = Array.from(decoded).filter((char) => {
      const code = char.codePointAt(0);
      return code === 9 || code === 10 || code === 13 || code >= 32;
    }).length;
    return printable / Array.from(decoded).length > 0.95;
  }

  function createBase64DecodeElement(original, decoded) {
    const decodedUrl = getDecodedLinkUrl(decoded);
    const element = document.createElement(decodedUrl ? "a" : "span");
    element.className = BASE64_DECODE_CLASS;
    element.dataset.originalBase64 = original;
    element.dataset.decodedBase64 = decoded;
    if (decodedUrl) {
      element.dataset.decodedBase64Url = decodedUrl;
      element.href = decodedUrl;
      element.target = "_blank";
      element.rel = "noopener noreferrer";
    }
    element.dataset.base64State = "decoded";
    element.textContent = decoded;
    element.title = decodedUrl ? "클릭: 열기 / Ctrl+클릭: 원문/복원문 전환" : "Ctrl+클릭: 원문/복원문 전환";
    return element;
  }

  function getDecodedLinkUrl(decoded) {
    if (!/^(https?|ftp|magnet):/i.test(decoded || "")) {
      return "";
    }

    try {
      return new URL(decoded).href;
    } catch (_) {
      return "";
    }
  }

  function onBase64DecodeClick(event) {
    if (!event.ctrlKey) {
      return;
    }

    const element = event.target?.closest?.(`.${BASE64_DECODE_CLASS}`);
    if (!element) {
      return;
    }

    event.preventDefault();
    event.stopPropagation();
    const showingDecoded = element.dataset.base64State !== "original";
    element.dataset.base64State = showingDecoded ? "original" : "decoded";
    element.textContent = showingDecoded ? element.dataset.originalBase64 : element.dataset.decodedBase64;
    if (element instanceof HTMLAnchorElement && element.dataset.decodedBase64Url) {
      if (showingDecoded) {
        element.removeAttribute("href");
      } else {
        element.href = element.dataset.decodedBase64Url;
      }
    }
  }

  function ensureBase64DecodeStyle() {
    if (document.getElementById(BASE64_DECODE_STYLE_ID)) {
      return;
    }

    const style = document.createElement("style");
    style.id = BASE64_DECODE_STYLE_ID;
    style.textContent = `
      .${BASE64_DECODE_CLASS} {
        color: inherit !important;
        text-decoration: inherit;
        cursor: pointer;
        overflow-wrap: anywhere;
      }

      a.${BASE64_DECODE_CLASS}[href] {
        color: #0000ee !important;
        text-decoration: underline;
      }
    `;
    document.documentElement.appendChild(style);
  }

  function initGmailUnreadObserver() {
    if (!isTopLevelGmailPage()) {
      return;
    }

    if (!gmailTitleObserver) {
      gmailTitleObserver = new MutationObserver(reportGmailUnreadFromTitle);
      const titleElement = document.querySelector("title") || document.documentElement;
      gmailTitleObserver.observe(titleElement, { childList: true, subtree: true, characterData: true });
      window.addEventListener("focus", reportGmailUnreadFromTitle);
      addRuntimeMessageListener(onGmailMessage);
    }

    reportGmailUnreadFromTitle();
  }

  function stopGmailUnreadObserver() {
    if (gmailTitleObserver) {
      gmailTitleObserver.disconnect();
      gmailTitleObserver = null;
      window.removeEventListener("focus", reportGmailUnreadFromTitle);
      removeRuntimeMessageListener(onGmailMessage);
    }
  }

  function onGmailMessage(message, _sender, sendResponse) {
    if (message?.type !== "read-gmail-unread-from-page" || !isTopLevelGmailPage()) {
      return false;
    }

    sendResponse({ ok: true, unread: getGmailUnreadFromTitle() });
    return false;
  }

  function reportGmailUnreadFromTitle() {
    if (!featureSettings.gmailUnreadEnabled || !isTopLevelGmailPage()) {
      return;
    }

    const unread = getGmailUnreadFromTitle();
    if (unread === lastReportedGmailUnread) {
      return;
    }

    lastReportedGmailUnread = unread;
    sendRuntimeMessage({ type: "gmail-unread-from-page", unread });
  }

  function getGmailUnreadFromTitle() {
    const match = document.title.match(/\((\d+)\)/);
    return match ? Number.parseInt(match[1], 10) : 0;
  }

  function isTopLevelGmailPage() {
    return window.top === window && /^mail\.google\.com$/i.test(window.location.hostname);
  }

  async function saveSingleFilePage() {
    if (!featureSettings.singleFileSaveEnabled) {
      throw new Error("페이지 단일파일 저장이 비활성화되어 있습니다.");
    }

    const clone = document.documentElement.cloneNode(true);
    preserveFormState(clone);
    cleanupPageClone(clone);
    ensureSingleFileHead(clone);
    await inlinePageStyles(clone);
    await inlinePageScripts(clone);
    await inlinePageImages(clone);
    await inlinePageMedia(clone);
    await inlinePageFrames(clone);
    await inlinePageEmbeds(clone);
    absolutizePageUrls(clone);

    const html = `${getDocumentType()}\n<!-- Saved from ${location.href} at ${new Date().toISOString()} by ONE -->\n${clone.outerHTML}`;
    const result = downloadSingleFileHtml(html);
    showToast("페이지 단일파일 저장");
    return result;
  }

  function preserveFormState(clone) {
    const originals = Array.from(document.querySelectorAll("input, textarea, select"));
    const copies = Array.from(clone.querySelectorAll("input, textarea, select"));
    originals.forEach((original, index) => {
      const copy = copies[index];
      if (!copy) {
        return;
      }

      if (copy instanceof HTMLInputElement) {
        if (copy.type === "checkbox" || copy.type === "radio") {
          copy.toggleAttribute("checked", original.checked);
        } else {
          copy.setAttribute("value", original.value);
        }
        return;
      }

      if (copy instanceof HTMLTextAreaElement) {
        copy.textContent = original.value;
        return;
      }

      if (copy instanceof HTMLSelectElement) {
        Array.from(copy.options).forEach((option, optionIndex) => {
          option.toggleAttribute("selected", Boolean(original.options[optionIndex]?.selected));
        });
      }
    });
  }

  function cleanupPageClone(clone) {
    for (const id of [BOX_ID, TOAST_ID, YOUTUBE_DISLIKE_ID, YOUTUBE_DISLIKE_STYLE_ID, YOUTUBE_LOOP_ID, YOUTUBE_LOOP_STYLE_ID]) {
      clone.querySelector(`#${id}`)?.remove();
    }
  }

  function ensureSingleFileHead(clone) {
    let head = clone.querySelector("head");
    if (!head) {
      head = document.createElement("head");
      clone.insertBefore(head, clone.firstChild);
    }

    if (!head.querySelector("meta[charset]")) {
      const meta = document.createElement("meta");
      meta.setAttribute("charset", "utf-8");
      head.insertBefore(meta, head.firstChild);
    }

    if (!head.querySelector("base[data-one-single-file]")) {
      const base = document.createElement("base");
      base.dataset.oneSingleFile = "true";
      base.href = document.baseURI;
      head.insertBefore(base, head.firstChild?.nextSibling || head.firstChild);
    }
  }

  async function inlinePageStyles(clone) {
    const originalLinks = Array.from(document.querySelectorAll('link[rel~="stylesheet"][href]'));
    const cloneLinks = Array.from(clone.querySelectorAll('link[rel~="stylesheet"][href]'));
    for (let index = 0; index < cloneLinks.length; index += 1) {
      const link = cloneLinks[index];
      const href = originalLinks[index]?.href || resolveResourceUrl(link.getAttribute("href"), document.baseURI);
      if (!href) {
        continue;
      }

      const cssText = await fetchResourceAsText(href);
      if (!cssText) {
        link.href = href;
        continue;
      }

      const style = document.createElement("style");
      style.dataset.oneSingleFileSource = href;
      style.textContent = await inlineCssText(cssText, href);
      link.replaceWith(style);
    }

    for (const style of clone.querySelectorAll("style")) {
      style.textContent = await inlineCssText(style.textContent || "", document.baseURI);
    }
  }

  async function inlineCssText(cssText, baseUrl, depth = 0) {
    const withImports = await inlineCssImports(cssText, baseUrl, depth);
    return inlineCssUrls(withImports, baseUrl);
  }

  async function inlineCssImports(cssText, baseUrl, depth) {
    if (depth > 3 || !cssText.includes("@import")) {
      return cssText;
    }

    const replacements = [];
    const importPattern = /@import\s+(?:url\(\s*)?["']?([^"')\s;]+)["']?\s*\)?[^;]*;/gi;
    let match;
    while ((match = importPattern.exec(cssText))) {
      const importUrl = resolveResourceUrl(match[1], baseUrl);
      if (!importUrl) {
        continue;
      }

      const importedText = await fetchResourceAsText(importUrl);
      if (importedText) {
        replacements.push([match[0], await inlineCssText(importedText, importUrl, depth + 1)]);
      }
    }

    return replacements.reduce((result, [from, to]) => result.replace(from, to), cssText);
  }

  async function inlineCssUrls(cssText, baseUrl) {
    const replacements = new Map();
    const urlPattern = /url\(\s*(['"]?)([^"')]+)\1\s*\)/gi;
    let match;
    while ((match = urlPattern.exec(cssText))) {
      const rawUrl = match[2].trim();
      if (replacements.has(rawUrl)) {
        continue;
      }

      const resourceUrl = resolveResourceUrl(rawUrl, baseUrl);
      if (!resourceUrl) {
        continue;
      }

      const dataUrl = await fetchResourceAsDataUrl(resourceUrl);
      if (dataUrl) {
        replacements.set(rawUrl, `url("${dataUrl}")`);
      }
    }

    return cssText.replace(urlPattern, (fullMatch, _quote, rawUrl) => replacements.get(rawUrl.trim()) || fullMatch);
  }

  async function inlinePageScripts(clone) {
    const originalScripts = Array.from(document.querySelectorAll("script[src]"));
    const cloneScripts = Array.from(clone.querySelectorAll("script[src]"));
    for (let index = 0; index < cloneScripts.length; index += 1) {
      const script = cloneScripts[index];
      const src = originalScripts[index]?.src || resolveResourceUrl(script.getAttribute("src"), document.baseURI);
      if (!src) {
        continue;
      }

      const scriptText = await fetchResourceAsText(src);
      if (!scriptText) {
        script.src = src;
        continue;
      }

      script.dataset.oneSingleFileSource = src;
      script.removeAttribute("src");
      script.textContent = scriptText;
    }
  }

  async function inlinePageImages(clone) {
    await inlineAttributeUrls(clone, "img[src], input[type='image'][src], image[href], image[xlink\\:href]", "src");
    await inlineAttributeUrls(clone, "video[poster]", "poster");
    await inlineAttributeUrls(clone, "link[rel~='icon'][href], link[rel='apple-touch-icon'][href]", "href");
    await inlineSrcsetUrls(clone, "img[srcset], source[srcset]", "srcset");

    for (const image of clone.querySelectorAll("image[href], image[xlink\\:href]")) {
      const value = image.getAttribute("href") || image.getAttribute("xlink:href");
      const dataUrl = await fetchResourceAsDataUrl(resolveResourceUrl(value, document.baseURI));
      if (dataUrl) {
        image.setAttribute("href", dataUrl);
        image.setAttribute("xlink:href", dataUrl);
      }
    }
  }

  async function inlinePageMedia(clone) {
    await inlineAttributeUrls(clone, "video[src], audio[src], source[src], track[src]", "src");
    await inlineSrcsetUrls(clone, "source[srcset]", "srcset");
  }

  async function inlinePageFrames(clone) {
    for (const frame of clone.querySelectorAll("iframe[src], frame[src]")) {
      const frameUrl = resolveResourceUrl(frame.getAttribute("src"), document.baseURI);
      const frameHtml = await fetchResourceAsText(frameUrl);
      if (frameHtml) {
        frame.setAttribute("srcdoc", frameHtml);
        frame.removeAttribute("src");
      }
    }
  }

  async function inlinePageEmbeds(clone) {
    await inlineAttributeUrls(clone, "embed[src]", "src");
    await inlineAttributeUrls(clone, "object[data]", "data");
  }

  async function inlineAttributeUrls(clone, selector, attributeName) {
    for (const element of clone.querySelectorAll(selector)) {
      const resourceUrl = resolveResourceUrl(element.getAttribute(attributeName), document.baseURI);
      const dataUrl = await fetchResourceAsDataUrl(resourceUrl);
      if (dataUrl) {
        element.setAttribute(attributeName, dataUrl);
      }
    }
  }

  async function inlineSrcsetUrls(clone, selector, attributeName) {
    for (const element of clone.querySelectorAll(selector)) {
      const entries = parseSrcset(element.getAttribute(attributeName) || "");
      if (!entries.length) {
        continue;
      }

      const inlinedEntries = [];
      for (const entry of entries) {
        const dataUrl = await fetchResourceAsDataUrl(resolveResourceUrl(entry.url, document.baseURI));
        inlinedEntries.push(`${dataUrl || entry.url}${entry.descriptor ? ` ${entry.descriptor}` : ""}`);
      }
      element.setAttribute(attributeName, inlinedEntries.join(", "));
    }
  }

  function parseSrcset(value) {
    return value
      .split(",")
      .map((entry) => entry.trim())
      .filter(Boolean)
      .map((entry) => {
        const parts = entry.split(/\s+/);
        return {
          url: parts.shift() || "",
          descriptor: parts.join(" ")
        };
      })
      .filter((entry) => entry.url);
  }

  function absolutizePageUrls(clone) {
    const attributes = [
      ["a[href]", "href"],
      ["area[href]", "href"],
      ["form[action]", "action"],
      ["blockquote[cite]", "cite"],
      ["q[cite]", "cite"],
      ["del[cite]", "cite"],
      ["ins[cite]", "cite"],
      ["link[href]", "href"],
      ["source[src]", "src"],
      ["audio[src]", "src"],
      ["video[src]", "src"],
      ["track[src]", "src"],
      ["iframe[src]", "src"],
      ["embed[src]", "src"],
      ["object[data]", "data"],
      ["button[formaction]", "formaction"],
      ["input[formaction]", "formaction"]
    ];

    for (const [selector, attributeName] of attributes) {
      for (const element of clone.querySelectorAll(selector)) {
        const absoluteUrl = resolveAbsoluteUrl(element.getAttribute(attributeName), document.baseURI);
        if (absoluteUrl) {
          element.setAttribute(attributeName, absoluteUrl);
        }
      }
    }
  }

  function resolveResourceUrl(value, baseUrl) {
    const absoluteUrl = resolveAbsoluteUrl(value, baseUrl);
    if (!absoluteUrl || /^data:/i.test(absoluteUrl)) {
      return "";
    }
    return /^(https?|file|blob):/i.test(absoluteUrl) ? absoluteUrl : "";
  }

  function resolveAbsoluteUrl(value, baseUrl) {
    const trimmed = String(value || "").trim();
    if (!trimmed || trimmed.startsWith("#") || /^(data|javascript|mailto|tel):/i.test(trimmed)) {
      return "";
    }

    try {
      return new URL(trimmed, baseUrl).href;
    } catch (_) {
      return "";
    }
  }

  async function fetchResourceAsText(url) {
    if (!url) {
      return "";
    }
    if (/^blob:/i.test(url)) {
      try {
        const response = await fetch(url);
        return response.ok ? await response.text() : "";
      } catch (_) {
        return "";
      }
    }

    const response = await sendRuntimeRequest({
      type: "fetch-single-file-resource",
      responseType: "text",
      url
    });
    return response?.ok ? response.text || "" : "";
  }

  async function fetchResourceAsDataUrl(url) {
    if (!url) {
      return "";
    }
    if (/^blob:/i.test(url)) {
      try {
        const response = await fetch(url);
        return response.ok ? blobToDataUrl(await response.blob()) : "";
      } catch (_) {
        return "";
      }
    }

    const response = await sendRuntimeRequest({
      type: "fetch-single-file-resource",
      responseType: "dataUrl",
      url
    });
    return response?.ok && hasDataUrlPayload(response.dataUrl) ? response.dataUrl : "";
  }

  function blobToDataUrl(blob) {
    return new Promise((resolve) => {
      const reader = new FileReader();
      reader.onerror = () => resolve("");
      reader.onloadend = () => {
        const dataUrl = String(reader.result || "");
        resolve(hasDataUrlPayload(dataUrl) ? dataUrl : "");
      };
      reader.readAsDataURL(blob);
    });
  }

  function hasDataUrlPayload(value) {
    return /^data:[^,]*;base64,.+/i.test(String(value || ""));
  }

  function sendRuntimeRequest(message) {
    return new Promise((resolve) => {
      sendRuntimeMessage(message, (response, error) => {
        if (error) {
          resolve({ ok: false, error: error.message || EXTENSION_CONTEXT_ERROR });
          return;
        }
        resolve(response || { ok: false });
      });
    });
  }

  function getDocumentType() {
    const doctype = document.doctype;
    if (!doctype) {
      return "<!doctype html>";
    }

    const publicId = doctype.publicId ? ` PUBLIC "${doctype.publicId}"` : "";
    const systemId = doctype.systemId ? ` "${doctype.systemId}"` : "";
    return `<!doctype ${doctype.name}${publicId}${systemId}>`;
  }

  function downloadSingleFileHtml(html) {
    const filename = `${sanitizeFileName(document.title || location.hostname || "page")}.html`;
    const blob = new Blob([html], { type: "text/html;charset=utf-8" });
    const url = URL.createObjectURL(blob);
    const anchor = document.createElement("a");
    anchor.href = url;
    anchor.download = filename;
    anchor.rel = "noopener";
    anchor.style.display = "none";
    document.documentElement.appendChild(anchor);
    anchor.click();
    anchor.remove();
    window.setTimeout(() => URL.revokeObjectURL(url), 30000);
    return { filename, bytes: blob.size };
  }

  function sanitizeFileName(value) {
    return String(value || "page")
      .replace(/[\\/:*?"<>|]/g, "_")
      .replace(/[\r\n]+/g, " ")
      .replace(/\s+/g, " ")
      .trim()
      .slice(0, 120) || "page";
  }

  function showToast(message) {
    const oldToast = document.getElementById(TOAST_ID);
    if (oldToast) {
      oldToast.remove();
    }

    const toast = document.createElement("div");
    toast.id = TOAST_ID;
    toast.textContent = message;
    Object.assign(toast.style, {
      position: "fixed",
      right: "16px",
      bottom: "16px",
      padding: "8px 10px",
      borderRadius: "6px",
      background: "rgba(17, 24, 39, 0.94)",
      color: "#fff",
      font: "13px/1.4 system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif",
      pointerEvents: "none",
      zIndex: "2147483647"
    });

    document.documentElement.appendChild(toast);
    window.setTimeout(() => toast.remove(), 1600);
  }
})();

(() => {
  const buttonClass = "one-video-download-button";
  const menuClass = "one-video-download-menu";
  const styleId = "one-video-download-style";
  const seen = new WeakSet();
  const defaults = { downloadAcceleratorEnabled: true };
  const extensionContextError = "확장 컨텍스트가 만료되었습니다. 탭을 새로고침하세요.";
  let activeMenu = null;

  init();

  function init() {
    if (window.top !== window) {
      return;
    }

    readSyncStorage(defaults, (settings) => {
      if (settings.downloadAcceleratorEnabled === false) {
        return;
      }

      ensureStyle();
      scan();
      new MutationObserver(scan).observe(document.documentElement, { childList: true, subtree: true });
      document.addEventListener("loadedmetadata", scan, true);
      document.addEventListener("emptied", scan, true);
    });
  }

  function readSyncStorage(defaultValues, callback) {
    const syncStorage = globalThis.chrome?.storage?.sync;
    if (typeof syncStorage?.get !== "function") {
      callback(defaultValues);
      return;
    }

    try {
      syncStorage.get(defaultValues, (settings) => callback(settings || defaultValues));
    } catch {
      callback(defaultValues);
    }
  }

  function scan() {
    for (const video of document.querySelectorAll("video")) {
      attach(video);
    }
  }

  function attach(video) {
    if (seen.has(video) || !isLikelyDownloadableVideo(video)) {
      return;
    }

    const host = findHost(video);
    if (!host) {
      return;
    }

    if (getComputedStyle(host).position === "static") {
      host.style.position = "relative";
    }

    const button = document.createElement("button");
    button.type = "button";
    button.className = buttonClass;
    button.textContent = "원본 다운로드";
    button.title = "ONE 다운로드 가속기로 현재 동영상 원본 URL을 받습니다.";
    button.addEventListener("click", (event) => {
      event.preventDefault();
      event.stopPropagation();
      startVideoDownload(video, button);
    }, true);

    host.appendChild(button);
    seen.add(video);
  }

  function findHost(video) {
    const candidates = [video.parentElement, video.closest(".html5-video-player"), video.closest("div")];
    return candidates.find((element) => element && element.isConnected) || null;
  }

  function isLikelyDownloadableVideo(video) {
    return Boolean(getVideoUrl(video)
      || String(video.currentSrc || video.src || "").startsWith("blob:")
      || video.readyState > 0
      || video.clientWidth >= 160
      || video.clientHeight >= 90);
  }

  function getVideoUrl(video) {
    const candidates = [
      video.currentSrc,
      video.src,
      ...Array.from(video.querySelectorAll("source[src]")).map((source) => source.src)
    ];
    return candidates.find((url) => /^https?:\/\//i.test(url || "")) || "";
  }

  async function startVideoDownload(video, button) {
    const candidates = await getVideoDownloadCandidates(video);
    if (!candidates.length) {
      toast("원본 URL을 찾지 못했습니다.");
      return;
    }

    if (candidates.length > 1) {
      showCandidateMenu(button, candidates);
      return;
    }

    accelerateDownloadLink(candidates[0]);
  }

  function accelerateDownloadLink(candidate) {
    sendRuntimeMessage({
      type: "accelerate-download-link",
      url: candidate.url,
      videoUrl: candidate.videoUrl || "",
      audioUrl: candidate.audioUrl || "",
      manifestUrl: candidate.manifestUrl || "",
      referrer: location.href,
      tabUrl: location.href,
      filename: candidate.filename || pickFilename(candidate.url),
      mime: candidate.mime || "",
      mediaType: candidate.mediaType || "",
      source: candidate.source || "video-direct",
      qualityLabel: candidate.qualityLabel || ""
    }, (response, error) => {
      if (error || !response?.ok) {
        toast(response?.error || error?.message || "다운로드 시작 실패");
        return;
      }

      toast("다운로드 시작 창 열림");
    });
  }

  async function getVideoDownloadCandidates(video) {
    const candidates = [];
    const seenUrls = new Set();
    const directUrl = getVideoUrl(video);
    if (directUrl) {
      candidates.push({
        url: directUrl,
        filename: pickFilename(directUrl),
        mediaType: "video",
        source: "video-direct",
        qualityLabel: "직접 URL"
      });
      seenUrls.add(normalizeUrl(directUrl));
    }

    const response = await sendRuntimeRequest({
      type: "get-video-download-candidates",
      pageUrl: location.href,
      tabUrl: location.href
    });
    for (const candidate of Array.isArray(response?.candidates) ? response.candidates : []) {
      const key = normalizeUrl(candidate.url);
      if (key && !seenUrls.has(key)) {
        seenUrls.add(key);
        candidates.push(candidate);
      }
    }
    return candidates;
  }

  function showCandidateMenu(button, candidates) {
    closeCandidateMenu();
    const menu = document.createElement("div");
    menu.className = menuClass;
    for (const candidate of candidates.slice(0, 12)) {
      const item = document.createElement("button");
      item.type = "button";
      item.textContent = candidateLabel(candidate);
      item.title = candidate.url || "";
      item.addEventListener("click", (event) => {
        event.preventDefault();
        event.stopPropagation();
        closeCandidateMenu();
        accelerateDownloadLink(candidate);
      }, true);
      menu.appendChild(item);
    }

    const rect = button.getBoundingClientRect();
    Object.assign(menu.style, {
      right: `${Math.max(12, window.innerWidth - rect.right)}px`,
      top: `${Math.max(12, rect.bottom + 6)}px`
    });
    document.documentElement.appendChild(menu);
    activeMenu = menu;
    window.setTimeout(() => {
      document.addEventListener("click", closeCandidateMenu, { once: true, capture: true });
      document.addEventListener("keydown", closeCandidateMenuOnEscape, { once: true, capture: true });
    }, 0);
  }

  function closeCandidateMenuOnEscape(event) {
    if (event.key === "Escape") {
      closeCandidateMenu();
    }
  }

  function closeCandidateMenu() {
    activeMenu?.remove();
    activeMenu = null;
  }

  function candidateLabel(candidate) {
    const type = candidate.mediaType === "hls"
      ? "HLS"
      : candidate.mediaType === "dash" || candidate.mediaType === "dash-pair" || candidate.mediaType === "dash-video"
        ? "DASH"
        : "영상";
    const quality = candidate.qualityLabel || "";
    return [type, quality, pickFilename(candidate.url)].filter(Boolean).join(" · ");
  }

  function normalizeUrl(value) {
    try {
      const url = new URL(value || "");
      url.hash = "";
      return url.href;
    } catch {
      return String(value || "");
    }
  }

  function pickFilename(url) {
    try {
      const parsed = new URL(url);
      const name = decodeURIComponent(parsed.pathname.split("/").filter(Boolean).pop() || "");
      return name || "video";
    } catch {
      return "video";
    }
  }

  function sendRuntimeRequest(message) {
    return new Promise((resolve) => {
      sendRuntimeMessage(message, (response, error) => {
        if (error) {
          resolve({ ok: false, error: error.message || extensionContextError });
          return;
        }
        resolve(response || { ok: false });
      });
    });
  }

  function getRuntimeLastError() {
    try {
      return globalThis.chrome?.runtime?.lastError || null;
    } catch {
      return { message: extensionContextError };
    }
  }

  function sendRuntimeMessage(message, callback) {
    const runtime = globalThis.chrome?.runtime;
    if (typeof runtime?.sendMessage !== "function") {
      callback?.({ ok: false, error: extensionContextError }, { message: extensionContextError });
      return false;
    }

    try {
      runtime.sendMessage(message, (response) => callback?.(response, getRuntimeLastError()));
      return true;
    } catch (error) {
      const messageText = error?.message || extensionContextError;
      callback?.({ ok: false, error: messageText }, { message: messageText });
      return false;
    }
  }

  function ensureStyle() {
    if (document.getElementById(styleId)) {
      return;
    }

    const style = document.createElement("style");
    style.id = styleId;
    style.textContent = `
      .${buttonClass} {
        position: absolute !important;
        right: 12px !important;
        top: 12px !important;
        z-index: 2147483647 !important;
        display: inline-flex !important;
        align-items: center !important;
        justify-content: center !important;
        min-height: 34px !important;
        padding: 0 12px !important;
        border: 1px solid rgba(255,255,255,.32) !important;
        border-radius: 4px !important;
        background: rgba(0,0,0,.82) !important;
        color: #fff !important;
        font: 600 13px/1.2 system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif !important;
        box-shadow: 0 8px 24px rgba(0,0,0,.28) !important;
        cursor: pointer !important;
        opacity: .84 !important;
      }

      .${buttonClass}:hover {
        opacity: 1 !important;
        background: #0b5cff !important;
      }

      .${menuClass} {
        position: fixed !important;
        z-index: 2147483647 !important;
        display: grid !important;
        gap: 2px !important;
        width: min(360px, calc(100vw - 24px)) !important;
        max-height: min(420px, calc(100vh - 24px)) !important;
        overflow: auto !important;
        padding: 6px !important;
        border: 1px solid rgba(255,255,255,.22) !important;
        border-radius: 4px !important;
        background: rgba(10, 14, 20, .96) !important;
        box-shadow: 0 14px 36px rgba(0,0,0,.36) !important;
      }

      .${menuClass} button {
        display: block !important;
        width: 100% !important;
        min-height: 32px !important;
        padding: 7px 8px !important;
        border: 0 !important;
        border-radius: 3px !important;
        background: transparent !important;
        color: #fff !important;
        font: 500 13px/1.3 system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif !important;
        text-align: left !important;
        overflow: hidden !important;
        text-overflow: ellipsis !important;
        white-space: nowrap !important;
        cursor: pointer !important;
      }

      .${menuClass} button:hover {
        background: #0b5cff !important;
      }
    `;
    document.documentElement.appendChild(style);
  }

  function toast(message) {
    const id = "one-video-download-toast";
    document.getElementById(id)?.remove();
    const element = document.createElement("div");
    element.id = id;
    element.textContent = message;
    Object.assign(element.style, {
      position: "fixed",
      right: "16px",
      bottom: "16px",
      zIndex: "2147483647",
      padding: "8px 10px",
      borderRadius: "6px",
      background: "rgba(17, 24, 39, .94)",
      color: "#fff",
      font: "13px/1.4 system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif"
    });
    document.documentElement.appendChild(element);
    window.setTimeout(() => element.remove(), 1800);
  }
})();
