// ─────────────────────────────────────────────────────────────────────────────
// Sayso — single source of truth for a Vimeo film embed.
// Used by the grid (background previews), the reels player and the fullscreen
// viewer. SDK-ONLY: playback is driven through window.Vimeo.Player and we NEVER
// send raw postMessage strings — mixing the two on one iframe is what stalled
// mobile playback.
//
// POSTER-FIRST LOADING ARCHITECTURE
//   • Every surface renders its local poster immediately. The Vimeo player is
//     built into a wrapper <div> (SDK `new Vimeo.Player(div, options)`), so the
//     iframe is owned by the SDK and player.destroy() fully removes it.
//   • The poster stays visible until the player is ready AND has actually
//     started ("playing"), then we crossfade poster→video (≈200ms). No black
//     frame, no flashing iframe.
//   • A global gate (window.saysoPreviewGate) caps BACKGROUND grid previews at
//     TWO active players at once. Opening a film puts the gate in "foreground"
//     mode, which suspends/destroys every background preview and gives the
//     opened player priority.
//   • saveData / slow (2g) / prefers-reduced-motion → background autoplay is
//     disabled entirely (poster only); video loads only on an explicit open.
//   • Fail / blocked / offline / not-ready-within-8s → keep the poster; a small
//     retry control is offered on foreground surfaces (the grid is poster by
//     design, so it stays silent there).
//   • Stale async callbacks are guarded by a per-attach `cancelled` flag and a
//     film-id check, so a resolving promise from a previous film can never flip
//     the state of a newly selected one.
// ─────────────────────────────────────────────────────────────────────────────
(function () {
  function parts(url) {
    if (!url || url.indexOf("vimeo") < 0) return null;
    const m = url.match(/vimeo\.com\/(?:video\/)?(\d+)(?:\/([a-zA-Z0-9]+))?/);
    return m ? { id: m[1], hash: m[2] || null } : null;
  }
  window.saysoVimeoParts = parts;

  // ── lightweight adjacent-film preload (Phase 1) ────────────────────────────
  // Warms the NEXT film so its player starts faster, WITHOUT a second/hidden
  // player instance: we prefetch the Vimeo player document (and the poster) via
  // a low-priority <link rel="prefetch">. This never blocks or competes with the
  // current playback — prefetch is best-effort and browser-throttled. Deduped
  // per film id so repeated Next presses don't refetch.
  window.saysoPrefetchFilm = function (film) {
    if (!film) return;
    try { if (film.img) { const im = new Image(); im.src = film.img; } } catch (e) {}
    const url = film.video; if (!url) return;
    const vp = parts(url);
    const key = vp ? ("v" + vp.id) : url;
    window.__saysoPrefetched = window.__saysoPrefetched || {};
    if (window.__saysoPrefetched[key]) return;
    window.__saysoPrefetched[key] = true;
    let href;
    if (vp) {
      href = "https://player.vimeo.com/video/" + vp.id +
        "?autoplay=0&muted=1&title=0&byline=0&portrait=0&controls=0&playsinline=1" +
        (vp.hash ? "&h=" + vp.hash : "");
    } else { href = url; }
    try {
      const l = document.createElement("link");
      l.rel = "prefetch"; l.href = href; if (vp) l.crossOrigin = "anonymous";
      (document.head || document.documentElement).appendChild(l);
    } catch (e) {}
  };

  // ── global background-preview concurrency gate (max 2) ─────────────────────
  if (!window.saysoPreviewGate) {
    var MAX = 2;
    var active = [];      // keys currently holding a slot (FIFO)
    var subs = [];        // {key, fn} recheck callbacks
    var foreground = false;
    function notify() { subs.slice().forEach(function (s) { try { s.fn(); } catch (e) {} }); }
    window.saysoPreviewGate = {
      max: MAX,
      request: function (key) {
        if (foreground) return false;
        if (active.indexOf(key) >= 0) return true;
        if (active.length < MAX) { active.push(key); return true; }
        return false;
      },
      release: function (key) {
        var i = active.indexOf(key);
        if (i >= 0) { active.splice(i, 1); notify(); }   // a slot freed → wake waiters
      },
      // Opening a film → true: vacate all background slots and deny new ones so
      // the foreground player is the only thing playing.
      setForeground: function (v) {
        v = !!v;
        if (v === foreground) return;
        foreground = v;
        if (v) active = [];
        notify();
      },
      isForeground: function () { return foreground; },
      activeCount: function () { return active.length; },
      subscribe: function (key, fn) {
        var s = { key: key, fn: fn }; subs.push(s);
        return function () { var i = subs.indexOf(s); if (i >= 0) subs.splice(i, 1); };
      }
    };
  }

  // saveData / slow connection detection (re-evaluated per mount)
  window.saysoLowData = function () {
    try {
      var c = navigator.connection || navigator.webkitConnection || navigator.mozConnection;
      if (c) {
        if (c.saveData) return true;
        if (c.effectiveType && /(^|\b)(slow-2g|2g)$/.test(c.effectiveType)) return true;
      }
    } catch (e) {}
    return false;
  };

  // one-time styles for reveal crossfade, retry control and loading dot
  if (typeof document !== "undefined" && !document.getElementById("sayso-vimeo-css")) {
    var st = document.createElement("style");
    st.id = "sayso-vimeo-css";
    st.textContent =
      ".sv-fade{transition:opacity .2s ease}" +
      "@media (prefers-reduced-motion: reduce){.sv-fade{transition:none}}" +
      ".sv-retry{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);z-index:5;" +
      "display:flex;align-items:center;gap:7px;padding:9px 14px;border:0;border-radius:999px;cursor:pointer;" +
      "background:rgba(0,0,0,0.55);backdrop-filter:blur(6px);color:#fff;font:600 13px/1 var(--font-sans,sans-serif);" +
      "box-shadow:0 0 0 1px rgba(255,255,255,0.16)}" +
      ".sv-retry:hover{background:rgba(0,0,0,0.7)}" +
      ".sv-dot{position:absolute;left:50%;top:50%;width:26px;height:26px;margin:-13px 0 0 -13px;z-index:4;" +
      "border-radius:999px;border:2px solid rgba(255,255,255,0.28);border-top-color:rgba(255,255,255,0.95);" +
      "animation:svSpin .7s linear infinite,svTint 1.8s ease-in-out infinite;pointer-events:none}" +
      "@keyframes svSpin{to{transform:rotate(360deg)}}" +
      "@keyframes svTint{0%,100%{border-top-color:rgba(255,255,255,0.95)}50%{border-top-color:#FF238C}}" +
      "@media (prefers-reduced-motion: reduce){.sv-dot{animation:none;opacity:.6}}";
    (document.head || document.documentElement).appendChild(st);
  }
})();

