<?php
declare(strict_types=1);

function ef_admin(string $route): void
{
    global $config;
    $route = trim($route, '/');
    if ($route === 'login') { ef_admin_login(); return; }
    if ($route === 'logout') {
        $_SESSION = [];
        session_destroy();
        header('Location: '.url(($config['admin_path'] ?? 'adminji').'/login'));
        return;
    }
    require_admin();
    try {
        if ($_SERVER['REQUEST_METHOD'] === 'POST') { csrf_check(); ef_admin_post($route); return; }
        if ($route === 'template') { ef_template_download(); return; }
        if ($route === 'addon-template') { ef_addon_template_download(); return; }
        if ($route === 'example') { ef_example_download(); return; }
        if ($route === 'blog-template') { ef_named_zip_download('blog-bulk-template.zip','ExamsFit_Blog_Bulk_Import_Template_v3.1.0.zip'); return; }
        if ($route === 'tools-template') { ef_named_zip_download('tools-bulk-template.zip','ExamsFit_Tools_Bulk_Import_Template_v3.1.0.zip'); return; }
        if ($route === 'backup-download') { ef_backup_download(); return; }
        $pages = [
            ''=>'Dashboard','content'=>'Content review','blog'=>'Blog manager','tools'=>'Tools manager','import'=>'ZIP importer','imports'=>'Import history',
            'templates'=>'Template center','addons'=>'Import add-ons','sections'=>'Homepage sections','menus'=>'Menus',
            'media'=>'Media & PDFs','videos'=>'Video lessons','feeds'=>'Feed fetcher','seo'=>'SEO & sitemaps',
            'redirects'=>'Redirects','ads'=>'Ad manager','analytics'=>'Analytics','reports'=>'Trust & quality',
            'cron'=>'Automation & cron','maintenance'=>'System updater & backups','settings'=>'Settings'
        ];
        $title = $pages[$route] ?? 'Dashboard';
        $body = match ($route) {
            'content'=>ef_admin_content(), 'blog'=>ef_admin_blog(), 'tools'=>ef_admin_tools(), 'import'=>ef_admin_import(), 'imports'=>ef_admin_imports(),
            'templates'=>ef_admin_templates(), 'addons'=>ef_admin_addons(), 'sections'=>ef_admin_sections(),
            'menus'=>ef_admin_menus(), 'media'=>ef_admin_media(), 'videos'=>ef_admin_videos(),
            'feeds'=>ef_admin_feeds(), 'seo'=>ef_admin_seo(), 'redirects'=>ef_admin_redirects(),
            'ads'=>ef_admin_ads(), 'analytics'=>ef_admin_analytics(), 'reports'=>ef_admin_reports(),
            'cron'=>ef_admin_cron(), 'maintenance'=>ef_admin_maintenance(), 'settings'=>ef_admin_settings(),
            default=>ef_admin_dashboard()
        };
        ef_admin_layout($title, $body);
    } catch (Throwable $e) {
        ef_admin_layout('Operation failed', '<div class="notice error"><strong>Nothing was changed.</strong><br>'.e($e->getMessage()).'</div>');
    }
}

function ef_admin_login(): void
{
    global $config;
    header('Cache-Control: no-store, private');
    if (admin_user()) { header('Location: '.url($config['admin_path'])); return; }
    $error = '';
    if ($_SERVER['REQUEST_METHOD'] === 'POST') {
        if (!hash_equals($_SESSION['login_csrf'] ?? '', $_POST['csrf'] ?? '')) $error = 'Session expired. Reload and try again.';
        else {
            $email = filter_var($_POST['email'] ?? '', FILTER_VALIDATE_EMAIL);
            $user = $email ? one('SELECT * FROM admins WHERE email=?', [$email]) : null;
            if ($user && $user['locked_until'] && strtotime($user['locked_until']) > time()) $error = 'Too many attempts. Try again after the temporary lock expires.';
            elseif ($user && password_verify((string)($_POST['password'] ?? ''), $user['password'])) {
                if (password_needs_rehash($user['password'], PASSWORD_DEFAULT)) q('UPDATE admins SET password=? WHERE id=?', [password_hash((string)$_POST['password'], PASSWORD_DEFAULT), $user['id']]);
                q('UPDATE admins SET failed_attempts=0,locked_until=NULL WHERE id=?', [$user['id']]);
                session_regenerate_id(true);
                $_SESSION['admin'] = ['id'=>$user['id'], 'email'=>$user['email']];
                unset($_SESSION['login_csrf']);
                header('Location: '.url($config['admin_path'])); return;
            } else {
                if ($user) q('UPDATE admins SET failed_attempts=failed_attempts+1,locked_until=IF(failed_attempts+1>=5,DATE_ADD(NOW(),INTERVAL 15 MINUTE),NULL) WHERE id=?', [$user['id']]);
                usleep(350000);
                $error = 'Invalid sign-in details.';
            }
        }
    }
    $_SESSION['login_csrf'] = bin2hex(random_bytes(24));
    echo '<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width"><meta name="robots" content="noindex"><title>Exams.fit Admin</title><link rel="stylesheet" href="'.url('assets/app.css?v=3').'"></head><body><main><section class="login"><a class="brand" href="'.url().'">Exams<span>.fit</span></a><h1>Admin sign in</h1>'.($error?'<p class="notice error">'.e($error).'</p>':'').'<form method="post" class="form"><input type="hidden" name="csrf" value="'.e($_SESSION['login_csrf']).'"><label>Email<input type="email" name="email" autocomplete="username" required></label><label>Password<input type="password" name="password" autocomplete="current-password" required></label><button class="button">Sign in</button></form></section></main></body></html>';
}

function ef_admin_layout(string $title, string $body): void
{
    global $config;
    header('Cache-Control: no-store, private');
    $base = url($config['admin_path']);
    $links = [''=>'Dashboard','content'=>'Content','blog'=>'Blog','tools'=>'Tools','import'=>'Importer','imports'=>'History','templates'=>'Templates','addons'=>'Add-ons','sections'=>'Sections','menus'=>'Menus','media'=>'Media/PDF','videos'=>'Videos','feeds'=>'Feeds','seo'=>'SEO','redirects'=>'Redirects','ads'=>'Ads','analytics'=>'Analytics','reports'=>'Reports','cron'=>'Cron','maintenance'=>'Updater','settings'=>'Settings'];
    echo '<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width"><meta name="robots" content="noindex,nofollow"><title>'.e($title).' – Exams.fit</title><link rel="stylesheet" href="'.url('assets/app.css?v=3').'"><script defer src="'.url('assets/admin.js?v=3').'"></script></head><body class="admin"><aside><a class="brand" href="'.$base.'">Exams<span>.fit</span></a><nav>';
    foreach ($links as $route=>$label) echo '<a href="'.$base.($route?'/'.$route:'').'">'.e($label).'</a>';
    echo '<a href="'.url().'" target="_blank" rel="noopener">View website ↗</a><a href="'.$base.'/logout">Sign out</a></nav></aside><main><div class="admin-head"><h1>'.e($title).'</h1><span>'.e(admin_user()['email']).'</span></div>'.$body.'</main></body></html>';
}

