<?php
declare(strict_types=1);
if (session_status() === PHP_SESSION_NONE) session_start();

require_once __DIR__ . '/../../includes/db.php';
if (is_file(__DIR__ . '/../../includes/auth.php')) {
  require_once __DIR__ . '/../../includes/auth.php';
  if (function_exists('require_role')) require_role(['super_admin','branch_admin']);
}

/* DEBUG (remove in prod) */
ini_set('display_errors','1');
ini_set('display_startup_errors','1');
error_reporting(E_ALL);

/* PHP7 polyfills */
if (!function_exists('str_starts_with')) {
  function str_starts_with($h,$n){ return $n==='' || strpos((string)$h,(string)$n)===0; }
}
if (!function_exists('str_contains')) {
  function str_contains($h,$n){ return $n==='' || strpos((string)$h,(string)$n)!==false; }
}

/* helpers */
function esc($s){ return htmlspecialchars((string)$s, ENT_QUOTES|ENT_SUBSTITUTE,'UTF-8'); }
function t_exists(mysqli $db,string $t): bool {
  $st=$db->prepare("SELECT 1 FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME=? LIMIT 1");
  $st->bind_param('s',$t); $st->execute(); $ok=(bool)$st->get_result()->fetch_row(); $st->close(); return $ok;
}
function c_exists(mysqli $db,string $t,string $c): bool {
  $st=$db->prepare("SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME=? AND COLUMN_NAME=? LIMIT 1");
  $st->bind_param('ss',$t,$c); $st->execute(); $ok=(bool)$st->get_result()->fetch_row(); $st->close(); return $ok;
}
function first_existing(mysqli $db,string $t,array $cols): ?string {
  foreach($cols as $c) if (c_exists($db,$t,$c)) return $c; return null;
}
function detect_content_table(mysqli $db): array {
  foreach (['lms_content','lms_contents','lms_module_contents','module_contents','lms_content_items'] as $T) {
    if (!t_exists($db,$T)) continue;
    $mid   = first_existing($db,$T,['module_id','lms_module_id']);
    $title = first_existing($db,$T,['title','name']);
    $url   = first_existing($db,$T,['url','file_url','path','link']);
    $type  = first_existing($db,$T,['type','content_type']);
    $desc  = first_existing($db,$T,['description','details','desc']);
    $rel   = first_existing($db,$T,['is_released','active','published']);
    if ($mid && $title && $url) return compact('T','mid','title','url','type','desc','rel');
  } return ['T'=>null];
}
function course_list(mysqli $db): array {
  if (!t_exists($db,'courses')) return [];
  $label = first_existing($db,'courses',['course_name','name','title']) ?? 'id';
  $res=$db->query("SELECT id,$label AS label FROM courses ORDER BY label");
  $out=[]; while($res && ($r=$res->fetch_assoc())) $out[]=['id'=>(int)$r['id'],'label'=>$r['label']]; if($res) $res->close();
  return $out;
}
function modules_for_course(mysqli $db,int $cid): array {
  $T = t_exists($db,'lms_modules') ? 'lms_modules' : (t_exists($db,'course_modules') ? 'course_modules' : null);
  if(!$T) return [];
  $title = first_existing($db,$T,['title','name']) ?? 'id';
  $cidc  = first_existing($db,$T,['course_id','cid']) ?? 'course_id';
  $res=$db->query("SELECT id,$title AS title FROM `$T` WHERE `$cidc`=".(int)$cid." ORDER BY id");
  $out=[]; while($res && ($r=$res->fetch_assoc())) $out[]=['id'=>(int)$r['id'],'title'=>$r['title']]; if($res) $res->close();
  return $out;
}
function slugify(string $s): string {
  $s=preg_replace('~[^\pL0-9]+~u','-',$s); $s=trim($s,'-');
  if(function_exists('iconv')) $s=@iconv('UTF-8','ASCII//TRANSLIT',$s);
  $s=strtolower($s); $s=preg_replace('~[^-a-z0-9]+~','',$s);
  return $s ?: ('game-'.date('YmdHis'));
}

