const params = new URLSearchParams(location.search);
const requestId = params.get("requestId") || "";

const fileNameEl = document.getElementById("file-name");
const statusEl = document.getElementById("status-text");
const progressBarEl = document.getElementById("progress-bar");
const progressTextEl = document.getElementById("progress-text");
const autoCloseEl = document.getElementById("auto-close");
const closeBtnEl = document.getElementById("close-btn");
const DOWNLOAD_SAVE_TIMEOUT_MS = 6000;

let objectUrl = "";
let activeRuleId = 0;

closeBtnEl.addEventListener("click", () => closeDownloaderWindow());
window.addEventListener("unload", () => {
  if (objectUrl) URL.revokeObjectURL(objectUrl);
  clearDownloadHeaders();
});

boot();

async function boot() {
  if (!requestId) {
    fail("缺少下载任务");
    return;
  }
  try {
    const request = await getRequestData();
    if (Array.isArray(request.items) && request.items.length) {
      await runBatchDownload(request.items);
    } else {
      await runSingleDownload(request);
    }

    await clearRequestData();
    await clearDownloadHeaders();
    setProgress(100, "完成");
    setStatus("下载完成");
    if (autoCloseEl.checked) setTimeout(() => closeDownloaderWindow(), 800);
  } catch (error) {
    fail(error?.message || "下载失败");
  }
}

async function runSingleDownload(request) {
  activeRuleId = Number(request.ruleId || 0) || 0;
  await downloadOneRequest(request);
}

async function runBatchDownload(items) {
  let successCount = 0;
  let lastError = null;
  for (let index = 0; index < items.length; index += 1) {
    const request = items[index];
    activeRuleId = Number(request.ruleId || 0) || 0;
    try {
      setStatus(`正在下载 ${index + 1}/${items.length}`);
      await downloadOneRequest(request);
      successCount += 1;
    } catch (error) {
      lastError = error;
    } finally {
      await clearDownloadHeaders();
    }
  }
  if (!successCount) throw lastError || new Error("批量下载失败");
}

async function downloadOneRequest(request) {
  const filename = buildFileName(request);
  fileNameEl.textContent = filename;
  setStatus("正在连接视频资源");

  const blob = await fetchVideoBlob(request);
  if (!blob.size) throw new Error("视频内容为空");

  setProgress(88, "保存中");
  const downloadId = await saveBlob(blob, filename);
  setStatus("正在写入浏览器下载");
  await waitForDownload(downloadId);
}

async function getRequestData() {
  const response = await sendMessage({ type: "VD_GET_BACKUP_DOWNLOAD_REQUEST", requestId });
  if (!response?.ok || !response.request) throw new Error(response?.error || "下载任务不存在");
  return response.request;
}

async function clearRequestData() {
  await sendMessage({ type: "VD_CLEAR_BACKUP_DOWNLOAD_REQUEST", requestId }).catch(() => {});
}

async function fetchVideoBlob(request) {
  const attempts = [
    { label: "正在读取视频", headers: request.requestHeaders || {} },
    { label: "正在重试读取", headers: { ...(request.requestHeaders || {}), Range: "bytes=0-" } }
  ];
  let lastError = null;

  for (let index = 0; index < attempts.length; index += 1) {
    const attempt = attempts[index];
    try {
      setStatus(attempt.label);
      await prepareFetch(request, attempt.headers);
      await delay(80);
      return await readBlob(request.url, request.size || 0, index);
    } catch (error) {
      lastError = error;
      await clearDownloadHeaders();
    }
  }

  throw lastError || new Error("视频读取失败");
}

async function prepareFetch(request, headers) {
  const response = await sendMessage({
    type: "VD_PREPARE_BACKUP_FETCH",
    url: request.url,
    referer: request.referer || request.pageUrl || "https://www.douyin.com/",
    requestId,
    ruleId: activeRuleId || request.ruleId || 0,
    requestHeaders: headers
  });
  if (!response?.ok) throw new Error(response?.error || "准备下载失败");
}

async function readBlob(url, knownSize, attemptIndex) {
  const response = await fetch(url, { credentials: "include", cache: "no-store" });
  if (!response.ok && response.status !== 206) {
    throw new Error(`视频读取失败：HTTP ${response.status}`);
  }

  const mimeType = String(response.headers.get("content-type") || "").split(";")[0].trim().toLowerCase();
  if (mimeType === "text/html" || mimeType === "application/xhtml+xml") {
    throw new Error("链接返回的是网页，不是视频文件");
  }

  const total = Number(response.headers.get("content-length") || knownSize || 0);
  const start = attemptIndex ? 20 : 8;
  const span = attemptIndex ? 62 : 74;

  if (!response.body?.getReader) {
    const blob = await response.blob();
    await assertNotHtml(blob, mimeType);
    await assertPlayableVideo(blob, mimeType);
    setProgress(82, "读取完成");
    return blob;
  }

  const reader = response.body.getReader();
  const chunks = [];
  let received = 0;

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    if (value?.length) {
      chunks.push(value);
      received += value.length;
      if (total) setProgress(start + Math.min(span, Math.round((received / total) * span)));
      else setProgress(Math.min(80, start + Math.round(received / 1024 / 1024)));
    }
  }

  const blob = new Blob(chunks, { type: mimeType || "video/mp4" });
  await assertNotHtml(blob, mimeType);
  await assertPlayableVideo(blob, mimeType);
  setProgress(82, "读取完成");
  return blob;
}