function ef_admin_dashboard(): string
{
    $stats = [
        'Published'=>(int)(one("SELECT COUNT(*) n FROM content WHERE status='published'")['n'] ?? 0),
        'Awaiting review'=>(int)(one("SELECT COUNT(*) n FROM content WHERE status IN('draft','needs_review','validation_failed')")['n'] ?? 0),
        'Page views (30d)'=>(int)(one("SELECT COUNT(*) n FROM analytics WHERE event='pageview' AND day>=DATE_SUB(CURDATE(),INTERVAL 30 DAY)")['n'] ?? 0),
        'Open reports'=>(int)(one("SELECT COUNT(*) n FROM reports WHERE status='open'")['n'] ?? 0)
    ];
    $h = '<div class="stats">'; foreach ($stats as $k=>$v) $h .= '<div><strong>'.number_format($v).'</strong><span>'.e($k).'</span></div>';
    $lastCron = one('SELECT * FROM cron_log ORDER BY id DESC LIMIT 1');
    return $h.'</div><div class="cards"><section class="panel"><h2>Import-first workflow</h2><ol><li>Download the matching template.</li><li>Generate the small JSON/ZIP package with AI.</li><li>Validate and inspect the preview.</li><li>Import as draft, review, then publish.</li></ol><a class="button" href="'.ef_admin_url('templates').'">Open templates</a></section><section class="panel"><h2>System snapshot</h2><p>Core <strong>'.e(trim((string)file_get_contents(EF_ROOT.'/VERSION'))).'</strong></p><p>Last cron: '.e($lastCron ? $lastCron['ran_at'].' · '.$lastCron['status'] : 'Not run yet').'</p><p>Downloads default to disabled. Ads default to disabled. Feed items default to noindex.</p></section></div>';
}

function ef_admin_content(): string
{
    $id = (int)($_GET['id'] ?? 0);
    if ($id) {
        $c = one('SELECT * FROM content WHERE id=?', [$id]); if (!$c) return '<div class="notice error">Content not found.</div>';
        return '<div class="panel"><form method="post" class="form"><input type="hidden" name="csrf" value="'.csrf().'"><input type="hidden" name="id" value="'.$id.'"><label>Title<input name="title" maxlength="255" value="'.e($c['title']).'" required></label><label>Excerpt<textarea name="excerpt" rows="3" required>'.e($c['excerpt']).'</textarea></label><label>Body (safe HTML)<textarea name="body" rows="18" required>'.e($c['body']).'</textarea></label><div class="form-grid"><label>Status<select name="status">'.ef_options(['draft','validation_failed','needs_review','scheduled','published'],$c['status']).'</select></label><label>Meta title<input name="meta_title" maxlength="65" value="'.e($c['meta_title']).'"></label><label>Meta description<textarea name="meta_description" maxlength="170">'.e($c['meta_description']).'</textarea></label><label>Canonical URL<input type="url" name="canonical_url" value="'.e($c['canonical_url']).'"></label><label>Author<input name="author" value="'.e($c['author']).'"></label><label>Reviewer<input name="reviewer" value="'.e($c['reviewer']).'"></label></div><label><input type="checkbox" name="noindex" value="1" '.($c['noindex']?'checked':'').'> noindex</label><label><input type="checkbox" name="featured" value="1" '.($c['featured']?'checked':'').'> Featured</label><label><input type="checkbox" name="ad_free" value="1" '.($c['ad_free']?'checked':'').'> Ad-free page</label><button class="button" name="action" value="save_content">Save review</button> <a href="'.url($c['slug']).'" target="_blank">Preview ↗</a></form></div>';
    }
    $status = preg_replace('/[^a-z_]/','',$_GET['status'] ?? ''); $type = preg_replace('/[^a-z_]/','',$_GET['type'] ?? ''); $term = trim($_GET['q'] ?? '');
    $where=[]; $params=[]; if ($status) {$where[]='status=?';$params[]=$status;} if ($type) {$where[]='type=?';$params[]=$type;} if ($term!=='') {$where[]='(title LIKE ? OR external_id LIKE ?)';$params[]='%'.$term.'%';$params[]='%'.$term.'%';}
    $rows = all('SELECT id,external_id,type,title,slug,status,noindex,quality_score,views,updated_at FROM content'.($where?' WHERE '.implode(' AND ',$where):'').' ORDER BY updated_at DESC LIMIT 250',$params);
    $types = all('SELECT DISTINCT type FROM content ORDER BY type');
    $h='<div class="panel"><form method="get" class="filters"><input name="q" value="'.e($term).'" placeholder="Title or external ID"><select name="status"><option value="">All statuses</option>'.ef_options(['draft','validation_failed','needs_review','scheduled','published'],$status).'</select><select name="type"><option value="">All types</option>'; foreach($types as$t)$h.='<option '.($type===$t['type']?'selected':'').' value="'.e($t['type']).'">'.e($t['type']).'</option>';
    $h.='</select><button>Filter</button></form></div><div class="panel table"><table><thead><tr><th>Content</th><th>Status</th><th>Quality</th><th>Views</th><th></th></tr></thead><tbody>'; foreach($rows as$r)$h.='<tr><td><strong>'.e($r['title']).'</strong><small>'.e($r['external_id']).' · '.e($r['type']).'</small></td><td>'.e($r['status']).($r['noindex']?' · noindex':'').'</td><td>'.(int)$r['quality_score'].'/100</td><td>'.number_format((int)$r['views']).'</td><td><a href="'.ef_admin_url('content?id='.$r['id']).'">Review</a></td></tr>';
    return $h.'</tbody></table></div>';
}

function ef_admin_blog(): string
{
    $stats=['Published'=>(int)(one("SELECT COUNT(*) n FROM content WHERE type='blog_post' AND status='published'")['n']??0),'Draft/review'=>(int)(one("SELECT COUNT(*) n FROM content WHERE type='blog_post' AND status<>'published'")['n']??0),'Topics'=>(int)(one("SELECT COUNT(*) n FROM categories WHERE type='blog_category'")['n']??0)];$h='<div class="stats">';foreach($stats as$label=>$value)$h.='<div><strong>'.number_format($value).'</strong><span>'.e($label).'</span></div>';$h.='</div><div class="panel"><h2>Automated blog workflow</h2><p>Download the bulk ZIP template, ask AI to create original US-English articles, then validate and import. New items remain draft/noindex until editorial review; only published indexable posts enter the blog sitemap and RSS feed.</p><a class="button" href="'.ef_admin_url('blog-template').'">Download blog bulk ZIP template</a> <a href="'.ef_admin_url('import').'">Open importer</a> · <a href="'.url('blog').'" target="_blank">View blog ↗</a> · <a href="'.url('blog/feed.xml').'" target="_blank">RSS ↗</a></div>';
    $rows=all("SELECT id,external_id,title,slug,status,noindex,quality_score,views,updated_at FROM content WHERE type='blog_post' ORDER BY updated_at DESC LIMIT 250");$h.='<div class="panel table"><table><tr><th>Article</th><th>Status</th><th>SEO gate</th><th>Views</th><th></th></tr>';foreach($rows as$r)$h.='<tr><td><strong>'.e($r['title']).'</strong><small>'.e($r['external_id']).' · /'.e($r['slug']).'</small></td><td>'.e($r['status']).'</td><td>'.($r['noindex']?'noindex':'indexable').' · '.(int)$r['quality_score'].'/100</td><td>'.number_format((int)$r['views']).'</td><td><a href="'.ef_admin_url('content?id='.$r['id']).'">Review</a></td></tr>';return$h.'</table></div>';
}

