/* scorm-player.jsx — import, host and PLAY a SCORM package (1.2 or 2004).

   Turns an uploaded SCORM .zip into a working, launchable course:
   - readScormZip(file)      → unzip (JSZip), find & parse imsmanifest.xml
   - parseScormManifest(xml) → { title, version, items:[{id,title,href}] } in order
   - ScormPlayer            → a fullscreen player: table-of-contents + hosted SCO
                              iframe + a real SCORM runtime API (window.API /
                              window.API_1484_11) that records completion/score
                              per lesson and persists it per user via VHStore.

   Storage of the package bytes reuses AppStore (server-side /api/objects + an
   IndexedDB mirror), so an imported SCORM course survives a cache-clear and is
   available on any device. Self-contained: does NOT depend on educator-apps.jsx,
   so the same player works in the learner shell. */

/* ── mime + path helpers ── */
const SCORM_MIME = { html: "text/html", htm: "text/html", js: "text/javascript", mjs: "text/javascript", css: "text/css", json: "application/json", xml: "application/xml", 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", eot: "application/vnd.ms-fontobject", txt: "text/plain", md: "text/plain", pdf: "application/pdf", mp4: "video/mp4", webm: "video/webm", mp3: "audio/mpeg", wav: "audio/wav", swf: "application/x-shockwave-flash" };
function scormMime(path) { const m = String(path).toLowerCase().match(/\.([a-z0-9]+)$/); return m && SCORM_MIME[m[1]] || "application/octet-stream"; }
function scormBaseName(u) { u = String(u).split("#")[0].split("?")[0]; const s = u.lastIndexOf("/"); return s >= 0 ? u.slice(s + 1) : u; }
function scormDir(p) { return p.indexOf("/") >= 0 ? p.slice(0, p.lastIndexOf("/") + 1) : ""; }
function scormNormalize(p) {
  const parts = String(p).split("/"), out = [];
  for (const seg of parts) { if (seg === "" || seg === ".") continue; if (seg === "..") out.pop(); else out.push(seg); }
  return out.join("/");
}

/* ── manifest parsing (namespace-tolerant: matches by localName) ── */
function scormText(el) { return el && el.textContent ? el.textContent.trim() : ""; }
function scormKids(node, name) {
  const out = [], ch = node && node.children ? node.children : [];
  for (let i = 0; i < ch.length; i++) if (ch[i].localName === name) out.push(ch[i]);
  return out;
}
function parseScormManifest(xmlText) {
  const doc = new DOMParser().parseFromString(xmlText, "application/xml");
  if (doc.getElementsByTagName("parsererror").length) throw new Error("imsmanifest.xml could not be parsed as XML");
  const manifest = doc.getElementsByTagName("manifest")[0];
  if (!manifest) throw new Error("no <manifest> element — this does not look like a SCORM package");

  const svt = scormText(doc.getElementsByTagName("schemaversion")[0]);
  const version = /2004|1\.3|cam/i.test(svt) ? "2004" : "1.2";

  /* resources: identifier → launch href */
  const resById = {};
  const resources = doc.getElementsByTagName("resource");
  for (let i = 0; i < resources.length; i++) {
    const id = resources[i].getAttribute("identifier");
    if (id) resById[id] = resources[i].getAttribute("href") || "";
  }

  /* default organization */
  const orgsEl = doc.getElementsByTagName("organizations")[0];
  const defId = orgsEl ? orgsEl.getAttribute("default") : null;
  const orgEls = doc.getElementsByTagName("organization");
  let org = null;
  for (let j = 0; j < orgEls.length; j++) { if (!org) org = orgEls[j]; if (defId && orgEls[j].getAttribute("identifier") === defId) { org = orgEls[j]; break; } }

  let title = "SCORM course";
  if (org) { const tk = scormKids(org, "title")[0]; if (tk) title = scormText(tk) || title; }

  /* walk <item>s in document order, keeping only launchable ones */
  const items = [];
  (function walk(node) {
    const its = scormKids(node, "item");
    for (let k = 0; k < its.length; k++) {
      const it = its[k];
      const ref = it.getAttribute("identifierref");
      const tt = scormKids(it, "title")[0];
      const label = (tt ? scormText(tt) : "") || it.getAttribute("identifier") || ("Lesson " + (items.length + 1));
      if (ref && resById[ref]) items.push({ id: it.getAttribute("identifier") || ("i" + items.length), title: label, href: resById[ref] });
      walk(it);
    }
  })(org || manifest);

  if (!items.length) throw new Error("the manifest has no launchable lessons (SCOs)");
  return { title, version, items };
}

/* ── read a SCORM .zip into { files, root, manifest } ── */
async function readScormZip(file) {
  if (!window.JSZip) throw new Error("zip support did not load — check your connection and retry");
  const zip = await JSZip.loadAsync(file);
  const files = {};
  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: scormMime(path) });
  }
  const manPath = Object.keys(files).find((p) => /(^|\/)imsmanifest\.xml$/i.test(p));
  if (!manPath) throw new Error("no imsmanifest.xml found — this is not a SCORM package");
  const root = scormDir(manPath);
  const manifest = parseScormManifest(await files[manPath].text());
  return { files, root, manifest };
}

