(() => {
  const CLOSED_TABS_KEY = "closedTabs";
  const MAX_CLOSED_TABS_KEY = "maxClosedTabs";
  const OPEN_TABS_KEY = "openTabs";
  const WINDOW_SESSION_KEY = "windowSession";
  const CLOSED_TABS_ENABLED_KEY = "closedTabsEnabled";
  const GMAIL_UNREAD_ENABLED_KEY = "gmailUnreadEnabled";
  const GMAIL_UNREAD_STATE_KEY = "gmailUnreadState";
  const WINDOW_SESSION_ENABLED_KEY = "windowSessionEnabled";
  const YOUTUBE_DISLIKE_ENABLED_KEY = "youtubeDislikeEnabled";
  const SPONSOR_BLOCK_ENABLED_KEY = "sponsorBlockEnabled";
  const SINGLE_FILE_SAVE_ENABLED_KEY = "singleFileSaveEnabled";
  const DOWNLOAD_ACCELERATOR_ENABLED_KEY = "downloadAcceleratorEnabled";
  const DOWNLOAD_ACCELERATOR_CANCEL_CHROME_ENABLED_KEY = "downloadAcceleratorCancelChromeEnabled";
  const DOWNLOAD_ACCELERATOR_BRIDGE_URL_KEY = "downloadAcceleratorBridgeUrl";
  const DEFAULT_MAX_CLOSED_TABS = 1000;
  const MIN_MAX_CLOSED_TABS = 10;
  const MAX_MAX_CLOSED_TABS = 10000;
  const GMAIL_ALARM_NAME = "refresh-gmail-unread";
  const AUTO_UPDATE_RELOAD_ALARM_NAME = "reload-after-auto-update";
  const GMAIL_FEED_URL = "https://mail.google.com/mail/feed/atom";
  const GMAIL_INBOX_URL = "https://mail.google.com/mail/u/0/#inbox";
  const GMAIL_REFRESH_MINUTES = 30;
  const AUTO_UPDATE_RELOAD_MINUTES = 5;
  const GMAIL_STATE_TTL = 30 * 60 * 1000;
  const YOUTUBE_DISLIKE_CACHE_TTL = 10 * 60 * 1000;
  const SPONSOR_BLOCK_CACHE_TTL = 10 * 60 * 1000;
  const SPONSOR_BLOCK_SERVER = "https://sponsor.ajay.app";
  const SPONSOR_BLOCK_CATEGORIES = ["sponsor", "interaction", "selfpromo", "intro", "preview", "outro", "hook"];
  const DEFAULT_DOWNLOAD_ACCELERATOR_BRIDGE_URL = "http://127.0.0.1:17661/download";
  const DOWNLOAD_ACCELERATOR_TIMEOUT_MS = 2500;
  const DOWNLOAD_REQUEST_TTL_MS = 2 * 60 * 1000;
  const MEDIA_REQUEST_TTL_MS = 10 * 60 * 1000;
  const MAX_MEDIA_CANDIDATES_PER_TAB = 20;
  const DOWNLOAD_MAX_BODY_BYTES = 2 * 1024 * 1024;
  const DOWNLOAD_PENDING_REQUESTS_KEY = "downloadPendingRequests";
  const DOWNLOAD_QUEUE_KEY = "downloadQueue";
  const DOWNLOAD_PENDING_TTL_MS = 10 * 60 * 1000;
  const DOWNLOAD_START_WINDOW_WIDTH = 700;
  const DOWNLOAD_START_WINDOW_HEIGHT = 520;
  const EXTENSION_ORIGIN = chrome.runtime.getURL("");
  const youtubeDislikeCache = new Map();
  const sponsorBlockCache = new Map();
  const downloadAcceleratorInFlight = new Set();
  const downloadRequestInfo = new Map();
  const mediaDownloadCandidates = new Map();
  let openTabsOperation = Promise.resolve();
  let closedTabsOperation = Promise.resolve();
  let restoringClosedTab = false;
  let togglingWindowSession = false;

  chrome.runtime.onInstalled.addListener(() => {
    refreshOpenTabs();
    setupGmailUnreadBadge();
    setupAutoUpdateReloadCheck();
  });
  chrome.runtime.onStartup.addListener(() => {
    refreshOpenTabs();
    setupGmailUnreadBadge();
    setupAutoUpdateReloadCheck();
  });
  chrome.alarms.onAlarm.addListener((alarm) => {
    if (alarm.name === GMAIL_ALARM_NAME) {
      updateGmailUnreadBadge();
    }
    if (alarm.name === AUTO_UPDATE_RELOAD_ALARM_NAME) {
      checkAutoUpdateReload();
    }
  });
  chrome.storage.onChanged.addListener((changes, areaName) => {
    if (areaName === "sync" && changes[GMAIL_UNREAD_ENABLED_KEY]) {
      setupGmailUnreadBadge();
    }
  });
  chrome.tabs.onCreated.addListener(upsertOpenTab);
  chrome.tabs.onUpdated.addListener((_tabId, changeInfo, tab) => {
    if (changeInfo.url || changeInfo.title || changeInfo.favIconUrl || changeInfo.status) {
      upsertOpenTab(tab);
    }
  });
  chrome.tabs.onReplaced.addListener(async (addedTabId, removedTabId) => {
    const openTabs = await getOpenTabs();
    delete openTabs[removedTabId];
    await setOpenTabs(openTabs);

    try {
      await upsertOpenTab(await chrome.tabs.get(addedTabId));
    } catch (_) {
      // 탭이 이미 사라졌으면 기록하지 않는다.
    }
  });
  chrome.tabs.onRemoved.addListener(saveClosedTab);
  chrome.downloads.onCreated.addListener(onDownloadCreated);
  chrome.webRequest.onBeforeRequest.addListener(
    recordDownloadRequestBody,
    { urls: ["http://*/*", "https://*/*"] },
    ["requestBody"]
  );
  chrome.webRequest.onBeforeSendHeaders.addListener(
    recordDownloadRequestHeaders,
    { urls: ["http://*/*", "https://*/*"] },
    ["requestHeaders", "extraHeaders"]
  );
  chrome.webRequest.onHeadersReceived.addListener(
    recordDownloadResponseHeaders,
    { urls: ["http://*/*", "https://*/*"] },
    ["responseHeaders", "extraHeaders"]
  );
  chrome.commands.onCommand.addListener((command) => {
    if (command === "restore-last-closed-tab") {
      restoreLastClosedTab();
      return;
    }

    if (command === "toggle-window-session") {
      toggleWindowSession();
      return;
    }

    if (command === "open-gmail-inbox") {
      openGmailInbox();
    }
  });
  chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
    if (message?.type === "restore-last-closed-tab") {
      restoreLastClosedTab();
      return false;
    }

    if (message?.type === "toggle-window-session") {
      toggleWindowSession();
      return false;
    }

    if (message?.type === "get-youtube-dislike") {
      getYoutubeDislike(message.videoId)
        .then((data) => sendResponse({ ok: true, data }))
        .catch((error) => sendResponse({ ok: false, error: error.message }));
      return true;
    }

    if (message?.type === "get-sponsorblock-segments") {
      getSponsorBlockSegments(message.videoId)
        .then((data) => sendResponse({ ok: true, data }))
        .catch((error) => sendResponse({ ok: false, error: error.message }));
      return true;
    }

    if (message?.type === "get-gmail-unread") {
      getGmailUnreadState()
        .then((data) => sendResponse({ ok: true, data }))
        .catch((error) => sendResponse({ ok: false, error: error.message }));
      return true;
    }

    if (message?.type === "refresh-gmail-unread") {
      updateGmailUnreadBadge()
        .then((data) => sendResponse({ ok: true, data }))
        .catch((error) => sendResponse({ ok: false, error: error.message }));
      return true;
    }

    if (message?.type === "gmail-unread-from-page") {
      saveGmailUnreadState(Number.parseInt(message.unread, 10), "page")
        .then((data) => sendResponse({ ok: true, data }))
        .catch((error) => sendResponse({ ok: false, error: error.message }));
      return true;
    }

    if (message?.type === "open-gmail") {
      openGmailInbox();
      return false;
    }

    if (message?.type === "accelerate-download-url") {
      accelerateManualDownload(message)
        .then((data) => sendResponse({ ok: true, ...data }))
        .catch((error) => sendResponse({ ok: false, error: error.message }));
      return true;
    }

    if (message?.type === "accelerate-download-link") {
      accelerateDownloadLink(message)
        .then((data) => sendResponse({ ok: true, ...data }))
        .catch((error) => sendResponse({ ok: false, error: error.message }));
      return true;
    }

    if (message?.type === "get-video-download-candidates") {
      getVideoDownloadCandidates(sender?.tab?.id, message)
        .then((candidates) => sendResponse({ ok: true, candidates }))
        .catch((error) => sendResponse({ ok: false, error: error.message }));
      return true;
    }

    if (message?.type === "get-pending-download") {
      getPendingDownload(message.requestId)
        .then((data) => sendResponse({ ok: true, ...data }))
        .catch((error) => sendResponse({ ok: false, error: error.message }));
      return true;
    }

    if (message?.type === "start-pending-download") {
      startPendingDownload(message.requestId, message)
        .then((data) => sendResponse({ ok: true, ...data }))
        .catch((error) => sendResponse({ ok: false, error: error.message }));
      return true;
    }

    if (message?.type === "queue-pending-download") {
      queuePendingDownload(message.requestId, message)
        .then((data) => sendResponse({ ok: true, ...data }))
        .catch((error) => sendResponse({ ok: false, error: error.message }));
      return true;
    }

    if (message?.type === "cancel-pending-download") {
      cancelPendingDownload(message.requestId)
        .then(() => sendResponse({ ok: true }))
        .catch((error) => sendResponse({ ok: false, error: error.message }));
      return true;
    }

    if (message?.type === "get-download-jobs") {
      getDownloadJobs()
        .then((jobs) => sendResponse({ ok: true, jobs }))
        .catch((error) => sendResponse({ ok: false, error: error.message }));
      return true;
    }

    if (message?.type === "get-download-manager-jobs") {
      getDownloadManagerJobs()
        .then((data) => sendResponse({ ok: true, ...data }))
        .catch((error) => sendResponse({ ok: false, error: error.message }));
      return true;
    }

    if (message?.type === "control-download-job") {
      controlDownloadJob(message.jobId, message.action)
        .then((data) => sendResponse({ ok: true, ...data }))
        .catch((error) => sendResponse({ ok: false, error: error.message }));
      return true;
    }

    if (message?.type === "open-download-manager") {
      openDownloadManager()
        .then(() => sendResponse({ ok: true }))
        .catch((error) => sendResponse({ ok: false, error: error.message }));
      return true;
    }

    if (message?.type === "save-current-page-single-file") {
      saveCurrentPageSingleFile()
        .then((data) => sendResponse({ ok: true, ...data }))
        .catch((error) => sendResponse({ ok: false, error: error.message }));
      return true;
    }

    if (message?.type === "fetch-single-file-resource") {
      fetchSingleFileResource(message.url, message.responseType)
        .then((data) => sendResponse({ ok: true, ...data }))
        .catch((error) => sendResponse({ ok: false, error: error.message }));
      return true;
    }

    return false;
  });

  ensureOpenTabs();
  setupGmailUnreadBadge();
  setupAutoUpdateReloadCheck();

  async function ensureOpenTabs() {
    const result = await chrome.storage.session.get(OPEN_TABS_KEY);
    if (!result[OPEN_TABS_KEY]) {
      await refreshOpenTabs();
    }
  }

  async function setupAutoUpdateReloadCheck() {
    await chrome.alarms.create(AUTO_UPDATE_RELOAD_ALARM_NAME, { periodInMinutes: AUTO_UPDATE_RELOAD_MINUTES });
    checkAutoUpdateReload();
  }

  async function checkAutoUpdateReload() {
    try {
      const loadedVersion = chrome.runtime.getManifest().version;
      const response = await fetch(`${chrome.runtime.getURL("UserData/reload-required.json")}?t=${Date.now()}`, { cache: "no-store" });
      if (!response.ok) {
        return;
      }

      const reloadRequest = await response.json();
      const requestedVersion = String(reloadRequest?.version || "");
      if (isVersionNewer(requestedVersion, loadedVersion)) {
        chrome.runtime.reload();
      }
    } catch (_) {
      // 자동업데이트 미등록 상태이거나 업데이트 파일 복사 중일 수 있다.
    }
  }

  function isVersionNewer(left, right) {
    const leftParts = toVersionParts(left);
    const rightParts = toVersionParts(right);
    const max = Math.max(leftParts.length, rightParts.length);
    for (let i = 0; i < max; i += 1) {
      const leftPart = leftParts[i] || 0;
      const rightPart = rightParts[i] || 0;
      if (leftPart > rightPart) {
        return true;
      }
      if (leftPart < rightPart) {
        return false;
      }
    }
    return false;
  }

  function toVersionParts(version) {
    return String(version)
      .replaceAll("-", ".")
      .split(".")
      .map((part) => Number.parseInt(part.replace(/\D/g, ""), 10) || 0);
  }

  async function refreshOpenTabs() {
    const tabs = await chrome.tabs.query({});
    const openTabs = {};

    for (const tab of tabs) {
      const snapshot = toSnapshot(tab);
      if (snapshot) {
        openTabs[tab.id] = snapshot;
      }
    }

    await setOpenTabs(openTabs);
  }

  async function upsertOpenTab(tab) {
    if (!tab?.id) {
      return;
    }

    await mutateOpenTabs((openTabs) => {
      const snapshot = toSnapshot(tab);
      if (snapshot) {
        openTabs[tab.id] = snapshot;
      } else {
        delete openTabs[tab.id];
      }
      return openTabs;
    });
  }

  async function saveClosedTab(tabId) {
    let snapshot = null;
    await mutateOpenTabs((openTabs) => {
      snapshot = openTabs[tabId] || null;
      delete openTabs[tabId];
      return openTabs;
    });

    if (!snapshot || !isRecordableUrl(snapshot.url)) {
      return;
    }
    if (!(await isFeatureEnabled(CLOSED_TABS_ENABLED_KEY))) {
      return;
    }

    const closedTab = {
      ...snapshot,
      id: `${Date.now()}-${tabId}-${Math.random().toString(36).slice(2)}`,
      closedAt: Date.now()
    };
    await mutateClosedTabs(async (closedTabs) => {
      closedTabs.unshift(closedTab);
      const maxClosedTabs = await getMaxClosedTabs();
      return closedTabs.slice(0, maxClosedTabs);
    });
  }

  async function restoreLastClosedTab() {
    if (restoringClosedTab) {
      return;
    }
    if (!(await isFeatureEnabled(CLOSED_TABS_ENABLED_KEY))) {
      return;
    }

    restoringClosedTab = true;
    try {
      await mutateClosedTabs(async (closedTabs) => {
        const tab = closedTabs.find((entry) => isRecordableUrl(entry.url));
        if (!tab) {
          return closedTabs;
        }

        await chrome.tabs.create({ url: tab.url });
        return closedTabs.filter((entry) => entry.id !== tab.id);
      });
    } finally {
      restoringClosedTab = false;
    }
  }

  async function toggleWindowSession() {
    if (togglingWindowSession) {
      return;
    }
    if (!(await isFeatureEnabled(WINDOW_SESSION_ENABLED_KEY))) {
      return;
    }

    togglingWindowSession = true;
    try {
      const result = await chrome.storage.local.get({ [WINDOW_SESSION_KEY]: null });
      const session = result[WINDOW_SESSION_KEY];
      if (isRestorableWindowSession(session)) {
        await restoreWindowSession(session);
        await chrome.storage.local.remove(WINDOW_SESSION_KEY);
        return;
      }

      await saveCurrentWindowSession();
    } finally {
      togglingWindowSession = false;
    }
  }

  async function saveCurrentWindowSession() {
    const windows = await chrome.windows.getAll({
      populate: true,
      windowTypes: ["normal"]
    });
    const sessionWindows = [];

    for (const windowInfo of windows) {
      const tabs = (windowInfo.tabs || [])
        .filter((tab) => isRecordableUrl(tab.pendingUrl || tab.url || ""))
        .sort((a, b) => a.index - b.index)
        .map((tab) => ({
          url: tab.pendingUrl || tab.url,
          title: tab.title || tab.pendingUrl || tab.url,
          pinned: Boolean(tab.pinned),
          active: Boolean(tab.active),
          index: tab.index
        }));

      if (tabs.length) {
        sessionWindows.push({
          focused: Boolean(windowInfo.focused),
          state: windowInfo.state || "normal",
          tabs
        });
      }
    }

    await chrome.storage.local.set({
      [WINDOW_SESSION_KEY]: {
        savedAt: Date.now(),
        windows: sessionWindows
      }
    });
  }

  async function restoreWindowSession(session) {
    let focusedWindowId = null;

    for (const windowInfo of session.windows) {
      const tabs = windowInfo.tabs.filter((tab) => isRecordableUrl(tab.url));
      if (!tabs.length) {
        continue;
      }

      const firstTab = tabs[0];
      const restoredWindow = await chrome.windows.create({
        url: firstTab.url,
        focused: false,
        state: normalizeWindowState(windowInfo.state)
      });
      const firstCreatedTab = restoredWindow.tabs?.[0];
      if (firstCreatedTab?.id) {
        await chrome.tabs.update(firstCreatedTab.id, { pinned: firstTab.pinned });
      }

      for (const tab of tabs.slice(1)) {
        await chrome.tabs.create({
          windowId: restoredWindow.id,
          url: tab.url,
          active: false,
          pinned: tab.pinned
        });
      }

      const activeIndex = Math.max(0, tabs.findIndex((tab) => tab.active));
      const restoredTabs = await chrome.tabs.query({ windowId: restoredWindow.id });
      const activeTab = restoredTabs.sort((a, b) => a.index - b.index)[activeIndex];
      if (activeTab?.id) {
        await chrome.tabs.update(activeTab.id, { active: true });
      }

      if (windowInfo.focused) {
        focusedWindowId = restoredWindow.id;
      }
    }

    if (focusedWindowId) {
      await chrome.windows.update(focusedWindowId, { focused: true });
    }
  }

  function isRestorableWindowSession(session) {
    return Boolean(session?.windows?.some((windowInfo) => windowInfo.tabs?.some((tab) => isRecordableUrl(tab.url))));
  }

  function normalizeWindowState(state) {
    return ["normal", "minimized", "maximized", "fullscreen"].includes(state) ? state : "normal";
  }

  async function getOpenTabs() {
    const result = await chrome.storage.session.get({ [OPEN_TABS_KEY]: {} });
    return result[OPEN_TABS_KEY] || {};
  }

  function setOpenTabs(openTabs) {
    return chrome.storage.session.set({ [OPEN_TABS_KEY]: openTabs });
  }

  function mutateOpenTabs(mutator) {
    const operation = openTabsOperation
      .catch(() => {})
      .then(async () => {
        const openTabs = await getOpenTabs();
        const nextOpenTabs = (await mutator(openTabs)) || openTabs;
        await setOpenTabs(nextOpenTabs);
        return nextOpenTabs;
      });
    openTabsOperation = operation.catch(() => {});
    return operation;
  }

  function mutateClosedTabs(mutator) {
    const operation = closedTabsOperation
      .catch(() => {})
      .then(async () => {
        const result = await chrome.storage.local.get({ [CLOSED_TABS_KEY]: [] });
        const closedTabs = Array.isArray(result[CLOSED_TABS_KEY]) ? result[CLOSED_TABS_KEY] : [];
        const nextClosedTabs = (await mutator(closedTabs)) || closedTabs;
        await chrome.storage.local.set({ [CLOSED_TABS_KEY]: nextClosedTabs });
        return nextClosedTabs;
      });
    closedTabsOperation = operation.catch(() => {});
    return operation;
  }

  async function getMaxClosedTabs() {
    const result = await chrome.storage.sync.get({ [MAX_CLOSED_TABS_KEY]: DEFAULT_MAX_CLOSED_TABS });
    const number = Number.parseInt(result[MAX_CLOSED_TABS_KEY], 10);
    if (!Number.isFinite(number)) {
      return DEFAULT_MAX_CLOSED_TABS;
    }

    return Math.min(MAX_MAX_CLOSED_TABS, Math.max(MIN_MAX_CLOSED_TABS, number));
  }

  function recordDownloadRequestBody(details) {
    if (!isAccelerableDownloadUrl(details.url)) {
      return;
    }

    const info = getDownloadRequestInfo(details);
    info.method = details.method || "GET";
    info.requestBody = extractRequestBody(details.requestBody);
    pruneDownloadRequestInfo();
  }

  function recordDownloadRequestHeaders(details) {
    if (!isAccelerableDownloadUrl(details.url)) {
      return;
    }

    const info = getDownloadRequestInfo(details);
    info.method = details.method || info.method || "GET";
    info.requestHeaders = sanitizeRequestHeaders(details.requestHeaders || []);
    pruneDownloadRequestInfo();
  }

  function recordDownloadResponseHeaders(details) {
    if (!isAccelerableDownloadUrl(details.url)) {
      return;
    }

    const info = getDownloadRequestInfo(details);
    info.statusCode = details.statusCode || 0;
    info.responseHeaders = normalizeHeaderList(details.responseHeaders || []);
    info.responseAt = Date.now();
    recordMediaDownloadCandidate(details, info);
    pruneDownloadRequestInfo();
    pruneMediaDownloadCandidates();
  }

  function getDownloadRequestInfo(details) {
    const now = Date.now();
    const oldInfo = downloadRequestInfo.get(details.requestId) || {};
    const info = {
      ...oldInfo,
      requestId: details.requestId,
      url: details.url,
      method: details.method || oldInfo.method || "GET",
      tabId: details.tabId,
      frameId: details.frameId,
      type: details.type || oldInfo.type || "",
      initiator: details.initiator || oldInfo.initiator || "",
      documentUrl: details.documentUrl || oldInfo.documentUrl || "",
      parentFrameId: details.parentFrameId,
      updatedAt: now
    };
    downloadRequestInfo.set(details.requestId, info);
    return info;
  }

  function pruneDownloadRequestInfo() {
    const threshold = Date.now() - DOWNLOAD_REQUEST_TTL_MS;
    for (const [requestId, info] of downloadRequestInfo) {
      if ((info.updatedAt || 0) < threshold) {
        downloadRequestInfo.delete(requestId);
      }
    }
  }

  async function onDownloadCreated(item) {
    if (!item?.id || downloadAcceleratorInFlight.has(item.id)) {
      return;
    }
    if (!isAccelerableDownloadUrl(item.finalUrl || item.url)) {
      return;
    }

    downloadAcceleratorInFlight.add(item.id);
    try {
      const [latestItem] = await chrome.downloads.search({ id: item.id });
      const downloadItem = latestItem || item;
      const settings = await getDownloadAcceleratorSettings();
      if (!settings.enabled || !isAccelerableDownloadUrl(downloadItem.finalUrl || downloadItem.url)) {
        return;
      }

      let accepted = false;
      try {
        accepted = await sendDownloadToAccelerator(downloadItem, settings.cancelChromeEnabled);
      } catch (_) {
        accepted = false;
      }
      if (accepted && settings.cancelChromeEnabled) {
        await cancelChromeDownload(downloadItem.id);
      }
    } finally {
      downloadAcceleratorInFlight.delete(item.id);
    }
  }

  async function accelerateManualDownload(payload) {
    const url = String(payload?.url || payload || "");
    if (!isAccelerableDownloadUrl(url)) {
      throw new Error("http 또는 https URL만 가능합니다.");
    }

    return openDownloadStartDialog({
      id: null,
      url,
      videoUrl: String(payload?.videoUrl || ""),
      audioUrl: String(payload?.audioUrl || ""),
      manifestUrl: String(payload?.manifestUrl || ""),
      finalUrl: url,
      referrer: "",
      filename: "",
      mime: "",
      totalBytes: 0,
      fileSize: 0,
      tabUrl: "",
      source: "manual-url"
    }, false);
  }

  async function accelerateDownloadLink(message) {
    const url = String(message?.url || "");
    if (!isAccelerableDownloadUrl(url)) {
      throw new Error("http 또는 https URL만 가능합니다.");
    }

    const requestInfo = findMatchingUrlRequest(url, Number(message?.tabId || 0), String(message?.tabUrl || message?.referrer || ""));
    const referrer = String(message.referrer || requestInfo?.documentUrl || requestInfo?.initiator || message?.tabUrl || "");
    const cookieHeader = await getCookieHeader(url);
    const requestHeaders = mergeDownloadHeaders(requestInfo?.requestHeaders || [], {
      cookie: cookieHeader,
      referrer,
      userAgent: globalThis.navigator?.userAgent || "Mozilla/5.0"
    });

    return openDownloadStartDialog({
      id: null,
      url,
      finalUrl: url,
      referrer,
      filename: String(message.filename || ""),
      mime: String(message.mime || getHeaderValue(requestInfo?.responseHeaders, "Content-Type") || ""),
      totalBytes: 0,
      fileSize: 0,
      tabUrl: String(message.tabUrl || ""),
      source: String(message.source || "link-click"),
      mediaType: String(message.mediaType || ""),
      qualityLabel: String(message.qualityLabel || ""),
      method: requestInfo?.method || "GET",
      headers: requestHeaders,
      responseHeaders: requestInfo?.responseHeaders || [],
      statusCode: requestInfo?.statusCode || 0
    }, false);
  }

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

    const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
    if (!tab?.id) {
      throw new Error("저장할 탭을 찾지 못했습니다.");
    }
    if (!isSaveablePageUrl(tab.url || "")) {
      throw new Error("이 페이지는 저장할 수 없습니다.");
    }

    try {
      const response = await chrome.tabs.sendMessage(tab.id, { type: "save-single-file-page" });
      if (!response?.ok) {
        throw new Error(response?.error || "페이지 저장 실패");
      }
      return {
        filename: response.filename || "",
        bytes: Number(response.bytes || 0)
      };
    } catch (error) {
      throw new Error(error.message || "페이지에서 저장 스크립트를 실행하지 못했습니다.");
    }
  }

  async function fetchSingleFileResource(url, responseType) {
    if (!isSingleFileResourceUrl(url)) {
      throw new Error("지원하지 않는 리소스 주소입니다.");
    }

    const response = await fetch(url, {
      cache: "no-store",
      credentials: "include",
      redirect: "follow"
    });
    if (!response.ok) {
      throw new Error(`리소스 응답 오류: ${response.status}`);
    }

    const contentType = response.headers.get("content-type") || "";
    if (responseType === "text") {
      return { text: await response.text(), contentType };
    }

    const buffer = await response.arrayBuffer();
    return {
      dataUrl: `data:${contentType || "application/octet-stream"};base64,${arrayBufferToBase64(buffer)}`,
      contentType
    };
  }

  async function sendDownloadToAccelerator(downloadItem, cancelChromeDownloadWhenAccepted) {
    const payload = await buildDownloadPayload(downloadItem, cancelChromeDownloadWhenAccepted);
    return openDownloadStartDialog(payload);
  }

  async function buildDownloadPayload(downloadItem, cancelChromeDownloadWhenAccepted) {
    const settings = await getDownloadAcceleratorSettings();
    if (!settings.enabled) {
      throw new Error("다운로드 가속기가 비활성화되어 있습니다.");
    }

    const url = downloadItem.finalUrl || downloadItem.url || "";
    if (!isAccelerableDownloadUrl(url)) {
      throw new Error("http 또는 https URL만 가능합니다.");
    }
    const requestInfo = findMatchingDownloadRequest(downloadItem);
    const cookieHeader = await getCookieHeader(url);
    const requestHeaders = mergeDownloadHeaders(requestInfo?.requestHeaders || [], {
      cookie: cookieHeader,
      referrer: downloadItem.referrer || requestInfo?.documentUrl || requestInfo?.initiator || downloadItem.tabUrl || "",
      userAgent: globalThis.navigator?.userAgent || "Mozilla/5.0"
    });

    return {
      url,
      originalUrl: downloadItem.url || url,
      referrer: downloadItem.referrer || requestInfo?.documentUrl || requestInfo?.initiator || downloadItem.tabUrl || "",
      filename: getDownloadFileName(downloadItem),
      mime: downloadItem.mime || "",
      totalBytes: Number(downloadItem.totalBytes || downloadItem.fileSize || 0),
      fileSize: Number(downloadItem.fileSize || 0),
      downloadId: downloadItem.id || null,
      tabUrl: downloadItem.tabUrl || "",
      source: downloadItem.source || "chrome-download",
      cancelChromeDownload: Boolean(cancelChromeDownloadWhenAccepted),
      method: requestInfo?.method || "GET",
      headers: requestHeaders,
      postDataBase64: requestInfo?.requestBody?.base64 || "",
      postDataTooLarge: Boolean(requestInfo?.requestBody?.tooLarge),
      responseHeaders: requestInfo?.responseHeaders || [],
      statusCode: requestInfo?.statusCode || 0
    };
  }

  async function postDownloadToBridge(bridgeUrl, payload) {
    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), DOWNLOAD_ACCELERATOR_TIMEOUT_MS);

    try {
      const response = await fetch(bridgeUrl, {
        method: "POST",
        cache: "no-store",
        headers: {
          "Content-Type": "application/json"
        },
        body: JSON.stringify(payload),
        signal: controller.signal
      });
      if (!response.ok) {
        return false;
      }

      try {
        const data = await response.json();
        return data?.ok !== false;
      } catch (_) {
        return true;
      }
    } catch (_) {
      return false;
    } finally {
      clearTimeout(timeoutId);
    }
  }

  async function openDownloadStartDialog(payload) {
    const defaults = await getDownloadDefaults();
    const requestId = makeDownloadRequestId("pending");
    const request = normalizeDownloadPayload({
      ...payload,
      requestId,
      createdAt: Date.now(),
      saveDir: payload.saveDir || defaults.defaultSaveDir || "",
      filename: payload.filename || filenameFromUrl(payload.url)
    });

    await prunePendingDownloads();
    const pending = await getPendingDownloads();
    pending[requestId] = request;
    await chrome.storage.session.set({ [DOWNLOAD_PENDING_REQUESTS_KEY]: pending });
    await chrome.windows.create({
      url: chrome.runtime.getURL(`download-start.html?id=${encodeURIComponent(requestId)}`),
      type: "popup",
      width: DOWNLOAD_START_WINDOW_WIDTH,
      height: DOWNLOAD_START_WINDOW_HEIGHT,
      focused: true
    });
    return { prompted: true, requestId };
  }

  async function getPendingDownload(requestId) {
    await prunePendingDownloads();
    const pending = await getPendingDownloads();
    const request = pending[String(requestId || "")];
    if (!request) {
      throw new Error("다운로드 요청을 찾지 못했습니다.");
    }
    return {
      request,
      defaults: await getDownloadDefaults()
    };
  }

  async function startPendingDownload(requestId, selection) {
    const pending = await getPendingDownloads();
    const request = pending[String(requestId || "")];
    if (!request) {
      throw new Error("다운로드 요청을 찾지 못했습니다.");
    }

    const payload = applyDownloadSelection(request, selection);
    await submitDownloadPayload(payload);
    delete pending[String(requestId || "")];
    await chrome.storage.session.set({ [DOWNLOAD_PENDING_REQUESTS_KEY]: pending });
    return { started: true };
  }

  async function queuePendingDownload(requestId, selection) {
    const pending = await getPendingDownloads();
    const request = pending[String(requestId || "")];
    if (!request) {
      throw new Error("다운로드 요청을 찾지 못했습니다.");
    }

    const item = {
      ...applyDownloadSelection(request, selection),
      id: makeDownloadRequestId("queue"),
      queuedAt: Date.now()
    };
    const queue = await getDownloadQueue();
    queue.unshift(item);
    await chrome.storage.local.set({ [DOWNLOAD_QUEUE_KEY]: queue });
    delete pending[String(requestId || "")];
    await chrome.storage.session.set({ [DOWNLOAD_PENDING_REQUESTS_KEY]: pending });
    return { queued: true, queueId: item.id };
  }

  async function cancelPendingDownload(requestId) {
    const pending = await getPendingDownloads();
    delete pending[String(requestId || "")];
    await chrome.storage.session.set({ [DOWNLOAD_PENDING_REQUESTS_KEY]: pending });
  }

  async function submitDownloadPayload(payload) {
    const settings = await getDownloadAcceleratorSettings();
    if (!settings.enabled) {
      throw new Error("다운로드 가속기가 비활성화되어 있습니다.");
    }

    const accepted = await postDownloadToBridge(settings.bridgeUrl, normalizeDownloadPayload(payload));
    if (!accepted) {
      throw new Error("브릿지 접수 실패");
    }
    return true;
  }

  async function getDownloadDefaults() {
    const settings = await getDownloadAcceleratorSettings();
    try {
      const data = await fetchDownloadBridgeJson(getBridgeEndpoint(settings.bridgeUrl, "/config"));
      return {
        defaultSaveDir: String(data.defaultDownloadDir || "")
      };
    } catch (_) {
      return {
        defaultSaveDir: ""
      };
    }
  }

  async function getPendingDownloads() {
    const result = await chrome.storage.session.get({ [DOWNLOAD_PENDING_REQUESTS_KEY]: {} });
    return isPlainObject(result[DOWNLOAD_PENDING_REQUESTS_KEY]) ? result[DOWNLOAD_PENDING_REQUESTS_KEY] : {};
  }

  async function prunePendingDownloads() {
    const pending = await getPendingDownloads();
    const threshold = Date.now() - DOWNLOAD_PENDING_TTL_MS;
    let changed = false;
    for (const [id, request] of Object.entries(pending)) {
      if (!request?.createdAt || request.createdAt < threshold) {
        delete pending[id];
        changed = true;
      }
    }
    if (changed) {
      await chrome.storage.session.set({ [DOWNLOAD_PENDING_REQUESTS_KEY]: pending });
    }
  }

  async function getDownloadQueue() {
    const result = await chrome.storage.local.get({ [DOWNLOAD_QUEUE_KEY]: [] });
    return Array.isArray(result[DOWNLOAD_QUEUE_KEY])
      ? result[DOWNLOAD_QUEUE_KEY].filter((item) => item?.id && isAccelerableDownloadUrl(item.url))
      : [];
  }

  async function startQueuedDownload(queueId) {
    const queue = await getDownloadQueue();
    const item = queue.find((entry) => entry.id === queueId);
    if (!item) {
      throw new Error("큐 항목을 찾지 못했습니다.");
    }

    await submitDownloadPayload(item);
    await chrome.storage.local.set({ [DOWNLOAD_QUEUE_KEY]: queue.filter((entry) => entry.id !== queueId) });
    return { started: true };
  }

  async function removeQueuedDownload(queueId) {
    const queue = await getDownloadQueue();
    const nextQueue = queue.filter((entry) => entry.id !== queueId);
    await chrome.storage.local.set({ [DOWNLOAD_QUEUE_KEY]: nextQueue });
    return { removed: nextQueue.length !== queue.length };
  }

  function applyDownloadSelection(payload, selection) {
    const filename = sanitizeDownloadFilename(selection?.filename || payload.filename || filenameFromUrl(payload.url));
    const saveDir = String(selection?.saveDir || payload.saveDir || "").trim();
    const nextPayload = normalizeDownloadPayload({
      ...payload,
      filename,
      saveDir
    });
    delete nextPayload.requestId;
    return nextPayload;
  }

  function normalizeDownloadPayload(payload) {
    return {
      ...payload,
      url: String(payload.url || ""),
      originalUrl: String(payload.originalUrl || payload.url || ""),
      filename: sanitizeDownloadFilename(payload.filename || filenameFromUrl(payload.url)),
      saveDir: String(payload.saveDir || "").trim(),
      source: String(payload.source || "")
    };
  }

  function makeDownloadRequestId(prefix) {
    return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2)}`;
  }

  function sanitizeDownloadFilename(value) {
    return String(value || "")
      .split(/[\\/]/)
      .filter(Boolean)
      .pop()
      ?.replace(/[\r\n]+/g, " ")
      .trim() || "";
  }

  function filenameFromUrl(value) {
    try {
      const pathname = new URL(value || "").pathname;
      return decodeURIComponent(pathname.split("/").filter(Boolean).pop() || "") || "download";
    } catch (_) {
      return "download";
    }
  }

  function isPlainObject(value) {
    return Boolean(value && typeof value === "object" && !Array.isArray(value));
  }

  async function getDownloadJobs() {
    const queueJobs = (await getDownloadQueue()).map((item) => ({
      ...item,
      queuedLocal: true,
      status: "queued",
      file: item.saveDir && item.filename ? `${item.saveDir}\\${item.filename}` : ""
    }));
    const settings = await getDownloadAcceleratorSettings();
    let data = {};
    try {
      data = await fetchDownloadBridgeJson(getBridgeEndpoint(settings.bridgeUrl, "/status"));
    } catch (error) {
      if (queueJobs.length) {
        return queueJobs;
      }
      throw error;
    }
    const bridgeJobs = Array.isArray(data.jobs) ? data.jobs.filter((job) => job.status !== "done") : [];
    return [...queueJobs, ...bridgeJobs];
  }

  async function getDownloadManagerJobs() {
    const queueJobs = (await getDownloadQueue()).map((item) => ({
      ...item,
      queuedLocal: true,
      status: "queued",
      file: item.saveDir && item.filename ? `${item.saveDir}\\${item.filename}` : ""
    }));
    const settings = await getDownloadAcceleratorSettings();
    const statusUrl = `${getBridgeEndpoint(settings.bridgeUrl, "/status")}?includeDone=1`;
    try {
      const data = await fetchDownloadBridgeJson(statusUrl);
      const bridgeJobs = Array.isArray(data.jobs) ? data.jobs : [];
      return { jobs: [...queueJobs, ...bridgeJobs], bridgeOk: true };
    } catch (error) {
      return {
        jobs: queueJobs,
        bridgeOk: false,
        bridgeError: error?.message || "브릿지 연결 실패"
      };
    }
  }

  async function controlDownloadJob(jobId, action) {
    const normalizedAction = String(action || "");
    if (!["pause", "resume", "retry", "cancel", "clear", "start", "remove", "open", "folder"].includes(normalizedAction)) {
      throw new Error("지원하지 않는 작업입니다.");
    }
    if (normalizedAction === "start") {
      return startQueuedDownload(String(jobId || ""));
    }
    if (normalizedAction === "remove") {
      return removeQueuedDownload(String(jobId || ""));
    }
    const settings = await getDownloadAcceleratorSettings();
    return fetchDownloadBridgeJson(getBridgeEndpoint(settings.bridgeUrl, "/control"), {
      method: "POST",
      cache: "no-store",
      headers: {
        "Content-Type": "application/json"
      },
      body: JSON.stringify({
        jobId: String(jobId || ""),
        action: normalizedAction
      })
    });
  }

  async function openDownloadManager() {
    await chrome.tabs.create({ url: chrome.runtime.getURL("downloads.html") });
  }

  async function fetchDownloadBridgeJson(url, options = {}) {
    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), 2000);
    try {
      const response = await fetch(url, {
        cache: "no-store",
        ...options,
        signal: controller.signal
      });
      const data = await response.json().catch(() => ({}));
      if (!response.ok || data?.ok === false) {
        throw new Error(data?.error || `브릿지 응답 오류: ${response.status}`);
      }
      return data || {};
    } finally {
      clearTimeout(timeoutId);
    }
  }

  function getBridgeEndpoint(bridgeUrl, pathname) {
    const url = new URL(bridgeUrl);
    url.pathname = pathname;
    url.search = "";
    url.hash = "";
    return url.href;
  }

  async function cancelChromeDownload(downloadId) {
    if (!downloadId) {
      return;
    }

    try {
      await chrome.downloads.cancel(downloadId);
    } catch (_) {
      // 이미 완료되었거나 취소된 다운로드는 무시한다.
    }

    try {
      await chrome.downloads.erase({ id: downloadId });
    } catch (_) {
      // 기록 삭제 실패는 다운로드 가속 처리와 무관하다.
    }
  }

  function findMatchingDownloadRequest(downloadItem) {
    pruneDownloadRequestInfo();

    const urls = new Set([downloadItem.url, downloadItem.finalUrl].filter(Boolean).map(normalizeComparableUrl));
    const tabId = downloadItem.tabId;
    const now = Date.now();
    let best = null;
    let bestScore = -1;

    for (const info of downloadRequestInfo.values()) {
      const requestUrl = normalizeComparableUrl(info.url || "");
      if (!urls.has(requestUrl)) {
        continue;
      }

      let score = 0;
      if (info.tabId === tabId) {
        score += 5;
      }
      if (info.method && info.method !== "GET") {
        score += 3;
      }
      score += Math.max(0, 3 - Math.floor((now - (info.updatedAt || 0)) / 10000));

      if (score > bestScore) {
        best = info;
        bestScore = score;
      }
    }

    return best;
  }

  function findMatchingUrlRequest(url, tabId, pageUrl) {
    pruneDownloadRequestInfo();

    const targetUrl = normalizeComparableUrl(url);
    const pageHost = getUrlHost(pageUrl);
    const now = Date.now();
    let best = null;
    let bestScore = -1;

    for (const info of downloadRequestInfo.values()) {
      if (normalizeComparableUrl(info.url || "") !== targetUrl) {
        continue;
      }

      let score = 0;
      if (tabId && info.tabId === tabId) {
        score += 6;
      }
      if (pageHost && getUrlHost(info.documentUrl || info.initiator || "") === pageHost) {
        score += 3;
      }
      if (info.responseHeaders?.length) {
        score += 2;
      }
      score += Math.max(0, 5 - Math.floor((now - (info.updatedAt || 0)) / 10000));

      if (score > bestScore) {
        best = info;
        bestScore = score;
      }
    }

    return best;
  }

  async function getVideoDownloadCandidates(senderTabId, message) {
    pruneMediaDownloadCandidates();

    const tabId = Number(senderTabId || message?.tabId || 0);
    const pageUrl = String(message?.pageUrl || message?.tabUrl || "");
    const candidates = tabId ? [...(mediaDownloadCandidates.get(tabId) || [])] : [];
    const pageHost = getUrlHost(pageUrl);
    const filtered = candidates
      .filter((candidate) => !pageHost || !candidate.pageHost || candidate.pageHost === pageHost)
      .sort(compareMediaCandidates)
      .slice(0, 8);
    const expanded = await expandMediaCandidates(filtered);

    return expanded
      .sort(compareMediaCandidates)
      .slice(0, 12);
  }

  function recordMediaDownloadCandidate(details, info) {
    const mediaType = classifyMediaCandidate(details.url, info.responseHeaders || [], details.type || "");
    if (!mediaType || details.tabId < 0) {
      return;
    }

    const now = Date.now();
    const tabCandidates = mediaDownloadCandidates.get(details.tabId) || [];
    const nextCandidate = {
      id: `${details.tabId}-${normalizeComparableUrl(details.url)}`,
      url: details.url,
      mediaType,
      mime: getHeaderValue(info.responseHeaders, "Content-Type"),
      filename: filenameFromMediaUrl(details.url, mediaType),
      tabId: details.tabId,
      frameId: details.frameId,
      documentUrl: details.documentUrl || "",
      initiator: details.initiator || "",
      pageHost: getUrlHost(details.documentUrl || details.initiator || ""),
      source: mediaType === "hls" ? "video-hls" : mediaType === "dash" ? "video-dash" : "video-direct",
      requestHeaders: info.requestHeaders || [],
      updatedAt: now
    };

    const withoutDuplicate = tabCandidates.filter((candidate) => normalizeComparableUrl(candidate.url) !== normalizeComparableUrl(nextCandidate.url));
    withoutDuplicate.unshift(nextCandidate);
    mediaDownloadCandidates.set(details.tabId, withoutDuplicate.sort(compareMediaCandidates).slice(0, MAX_MEDIA_CANDIDATES_PER_TAB));
  }

  async function expandMediaCandidates(candidates) {
    const expanded = [];
    const seen = new Set();
    for (const candidate of candidates) {
      const variants = candidate.mediaType === "hls"
        ? await loadHlsVariants(candidate)
        : candidate.mediaType === "dash"
          ? await loadDashVariants(candidate)
          : [];
      const items = variants.length ? variants : [candidate];
      for (const item of items) {
        const key = normalizeComparableUrl([item.url, item.videoUrl, item.audioUrl].filter(Boolean).join("|"));
        if (key && !seen.has(key)) {
          seen.add(key);
          expanded.push(item);
        }
      }
    }
    return expanded;
  }

  async function loadDashVariants(candidate) {
    try {
      const text = await fetchMediaManifest(candidate);
      return parseDashVariants(text, candidate);
    } catch (_) {
      return [];
    }
  }

  async function loadHlsVariants(candidate) {
    try {
      const text = await fetchMediaManifest(candidate);
      return parseHlsVariants(text, candidate);
    } catch (_) {
      return [];
    }
  }

  async function fetchMediaManifest(candidate) {
    const headers = {};
    for (const header of candidate.requestHeaders || []) {
      const name = normalizeHeaderName(header.name);
      if (!name || shouldSkipForwardedHeader(name) || ["Cookie", "User-Agent", "Referer", "Origin"].includes(name)) {
        continue;
      }
      headers[name] = header.value || "";
    }

    const response = await fetch(candidate.url, {
      cache: "no-store",
      credentials: "include",
      headers
    });
    if (!response.ok) {
      throw new Error(`manifest fetch failed: ${response.status}`);
    }

    return response.text();
  }

  function parseHlsVariants(text, candidate) {
    const lines = String(text || "")
      .split(/\r?\n/)
      .map((line) => line.trim())
      .filter(Boolean);
    const variants = [];
    for (let index = 0; index < lines.length; index += 1) {
      const line = lines[index];
      if (!line.startsWith("#EXT-X-STREAM-INF:")) {
        continue;
      }

      const attrs = parseHlsAttributes(line.slice("#EXT-X-STREAM-INF:".length));
      const uri = lines.slice(index + 1).find((candidateLine) => !candidateLine.startsWith("#"));
      if (!uri) {
        continue;
      }

      const url = resolveUrl(uri, candidate.url);
      if (!url) {
        continue;
      }

      const resolution = attrs.RESOLUTION || "";
      const height = Number.parseInt((resolution.split("x")[1] || "").trim(), 10) || 0;
      const bandwidth = Number.parseInt(attrs.BANDWIDTH || attrs["AVERAGE-BANDWIDTH"] || "0", 10) || 0;
      const qualityLabel = formatHlsQualityLabel(height, bandwidth);
      variants.push({
        ...candidate,
        id: `${candidate.id || "hls"}-${index}`,
        url,
        filename: filenameWithQuality(candidate.filename || filenameFromMediaUrl(candidate.url, "hls"), qualityLabel),
        mediaType: "hls",
        source: "video-hls",
        parentUrl: candidate.url,
        qualityHeight: height,
        bandwidth,
        qualityLabel,
        isVariant: true
      });
    }
    return variants;
  }

  function parseDashVariants(text, candidate) {
    const cleanText = String(text || "").replace(/<!--[\s\S]*?-->/g, "");
    const manifestBase = extractFirstXmlText(cleanText, "BaseURL");
    const videos = [];
    const audios = [];
    const adaptationPattern = /<AdaptationSet\b([^>]*)>([\s\S]*?)<\/AdaptationSet>/gi;
    let adaptationMatch;
    while ((adaptationMatch = adaptationPattern.exec(cleanText))) {
      const adaptationAttrs = parseXmlAttributes(adaptationMatch[1]);
      const adaptationBody = adaptationMatch[2] || "";
      const adaptationBase = extractFirstXmlText(adaptationBody, "BaseURL") || manifestBase;
      const adaptationType = dashTypeFrom(adaptationAttrs, adaptationBody);
      const representations = parseDashRepresentations(adaptationBody, adaptationAttrs, adaptationBase, candidate.url, adaptationType);
      for (const representation of representations) {
        if (representation.type === "audio") {
          audios.push(representation);
        } else if (representation.type === "video") {
          videos.push(representation);
        }
      }
    }

    const bestAudio = audios.sort((left, right) => (right.bandwidth || 0) - (left.bandwidth || 0))[0] || null;
    return videos
      .sort((left, right) => (right.height || 0) - (left.height || 0) || (right.bandwidth || 0) - (left.bandwidth || 0))
      .map((video, index) => {
        const qualityLabel = formatDashQualityLabel(video.height, video.bandwidth, bestAudio?.bandwidth || 0);
        return {
          ...candidate,
          id: `${candidate.id || "dash"}-${index}`,
          url: video.url,
          videoUrl: video.url,
          audioUrl: bestAudio?.url || "",
          manifestUrl: candidate.url,
          filename: filenameWithQuality(candidate.filename || filenameFromMediaUrl(candidate.url, "dash"), qualityLabel),
          mediaType: bestAudio?.url ? "dash-pair" : "dash-video",
          source: "video-dash",
          qualityHeight: video.height,
          bandwidth: video.bandwidth,
          audioBandwidth: bestAudio?.bandwidth || 0,
          qualityLabel,
          isVariant: true
        };
      });
  }

  function parseDashRepresentations(adaptationBody, adaptationAttrs, adaptationBase, manifestUrl, adaptationType) {
    const representations = [];
    const closedPattern = /<Representation\b([^>]*)>([\s\S]*?)<\/Representation>/gi;
    let match;
    while ((match = closedPattern.exec(adaptationBody))) {
      representations.push(dashRepresentationFrom(match[1], match[2] || "", adaptationAttrs, adaptationBase, manifestUrl, adaptationType));
    }

    const selfClosingPattern = /<Representation\b([^>]*)\/>/gi;
    while ((match = selfClosingPattern.exec(adaptationBody))) {
      representations.push(dashRepresentationFrom(match[1], "", adaptationAttrs, adaptationBase, manifestUrl, adaptationType));
    }

    return representations.filter((representation) => representation.url);
  }

  function dashRepresentationFrom(attrsText, body, adaptationAttrs, adaptationBase, manifestUrl, adaptationType) {
    const attrs = { ...adaptationAttrs, ...parseXmlAttributes(attrsText) };
    const type = dashTypeFrom(attrs, body) || adaptationType;
    const baseUrl = extractFirstXmlText(body, "BaseURL") || adaptationBase;
    const resolvedUrl = baseUrl ? resolveUrl(baseUrl, manifestUrl) : "";
    return {
      type,
      url: resolvedUrl,
      bandwidth: Number.parseInt(attrs.bandwidth || attrs.BANDWIDTH || "0", 10) || 0,
      width: Number.parseInt(attrs.width || attrs.WIDTH || "0", 10) || 0,
      height: Number.parseInt(attrs.height || attrs.HEIGHT || "0", 10) || 0
    };
  }

  function dashTypeFrom(attrs, body) {
    const typeText = `${attrs.contentType || ""} ${attrs.mimeType || ""} ${body || ""}`.toLowerCase();
    if (typeText.includes("audio")) {
      return "audio";
    }
    if (typeText.includes("video") || attrs.width || attrs.height) {
      return "video";
    }
    return "";
  }

  function parseXmlAttributes(value) {
    const attrs = {};
    const pattern = /([A-Za-z_:][-A-Za-z0-9_:.]*)\s*=\s*("([^"]*)"|'([^']*)')/g;
    let match;
    while ((match = pattern.exec(value || ""))) {
      attrs[match[1]] = decodeXmlEntities(match[3] ?? match[4] ?? "");
    }
    return attrs;
  }

  function extractFirstXmlText(xml, name) {
    const pattern = new RegExp(`<${name}\\b[^>]*>([\\s\\S]*?)<\\/${name}>`, "i");
    const match = pattern.exec(xml || "");
    return match ? decodeXmlEntities(match[1].trim()) : "";
  }

  function decodeXmlEntities(value) {
    return String(value || "")
      .replace(/&amp;/g, "&")
      .replace(/&lt;/g, "<")
      .replace(/&gt;/g, ">")
      .replace(/&quot;/g, "\"")
      .replace(/&apos;/g, "'");
  }

  function formatDashQualityLabel(height, videoBandwidth, audioBandwidth) {
    const parts = [];
    if (height > 0) {
      parts.push(`${height}p`);
    }
    if (videoBandwidth > 0) {
      parts.push(`${(videoBandwidth / 1000 / 1000).toFixed(1)}Mbps`);
    }
    if (audioBandwidth > 0) {
      parts.push(`audio ${(audioBandwidth / 1000).toFixed(0)}kbps`);
    }
    return parts.join(" · ") || "DASH";
  }

  function parseHlsAttributes(value) {
    const attrs = {};
    const pattern = /([A-Z0-9-]+)=("[^"]*"|[^,]*)/g;
    let match;
    while ((match = pattern.exec(value))) {
      attrs[match[1]] = match[2].replace(/^"|"$/g, "");
    }
    return attrs;
  }

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

  function formatHlsQualityLabel(height, bandwidth) {
    const parts = [];
    if (height > 0) {
      parts.push(`${height}p`);
    }
    if (bandwidth > 0) {
      parts.push(`${(bandwidth / 1000 / 1000).toFixed(1)}Mbps`);
    }
    return parts.join(" · ") || "HLS";
  }

  function filenameWithQuality(filename, qualityLabel) {
    const safeLabel = String(qualityLabel || "")
      .replace(/[\\/:*?"<>|]/g, "_")
      .replace(/\s+/g, " ")
      .trim();
    if (!safeLabel) {
      return filename;
    }

    const extension = filename.toLowerCase().endsWith(".mp4") ? ".mp4" : "";
    const stem = extension ? filename.slice(0, -extension.length) : filename;
    return `${stem} ${safeLabel}${extension || ".mp4"}`;
  }

  function pruneMediaDownloadCandidates() {
    const threshold = Date.now() - MEDIA_REQUEST_TTL_MS;
    for (const [tabId, candidates] of mediaDownloadCandidates) {
      const nextCandidates = candidates.filter((candidate) => (candidate.updatedAt || 0) >= threshold);
      if (nextCandidates.length) {
        mediaDownloadCandidates.set(tabId, nextCandidates);
      } else {
        mediaDownloadCandidates.delete(tabId);
      }
    }
  }

  function classifyMediaCandidate(url, responseHeaders, resourceType) {
    const loweredUrl = String(url || "").toLowerCase();
    const contentType = String(getHeaderValue(responseHeaders, "Content-Type") || "").toLowerCase();
    if (/(^|[/?&.])m3u8([?&#.]|$)/i.test(loweredUrl) || contentType.includes("mpegurl") || contentType.includes("vnd.apple.mpegurl")) {
      return "hls";
    }
    if (/(^|[/?&.])mpd([?&#.]|$)/i.test(loweredUrl) || contentType.includes("dash+xml")) {
      return "dash";
    }
    if (contentType.startsWith("video/") || resourceType === "media" || /\.(mp4|webm|mov|mkv)([?#]|$)/i.test(loweredUrl)) {
      if (/\.(m4s|ts)([?#]|$)/i.test(loweredUrl)) {
        return "";
      }
      return "video";
    }
    return "";
  }

  function compareMediaCandidates(left, right) {
    const priority = { hls: 0, dash: 1, video: 2 };
    const leftPriority = priority[left.mediaType] ?? 9;
    const rightPriority = priority[right.mediaType] ?? 9;
    if (leftPriority !== rightPriority) {
      return leftPriority - rightPriority;
    }
    if ((left.qualityHeight || right.qualityHeight) && left.qualityHeight !== right.qualityHeight) {
      return (right.qualityHeight || 0) - (left.qualityHeight || 0);
    }
    if ((left.bandwidth || right.bandwidth) && left.bandwidth !== right.bandwidth) {
      return (right.bandwidth || 0) - (left.bandwidth || 0);
    }
    return (right.updatedAt || 0) - (left.updatedAt || 0);
  }

  function getHeaderValue(headerList, name) {
    const normalized = normalizeHeaderName(name);
    return (headerList || []).find((header) => normalizeHeaderName(header.name) === normalized)?.value || "";
  }

  function getUrlHost(value) {
    try {
      return new URL(value || "").host;
    } catch (_) {
      return "";
    }
  }

  function filenameFromMediaUrl(url, mediaType) {
    const fallback = mediaType === "hls" || mediaType === "dash" ? "video.mp4" : "video";
    try {
      const parsed = new URL(url);
      const name = decodeURIComponent(parsed.pathname.split("/").filter(Boolean).pop() || "");
      if (!name) {
        return fallback;
      }
      if (mediaType === "hls" || mediaType === "dash") {
        return name.replace(/\.(m3u8|mpd)$/i, ".mp4");
      }
      return name;
    } catch (_) {
      return fallback;
    }
  }

  function normalizeComparableUrl(url) {
    try {
      const parsed = new URL(url);
      parsed.hash = "";
      return parsed.href;
    } catch (_) {
      return url || "";
    }
  }

  async function getCookieHeader(url) {
    try {
      const cookies = await chrome.cookies.getAll({ url });
      return cookies
        .sort((a, b) => (b.path || "").length - (a.path || "").length)
        .map((cookie) => `${cookie.name}=${cookie.value}`)
        .join("; ");
    } catch (_) {
      return "";
    }
  }

  function mergeDownloadHeaders(headerList, values) {
    const headers = {};
    for (const header of headerList) {
      const name = normalizeHeaderName(header.name);
      if (!name || shouldSkipForwardedHeader(name)) {
        continue;
      }
      headers[name] = header.value || "";
    }

    if (values.userAgent) {
      headers["User-Agent"] = values.userAgent;
    }
    if (values.referrer) {
      headers.Referer = values.referrer;
    }
    if (values.cookie) {
      headers.Cookie = values.cookie;
    }

    return headers;
  }

  function sanitizeRequestHeaders(headerList) {
    return normalizeHeaderList(headerList).filter((header) => !shouldSkipForwardedHeader(normalizeHeaderName(header.name)));
  }

  function normalizeHeaderList(headerList) {
    return headerList
      .filter((header) => header?.name)
      .map((header) => ({
        name: normalizeHeaderName(header.name),
        value: header.value || ""
      }));
  }

  function normalizeHeaderName(name) {
    return String(name || "")
      .split("-")
      .filter(Boolean)
      .map((part) => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase())
      .join("-");
  }

  function shouldSkipForwardedHeader(name) {
    return [
      "Host",
      "Connection",
      "Content-Length",
      "Proxy-Connection",
      "Range",
      "If-Range",
      "Accept-Encoding"
    ].includes(name);
  }

  function extractRequestBody(requestBody) {
    if (!requestBody) {
      return null;
    }

    if (requestBody.raw?.length) {
      return extractRawRequestBody(requestBody.raw);
    }

    if (requestBody.formData) {
      const params = new URLSearchParams();
      for (const [key, values] of Object.entries(requestBody.formData)) {
        for (const value of values) {
          params.append(key, value);
        }
      }
      return stringToBase64RequestBody(params.toString());
    }

    return null;
  }

  function extractRawRequestBody(rawParts) {
    const parts = rawParts
      .map((part) => part.bytes)
      .filter(Boolean)
      .map((bytes) => new Uint8Array(bytes));
    const totalLength = parts.reduce((total, bytes) => total + bytes.length, 0);
    if (!totalLength) {
      return null;
    }
    if (totalLength > DOWNLOAD_MAX_BODY_BYTES) {
      return { base64: "", tooLarge: true };
    }

    const merged = new Uint8Array(totalLength);
    let offset = 0;
    for (const part of parts) {
      merged.set(part, offset);
      offset += part.length;
    }

    return {
      base64: bytesToBase64(merged),
      tooLarge: false
    };
  }

  function stringToBase64RequestBody(value) {
    return {
      base64: bytesToBase64(new TextEncoder().encode(value)),
      tooLarge: false
    };
  }

  function bytesToBase64(bytes) {
    let binary = "";
    const chunkSize = 0x8000;
    for (let index = 0; index < bytes.length; index += chunkSize) {
      binary += String.fromCharCode(...bytes.subarray(index, index + chunkSize));
    }
    return btoa(binary);
  }

  async function getDownloadAcceleratorSettings() {
    const result = await chrome.storage.sync.get({
      [DOWNLOAD_ACCELERATOR_ENABLED_KEY]: true,
      [DOWNLOAD_ACCELERATOR_CANCEL_CHROME_ENABLED_KEY]: true,
      [DOWNLOAD_ACCELERATOR_BRIDGE_URL_KEY]: DEFAULT_DOWNLOAD_ACCELERATOR_BRIDGE_URL
    });

    return {
      enabled: result[DOWNLOAD_ACCELERATOR_ENABLED_KEY] !== false,
      cancelChromeEnabled: result[DOWNLOAD_ACCELERATOR_CANCEL_CHROME_ENABLED_KEY] !== false,
      bridgeUrl: normalizeBridgeUrl(result[DOWNLOAD_ACCELERATOR_BRIDGE_URL_KEY]) || DEFAULT_DOWNLOAD_ACCELERATOR_BRIDGE_URL
    };
  }

  function normalizeBridgeUrl(value) {
    const trimmed = String(value || "").trim();
    if (!trimmed) {
      return "";
    }

    try {
      const url = new URL(trimmed);
      const isLocalHost = url.hostname === "127.0.0.1" || url.hostname === "localhost";
      if (isLocalHost && (url.port === "17651" || url.port === "17652")) {
        return DEFAULT_DOWNLOAD_ACCELERATOR_BRIDGE_URL;
      }
      return url.protocol === "http:" && isLocalHost ? url.href : "";
    } catch (_) {
      return "";
    }
  }

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

  function isSaveablePageUrl(url) {
    return /^(https?|file):\/\//i.test(url || "");
  }

  function isSingleFileResourceUrl(url) {
    return /^(https?|file):\/\//i.test(url || "");
  }

  function arrayBufferToBase64(buffer) {
    const bytes = new Uint8Array(buffer);
    let binary = "";
    const chunkSize = 0x8000;
    for (let index = 0; index < bytes.length; index += chunkSize) {
      binary += String.fromCharCode(...bytes.subarray(index, index + chunkSize));
    }
    return btoa(binary);
  }

  function getDownloadFileName(downloadItem) {
    const filename = downloadItem.filename || "";
    if (filename) {
      return filename.split(/[\\/]/).filter(Boolean).pop() || "";
    }

    try {
      const pathname = new URL(downloadItem.finalUrl || downloadItem.url || "").pathname;
      return decodeURIComponent(pathname.split("/").filter(Boolean).pop() || "");
    } catch (_) {
      return "";
    }
  }

  async function getYoutubeDislike(videoId) {
    if (!(await isFeatureEnabled(YOUTUBE_DISLIKE_ENABLED_KEY))) {
      throw new Error("YouTube 싫어요 표시가 비활성화되어 있습니다.");
    }
    if (!/^[a-zA-Z0-9_-]{11}$/.test(videoId || "")) {
      throw new Error("YouTube 영상 ID가 올바르지 않습니다.");
    }

    const cached = youtubeDislikeCache.get(videoId);
    if (cached && Date.now() - cached.savedAt < YOUTUBE_DISLIKE_CACHE_TTL) {
      return cached.data;
    }

    const response = await fetch(`https://returnyoutubedislikeapi.com/votes?videoId=${encodeURIComponent(videoId)}`);
    if (!response.ok) {
      throw new Error(`Return YouTube Dislike API 오류: ${response.status}`);
    }

    const data = await response.json();
    youtubeDislikeCache.set(videoId, { data, savedAt: Date.now() });
    return data;
  }

  async function getSponsorBlockSegments(videoId) {
    if (!(await isFeatureEnabled(SPONSOR_BLOCK_ENABLED_KEY))) {
      throw new Error("YouTube SponsorBlock이 비활성화되어 있습니다.");
    }
    if (!/^[a-zA-Z0-9_-]{11}$/.test(videoId || "")) {
      throw new Error("YouTube 영상 ID가 올바르지 않습니다.");
    }

    const cached = sponsorBlockCache.get(videoId);
    if (cached && Date.now() - cached.savedAt < SPONSOR_BLOCK_CACHE_TTL) {
      return cached.data;
    }

    const categories = encodeURIComponent(JSON.stringify(SPONSOR_BLOCK_CATEGORIES));
    const response = await fetch(`${SPONSOR_BLOCK_SERVER}/api/skipSegments?videoID=${encodeURIComponent(videoId)}&categories=${categories}`);
    if (response.status === 404) {
      sponsorBlockCache.set(videoId, { data: [], savedAt: Date.now() });
      return [];
    }
    if (!response.ok) {
      throw new Error(`SponsorBlock API 오류: ${response.status}`);
    }

    const data = await response.json();
    const segments = Array.isArray(data)
      ? data
          .map((entry) => ({
            start: Number(entry.segment?.[0]),
            end: Number(entry.segment?.[1]),
            category: entry.category || "",
            actionType: entry.actionType || "skip"
          }))
          .filter((entry) => entry.actionType === "skip" && Number.isFinite(entry.start) && Number.isFinite(entry.end) && entry.end > entry.start)
          .sort((a, b) => a.start - b.start)
      : [];

    sponsorBlockCache.set(videoId, { data: segments, savedAt: Date.now() });
    return segments;
  }

  async function setupGmailUnreadBadge() {
    if (await isFeatureEnabled(GMAIL_UNREAD_ENABLED_KEY)) {
      await chrome.alarms.create(GMAIL_ALARM_NAME, { periodInMinutes: GMAIL_REFRESH_MINUTES });
      return updateGmailUnreadBadge();
    }

    await chrome.alarms.clear(GMAIL_ALARM_NAME);
    return clearGmailUnreadBadge("Gmail 안 읽은 수 표시가 비활성화되어 있습니다.");
  }

  async function getGmailUnreadState() {
    if (!(await isFeatureEnabled(GMAIL_UNREAD_ENABLED_KEY))) {
      return clearGmailUnreadBadge("Gmail 안 읽은 수 표시가 비활성화되어 있습니다.");
    }

    const result = await chrome.storage.local.get({ [GMAIL_UNREAD_STATE_KEY]: null });
    const state = result[GMAIL_UNREAD_STATE_KEY];
    if (state?.updatedAt && Date.now() - state.updatedAt < GMAIL_STATE_TTL) {
      return state;
    }

    return updateGmailUnreadBadge();
  }

  async function updateGmailUnreadBadge() {
    if (!(await isFeatureEnabled(GMAIL_UNREAD_ENABLED_KEY))) {
      return clearGmailUnreadBadge("Gmail 안 읽은 수 표시가 비활성화되어 있습니다.");
    }

    try {
      const pageUnread = await getGmailUnreadFromOpenTab();
      if (Number.isFinite(pageUnread)) {
        return saveGmailUnreadState(pageUnread, "page");
      }

      return saveGmailUnreadState(await fetchGmailUnreadCount(), "atom");
    } catch (error) {
      const state = {
        enabled: true,
        ok: false,
        unread: null,
        error: error.message,
        updatedAt: Date.now()
      };
      await chrome.storage.local.set({ [GMAIL_UNREAD_STATE_KEY]: state });
      await chrome.action.setBadgeText({ text: "" });
      await chrome.action.setTitle({ title: "Gmail 안 읽은 수를 가져오지 못했습니다." });
      return state;
    }
  }

  async function openGmailInbox() {
    if (!(await isFeatureEnabled(GMAIL_UNREAD_ENABLED_KEY))) {
      return;
    }

    await chrome.tabs.create({ url: GMAIL_INBOX_URL });
  }

  async function saveGmailUnreadState(unread, source) {
    if (!Number.isFinite(unread) || unread < 0) {
      throw new Error("Gmail 안 읽은 수가 올바르지 않습니다.");
    }

    const state = {
      enabled: true,
      ok: true,
      unread,
      source,
      updatedAt: Date.now()
    };
    await chrome.storage.local.set({ [GMAIL_UNREAD_STATE_KEY]: state });
    await chrome.action.setBadgeBackgroundColor({ color: "#dc2626" });
    await chrome.action.setBadgeText({ text: unread > 0 ? formatBadgeCount(unread) : "" });
    await chrome.action.setTitle({ title: unread > 0 ? `Gmail 안 읽음 ${unread}개` : "Gmail 안 읽음 없음" });
    return state;
  }

  async function clearGmailUnreadBadge(message) {
    const state = {
      enabled: false,
      ok: false,
      unread: null,
      error: message,
      updatedAt: Date.now()
    };
    await chrome.storage.local.set({ [GMAIL_UNREAD_STATE_KEY]: state });
    await chrome.action.setBadgeText({ text: "" });
    await chrome.action.setTitle({ title: "ONE" });
    return state;
  }

  async function fetchGmailUnreadCount() {
    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), 8000);

    try {
      const response = await fetch(GMAIL_FEED_URL, {
        cache: "no-store",
        credentials: "include",
        signal: controller.signal
      });
      if (!response.ok) {
        if (response.status === 401 || response.status === 403) {
          throw new Error("로그인 필요");
        }
        throw new Error(`Gmail 응답 오류: ${response.status}`);
      }

      const text = await response.text();
      const match = text.match(/<fullcount>(\d+)<\/fullcount>/i);
      if (!match) {
        throw new Error("Gmail 로그인 상태를 확인하세요.");
      }

      return Number.parseInt(match[1], 10);
    } finally {
      clearTimeout(timeoutId);
    }
  }

  async function getGmailUnreadFromOpenTab() {
    const tabs = await chrome.tabs.query({ url: "https://mail.google.com/*" });

    for (const tab of tabs) {
      if (!tab.id) {
        continue;
      }

      const unread = await requestGmailUnreadFromTab(tab.id);
      if (Number.isFinite(unread)) {
        return unread;
      }
    }

    return null;
  }

  function requestGmailUnreadFromTab(tabId) {
    return new Promise((resolve) => {
      chrome.tabs.sendMessage(tabId, { type: "read-gmail-unread-from-page" }, (response) => {
        if (chrome.runtime.lastError || !response?.ok) {
          resolve(null);
          return;
        }

        const unread = Number.parseInt(response.unread, 10);
        resolve(Number.isFinite(unread) ? unread : null);
      });
    });
  }

  function formatBadgeCount(value) {
    if (value > 999) {
      return "999+";
    }

    return String(value);
  }

  async function isFeatureEnabled(key) {
    const result = await chrome.storage.sync.get({ [key]: true });
    return result[key] !== false;
  }

  function toSnapshot(tab) {
    const url = tab.pendingUrl || tab.url || "";
    if (!isRecordableUrl(url)) {
      return null;
    }

    return {
      url,
      title: tab.title || url,
      favIconUrl: tab.favIconUrl || "",
      windowId: tab.windowId,
      index: tab.index,
      pinned: Boolean(tab.pinned),
      capturedAt: Date.now()
    };
  }

  function isRecordableUrl(url) {
    return Boolean(
      url &&
        url !== "about:blank" &&
        url !== "chrome://newtab/" &&
        !url.startsWith(EXTENSION_ORIGIN)
    );
  }
})();