function ef_admin_tools(): string
{
    $stats=['Published'=>(int)(one("SELECT COUNT(*) n FROM content WHERE type='tool' AND status='published'")['n']??0),'Draft/review'=>(int)(one("SELECT COUNT(*) n FROM content WHERE type='tool' AND status<>'published'")['n']??0),'Categories'=>(int)(one("SELECT COUNT(*) n FROM categories WHERE type='tool_category'")['n']??0)];$h='<div class="stats">';foreach($stats as$label=>$value)$h.='<div><strong>'.number_format($value).'</strong><span>'.e($label).'</span></div>';$h.='</div><div class="panel"><h2>Safe importable tools</h2><p>Generate calculator definitions from the supplied JSON/ZIP format. The built-in engine supports grade, percentage, unit conversion, countdown and study timer tools without importing executable PHP or JavaScript.</p><a class="button" href="'.ef_admin_url('tools-template').'">Download tools bulk ZIP template</a> <a href="'.ef_admin_url('import').'">Open importer</a> · <a href="'.url('tools').'" target="_blank">View tools page ↗</a></div>';
    $rows=all("SELECT id,external_id,title,slug,status,noindex,quality_score,views,updated_at FROM content WHERE type='tool' ORDER BY updated_at DESC LIMIT 250");$h.='<div class="panel table"><table><tr><th>Tool</th><th>Status</th><th>SEO gate</th><th>Uses</th><th></th></tr>';foreach($rows as$r)$h.='<tr><td><strong>'.e($r['title']).'</strong><small>'.e($r['external_id']).' · /'.e($r['slug']).'</small></td><td>'.e($r['status']).'</td><td>'.($r['noindex']?'noindex':'indexable').' · '.(int)$r['quality_score'].'/100</td><td>'.number_format((int)$r['views']).'</td><td><a href="'.ef_admin_url('content?id='.$r['id']).'">Review</a></td></tr>';return$h.'</table></div>';
}

function ef_admin_import(): string
{
    return '<div class="panel"><h2>Validate → preview → import</h2><p>ZIP contents are checked for structure, IDs, SEO fields, HTML safety, duplicate slugs/hashes, links, MIME types, rights metadata and required checksums. No database write occurs in preview mode.</p><form method="post" enctype="multipart/form-data" class="form"><input type="hidden" name="csrf" value="'.csrf().'"><label>Package ZIP<input type="file" name="package" accept=".zip,application/zip" required></label><label>Existing item behavior<select name="mode"><option value="skip">Skip existing</option><option value="update">Update matching</option><option value="create">Create only</option><option value="replace">Replace matching safely</option></select></label><button name="action" value="validate">Validate / preview</button><button class="button" name="action" value="import">Validate and import</button></form><p><strong>Limits:</strong> 25 MB uploaded ZIP, 100 MB uncompressed and 5,000 ZIP files per package. Keep this tab open during large validation and import jobs.</p></div>';
}

function ef_admin_imports(): string
{
    $rows=all('SELECT * FROM imports ORDER BY id DESC LIMIT 200'); $h='<div class="panel table"><table><thead><tr><th>Package</th><th>Status</th><th>Result</th><th>Finished</th><th>Action</th></tr></thead><tbody>';
    foreach($rows as$r){$details=trim((string)$r['errors'])?:trim((string)$r['log']);$h.='<tr><td><strong>'.e($r['package_id']).'</strong><small>v'.e($r['version']).' · '.e($r['mode']).'</small></td><td>'.e($r['status']).'</td><td>+'.(int)$r['items_created'].' / ~'.(int)$r['items_updated'].' / skipped '.(int)$r['items_skipped'].($details?'<details><summary>Log</summary><pre>'.e($details).'</pre></details>':'').'</td><td>'.e($r['finished_at']?:$r['created_at']).'</td><td>'.($r['status']==='complete'?ef_action_form('rollback','Rollback',['id'=>$r['id']],true):'—').'</td></tr>';}
    return $h.'</tbody></table></div>';
}

function ef_admin_templates(): string
{
    $special=['practice_test'=>'practice_test','mock_exam'=>'practice_test','question_bank'=>'practice_test','daily_quiz'=>'practice_test','youtube_video'=>'video_lesson','video_playlist'=>'video_lesson','pdf_material'=>'pdf_material','blog_post'=>'blog_post','tool'=>'tool'];
    $h='<div class="panel"><h2>AI-ready templates</h2><p>Choose a type, download the blank schema or working ZIP, then paste the included AI instructions into your content-generation chat. Generated items remain drafts unless the package explicitly requests auto-publish and passes the quality gate.</p><div class="template-grid">';
    foreach(EF_TYPES as$type){$tpl=$special[$type]??'universal_content';$h.='<article><strong>'.e(ucwords(str_replace('_',' ',$type))).'</strong><a href="'.ef_admin_url('template?type='.$tpl.'&content_type='.$type).'">Blank JSON</a></article>';}
    $h.='</div><p><a class="button" href="'.ef_admin_url('example').'">Download complete example ZIP</a> <a class="button" href="'.ef_admin_url('blog-template').'">Blog bulk ZIP</a> <a class="button" href="'.ef_admin_url('tools-template').'">Tools bulk ZIP</a> <a href="'.url('templates/AI-INSTRUCTIONS.txt').'">AI instructions</a> · <a href="'.url('docs/IMPORT-GUIDE.md').'">Field documentation</a></p></div>';
    $addons=all('SELECT addon_id,name FROM addons WHERE status="enabled" ORDER BY name');if($addons){$h.='<div class="panel"><h2>Installed add-on templates</h2><ul>';foreach($addons as$addon)foreach(glob(EF_ROOT.'/storage/addons/'.$addon['addon_id'].'/templates/*.{json,txt}',GLOB_BRACE)?:[] as$file)$h.='<li><a href="'.ef_admin_url('addon-template?addon='.rawurlencode($addon['addon_id']).'&file='.rawurlencode(basename($file))).'">'.e($addon['name'].' — '.basename($file)).'</a></li>';$h.='</ul></div>';}
    return $h;
}

function ef_admin_addons(): string
{
    $rows=all('SELECT * FROM addons ORDER BY updated_at DESC');$h='<div class="panel"><h2>Add a future importer or presentation module</h2><p>Addon ZIPs require a manifest, semantic version, minimum core version, declared permissions and reversible migrations. Executable PHP is intentionally blocked.</p><p><a href="'.url('templates/example-import-addon.zip').'">Download example add-on ZIP</a> · <a href="'.url('templates/ADDON-INSTRUCTIONS.txt').'">Add-on instructions</a></p><form method="post" enctype="multipart/form-data" class="form"><input type="hidden" name="csrf" value="'.csrf().'"><input type="file" name="addon" accept=".zip" required><button class="button" name="action" value="install_addon">Validate and install/update</button></form></div><div class="panel table"><table><tr><th>Add-on</th><th>Version</th><th>Status</th><th>Actions</th></tr>';
    foreach($rows as$r)$h.='<tr><td>'.e($r['name']).'<small>'.e($r['addon_id']).'</small></td><td>'.e($r['version']).'</td><td>'.e($r['status']).'</td><td>'.ef_action_form($r['status']==='enabled'?'disable_addon':'enable_addon',$r['status']==='enabled'?'Disable':'Enable',['id'=>$r['id']]).ef_action_form('rollback_addon','Rollback',['id'=>$r['id']],true).ef_action_form('uninstall_addon','Uninstall',['id'=>$r['id']],true).'</td></tr>';
    return $h.'</table></div>';
}

function ef_admin_sections(): string
{
    $rows=all('SELECT * FROM sections ORDER BY position,id');$h='<div class="panel"><form method="post" class="form-grid"><input type="hidden" name="csrf" value="'.csrf().'"><label>Title<input name="title" required></label><label>Content types (comma separated)<input name="content_types" placeholder="exam_profile,exam_update"></label><label>Layout<select name="layout">'.ef_options(['cards','compact','list'],'cards').'</select></label><label>Items<input type="number" name="item_limit" min="1" max="12" value="6"></label><button class="button" name="action" value="save_section">Add section</button></form></div><div class="panel table"><table data-sort-sections><tr><th>↕ Section</th><th>Types</th><th>Layout</th><th>Items</th><th>State</th></tr>';
    foreach($rows as$r)$h.='<tr data-id="'.$r['id'].'"><td>'.e($r['title']).'</td><td>'.e(implode(', ',json_decode($r['content_types']?:'[]',true)?:[])).'</td><td>'.e($r['layout']).'</td><td>'.(int)$r['item_limit'].'</td><td>'.ef_action_form('toggle_section',$r['enabled']?'Disable':'Enable',['id'=>$r['id']]).ef_action_form('delete_section','Delete',['id'=>$r['id']],true).'</td></tr>';
    return $h.'</table><form method="post"><input type="hidden" name="csrf" value="'.csrf().'"><input type="hidden" name="order"><button class="button" data-save-order name="action" value="reorder_sections">Save drag order</button></form></div>';
}

