// The immortal shell — served by the supervisor at top-level '/' (and document navigations).
// It contains ONLY the bubble + chat chrome (widget.js) and a same-origin iframe hosting the
// user's workspace app. There is NO Vite client here, NO HMR, no service-worker auto-reload —
// nothing that can reload this page. All workspace reloads, rebuilds, and interstitials
// (RECOVERING_HTML / backend-down) happen INSIDE the iframe, so the chat — the user's lifeline
// to the agent — never dies. The iframe src carries ?__bloby_frame=1 so the supervisor routes
// it past the shell branch to the real workspace (Vite proxy / interstitials).
// Kill switch: BLOBY_NO_SHELL=1 makes the supervisor skip this entirely (legacy top-level
// workspace). Static string by design — no server-side templating.

export const SHELL_HTML = `<!DOCTYPE html>
<html lang="en" style="background-color:#0A0A0A;margin:0;overflow:hidden">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, interactive-widget=resizes-content" />
    <meta name="theme-color" content="#212121" />
    <meta name="mobile-web-app-capable" content="yes" />
    <meta name="apple-mobile-web-app-capable" content="yes" />
    <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
    <meta name="apple-mobile-web-app-title" content="Bloby" />
    <link rel="icon" type="image/png" href="/morphy-favicon.png" />
    <link rel="apple-touch-icon" href="/morphy-icon-192.png" />
    <link rel="manifest" href="/manifest.json" />
    <!-- widget.js sits at the end of body; fetch it while the iframe + inline scripts parse -->
    <link rel="preload" href="/bloby/widget.js" as="script" />
    <title>Bloby</title>
  </head>
  <body style="background-color:#0A0A0A;margin:0;overflow:hidden">
    <!-- Dark background fallback — visible instantly while widget.js loads.
         The canvas animation (in widget.js) takes over as the actual splash. -->
    <div id="splash" style="background:#0A0A0A;position:fixed;inset:0;z-index:9998;transition:opacity .25s ease-out"></div>

    <iframe
      id="bloby-workspace"
      style="position:fixed;inset:0;width:100vw;height:100dvh;border:none;background:#0A0A0A"
      allow="camera; microphone; geolocation; clipboard-read; clipboard-write; fullscreen; autoplay; display-capture"
    ></iframe>

    <script>
    // ── Refresh-performance marks ──────────────────────────────────────────
    // Collected from this document + widget.js (same window, via window.__blobyPerf.mark) +
    // the workspace iframe (workspace-guard posts {type:'bloby:perf'}), assembled into ONE
    // beacon per page load and POSTed to /__bloby/perf (kept server-side, localhost-readable).
    // Live console print: localStorage.bloby_perf = '1'.
    (function () {
      var marks = {};
      var workspace = null;
      var sent = false;
      window.__blobyPerf = {
        mark: function (name) { if (marks[name] === undefined) marks[name] = Math.round(performance.now()); },
        workspace: function (data) { workspace = data; },
      };
      function assemble() {
        var nav = null;
        try { nav = performance.getEntriesByType('navigation')[0] || null; } catch (e) {}
        var b = {
          host: location.host,
          shell: nav ? {
            ttfb: Math.round(nav.responseStart),
            htmlDone: Math.round(nav.responseEnd),
            domLoaded: Math.round(nav.domContentLoadedEventEnd),
          } : null,
          marks: marks,
          workspace: workspace, // its marks arrive on the iframe's clock; offsetMs aligns them
        };
        if (workspace && typeof workspace.timeOrigin === 'number') {
          b.workspace.offsetMs = Math.round(workspace.timeOrigin - performance.timeOrigin);
        }
        return b;
      }
      function send() {
        if (sent) return;
        sent = true;
        var beacon = assemble();
        try { if (localStorage.getItem('bloby_perf') === '1') console.log('[bloby-perf]', JSON.stringify(beacon, null, 2)); } catch (e) {}
        try { fetch('/__bloby/perf', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(beacon), keepalive: true }).catch(function () {}); } catch (e) {}
      }
      // Send a few seconds after the workspace reports ready, so stragglers (sprite sheet,
      // the deferred chat iframe, late modules) land in the same beacon. Backstop for broken
      // workspaces. 7s covers the chat's idle-deferred load (~app-ready + 4s timeout + load).
      window.addEventListener('bloby:app-ready', function () { setTimeout(send, 7000); });
      setTimeout(send, 20000);
    })();
    </script>

    <script>
    (function () {
      var frame = document.getElementById('bloby-workspace');

      // Preserve deep links: the iframe loads the SAME url the shell was opened with, plus
      // __bloby_frame=1 so the supervisor serves the workspace instead of the shell again.
      // Collapse leading slashes first — a pathname like '//evil.com' would otherwise be a
      // protocol-relative URL and point the iframe off-origin under our trusted domain.
      var path = location.pathname.replace(/^\\/+/, '/');
      var target = new URL(path + location.search + location.hash, location.origin);
      if (target.origin !== location.origin) target = new URL('/', location.origin);
      target.searchParams.set('__bloby_frame', '1');

      // Framed shell self-check: browsers without Fetch Metadata (older Safari/WebViews) can
      // get a shell served INTO the workspace iframe when an in-frame navigation loses the
      // __bloby_frame param (the supervisor's sec-fetch-dest exclusion can't see those).
      // A nested shell replaces itself with the workspace document it should have been.
      try {
        if (window.top !== window) {
          location.replace(target.pathname + target.search + target.hash);
          return;
        }
      } catch (e) { /* cross-origin top — not our nesting case; carry on */ }

      frame.src = target.pathname + target.search + target.hash;

      // widget.js (loaded below, in THIS document) waits for window.__blobyAppReady / the
      // 'bloby:app-ready' window event before transitioning splash → bubble. The workspace
      // posts 'bloby:app-ready' up to us; we re-dispatch it locally. Idempotent.
      var appReadyFired = false;
      function fireAppReady() {
        if (appReadyFired) return;
        appReadyFired = true;
        window.__blobyAppReady = true;
        if (window.__blobyPerf) window.__blobyPerf.mark('shell:app-ready');
        window.dispatchEvent(new Event('bloby:app-ready'));
      }
      if (window.__blobyPerf) window.__blobyPerf.mark('shell:frame-src-set');

      window.addEventListener('message', function (e) {
        // Same-origin iframe only — drop anything else (extensions, third-party embeds).
        if (e.origin !== location.origin) return;
        var d = e.data;
        if (!d || typeof d.type !== 'string') return;
        if (d.type === 'bloby:app-ready') {
          fireAppReady();
        } else if (d.type === 'bloby:title') {
          document.title = String(d.title || '').slice(0, 200) || 'Bloby';
        } else if (d.type === 'bloby:perf' && d.data && window.__blobyPerf) {
          window.__blobyPerf.workspace(d.data);
        }
      });

      // Interstitials and broken workspaces never post 'bloby:app-ready' — the bubble must
      // still appear (it IS the recovery path). Fire on iframe load, with a timer backstop
      // for the case where the iframe never finishes loading at all.
      frame.addEventListener('load', fireAppReady);
      setTimeout(fireAppReady, 8000);

      // Last-resort splash backstop: only widget.js ever hides #splash (it does so as soon
      // as its sprite loads, ~1s). If widget.js fails to load, throws, or its sprite fetch
      // fails, the shell — the user's lifeline — must not sit as a permanent black screen
      // over the workspace iframe.
      setTimeout(function () {
        var s = document.getElementById('splash');
        if (s && s.style.display !== 'none') s.style.display = 'none';
      }, 10000);

      /* ── Workspace watchdog ──────────────────────────────────────────────
         The iframe can land on a document with NO Bloby code in it: cloudflared's raw
         "502 Bad Gateway" while the supervisor restarts during a self-update, a relay
         hiccup, or the browser's own network-error page. Nothing inside the frame can
         recover from those — so the shell owns recovery: poll the supervisor, show a
         branded "restarting" overlay while it's unreachable, and reload the frame once
         it's back (then verify the frame really shows one of our documents again). */
      var overlay = document.createElement('div');
      overlay.id = 'bloby-ws-restarting';
      // z-index 9997: above the workspace iframe, below the splash (9998), the bubble
      // canvas (9999+) and the chat panel (99999) — the chat stays usable on top.
      overlay.style.cssText = 'display:none;position:fixed;inset:0;z-index:9997;background:#0a0a0b;color:#e4e4e7;align-items:center;justify-content:center;padding:1.5rem;font-family:system-ui,-apple-system,sans-serif';
      overlay.innerHTML =
        '<div style="text-align:center;max-width:460px;width:100%">' +
          '<div style="position:relative;width:160px;height:160px;margin:0 auto 1.2rem">' +
            '<div style="position:absolute;inset:-18px;background:radial-gradient(circle,rgba(1,102,255,.18) 0%,transparent 60%);filter:blur(18px)"></div>' +
            // preload=auto so the clip is fetched (and SW-cached) while the supervisor is
            // still up — by the time we show this, the origin is unreachable.
            '<video autoplay loop muted playsinline preload="auto" style="position:relative;width:100%;height:100%;object-fit:contain;border-radius:50%">' +
              '<source src="/what-happened.webm" type="video/webm"><source src="/what-happened.mp4" type="video/mp4">' +
            '</video>' +
          '</div>' +
          '<h1 style="font-size:1.5rem;font-weight:700;margin:0 0 .6rem;background:linear-gradient(135deg,#0166FF,#009AFE,#4AEEFF);-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text">Workspace is restarting&hellip;</h1>' +
          '<p style="color:#e4e4e7;font-size:1rem;line-height:1.6;margin:0 0 .4rem">Your app is coming back online &mdash; this usually happens right after an update or a restart.</p>' +
          '<p style="color:#a1a1aa;font-size:.93rem;line-height:1.6;margin:0">No action needed. Your chat stays online the whole time &mdash; ask your agent anything.</p>' +
          '<div style="font-size:.82rem;color:#71717a;display:inline-flex;align-items:center;gap:.5rem;background:#18181b;border:1px solid #27272a;border-radius:9999px;padding:.35rem .9rem;margin-top:1.1rem"><span style="width:8px;height:8px;border-radius:50%;background:linear-gradient(135deg,#0166FF,#009AFE);box-shadow:0 0 8px rgba(1,102,255,.6)"></span><span>Watching for recovery&hellip;</span></div>' +
        '</div>';
      document.body.appendChild(overlay);

      function refreshFrame() {
        var u = target.pathname + target.search + target.hash;
        try { frame.contentWindow.location.replace(u); }
        catch (e) { frame.src = u; } // error pages are opaque — reassigning src re-navigates
      }

      function frameLooksBroken() {
        try {
          var d = frame.contentDocument;
          if (!d) return true; // browser error pages are opaque to the same-origin parent
          if (d.readyState === 'loading') return false; // still navigating — don't judge
          // Every document WE can legitimately serve into the frame: the workspace app
          // (#root / #splash), the supervisor interstitials (.video-wrap), and the relay's
          // branded pages ("Powered by Bloby" — those self-recover, leave them alone).
          if (d.getElementById('root') || d.getElementById('splash')) return false;
          if (d.querySelector('.video-wrap')) return false;
          var txt = String((d.body && d.body.textContent) || '');
          if (txt.indexOf('Powered by Bloby') !== -1) return false;
          // Only short, clearly error-shaped documents count as broken — NEVER judge a real
          // app by the absence of our markers (an agent-rebuilt app may have none of them).
          return txt.length < 600 && /bad gateway|gateway time|stream to the origin|cloudflare|connection refused|50[234]/i.test(txt);
        } catch (e) {
          return true; // cross-origin contentDocument → browser error page → broken
        }
      }

      var downCount = 0, upCount = 0, isDown = false, brokenRetries = 0;
      function watchTick() {
        fetch('/__bloby/backend-status', { cache: 'no-store' })
          .then(function (r) { if (!r.ok) throw new Error('status ' + r.status); return r.json(); })
          .then(function () {
            downCount = 0;
            if (isDown) {
              // Two consecutive healthy polls before reloading the frame — right after a
              // restart the tunnel still drops the odd request, and reloading into that
              // window is exactly how the frame lands on cloudflared's raw 502.
              upCount++;
              if (upCount >= 2) {
                isDown = false; upCount = 0; brokenRetries = 0;
                overlay.style.display = 'none';
                refreshFrame();
              }
            } else if (frameLooksBroken()) {
              // Supervisor healthy but the frame shows a foreign error page (e.g. it
              // reloaded itself into a tunnel blip the watchdog never saw). Capped so an
              // exotic-but-working document can never be refresh-looped forever.
              if (brokenRetries < 5) { brokenRetries++; refreshFrame(); }
            } else {
              brokenRetries = 0;
            }
          })
          .catch(function () {
            upCount = 0;
            downCount++;
            if (downCount >= 2 && !isDown) { // ~8s of failures = real outage, not a blip
              isDown = true;
              overlay.style.display = 'flex';
            }
          });
      }
      setInterval(watchTick, 4000);
    })();
    </script>

    <script>
    // Register the SW but DO NOT auto-reload on controllerchange.
    // The old pattern (skipWaiting + claim + controllerchange → location.reload)
    // caused surprise reloads on mobile PWAs: iOS aggressively re-checks sw.js
    // and any byte-level variation in the response (esp. via Vite dev) trips a
    // new SW activation → controllerchange → unwanted reload. The new SW
    // silently takes over for future fetches; the user gets the new build on
    // their next natural refresh.
    if ('serviceWorker' in navigator) {
      navigator.serviceWorker.register('/sw.js').catch(function (err) {
        console.error('[sw-reg] registration failed:', err);
      });
    }
    </script>
    <script src="/bloby/widget.js"></script>
  </body>
</html>
`;
