/* Email-capture popup — grows the newsletter list from non-subscribed visitors.
   Mounted globally in App.jsx as <EmailCapturePopup route={route} user={user} />.

   Incentive: the weekly Deep Dive newsletter (reuses NewsletterSignup for the
   subscribers write + duplicate handling; source="popup").

   Triggers (FIRST to fire wins, then suppressed): after 3 album views · desktop
   exit-intent · 50% scroll on /deep-dives · 2 viewports of scroll on home ·
   45s dwell on any page except home.

   Suppression: never for logged-in accounts (already recipients) or after a
   successful subscribe (localStorage), once per session, a 30-day cooldown
   after any dismiss, and a quiet window after a record is added (below).
   Emits GA4 events popup_shown / popup_submit / popup_dismiss /
   popup_held_after_add.

   Mobile = bottom slide-up (no full-screen interstitial → protects SEO);
   desktop = centered 440px modal. Styles are injected once, below. */

(function () {
  const { useState, useEffect, useRef } = React;

  const LS_SUB = "bbr:nlpop:sub";        // subscribed (permanent)
  const LS_DISMISS = "bbr:nlpop:dismiss"; // last dismiss timestamp
  const LS_ALBUMS = "bbr:nlpop:albums";   // cumulative album-page views
  const SS_SHOWN = "bbr:nlpop:shown";     // shown this session
  const SS_ELIGIBLE = "bbr:nlpop:elig";   // counted as an eligible session (telemetry only)
  const DISMISS_DAYS = 30, ALBUM_THRESHOLD = 3, DWELL_MS = 45000, SCROLL_PCT = 0.5;
  // Home's scroll trigger is measured in viewports, not in page percentage. See
  // the trigger block below for why, and for the measurements behind the 2.
  const HOME_SCROLL_VIEWPORTS = 2;

  // ---- quiet window after a record lands ------------------------------------
  // Adding a record is the strongest engagement signal the site has, and the
  // worst moment to interrupt. The celebration card (App.jsx: RecordAddedCheer)
  // owns the screen for 3.4s, and this overlay is z-index 100000 against its
  // 10001 — so a trigger firing in that window does not merely compete with the
  // moment, it ERASES it, and the card burns its whole life behind the scrim.
  // Observed on production: a dwell timer landed ~2s after an add.
  //
  // Firing the instant the card clears is no better. It reads as the ask being
  // the price of adding, on the one screen that was pure reward. So the window
  // runs well past the animation: long enough to cover the moment and the beat
  // after it (reading the new total, watching the row flip to "In collection"),
  // short enough that the trigger is delayed rather than lost.
  //
  // A trigger landing inside the window is RE-ARMED for the end of it, not
  // dropped. Dropping would silently spend the single dwell timer a route ever
  // gets: show() sets no session flag when it declines, so the popup would
  // simply never appear again on that route.
  //
  // Module scope, not component state, and listening from load: the add can
  // happen on a route where the popup isn't eligible and this component never
  // wired a thing, and the window must still be in force when they navigate.
  const QUIET_AFTER_ADD_MS = 20000;
  let lastAddAt = 0;
  try {
    window.addEventListener("bbr:record-added", () => { lastAddAt = Date.now(); });
  } catch (e) {}
  // Milliseconds still owed to the last add, 0 if the window has passed.
  const quietRemaining = () => (lastAddAt ? Math.max(0, (lastAddAt + QUIET_AFTER_ADD_MS) - Date.now()) : 0);

  // Routes this must never appear on, whatever the trigger.
  //
  // The dwell trigger fires on every route except home, which meant a record shop
  // owner opening their own private dashboard got a consumer newsletter modal
  // ("one classic album in your inbox, every morning") over the top of their
  // numbers, 45 seconds in. Wrong audience, wrong offer, and it covers the thing
  // they came to read. /partner and /partner-admin are B2B surfaces reached only
  // by a private link — there is no acquisition to do on either.
  const NEVER_ROUTES = ["partner", "partner-admin", "valuations-admin"];

  const lsGet = (k) => { try { return localStorage.getItem(k); } catch (e) { return null; } };
  const lsSet = (k, v) => { try { localStorage.setItem(k, v); } catch (e) {} };
  const ssGet = (k) => { try { return sessionStorage.getItem(k); } catch (e) { return null; } };
  const ssSet = (k, v) => { try { sessionStorage.setItem(k, v); } catch (e) {} };
  const track = (name, params) => { try { if (window.gtag) window.gtag("event", name, params || {}); } catch (e) {} };

  function EmailCapturePopup({ route = "home", user = null }) {
    const [open, setOpen] = useState(false);
    const doneRef = useRef(false);   // true once a subscribe succeeds (skip dismiss bookkeeping)
    const openRef = useRef(false);
    openRef.current = open;

    function eligible() {
      if (NEVER_ROUTES.indexOf(route) >= 0) return false;   // see NEVER_ROUTES
      if (user) return false;                       // accounts are already recipients
      if (lsGet(LS_SUB)) return false;              // already subscribed via popup
      if (ssGet(SS_SHOWN)) return false;            // once per session
      const d = parseInt(lsGet(LS_DISMISS) || "0", 10);
      if (d && (Date.now() - d) < DISMISS_DAYS * 864e5) return false; // cooldown
      return true;
    }

    // Pending re-arm from a trigger that landed inside the quiet window.
    const retryRef = useRef(0);

    function show(trigger) {
      if (openRef.current || !eligible()) return;
      const quiet = quietRemaining();
      if (quiet > 0) {
        // Hold this trigger, don't spend it. Re-checks eligibility on the way
        // back through, and re-arms again if they added another record since.
        clearTimeout(retryRef.current);
        retryRef.current = setTimeout(() => show(trigger), quiet + 50);
        track("popup_held_after_add", { trigger: trigger, route: route });
        return;
      }
      ssSet(SS_SHOWN, "1");
      setOpen(true);
      track("popup_shown", { trigger: trigger, route: route });
    }

    // A held trigger outlives the listeners that raised it, so it needs its own
    // unmount cleanup — the trigger effect's teardown runs on every route change.
    useEffect(() => () => clearTimeout(retryRef.current), []);

    // Count album/deep-dive page views across client-side navigation (fires on
    // each entry to a reading route). Only bothers while still eligible.
    useEffect(() => {
      if (route === "deep-dive" && !user && !lsGet(LS_SUB)) {
        const n = (parseInt(lsGet(LS_ALBUMS) || "0", 10) || 0) + 1;
        lsSet(LS_ALBUMS, String(n));
      }
    }, [route]);

    // Wire the triggers. Re-runs when route/user change; cleans up its listeners.
    useEffect(() => {
      if (!eligible()) return;

      // Count this session as one the popup was ALLOWED to appear in, once, before
      // any trigger is wired.
      //
      // Without it the panel cannot tell the two reasons for a low impression
      // count apart, and they need opposite fixes: sessions that were never
      // eligible (logged in, already subscribed, inside the 30-day dismiss
      // cooldown) versus sessions that were eligible and no trigger ever fired.
      // 16 impressions against 271 weekly sessions is unreadable on its own, and
      // at that rate the panel's own 200-impression floor is 12 weeks away, so
      // waiting for the conversion rate to become significant is not a plan.
      //
      // Its own session key, not SS_SHOWN: this effect re-runs on every route and
      // user change, so a reader moving between three pages would otherwise count
      // three times. SS_SHOWN can't do the job because it is only set once the
      // popup actually appears, which is the very thing being measured.
      //
      // An event name rather than a param on popup_shown, for the same reason the
      // add funnel uses names: a GA4 custom dimension has to be registered in the
      // property first and an unregistered one fails the whole request.
      if (!ssGet(SS_ELIGIBLE)) { ssSet(SS_ELIGIBLE, "1"); track("popup_eligible", { route: route }); }

      // 1) Album-view threshold (already accumulated).
      if ((parseInt(lsGet(LS_ALBUMS) || "0", 10) || 0) >= ALBUM_THRESHOLD) { show("albums"); return; }

      const cleanup = [];
      // 2) Dwell — every route EXCEPT home. On the homepage this fired straight
      // over the Album of the Day card: the one surface with something to say,
      // covered by an ask, 45 seconds in. That was half the reason the popup was
      // unmounted on 3 Aug, so it does not come back with it. A reader who wants
      // the daily record can subscribe from the card itself. To restore the old
      // behaviour, drop the route check.
      if (route !== "home") {
        const timer = setTimeout(() => show("dwell"), DWELL_MS);
        cleanup.push(() => clearTimeout(timer));
      }

      // 3) Desktop exit-intent (cursor leaves via the top of the viewport).
      const onMouseOut = (e) => {
        if (!e.relatedTarget && e.clientY <= 0 && window.innerWidth > 768) show("exit");
      };
      document.addEventListener("mouseout", onMouseOut);
      cleanup.push(() => document.removeEventListener("mouseout", onMouseOut));

      // 4) Scroll depth — two routes, two rules, for two different reasons.
      //
      // /deep-dives keeps the 50%-of-page rule that /list had: it is one long
      // A-to-Z archive page, so half of it is a genuine browse. (/list itself
      // 301s to /deep-dives since the Hundred was sunset on 3 Oct 2026.)
      //
      // home is new, and it is the ONLY trigger home has. Dwell is deliberately
      // off there (see 2 above), exit-intent needs a cursor and a >768px window,
      // and the album threshold needs three prior reading-route views held in
      // localStorage — which nearly nobody accumulates, because the site gets
      // almost no repeat visits (~0.95 visit-days per weekly user). So a phone
      // landing on / could fire NO trigger at all. Verified on a clean session:
      // 45s+ on mobile home produced no popup_shown, no overlay and no session
      // flag, while the same wait on /album fired dwell correctly. That is the
      // popup's largest reachability gap, and the fix has to respect the reason
      // dwell was removed rather than quietly undo it.
      //
      // Why viewports rather than a percentage: the objection that took dwell off
      // home was that it COVERED the Album of the Day card, so the threshold has
      // to be relative to where that card sits. The homepage is ~10-12k px and
      // its section offsets differ per breakpoint, so one percentage lands in a
      // different place on each. Measured, the section after the card starts at
      // y=906 on 1024x768 and y=1354 on 375x812, so the card is clear of the
      // viewport once scrollY passes 138 and 542 respectively. Two viewports
      // (1536 / 1624) clears it with room on both and scales with the device
      // instead of with however long the page happens to be.
      if (route === "deep-dives" || route === "home") {
        const onScroll = () => {
          const el = document.documentElement;
          const sc = el.scrollTop || document.body.scrollTop || 0;
          if (route === "home") {
            if (sc >= HOME_SCROLL_VIEWPORTS * (el.clientHeight || 1)) show("scroll");
            return;
          }
          const max = (el.scrollHeight - el.clientHeight) || 1;
          if (sc / max >= SCROLL_PCT) show("scroll");
        };
        window.addEventListener("scroll", onScroll, { passive: true });
        cleanup.push(() => window.removeEventListener("scroll", onScroll));
      }

      return () => cleanup.forEach((fn) => fn());
    }, [route, user]);

    // Esc to close while open.
    useEffect(() => {
      if (!open) return;
      const onKey = (e) => { if (e.key === "Escape") dismiss(); };
      document.addEventListener("keydown", onKey);
      return () => document.removeEventListener("keydown", onKey);
    }, [open]);

    // Focus, because this claims aria-modal="true". A dialog that says it is
    // modal and then leaves focus behind it is worse than one that never
    // claimed to be: a screen reader goes on reading the page underneath, and a
    // keyboard user has to tab through the whole document to reach the field.
    // The CARD takes focus, not the input — auto-focusing the input throws up
    // the keyboard over a mobile slide-up. Focus returns where it came from on
    // close, so dismissing does not dump the reader back at the top of the page.
    const cardRef = useRef(null);
    const returnFocusRef = useRef(null);
    useEffect(() => {
      if (!open) return;
      returnFocusRef.current = document.activeElement;
      const t = setTimeout(() => { try { cardRef.current && cardRef.current.focus(); } catch (e) {} }, 0);
      return () => {
        clearTimeout(t);
        const el = returnFocusRef.current;
        try { el && el.focus && document.contains(el) && el.focus(); } catch (e) {}
      };
    }, [open]);

    function dismiss() {
      if (!doneRef.current) {
        lsSet(LS_DISMISS, String(Date.now()));
        track("popup_dismiss", { route: route });
      }
      setOpen(false);
    }

    function handleDone(state) {
      doneRef.current = true;
      lsSet(LS_SUB, "1");
      track("popup_submit", { route: route, state: state });
      setTimeout(() => setOpen(false), 2000); // let them read the confirmation
    }

    if (!open) return null;
    const readingCtx = route === "deep-dive";
    return (
      <div
        className="nlpop-overlay"
        role="dialog"
        aria-modal="true"
        aria-label="Subscribe to The Lead-In"
        onClick={(e) => { if (e.target.classList.contains("nlpop-overlay")) dismiss(); }}
      >
        <div className="nlpop-card" ref={cardRef} tabIndex={-1}>
          <button className="nlpop-close" onClick={dismiss} aria-label="Close">×</button>
          <div className="nlpop-kicker">The Lead-In</div>
          <div className="nlpop-title">
            {readingCtx
              ? "Enjoying the read? Get a record every morning."
              : "One classic album in your inbox, every morning."}
          </div>
          <div className="nlpop-sub">
            A record a day from the archive, and a new deep dive written up in full every Wednesday. No filler, no spam.
          </div>
          {window.NewsletterSignup && (
            <NewsletterSignup variant="feature" source="popup" hideCopy onDone={handleDone} />
          )}
          <button className="nlpop-decline" onClick={dismiss}>Maybe later</button>
          <div className="nlpop-legal">
            {/* "a week" was the old cadence. The shared NewsletterSignup this popup
                wraps already says "One email a day"; the popup's own footer line
                sat directly beneath it saying something different. */}
            One email a day · unsubscribe anytime. See our <a href="/legal">privacy terms</a>.
          </div>
        </div>
      </div>
    );
  }

  // ---- styles (injected once) ----------------------------------------------
  if (!document.getElementById("nlpop-styles")) {
    const s = document.createElement("style");
    s.id = "nlpop-styles";
    s.textContent = `
      .nlpop-overlay{position:fixed;inset:0;z-index:100000;background:rgba(23,19,15,.55);
        display:flex;align-items:center;justify-content:center;padding:20px;
        animation:nlpopFade .2s ease both;}
      .nlpop-card{position:relative;width:100%;max-width:440px;background:#fffdf8;
        border:1px solid #d8d0bd;border-radius:14px;box-shadow:0 24px 60px rgba(23,19,15,.28);
        padding:30px 26px 22px;animation:nlpopPop .24s cubic-bezier(.2,.8,.3,1) both;}
      .nlpop-close{position:absolute;top:8px;right:10px;width:36px;height:36px;border:0;
        background:transparent;color:#8a8072;font:400 26px/1 Helvetica,Arial,sans-serif;
        cursor:pointer;border-radius:8px;}
      .nlpop-close:hover{color:#17130f;background:#f3ede1;}
      .nlpop-kicker{font:700 11px Helvetica,Arial,sans-serif;letter-spacing:.14em;
        text-transform:uppercase;color:#cf4827;margin-bottom:8px;}
      .nlpop-title{font:700 23px/1.2 Georgia,serif;color:#17130f;margin-bottom:8px;}
      .nlpop-sub{font:400 14px/1.5 Helvetica,Arial,sans-serif;color:#3a322a;margin-bottom:16px;}
      .nlpop-card .nl-copy{display:none;}
      .nlpop-decline{display:block;margin:12px auto 0;border:0;background:transparent;
        color:#8a8072;font:400 13px Helvetica,Arial,sans-serif;text-decoration:underline;
        cursor:pointer;}
      .nlpop-decline:hover{color:#17130f;}
      .nlpop-legal{margin-top:12px;text-align:center;font:400 11px Helvetica,Arial,sans-serif;color:#a89e8c;}
      .nlpop-legal a{color:#8a8072;}
      @keyframes nlpopFade{from{opacity:0}to{opacity:1}}
      @keyframes nlpopPop{from{opacity:0;transform:translateY(8px) scale(.98)}to{opacity:1;transform:none}}
      @keyframes nlpopUp{from{transform:translateY(100%)}to{transform:none}}
      @media (max-width:600px){
        .nlpop-overlay{align-items:flex-end;padding:0;}
        .nlpop-card{max-width:100%;border-radius:16px 16px 0 0;padding:26px 20px calc(22px + env(safe-area-inset-bottom));
          animation:nlpopUp .28s cubic-bezier(.2,.8,.3,1) both;}
      }
      @media (prefers-reduced-motion:reduce){
        .nlpop-overlay,.nlpop-card{animation:none;}
      }
    `;
    document.head.appendChild(s);
  }

  window.EmailCapturePopup = EmailCapturePopup;
})();