function ef_admin_menus(): string
{
    $rows=all('SELECT * FROM menus ORDER BY location,position,id');$h='<div class="panel"><form method="post" class="form-grid"><input type="hidden" name="csrf" value="'.csrf().'"><label>Label<input name="label" required></label><label>URL<input name="url" placeholder="/school" required></label><label>Location<select name="location">'.ef_options(['header','footer'],'header').'</select></label><label>Position<input type="number" name="position" value="10"></label><button class="button" name="action" value="save_menu">Add item</button></form></div><div class="panel table"><table><tr><th>Label</th><th>URL</th><th>Location</th><th>Position</th><th>Actions</th></tr>';
    foreach($rows as$r)$h.='<tr><td>'.e($r['label']).'</td><td>'.e($r['url']).'</td><td>'.e($r['location']).'</td><td>'.(int)$r['position'].'</td><td>'.ef_action_form('toggle_menu',$r['enabled']?'Disable':'Enable',['id'=>$r['id']]).ef_action_form('delete_menu','Delete',['id'=>$r['id']],true).'</td></tr>';
    return $h.'</table></div>';
}

function ef_admin_media(): string
{
    $rows=all('SELECT * FROM media ORDER BY id DESC LIMIT 300');$h='<div class="panel"><p>Media is accepted only through a validated ZIP. PDF download/print remain disabled unless each item explicitly permits them.</p></div><div class="panel table"><table><tr><th>File</th><th>MIME</th><th>Size</th><th>Rights/metadata</th></tr>';
    foreach($rows as$r)$h.='<tr><td><a href="'.url($r['path']).'" target="_blank">'.e($r['title']).'</a><small>'.e($r['path']).'</small></td><td>'.e($r['mime']).'</td><td>'.number_format((int)$r['size']).'</td><td><details><summary>View</summary><pre>'.e($r['meta']).'</pre></details></td></tr>';
    return $h.'</table></div>';
}

function ef_admin_videos(): string
{
    $rows=all("SELECT external_id,title,slug,status,quality_score,updated_at FROM content WHERE type IN('youtube_video','video_playlist') ORDER BY updated_at DESC LIMIT 300");$h='<div class="panel"><p>YouTube URL/ID, privacy-enhanced player, duration, chapters, transcript, notes and related resources are imported using the Video Lesson template.</p></div><div class="panel table"><table><tr><th>Video</th><th>Status</th><th>Quality</th><th>Updated</th></tr>';
    foreach($rows as$r)$h.='<tr><td><a href="'.url($r['slug']).'" target="_blank">'.e($r['title']).'</a><small>'.e($r['external_id']).'</small></td><td>'.e($r['status']).'</td><td>'.(int)$r['quality_score'].'</td><td>'.e($r['updated_at']).'</td></tr>';
    return $h.'</table></div>';
}

function ef_admin_feeds(): string
{
    $rows=all('SELECT * FROM feeds ORDER BY id DESC');$cats=all('SELECT id,name FROM categories ORDER BY name');$h='<div class="panel"><p>Add only official HTTPS RSS/Atom sources that permit feed use. Host allowlisting, timeouts and size limits reduce SSRF risk. Fetched posts are attributed and <strong>noindex</strong> until reviewed.</p><form method="post" class="form-grid"><input type="hidden" name="csrf" value="'.csrf().'"><label>Source name<input name="name" required></label><label>HTTPS feed URL<input type="url" name="url" required></label><label>Content kind<select name="kind">'.ef_options(['education_news','exam_update','admission_update','education_job'],'education_news').'</select></label><label>Category<select name="category_id"><option value="0">None</option>';foreach($cats as$c)$h.='<option value="'.$c['id'].'">'.e($c['name']).'</option>';
    $h.='</select></label><label>Interval minutes<input type="number" name="fetch_interval" min="15" max="1440" value="60"></label><label>Items/run<input type="number" name="item_limit" min="1" max="25" value="10"></label><button class="button" name="action" value="save_feed">Add feed</button></form></div><div class="panel table"><table><tr><th>Feed</th><th>Schedule/health</th><th>Last result</th><th>Actions</th></tr>';
    foreach($rows as$r)$h.='<tr><td>'.e($r['name']).'<small>'.e($r['url']).'</small></td><td>Every '.(int)$r['fetch_interval'].'m · '.e($r['health']).' · '.($r['enabled']?'enabled':'disabled').'</td><td>'.e($r['last_run']).'<small>'.e($r['last_status']).'</small></td><td>'.ef_action_form('run_feed','Run now',['id'=>$r['id']]).ef_action_form('toggle_feed',$r['enabled']?'Disable':'Enable',['id'=>$r['id']]).ef_action_form('delete_feed','Delete',['id'=>$r['id']],true).'</td></tr>';
    return $h.'</table></div>';
}

function ef_admin_seo(): string
{
    $groups=['static','blog','exams','practice','school','college','notes','courses','articles','news','videos','pdfs','tools','categories','images'];$h='<div class="panel"><h2>Search engine endpoints</h2><p><a href="'.url('robots.txt').'" target="_blank">robots.txt</a> · <a href="'.url('sitemap_index.xml').'" target="_blank">sitemap_index.xml</a> · <a href="'.url('feed.xml').'" target="_blank">All-content RSS</a> · <a href="'.url('blog/feed.xml').'" target="_blank">Blog RSS</a></p><p>Submit only <code>'.e(url('sitemap_index.xml')).'</code> to Google Search Console and Bing Webmaster Tools. Search engines—not the website—decide crawling and indexing.</p></div><div class="panel table"><table><tr><th>Sitemap group</th><th>Eligible URLs</th></tr>';
    foreach($groups as$g){$n=ef_sitemap_count($g);$h.='<tr><td><a href="'.url('sitemap-'.$g.'.xml').'">'.e(ucfirst($g)).'</a></td><td>'.number_format($n).'</td></tr>';}
    $bad=(int)(one("SELECT COUNT(*) n FROM content WHERE status='published' AND (meta_title IS NULL OR CHAR_LENGTH(meta_title) NOT BETWEEN 25 AND 65 OR meta_description IS NULL OR CHAR_LENGTH(meta_description) NOT BETWEEN 70 AND 170)")['n']??0);
    return $h.'</table><p><strong>'.number_format($bad).'</strong> published pages currently need metadata review.</p></div>';
}

function ef_admin_redirects(): string
{
    $rows=all('SELECT * FROM redirects ORDER BY id DESC');$h='<div class="panel"><form method="post" class="form-grid"><input type="hidden" name="csrf" value="'.csrf().'"><label>Old relative path<input name="source" placeholder="old-page" required></label><label>Target path/URL<input name="target" placeholder="new-page" required></label><label>Status<select name="status_code">'.ef_options(['301','302','307','308'],'301').'</select></label><button class="button" name="action" value="save_redirect">Add redirect</button></form></div><div class="panel table"><table><tr><th>Source</th><th>Target</th><th>Status/hits</th><th>Actions</th></tr>';
    foreach($rows as$r)$h.='<tr><td>'.e($r['source']).'</td><td>'.e($r['target']).'</td><td>'.(int)$r['status_code'].' · '.number_format((int)$r['hits']).'</td><td>'.ef_action_form('toggle_redirect',$r['enabled']?'Disable':'Enable',['id'=>$r['id']]).ef_action_form('delete_redirect','Delete',['id'=>$r['id']],true).'</td></tr>';
    return $h.'</table></div>';
}

