//go:build dev

package jsbundler

import "net/http"

// hmrUpdate names one boundary module to re-import at a given version.
type hmrUpdate struct {
	Path      string `json:"path"`
	Timestamp int64  `json:"timestamp"`
}

// hmrError describes a compile/transform failure surfaced to the browser as a
// full-screen error overlay (Vite-style). Message is the full formatted error
// text (esbuild carries a code frame; the Solid compiler a plain message).
type hmrError struct {
	Message string `json:"message"`
	File    string `json:"file,omitempty"`
}

// hmrMessage is the WebSocket payload pushed to the browser.
type hmrMessage struct {
	Type    string      `json:"type"` // "update" | "full-reload" | "css-update" | "error"
	Updates []hmrUpdate `json:"updates,omitempty"`
	Path    string      `json:"path,omitempty"` // css-update: the stylesheet path
	Err     *hmrError   `json:"err,omitempty"`  // error: the compile failure to display
}

// serveClient serves the HMR client runtime as an ES module.
func (d *devServer) serveClient(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "application/javascript; charset=utf-8")
	w.Header().Set("Cache-Control", "no-cache")
	w.Write([]byte(hmrClientJS))
}

// hmrClientJS is the browser runtime: it owns the WebSocket, exposes
// createHotContext (the import.meta.hot the transformed modules bind), and
// applies updates. The accept/dispose/data protocol mirrors Vite's so
// solid-refresh's `esm` path (hot.data + hot.accept(cb) + hot.invalidate) works
// unchanged — a changed boundary is re-imported, and the PREVIOUS instance's
// accept callback runs with the new module namespace, patching the live registry.
//
// It also exports showErrorOverlay/clearErrorOverlay and renders a Vite-style
// full-screen compile-error overlay. A module that fails to transform is served
// as a tiny stub that imports showErrorOverlay and calls it (see errorModule in
// hmr_server.go), so the overlay pops the instant a broken module is imported —
// on first load or on a hot re-import. The overlay auto-clears once a hot-update
// batch completes without any module surfacing an error (see applyUpdates).
const hmrClientJS = `
// --- module-level HMR state, keyed by base module URL --------------------------
const hotModulesMap = new Map();  // id -> { id, callbacks: [{fn}] }
const dataMap = new Map();        // id -> persistent data object (survives reloads)
const disposeMap = new Map();     // id -> dispose callback
const declined = new Set();       // ids that opted out of HMR

export function createHotContext(id) {
  if (!dataMap.has(id)) dataMap.set(id, {});
  const existing = hotModulesMap.get(id);
  // A fresh instance of this module is registering; clear its accept callbacks
  // (applyUpdate has already snapshotted the previous instance's).
  if (existing) existing.callbacks = [];

  function pushAccept(fn) {
    let mod = hotModulesMap.get(id);
    if (!mod) { mod = { id, callbacks: [] }; hotModulesMap.set(id, mod); }
    mod.callbacks.push({ fn });
  }

  return {
    get data() { return dataMap.get(id); },
    accept(deps, cb) {
      // accept() | accept(fn) | accept(deps, fn) — self-accept in every form we use.
      if (typeof deps === 'function' || deps == null) pushAccept(deps);
      else pushAccept(cb);
    },
    dispose(cb) { disposeMap.set(id, cb); },
    prune(cb) { disposeMap.set(id, cb); },
    invalidate() { fullReload(); },
    decline() { declined.add(id); },
    on() {}, off() {}, send() {},
  };
}

// --- recompile indicator -------------------------------------------------------
// A small, non-blocking badge so a recompile doesn't look like a frozen page:
// shown the moment an update arrives and hidden once the new module is imported
// and applied. The
// await below yields the event loop, so the spinner paints and animates while the
// server compiles.
let hmrBusy = 0;
let hmrEl = null;
function hmrIndicator() {
  if (hmrEl || typeof document === 'undefined') return hmrEl;
  const head = document.head || document.documentElement;
  const style = document.createElement('style');
  style.textContent = '@keyframes hmr-spin{to{transform:rotate(360deg)}}';
  head.appendChild(style);
  hmrEl = document.createElement('div');
  hmrEl.setAttribute('style',
    'position:fixed;bottom:14px;right:14px;z-index:2147483647;display:none;' +
    'align-items:center;gap:8px;padding:7px 12px;border-radius:9px;' +
    'font:600 12px/1 ui-monospace,SFMono-Regular,Menlo,monospace;color:#e5e7eb;' +
    'background:rgba(17,24,39,.92);box-shadow:0 6px 18px rgba(0,0,0,.35);' +
    'pointer-events:none;user-select:none');
  hmrEl.innerHTML =
    '<span style="width:11px;height:11px;border-radius:50%;display:inline-block;' +
    'border:2px solid rgba(148,163,184,.4);border-top-color:#60a5fa;' +
    'animation:hmr-spin .6s linear infinite"></span><span data-hmr-label></span>';
  (document.body || document.documentElement).appendChild(hmrEl);
  return hmrEl;
}
function hmrShow(label) {
  const el = hmrIndicator();
  if (!el) return;
  const l = el.querySelector('[data-hmr-label]');
  if (l) l.textContent = label || 'recompiling…';
  el.style.display = 'flex';
}
function hmrBusyStart() { hmrBusy++; hmrShow('recompiling…'); }
function hmrBusyEnd() { hmrBusy = Math.max(0, hmrBusy - 1); if (hmrBusy === 0 && hmrEl) hmrEl.style.display = 'none'; }

// --- compile-error overlay -----------------------------------------------------
// A Vite-style full-screen overlay for compile/transform failures. errorEpoch is
// bumped every time an error surfaces; applyUpdates snapshots it around a hot
// batch and clears the overlay only if the batch introduced no new error, so a
// fixed file dismisses the overlay automatically. The overlay lives in a shadow
// root so the app's stylesheet (Tailwind reset et al.) can't restyle it.
let overlayEl = null;
let errorEpoch = 0;
const HMR_OVERLAY_ID = '__hmr-error-overlay';

function escapeHTML(s) {
  return String(s).replace(/[&<>]/g, function (c) {
    return c === '&' ? '&amp;' : c === '<' ? '&lt;' : '&gt;';
  });
}

function overlayHTML(message, file) {
  const css =
    ':host{all:initial}' +
    '.backdrop{position:fixed;inset:0;z-index:2147483647;background:rgba(0,0,0,.66);' +
      'display:flex;align-items:flex-start;justify-content:center;overflow:auto;padding:6vh 20px;' +
      'font:14px/1.55 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}' +
    '.panel{width:100%;max-width:min(1000px,92vw);margin:auto 0;background:#1b1b1f;color:#e6e6e6;' +
      'border:1px solid #ff5555;border-radius:10px;box-shadow:0 20px 60px rgba(0,0,0,.5);overflow:hidden}' +
    '.head{display:flex;align-items:center;gap:10px;padding:12px 14px;background:#2a1416;' +
      'border-bottom:1px solid rgba(255,85,85,.35)}' +
    '.badge{color:#ff6b6b;font-weight:700;letter-spacing:.03em;text-transform:uppercase;font-size:12px}' +
    '.file{color:#9aa0a6;font-size:12.5px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}' +
    '.close{margin-left:auto;background:transparent;border:0;color:#9aa0a6;cursor:pointer;' +
      'font-size:16px;line-height:1;padding:4px 7px;border-radius:6px}' +
    '.close:hover{color:#fff;background:rgba(255,255,255,.08)}' +
    '.body{margin:0;padding:16px;white-space:pre-wrap;word-break:break-word;color:#ffb4b4;' +
      'font-size:13px;max-height:62vh;overflow:auto}' +
    '.hint{padding:10px 14px;border-top:1px solid rgba(255,255,255,.06);color:#7c828a;font-size:12px}';
  return '<style>' + css + '</style>' +
    '<div class="backdrop">' +
      '<div class="panel">' +
        '<div class="head">' +
          '<span class="badge">Compile Error</span>' +
          (file ? '<span class="file">' + escapeHTML(file) + '</span>' : '') +
          '<button class="close" title="Dismiss (Esc)">✕</button>' +
        '</div>' +
        '<pre class="body">' + escapeHTML(message) + '</pre>' +
        '<div class="hint">Fix the error and save — this overlay clears automatically.</div>' +
      '</div>' +
    '</div>';
}

export function showErrorOverlay(err) {
  errorEpoch++;
  if (typeof document === 'undefined') return;
  const message = (err && (err.message || err.msg)) || String(err || 'Unknown error');
  const file = (err && err.file) || '';
  console.error('[hmr] compile error' + (file ? ' in ' + file : '') + '\n' + message);
  clearErrorOverlay();
  const host = document.createElement('div');
  host.id = HMR_OVERLAY_ID;
  const root = host.attachShadow ? host.attachShadow({ mode: 'open' }) : host;
  root.innerHTML = overlayHTML(message, file);
  const closeBtn = root.querySelector('.close');
  if (closeBtn) closeBtn.addEventListener('click', clearErrorOverlay);
  (document.body || document.documentElement).appendChild(host);
  overlayEl = host;
}

export function clearErrorOverlay() {
  if (overlayEl && overlayEl.parentNode) overlayEl.parentNode.removeChild(overlayEl);
  overlayEl = null;
}

if (typeof document !== 'undefined') {
  document.addEventListener('keydown', function (e) {
    if (e.key === 'Escape' && overlayEl) clearErrorOverlay();
  });
}

async function applyUpdate(update) {
  const id = update.path;
  hmrBusyStart();
  try {
    if (declined.has(id)) return fullReload();
    const mod = hotModulesMap.get(id);
    if (!mod) return fullReload(); // module not tracked yet — reload to be safe

    const callbacks = mod.callbacks;           // the live instance's accept callbacks
    const disposer = disposeMap.get(id);
    if (disposer) { try { await disposer(dataMap.get(id)); } catch (e) { console.error(e); } }

    let newMod;
    try {
      newMod = await import(id + (id.includes('?') ? '&' : '?') + 't=' + update.timestamp);
    } catch (e) {
      console.error('[hmr] failed to re-import', id, e);
      return fullReload();
    }
    for (const cb of callbacks) {
      if (cb.fn) { try { cb.fn(newMod); } catch (e) { console.error(e); } }
    }
    console.log('[hmr] updated', id);
  } finally {
    hmrBusyEnd();
  }
}

function updateCSS(path) {
  const links = document.querySelectorAll('link[rel="stylesheet"]');
  for (const link of links) {
    const url = new URL(link.href, location.href);
    if (url.pathname === path) {
      const next = link.cloneNode();
      next.href = url.pathname + '?t=' + Date.now();
      next.onload = () => link.remove();
      link.after(next);
      console.log('[hmr] css updated', path);
      return;
    }
  }
  // The stylesheet isn't linked on this page — the dev server broadcasts CSS
  // updates for both the SPA (/bundle.min.css) and public (/public.bundle.min.css)
  // bundles to every client, but each page carries only one. An update for the
  // other bundle is simply not applicable here, so ignore it. (Forcing a full
  // reload instead would defeat the .tsx component HMR that ran moments earlier.)
}

function fullReload() { hmrShow('reloading…'); location.reload(); }

// applyUpdates runs a hot-update batch, then clears the error overlay iff no
// module surfaced a compile error while importing (errorEpoch unchanged). Module
// evaluation is synchronous within an import(), so a broken module's
// showErrorOverlay() has already run by the time its applyUpdate resolves — the
// check is race-free.
async function applyUpdates(updates) {
  const before = errorEpoch;
  for (const u of updates) { await applyUpdate(u); }
  if (errorEpoch === before) clearErrorOverlay();
}

function handle(raw) {
  let msg;
  try { msg = JSON.parse(raw); } catch { return; }
  switch (msg.type) {
    case 'update': applyUpdates(msg.updates || []); break;
    case 'css-update': updateCSS(msg.path); break;
    case 'full-reload': fullReload(); break;
    case 'error': showErrorOverlay(msg.err || {}); break;
  }
}

function connect() {
  const proto = location.protocol === 'https:' ? 'wss' : 'ws';
  const ws = new WebSocket(proto + '://' + location.host + '/@hmr/ws');
  ws.addEventListener('message', (e) => handle(e.data));
  ws.addEventListener('open', () => console.log('[hmr] connected'));
  ws.addEventListener('close', () => { console.log('[hmr] connection lost, retrying...'); setTimeout(connect, 1000); });
  ws.addEventListener('error', () => ws.close());
}
connect();
`