/* --------- SAFE Student runner (single-file HTML) --------- */
if (!function_exists('build_runner_html')) {
  function build_runner_html(string $title, string $type, array $data): string {
    $safe = esc($title);
    $json_b64 = base64_encode(json_encode(['type'=>$type,'data'=>$data], JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES));
    $ALL='/elms/student/games/'; $HOME='/elms/student/';
    return <<<HTML
<!doctype html><html lang="en"><head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<meta http-equiv="Content-Security-Policy" content="default-src 'self' data: blob:; img-src 'self' data: blob: *; media-src 'self' data: blob:; font-src 'self' data:; connect-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline' 'unsafe-eval'">
<title>{$safe} · EDEN CAMPUS</title>
<style>
:root{--eden1:#1e3a8a;--eden2:#2563eb;--ink:#0f172a;--bg:#f7f9ff;--card:#fff}
*{box-sizing:border-box}body{margin:0;font-family:system-ui,Segoe UI,Roboto,Arial,sans-serif;background:var(--bg);color:var(--ink)}
.header{background:linear-gradient(90deg,var(--eden1),var(--eden2));color:#fff;padding:14px 16px;display:flex;gap:12px;align-items:center}
.header a{color:#fff;text-decoration:none}.brand{font-weight:800}
.wrap{max-width:980px;margin:20px auto;padding:0 14px}
.h1{font-weight:800}.card{background:var(--card);border:1px solid #e6eaf3;border-radius:14px;padding:16px}
.btn{cursor:pointer;border:1px solid #d7def0;background:#fff;border-radius:10px;padding:8px 12px;font-weight:600}
.btn.primary{background:#2563eb;color:#fff;border-color:transparent}
.ws-grid{display:grid;gap:4px;grid-template-columns:repeat(auto-fit,minmax(24px,24px))}
.ws-cell{width:24px;height:24px;display:grid;place-items:center;border:1px solid #c7d2fe;border-radius:6px}
.cw-row{display:flex;gap:4px;margin:2px 0}.cw-cell input{width:28px;height:28px;text-transform:uppercase;text-align:center;border:1px solid #cbd5e1;border-radius:6px}
.cw-block{width:28px;height:28px;background:#dbeafe;border-radius:6px;border:1px solid #bfdbfe}
.badge{display:inline-block;background:#e5efff;border:1px solid #cfe1ff;padding:4px 8px;border-radius:10px;cursor:pointer}
.ok{outline:2px solid #22c55e}.bad{outline:2px solid #ef4444}
.err{background:#fff3cd;border:1px solid #ffe69c;color:#664d03;padding:10px;border-radius:10px;margin-top:12px;white-space:pre-wrap}
</style>
</head><body>
  <div class="header"><a href="{$ALL}">← All Games / Activities</a><div class="brand">EDEN CAMPUS</div><div style="flex:1"></div><a href="{$HOME}">Student Home</a></div>
  <div class="wrap">
    <div style="display:flex;justify-content:space-between;align-items:center;gap:12px;flex-wrap:wrap">
      <div class="h1">{$safe}</div>
      <div style="display:flex;align-items:center;gap:8px"><div style="width:220px;height:8px;background:#e2e8ff;border-radius:999px;overflow:hidden"><i id="tbar" style="display:block;height:100%;background:#2563eb;width:100%"></i></div><span id="tleft">∞</span></div>
    </div>
    <div class="card" id="app">Loading…</div>
    <div style="display:flex;gap:10px;margin-top:10px"><button class="btn primary" id="btnCheck">Check</button><button class="btn" id="btnReset">Reset</button><button class="btn" id="btnSpin" style="display:none">Spin</button></div>
    <div id="score" style="margin-top:8px;color:#64748b"></div>
    <div id="errorBox"></div>
  </div>
<script>
(function(){const box=document.getElementById('errorBox');function showErr(m,src,l,c,e){const d=document.createElement('div');d.className='err';d.textContent='Script error: '+m+(src?('\\n'+src):'')+(l?(':'+l):'')+(c?(':'+c):'')+(e&&e.stack?('\\n'+e.stack):'');box.appendChild(d);}window.addEventListener('error',e=>{try{showErr(e.message,e.filename,e.lineno,e.colno,e.error);}catch(_){}});window.addEventListener('unhandledrejection',e=>{try{showErr('Unhandled promise rejection','',0,0,e.reason);}catch(_){}});})();
if(!window.CSS) window.CSS={}; if(typeof window.CSS.escape!=='function'){window.CSS.escape=function(s){return String(s).replace(/[^a-zA-Z0-9_\\-]/g, m=>'\\\\'+m)}}
let GD={type:'fill',data:{}};try{GD=JSON.parse(atob("{$json_b64}"))||GD;}catch(e){console.error('JSON decode failed',e);}
const type=(GD.type||'fill').toLowerCase(), data=GD.data||{};
const app=document.getElementById('app'), scoreBox=document.getElementById('score'), tbar=document.getElementById('tbar'), tleft=document.getElementById('tleft');
const btnCheck=document.getElementById('btnCheck'), btnReset=document.getElementById('btnReset'), btnSpin=document.getElementById('btnSpin');
function H(h){const t=document.createElement('template');t.innerHTML=h.trim();return t.content.firstChild}
function score(ok,tot){return 'Score '+ok+' / '+tot}
function esc(s){return String(s).replace(/[&<>"']/g,m=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[m]))}
let inst={check(){return {ok:0,tot:0}},reset(){}};
/* ---- activities (subset needed for all 11 types) ---- */
function act_fill(){const w=H('<div style="line-height:2"></div>');w.innerHTML=String(data.text||'').replace(/\[\[(.+?)\]\]/g,(_,a)=>'<input class="slot" data-ans="'+String(a).replace(/"/g,'&quot;')+'">');app.append(w);return{check(){let ok=0,tot=0;w.querySelectorAll('input').forEach(i=>{tot++;const want=(i.dataset.ans||'').trim().toLowerCase();const got=(i.value||'').trim().toLowerCase();i.classList.remove('ok','bad');if(want&&got===want){ok++;i.classList.add('ok')}else i.classList.add('bad')});scoreBox.textContent=score(ok,tot);return{ok,tot}},reset(){w.querySelectorAll('input').forEach(i=>{i.value='';i.classList.remove('ok','bad')});scoreBox.textContent=''}}}
function act_match(){const pairs=Array.isArray(data.pairs)?data.pairs:[];if(!pairs.length){app.textContent='No pairs.';return inst;}const box=H('<div></div>');pairs.forEach(p=>{const r=H('<div style="display:flex;gap:8px;align-items:center;margin:6px 0"></div>');r.append(H('<div>'+esc(p.left||'')+'</div>'));const s=H('<select class="slot"><option value=""></option></select>');pairs.map(x=>x.right).forEach(v=>s.append(H('<option>'+esc(v||'')+'</option>')));s.dataset.want=String(p.right||'');r.append(s);box.append(r)});app.append(box);return{check(){let ok=0,tot=0;box.querySelectorAll('select').forEach(s=>{tot++;s.classList.remove('ok','bad');if(s.value===s.dataset.want){ok++;s.classList.add('ok')}else s.classList.add('bad')});scoreBox.textContent=score(ok,tot);return{ok,tot}},reset(){box.querySelectorAll('select').forEach(s=>{s.value='';s.classList.remove('ok','bad')});scoreBox.textContent=''}}}
function act_order(){const items=Array.isArray(data.items)?data.items:[];if(!items.length){app.textContent='No items.';return inst;}const ul=H('<div></div>');items.slice().sort(()=>Math.random()-.5).forEach(t=>ul.append(H('<div class="card" style="padding:8px;margin:6px 0;display:flex;gap:8px;align-items:center"><span>☰</span><div>'+esc(t)+'</div></div>')));app.append(ul);let drag=null;ul.querySelectorAll('.card').forEach(li=>{li.draggable=true;li.addEventListener('dragstart',e=>{drag=li;e.dataTransfer.effectAllowed='move'});li.addEventListener('dragover',e=>{e.preventDefault();const rect=li.getBoundingClientRect();const after=(e.clientY-rect.top)>rect.height/2;ul.insertBefore(drag,after?li.nextSibling:li);})});return{check(){const got=[...ul.children].map(n=>n.children[1].textContent);let ok=0;for(let i=0;i<items.length;i++) if((items[i]||'')===(got[i]||'')) ok++;scoreBox.textContent=score(ok,items.length);return{ok,tot:items.length}},reset(){ul.innerHTML='';items.slice().sort(()=>Math.random()-.5).forEach(t=>ul.append(H('<div class="card" style="padding:8px;margin:6px 0;display:flex;gap:8px;align-items:center"><span>☰</span><div>'+esc(t)+'</div></div>')));scoreBox.textContent=''}}}
function act_word(){const box=H('<div></div>');const p=H('<div style="line-height:2"></div>');p.innerHTML=String(data.text||'').replace(/\[\[(.+?)\]\]/g,()=>'<input class="slot">');const bank=H('<div style="display:flex;flex-wrap:wrap;gap:8px;margin-top:8px"></div>');(Array.isArray(data.bank)?data.bank:[]).forEach(w=>bank.append(H('<span class="badge" data-w="'+esc(w)+'">'+esc(w)+'</span>')));box.append(p,bank);bank.addEventListener('click',e=>{const b=e.target.closest('.badge');if(!b)return;const empty=[...p.querySelectorAll('input')].find(i=>!i.value);if(empty) empty.value=b.dataset.w||'';});app.append(box);return{check(){let ok=0,tot=0;const ans=(String(data.text||'').match(/\[\[(.+?)\]\]/g)||[]).map(s=>s.slice(2,-2).trim().toLowerCase());p.querySelectorAll('input').forEach((i,idx)=>{tot++;const g=(i.value||'').trim().toLowerCase();i.classList.remove('ok','bad');if(g&&g===ans[idx]){ok++;i.classList.add('ok')}else i.classList.add('bad')});scoreBox.textContent=score(ok,tot);return{ok,tot}},reset(){p.querySelectorAll('input').forEach(i=>{i.value='';i.classList.remove('ok','bad')});scoreBox.textContent=''}}}
function act_mcq(){const qs=Array.isArray(data.questions)?data.questions:[];if(!qs.length){app.textContent='No questions.';return inst;}const box=H('<div></div>');qs.forEach((q,i)=>{const b=H('<div class="card" style="margin:8px 0"></div>');b.append(H('<div style="margin-bottom:6px;color:#64748b">'+esc(q.q||('Q'+(i+1)))+'</div>'));const ul=H('<div></div>');(Array.isArray(q.choices)?q.choices:[]).forEach((c,idx)=>{ul.append(H('<div><label><input type="radio" name="mcq_'+i+'" value="'+idx+'"> '+esc(c)+'</label></div>'))});b.append(ul);box.append(b)});app.append(box);return{check(){let ok=0,tot=qs.length;qs.forEach((q,i)=>{const a=box.querySelector('input[name="mcq_'+i+'"]:checked');const got=a?parseInt(a.value,10):-1;const want=parseInt(q.ans??-1,10);if(got===want) ok++;});scoreBox.textContent=score(ok,tot);return{ok,tot}},reset(){box.querySelectorAll('input[type=radio]').forEach(r=>{r.checked=false});scoreBox.textContent=''}}}
function act_truefalse(){const qs=Array.isArray(data.questions)?data.questions:[];if(!qs.length){app.textContent='No questions.';return inst;}const box=H('<div></div>');qs.forEach((q,i)=>{const b=H('<div class="card" style="margin:8px 0"></div>');b.append(H('<div style="margin-bottom:6px;color:#64748b">'+esc(q.q||('Q'+(i+1)))+'</div>'));b.append(H('<label style="margin-right:8px"><input type="radio" name="tf_'+i+'" value="true"> True</label>'));b.append(H('<label><input type="radio" name="tf_'+i+'" value="false"> False</label>'));box.append(b)});app.append(box);return{check(){let ok=0,tot=qs.length;qs.forEach((q,i)=>{const a=box.querySelector('input[name="tf_'+i+'"]:checked');const got=a?(a.value==='true'):null;const want=!!q.ans;if(got===want) ok++;});scoreBox.textContent=score(ok,tot);return{ok,tot}},reset(){box.querySelectorAll('input[type=radio]').forEach(r=>{r.checked=false});scoreBox.textContent=''}}}
function act_draglabel(){const targets=Array.isArray(data.targets)?data.targets:[];const bank=Array.isArray(data.bank)?data.bank:[];if(!targets.length||!bank.length){app.textContent='No drag/label data.';return inst;}const box=H('<div></div>');const zoneWrap=H('<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:10px"></div>');targets.forEach(t=>{const z=H('<div class="card" style="min-height:56px;display:flex;align-items:center;justify-content:space-between;gap:8px;padding:10px"></div>');z.append(H('<div>'+esc(t.label||'Target')+'</div>'));const drop=H('<div class="badge" style="min-width:90px;background:#fff;border-style:dashed" draggable="false">Drop</div>');drop.dataset.want=String(t.id||'');z.append(drop);zoneWrap.append(z)});const bankWrap=H('<div style="display:flex;flex-wrap:wrap;gap:8px;margin-top:8px"></div>');bank.forEach(b=>{const chip=H('<span class="badge" draggable="true" data-id="'+esc(b.id)+'">'+esc(b.label||b.text||('ID '+b.id))+'</span>');bankWrap.append(chip)});box.append(zoneWrap,H('<div style="color:#64748b;margin-top:8px">Drag from bank onto targets.</div>'),bankWrap);app.append(box);function onDragStart(e){const el=e.target.closest('.badge');if(!el)return;e.dataTransfer.setData('text/plain',el.dataset.id||'')}function onDrop(e){e.preventDefault();const id=e.dataTransfer.getData('text/plain');const drop=e.currentTarget;drop.textContent=(bank.find(x=>String(x.id)===String(id))?.label)||('ID '+id);drop.dataset.got=id;drop.style.background='#eef6ff'}function onDragOver(e){e.preventDefault()}bankWrap.addEventListener('dragstart',onDragStart);zoneWrap.querySelectorAll('.badge').forEach(d=>{d.addEventListener('dragover',onDragOver);d.addEventListener('drop',onDrop)});return{check(){let ok=0,tot=targets.length;zoneWrap.querySelectorAll('.badge').forEach(d=>{d.classList.remove('ok','bad');if((d.dataset.got||'')===(d.dataset.want||'')){ok++;d.classList.add('ok')}else d.classList.add('bad')});scoreBox.textContent=score(ok,tot);return{ok,tot}},reset(){zoneWrap.querySelectorAll('.badge').forEach(d=>{d.textContent='Drop';d.removeAttribute('data-got');d.style.background='#fff';d.classList.remove('ok','bad')});scoreBox.textContent=''}}}
function act_wordsearch(){const grid=Array.isArray(data.grid)?data.grid.map(r=>String(r)):[];const words=(Array.isArray(data.words)?data.words:[]).map(w=>String(w||'').toUpperCase()).filter(Boolean);if(!grid.length||!words.length){app.textContent='No grid/words.';return inst;}const rows=grid.length,cols=grid[0].length;const wrap=H('<div></div>');const gEl=H('<div class="ws-grid"></div>');const selected=new Set();for(let r=0;r<rows;r++){for(let c=0;c<cols;c++){const ch=(grid[r][c]||' ').toUpperCase();const cell=H('<div class="ws-cell" data-r="'+r+'" data-c="'+c+'">'+esc(ch)+'</div>');cell.addEventListener('mousedown',()=>toggle(cell));cell.addEventListener('mouseover',e=>{if(mouseDown)toggle(cell,true)});gEl.append(cell)}}let mouseDown=false;document.addEventListener('mousedown',()=>mouseDown=true);document.addEventListener('mouseup',()=>mouseDown=false);function key(r,c){return r+','+c}function toggle(cell,drag=false){if(!mouseDown&&drag)return;const k=key(parseInt(cell.dataset.r,10),parseInt(cell.dataset.c,10));if(selected.has(k)){selected.delete(k);cell.style.background='#fff';}else{selected.add(k);cell.style.background='#dbebff';}}const bank=H('<div style="display:flex;flex-wrap:wrap;gap:8px;margin-top:8px"></div>');words.forEach(w=>bank.append(H('<span class="badge" data-w="'+esc(w)+'">'+esc(w)+'</span>')));wrap.append(gEl,bank,H('<div style="color:#64748b;margin-top:6px">Select letters; a word is correct if any full occurrence is selected.</div>'));app.append(wrap);const dirs=[[1,0],[-1,0],[0,1],[0,-1],[1,1],[-1,-1],[1,-1],[-1,1]];function findOccurrences(w){const out=[];for(let r=0;r<rows;r++)for(let c=0;c<cols;c++){for(const[dr,dc]of dirs){let ok=true,coords=[];for(let k=0;k<w.length;k++){const rr=r+dr*k,cc=c+dc*k;if(rr<0||cc<0||rr>=rows||cc>=cols||(grid[rr][cc]||' ').toUpperCase()!==w[k]){ok=false;break}coords.push(key(rr,cc))}if(ok)out.push(coords)}}return out}return{check(){let ok=0,tot=words.length;words.forEach(w=>{const occ=findOccurrences(w);let hit=false;for(const coords of occ){if(coords.every(k=>selected.has(k))){hit=true;break}}const chip=bank.querySelector('[data-w="'+CSS.escape(w)+'"]');if(hit){ok++;chip?.classList.add('ok');chip?.classList.remove('bad')}else{chip?.classList.remove('ok');chip?.classList.add('bad')}});scoreBox.textContent=score(ok,tot);return{ok,tot}},reset(){selected.clear();gEl.querySelectorAll('.ws-cell').forEach(c=>c.style.background='#fff');bank.querySelectorAll('.badge').forEach(b=>b.classList.remove('ok','bad'));scoreBox.textContent=''}}}
function act_memory(){let cards=Array.isArray(data.cards)?data.cards.slice():[];if(!cards.length&&Array.isArray(data.pairs)){cards=[];data.pairs.forEach((lab,i)=>{cards.push({id:'p'+i,label:String(lab)});cards.push({id:'p'+i,label:String(lab)});});}if(cards.length<2){app.textContent='No cards.';return inst;}cards=cards.slice().sort(()=>Math.random()-.5);const grid=H('<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(80px,1fr));gap:8px"></div>');cards.forEach((c,i)=>{const el=H('<div style="height:80px;display:grid;place-items:center;border:1px solid #d7def0;border-radius:10px;background:#f8fbff;cursor:pointer;font-weight:700" data-id="'+esc(c.id)+'" data-idx="'+i+'">?</div>');grid.append(el)});app.append(grid);let open=[],matched=new Set();function render(el){const idx=parseInt(el.dataset.idx,10);const c=cards[idx];if(matched.has(idx)){el.style.background='#e7ffe9';el.textContent=c.label}else if(open.includes(idx)){el.classList.add('open');el.textContent=c.label}else{el.classList.remove('open');el.textContent='?'}}grid.addEventListener('click',e=>{const el=e.target.closest('div[data-idx]');if(!el)return;const idx=parseInt(el.dataset.idx,10);if(matched.has(idx)||open.includes(idx))return;open.push(idx);render(el);if(open.length===2){const[a,b]=open;if(cards[a].id===cards[b].id){matched.add(a);matched.add(b);grid.querySelectorAll('div[data-idx]').forEach(render)}setTimeout(()=>{open=[];grid.querySelectorAll('div[data-idx]').forEach(render)},600)}});return{check(){const pairs=Math.floor(cards.length/2);const m=matched.size/2;scoreBox.textContent=score(m,pairs);return{ok:m,tot:pairs}},reset(){open=[];matched.clear();cards=cards.slice().sort(()=>Math.random()-.5);grid.innerHTML='';cards.forEach((c,i)=>grid.append(H('<div style="height:80px;display:grid;place-items:center;border:1px solid #d7def0;border-radius:10px;background:#f8fbff;cursor:pointer;font-weight:700" data-id="'+esc(c.id)+'" data-idx="'+i+'">?</div>')));scoreBox.textContent=''}}}
function act_wheel(){const segs=(Array.isArray(data.segments)?data.segments:[]).map(s=>String(s)).filter(Boolean);if(!segs.length){app.textContent='No segments.';return inst;}const list=H('<div style="display:flex;gap:8px;flex-wrap:wrap"></div>');segs.forEach(s=>list.append(H('<span class="badge">'+esc(s)+'</span>')));const result=H('<div style="color:#64748b;margin-top:8px">Click SPIN.</div>');app.append(list,result);btnSpin.style.display='inline-block';btnSpin.onclick=()=>{result.textContent='Result: '+segs[Math.floor(Math.random()*segs.length)]};return{check(){scoreBox.textContent='';return{ok:0,tot:0}},reset(){result.textContent='Click SPIN.'}}}
function act_crossword(){const rows=Array.isArray(data.rows)?data.rows.map(r=>String(r)):[];const sol=Array.isArray(data.solution)?data.solution.map(r=>String(r).toUpperCase()):[];if(!rows.length||!sol.length||rows.length!==sol.length||rows[0].length!==sol[0].length){app.textContent='Crossword data missing.';return inst;}const R=rows.length,C=rows[0].length;const grid=H('<div></div>');let total=0;for(let r=0;r<R;r++){const row=H('<div class="cw-row"></div>');for(let c=0;c<C;c++){if(rows[r][c]==='#'){row.append(H('<div class="cw-block"></div>'))}else{total++;row.append(H('<div class="cw-cell"><input maxlength="1" data-r="'+r+'" data-c="'+c+'"></div>'))}}grid.append(row)}app.append(grid);return{check(){let ok=0;grid.querySelectorAll('input').forEach(i=>{const r=parseInt(i.dataset.r,10),c=parseInt(i.dataset.c,10);const want=sol[r][c]||'#';const got=(i.value||'').toUpperCase();i.classList.remove('ok','bad');if(got&&got===want){ok++;i.classList.add('ok')}else i.classList.add('bad')});scoreBox.textContent=score(ok,total);return{ok,tot:total}},reset(){grid.querySelectorAll('input').forEach(i=>{i.value='';i.classList.remove('ok','bad')});scoreBox.textContent=''}}}
/* map + mount */
const map={fill:act_fill,match:act_match,order:act_order,word:act_word,mcq:act_mcq,truefalse:act_truefalse,draglabel:act_draglabel,wordsearch:act_wordsearch,memory:act_memory,wheel:act_wheel,crossword:act_crossword};
function mount(){try{app.innerHTML='';scoreBox.textContent='';const fn=map[type]||act_fill;inst=fn();}catch(e){console.error(e);const b=document.getElementById('errorBox');const d=document.createElement('div');d.className='err';d.textContent='Render error: '+(e&&e.message?e.message:e);b.appendChild(d);}}
mount();
/* timer */
const DUR=Math.max(0, parseInt(data.duration||0,10) || 0);
if (DUR>0){let t=DUR;tleft.textContent=t+'s';tbar.style.width='100%';const iv=setInterval(()=>{t=Math.max(0,t-1);tleft.textContent=t+'s';tbar.style.width=(t*100/DUR)+'%';if(t<=0){clearInterval(iv);btnCheck.click();}},1000);}else{tleft.textContent='∞';tbar.style.width='100%'}
btnCheck.addEventListener('click',()=>{const {ok,tot}=inst.check();scoreBox.textContent=score(ok,tot);});
btnReset.addEventListener('click',()=>{inst.reset();});
</script></body></html>
HTML;
  }
}

/* DB + detection */
$mysqli = $mysqli ?? ($conn ?? new mysqli('localhost','root','','edencamp_edencampus'));
if ($mysqli instanceof mysqli) $mysqli->set_charset('utf8mb4');
$CT = detect_content_table($mysqli);

/* inline preview (no save) */
if (($_GET['preview_runner'] ?? '') === '1' && $_SERVER['REQUEST_METHOD']==='POST') {
  $in = json_decode(file_get_contents('php://input'), true);
  $ptitle  = (string)($in['title'] ?? 'Preview');
  $payload = (array)($in['payload'] ?? ['type'=>'fill','data'=>['text'=>'[[example]]']] );
  echo build_runner_html($ptitle, (string)($payload['type'] ?? 'fill'), (array)($payload['data'] ?? []));
  exit;
}

/* inputs */
$course_id = (int)($_GET['course_id'] ?? $_POST['course_id'] ?? 0);
$module_id = (int)($_GET['module_id'] ?? $_POST['module_id'] ?? 0);
$allowed_types = ['fill','match','order','word','mcq','truefalse','draglabel','wordsearch','memory','wheel','crossword'];
$errors = [];

/* existing list */
$EXISTING = [];
if ($CT['T'] && $course_id>0 && $module_id>0) {
  $ttl=$CT['title']; $url=$CT['url']; $mid=$CT['mid'];
  $sql="SELECT `$ttl` AS title, `$url` AS url FROM `{$CT['T']}` WHERE `$mid`=? AND `$url` LIKE '%/uploads/games/%' ORDER BY `$ttl`";
  if ($st=$mysqli->prepare($sql)) { $st->bind_param('i',$module_id); $st->execute(); $r=$st->get_result(); while($row=$r->fetch_assoc()) $EXISTING[]=$row; $st->close(); }
}

/* load for edit */
$loaded = ['slug'=>'','title'=>'','type'=>'fill','data'=>null,'duration'=>0];
if (isset($_GET['load']) && is_string($_GET['load'])) {
  $loadUrl = (string)$_GET['load'];
  $root = realpath(dirname(__DIR__,2)); $gamesDir = $root . '/uploads/games';
  $path = parse_url($loadUrl, PHP_URL_PATH) ?: '';
  $file = $path && str_starts_with($path,'/elms/') ? $root . substr($path, strlen('/elms')) : '';
  $realGames = realpath($gamesDir) ?: ''; $realFile  = is_file($file) ? (realpath($file) ?: '') : '';
  if (!$realFile || strpos($realFile, $realGames) !== 0) $file = $gamesDir . '/' . basename($path);
  if (is_file($file)) {
    $html = file_get_contents($file);
    if (preg_match('~<script[^>]*>.*?atob\("([^"]+)"\).*?</script>~si', $html, $m)) {
      $payload = json_decode(base64_decode($m[1]), true);
      $loaded['type'] = (string)($payload['type'] ?? 'fill');
      $loaded['data'] = is_array($payload['data']??null) ? $payload['data'] : [];
      $loaded['duration'] = (int)($loaded['data']['duration'] ?? 0);
    }
    if (preg_match('~<title>(.*?)</title>~si',$html,$m2)) $loaded['title']=trim(strip_tags($m2[1]));
    else $loaded['title']=str_replace('-',' ', pathinfo($file, PATHINFO_FILENAME));
    $loaded['slug']=pathinfo($file, PATHINFO_FILENAME);
  }
}

/* save */
if ($_SERVER['REQUEST_METHOD']==='POST' && isset($_POST['save_game'])) {
  $title    = trim((string)($_POST['title'] ?? 'Untitled Game'));
  $desc     = trim((string)($_POST['description'] ?? ''));
  $type     = (string)($_POST['game_type'] ?? 'fill');
  $duration = (int)($_POST['duration'] ?? 0);
  $payload_raw = (string)($_POST['payload'] ?? '');

  if ($title==='') $errors[] = 'Title is required.';
  if (!in_array($type, $allowed_types, true)) $errors[] = 'Invalid activity type selected.';
  $data=null;
  if ($payload_raw==='') $errors[]='Payload (JSON) is required.';
  else {
    $json = json_decode($payload_raw, true);
    if (!is_array($json)) $errors[]='Payload must be valid JSON.';
    else {
      if (array_key_exists('type',$json) && array_key_exists('data',$json)) {
        if (!in_array((string)$json['type'], $allowed_types, true)) $errors[]='Payload.type is invalid.';
        else { $type=(string)$json['type']; $data=is_array($json['data'])?$json['data']:[]; }
      } else { $data=$json; }
    }
  }
  if (is_array($data)) $data['duration']=max(0,$duration);

  if (!$errors) {
    $root = realpath(dirname(__DIR__,2));
    $GAME_DIR = $root . '/uploads/games';
    if (!is_dir($GAME_DIR) && !@mkdir($GAME_DIR, 0775, true)) die('ERROR: Cannot create folder: '.$GAME_DIR);
    if (!is_writable($GAME_DIR)) die('ERROR: Folder not writable: '.$GAME_DIR);

    $slug = trim((string)($_POST['loaded_slug'] ?? ''));
    $overwrite = isset($_POST['overwrite']) && $_POST['overwrite']=='1';
    if (!$overwrite || !$slug) $slug = slugify($title);

    $file_path = $GAME_DIR . '/' . $slug . '.html';
    $html = build_runner_html($title,$type,$data);
    if (file_put_contents($file_path,$html) === false) $errors[]='Failed to write the game file.';
    else {
      $game_url = '/elms/uploads/games/' . basename($file_path);
      if ($CT['T'] && $course_id>0 && $module_id>0) {
        $mid=$CT['mid']; $tt=$CT['title']; $uu=$CT['url'];
        $descCol = $CT['desc'] ?? null; $typeCol = $CT['type'] ?? null; $relCol = $CT['rel'] ?? null;

        $sets="`$tt`=?,`$uu`=?"; $types='ss'; $vals=[$title,$game_url];
        if ($descCol){ $sets.=",`$descCol`=?"; $types.='s'; $vals[]=$desc; }
        if ($typeCol){ $sets.=",`$typeCol`=?"; $types.='s'; $vals[]='Game'; }
        if ($relCol){ $sets.=",`$relCol`=?"; $types.='i'; $vals[]=1; }

        $sqlU="UPDATE `{$CT['T']}` SET $sets WHERE `$mid`=? AND `$uu`=?";
        $stU=$mysqli->prepare($sqlU); if(!$stU) die('SQL PREPARE failed: '.$mysqli->error);
        $typesU=$types.'is'; $valsU=array_merge($vals,[$module_id,$game_url]);
        if(!$stU->bind_param($typesU, ...$valsU)) die('SQL BIND failed: '.$stU->error);
        if(!$stU->execute()) die('SQL EXECUTE failed: '.$stU->error);
        $aff=$stU->affected_rows; $stU->close();

        if ($aff<=0) {
          $cols = [$mid=>$module_id, $tt=>$title, $uu=>$game_url];
          if ($descCol) $cols[$descCol]=$desc;
          if ($typeCol) $cols[$typeCol]='Game';
          if ($relCol)  $cols[$relCol]=1;
          $fields=implode(',',array_map(fn($k)=>"`$k`",array_keys($cols)));
          $place =implode(',',array_fill(0,count($cols),'?'));
          $typesI=''; $valsI=[];
          foreach($cols as $v){ $typesI .= is_int($v)?'i':'s'; $valsI[]=$v; }
          $st=$mysqli->prepare("INSERT INTO `{$CT['T']}` ($fields) VALUES ($place)");
          if(!$st) die('SQL PREPARE (insert) failed: '.$mysqli->error);
          if(!$st->bind_param($typesI, ...$valsI)) die('SQL BIND (insert) failed: '.$st->error);
          if(!$st->execute()) die('SQL EXECUTE (insert) failed: '.$st->error);
          $st->close();
        }
      }
      header("Location: game_builder.php?course_id=$course_id&module_id=$module_id&file=".rawurlencode($game_url)."#builder");
      exit;
    }
  }
}

/* UI vars */
$COURSES = course_list($mysqli);
$MODULES = $course_id ? modules_for_course($mysqli,$course_id) : [];
$PAGE_TITLE='Game Builder (Lite)';
?>
<!doctype html><html lang="en"><head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title><?= esc($PAGE_TITLE) ?></title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body class="bg-light">
<div class="container py-4" id="builder">
  <div class="d-flex align-items-center mb-3 gap-2">
    <h3 class="m-0"><?= esc($PAGE_TITLE) ?></h3>
    <?php if (isset($_GET['file'])): ?>
      <a class="btn btn-success btn-sm" target="_blank" href="<?= esc((string)$_GET['file']) ?>">Open saved file</a>
    <?php endif; ?>
    <?php if ($course_id && $module_id): ?>
      <a class="btn btn-outline-secondary btn-sm ms-auto" href="content.php?course_id=<?= (int)$course_id ?>&module_id=<?= (int)$module_id ?>">← Back to Content</a>
    <?php endif; ?>
  </div>

  <div class="card mb-3"><div class="card-body">
    <form method="get" class="row g-2">
      <div class="col-md-6">
        <label class="form-label">Course</label>
        <select name="course_id" class="form-select" onchange="this.form.submit()">
          <option value="">Choose…</option>
          <?php foreach($COURSES as $c): ?>
            <option value="<?= (int)$c['id'] ?>" <?= (int)$c['id']===$course_id?'selected':'' ?>><?= esc($c['label']) ?></option>
          <?php endforeach; ?>
        </select>
      </div>
      <div class="col-md-6">
        <label class="form-label">Module</label>
        <select name="module_id" class="form-select">
          <option value="">Choose…</option>
          <?php foreach($MODULES as $m): ?>
            <option value="<?= (int)$m['id'] ?>" <?= (int)$m['id']===$module_id?'selected':'' ?>><?= esc($m['title']) ?></option>
          <?php endforeach; ?>
        </select>
      </div>
      <div class="col-12"><button class="btn btn-primary">Go</button></div>
    </form>
  </div></div>

  <?php if ($errors): ?>
    <div class="alert alert-danger">
      <b>Fix the following:</b>
      <ul class="mb-0"><?php foreach($errors as $e): ?><li><?= esc($e) ?></li><?php endforeach; ?></ul>
    </div>
  <?php endif; ?>

  <?php if ($course_id<=0 || $module_id<=0): ?>
    <div class="alert alert-info">Select a course and module to manage games.</div>
  <?php else: ?>

  <div class="card mb-3">
    <div class="card-header">Course #<?= (int)$course_id ?> • Module #<?= (int)$module_id ?></div>
    <div class="card-body">
      <form method="post">
        <input type="hidden" name="save_game" value="1">
        <input type="hidden" name="course_id" value="<?= (int)$course_id ?>">
        <input type="hidden" name="module_id" value="<?= (int)$module_id ?>">

        <?php if (!empty($loaded['slug'])): ?>
          <input type="hidden" name="loaded_slug" value="<?= esc($loaded['slug']) ?>">
          <input type="hidden" name="overwrite" value="1">
          <div class="alert alert-warning">Editing <b><?= esc($loaded['slug']) ?>.html</b>. Saving will overwrite this file.</div>
        <?php endif; ?>

        <div class="row g-3">
          <div class="col-md-6">
            <label class="form-label">Title</label>
            <input type="text" name="title" class="form-control" required value="<?= esc($loaded['title'] ?: '') ?>">
          </div>
          <div class="col-md-3">
            <label class="form-label">Activity Type</label>
            <select name="game_type" class="form-select">
              <?php foreach(['fill','match','order','word','mcq','truefalse','draglabel','wordsearch','memory','wheel','crossword'] as $t): ?>
                <option value="<?= $t ?>" <?= ($loaded['type']===$t?'selected':'') ?>><?= strtoupper($t) ?></option>
              <?php endforeach; ?>
            </select>
          </div>
          <div class="col-md-3">
            <label class="form-label">Timer (seconds)</label>
            <input type="number" min="0" step="1" name="duration" class="form-control" value="<?= (int)$loaded['duration'] ?>">
          </div>

          <div class="col-12">
            <label class="form-label">Payload JSON (data-only or {"type","data"})</label>
            <textarea name="payload" rows="12" class="form-control" spellcheck="false"><?= esc(json_encode($loaded['data'] ?? (object)[], JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES)) ?></textarea>
            <div class="form-text">
              Examples: <code>{"text":"Hello [[world]]"}</code> (fill) •
              <code>{"grid":["NETWORKXX","SECURITYX","XFIREWALL"],"words":["NETWORK","SECURITY","FIREWALL"]}</code>
            </div>
          </div>

          <div class="col-12 d-flex gap-2">
            <button class="btn btn-primary">Save</button>
            <a class="btn btn-outline-secondary" target="_blank" href="?preview_runner=1" onclick="event.preventDefault(); (async ()=>{
              const f=this.closest('form');
              const title=f.querySelector('input[name=title]').value||'Preview';
              const type=f.querySelector('select[name=game_type]').value||'fill';
              let data={}; try{ data=JSON.parse(f.querySelector('textarea[name=payload]').value||'{}'); }catch(e){alert('Invalid JSON'); return;}
              const full={title, payload: (data && data.type && data.data) ? data : {type,data}};
              const res=await fetch('?preview_runner=1',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(full)});
              const html=await res.text(); const w=window.open('','_blank'); if(w){w.document.open(); w.document.write(html); w.document.close();} else alert('Popup blocked.');
            })();">Preview (no save)</a>
          </div>
        </div>
      </form>
    </div>
  </div>

  <div class="card">
    <div class="card-header">Existing Games</div>
    <div class="card-body">
      <?php if(!$EXISTING): ?>
        <p class="text-muted m-0">No games yet.</p>
      <?php else: ?>
        <div class="table-responsive">
          <table class="table table-sm align-middle">
            <thead class="table-light"><tr><th>Title</th><th>Actions</th></tr></thead>
            <tbody>
              <?php foreach($EXISTING as $g): $url=(string)$g['url']; ?>
              <tr>
                <td><?= esc($g['title']) ?></td>
                <td class="d-flex gap-2">
                  <a class="btn btn-outline-secondary btn-sm" target="_blank" href="<?= esc($url) ?>">Open</a>
                  <a class="btn btn-outline-primary btn-sm" href="game_builder.php?course_id=<?= (int)$course_id ?>&module_id=<?= (int)$module_id ?>&load=<?= urlencode($url) ?>#builder">Edit</a>
                  <a class="btn btn-outline-danger btn-sm" onclick="return confirm('Delete this game?')" href="game_delete.php?course_id=<?= (int)$course_id ?>&module_id=<?= (int)$module_id ?>&url=<?= urlencode($url) ?>">Delete</a>
                </td>
              </tr>
              <?php endforeach; ?>
            </tbody>
          </table>
        </div>
      <?php endif; ?>
      <?php if (!$CT['T']): ?><div class="text-danger mt-2">No content table detected — files will save only (won’t appear in LMS list).</div><?php endif; ?>
    </div>
  </div>

  <?php endif; ?>
</div>
</body></html>
