const VIDEO_FETCH_TIMEOUT_MS = 5 * 60 * 1000;
const VIDEO_BLOB_REVOKE_DELAY_MS = 60 * 1000;

class OffscreenAudioPlayer {
  constructor() {
    this.audioCache = new Map();
  }

  async playAudio(audioUrl) {
    try {
      let audio = this.audioCache.get(audioUrl);
      if (!audio) {
        audio = new Audio(audioUrl);
        audio.preload = "auto";
        this.audioCache.set(audioUrl, audio);
      }
      audio.currentTime = 0;
      await audio.play();
      return { success: true, audioUrl, duration: audio.duration || 0 };
    } catch (error) {
      return { success: false, error: error.message, audioUrl };
    }
  }

  clearCache() {
    for (const audio of this.audioCache.values()) {
      audio.pause();
      audio.src = "";
      audio.load();
    }
    this.audioCache.clear();
  }
}

const audioPlayer = new OffscreenAudioPlayer();
const activeBlobUrls = new Set();

chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  if (!message || typeof message.type !== "string") return false;

  if (message.type === "PLAY_AUDIO") {
    audioPlayer.playAudio(message.audioUrl)
      .then((result) => {
        sendResponse(result);
        chrome.runtime.sendMessage({ type: "AUDIO_RESULT", ...result }).catch(() => {});
      })
      .catch((error) => {
        sendResponse({ success: false, error: error.message, audioUrl: message.audioUrl });
      });
    return true;
  }

  if (message.type === "OFFSCREEN_FETCH_VIDEO") {
    const request = normalizeVideoRequest(message.request);
    if (!request) {
      sendResponse({ ok: false, error: "隐藏下载任务无效" });
      return true;
    }
    downloadVideo(request).catch(() => {});
    sendResponse({ ok: true, started: true, requestId: request.requestId });
    return true;
  }

  return false;
});

async function downloadVideo(request) {
  const filename = buildFileName(request);
  try {
    const blob = await fetchVideoBlob(request);
    if (!blob.size) throw new Error("视频内容为空");
    const downloadId = await saveBlob(blob, filename);
    await reportVideoResult({
      ok: true,
      requestId: request.requestId,
      tabId: request.tabId,
      url: request.url,
      filename,
      size: blob.size,
      downloadId
    });
  } catch (error) {
    await clearDownloadHeaders(request);
    await reportVideoResult({
      ok: false,
      requestId: request.requestId,
      tabId: request.tabId,
      url: request.url,
      filename,
      error: error?.message || "隐藏下载失败"
    });
  }
}

function normalizeVideoRequest(raw) {
  if (!raw || typeof raw !== "object") return null;
  const url = String(raw.url || "").trim();
  if (!/^https?:\/\//i.test(url)) return null;
  return {
    ...raw,
    url,
    requestId: String(raw.requestId || `offscreen_${Date.now()}`),
    ruleId: Number(raw.ruleId || 0) || 0,
    tabId: Number.isInteger(raw.tabId) ? raw.tabId : undefined,
    kind: raw.kind || "video",
    extension: raw.extension || getExtension(url) || "mp4",
    referer: raw.referer || raw.pageUrl || "https://www.douyin.com/",
    requestHeaders: raw.requestHeaders || {}
  };
}

async function fetchVideoBlob(request) {
  const attempts = [
    { headers: request.requestHeaders || {} },
    { headers: { ...(request.requestHeaders || {}), Range: "bytes=0-" } }
  ];
  let lastError = null;

  for (const attempt of attempts) {
    try {
      await prepareFetch(request, attempt.headers);
      await delay(80);
      return await readBlob(request.url, request.size || 0);
    } catch (error) {
      lastError = error;
      await clearDownloadHeaders(request);
    }
  }

  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: request.requestId,
    ruleId: request.ruleId,
    requestHeaders: headers
  });
  if (!response?.ok) throw new Error(response?.error || "准备下载失败");
}

async function readBlob(url, knownSize) {
  const timeout = createTimeout(VIDEO_FETCH_TIMEOUT_MS);
  try {
    const response = await fetch(url, { credentials: "include", cache: "no-store", signal: timeout.signal });
    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("链接返回的是网页，不是视频文件");
    }

    if (!response.body?.getReader) {
      const blob = await response.blob();
      await assertNotHtml(blob, mimeType);
      await assertPlayableVideo(blob, mimeType);
      return blob;
    }

    const reader = response.body.getReader();
    const chunks = [];
    let received = 0;
    const total = Number(response.headers.get("content-length") || knownSize || 0);

    while (true) {
      const { done, value } = await reader.read();
      if (done) break;
      if (value?.length) {
        chunks.push(value);
        received += value.length;
      }
      if (total && received > total * 1.1) break;
    }

    const blob = new Blob(chunks, { type: mimeType || "video/mp4" });
    await assertNotHtml(blob, mimeType);
    await assertPlayableVideo(blob, mimeType);
    return blob;
  } catch (error) {
    if (timeout.signal.aborted) throw new Error("视频读取超时");
    throw error;
  } finally {
    timeout.cancel();
  }
}

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) {
  const objectUrl = URL.createObjectURL(blob);
  activeBlobUrls.add(objectUrl);
  try {
    const response = await sendMessage({
      type: "OFFSCREEN_SAVE_BLOB_URL",
      url: objectUrl,
      filename
    });
    if (!response?.ok) throw new Error(response?.error || "保存失败");
    setTimeout(() => revokeBlobUrl(objectUrl), VIDEO_BLOB_REVOKE_DELAY_MS);
    return response.downloadId;
  } catch (error) {
    revokeBlobUrl(objectUrl);
    throw error;
  }
}

function buildFileName(request) {
  const base = sanitizeFileName(
    request.suggestedTitle
      || String(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 clearDownloadHeaders(request) {
  return sendMessage({
    type: "VD_CLEAR_DOWNLOAD_HEADERS",
    requestId: request.requestId,
    ruleId: request.ruleId
  }).catch(() => {});
}

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

function reportVideoResult(payload) {
  return sendMessage({ type: "OFFSCREEN_VIDEO_RESULT", ...payload }).catch(() => {});
}

function revokeBlobUrl(url) {
  if (!url || !activeBlobUrls.has(url)) return;
  activeBlobUrls.delete(url);
  URL.revokeObjectURL(url);
}

function createTimeout(timeoutMs) {
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), Math.max(1000, Number(timeoutMs) || 0));
  return {
    signal: controller.signal,
    cancel: () => clearTimeout(timer)
  };
}

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

window.addEventListener("beforeunload", () => {
  audioPlayer.clearCache();
  for (const url of activeBlobUrls) URL.revokeObjectURL(url);
  activeBlobUrls.clear();
});
