(function() {
  var useState  = React.useState;
  var useEffect = React.useEffect;
  var useRef    = React.useRef;

  var NEUTRAL_HDR = 'https://modelviewer.dev/shared-assets/environments/neutral.hdr';
  // Large Meshy GLBs (50MB+) need patience on free hosts — never abort while progress moves
  var BASE_TIMEOUT_MS = 120000;
  var MAX_TIMEOUT_MS  = 600000;

  function isIOS() {
    return /iPad|iPhone|iPod/.test(navigator.userAgent) ||
      (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1);
  }

  function formatBytes(n) {
    if (!n || n < 0) return '';
    if (n < 1024 * 1024) return Math.round(n / 1024) + ' KB';
    return (n / (1024 * 1024)).toFixed(1) + ' MB';
  }

  /** Soft HEAD only — never Range-GET (servers that ignore Range dump the full 50MB+ file). */
  async function probeModelUrl(src) {
    try {
      var head = await fetch(src, { method: 'HEAD', mode: 'cors', credentials: 'omit' });
      if (!head.ok) throw new Error('HTTP ' + head.status);
      var ct = (head.headers.get('content-type') || '').toLowerCase();
      if (ct.indexOf('json') !== -1) throw new Error('3D model file missing on server');
      var len = Number(head.headers.get('content-length') || 0);
      return { bytes: len > 0 ? len : 0 };
    } catch (e) {
      // Viewer can still try — HEAD may be blocked on some proxies
      return { bytes: 0, soft: e.message || 'probe failed' };
    }
  }

  function timeoutForBytes(bytes) {
    if (!bytes || bytes < 20 * 1024 * 1024) return BASE_TIMEOUT_MS;
    // ~1.5s per MB, clamped
    var ms = Math.round((bytes / (1024 * 1024)) * 1500) + 60000;
    return Math.min(MAX_TIMEOUT_MS, Math.max(BASE_TIMEOUT_MS, ms));
  }

  function ModelViewerPanel(props) {
    var src     = props.src;
    var iosSrc  = props.iosSrc;
    var alt     = props.alt || '3D model';

    var stageRef = useRef(null);
    var cleanupRef = useRef(null);
    var progressRef = useRef(0);
    var _retry = useState(0);
    var retryCount = _retry[0], bumpRetry = _retry[1];

    var _state = useState({ phase: 'loading', progress: 0, error: null, engine: 'model-viewer', bytes: 0 });
    var state = _state[0], setState = _state[1];

    useEffect(function() {
      if (!src || !stageRef.current) return;

      var container = stageRef.current;
      var cancelled = false;
      var bootMeta = null;
      progressRef.current = 0;

      setState({ phase: 'loading', progress: 0, error: null, engine: 'model-viewer', bytes: 0 });

      function fail(message, engine) {
        if (cancelled) return;
        setState(function(s) {
          return { phase: 'error', progress: 0, error: message, engine: engine || s.engine, bytes: s.bytes || 0 };
        });
      }

      function ready(engine) {
        if (cancelled) return;
        setState(function(s) {
          return { phase: 'ready', progress: 100, error: null, engine: engine || 'model-viewer', bytes: s.bytes || 0 };
        });
      }

      function runCleanup() {
        if (cleanupRef.current) {
          try { cleanupRef.current(); } catch (e) {}
          cleanupRef.current = null;
        }
        container.innerHTML = '';
      }

      function mountModelViewer(meta) {
        if (cancelled || !container) return;

        runCleanup();
        var bytes = (meta && meta.bytes) || 0;
        var waitMs = timeoutForBytes(bytes);

        // Match render cost to the device — the model itself is untouched.
        var tier = (window.CBDeviceTier && window.CBDeviceTier.settings) ||
          { shadows: true, environment: true, autoRotate: true };

        var mv = document.createElement('model-viewer');
        mv.setAttribute('src', src);
        mv.setAttribute('crossorigin', 'anonymous');
        mv.setAttribute('camera-controls', '');
        // Constant auto-rotate re-renders every frame — only spin on strong GPUs.
        if (tier.autoRotate) mv.setAttribute('auto-rotate', '');
        // Real-time shadows are one of the most expensive effects on low-end.
        mv.setAttribute('shadow-intensity', tier.shadows ? '0.6' : '0');
        mv.setAttribute('exposure', '1');
        // The HDR environment is a big fetch + costly IBL — skip it on weak devices.
        if (tier.environment) mv.setAttribute('environment-image', NEUTRAL_HDR);
        mv.setAttribute('reveal', 'auto');
        // Prefer progressive load so huge GLBs can paint sooner
        mv.setAttribute('loading', 'eager');
        mv.setAttribute('alt', alt);
        mv.className = 'cb-model-viewer';

        /* AR must be enabled on every platform, not just iOS-with-USDZ.
           Previously `ar` was set only inside the iOS branch, so Android
           never advertised AR at all and scene-viewer could never launch.
           model-viewer picks whichever mode the device supports and hides
           the button when none are available, so this is safe to always set. */
        mv.setAttribute('ar', '');
        mv.setAttribute('ar-modes', 'webxr scene-viewer quick-look');
        mv.setAttribute('ar-scale', 'auto');
        if (iosSrc) {
          // Quick Look needs USDZ; harmless on other platforms.
          mv.setAttribute('ios-src', iosSrc);
        }

        var lastProgressAt = Date.now();
        var slowTimer = window.setTimeout(function onSlow() {
          if (cancelled) return;
          // If bytes are still arriving, keep waiting instead of double-downloading via Three
          if (progressRef.current > 0 && progressRef.current < 100) {
            lastProgressAt = Date.now();
            slowTimer = window.setTimeout(onSlow, Math.min(120000, waitMs));
            setState(function(s) {
              return Object.assign({}, s, {
                phase: 'loading',
                progress: progressRef.current,
              });
            });
            return;
          }
          // Stall with zero progress — offer Three fallback once
          setState(function(s) {
            if (s.phase === 'loading' && s.engine === 'model-viewer' && progressRef.current === 0) {
              mountThree('Primary viewer stalled — trying WebGL fallback');
            }
            return s;
          });
        }, waitMs);

        mv.addEventListener('progress', function(e) {
          if (cancelled || !e.detail) return;
          var pct = Math.round((e.detail.totalProgress || 0) * 100);
          if (pct > progressRef.current) {
            progressRef.current = pct;
            lastProgressAt = Date.now();
          }
          setState(function(s) {
            return Object.assign({}, s, {
              progress: pct,
              phase: pct >= 100 ? 'ready' : 'loading',
              bytes: s.bytes || bytes,
            });
          });
        });

        mv.addEventListener('load', function() {
          window.clearTimeout(slowTimer);
          progressRef.current = 100;
          ready('model-viewer');
        });
        mv.addEventListener('model-visibility', function(e) {
          if (e.detail && e.detail.visible) {
            window.clearTimeout(slowTimer);
            ready('model-viewer');
          }
        });

        mv.addEventListener('error', function() {
          window.clearTimeout(slowTimer);
          mountThree('model-viewer failed to render this GLB');
        });

        container.appendChild(mv);

        window.setTimeout(function() {
          if (cancelled) return;
          setState(function(s) {
            if (s.phase === 'loading' && s.progress === 0) {
              return Object.assign({}, s, { progress: 2, bytes: bytes });
            }
            return s;
          });
        }, 600);
      }

      function mountThree(reason) {
        if (cancelled || !container) return;

        runCleanup();
        setState(function(s) {
          return { phase: 'loading', progress: 8, error: null, engine: 'three', bytes: s.bytes || 0 };
        });

        function runThree() {
          if (!window.mountThreeModelViewer) {
            // Three bundle never arrived — hand off to model-viewer, which can
            // still render anything that is not meshopt-compressed.
            mountModelViewer(bootMeta || { bytes: 0 });
            return;
          }
          window.mountThreeModelViewer(container, src, {
            onProgress: function(pct) {
              if (cancelled) return;
              progressRef.current = pct;
              setState(function(s) {
                return Object.assign({}, s, { progress: pct, phase: 'loading', engine: 'three' });
              });
            },
          }).then(function(cleanup) {
            if (cancelled) { cleanup(); return; }
            cleanupRef.current = cleanup;
            ready('three');
          }).catch(function(err) {
            fail(
              (reason ? reason + '. ' : '') +
              ((err && err.message) || 'Could not load 3D model') +
              '. Try Download GLB if the file is very large.',
              'three'
            );
          });
        }

        if (window.mountThreeModelViewer) {
          runThree();
        } else {
          var tries = 0;
          var wait = setInterval(function() {
            tries += 1;
            if (window.mountThreeModelViewer || tries > 40) {
              clearInterval(wait);
              if (!cancelled) runThree();
            }
          }, 100);
        }
      }

      /* Backend ships every GLB meshopt-compressed (glbOptimizer.js runs
         `--compress meshopt`), and <model-viewer>'s internal GLTFLoader has no
         MeshoptDecoder registered — it throws "setMeshoptDecoder must be called
         before loading compressed files" and never paints. Our own Three.js
         viewer does register the decoder, so it leads; model-viewer is kept
         only as the fallback for when the Three bundle fails to load. */
      function boot() {
        probeModelUrl(src).then(function(meta) {
          if (cancelled) return;
          bootMeta = meta;
          setState(function(s) {
            return Object.assign({}, s, { bytes: meta.bytes || 0 });
          });
          mountThree('');
        }).catch(function() {
          if (!cancelled) mountThree('');
        });
      }

      boot();

      return function() {
        cancelled = true;
        runCleanup();
      };
    }, [src, iosSrc, alt, retryCount]);

    function retry() {
      bumpRetry(function(n) { return n + 1; });
    }

    var showOverlay = state.phase === 'loading' || state.phase === 'error';
    var sizeHint = state.bytes > 25 * 1024 * 1024
      ? ('Large model (' + formatBytes(state.bytes) + ') — loading in browser may take a minute…')
      : null;

    return React.createElement('div', { className: 'ar-model-stage' },
      React.createElement('div', { className: 'ar-model-stage-inner', ref: stageRef }),
      showOverlay && React.createElement('div', { className: 'ar-model-overlay' },
        state.phase === 'loading' && React.createElement('div', { className: 'ar-model-loading' },
          React.createElement('div', { className: 'ar-model-spinner' }),
          React.createElement('div', null,
            state.engine === 'three' ? 'Loading 3D preview (WebGL)…' : 'Loading 3D model…'
          ),
          sizeHint && React.createElement('div', {
            style: { fontFamily: 'DM Mono,monospace', fontSize: 10, color: 'var(--text-faint)', marginTop: 8, maxWidth: 280, lineHeight: 1.5 },
          }, sizeHint),
          state.progress > 0 && React.createElement('div', { className: 'ar-model-progress' },
            React.createElement('div', { className: 'ar-model-progress-bar', style: { width: state.progress + '%' } })
          ),
          state.progress > 0 && React.createElement('div', {
            style: { fontFamily: 'DM Mono,monospace', fontSize: 10, color: 'var(--violet)', marginTop: 6 },
          }, state.progress + '%')
        ),
        state.phase === 'error' && React.createElement('div', { className: 'ar-model-error' },
          React.createElement('div', null, state.error || 'Could not load 3D model'),
          React.createElement('button', {
            type: 'button',
            className: 'btn btn-o',
            style: { marginTop: 12, fontSize: 11 },
            onClick: retry,
          }, 'Retry'),
          src && React.createElement('a', {
            href: src + (src.indexOf('?') >= 0 ? '&' : '?') + 'download=1',
            className: 'btn btn-v',
            style: { marginTop: 10, fontSize: 11, textDecoration: 'none', display: 'inline-block' },
            download: true,
          }, 'Download GLB instead')
        )
      ),
      state.phase === 'ready' && state.engine === 'three' &&
        React.createElement('div', { className: 'ar-model-engine-tag' }, 'WebGL preview')
    );
  }

  window.ModelViewerPanel = ModelViewerPanel;
})();