function ef_admin_ads(): string
{
    $rows=all('SELECT a.*,c.title content_title,cat.name category_name FROM ads a LEFT JOIN content c ON c.id=a.content_id LEFT JOIN categories cat ON cat.id=a.category_id ORDER BY a.id DESC');$cats=all('SELECT id,name FROM categories ORDER BY name');$h='<div class="panel"><p>Global advertising is disabled until enabled in Settings. Per-page ad-free overrides always win.</p><form method="post" class="form"><input type="hidden" name="csrf" value="'.csrf().'"><div class="form-grid"><label>Name<input name="name" required></label><label>Slot<select name="slot">'.ef_options(['header','content_top','content_bottom','sidebar','footer'],'content_top').'</select></label><label>Device<select name="device">'.ef_options(['all','desktop','mobile'],'all').'</select></label><label>Content type<input name="content_type" placeholder="blank = all"></label><label>Category<select name="category_id"><option value="0">All categories</option>';foreach($cats as$c)$h.='<option value="'.$c['id'].'">'.e($c['name']).'</option>';
    $h.='</select></label><label>Exact content DB ID<input type="number" name="content_id" min="0" value="0"></label><label>Starts at<input type="datetime-local" name="starts_at"></label><label>Ends at<input type="datetime-local" name="ends_at"></label></div><label>Ad code<textarea name="code" rows="8" required></textarea></label><label><input type="checkbox" name="enabled" value="1"> Enable placement</label><button class="button" name="action" value="save_ad">Save placement</button></form></div><div class="panel table"><table><tr><th>Placement</th><th>Target</th><th>Schedule</th><th>Actions</th></tr>';
    foreach($rows as$r)$h.='<tr><td>'.e($r['name']).'<small>'.e($r['slot']).' · '.e($r['device']).'</small></td><td>'.e($r['content_title']?:$r['category_name']?:$r['content_type']?:'All content').'</td><td>'.e($r['starts_at']?:'now').' → '.e($r['ends_at']?:'open').'</td><td>'.ef_action_form('toggle_ad',$r['enabled']?'Disable':'Enable',['id'=>$r['id']]).ef_action_form('delete_ad','Delete',['id'=>$r['id']],true).'</td></tr>';
    return $h.'</table></div>';
}

function ef_admin_analytics(): string
{
    $period=in_array($_GET['period']??'30',['7','30','90'],true)?(int)$_GET['period']:30;$totals=all("SELECT event,COUNT(*) events,SUM(value) total FROM analytics WHERE day>=DATE_SUB(CURDATE(),INTERVAL $period DAY) GROUP BY event ORDER BY events DESC");$pages=all("SELECT path,COUNT(*) views,COUNT(DISTINCT session_hash) sessions FROM analytics WHERE event='pageview' AND day>=DATE_SUB(CURDATE(),INTERVAL $period DAY) GROUP BY path ORDER BY views DESC LIMIT 50");$daily=all("SELECT day,COUNT(*) pageviews,COUNT(DISTINCT session_hash) sessions FROM analytics WHERE event='pageview' AND day>=DATE_SUB(CURDATE(),INTERVAL $period DAY) GROUP BY day ORDER BY day DESC");$h='<div class="panel"><p><a href="?period=7">7 days</a> · <a href="?period=30">30 days</a> · <a href="?period=90">90 days</a></p><div class="stats">';foreach($totals as$t)$h.='<div><strong>'.number_format((int)$t['events']).'</strong><span>'.e(str_replace('_',' ',$t['event'])).'</span></div>';
    $h.='</div></div><div class="cards"><section class="panel table"><h2>Top pages</h2><table><tr><th>Path</th><th>Views</th><th>Sessions</th></tr>';foreach($pages as$p)$h.='<tr><td>'.e($p['path']).'</td><td>'.number_format((int)$p['views']).'</td><td>'.number_format((int)$p['sessions']).'</td></tr>';
    $h.='</table></section><section class="panel table"><h2>Daily trend</h2><table><tr><th>Day</th><th>Views</th><th>Sessions</th></tr>';foreach($daily as$d)$h.='<tr><td>'.e($d['day']).'</td><td>'.number_format((int)$d['pageviews']).'</td><td>'.number_format((int)$d['sessions']).'</td></tr>';
    return $h.'</table></section></div><div class="panel"><p>First-party events include page views, engagement time, scroll depth, searches/zero results, practice starts/completions, video/PDF activity and downloads. Browser privacy controls may reduce totals.</p></div>';
}

function ef_admin_reports(): string
{
    $rows=all('SELECT r.*,c.title,c.slug FROM reports r LEFT JOIN content c ON c.id=r.content_id ORDER BY r.id DESC LIMIT 250');$broken=all('SELECT b.*,c.title FROM broken_links b LEFT JOIN content c ON c.id=b.content_id WHERE b.status="open" ORDER BY b.id DESC LIMIT 100');$h='<div class="panel"><form method="post"><input type="hidden" name="csrf" value="'.csrf().'"><button class="button" name="action" value="quality_scan">Run content quality scan</button> <button name="action" value="link_scan">Check links</button></form></div><div class="panel table"><h2>Reader reports</h2><table><tr><th>Page</th><th>Issue</th><th>Status</th><th>Action</th></tr>';
    foreach($rows as$r)$h.='<tr><td>'.($r['slug']?'<a href="'.url($r['slug']).'">'.e($r['title']).'</a>':'Deleted item').'</td><td><strong>'.e($r['kind']).'</strong><br>'.e($r['message']).'</td><td>'.e($r['status']).'</td><td>'.ef_action_form('resolve_report','Mark resolved',['id'=>$r['id']]).'</td></tr>';
    $h.='</table></div><div class="panel table"><h2>Open broken links</h2><table><tr><th>Content</th><th>URL</th><th>Code</th></tr>';foreach($broken as$b)$h.='<tr><td>'.e($b['title']).'</td><td>'.e($b['url']).'</td><td>'.(int)$b['status_code'].'</td></tr>';
    return $h.'</table></div>';
}

function ef_admin_cron(): string
{
    global$config;$jobs=['all','publish','queue','feeds','sitemap','cache','analytics','links','health','optimize','backup','freshness'];$rows=all('SELECT * FROM cron_log ORDER BY id DESC LIMIT 100');$command='wget -q -O /dev/null "'.url('cron?token='.$config['cron_token'].'&job=all').'"';$h='<div class="panel"><h2>cPanel command</h2><pre>'.e($command).'</pre><p>Recommended: every 15 minutes. Each job uses an overlap lock and records duration/status. The token shown here is visible only to administrators.</p><div class="actions">';foreach($jobs as$j)$h.=ef_action_form('run_cron','Run '.ucfirst($j),['job'=>$j]);
    $h.='</div></div><div class="panel table"><table><tr><th>Ran at</th><th>Job</th><th>Status</th><th>Duration/message</th></tr>';foreach($rows as$r)$h.='<tr><td>'.e($r['ran_at']).'</td><td>'.e($r['job']).'</td><td>'.e($r['status']).'</td><td>'.number_format((int)$r['duration_ms']).' ms<small>'.e($r['message']).'</small></td></tr>';
    return $h.'</table></div>';
}