/* ── build a hostable blob: URL for one SCO: rewrite intra-course navigation to
   the parent player, and relative asset refs to blob URLs ── */
async function buildScoUrl(files, root, href, pageSet) {
  const path = scormNormalize((root || "") + href);
  let blob = files[path];
  if (!blob) { const bn = scormBaseName(path).toLowerCase(); const hit = Object.keys(files).find((p) => scormBaseName(p).toLowerCase() === bn); blob = hit ? files[hit] : null; }
  if (!blob) throw new Error("lesson file not found in the package: " + href);
  let html = await blob.text();
  const dir = scormDir(path);

  /* 1 — intra-course navigation → parent.__scormNav (blob: URLs can't resolve siblings) */
  html = html.replace(/((?:window\.|document\.)?location(?:\.href)?)\s*=\s*(['"])([^'"]+?)\2/gi, (m, lhs, q, url) => {
    const bn = scormBaseName(url); return pageSet[bn.toLowerCase()] ? "parent.__scormNav('" + bn + "')" : m;
  });
  html = html.replace(/href\s*=\s*(['"])([^'"]+?\.html?)(?:#[^'"]*)?\1/gi, (m, q, url) => {
    const bn = scormBaseName(url); return pageSet[bn.toLowerCase()] ? 'href="javascript:void(parent.__scormNav(\'' + bn + '\'))"' : m;
  });

  /* 2 — relative asset refs → blob URLs (longest first) */
  const others = Object.keys(files).filter((p) => p !== path && !/\.html?$/i.test(p)).sort((a, b) => b.length - a.length);
  for (const p of others) {
    const rel = dir && p.indexOf(dir) === 0 ? p.slice(dir.length) : p;
    if (!rel) continue;
    const url = URL.createObjectURL(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" }));
}

/* ── a minimal SCORM 1.2 + 2004 runtime API. Records the CMI data model in
   memory and calls onStatus(scoId, status) whenever lesson_status/completion
   is set, so the player can track completion. ── */
function makeScormApi(getScoId, onStatus, cmi) {
  function setV(key, val) {
    cmi[key] = val;
    const kl = String(key).toLowerCase();
    if (kl.indexOf("lesson_status") >= 0 || kl === "cmi.completion_status" || kl === "cmi.success_status") onStatus(getScoId(), String(val).toLowerCase());
    return "true";
  }
  function getV(key) { return cmi[key] != null ? String(cmi[key]) : ""; }
  return {
    api12: { LMSInitialize: () => "true", LMSFinish: () => "true", LMSGetValue: (k) => getV(k), LMSSetValue: (k, v) => setV(k, v), LMSCommit: () => "true", LMSGetLastError: () => "0", LMSGetErrorString: () => "", LMSGetDiagnostic: () => "" },
    api2004: { Initialize: () => "true", Terminate: () => "true", GetValue: (k) => getV(k), SetValue: (k, v) => setV(k, v), Commit: () => "true", GetLastError: () => "0", GetErrorString: () => "", GetDiagnostic: () => "" },
  };
}
function statusIsComplete(s) { return s === "completed" || s === "passed"; }

/* ── the player ── */
function ScormPlayer({ pkg, appId, storeKey, title, accent, onClose }) {
  const color = accent || "#7c3aed";
  const [rec, setRec] = useState(pkg && pkg.files ? pkg : null);
  const [err, setErr] = useState(null);
  const [idx, setIdx] = useState(0);
  const [src, setSrc] = useState(null);
  const [done, setDone] = useState({});
  const idxRef = React.useRef(0);
  const urlsRef = React.useRef([]);
  const key = storeKey || (appId ? "scorm.progress." + appId : null);

  const items = rec ? rec.manifest.items : [];
  const pageSet = React.useMemo(() => {
    const s = {};
    if (rec) { rec.manifest.items.forEach((it) => { s[scormBaseName(it.href).toLowerCase()] = true; }); Object.keys(rec.files).forEach((p) => { if (/\.html?$/i.test(p)) s[scormBaseName(p).toLowerCase()] = true; }); }
    return s;
  }, [rec]);

  /* load the package (if only an appId was given) + saved progress */
  useEffect(() => {
    let live = true;
    if (!rec && appId && window.AppStore) {
      window.AppStore.get(appId).then((r) => {
        if (!live) return;
        if (!r) { setErr("This SCORM package isn't available on this device yet — re-import the .zip."); return; }
        if (!r.manifest && r.files) { const mp = Object.keys(r.files).find((p) => /imsmanifest\.xml$/i.test(p)); if (mp) { r.files[mp].text().then((x) => { try { r.manifest = parseScormManifest(x); r.root = scormDir(mp); setRec(r); } catch (e) { setErr(e.message); } }); return; } }
        setRec(r);
      }).catch(() => live && setErr("Couldn't load the SCORM package."));
    }
    if (key && window.VHStore) window.VHStore.get(key, null).then((v) => { if (live && v && typeof v === "object") setDone(v); });
    return () => { live = false; };
  }, [appId]);

  /* mount the SCORM runtime API on the window while the player is open */
  useEffect(() => {
    const cmi = {};
    const nav = (basename) => { const bn = scormBaseName(basename).toLowerCase(); const i = items.findIndex((it) => scormBaseName(it.href).toLowerCase() === bn); if (i >= 0) go(i); };
    const api = makeScormApi(() => (items[idxRef.current] ? items[idxRef.current].id : null), markComplete, cmi);
    window.API = api.api12; window.API_1484_11 = api.api2004; window.__scormNav = nav;
    return () => { if (window.API === api.api12) delete window.API; if (window.API_1484_11 === api.api2004) delete window.API_1484_11; if (window.__scormNav === nav) delete window.__scormNav; };
  }, [rec, items.length]);

  /* build the hosted URL for the current lesson */
  useEffect(() => {
    if (!rec || !items.length) return;
    let live = true;
    idxRef.current = idx;
    setSrc(null);
    buildScoUrl(rec.files, rec.root, items[idx].href, pageSet).then((u) => { if (live) { urlsRef.current.push(u); setSrc(u); } }).catch((e) => live && setErr(e.message));
    return () => { live = false; };
  }, [rec, idx]);

  useEffect(() => () => { urlsRef.current.forEach((u) => { try { URL.revokeObjectURL(u); } catch (e) {} }); }, []);

  function saveDone(scoId) {
    setDone((d) => { if (!scoId || d[scoId]) return d; const nd = { ...d, [scoId]: true }; if (key && window.VHStore) window.VHStore.set(key, nd); return nd; });
  }
  function markComplete(scoId, status) { if (statusIsComplete(status)) saveDone(scoId); }
  function completeCurrent() { const it = items[idx]; if (it) saveDone(it.id); }
  function go(i) { if (i >= 0 && i < items.length) setIdx(i); }

  const doneCount = items.filter((it) => done[it.id]).length;
  const allDone = items.length > 0 && doneCount === items.length;

  return ReactDOM.createPortal(
    <div style={{ position: "fixed", inset: 0, zIndex: 720, background: "rgba(13,26,52,.62)", backdropFilter: "blur(3px)", display: "grid", placeItems: "center", padding: "2vh 2vw" }} 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)" }}>
        {/* header */}
        <div style={{ flex: "none", display: "flex", alignItems: "center", gap: 12, padding: "10px 16px", borderBottom: "1px solid var(--line)", background: "var(--surface)" }}>
          <span style={{ width: 30, height: 30, borderRadius: 8, background: color, display: "grid", placeItems: "center", flexShrink: 0 }}><Icon name="stack" size={16} style={{ color: "#fff" }} /></span>
          <div style={{ flex: 1, minWidth: 0 }}>
            <div style={{ fontWeight: 800, fontSize: 14.5, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", color: "var(--ink)" }}>{(rec && rec.manifest.title) || title || "SCORM course"}</div>
            <div style={{ fontSize: 11.5, color: "var(--ink-3)" }}>SCORM {rec ? rec.manifest.version : ""} · {items.length} lesson{items.length === 1 ? "" : "s"} · {doneCount}/{items.length} complete</div>
          </div>
          {items.length > 0 &&
          <div style={{ width: 120, height: 7, borderRadius: 999, background: "var(--surface-2)", overflow: "hidden", flexShrink: 0 }}><div style={{ width: (items.length ? Math.round(doneCount / items.length * 100) : 0) + "%", height: "100%", background: allDone ? "#2e7d32" : color, transition: "width .3s" }} /></div>}
          <button className="btn btn-ghost btn-sm" onClick={onClose} style={{ flexShrink: 0 }}><Icon name="x" size={14} />Close</button>
        </div>

        {err ?
        <div style={{ flex: 1, display: "grid", placeItems: "center", color: "#c62828", fontWeight: 600, fontSize: 14, padding: 30, textAlign: "center" }}>{err}</div> :
        <div style={{ flex: 1, minHeight: 0, display: "flex" }}>
          {/* TOC */}
          <div style={{ width: 270, flexShrink: 0, borderRight: "1px solid var(--line)", background: "var(--surface)", overflowY: "auto", padding: "10px 0" }}>
            {items.map((it, i) =>
            <button key={it.id} onClick={() => go(i)} style={{ width: "100%", textAlign: "left", display: "flex", alignItems: "center", gap: 10, padding: "9px 14px", border: "none", cursor: "pointer", background: i === idx ? color + "12" : "transparent", borderLeft: "3px solid " + (i === idx ? color : "transparent") }}>
              <span style={{ width: 22, height: 22, borderRadius: "50%", flexShrink: 0, display: "grid", placeItems: "center", fontSize: 11, fontWeight: 800, background: done[it.id] ? "#2e7d32" : (i === idx ? color : "var(--surface-2)"), color: done[it.id] || i === idx ? "#fff" : "var(--ink-3)" }}>{done[it.id] ? <Icon name="check" size={12} /> : i + 1}</span>
              <span style={{ fontSize: 12.5, lineHeight: 1.35, color: i === idx ? "var(--ink)" : "var(--ink-2)", fontWeight: i === idx ? 700 : 500 }}>{it.title}</span>
            </button>)}
          </div>
          {/* stage */}
          <div style={{ flex: 1, minWidth: 0, position: "relative", background: "#f4f5f8", display: "flex", flexDirection: "column" }}>
            <div style={{ flex: 1, minHeight: 0, position: "relative" }}>
              {src ?
              <iframe key={src} src={src} title={items[idx] ? items[idx].title : "lesson"} style={{ width: "100%", height: "100%", border: "none", display: "block", background: "#fff" }} /> :
              <div style={{ position: "absolute", inset: 0, display: "grid", placeItems: "center", color: "var(--ink-3)", fontSize: 14, fontWeight: 600 }}>Loading lesson…</div>}
            </div>
            {/* footer nav */}
            <div style={{ flex: "none", display: "flex", alignItems: "center", gap: 10, padding: "9px 14px", borderTop: "1px solid var(--line)", background: "var(--surface)" }}>
              <button className="btn btn-outline btn-sm" disabled={idx === 0} onClick={() => go(idx - 1)}><Icon name="chevL" size={13} />Previous</button>
              <div style={{ flex: 1, textAlign: "center", fontSize: 12.5, fontWeight: 600, color: "var(--ink-2)" }}>{items[idx] ? items[idx].title : ""}{allDone ? <span style={{ color: "#2e7d32", marginLeft: 8 }}>· course complete ✓</span> : null}</div>
              {items[idx] && done[items[idx].id]
                ? <span style={{ display: "inline-flex", alignItems: "center", gap: 5, fontSize: 12.5, fontWeight: 600, color: "#2e7d32" }}><Icon name="check" size={14} />Completed</span>
                : <button className="btn btn-sm" style={{ background: "#2e7d32", borderColor: "#2e7d32", color: "#fff" }} onClick={completeCurrent}><Icon name="check" size={13} />Mark complete</button>}
              <button className="btn btn-outline btn-sm" disabled={idx >= items.length - 1} onClick={() => go(idx + 1)}>Next<Icon name="chevR" size={13} /></button>
            </div>
          </div>
        </div>}
      </div>
    </div>, document.body);
}

Object.assign(window, { parseScormManifest, readScormZip, buildScoUrl, ScormPlayer });