async function assertNotHtml(blob, mimeType) {
  if (mimeType && mimeType !== "application/octet-stream") return;
  const text = await blob.slice(0, 160).text().catch(() => "");
  if (/^\s*<!doctype html|^\s*<html[\s>]/i.test(text)) {
    throw new Error("链接返回的是网页，不是视频文件");
  }
}

async function assertPlayableVideo(blob, mimeType) {
  const bytes = new Uint8Array(await blob.slice(0, 2048).arrayBuffer());
  if (isPlayableMediaSample(bytes, mimeType)) return;
  throw new Error("当前资源不是可播放视频，已拦截");
}

function isPlayableMediaSample(bytes, mimeType) {
  if (!bytes || bytes.length < 12) return false;
  const mime = String(mimeType || "").split(";")[0].trim().toLowerCase();
  const text = asciiFromBytes(bytes, 0, Math.min(32, bytes.length)).trimStart().toLowerCase();
  if (text.startsWith("<!doctype html") || text.startsWith("<html")) return false;
  if (text.startsWith("#extm3u")) return mime.includes("mpegurl");
  if (bytes[0] === 0x1a && bytes[1] === 0x45 && bytes[2] === 0xdf && bytes[3] === 0xa3) return true;
  if (bytes[0] === 0x47 && mime.includes("mp2t")) return true;
  const box = asciiFromBytes(bytes, 4, 4);
  return box === "ftyp" || box === "styp";
}

function asciiFromBytes(bytes, start, length) {
  let text = "";
  for (let index = start; index < start + length && index < bytes.length; index += 1) {
    text += String.fromCharCode(bytes[index]);
  }
  return text;
}

async function saveBlob(blob, filename) {
  if (objectUrl) URL.revokeObjectURL(objectUrl);
  objectUrl = URL.createObjectURL(blob);
  return new Promise((resolve, reject) => {
    chrome.downloads.download({
      url: objectUrl,
      filename,
      saveAs: false,
      conflictAction: "uniquify"
    }, (downloadId) => {
      if (chrome.runtime.lastError || downloadId === undefined) {
        reject(new Error(chrome.runtime.lastError?.message || "保存失败"));
        return;
      }
      resolve(downloadId);
    });
  });
}

function waitForDownload(downloadId) {
  return new Promise((resolve, reject) => {
    let settled = false;
    const finish = (handler) => {
      if (settled) return;
      settled = true;
      clearTimeout(timer);
      chrome.downloads.onChanged.removeListener(listener);
      handler();
    };
    const listener = (delta) => {
      if (delta.id !== downloadId || !delta.state?.current) return;
      if (delta.state.current === "complete") {
        finish(resolve);
      }
      if (delta.state.current === "interrupted") {
        finish(() => reject(new Error(delta.error?.current || "浏览器下载中断")));
      }
    };
    const timer = setTimeout(() => finish(resolve), DOWNLOAD_SAVE_TIMEOUT_MS);
    chrome.downloads.onChanged.addListener(listener);
  });
}

function buildFileName(request) {
  const base = sanitizeFileName(
    request.suggestedTitle
      || request.name?.replace(/\.[a-z0-9]{2,8}$/i, "")
      || extractNameFromUrl(request.url)
      || "douyin-video"
  );
  const ext = safeExtension(request.extension || getExtension(request.url) || defaultExtension(request.kind));
  return `${base}.${ext}`;
}

function extractNameFromUrl(url) {
  try {
    const raw = decodeURIComponent(new URL(url).pathname.split("/").filter(Boolean).pop() || "");
    return raw.replace(/\.[a-z0-9]{2,8}$/i, "");
  } catch {
    return "";
  }
}

function getExtension(url) {
  try {
    return new URL(url).pathname.toLowerCase().match(/\.([a-z0-9]{2,8})$/i)?.[1] || "";
  } catch {
    return String(url || "").toLowerCase().match(/\.([a-z0-9]{2,8})(?:[?#]|$)/)?.[1] || "";
  }
}

function safeExtension(ext) {
  const value = String(ext || "").toLowerCase();
  if (["htm", "html", "php", "asp", "aspx", "shtml", "xhtml"].includes(value)) return "mp4";
  return value || "mp4";
}

function defaultExtension(kind) {
  return kind === "audio" ? "mp3" : "mp4";
}

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

function setProgress(value, label) {
  const percent = Math.max(0, Math.min(100, Number(value) || 0));
  progressBarEl.style.width = `${percent}%`;
  progressTextEl.textContent = label || `${percent.toFixed(0)}%`;
}

function setStatus(text) {
  statusEl.textContent = text;
}

async function fail(message) {
  setStatus(message);
  setProgress(0, "失败");
  await clearDownloadHeaders();
}

async function closeDownloaderWindow() {
  await clearDownloadHeaders();
  window.close();
  setTimeout(() => {
    try {
      chrome.windows?.getCurrent?.((win) => {
        if (chrome.runtime.lastError || !win?.id) return;
        chrome.windows.remove(win.id, () => void chrome.runtime.lastError);
      });
    } catch {}
  }, 80);
}

function sendMessage(message) {
  return chrome.runtime.sendMessage(message);
}

function clearDownloadHeaders() {
  return chrome.runtime.sendMessage({
    type: "VD_CLEAR_DOWNLOAD_HEADERS",
    requestId,
    ruleId: activeRuleId
  }).catch(() => {});
}

function delay(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}