function ef_admin_maintenance(): string
{
    $files=array_reverse(glob(EF_ROOT.'/storage/backups/*.sql')?:[]);$version=setting('schema_version','2.x/unknown');$history=ef_update_history();$recoveries=array_reverse(glob(EF_ROOT.'/storage/backups/ExamsFit_update_files_*.zip')?:[]);$h='<div class="stats"><div><strong>'.e(trim((string)file_get_contents(EF_ROOT.'/VERSION'))).'</strong><span>Core version</span></div><div><strong>'.e($version).'</strong><span>Database schema</span></div><div><strong>'.number_format(ef_dir_size(EF_ROOT.'/storage/cache')).'</strong><span>Cache bytes</span></div></div><div class="panel"><h2>System Updater</h2><p>Upload a trusted Exams.fit core upgrade ZIP. Before changing anything, the updater validates every checksum and target path, creates a complete SQL backup plus file recovery ZIP, enables maintenance mode, applies only additive migrations, runs health checks and automatically restores files if the upgrade fails. It never replaces <code>config.php</code>, <code>uploads/</code> or operational <code>storage/</code> data.</p><form method="post" enctype="multipart/form-data" class="form" onsubmit="return confirm(\'Install this trusted core update now? A recovery package will be created automatically.\')"><input type="hidden" name="csrf" value="'.csrf().'"><label>Trusted upgrade ZIP<input type="file" name="upgrade" accept=".zip,application/zip" required></label><button class="button" name="action" value="system_update">Validate, back up and upgrade</button></form><p><a href="'.url('docs/SYSTEM-UPDATER.md').'">Upgrade package specification and safety guide</a></p></div><div class="cards"><section class="panel"><h2>Legacy database migration</h2><p>For an existing v2 database only; fresh installs do not need this.</p>'.ef_action_form('upgrade_v3','Back up and upgrade schema to v3',[],true).'</section><section class="panel"><h2>Database backup</h2>'.ef_action_form('backup','Create SQL backup').'</section><section class="panel"><h2>Cache</h2>'.ef_action_form('clear_cache','Clear cache').'</section></div><div class="panel table"><h2>Update history</h2><table><tr><th>Time</th><th>Status</th><th>Version</th><th>Recovery</th></tr>';
    foreach($history as$row)$h.='<tr><td>'.e($row['at']??'').'</td><td>'.e($row['status']??'').'</td><td>'.e(($row['from']??'?').' → '.($row['to']??'?')).'</td><td>'.e($row['backup']??'').'<small>'.e($row['database_backup']??'').'</small></td></tr>';$h.='</table></div><div class="panel"><h2>File recovery packages</h2><ul>';foreach(array_slice($recoveries,0,20) as$f)$h.='<li>'.e(basename($f)).' '.ef_action_form('rollback_system_update','Restore files',['backup'=>basename($f)],true).'</li>';$h.='</ul><p>File rollback does not remove additive database columns/tables because that could destroy newer data. The paired SQL backup is retained for a deliberate phpMyAdmin restore if ever required.</p></div><div class="panel"><h2>Available database backups</h2><ul>';
    foreach(array_slice($files,0,30) as$f)$h.='<li><a href="'.ef_admin_url('backup-download?file='.rawurlencode(basename($f))).'">'.e(basename($f)).'</a> — '.number_format(filesize($f)).' bytes</li>';
    return $h.'</ul><p>Creating a backup never deletes old backups.</p></div>';
}

function ef_admin_settings(): string
{
    $keys=['site_name','site_description','theme_color','contact_email','google_verification','bing_verification','blog_posts_per_page','analytics_enabled','analytics_retention_days','ads_enabled','ads_txt','auto_ads','content_auto_publish','robots_extra','head_code','footer_code'];$h='<div class="panel"><form method="post" class="form"><input type="hidden" name="csrf" value="'.csrf().'">';foreach($keys as$k){$large=in_array($k,['site_description','ads_txt','robots_extra','head_code','footer_code'],true);$h.='<label>'.e(ucwords(str_replace('_',' ',$k))).($large?'<textarea name="settings['.e($k).']" rows="'.(str_contains($k,'code')?7:3).'">'.e(setting($k)).'</textarea>':'<input name="settings['.e($k).']" value="'.e(setting($k)).'">').'</label>';}
    return $h.'<button class="button" name="action" value="save_settings">Save settings</button></form><p>Use 1/0 for analytics, ads and auto-publish switches. Third-party scripts are your responsibility; add consent controls where law or ad policy requires them.</p></div>';
}