// props:
//   video       — Vimeo url
//   poster      — fallback image (shown before playing and if the embed fails)
//   alt         — media title
//   className   — applied to the poster AND the player wrapper (positioning)
//   posterStyle — extra inline style for the poster (per-film object-position)
//   muted       — desired mute state (default true). Parent flips it on tap.
//   paused      — desired paused state (default false)
//   active      — whether this film should attach a player. Grid: in-view.
//   background  — grid background mode (adds background:1 + goes through the gate)
//   onFail      — called once when the embed is deemed unplayable
//   onReady     — called with the Vimeo.Player once it is ready
//   onSoundBlocked — UNMUTED autoplay was rejected and we recovered by playing
//                 muted; the parent flips its UI back to muted (iOS safety net)
function VimeoFilm({ video, poster, alt, className, posterStyle, muted = true, paused = false, active = true, background = false, loader, onFail, onReady, onStarted, onSoundBlocked }) {
  const vp = window.saysoVimeoParts(video);
  const holderRef = React.useRef(null);
  const playerRef = React.useRef(null);
  const startedRef = React.useRef(false);
  const mutedRef = React.useRef(muted);
  const pausedRef = React.useRef(paused);
  mutedRef.current = muted;
  pausedRef.current = paused;
  const keyRef = React.useRef("sv-" + Math.random().toString(36).slice(2));
  const lowData = React.useRef(window.saysoLowData()).current;

  const [failed, setFailed] = React.useState(false);
  const [revealed, setRevealed] = React.useState(false);
  const [slot, setSlot] = React.useState(!background); // foreground never needs a slot
  const [showDot, setShowDot] = React.useState(false);

  React.useEffect(() => { setFailed(false); setRevealed(false); startedRef.current = false; }, [vp && vp.id]);

  // ── background concurrency: request/release a slot from the global gate ────
  React.useEffect(() => {
    if (!background || !vp) return;
    const key = keyRef.current;
    let mounted = true;
    const recheck = () => {
      if (!mounted) return;
      const want = active && !failed && !lowData;
      if (want) setSlot(window.saysoPreviewGate.request(key));
      else { window.saysoPreviewGate.release(key); setSlot(false); }
    };
    const unsub = window.saysoPreviewGate.subscribe(key, recheck);
    recheck();
    return () => { mounted = false; unsub(); window.saysoPreviewGate.release(key); };
  }, [background, active, failed, vp && vp.id, lowData]);

  // whether we may build a player right now
  const shouldAttach = !!vp && !failed && active &&
    (background ? (slot && !lowData) : true);

  // ── build (and fully tear down) the player ─────────────────────────────────
  React.useEffect(() => {
    if (!shouldAttach) return;
    const el = holderRef.current;
    if (!el) return;
    if (typeof navigator !== "undefined" && navigator.onLine === false) { setFailed(true); onFail && onFail(); return; }

    let cancelled = false, waitTimer = null, readyTimer = null, dotTimer = null;
    const myId = vp.id;
    const fail = () => { if (!cancelled) { setFailed(true); setRevealed(false); onFail && onFail(); } };
    // reveal fires when a real frame is rendered (Vimeo 'playing') — the strong
    // "transition-ready" signal used by the viewer's crossfade preload.
    // Sound is restored ONLY after real playback begins, then VERIFIED with
    // getMuted() — iOS often "accepts" a programmatic unmute outside a gesture
    // and then either keeps audio muted or pauses the video. On refusal we keep
    // playing muted and tell the parent, so the UI honestly shows sound-off
    // (the "Tap for sound" CTA reappears) instead of a silent film with a
    // sound-on icon. One attempt per attach; a user tap goes through [muted].
    let soundTried = false;
    const applySound = (player) => {
      if (cancelled || soundTried || mutedRef.current) return;
      soundTried = true;
      Promise.resolve()
        .then(() => player.setMuted(false))
        .then(() => player.setVolume(1))
        .then(() => (player.getMuted ? player.getMuted() : false))
        .then((m) => { if (m) throw new Error("unmute-blocked"); return player.getPaused ? player.getPaused() : false; })
        .then((pz) => { if (pz && !pausedRef.current) { try { const r = player.play(); if (r && r.catch) r.catch(() => {}); } catch (e) {} } })
        .catch(() => {
          if (cancelled) return;
          try { const m = player.setMuted(true); if (m && m.catch) m.catch(() => {}); } catch (e) {}
          try { const r = player.play(); if (r && r.catch) r.catch(() => {}); } catch (e) {}
          if (onSoundBlocked) onSoundBlocked();
        });
    };
    const reveal = () => {
      if (cancelled) return;
      clearTimeout(readyTimer); clearTimeout(dotTimer); setShowDot(false); setRevealed(true);
      if (!startedRef.current) { startedRef.current = true; if (onStarted) onStarted(); }
      const p = playerRef.current; if (p) applySound(p);
    };

    // subtle loading indicator only after a short delay, foreground only.
    // Viewer transitions (loader="logo") show it sooner — it IS the transition.
    if (!background) dotTimer = setTimeout(() => { if (!cancelled) setShowDot(true); }, loader === "logo" ? 200 : 450);

    // Play, and if UNMUTED autoplay is rejected (common on iOS when the request
    // doesn't originate from the tap), fall back to muted playback immediately
    // and tell the parent — instead of stalling on the poster until the 8s timeout.
    function playSafely(player) {
      if (pausedRef.current) { try { const r = player.pause(); if (r && r.catch) r.catch(() => {}); } catch (e) {} return; }
      let pr; try { pr = player.play(); } catch (e) { return; }
      if (pr && pr.catch) pr.catch(() => {
        if (cancelled) return;
        const wantedSound = !mutedRef.current;
        try { const m = player.setMuted(true); if (m && m.catch) m.catch(() => {}); } catch (e) {}
        try { const r2 = player.play(); if (r2 && r2.catch) r2.catch(() => {}); } catch (e) {}
        if (wantedSound && onSoundBlocked) onSoundBlocked();
      });
    }

    function attach() {
      if (cancelled) return;
      let player;
      try {
        // SDK builds the iframe INSIDE our div → destroy() removes it cleanly.
        player = new window.Vimeo.Player(el, {
          url: video,
          autoplay: true,
          loop: true,
          muted: true,
          background: !!background,
          playsinline: true,
          controls: false,
          title: false,
          byline: false,
          portrait: false
        });
      } catch (e) { fail(); return; }
      playerRef.current = player;
      // Fatal embed errors only — mobile autoplay/volume rejections are NOT fatal.
      player.on("error", (e) => { const nm = (e && e.name) || ""; if (/NotFoundError|PrivacyError|PasswordError/.test(nm)) fail(); });
      // Poster stays until playback actually starts, then crossfade.
      player.on("playing", reveal);
      // Pause watchdog: iOS can revoke playback right after a non-gesture unmute
      // — the film freezes on a frame with no visible reason. If the player
      // pauses while the parent did NOT ask for a pause, recover immediately:
      // re-mute, resume, and flip the UI to sound-off. The film never freezes.
      player.on("pause", () => {
        if (cancelled || pausedRef.current || !startedRef.current) return;
        try { const m = player.setMuted(true); if (m && m.catch) m.catch(() => {}); } catch (e) {}
        try { const r = player.play(); if (r && r.catch) r.catch(() => {}); } catch (e) {}
        if (!mutedRef.current && onSoundBlocked) onSoundBlocked();
      });
      readyTimer = setTimeout(fail, 8000);
      player.ready().then(() => {
        if (cancelled || myId !== vp.id) return;   // stale film guard
        try { const ifr = player.element; if (ifr && className) ifr.className = className; } catch (e) {}
        // ALWAYS start muted (autoplay-safe on every device); applySound()
        // restores + verifies the session's sound choice once 'playing' fires.
        try { const m = player.setMuted(true); if (m && m.catch) m.catch(() => {}); } catch (e) {}
        playSafely(player);
        if (onReady) onReady(player);
      }).catch(fail);
    }

    if (window.Vimeo) attach();
    else {
      let waited = 0;
      waitTimer = setInterval(() => {
        if (window.Vimeo) { clearInterval(waitTimer); waitTimer = null; attach(); }
        else if ((waited += 200) >= 3000) { clearInterval(waitTimer); waitTimer = null; fail(); }
      }, 200);
    }

    return () => {
      cancelled = true;
      if (readyTimer) clearTimeout(readyTimer);
      if (waitTimer) clearInterval(waitTimer);
      if (dotTimer) clearTimeout(dotTimer);
      setShowDot(false);
      const p = playerRef.current; playerRef.current = null;
      if (p) {
        try { p.off("error"); p.off("playing"); p.off("pause"); } catch (e) {}
        // Real teardown: destroy() removes the iframe, not just listeners.
        try { const r = p.destroy(); if (r && r.catch) r.catch(() => {}); } catch (e) {}
      }
    };
  }, [shouldAttach, vp && vp.id]);

  // Mute/volume — SDK only. Runs synchronously after the parent's tap-driven
  // state flip, so iOS treats the unmute as part of the user gesture.
  React.useEffect(() => {
    const p = playerRef.current; if (!p) return;
    if (muted) { try { p.setMuted(true).catch(() => {}); } catch (e) {} return; }
    if (!startedRef.current) return;   // pre-playback unmute is handled (and verified) at 'playing'
    try { p.setMuted(false).catch(() => {}); } catch (e) {}
    try { p.setVolume(1).catch(() => {}); } catch (e) {}
  }, [muted]);

  // Play / pause — SDK only.
  React.useEffect(() => {
    const p = playerRef.current; if (!p) return;
    if (paused) { try { const r = p.pause(); if (r && r.catch) r.catch(() => {}); } catch (e) {} return; }
    let pr; try { pr = p.play(); } catch (e) { return; }
    if (pr && pr.catch) pr.catch(() => {
      const wantedSound = !mutedRef.current;
      try { const m = p.setMuted(true); if (m && m.catch) m.catch(() => {}); } catch (e) {}
      try { const r2 = p.play(); if (r2 && r2.catch) r2.catch(() => {}); } catch (e) {}
      if (wantedSound && onSoundBlocked) onSoundBlocked();
    });
  }, [paused]);

  const retry = (e) => {
    if (e) e.stopPropagation();
    startedRef.current = false;          // so onStarted (→ next-film preload) can fire again
    setFailed(false); setRevealed(false);
  };

  // Non-Vimeo (no parts): just the poster.
  if (!vp) return <img className={className} src={poster} alt={alt || ""} draggable="false" style={posterStyle} />;

  const posterOpacity = revealed ? 0 : 1;
  // loader="logo": a calm plain-black hold panel while the video becomes ready;
  // the only motion is the spinning ring (sv-dot) pulsing white → Sayso pink.
  const holdLayer = loader === "logo"
    ? <div className={className + " sv-fade"} aria-hidden="true"
        style={Object.assign({ opacity: posterOpacity, zIndex: 0, background: "#000" }, posterStyle || {})}></div>
    : <img className={className + " sv-fade"} src={poster} alt={alt || ""} draggable="false"
        style={Object.assign({ opacity: posterOpacity, zIndex: 0 }, posterStyle || {})} />;
  return (
    <React.Fragment>
      {holdLayer}
      {shouldAttach && !failed &&
        <div ref={holderRef} className={className + " sv-fade"} aria-label={alt || ""}
          style={{ opacity: revealed ? 1 : 0, zIndex: 1 }}></div>}
      {showDot && !revealed && !failed && <span className="sv-dot" aria-hidden="true"></span>}
      {failed && !background &&
        <button className="sv-retry" type="button" onClick={retry} aria-label="Retry loading video">
          <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="#fff" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M23 4v6h-6" /><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10" /></svg>
          Retry
        </button>}
    </React.Fragment>
  );
}

window.VimeoFilm = VimeoFilm;
