// AdBlockMaxxer — YouTube cosmetic / safety net + SponsorBlock (ISOLATED world).
//
// The MAIN-world stripper kills almost everything. This is the backstop:
//  - fast-forwards any ad that still slips through (rare unskippable formats),
//  - closes banner/overlay ads,
//  - dismisses the "Ad blockers violate YouTube's Terms" nag and resumes playback,
//  - SponsorBlock: auto-skips sponsor / intro / outro / self-promo segments using the
//    public sponsor.ajay.app crowd database,
//  - counts kills for the popup + stats.
//
// Honours the "youtubeSkip" + "sponsorblock" toggles, the per-site allowlist, and snooze.
// Network + the MAIN-world stripper stay on regardless (disable the whole extension to pause).

(function () {
  "use strict";

  let enabled = true;
  let allowed = true; // not allowlisted
  let snoozeUntil = 0;
  let perf = false; // performance/lite mode: slower polling
  let sbEnabled = true;
  let sbCategories = ["sponsor", "selfpromo", "interaction", "intro", "outro", "music_offtopic"];
  const KEY = "youtubeSkip";
  const HOST = location.hostname;
  const ENF_KEY = "__abm_yt_enforced"; // shared with youtube-main.js (MAIN world) via localStorage

  function refreshAllow(list) {
    const h = location.hostname;
    allowed = !((list || []).some((d) => h === d || h.endsWith("." + d)));
  }
  function active() { return enabled && allowed && Date.now() >= snoozeUntil; }

  // Server-side ad-enforcement auto-pause: youtube-main.js writes ENF_KEY when YouTube 403s
  // the media on this account. While paused we let ads play untouched (so the video loads at
  // all) and just surface an honest notice + report the state to the popup.
  function enforcedPaused() {
    try { return (parseInt(localStorage.getItem(ENF_KEY) || "0", 10) || 0) > Date.now(); }
    catch (e) { return false; }
  }

  chrome.storage.local.get([KEY, "allowlist", "snoozeUntil", "sponsorblock", "sbCategories", "perfMode"], (r) => {
    if (r && r[KEY] === false) enabled = false;
    snoozeUntil = (r && r.snoozeUntil) || 0;
    if (r && r.sponsorblock === false) sbEnabled = false;
    if (r && Array.isArray(r.sbCategories)) sbCategories = r.sbCategories;
    if (r && r.perfMode) perf = true;
    refreshAllow(r && r.allowlist);
    startLoop();
  });
  chrome.storage.onChanged.addListener((c) => {
    if (c[KEY]) enabled = c[KEY].newValue !== false;
    if (c.snoozeUntil) snoozeUntil = c.snoozeUntil.newValue || 0;
    if (c.sponsorblock) sbEnabled = c.sponsorblock.newValue !== false;
    if (c.sbCategories && Array.isArray(c.sbCategories.newValue)) sbCategories = c.sbCategories.newValue;
    if (c.allowlist) refreshAllow(c.allowlist.newValue);
    if (c.perfMode) { perf = !!c.perfMode.newValue; startLoop(); }
  });

  let lastBump = 0;
  function bump(category) {
    const now = Date.now();
    if (now - lastBump < 1500) return; // de-dupe rapid frames of the same event
    lastBump = now;
    try { chrome.runtime.sendMessage({ type: "kill", n: 1, category: category || "youtube", host: HOST }); } catch (e) {}
  }

  // The MAIN-world stripper can't reach the extension; it postMessages the counter + the
  // enforcement signals (it has no chrome.* access of its own).
  window.addEventListener("message", function (e) {
    if (e.source !== window || !e.data) return;
    const d = e.data;
    if (d.__abmYT === "ad") { if (active()) bump("youtube"); return; }
    if (d.__abmYTEnforced) {
      // detected a server-side 403; main world is about to reload us into paused mode
      reportEnforced();
      return;
    }
    if (d.__abmYTOk) {
      // a strip succeeded again — account is out of the enforcement bucket
      try { chrome.runtime.sendMessage({ type: "ytRecovered" }); } catch (x) {}
      try { sessionStorage.removeItem("__abm_yt_noticed"); } catch (x) {}
      return;
    }
  });

  function reportEnforced() {
    try {
      chrome.runtime.sendMessage({ type: "ytEnforced", until: parseInt(localStorage.getItem(ENF_KEY) || "0", 10) || 0 });
    } catch (x) {}
  }

  // Clear the pause + reload to re-attempt blocking (shared by the popup command and the
  // in-notice "Re-test now" button).
  function doRetest() {
    try { localStorage.removeItem(ENF_KEY); } catch (e) {}
    try { sessionStorage.removeItem("__abm_yt_noticed"); } catch (e) {}
    location.reload();
  }

  // Is the local companion reachable? Content scripts can't hit 127.0.0.1 directly (CSP/PNA/
  // Brave's localhost block), so we ask the background worker — which also pokes the companion
  // to launch on a failed probe. Resolves true/false, never rejects.
  function companionHealth() {
    return new Promise((resolve) => {
      try {
        chrome.runtime.sendMessage({ type: "companion", path: "health", port: 47823 }, (resp) => {
          if (chrome.runtime.lastError) { resolve(false); return; }
          resolve(!!(resp && resp.ok));
        });
      } catch (e) { resolve(false); }
    });
  }

  // Honest, dismissable on-page notice (once per browser session) so an enforced user knows
  // WHY ads are showing — instead of assuming the extension is broken.
  let noticeShown = false;
  function showEnforcedNotice() {
    if (noticeShown) return;
    if (window.__abmReclaimActive) return; // Reclaim is serving the video ad-free — no "paused" notice

    try { if (sessionStorage.getItem("__abm_yt_noticed")) return; } catch (e) {}
    noticeShown = true;
    try { sessionStorage.setItem("__abm_yt_noticed", "1"); } catch (e) {}
    const build = () => {
      if (!document.body) { requestAnimationFrame(build); return; }
      if (document.getElementById("abm-yt-enforced")) return;
      const bar = document.createElement("div");
      bar.id = "abm-yt-enforced";
      bar.style.cssText = [
        "position:fixed", "z-index:2147483647", "left:50%", "bottom:18px", "transform:translateX(-50%)",
        "max-width:540px", "width:calc(100% - 32px)", "box-sizing:border-box",
        "background:#1b1030", "color:#f3ecff", "border:1px solid #7a3cff", "border-radius:12px",
        "padding:13px 15px", "font:13px/1.45 Roboto,Arial,sans-serif", "box-shadow:0 8px 30px rgba(0,0,0,.55)",
      ].join(";");
      const btnCss = "border:none;border-radius:8px;cursor:pointer;font:600 12px/1 Roboto,Arial,sans-serif;padding:8px 12px";
      bar.innerHTML =
        '<div style="display:flex;gap:10px;align-items:flex-start">' +
          '<span style="font-size:18px;line-height:1">🛡️</span>' +
          '<div style="flex:1">' +
            '<b>AdBlock-Maxxer paused YouTube on this account.</b><br>' +
            'YouTube is enforcing ads <i>server-side</i> for your account — it refuses to play the ' +
            'video unless an ad runs, and no in-browser blocker can bypass that on its own. We paused ' +
            'YouTube so your videos keep playing.' +
            '<br><span id="abm-yt-status" style="opacity:.85">Our companion app (YouTube Reclaim) fixes ' +
            'exactly this — it fetches the video ad-free and plays it right here.</span>' +
            '<div style="display:flex;gap:8px;align-items:center;margin-top:10px">' +
              '<button id="abm-yt-retest" style="' + btnCss + ';background:#7a3cff;color:#fff">Re-test now</button>' +
              '<a id="abm-yt-dl" href="https://dl.maxxtopia.com/adblockmaxxer-companion.zip" target="_blank" ' +
                'rel="noopener" style="display:none;' + btnCss + ';background:rgba(255,255,255,.08);color:#cdbbff;' +
                'text-decoration:none">Get the companion</a>' +
            '</div>' +
          '</div>' +
          '<button id="abm-yt-x" aria-label="Dismiss" style="background:none;border:none;color:#cdbbff;' +
            'font-size:18px;cursor:pointer;line-height:1;padding:0 2px">×</button>' +
        '</div>';
      document.body.appendChild(bar);
      const close = () => { try { bar.remove(); } catch (e) {} };
      const x = bar.querySelector("#abm-yt-x");
      if (x) x.addEventListener("click", close);
      let autoClose = setTimeout(close, 30000);

      const statusEl = bar.querySelector("#abm-yt-status");
      const dlEl = bar.querySelector("#abm-yt-dl");
      const retestEl = bar.querySelector("#abm-yt-retest");

      // Tailor the copy to whether the companion is actually running right now.
      companionHealth().then((up) => {
        if (up) {
          if (statusEl) statusEl.textContent =
            "Companion is running — click Re-test now to play this video ad-free.";
          if (dlEl) dlEl.style.display = "none";
        } else {
          if (statusEl) statusEl.textContent =
            "Companion not detected. Start it (or grab it below), then click Re-test now.";
          if (dlEl) dlEl.style.display = "";
        }
      });

      if (retestEl) retestEl.addEventListener("click", () => {
        clearTimeout(autoClose); // user is engaging — don't auto-dismiss mid-action
        retestEl.disabled = true;
        retestEl.textContent = "Re-testing…";
        companionHealth().then((up) => {
          if (up) { doRetest(); return; } // clears the pause + reloads into a blocking attempt
          // Probe failed — the worker just poked the companion to launch; ask the user to wait.
          if (statusEl) statusEl.textContent =
            "Starting the companion… give it a few seconds, then click Re-test now again.";
          if (dlEl) dlEl.style.display = "";
          retestEl.disabled = false;
          retestEl.textContent = "Re-test now";
          autoClose = setTimeout(close, 30000);
        });
      });
    };
    build();
  }

  // Manual "Re-test now" from the popup: clear the pause + reload to re-attempt blocking.
  chrome.runtime.onMessage.addListener(function (msg, _s, sendResponse) {
    if (msg && msg.type === "cmd" && msg.cmd === "yt-retest") {
      try { sendResponse({ ok: true }); } catch (e) {}
      doRetest();
      return;
    }
  });

  // On any load where we're already in the paused state, keep the popup accurate + remind once.
  // Defer slightly so Reclaim (document_idle) can claim the page first — if it's serving the
  // video ad-free, there's nothing to apologise for and we skip the notice entirely.
  setTimeout(() => {
    if (enforcedPaused() && !window.__abmReclaimActive) { reportEnforced(); showEnforcedNotice(); }
  }, 1800);

  // ---- ad backstop ---------------------------------------------------------
  function killNag() {
    const enf = document.querySelector(
      "ytd-enforcement-message-view-model, ytd-enforcement-message-view-model-renderer, .ytd-enforcement-message-view-model"
    );
    if (!enf) return false;
    // Tear down the whole modal stack: the view-model, its hosting dialog/popup, and the
    // scroll-locking backdrop YouTube drops behind it.
    const dialog =
      enf.closest("tp-yt-paper-dialog, ytd-popup-container, .ytd-popup-container") ||
      enf.parentElement;
    if (dialog) dialog.remove();
    document
      .querySelectorAll("tp-yt-iron-overlay-backdrop, .ytd-popup-container[opened]")
      .forEach((b) => b.remove());
    // YouTube freezes scroll + dims the page while the wall is up — undo it.
    document.body.style.overflow = "";
    document.body.style.position = "";
    document.documentElement.style.overflow = "";
    // It also pauses the video and leaves it paused; resume it.
    const v = document.querySelector("video");
    if (v && v.paused) v.play().catch(() => {});
    bump("youtube");
    return true;
  }

  // The wall is a hard, page-blocking modal, so react the instant it's inserted rather
  // than waiting up to 300ms for the next adTick — that gap is long enough for YouTube
  // to pause the player and lock scroll, which is the "stuck even after refresh" feel.
  function watchForNag() {
    const start = () => {
      if (!document.body) return requestAnimationFrame(start);
      const nagMo = new MutationObserver(() => { if (active()) killNag(); });
      nagMo.observe(document.body, { childList: true, subtree: true });
      killNag(); // catch one already on the page (e.g. injected into a stale tab)
    };
    start();
  }
  watchForNag();

  // Track whether WE are mid-ad so we only fast-forward once (repeated seeks cause
  // YouTube's "experiencing interruptions" rebuffer) and so we can restore the
  // real video's speed/mute afterwards — the player reuses one <video> for ad→content,
  // so leaving it at 16x/muted is what strands the next playlist song.
  let inAd = false;
  let savedRate = 1;
  let savedMuted = false;

  function adTick() {
    if (!active()) return;
    killNag();
    // Reclaim is playing the video through our own player (fed by the local companion); the
    // native player is hidden + suppressed, so there's nothing here to fast-forward. Stand down
    // (we still ran killNag above, in case YouTube tries to throw the wall up regardless).
    if (window.__abmReclaimActive) return;
    // While the account is under server-side enforcement, DON'T touch ads — let them play so
    // the server keeps serving video bytes. Fast-forwarding/skipping here would only re-provoke
    // the 403. (SponsorBlock stays on: those are creator segments, unrelated to Google's ads.)
    if (enforcedPaused()) return;

    const player = document.querySelector(".html5-video-player");
    const showing = player && player.classList.contains("ad-showing");
    const video = document.querySelector("video.html5-main-video") || document.querySelector("video");

    const skip = document.querySelector(
      ".ytp-ad-skip-button, .ytp-ad-skip-button-modern, .ytp-skip-ad-button"
    );
    if (skip) { skip.click(); bump("youtube"); }

    document
      .querySelectorAll(".ytp-ad-overlay-close-button, .ytp-ad-overlay-close-container .ytp-ad-overlay-close-button")
      .forEach((b) => b.click());

    if (showing && video) {
      if (!inAd) {
        // entering an ad: snapshot the real settings, then blow past the ad ONCE
        inAd = true;
        savedRate = video.playbackRate || 1;
        savedMuted = video.muted;
        video.muted = true;
        video.playbackRate = 16;
        if (isFinite(video.duration) && video.duration > 0) {
          try { video.currentTime = video.duration; } catch (e) {}
        }
        bump("youtube");
      }
    } else if (inAd) {
      // ad finished (or playlist advanced): hand the real video back exactly as it was
      inAd = false;
      if (video) {
        try { if (video.playbackRate === 16) video.playbackRate = savedRate || 1; } catch (e) {}
        try { video.muted = savedMuted; } catch (e) {}
      }
    }
  }

  // ---- SponsorBlock --------------------------------------------------------
  let sbVideoId = null;
  let sbSegments = [];   // [{start, end, category, skipped}]
  let sbFetching = false;

  function currentVideoId() {
    try {
      const u = new URL(location.href);
      if (u.pathname === "/watch") return u.searchParams.get("v");
      const m = u.pathname.match(/^\/(?:shorts|embed)\/([\w-]{6,})/);
      if (m) return m[1];
    } catch (e) {}
    return null;
  }

  function fetchSegments(id) {
    if (sbFetching) return;
    sbFetching = true;
    sbSegments = [];
    const cats = encodeURIComponent(JSON.stringify(sbCategories));
    const url = "https://sponsor.ajay.app/api/skipSegments?videoID=" + encodeURIComponent(id) + "&categories=" + cats;
    fetch(url, { cache: "no-store" })
      .then((res) => (res.ok ? res.json() : []))
      .then((data) => {
        if (!Array.isArray(data)) return;
        sbSegments = data
          .filter((s) => s && Array.isArray(s.segment) && s.segment.length === 2)
          .map((s) => ({ start: s.segment[0], end: s.segment[1], category: s.category, skipped: false }));
      })
      .catch(() => {})
      .finally(() => { sbFetching = false; });
  }

  function sbTick() {
    if (!active() || !sbEnabled) return;
    if (window.__abmReclaimActive) return; // SponsorBlock runs on the native player; Reclaim bypasses it
    const id = currentVideoId();
    if (id && id !== sbVideoId) { sbVideoId = id; fetchSegments(id); return; }
    if (!sbSegments.length) return;
    const video = document.querySelector("video.html5-main-video") || document.querySelector("video");
    if (!video || video.paused || !isFinite(video.currentTime)) return;
    const t = video.currentTime;
    for (const seg of sbSegments) {
      if (seg.skipped) continue;
      // skip when we're inside the segment (small guard so we don't fight the player at the edge)
      if (t >= seg.start && t < seg.end - 0.15) {
        try { video.currentTime = seg.end; seg.skipped = true; bump("sponsorblock"); } catch (e) {}
        break;
      }
      // re-arm if the user scrubbed back before the segment
      if (t < seg.start - 1) seg.skipped = false;
    }
  }

  // ---- run -----------------------------------------------------------------
  function tick() { adTick(); sbTick(); }
  // Restartable so performance mode can relax the cadence (300ms → 700ms) without a reload.
  let loopIv = null;
  function startLoop() {
    if (loopIv) clearInterval(loopIv);
    loopIv = setInterval(tick, perf ? 700 : 300);
  }
  startLoop();
  // Coalesce DOM churn: the 300ms interval owns responsiveness; the observer only needs
  // to catch ad UI that appears between ticks, so we throttle it rather than run adTick
  // on every one of YouTube's many mutations.
  let moScheduled = false;
  const mo = new MutationObserver(() => {
    if (moScheduled) return;
    moScheduled = true;
    setTimeout(() => { moScheduled = false; adTick(); }, 150);
  });
  function attach() {
    if (document.body) mo.observe(document.body, { childList: true, subtree: true });
    else requestAnimationFrame(attach);
  }
  attach();
})();