function ef_admin_post(string $route): void
{
    global$config;$action=$_POST['action']??'';$back=ef_admin_url($route);
    if($route==='content'&&$action==='save_content'){
        $id=(int)$_POST['id'];$status=in_array($_POST['status']??'draft',['draft','validation_failed','needs_review','scheduled','published'],true)?$_POST['status']:'draft';$body=clean_html((string)$_POST['body']);$title=trim((string)$_POST['title']);$excerpt=trim((string)$_POST['excerpt']);if(mb_strlen($title)<5||mb_strlen($excerpt)<20||mb_strlen(strip_tags($body))<50)throw new RuntimeException('Title, excerpt or body is too short.');$published=$status==='published'?'COALESCE(published_at,NOW())':'published_at';q("UPDATE content SET title=?,excerpt=?,body=?,meta_title=?,meta_description=?,canonical_url=?,status=?,noindex=?,featured=?,ad_free=?,author=?,reviewer=?,content_hash=?,published_at=$published,reviewed_at=NOW(),updated_at=NOW() WHERE id=?",[$title,$excerpt,$body,trim((string)$_POST['meta_title']),trim((string)$_POST['meta_description']),trim((string)$_POST['canonical_url'])?:null,$status,(int)!empty($_POST['noindex']),(int)!empty($_POST['featured']),(int)!empty($_POST['ad_free']),trim((string)$_POST['author']),trim((string)$_POST['reviewer']),hash('sha256',mb_strtolower(strip_tags($title.' '.$body))),$id]);audit('content.review',['id'=>$id,'status'=>$status]);ef_go('content?id='.$id);
    }
    if($route==='import'&&in_array($action,['validate','import'],true)){
        if(function_exists('set_time_limit'))@set_time_limit(600);$file=$_FILES['package']??null;if(!$file||$file['error']!==UPLOAD_ERR_OK)throw new RuntimeException('ZIP upload failed.');if($file['size']>25*1024*1024)throw new RuntimeException('Compressed ZIP exceeds 25 MB.');$tmp=EF_ROOT.'/storage/imports/package-'.bin2hex(random_bytes(8)).'.zip';if(!move_uploaded_file($file['tmp_name'],$tmp))throw new RuntimeException('Cannot store uploaded ZIP.');try{$preview=ef_validate_package($tmp);if($action==='validate'){ef_admin_layout('Import preview','<div class="panel"><h2>'.($preview['errors']?'Validation failed':'Package is valid').'</h2><pre>'.e(json_encode($preview,JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES)).'</pre><a class="button" href="'.ef_admin_url('import').'">Back to importer</a></div>');return;}ef_import_package($tmp,$_POST['mode']??'skip');}finally{@unlink($tmp);}ef_go('imports');
    }
    if($route==='imports'&&$action==='rollback'){ef_rollback_import((int)$_POST['id']);ef_go('imports');}
    if($route==='addons'&&$action==='install_addon'){$f=$_FILES['addon']??null;if(!$f||$f['error']!==UPLOAD_ERR_OK||$f['size']>10*1024*1024)throw new RuntimeException('Valid add-on ZIP up to 10 MB required.');$tmp=EF_ROOT.'/storage/imports/addon-'.bin2hex(random_bytes(8)).'.zip';if(!move_uploaded_file($f['tmp_name'],$tmp))throw new RuntimeException('Cannot store add-on ZIP.');try{ef_install_addon($tmp);}finally{@unlink($tmp);}ef_go('addons');}
    if($route==='addons'&&in_array($action,['enable_addon','disable_addon','rollback_addon','uninstall_addon'],true)){$id=(int)$_POST['id'];match($action){'enable_addon'=>ef_toggle_addon($id,'enabled'),'disable_addon'=>ef_toggle_addon($id,'disabled'),'rollback_addon'=>ef_rollback_addon($id),'uninstall_addon'=>ef_uninstall_addon($id)};ef_go('addons');}
    if($route==='sections'&&$action==='save_section'){$types=array_values(array_filter(array_map(fn($x)=>preg_replace('/[^a-z_]/','',trim($x)),explode(',',$_POST['content_types']??''))));$layout=in_array($_POST['layout']??'cards',['cards','compact','list'],true)?$_POST['layout']:'cards';q('INSERT INTO sections(title,content_types,layout,item_limit,position,enabled) VALUES(?,?,?,?,(SELECT COALESCE(MAX(s.position),0)+1 FROM sections s),1)',[trim((string)$_POST['title']),json_encode($types),$layout,min(12,max(1,(int)$_POST['item_limit']))]);audit('section.add');ef_go('sections');}
    if($route==='sections'&&$action==='reorder_sections'){$ids=array_values(array_filter(array_map('intval',explode(',',$_POST['order']??''))));foreach($ids as$i=>$id)q('UPDATE sections SET position=? WHERE id=?',[$i+1,$id]);audit('sections.reorder');ef_go('sections');}
    if($route==='sections'&&in_array($action,['toggle_section','delete_section'],true)){if($action==='toggle_section')q('UPDATE sections SET enabled=1-enabled WHERE id=?',[(int)$_POST['id']]);else q('DELETE FROM sections WHERE id=?',[(int)$_POST['id']]);audit('section.'.$action);ef_go('sections');}
    if($route==='menus'&&$action==='save_menu'){$u=trim((string)$_POST['url']);if(!str_starts_with($u,'/')&&!(filter_var($u,FILTER_VALIDATE_URL)&&str_starts_with($u,'https://')))throw new RuntimeException('Menu URL must be a relative path or HTTPS URL.');q('INSERT INTO menus(label,url,location,position,enabled) VALUES(?,?,?,?,1)',[trim((string)$_POST['label']),$u,in_array($_POST['location']??'header',['header','footer'],true)?$_POST['location']:'header',(int)$_POST['position']]);audit('menu.add');ef_go('menus');}
    if($route==='menus'&&in_array($action,['toggle_menu','delete_menu'],true)){q($action==='toggle_menu'?'UPDATE menus SET enabled=1-enabled WHERE id=?':'DELETE FROM menus WHERE id=?',[(int)$_POST['id']]);audit('menu.'.$action);ef_go('menus');}
    if($route==='feeds'&&$action==='save_feed'){$u=trim((string)$_POST['url']);$host=strtolower((string)parse_url($u,PHP_URL_HOST));if(!str_starts_with($u,'https://')||!$host)throw new RuntimeException('Feed must use a valid public HTTPS URL.');q('INSERT INTO feeds(name,url,allowed_host,category_id,kind,fetch_interval,item_limit,next_run,enabled) VALUES(?,?,?,?,?,?,?,NOW(),1)',[trim((string)$_POST['name']),$u,$host,(int)$_POST['category_id']?:null,in_array($_POST['kind']??'education_news',['education_news','exam_update','admission_update','education_job'],true)?$_POST['kind']:'education_news',min(1440,max(15,(int)$_POST['fetch_interval'])),min(25,max(1,(int)$_POST['item_limit']))]);audit('feed.add');ef_go('feeds');}
    if($route==='feeds'&&$action==='run_feed'){q('UPDATE feeds SET next_run=NOW(),enabled=1 WHERE id=?',[(int)$_POST['id']]);ef_fetch_feeds(false,(int)$_POST['id']);audit('feed.run');ef_go('feeds');}
    if($route==='feeds'&&in_array($action,['toggle_feed','delete_feed'],true)){q($action==='toggle_feed'?'UPDATE feeds SET enabled=1-enabled WHERE id=?':'DELETE FROM feeds WHERE id=?',[(int)$_POST['id']]);audit('feed.'.$action);ef_go('feeds');}
    if($route==='redirects'&&$action==='save_redirect'){$src=trim((string)$_POST['source'],'/');$target=trim((string)$_POST['target']);$code=(int)$_POST['status_code'];if(!preg_match('~^[a-z0-9][a-z0-9/_-]*$~',$src)||!in_array($code,[301,302,307,308],true))throw new RuntimeException('Invalid redirect source or status.');if(!str_starts_with($target,'/')&&!preg_match('~^https://~',$target))$target='/'.$target;q('INSERT INTO redirects(source,target,status_code,enabled) VALUES(?,?,?,1)',[$src,$target,$code]);audit('redirect.add');ef_go('redirects');}
    if($route==='redirects'&&in_array($action,['toggle_redirect','delete_redirect'],true)){q($action==='toggle_redirect'?'UPDATE redirects SET enabled=1-enabled WHERE id=?':'DELETE FROM redirects WHERE id=?',[(int)$_POST['id']]);audit('redirect.'.$action);ef_go('redirects');}
    if($route==='ads'&&$action==='save_ad'){$slot=in_array($_POST['slot']??'', ['header','content_top','content_bottom','sidebar','footer'],true)?$_POST['slot']:'content_top';$device=in_array($_POST['device']??'all',['all','desktop','mobile'],true)?$_POST['device']:'all';q('INSERT INTO ads(name,slot,category_id,content_type,content_id,code,device,starts_at,ends_at,enabled) VALUES(?,?,?,?,?,?,?,?,?,?)',[trim((string)$_POST['name']),$slot,(int)$_POST['category_id']?:null,trim((string)$_POST['content_type'])?:null,(int)$_POST['content_id']?:null,(string)$_POST['code'],$device,ef_datetime($_POST['starts_at']??''),ef_datetime($_POST['ends_at']??''),(int)!empty($_POST['enabled'])]);audit('ad.add');ef_go('ads');}
    if($route==='ads'&&in_array($action,['toggle_ad','delete_ad'],true)){q($action==='toggle_ad'?'UPDATE ads SET enabled=1-enabled WHERE id=?':'DELETE FROM ads WHERE id=?',[(int)$_POST['id']]);audit('ad.'.$action);ef_go('ads');}
    if($route==='reports'&&$action==='resolve_report'){q('UPDATE reports SET status="resolved",updated_at=NOW() WHERE id=?',[(int)$_POST['id']]);audit('report.resolve');ef_go('reports');}
    if($route==='reports'&&$action==='quality_scan'){$r=ef_quality_scan();ef_admin_layout('Quality scan','<div class="panel"><pre>'.e(json_encode($r,JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES)).'</pre><a class="button" href="'.ef_admin_url('reports').'">Back</a></div>');return;}
    if($route==='reports'&&$action==='link_scan'){$r=ef_broken_link_scan(200);ef_admin_layout('Link scan','<div class="notice success">Checked '.(int)$r['checked'].' links; found '.(int)$r['broken'].' possible problems.</div><a class="button" href="'.ef_admin_url('reports').'">View reports</a>');return;}
    if($route==='cron'&&$action==='run_cron'){$job=in_array($_POST['job']??'all',['all','publish','queue','feeds','sitemap','cache','analytics','links','health','optimize','backup','freshness'],true)?$_POST['job']:'all';ef_run_cron_job($job);ef_go('cron');}
    if($route==='maintenance'&&$action==='backup'){$name=ef_backup_create();audit('backup.create',['file'=>$name]);ef_go('maintenance');}
    if($route==='maintenance'&&$action==='clear_cache'){$n=ef_cache_clear();audit('cache.clear',['files'=>$n]);ef_go('maintenance');}
    if($route==='maintenance'&&$action==='upgrade_v3'){$backup=ef_backup_create();ef_upgrade_v3();audit('system.upgrade',['backup'=>$backup]);ef_go('maintenance');}
    if($route==='maintenance'&&$action==='system_update'){$f=$_FILES['upgrade']??null;if(!$f||$f['error']!==UPLOAD_ERR_OK||$f['size']>50*1024*1024)throw new RuntimeException('A valid trusted upgrade ZIP up to 50 MB is required.');$tmp=EF_ROOT.'/storage/imports/core-update-'.bin2hex(random_bytes(8)).'.zip';if(!move_uploaded_file($f['tmp_name'],$tmp))throw new RuntimeException('Cannot store upgrade ZIP.');try{$result=ef_system_update_install($tmp);}finally{@unlink($tmp);}ef_admin_layout('Upgrade complete','<div class="notice success"><h2>Exams.fit upgraded safely</h2><p>Core '.e($result['from']).' → '.e($result['to']).'</p><p>File recovery: '.e($result['backup']).'<br>Database backup: '.e($result['database_backup']).'</p><a class="button" href="'.ef_admin_url('maintenance').'">Return to System Updater</a></div>');return;}
    if($route==='maintenance'&&$action==='rollback_system_update'){ef_system_update_rollback((string)($_POST['backup']??''));ef_admin_layout('File rollback complete','<div class="notice success"><h2>Core files restored</h2><p>Additive database changes were preserved to prevent content loss.</p><a class="button" href="'.ef_admin_url('maintenance').'">Return to System Updater</a></div>');return;}
    if($route==='settings'&&$action==='save_settings'){$allowed=['site_name','site_description','theme_color','contact_email','google_verification','bing_verification','blog_posts_per_page','analytics_enabled','analytics_retention_days','ads_enabled','ads_txt','auto_ads','content_auto_publish','robots_extra','head_code','footer_code'];$s=db()->prepare('INSERT INTO settings(`key`,value) VALUES(?,?) ON DUPLICATE KEY UPDATE value=VALUES(value)');foreach($_POST['settings']??[] as$k=>$v)if(in_array($k,$allowed,true)){$value=trim((string)$v);if($k==='blog_posts_per_page')$value=(string)max(6,min(30,(int)$value));$s->execute([$k,$value]);}audit('settings.update');ef_go('settings');}
    throw new RuntimeException('Unsupported admin action.');
}

