/* educator-apps.jsx — Import & host packaged HTML apps as course products.
   Educators can import a .zip containing a standalone HTML app (index.html + assets)
   or a single .html file; it is stored in IndexedDB (AppStore, see deck-store.js)
   and hosted in an iframe inside the product workspace. */

const APP_MIME = { html: "text/html", htm: "text/html", js: "text/javascript", mjs: "text/javascript", jsx: "text/javascript", css: "text/css", json: "application/json", svg: "image/svg+xml", png: "image/png", jpg: "image/jpeg", jpeg: "image/jpeg", gif: "image/gif", webp: "image/webp", ico: "image/x-icon", woff: "font/woff", woff2: "font/woff2", ttf: "font/ttf", otf: "font/otf", txt: "text/plain", md: "text/plain", pdf: "application/pdf", mp4: "video/mp4", mp3: "audio/mpeg", wav: "audio/wav" };
function appMime(path) {const m = String(path).toLowerCase().match(/\.([a-z0-9]+)$/);return m && APP_MIME[m[1]] || "application/octet-stream";}
function appFmtSize(b) {return b > 1048576 ? (b / 1048576).toFixed(1) + " MB" : Math.max(1, Math.round(b / 1024)) + " KB";}

/* Build a hostable blob: URL for a stored app — rewrites relative refs in index.html to blob URLs. */
async function buildAppUrl(rec) {
  const paths = Object.keys(rec.files || {});
  const indexPath = paths.find((p) => /(^|\/)index\.html?$/i.test(p)) || paths.find((p) => /\.html?$/i.test(p));
  if (!indexPath) throw new Error("No index.html found in the package");
  let html = await rec.files[indexPath].text();
  const base = indexPath.includes("/") ? indexPath.slice(0, indexPath.lastIndexOf("/") + 1) : "";
  /* longest paths first so "src/app.js" is replaced before a shorter "app.js" */
  const rest = paths.filter((p) => p !== indexPath).sort((a, b) => b.length - a.length);
  for (const p of rest) {
    const rel = base && p.indexOf(base) === 0 ? p.slice(base.length) : p;
    const url = URL.createObjectURL(rec.files[p]);
    html = html.
    split('"' + rel + '"').join('"' + url + '"').
    split("'" + rel + "'").join("'" + url + "'").
    split('"./' + rel + '"').join('"' + url + '"').
    split("'./" + rel + "'").join("'" + url + "'");
  }
  return URL.createObjectURL(new Blob([html], { type: "text/html" }));
}

/* Read a .zip into { path: Blob } using JSZip */
async function readZipFiles(file) {
  if (!window.JSZip) throw new Error("zip support didn't load — check your connection");
  const zip = await JSZip.loadAsync(file);
  const files = {};
  let count = 0, size = 0;
  for (const path of Object.keys(zip.files)) {
    const entry = zip.files[path];
    if (entry.dir || /__MACOSX|\.DS_Store/i.test(path)) continue;
    const blob = await entry.async("blob");
    files[path] = new Blob([blob], { type: appMime(path) });
    count++;size += blob.size;
  }
  return { files, count, size };
}

