// Shared fetch helper with credential fallback (avoid CORS '*' issues)
async function fetchTextWithFallback(url, opts = {}) {
  const tries = [
    { credentials: "omit", mode: "cors" },
    { credentials: "include", mode: "cors" },
  ];
  let lastErr;
  for (const t of tries) {
    try {
      const res = await fetch(url, { ...opts, ...t });
      const text = await res.text();
      return { res, text };
    } catch (err) {
      lastErr = err;
      continue;
    }
  }
  throw lastErr;
}

// BlackEagle video extractor content script
(() => {
  const VIDEO_EXT_PATTERN = /(\.m3u8($|\?))|(\.mpd($|\?))|(\.mp4($|\?))|(\.webm($|\?))|(\.mov($|\?))|(\.flv($|\?))|(\.f4v($|\?))|(\.mkv($|\?))|(\.avi($|\?))|(\.ts($|\?))/i;
  const VIDEO_CT_PATTERN = /(video\/|application\/vnd\.apple\.mpegurl|application\/x-mpegurl|application\/dash\+xml|application\/octet-stream)/i;

  const state = {
    seen: new Map(),
    lastScan: 0,
  };

  const normalizeUrl = (maybeUrl) => {
    if (!maybeUrl) return null;
    try {
      return new URL(maybeUrl, location.href).href;
    } catch (err) {
      return null;
    }
  };

  const addCandidate = (url, meta = {}) => {
    const normalized = normalizeUrl(url);
    if (!normalized) return;
    if (state.seen.has(normalized)) {
      const existing = state.seen.get(normalized);
      state.seen.set(normalized, { ...existing, ...meta });
      return;
    }
    state.seen.set(normalized, {
      url: normalized,
      discoveredAt: Date.now(),
      ...meta,
    });
  };

  const looksLikeVideo = (url, contentType = "") => {
    if (!url) return false;
    if (VIDEO_EXT_PATTERN.test(url)) return true;
    if (VIDEO_CT_PATTERN.test(contentType)) return true;
    return false;
  };

  const isMPDLike = (url, contentType = "") => /\.mpd(\?|$)/i.test(url || "") || /application\/dash\+xml/i.test(contentType || "");

  const detectManifestType = async (url) => {
    try {
      const ctrl = new AbortController();
      const timer = setTimeout(() => ctrl.abort(), 3000);
      const { res, text } = await fetchTextWithFallback(url, {
        method: "GET",
        headers: { Range: "bytes=0-4096" },
        signal: ctrl.signal,
      });
      clearTimeout(timer);
      const ct = res.headers?.get?.("content-type") || "";
      if (/dash\+xml|mpd/i.test(ct)) {
        console.debug("BlackEagle detectManifestType: dash content-type", { url, ct });
        return { type: "mpd", contentType: ct };
      }
      const head = (text || "").slice(0, 2048);
      if (/\<MPD[\s>]/i.test(head)) {
        console.debug("BlackEagle detectManifestType: found <MPD>", { url });
        return { type: "mpd", contentType: ct };
      }
      if (/EXTM3U/.test(head)) {
        console.debug("BlackEagle detectManifestType: found EXTM3U", { url });
        return { type: "hls", contentType: ct };
      }
      return { type: null, contentType: ct };
    } catch (_err) {
      console.debug("BlackEagle detectManifestType failed", { url, err: _err?.message });
      return { type: null };
    }
  };

  const collectFromVideoTags = () => {
    document.querySelectorAll("video").forEach((video, idx) => {
      const src = video.currentSrc || video.src;
      if (looksLikeVideo(src)) addCandidate(src, { source: "video-tag", note: `video-${idx}` });
      video.querySelectorAll("source").forEach((source, sIdx) => {
        const s = source.src;
        if (looksLikeVideo(s)) addCandidate(s, { source: "source-tag", note: `video-${idx}-source-${sIdx}` });
      });
    });
  };

  const collectFromMeta = () => {
    const metaNames = ["og:video", "og:video:secure_url", "twitter:player:stream", "twitter:player:stream:content_type"];
    metaNames.forEach((name) => {
      const el = document.querySelector(`meta[property='${name}'], meta[name='${name}']`);
      if (el) {
        const content = el.getAttribute("content");
        if (looksLikeVideo(content)) addCandidate(content, { source: "meta", name });
      }
    });
  };

  const collectFromLdJson = () => {
    document.querySelectorAll('script[type="application/ld+json"]').forEach((script, idx) => {
      try {
        const data = JSON.parse(script.textContent || "{}");
        const candidates = [];
        if (Array.isArray(data)) candidates.push(...data);
        else candidates.push(data);
        candidates.forEach((item) => {
          if (!item || typeof item !== "object") return;
          const url = item.contentUrl || item.url || item.embedUrl;
          if (looksLikeVideo(url)) addCandidate(url, { source: "ld+json", note: `block-${idx}` });
        });
      } catch (err) {
        // ignore JSON parse errors
      }
    });
  };

  const collectFromInlineScripts = () => {
    const urlRegex = /https?:\/\/[A-Za-z0-9._~:/?#\[\]@!$&'()*+,;=%-]+/g;
    document.querySelectorAll("script").forEach((script, idx) => {
      const txt = script.textContent || "";
      let match;
      let found = 0;
      while ((match = urlRegex.exec(txt))) {
        const candidate = match[0];
        if (looksLikeVideo(candidate)) {
          addCandidate(candidate, { source: "script", note: `script-${idx}` });
          found += 1;
        }
        if (found > 20) break;
      }
    });
  };

  const collectFromPerformance = () => {
    performance.getEntriesByType("resource").forEach((entry) => {
      const name = entry.name;
      if (looksLikeVideo(name)) addCandidate(name, { source: "performance" });
    });
  };

  const collectFromLinks = () => {
    document.querySelectorAll("a[href]").forEach((a, idx) => {
      const href = a.getAttribute("href");
      if (looksLikeVideo(href)) addCandidate(href, { source: "anchor", note: `anchor-${idx}` });
    });
  };

  const scanAll = () => {
    collectFromVideoTags();
    collectFromMeta();
    collectFromLdJson();
    collectFromInlineScripts();
    collectFromLinks();
    collectFromPerformance();
    state.lastScan = Date.now();
    return getCandidates();
  };

  const getCandidates = () => Array.from(state.seen.values()).sort((a, b) => b.discoveredAt - a.discoveredAt);

  const patchFetch = () => {
    if (!window.fetch) return;
    const original = window.fetch.bind(window);
    window.fetch = async (...args) => {
      const req = args[0];
      const reqUrl = typeof req === "string" ? req : req?.url;
      try {
        const res = await original(...args);
        const url = res.url || reqUrl;
        const ct = res.headers?.get?.("content-type") || "";
        if (looksLikeVideo(url, ct)) addCandidate(url, { source: "fetch", contentType: ct });
        return res;
      } catch (err) {
        throw err;
      }
    };
  };

  const patchXHR = () => {
    const { open, send } = XMLHttpRequest.prototype;
    XMLHttpRequest.prototype.open = function patchedOpen(method, url, ...rest) {
      this.__be_url = url;
      return open.call(this, method, url, ...rest);
    };
    XMLHttpRequest.prototype.send = function patchedSend(body) {
      this.addEventListener("load", () => {
        const url = this.responseURL || this.__be_url;
        const ct = this.getResponseHeader("content-type") || "";
        if (looksLikeVideo(url, ct)) addCandidate(url, { source: "xhr", contentType: ct });
      });
      return send.call(this, body);
    };
  };

  const observeMutations = () => {
    const observer = new MutationObserver((records) => {
      let touched = false;
      for (const r of records) {
        r.addedNodes.forEach((node) => {
          if (!(node instanceof HTMLElement)) return;
          if (node.tagName === "VIDEO" || node.querySelector?.("video")) touched = true;
          if (node.tagName === "SOURCE" || node.querySelector?.("source")) touched = true;
        });
      }
      if (touched) collectFromVideoTags();
    });
    observer.observe(document.documentElement || document.body, { childList: true, subtree: true });
  };

  const listVideosIncludingFrames = () => {
    const vids = Array.from(document.querySelectorAll("video"));
    const iframes = Array.from(document.querySelectorAll("iframe"));
    iframes.forEach((f) => {
      try {
        const doc = f.contentDocument || f.contentWindow?.document;
        if (!doc) return;
        vids.push(...Array.from(doc.querySelectorAll("video")));
      } catch (_err) {
        // cross-origin iframe, ignore
      }
    });
    return vids;
  };

  const pickPlayableVideo = () => {
    const videos = listVideosIncludingFrames();
    return videos.find((v) => v.readyState >= 2 && (v.videoWidth || v.clientWidth)) || videos[0] || null;
  };

  const waitForFrame = (video, timeoutMs = 1500) => {
    return new Promise((resolve) => {
      if (!video) return resolve(false);
      if (video.readyState >= 2 && (video.videoWidth || video.clientWidth)) return resolve(true);
      let done = false;
      const finish = (ok) => {
        if (done) return;
        done = true;
        clearTimeout(timer);
        video.removeEventListener("loadeddata", onReady);
        video.removeEventListener("canplay", onReady);
        resolve(ok);
      };
      const onReady = () => {
        if (video.readyState >= 2 && (video.videoWidth || video.clientWidth)) finish(true);
      };
      const timer = setTimeout(() => finish(false), timeoutMs);
      video.addEventListener("loadeddata", onReady, { once: true });
      video.addEventListener("canplay", onReady, { once: true });
      // 尝试触发加载
      try {
        video.play().catch((err) => {
          console.debug("BlackEagle capture: play() rejected", err?.message);
        });
      } catch (err) {
        console.debug("BlackEagle capture: play() threw", err?.message);
      }
    });
  };

  const captureFrame = async () => {
    console.debug("BlackEagle capture: start");
    const video = pickPlayableVideo();
    if (!video) {
      console.debug("BlackEagle capture: no video element");
      return { ok: false, error: "no-video" };
    }
    console.debug("BlackEagle capture: candidate video", {
      readyState: video.readyState,
      vw: video.videoWidth,
      vh: video.videoHeight,
      cw: video.clientWidth,
      ch: video.clientHeight,
      src: video.currentSrc || video.src,
    });
    const ready = await waitForFrame(video, 2000);
    if (!ready) {
      console.debug("BlackEagle capture: still not ready", {
        readyState: video.readyState,
        vw: video.videoWidth,
        vh: video.videoHeight,
        cw: video.clientWidth,
        ch: video.clientHeight,
      });
      return { ok: false, error: "not-ready" };
    }
    const width = video.videoWidth || video.clientWidth;
    const height = video.videoHeight || video.clientHeight;
    if (!width || !height) {
      console.debug("BlackEagle capture: no dimensions", { width, height });
      return { ok: false, error: "no-dimensions" };
    }
    const canvas = document.createElement("canvas");
    canvas.width = width;
    canvas.height = height;
    const ctx = canvas.getContext("2d");
    if (!ctx) {
      console.debug("BlackEagle capture: no 2d context");
      return { ok: false, error: "no-context" };
    }
    try {
      ctx.drawImage(video, 0, 0, width, height);
      const dataUrl = canvas.toDataURL("image/jpeg", 0.92);
      return { ok: true, dataUrl, width, height };
    } catch (err) {
      console.debug("BlackEagle capture: draw failed", err?.message);
      return { ok: false, error: err?.message || "capture-failed" };
    }
  };

  const downloadViaBackground = (url) =>
    new Promise((resolve) => {
      if (!chrome?.runtime?.sendMessage) return resolve({ ok: false, error: "no-runtime" });
      chrome.runtime.sendMessage({ action: "blackeagle:download", url }, (resp) => {
        if (chrome.runtime.lastError) return resolve({ ok: false, error: chrome.runtime.lastError.message });
        resolve(resp || { ok: false, error: "no-response" });
      });
    });

  const downloadLocally = async (url) => {
    try {
      const res = await fetch(url, { credentials: "include" });
      const ct = res.headers.get("content-type") || "";
      const blob = await res.blob();
      const blobUrl = URL.createObjectURL(blob);
      return { ok: true, blobUrl, size: blob.size, contentType: ct };
    } catch (err) {
      return { ok: false, error: err?.message || "download-failed" };
    }
  };

  const handleMessage = (message, _sender, sendResponse) => {
    if (!message || typeof message !== "object") return;
    switch (message.action) {
      case "blackeagle:get-video-sources": {
        scanAll();
        const sources = getCandidates();
        const mpdJobs = sources.map(async (s) => {
          try {
            let mpdFlag = isMPDLike(s.url, s.contentType);
            if (!mpdFlag) {
              const det = await detectManifestType(s.url);
              if (det.type === "mpd") {
                mpdFlag = true;
                s.contentType = s.contentType || det.contentType;
                console.debug("BlackEagle MPD detected during scan", { url: s.url });
              }
            }
            if (!mpdFlag) return;
            console.debug("BlackEagle parsing MPD", { url: s.url });
            const parsed = await parseMPD(s.url);
            if (parsed?.ok) {
              s.manifestType = "mpd";
              s.segments = parsed.segments;
              s.segmentCount = parsed.count;
              s.segmentUrls = parsed.segments?.map((seg) => seg.url).filter(Boolean) || [];
              console.debug("BlackEagle MPD parsed", { url: s.url, count: parsed.count });
            } else {
              s.manifestType = "mpd";
              s.parseError = parsed?.error || "unknown";
              console.warn("BlackEagle MPD parse error", { url: s.url, err: s.parseError });
            }
          } catch (e) {
            s.manifestType = "mpd";
            s.parseError = e?.message || "parse-failed";
            console.warn("BlackEagle MPD parse exception", { url: s.url, err: s.parseError });
          }
        });
        Promise.allSettled(mpdJobs).finally(() => {
          sendResponse({ ok: true, sources, scannedAt: state.lastScan });
        });
        return true;
      }
      case "blackeagle:capture-frame": {
        captureFrame().then(sendResponse);
        return true;
      }
      case "blackeagle:parse-mpd": {
        const { url } = message || {};
        parseMPD(url).then(sendResponse);
        return true;
      }
      case "blackeagle:download": {
        const { url, preferBackground } = message;
        const handleMPD = () => {
          console.debug("BlackEagle download: MPD path", { url });
          parseMPD(url).then((resp) => {
            if (resp?.ok) sendResponse({ ok: true, type: "mpd", segments: resp.segments, segmentUrls: resp.segments?.map((s) => s.url).filter(Boolean) || [], count: resp.count });
            else sendResponse({ ok: false, error: resp?.error || "mpd-parse-failed" });
          });
        };
        if (isMPDLike(url)) {
          handleMPD();
          return true;
        }
        detectManifestType(url).then((det) => {
          if (det.type === "mpd") {
            handleMPD();
            return;
          }
          if (preferBackground) {
            downloadViaBackground(url).then(sendResponse);
            return;
          }
          downloadLocally(url).then(sendResponse);
        });
        return true;
      }
      default:
        break;
    }
    return undefined;
  };

  const exposeHelpers = () => {
    window.BlackEagleVideo = {
      scan: scanAll,
      get: getCandidates,
      capture: captureFrame,
      download: downloadLocally,
      parseMPD: (url) => parseMPD(url),
    };
  };

  const bootstrap = () => {
    patchFetch();
    patchXHR();
    observeMutations();
    collectFromPerformance();
    scanAll();
    exposeHelpers();
    if (chrome?.runtime?.onMessage) chrome.runtime.onMessage.addListener(handleMessage);
  };

  if (document.readyState === "complete" || document.readyState === "interactive") {
    bootstrap();
  } else {
    window.addEventListener("DOMContentLoaded", bootstrap, { once: true });
  }
})();

// --- MPD parsing helpers ---
async function parseMPD(url) {
  try {
    const mpdUrl = new URL(url, location.href).href;
    const { res, text } = await fetchTextWithFallback(mpdUrl, { method: "GET" });
    const ct = res.headers?.get?.("content-type") || "";
    const xml = new DOMParser().parseFromString(text, "application/xml");
    if (xml.querySelector("parsererror")) return { ok: false, error: "mpd-parse-error" };

    const isoDur = (str) => {
      if (!str) return 0;
      const re = /^P(?:(\d+)Y)?(?:(\d+)M)?(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+(?:\.\d+)?)S)?)?$/;
      const m = re.exec(str);
      if (!m) return 0;
      const [_, Y, Mo, D, H, Mi, S] = m;
      const years = parseInt(Y || "0", 10),
        months = parseInt(Mo || "0", 10),
        days = parseInt(D || "0", 10);
      const hours = parseInt(H || "0", 10),
        minutes = parseInt(Mi || "0", 10),
        seconds = parseFloat(S || "0");
      return (((years * 365 + months * 30 + days) * 24 + hours) * 60 + minutes) * 60 + seconds;
    };

    const resolve = (base, rel) => {
      try {
        return new URL(rel, base).href;
      } catch {
        return rel || base;
      }
    };
    const textOf = (el, sel) => {
      const node = el?.querySelector?.(sel);
      return (node && (node.textContent || "").trim()) || "";
    };
    const firstChildText = (el, tag) => {
      const node = Array.from(el?.children || []).find((n) => n.tagName === tag || n.tagName === tag.toUpperCase());
      return (node && (node.textContent || "").trim()) || "";
    };

    const mpdEl = xml.documentElement;
    const mpdBaseUrl = firstChildText(mpdEl, "BaseURL");
    const mpdBase = mpdBaseUrl ? resolve(mpdUrl, mpdBaseUrl) : mpdUrl;
    const mpdDuration = isoDur(mpdEl.getAttribute("mediaPresentationDuration"));

    const substitute = (tpl, vars) => {
      if (!tpl) return "";
      let out = tpl;
      out = out.replace(/\$RepresentationID\$/g, vars.representationId || "");
      out = out.replace(/\$Bandwidth\$/g, String(vars.bandwidth || ""));
      out = out.replace(/\$Number(?::?%0(\d+)d)?\$/g, (m, w) => {
        const num = String(vars.number ?? "");
        const pad = parseInt(w || "0", 10);
        return pad ? num.padStart(pad, "0") : num;
      });
      out = out.replace(/\$Time\$/g, String(vars.time || ""));
      return out;
    };

    const segments = [];
    const periods = Array.from(mpdEl.getElementsByTagName("Period"));
    periods.forEach((period) => {
      const perBase = firstChildText(period, "BaseURL");
      const periodBase = perBase ? resolve(mpdBase, perBase) : mpdBase;
      const periodDur = isoDur(period.getAttribute("duration")) || mpdDuration;
      const adaptationSets = Array.from(period.getElementsByTagName("AdaptationSet"));
      adaptationSets.forEach((aset) => {
        const asBaseTxt = firstChildText(aset, "BaseURL");
        const asBase = asBaseTxt ? resolve(periodBase, asBaseTxt) : periodBase;
        const asetST = aset.querySelector("SegmentTemplate");
        const representations = Array.from(aset.getElementsByTagName("Representation"));
        representations.forEach((rep) => {
          const repBaseTxt = firstChildText(rep, "BaseURL");
          const repBase = repBaseTxt ? resolve(asBase, repBaseTxt) : asBase;
          const repId = rep.getAttribute("id") || "";
          const bandwidth = parseInt(rep.getAttribute("bandwidth") || "0", 10) || undefined;

          // Prefer Representation-level SegmentTemplate, fallback to ASet-level
          const st = rep.querySelector("SegmentTemplate") || asetST;
          const sl = rep.querySelector("SegmentList") || aset.querySelector("SegmentList");

          if (sl) {
            const init = sl.querySelector("Initialization");
            const initSrc = init?.getAttribute("sourceURL");
            if (initSrc) segments.push({ url: resolve(repBase, initSrc), type: "init", representationId: repId, bandwidth });
            const urls = Array.from(sl.getElementsByTagName("SegmentURL"));
            urls.forEach((su) => {
              const media = su.getAttribute("media") || su.getAttribute("mediaURL") || su.getAttribute("sourceURL");
              if (media) segments.push({ url: resolve(repBase, media), representationId: repId, bandwidth });
            });
            return;
          }

          if (st) {
            const media = st.getAttribute("media") || "";
            const initialization = st.getAttribute("initialization") || "";
            const timescale = parseInt(st.getAttribute("timescale") || "1", 10) || 1;
            const startNumber = parseInt(st.getAttribute("startNumber") || "1", 10) || 1;
            const duration = parseInt(st.getAttribute("duration") || "0", 10) || 0;
            const stl = st.querySelector("SegmentTimeline");

            if (initialization) {
              const initPath = substitute(initialization, { representationId: repId, bandwidth });
              segments.push({ url: resolve(repBase, initPath), type: "init", representationId: repId, bandwidth });
            }

            if (stl) {
              let t = 0;
              const S = Array.from(stl.getElementsByTagName("S"));
              S.forEach((s) => {
                const d = parseInt(s.getAttribute("d") || "0", 10);
                if (!d) return;
                const r = parseInt(s.getAttribute("r") || "0", 10);
                const tAttr = s.getAttribute("t");
                if (tAttr) t = parseInt(tAttr, 10);
                const repeat = isNaN(r) ? 0 : r;
                const count = repeat >= 0 ? repeat + 1 : 1; // negative r is unlimited, ignore
                for (let i = 0; i < count; i++) {
                  const timeVal = t;
                  const urlPath = substitute(media, { representationId: repId, bandwidth, time: timeVal });
                  segments.push({ url: resolve(repBase, urlPath), representationId: repId, bandwidth, time: timeVal });
                  t += d;
                }
              });
              return;
            }

            if (duration > 0) {
              const totalSeconds = periodDur || 0;
              const segDurSec = duration / timescale;
              let segCount = 0;
              if (totalSeconds > 0) segCount = Math.ceil(totalSeconds / segDurSec);
              // safe cap to avoid runaway in live/no duration manifests
              if (!segCount || segCount > 20000) segCount = 20000;
              for (let i = 0; i < segCount; i++) {
                const number = startNumber + i;
                const urlPath = substitute(media, { representationId: repId, bandwidth, number });
                segments.push({ url: resolve(repBase, urlPath), representationId: repId, bandwidth, number });
              }
              return;
            }
          }

          // Fallback: If Representation has a direct BaseURL pointing to a media file
          const mediaUrl = repBaseTxt && repBaseTxt.match(/\.(mp4|m4s|m4v|webm|mpg|ts)(\?|$)/i) ? repBase : null;
          if (mediaUrl) segments.push({ url: mediaUrl, representationId: repId, bandwidth });
        });
      });
    });

    return { ok: true, contentType: ct, count: segments.length, segments };
  } catch (err) {
    return { ok: false, error: err?.message || "mpd-fetch-failed" };
  }
}