function ef_upgrade_v3(): void
{
    if(setting('schema_version')==='3.0.0') return;
    $sql=(string)file_get_contents(EF_ROOT.'/database/migrations/003_prompt_complete.sql');$sql=preg_replace('/^--.*$/m','',$sql);$statements=array_values(array_filter(array_map('trim',preg_split('/;\s*(?:\r?\n|$)/',$sql)?:[])));
    foreach($statements as$statement){try{db()->exec($statement);}catch(PDOException$e){$message=strtolower($e->getMessage());if(!str_contains($message,'duplicate column')&&!str_contains($message,'duplicate key')&&!str_contains($message,'already exists'))throw$e;}}
    foreach(all('SELECT id,url,allowed_host FROM feeds') as$feed)if(!$feed['allowed_host'])q('UPDATE feeds SET allowed_host=? WHERE id=?',[strtolower((string)parse_url($feed['url'],PHP_URL_HOST)),$feed['id']]);
    q('INSERT INTO settings(`key`,value) VALUES("schema_version","3.0.0") ON DUPLICATE KEY UPDATE value="3.0.0"');
}

function ef_template_download(): void
{
    $name=preg_replace('/[^a-z_]/','',$_GET['type']??'universal_content');$contentType=preg_replace('/[^a-z_]/','',$_GET['content_type']??'article');$path=EF_ROOT.'/templates/'.$name.'.json';if(!is_file($path))$path=EF_ROOT.'/templates/universal_content.json';$raw=(string)file_get_contents($path);$data=json_decode($raw,true);if(is_array($data)&&isset($data['type'])){$data['type']=$contentType;$raw=json_encode($data,JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES)."\n";}header('Content-Type:application/json');header('X-Content-Type-Options:nosniff');header('Content-Disposition: attachment; filename="ExamsFit_'.$contentType.'_blank_template.json"');echo$raw;
}
function ef_addon_template_download():void{$addon=preg_replace('/[^a-z0-9_-]/','',$_GET['addon']??'');$file=basename((string)($_GET['file']??''));if(!preg_match('/^[a-zA-Z0-9_.-]+\.(json|txt)$/',$file))throw new RuntimeException('Invalid add-on template.');$enabled=one('SELECT id FROM addons WHERE addon_id=? AND status="enabled"',[$addon]);$path=EF_ROOT.'/storage/addons/'.$addon.'/templates/'.$file;if(!$enabled||!is_file($path))throw new RuntimeException('Enabled add-on template not found.');header('Content-Type: '.(str_ends_with($file,'.json')?'application/json':'text/plain').'; charset=utf-8');header('Content-Disposition: attachment; filename="'.e($addon.'-'.$file).'"');readfile($path);}
function ef_named_zip_download(string$file,string$download):void{$file=basename($file);$path=EF_ROOT.'/templates/'.$file;if(!is_file($path))throw new RuntimeException('Template ZIP is missing.');header('Content-Type:application/zip');header('X-Content-Type-Options:nosniff');header('Content-Length: '.filesize($path));header('Content-Disposition: attachment; filename="'.e($download).'"');readfile($path);}
function ef_example_download(): void{$path=EF_ROOT.'/templates/example-package.zip';if(!is_file($path))throw new RuntimeException('Example package is missing.');header('Content-Type:application/zip');header('Content-Length: '.filesize($path));header('Content-Disposition: attachment; filename="ExamsFit_USA_Example_Import_v3.1.0.zip"');readfile($path);}
function ef_backup_download(): void{$file=basename((string)($_GET['file']??''));if(!preg_match('/^ExamsFit_backup_\d{8}_\d{6}\.sql$/',$file))throw new RuntimeException('Invalid backup name.');$path=EF_ROOT.'/storage/backups/'.$file;if(!is_file($path))throw new RuntimeException('Backup not found.');header('Content-Type:application/sql');header('Content-Length: '.filesize($path));header('Content-Disposition: attachment; filename="'.e($file).'"');readfile($path);}
function ef_admin_url(string$path=''): string{global$config;return url($config['admin_path'].($path?'/'.$path:''));}
function ef_go(string$path): never{header('Location: '.ef_admin_url($path));exit;}
function ef_options(array$items,string$current): string{$h='';foreach($items as$item)$h.='<option value="'.e((string)$item).'" '.((string)$item===$current?'selected':'').'>'.e(ucwords(str_replace('_',' ',(string)$item))).'</option>';return$h;}
function ef_action_form(string$action,string$label,array$hidden=[],bool$confirm=false): string{$h='<form method="post" class="inline"'.($confirm?' onsubmit="return confirm(\'Are you sure? A backup/rollback should exist before destructive changes.\')"':'').'><input type="hidden" name="csrf" value="'.csrf().'">';foreach($hidden as$k=>$v)$h.='<input type="hidden" name="'.e((string)$k).'" value="'.e((string)$v).'">';return$h.'<button name="action" value="'.e($action).'">'.e($label).'</button></form>';}
function ef_datetime(string$value): ?string{return $value!==''?date('Y-m-d H:i:s',strtotime($value)):null;}
function ef_dir_size(string$dir): int{$size=0;foreach(glob(rtrim($dir,'/').'/*')?:[] as$f)if(is_file($f))$size+=filesize($f);return$size;}