/* Fullscreen launch modal — hosts the app in an iframe */
function AppLaunchModal({ app, title, accent, onClose }) {
  const [src, setSrc] = useState(app.url || null);
  const [err, setErr] = useState(null);
  useEffect(() => {
    if (!app.url && app.appId && window.AppStore) {
      window.AppStore.get(app.appId).
      then((rec) => {if (!rec) throw new Error("Package not found in this browser — re-import the zip");return buildAppUrl(rec);}).
      then((u) => setSrc(u)).
      catch((e) => setErr(e && e.message || "Couldn't open the app"));
    }
  }, []);
  return ReactDOM.createPortal(
    <div style={{ position: "fixed", inset: 0, zIndex: 700, background: "rgba(13,26,52,.6)", backdropFilter: "blur(3px)", display: "grid", placeItems: "center", padding: "2.5vh 2.5vw" }} onClick={(e) => {if (e.target === e.currentTarget) onClose();}}>
      <div style={{ width: "100%", height: "100%", maxWidth: 1500, borderRadius: 14, overflow: "hidden", background: "#fff", display: "flex", flexDirection: "column", boxShadow: "0 24px 70px rgba(0,0,0,.35)" }}>
        <div style={{ flex: "none", display: "flex", alignItems: "center", gap: 12, padding: "10px 16px", borderBottom: "1px solid var(--line)", background: "var(--surface)" }}>
          <span style={{ width: 9, height: 9, borderRadius: "50%", background: accent || "#2e7d32", flexShrink: 0 }}></span>
          <div style={{ flex: 1, minWidth: 0, fontWeight: 700, fontSize: 14, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{app.name}{title ? <span style={{ fontWeight: 500, color: "var(--ink-3)" }}> — {title}</span> : null}</div>
          <span style={{ fontSize: 11.5, fontWeight: 700, color: "var(--ink-3)", background: "var(--surface-2)", borderRadius: 999, padding: "3px 10px", flexShrink: 0 }}>Hosted app{app.builtin ? " · sample" : " · imported"}</span>
          <button className="btn btn-ghost btn-sm" onClick={onClose} style={{ flexShrink: 0 }}><Icon name="x" size={14} />Close</button>
        </div>
        <div style={{ flex: 1, minHeight: 0, position: "relative", background: "#f4f5f8" }}>
          {err ?
          <div style={{ position: "absolute", inset: 0, display: "grid", placeItems: "center", color: "#c62828", fontWeight: 600, fontSize: 14, padding: 30, textAlign: "center" }}>{err}</div> :
          src ?
          <iframe src={src} title={app.name} style={{ width: "100%", height: "100%", border: "none", display: "block" }}></iframe> :
          <div style={{ position: "absolute", inset: 0, display: "grid", placeItems: "center", color: "var(--ink-3)", fontSize: 14, fontWeight: 600 }}>Loading app…</div>}
        </div>
      </div>
    </div>, document.body);
}

/* Import button + hosted-app card — shown at the top of every product material editor */
function HostedAppPanel({ mat, set, t, product, toast }) {
  const zipRef = React.useRef(null);
  const [busy, setBusy] = useState(false);
  const [launch, setLaunch] = useState(false);
  const app = mat && mat.app;

  async function handleImport(file) {
    if (!file || busy) return;
    setBusy(true);
    try {
      const name = file.name.replace(/\.(zip|html?)$/i, "");
      const id = "app_" + (product ? product.id : "x") + "_" + t.key + "_" + Date.now();
      let record, meta;
      if (/\.zip$/i.test(file.name)) {
        const { files, count, size } = await readZipFiles(file);
        if (!Object.keys(files).some((p) => /\.html?$/i.test(p))) throw new Error("no HTML file found in the zip");
        record = { id, name, files, created: Date.now() };
        meta = { name, appId: id, files: count, size };
      } else if (/\.html?$/i.test(file.name)) {
        record = { id, name, files: { "index.html": new Blob([await file.text()], { type: "text/html" }) }, created: Date.now() };
        meta = { name, appId: id, files: 1, size: file.size };
      } else {
        throw new Error("use a .zip (HTML app package) or a single .html file");
      }
      if (!window.AppStore) throw new Error("storage unavailable");
      if (app && app.appId) {try {await window.AppStore.remove(app.appId);} catch (e) {}}
      await window.AppStore.save(record);
      set({ app: meta, status: mat.status === "none" ? "draft" : mat.status });
      toast && toast("App imported — \"" + name + "\" is now hosted as this product", "check");
    } catch (e) {
      toast && toast("Import failed — " + (e && e.message || "invalid package"), "x");
    }
    setBusy(false);
  }

  function removeApp() {
    if (app && app.appId && window.AppStore) {try {window.AppStore.remove(app.appId);} catch (e) {}}
    set({ app: null });
    toast && toast("Hosted app removed — back to the built-in " + t.short.toLowerCase() + " experience", "x");
  }

  return (
    <div style={{ marginBottom: 18 }}>
      <input ref={zipRef} type="file" accept=".zip,.html,.htm" style={{ display: "none" }} onChange={(e) => {handleImport(e.target.files[0]);e.target.value = "";}} />
      {app ?
      <div className="card" style={{ padding: 16, display: "flex", alignItems: "center", gap: 14, border: "1.5px solid " + t.color + "55", background: t.color + "0a" }}>
        <div style={{ width: 44, height: 44, borderRadius: 12, background: t.color, display: "grid", placeItems: "center", flexShrink: 0 }}><Icon name="globe" size={21} style={{ color: "#fff" }} /></div>
        <div style={{ flex: 1, minWidth: 0 }}>
          <div style={{ fontWeight: 700, fontSize: 14.5, color: "var(--ink)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{app.name}</div>
          <div style={{ fontSize: 12, color: "var(--ink-3)", marginTop: 2 }}>{app.builtin ? "Sample app · bundled with this prototype" : (app.files || 1) + " files · " + appFmtSize(app.size || 0) + " · imported package"}</div>
          <div style={{ display: "inline-flex", alignItems: "center", gap: 5, marginTop: 6, fontSize: 11.5, fontWeight: 700, color: "#2e7d32", background: "#e8f5e9", borderRadius: 999, padding: "2px 9px" }}><Icon name="check" size={11} />Hosted as the {t.short.toLowerCase()} experience{product ? " · " + product.name : ""}</div>
        </div>
        <button className="btn btn-pri btn-sm" style={{ flexShrink: 0 }} onClick={() => setLaunch(true)}><Icon name="play" size={13} />Launch</button>
        <button className="btn btn-outline btn-sm" style={{ flexShrink: 0 }} disabled={busy} onClick={() => zipRef.current && zipRef.current.click()}><Icon name="download" size={13} style={{ transform: "rotate(180deg)" }} />{busy ? "Importing…" : "Replace"}</button>
        <button className="btn btn-ghost btn-sm" style={{ color: "#c62828", flexShrink: 0 }} onClick={removeApp}><Icon name="x" size={13} />Remove</button>
      </div> :
      <div className="card" style={{ padding: "13px 16px", display: "flex", alignItems: "center", gap: 12, border: "1.5px dashed var(--line)", boxShadow: "none" }}>
        <div style={{ width: 36, height: 36, borderRadius: 10, background: t.color + "1a", display: "grid", placeItems: "center", flexShrink: 0 }}><Icon name="globe" size={17} style={{ color: t.color }} /></div>
        <div style={{ flex: 1, minWidth: 0 }}>
          <div style={{ fontWeight: 700, fontSize: 13.5, color: "var(--ink)" }}>Import a ready-made app</div>
          <div style={{ fontSize: 12, color: "var(--ink-3)" }}>Host a packaged HTML app (<b>.zip</b> with an index.html, or a single <b>.html</b>) as this product — instead of the built-in editor below.</div>
        </div>
        <button className="btn btn-outline btn-sm" style={{ flexShrink: 0 }} disabled={busy} onClick={() => zipRef.current && zipRef.current.click()}><Icon name="download" size={13} style={{ transform: "rotate(180deg)" }} />{busy ? "Importing…" : "Import file"}</button>
      </div>}
      {launch && app && <AppLaunchModal app={app} title={product ? product.name + " · " + t.label : t.label} accent={t.color} onClose={() => setLaunch(false)} />}
    </div>);
}

Object.assign(window, { HostedAppPanel, AppLaunchModal, buildAppUrl });
