<?php
declare(strict_types=1);
function ef_cache_clear():int{$n=0;$dir=EF_ROOT.'/storage/cache';if(!is_dir($dir))mkdir($dir,0755,true);foreach(glob($dir.'/*')?:[] as$f)if(is_file($f)&&unlink($f))$n++;return$n;}
function ef_backup_create():string{$name='ExamsFit_backup_'.date('Ymd_His').'.sql';$path=EF_ROOT.'/storage/backups/'.$name;$out="-- EXAMSFIT SAFE BACKUP\n-- Generated ".date(DATE_ATOM)."\nSET NAMES utf8mb4;\nSET FOREIGN_KEY_CHECKS=0;\n";foreach(all('SHOW TABLES') as$row){$table=array_values($row)[0];$create=all('SHOW CREATE TABLE `'.$table.'`')[0];$out.="DROP TABLE IF EXISTS `{$table}`;\n".array_values($create)[1].";\n";foreach(all('SELECT * FROM `'.$table.'`') as$r){$values=array_map(fn($v)=>$v===null?'NULL':db()->quote((string)$v),array_values($r));$out.='INSERT INTO `'.$table.'` VALUES('.implode(',',$values).');'."\n";}}$out.="SET FOREIGN_KEY_CHECKS=1;\n";if(file_put_contents($path,$out,LOCK_EX)===false)throw new RuntimeException('Backup write failed.');return$name;}
function ef_backup_restore(string$path):void{$sql=(string)file_get_contents($path);if(!str_starts_with($sql,'-- EXAMSFIT SAFE BACKUP')||strlen($sql)>100*1024*1024)throw new RuntimeException('Only an Exams.fit safe backup up to 100 MB can be restored.');if(preg_match('/\b(DROP\s+DATABASE|CREATE\s+DATABASE|GRANT|REVOKE|OUTFILE|INFILE)\b/i',$sql))throw new RuntimeException('Unsafe backup statement blocked.');$pre=ef_backup_create();try{db()->exec($sql);audit('backup.restore',['pre_restore'=>$pre]);}catch(Throwable$e){throw new RuntimeException('Restore failed. Pre-restore backup: '.$pre.'. '.$e->getMessage());}}
function ef_quality_scan():array{$thin=all("SELECT id,title,slug,quality_score FROM content WHERE status='published' AND (CHAR_LENGTH(body)<600 OR quality_score<70) ORDER BY quality_score");$orphans=all("SELECT c.id,c.title,c.slug FROM content c LEFT JOIN content_relations r ON r.related_id=c.id LEFT JOIN sections s ON s.category_id=c.category_id WHERE c.status='published' AND c.external_id NOT LIKE 'EF-SYSTEM-%' AND r.content_id IS NULL AND s.id IS NULL");$dupes=all("SELECT content_hash,COUNT(*) total,GROUP_CONCAT(title SEPARATOR ' | ') titles FROM content GROUP BY content_hash HAVING COUNT(*)>1");$missing=all("SELECT id,title,slug FROM content WHERE status='published' AND (author IS NULL OR author='' OR reviewer IS NULL OR reviewer='' OR meta_description IS NULL OR CHAR_LENGTH(meta_description)<70)");return compact('thin','orphans','dupes','missing');}
function ef_broken_link_scan(int$limit=100):array{$checked=0;$broken=0;q('DELETE FROM broken_links');$rows=all('SELECT id,body FROM content WHERE status="published" ORDER BY updated_at DESC LIMIT '.max(1,min(500,$limit)));foreach($rows as$c){preg_match_all('~href=["\']([^"\']+)~i',$c['body'],$m);foreach(array_unique($m[1]) as$u){$checked++;$code=200;if(str_starts_with($u,'/')){$slug=trim((string)parse_url($u,PHP_URL_PATH),'/');$code=one("SELECT id FROM content WHERE slug=? AND status='published'",[$slug])?200:404;}elseif(str_starts_with($u,'https://')){$ctx=stream_context_create(['http'=>['method'=>'HEAD','timeout'=>5,'ignore_errors'=>true,'user_agent'=>'Exams.fit Link Checker/3.0','max_redirects'=>3],'ssl'=>['verify_peer'=>true,'verify_peer_name'=>true]]);$h=@get_headers($u,false,$ctx);$code=$h&&preg_match('~\s(\d{3})\s~',$h[0],$x)?(int)$x[1]:0;}if($code===0||$code>=400){$broken++;q('INSERT INTO broken_links(content_id,url,status_code,status,checked_at) VALUES(?,?,?,"open",NOW())',[$c['id'],$u,$code]);}}}return compact('checked','broken');}
function ef_analytics_aggregate():int{q('INSERT INTO analytics_daily(day,event,path,events,total_value) SELECT day,event,path,COUNT(*),SUM(value) FROM analytics WHERE day>=DATE_SUB(CURDATE(),INTERVAL 35 DAY) GROUP BY day,event,path ON DUPLICATE KEY UPDATE events=VALUES(events),total_value=VALUES(total_value)');return q('SELECT 1')->rowCount();}
function ef_process_import_queue(int$limit=3):array{$done=0;$failed=0;foreach(all("SELECT * FROM import_queue WHERE status='queued' ORDER BY id LIMIT ".max(1,min(10,$limit))) as$x){q('UPDATE import_queue SET status="running",ran_at=NOW() WHERE id=?',[$x['id']]);try{ef_import_package(EF_ROOT.'/'.$x['file_path'],$x['mode']);q('UPDATE import_queue SET status="complete",message="Imported",ran_at=NOW() WHERE id=?',[$x['id']]);$done++;}catch(Throwable$e){q('UPDATE import_queue SET status="error",message=?,ran_at=NOW() WHERE id=?',[substr($e->getMessage(),0,1000),$x['id']]);$failed++;}}return compact('done','failed');}
