<?php

declare(strict_types=1);

class AdminSettingController
{
    private static array $tabs = [
        'general'     => 'Branding',
        'storage'     => 'Storage',
        'transcode'   => 'Transcoding',
        'security'    => 'Security',
        'advanced'    => 'Contact',
        'maintenance' => 'Maintenance',
    ];

    private static array $subTabDefs = [
        'transcode' => [
            'formats'    => 'Formats',
            'watermark'  => 'Watermark',
            'intros'     => 'Intro & Outro',
            'timeline'   => 'Timeline',
            'servers'    => 'Servers',
            'queue'      => 'Queue',
        ],
        'security' => [
            '2fa'        => '2FA',
            'captcha'    => 'Captcha',
            'admin_url'  => 'Admin URL',
        ],
        'advanced' => [
            'email'     => 'Transport',
            'mails'     => 'Email types',
            'telegram'  => 'Telegram',
        ],
        'maintenance' => [
            'purge'  => 'Data Purge',
            'health' => 'Server Health',
        ],
    ];

    public static function routes(): void
    {
        Router::get('/admin/settings', [self::class, 'index']);
        Router::post('/admin/settings', [self::class, 'update']);
        Router::post('/admin/settings/2fa-setup', [self::class, 'twoFactorSetup']);
        Router::post('/admin/settings/2fa-enable', [self::class, 'twoFactorEnable']);
        Router::post('/admin/settings/2fa-disable', [self::class, 'twoFactorDisable']);
        Router::post('/admin/settings/test-email', [self::class, 'testEmail']);
        Router::post('/admin/settings/test-telegram', [self::class, 'testTelegram']);
        Router::post('/admin/settings/mail-preview', [self::class, 'mailPreview']);
        Router::post('/admin/settings/purge', [self::class, 'purge']);
        Router::get('/admin/settings/watermark-preview', [self::class, 'watermarkPreview']);
        Router::post('/admin/settings/format/save', [self::class, 'formatSave']);
        Router::post('/admin/settings/format/delete', [self::class, 'formatDelete']);
        Router::post('/admin/settings/transcode-server/save', [self::class, 'transcodeServerSave']);
        Router::post('/admin/settings/transcode-server/delete', [self::class, 'transcodeServerDelete']);
        Router::post('/admin/settings/transcode-server/check', [self::class, 'transcodeServerCheck']);
        Router::post('/admin/settings/storage/save', [self::class, 'storageSave']);
        Router::post('/admin/settings/storage/delete', [self::class, 'storageDelete']);
        Router::post('/admin/settings/storage/toggle-status', [self::class, 'storageToggleStatus']);
        Router::post('/admin/settings/storage/default', [self::class, 'storageSetDefault']);
        Router::post('/admin/settings/storage/reassign', [self::class, 'storageReassign']);
        Router::post('/admin/settings/storage/test', [self::class, 'storageTest']);
        Router::post('/admin/settings/storage/test-monitor', [self::class, 'storageTestMonitor']);
        Router::get('/admin/storage/create', [self::class, 'storageCreate']);
        Router::get('/admin/storage/{id}/edit', [self::class, 'storageEdit']);
        Router::get('/admin/settings/transcode-jobs', [self::class, 'transcodeJobsDetail']);
        Router::get('/admin/settings/transcode-queue-data', [self::class, 'transcodeQueueData']);
    }

    public static function index(): void
    {
        Auth::requireSuperAdmin();

        $tab = $_GET['tab'] ?? 'general';

        if (!isset(self::$tabs[$tab])) {
            $tab = 'general';
        }

        $keys = ['site_name','site_description','site_url','videos_per_page','videos_per_row',
                 'media_storage','media_url',
                 'media_ftp_host','media_ftp_port','media_ftp_user','media_ftp_ssl','media_ftp_passive','media_ftp_path',
                 'media_s3_key','media_s3_bucket','media_s3_region','media_s3_endpoint','media_s3_prefix',
                 'media_dav_url','media_dav_user',
                 'transcode_enabled','transcode_ffmpeg_path','transcode_ffprobe_path',
                 'transcode_max_per_tick','transcode_trim_start',
                 'transcode_timeline','transcode_timeline_count','transcode_timeline_start',
                 'watermark_enabled','watermark_position','watermark_opacity','watermark_max_width','watermark_padding','watermark_image',
                 'comments_enabled','comments_moderation','registration_enabled','albums_enabled',
                 'meta_keywords','meta_description',
                 'color_primary',
                 'color_bg','color_bg_surface','color_bg_card','color_bg_dark',
                 'color_text','color_text_muted','color_border',
                 'color_primary_opacity',
                 'color_bg_opacity','color_bg_surface_opacity','color_bg_card_opacity','color_bg_dark_opacity',
                 'color_text_opacity','color_text_muted_opacity','color_border_opacity',
                 'dk_color_primary','dk_color_primary_opacity',
                 'menu_categories','menu_tags','menu_pornstars','menu_channels','menu_search',
                 'related_videos_count',
                 'watch_show_views','watch_show_duration','watch_show_date',
                 'watch_show_likes','watch_show_favorites',
                 'watch_show_pornstars','watch_show_channels',
                 'watch_show_categories','watch_show_tags',
                 'comments_enabled','comments_moderation',
                 'player_playback_rate','player_allow_download','player_loop','player_doubleclick_fullscreen',
                 'default_sort',
                 'captcha_provider','captcha_site_key','captcha_secret_key',
                 'captcha_on_register','captcha_on_comment','captcha_theme',
                 'spam_honeypot','spam_timegate','spam_timegate_seconds',
                 'spam_ratelimit','spam_ratelimit_hour','spam_ratelimit_day',
                 'spam_duplicate','spam_duplicate_window',
                 'spam_links','spam_max_links',
                 'spam_length','spam_min_length','spam_max_length',
                 'timezone','contact_email',
                 'captcha_on_contact',
                 'mail_method','smtp_host','smtp_port','smtp_user','smtp_pass','smtp_encryption','smtp_from_email','smtp_from_name',
                 'categories_per_row',
                 'categories_per_page','categories_default_sort',
                 'performers_per_page','performers_default_sort',
                 'channels_per_page','channels_default_sort',
                 'admin_login_slug',
                 'seo_noindex_entire_site',
                 'mail_resend_key','mail_brevo_key','mail_mailgun_key','mail_mailgun_domain','mail_mailgun_region','mail_postmark_token','mail_sendgrid_key',
                 'telegram_enabled','telegram_bot_token','telegram_chat_id','telegram_event_contact','telegram_event_report','telegram_event_user','telegram_event_comment'];
        $keys = array_merge($keys, MailTypes::settingKeys());

         
        $activeSub = '';
        if (isset(self::$subTabDefs[$tab])) {
            $subs = self::$subTabDefs[$tab];
            $subs = HookSystem::applyFilter('admin.subtabs.' . $tab, $subs);
            $activeSub = $_GET['sub'] ?? array_key_first($subs);
            if (!isset($subs[$activeSub])) {
                $activeSub = array_key_first($subs);
            }
        }

        $data = [
            'pageTitle' => 'Settings',
            'activeMenu' => 'settings',
            'contentTemplate' => '_settings',
            'settings' => Setting::getMany($keys),
            'tabs' => self::$tabs,
            'activeTab' => $tab,
            'subTabs' => $subs ?? [],
            'activeSub' => $activeSub,
        ];

         
        if ($tab === 'security' && $activeSub === '2fa') {
            $user = Auth::user();
            $data['twoFactorEnabled'] = !empty($user['totp_secret']);
        }

         
        if ($tab === 'maintenance' && $activeSub === 'purge') {
            $nonAdminRow = Database::fetchOne("SELECT COUNT(*) as c FROM users WHERE role != 'admin'");
            $data['counts'] = [
                'videos'      => Database::count('videos'),
                'categories'  => Database::count('categories'),
                'tags'        => Database::count('tags'),
                'performers'  => Database::count('performers'),
                'channels'     => Database::count('channels'),
                'albums'             => Database::count('albums'),
                'album_categories'   => Database::count('album_categories'),
                'album_tags'         => Database::count('album_tags'),
                'album_performers'   => Database::count('album_performers'),
                'album_channels'     => Database::count('album_channels'),
                'users'       => (int) ($nonAdminRow['c'] ?? 0),
                'comments'    => Database::count('comments'),
                'pages'       => Database::count('pages'),
                'imports'     => Database::count('import_jobs'),
            ];
        }
        if ($tab === 'maintenance' && $activeSub === 'health') {
            $data['healthChecks'] = self::runHealthChecks();
        }

        Router::render('admin/settings', $data);
    }

    public static function update(): void
    {
        Auth::requireSuperAdmin();
        if (!Router::verifyCsrf()) { Router::redirect('/admin/settings'); }

        $tab = $_POST['_tab'] ?? 'general';

        switch ($tab) {
            case 'general':
                foreach (['site_name','site_description'] as $f) {
                    Setting::set($f, trim($_POST[$f] ?? ''));
                }
                $tz = trim($_POST['timezone'] ?? 'UTC');
                if (in_array($tz, \DateTimeZone::listIdentifiers(), true)) {
                    Setting::set('timezone', $tz);
                }
                 
                Setting::set('registration_enabled', isset($_POST['registration_enabled']) ? '1' : '0');
                Setting::set('albums_enabled', isset($_POST['albums_enabled']) ? '1' : '0');
                Setting::set('auto_update_enabled', isset($_POST['auto_update_enabled']) ? '1' : '0');
                Setting::set('channels_as_users', isset($_POST['channels_as_users']) ? '1' : '0');
                Setting::set('comments_enabled', isset($_POST['comments_enabled']) ? '1' : '0');
                Setting::set('comments_moderation', isset($_POST['comments_moderation']) ? '1' : '0');
                Setting::set('seo_noindex_entire_site', isset($_POST['seo_noindex_entire_site']) ? '1' : '0');
                 
                foreach (['spam_honeypot','spam_timegate','spam_ratelimit','spam_duplicate','spam_links','spam_length'] as $cb) {
                    Setting::set($cb, isset($_POST[$cb]) ? '1' : '0');
                }
                Setting::set('spam_timegate_seconds', (string) max(1, min(30, (int) ($_POST['spam_timegate_seconds'] ?? 3))));
                Setting::set('spam_ratelimit_hour', (string) max(1, min(100, (int) ($_POST['spam_ratelimit_hour'] ?? 5))));
                Setting::set('spam_ratelimit_day', (string) max(1, min(500, (int) ($_POST['spam_ratelimit_day'] ?? 30))));
                Setting::set('spam_duplicate_window', (string) max(1, min(1440, (int) ($_POST['spam_duplicate_window'] ?? 60))));
                Setting::set('spam_max_links', (string) max(0, min(20, (int) ($_POST['spam_max_links'] ?? 0))));
                Setting::set('spam_min_length', (string) max(1, min(500, (int) ($_POST['spam_min_length'] ?? 2))));
                Setting::set('spam_max_length', (string) max(10, min(50000, (int) ($_POST['spam_max_length'] ?? 5000))));
                break;

            case 'storage':
                break;

            case 'transcode':
                $sub = $_POST['_sub'] ?? '';
                if ($sub === 'formats' && isset($_POST['transcode_trim_start'])) {
                    Setting::set('transcode_trim_start', (string)max(0, min(300, (int)($_POST['transcode_trim_start'] ?? 0))));
                } elseif ($sub === 'watermark') {
                    $wantWm = isset($_POST['watermark_enabled']);
                    if ($wantWm && Setting::get('watermark_image', '') === '' && empty($_FILES['watermark_file']['name'])) {
                        $wantWm = false;
                    }
                    Setting::set('watermark_enabled', $wantWm ? '1' : '0');
                    Setting::set('watermark_position', in_array($_POST['watermark_position'] ?? '', ['top-left','top-right','bottom-left','bottom-right','center','random'], true) ? $_POST['watermark_position'] : 'bottom-right');
                    Setting::set('watermark_opacity', (string)max(10, min(100, (int)($_POST['watermark_opacity'] ?? 80))));
                    Setting::set('watermark_max_width', (string)max(5, min(50, (int)($_POST['watermark_max_width'] ?? 15))));
                    Setting::set('watermark_padding', (string)max(0, min(15, (int)($_POST['watermark_padding'] ?? 2))));
                    if (!empty($_FILES['watermark_file']) && $_FILES['watermark_file']['error'] === UPLOAD_ERR_OK) {
                        $wmMime = Mime::fromFile($_FILES['watermark_file']['tmp_name'], $_FILES['watermark_file']['name'] ?? null);
                        if ($wmMime === 'image/png') {
                            $dir = APP_ROOT . '/public/uploads/watermark';
                            if (!is_dir($dir)) mkdir($dir, 0755, true);
                            $oldImage = Setting::get('watermark_image', '');
                            if ($oldImage !== '' && file_exists($dir . '/' . $oldImage)) {
                                @unlink($dir . '/' . $oldImage);
                            }
                            $name = 'watermark_' . time() . '.png';
                            move_uploaded_file($_FILES['watermark_file']['tmp_name'], $dir . '/' . $name);
                            Setting::set('watermark_image', $name);
                        }
                    }
                } elseif ($sub === 'intros') {
                    $wantPre = isset($_POST['transcode_preroll_enabled']);
                    if ($wantPre && Setting::get('transcode_preroll_file', '') === '' && empty($_FILES['preroll_file']['name'])) $wantPre = false;
                    Setting::set('transcode_preroll_enabled', $wantPre ? '1' : '0');
                    $wantPost = isset($_POST['transcode_postroll_enabled']);
                    if ($wantPost && Setting::get('transcode_postroll_file', '') === '' && empty($_FILES['postroll_file']['name'])) $wantPost = false;
                    Setting::set('transcode_postroll_enabled', $wantPost ? '1' : '0');
                    $introDir = APP_ROOT . '/public/uploads/intros';
                    if (!is_dir($introDir)) mkdir($introDir, 0755, true);
                    foreach (['preroll', 'postroll'] as $type) {
                        if (!empty($_FILES[$type . '_file']) && $_FILES[$type . '_file']['error'] === UPLOAD_ERR_OK) {
                            $introMime = Mime::fromFile($_FILES[$type . '_file']['tmp_name'], $_FILES[$type . '_file']['name'] ?? null);
                            if ($introMime === 'video/mp4') {
                                $old = Setting::get('transcode_' . $type . '_file', '');
                                if ($old !== '' && file_exists($introDir . '/' . $old)) @unlink($introDir . '/' . $old);
                                $name = $type . '_' . time() . '.mp4';
                                move_uploaded_file($_FILES[$type . '_file']['tmp_name'], $introDir . '/' . $name);
                                Setting::set('transcode_' . $type . '_file', $name);
                            }
                        }
                    }
                    if (isset($_POST['remove_preroll']) && $_POST['remove_preroll'] === '1') {
                        $old = Setting::get('transcode_preroll_file', '');
                        if ($old !== '' && file_exists($introDir . '/' . $old)) @unlink($introDir . '/' . $old);
                        Setting::set('transcode_preroll_file', '');
                        Setting::set('transcode_preroll_enabled', '0');
                    }
                    if (isset($_POST['remove_postroll']) && $_POST['remove_postroll'] === '1') {
                        $old = Setting::get('transcode_postroll_file', '');
                        if ($old !== '' && file_exists($introDir . '/' . $old)) @unlink($introDir . '/' . $old);
                        Setting::set('transcode_postroll_file', '');
                        Setting::set('transcode_postroll_enabled', '0');
                    }
                } elseif ($sub === 'timeline') {
                    Setting::set('transcode_timeline', isset($_POST['transcode_timeline']) ? '1' : '0');
                    Setting::set('transcode_timeline_count', (string)max(5, min(60, (int)($_POST['transcode_timeline_count'] ?? 10))));
                    Setting::set('transcode_timeline_start', (string)max(0, min(300, (int)($_POST['transcode_timeline_start'] ?? 0))));
                } elseif ($sub === 'queue') {
                    if (($_POST['_action'] ?? '') === 'retry_failed') TranscodeManager::retryFailed();
                } elseif ($sub === 'servers' && isset($_POST['transcode_max_per_tick'])) {
                    Setting::set('transcode_max_per_tick', (string)max(0, min(4, (int)($_POST['transcode_max_per_tick'] ?? 1))));
                    $ffmpegPath = trim($_POST['transcode_ffmpeg_path'] ?? '');
                    if ($ffmpegPath !== '' && !is_executable($ffmpegPath)) {
                        $_SESSION['flash'] = ['type' => 'error', 'message' => 'FFmpeg path is not executable: ' . $ffmpegPath];
                    } else {
                        Setting::set('transcode_ffmpeg_path', $ffmpegPath);
                    }
                } else {
                    $wantEnabled = isset($_POST['transcode_enabled']);
                    if ($wantEnabled && !TranscodeManager::canProcess()) {
                        $_SESSION['flash'] = ['type' => 'error', 'message' => 'no_transcode_capability'];
                        Router::redirect('/admin/settings?tab=transcode&sub=servers');
                        return;
                    }
                    Setting::set('transcode_enabled', $wantEnabled ? '1' : '0');
                }
                break;

            case 'seo':
                foreach (['meta_keywords','meta_description'] as $f) {
                    Setting::set($f, trim($_POST[$f] ?? ''));
                }
                break;

            case 'advanced':
                $sub = $_POST['_sub'] ?? 'email';
                switch ($sub) {
                    case 'email':
                        $contactEmail = trim($_POST['contact_email'] ?? '');
                        if ($contactEmail === '' || filter_var($contactEmail, FILTER_VALIDATE_EMAIL)) {
                            Setting::set('contact_email', $contactEmail);
                        }
                        $mailMethod = $_POST['mail_method'] ?? 'php';
                        if (!in_array($mailMethod, ['php', 'smtp', 'resend', 'mailgun', 'brevo', 'postmark', 'sendgrid'], true)) { $mailMethod = 'php'; }
                        Setting::set('mail_method', $mailMethod);
                        Setting::set('smtp_host', trim($_POST['smtp_host'] ?? ''));
                        $smtpPort = max(1, min(65535, (int) ($_POST['smtp_port'] ?? 587)));
                        Setting::set('smtp_port', (string) $smtpPort);
                        $smtpEnc = $_POST['smtp_encryption'] ?? 'tls';
                        if (!in_array($smtpEnc, ['none', 'tls', 'ssl'], true)) { $smtpEnc = 'tls'; }
                        Setting::set('smtp_encryption', $smtpEnc);
                        Setting::set('smtp_user', trim($_POST['smtp_user'] ?? ''));
                        Setting::set('smtp_pass', $_POST['smtp_pass'] ?? '');
                        $fromEmail = trim($_POST['smtp_from_email'] ?? '');
                        if ($fromEmail !== '' && !filter_var($fromEmail, FILTER_VALIDATE_EMAIL)) { $fromEmail = ''; }
                        Setting::set('smtp_from_email', $fromEmail);
                        Setting::set('smtp_from_name', trim($_POST['smtp_from_name'] ?? ''));
                         
                        Setting::set('mail_resend_key', trim($_POST['mail_resend_key'] ?? ''));
                        Setting::set('mail_brevo_key', trim($_POST['mail_brevo_key'] ?? ''));
                        Setting::set('mail_mailgun_key', trim($_POST['mail_mailgun_key'] ?? ''));
                        Setting::set('mail_mailgun_domain', trim($_POST['mail_mailgun_domain'] ?? ''));
                        Setting::set('mail_mailgun_region', ($_POST['mail_mailgun_region'] ?? 'us') === 'eu' ? 'eu' : 'us');
                        Setting::set('mail_postmark_token', trim($_POST['mail_postmark_token'] ?? ''));
                        Setting::set('mail_sendgrid_key', trim($_POST['mail_sendgrid_key'] ?? ''));
                        break;

                    case 'mails':
                        foreach (array_keys(MailTypes::all()) as $mtKey) {
                            Setting::set("mail_type_{$mtKey}_enabled", isset($_POST["mail_type_{$mtKey}_enabled"]) ? '1' : '0');
                            Setting::set("mail_type_{$mtKey}_subject", trim($_POST["mail_type_{$mtKey}_subject"] ?? ''));
                            Setting::set("mail_type_{$mtKey}_body", trim($_POST["mail_type_{$mtKey}_body"] ?? ''));
                        }
                        break;

                    case 'telegram':
                        Setting::set('telegram_enabled', isset($_POST['telegram_enabled']) ? '1' : '0');
                        Setting::set('telegram_bot_token', trim($_POST['telegram_bot_token'] ?? ''));
                        Setting::set('telegram_chat_id', trim($_POST['telegram_chat_id'] ?? ''));
                        foreach (['contact', 'report', 'user', 'comment'] as $ev) {
                            Setting::set('telegram_event_' . $ev, isset($_POST['telegram_event_' . $ev]) ? '1' : '0');
                        }
                        break;
                }
                break;

            case 'security':
                $sub = $_POST['_sub'] ?? 'captcha';
                switch ($sub) {
                    case 'captcha':
                        $provider = $_POST['captcha_provider'] ?? 'none';
                        if (!in_array($provider, ['none', 'v2', 'v3', 'turnstile'], true)) { $provider = 'none'; }
                        Setting::set('captcha_provider', $provider);
                        Setting::set('captcha_site_key', trim($_POST['captcha_site_key'] ?? ''));
                        Setting::set('captcha_secret_key', trim($_POST['captcha_secret_key'] ?? ''));
                        Setting::set('captcha_on_register', isset($_POST['captcha_on_register']) ? '1' : '0');
                        Setting::set('captcha_on_comment', isset($_POST['captcha_on_comment']) ? '1' : '0');
                        Setting::set('captcha_on_contact', isset($_POST['captcha_on_contact']) ? '1' : '0');
                        $captchaTheme = $_POST['captcha_theme'] ?? 'light';
                        Setting::set('captcha_theme', in_array($captchaTheme, ['light', 'dark'], true) ? $captchaTheme : 'light');
                        break;
                     

                    case 'admin_url':
                        $slug = trim($_POST['admin_login_slug'] ?? '');
                        if ($slug !== '') {
                             
                            $slug = preg_replace('/[^a-z0-9-]/', '', strtolower($slug));
                            $reserved = ['admin', 'install', 'login', 'register', 'sitemap', 'api', 'embed',
                                         'search', 'video', 'category', 'tag', 'performer', 'channel', 'page',
                                         'contact', 'feed', 'rss', 'uploads', 'assets', 'themes', 'plugins'];
                            if (strlen($slug) < 3 || strlen($slug) > 50
                                || !preg_match('/^[a-z0-9][a-z0-9-]*[a-z0-9]$/', $slug)
                                || in_array($slug, $reserved, true)
                            ) {
                                $slug = '';
                            }
                        }
                        Setting::set('admin_login_slug', $slug);
                        break;
                }
                break;
        }

        $_SESSION['flash'] = ['type' => 'success', 'message' => 'Settings updated successfully.'];
        $redirect = '/admin/settings?tab=' . $tab;
        if (!empty($sub)) {
            $redirect .= '&sub=' . $sub;
        }
        Router::redirect($redirect);
    }

     

    public static function watermarkPreview(): void
    {
        require APP_ROOT . '/templates/admin/_watermark_preview_frame.php';
        exit;
    }

    public static function transcodeServerSave(): void
    {
        Auth::requireSuperAdmin();
        if (!Router::verifyCsrf()) Router::json(['error' => 'Session expired.'], 403);

        $input = json_decode(file_get_contents('php://input'), true);
        $id = (int)($input['id'] ?? 0);

         
        if (!empty($input['_activate_only']) && $id > 0) {
            Database::update('transcode_servers', ['is_active' => 1], 'id = ?', [$id]);
            Router::json(['success' => true]);
        }

        $name = trim($input['name'] ?? '');
        $url = rtrim(trim($input['url'] ?? ''), '/');
        $maxJobs = max(1, min(20, (int)($input['max_jobs'] ?? 2)));
        $isActive = !empty($input['is_active']);

        if ($name === '') Router::json(['error' => 'Name is required.'], 400);
        if ($url === '' || !str_starts_with($url, 'http')) Router::json(['error' => 'Valid URL is required.'], 400);

        $data = ['name' => $name, 'url' => $url, 'max_jobs' => $maxJobs, 'is_active' => $isActive ? 1 : 0];

        if ($id > 0) {
            if (($input['api_key'] ?? '') !== '') $data['api_key'] = $input['api_key'];
            Database::update('transcode_servers', $data, 'id = ?', [$id]);
        } else {
            $data['api_key'] = bin2hex(random_bytes(16));
            $id = Database::insert('transcode_servers', $data);
        }

        $server = Database::fetchOne('SELECT api_key FROM transcode_servers WHERE id = ?', [$id]);
        Router::json(['success' => true, 'id' => $id, 'api_key' => $server['api_key'] ?? '']);
    }

    public static function transcodeServerDelete(): void
    {
        Auth::requireSuperAdmin();
        if (!Router::verifyCsrf()) Router::json(['error' => 'Session expired.'], 403);

        $input = json_decode(file_get_contents('php://input'), true);
        $id = (int)($input['id'] ?? 0);
        if ($id > 0) {
             
            $server = Database::fetchOne('SELECT name FROM transcode_servers WHERE id = ?', [$id]);
            if ($server) {
                Database::query(
                    "UPDATE transcode_jobs SET status = 'pending', worker = NULL, last_worker = ?, started_at = NULL, error_message = 'Server removed — will retry on another server'
                     WHERE status = 'processing' AND worker = ?",
                    [$server['name'], $server['name']]
                );
            }
            Database::delete('transcode_servers', 'id = ?', [$id]);
        }
        Router::json(['success' => true]);
    }

    public static function transcodeServerCheck(): void
    {
        Auth::requireSuperAdmin();
        if (!Router::verifyCsrf()) Router::json(['error' => 'Session expired.'], 403);

        $input = json_decode(file_get_contents('php://input'), true);
        $id = (int)($input['id'] ?? 0);
        $server = Database::fetchOne('SELECT * FROM transcode_servers WHERE id = ?', [$id]);
        if (!$server) Router::json(['error' => 'Server not found.'], 404);

        $result = TranscodeManager::checkServer($server);
        Database::update('transcode_servers', [
            'status' => $result['ok'] ? 'online' : 'error',
            'last_check' => date('Y-m-d H:i:s'),
            'last_error' => $result['ok'] ? null : ($result['error'] ?? 'Unknown error'),
        ], 'id = ?', [$id]);

        Router::json($result);
    }

    public static function formatSave(): void
    {
        Auth::requireSuperAdmin();
        if (!Router::verifyCsrf()) Router::json(['error' => 'Session expired.'], 403);

        $input = json_decode(file_get_contents('php://input'), true);
        $id = (int)($input['id'] ?? 0);

        if (!empty($input['_toggle']) && $id > 0) {
            Database::update('video_formats', ['is_active' => !empty($input['is_active']) ? 1 : 0], 'id = ?', [$id]);
            Router::json(['success' => true, 'id' => $id]);
        }

        $postfix = trim($input['postfix'] ?? '');
        if ($postfix !== '' && !preg_match('/^_[a-zA-Z0-9]+\.(mp4|webm|ogg)$/', $postfix)) {
            Router::json(['error' => 'Postfix must be like _720p.mp4 (underscore + name + extension).'], 400);
        }

        $data = [
            'name' => trim($input['name'] ?? ''),
            'postfix' => $postfix,
            'width' => max(128, (int)($input['width'] ?? 0)),
            'height' => max(96, (int)($input['height'] ?? 0)),
            'keep_aspect' => !empty($input['keep_aspect']) ? 1 : 0,
            'ffmpeg_options' => trim($input['ffmpeg_options'] ?? ''),
            'is_active' => !empty($input['is_active']) ? 1 : 0,
        ];

        if ($data['name'] === '') Router::json(['error' => 'Name is required.'], 400);
        if ($data['postfix'] === '') Router::json(['error' => 'Postfix is required.'], 400);

        if ($id > 0) {
            Database::update('video_formats', $data, 'id = ?', [$id]);
        } else {
            $id = Database::insert('video_formats', $data);
        }

        Router::json(['success' => true, 'id' => $id]);
    }

    public static function formatDelete(): void
    {
        Auth::requireSuperAdmin();
        if (!Router::verifyCsrf()) Router::json(['error' => 'Session expired.'], 403);

        $input = json_decode(file_get_contents('php://input'), true);
        $id = (int)($input['id'] ?? 0);
        if ($id <= 0) Router::json(['error' => 'Invalid format.'], 400);

        $totalFormats = Database::count('video_formats');
        if ($totalFormats <= 1) Router::json(['error' => 'Cannot delete the last format. At least one format is required.']);

        $format = Database::fetchOne('SELECT postfix FROM video_formats WHERE id = ?', [$id]);
        $defaultPostfixes = ['_720p.mp4', '_480p.mp4', '_360p.mp4'];
        if ($format && in_array($format['postfix'], $defaultPostfixes, true)) {
            Router::json(['error' => 'This is a default format and cannot be deleted. You can disable it instead.']);
        }

        Database::delete('video_formats', 'id = ?', [$id]);
        Router::json(['success' => true]);
    }

    public static function storageSave(): void
    {
        Auth::requireSuperAdmin();
        if (!Router::verifyCsrf()) Router::json(['error' => 'Session expired.'], 403);

        $input = json_decode(file_get_contents('php://input'), true);
        $id = (int)($input['id'] ?? 0);
        $name = trim($input['name'] ?? '');
        $driver = $input['driver'] ?? 'local';
        if (!in_array($driver, ['local', 'ftp', 's3', 'webdav'], true)) $driver = 'local';
        if ($name === '') $name = ucfirst($driver) . ' Storage';

        $mediaUrl = trim($input['media_url'] ?? '');
        if ($mediaUrl !== '' && !str_starts_with($mediaUrl, 'http')) $mediaUrl = '';
        $mediaUrl = rtrim($mediaUrl, '/');

        $config = [];
        if ($driver === 'ftp') {
            $config = ['host' => trim($input['ftp_host'] ?? ''), 'port' => max(1, min(65535, (int)($input['ftp_port'] ?? 21))), 'user' => trim($input['ftp_user'] ?? ''), 'pass' => $input['ftp_pass'] ?? '', 'ssl' => !empty($input['ftp_ssl']), 'passive' => ($input['ftp_passive'] ?? true) !== false, 'path' => trim($input['ftp_path'] ?? '/')];
            if ($id > 0 && $config['pass'] === '') {
                $existing = Database::fetchOne('SELECT config FROM media_storages WHERE id = ?', [$id]);
                $oldCfg = $existing ? (json_decode($existing['config'], true) ?: []) : [];
                $config['pass'] = $oldCfg['pass'] ?? '';
            }
        } elseif ($driver === 's3') {
            $config = ['key' => trim($input['s3_key'] ?? ''), 'secret' => $input['s3_secret'] ?? '', 'bucket' => trim($input['s3_bucket'] ?? ''), 'region' => trim($input['s3_region'] ?? 'us-east-1'), 'endpoint' => trim($input['s3_endpoint'] ?? ''), 'prefix' => trim($input['s3_prefix'] ?? '')];
            if ($id > 0 && $config['secret'] === '') {
                $existing = Database::fetchOne('SELECT config FROM media_storages WHERE id = ?', [$id]);
                $oldCfg = $existing ? (json_decode($existing['config'], true) ?: []) : [];
                $config['secret'] = $oldCfg['secret'] ?? '';
            }
        } elseif ($driver === 'webdav') {
            $config = ['url' => trim($input['dav_url'] ?? ''), 'user' => trim($input['dav_user'] ?? ''), 'pass' => $input['dav_pass'] ?? ''];
            if ($id > 0 && $config['pass'] === '') {
                $existing = Database::fetchOne('SELECT config FROM media_storages WHERE id = ?', [$id]);
                $oldCfg = $existing ? (json_decode($existing['config'], true) ?: []) : [];
                $config['pass'] = $oldCfg['pass'] ?? '';
            }
        }

         
        $monitorUrl = trim($input['monitor_url'] ?? '');
        if ($monitorUrl !== '') {
            $config['monitor_url'] = $monitorUrl;
             
            $existingKey = '';
            if ($id > 0) {
                $existingRow = Database::fetchOne('SELECT config FROM media_storages WHERE id = ?', [$id]);
                $existingCfg = $existingRow ? (json_decode($existingRow['config'] ?: '{}', true) ?: []) : [];
                $existingKey = $existingCfg['monitor_key'] ?? '';
            }
            $config['monitor_key'] = $existingKey !== '' ? $existingKey : bin2hex(random_bytes(16));
        }

        $secure = !empty($input['secure']);
        $status = in_array($input['status'] ?? 'active', ['active', 'readonly', 'disabled'], true) ? $input['status'] : 'active';
        $maxUsage = max(50, min(99, (int)($input['max_usage_pct'] ?? 90)));

         
        if ($id > 0 && $status !== 'active') {
            $current = Database::fetchOne('SELECT is_default FROM media_storages WHERE id = ?', [$id]);
            if ($current && $current['is_default']) {
                $otherActive = Database::fetchOne("SELECT id FROM media_storages WHERE id != ? AND status = 'active'", [$id]);
                if (!$otherActive) {
                    Router::json(['error' => 'Cannot disable the default storage — set another storage as default first.']);
                }
            }
        }
        $data = ['name' => $name, 'driver' => $driver, 'config' => json_encode($config), 'media_url' => $mediaUrl, 'secure_urls' => $secure ? 1 : 0, 'status' => $status, 'max_usage_pct' => $maxUsage];

        if ($secure) {
            $existing = $id > 0 ? Database::fetchOne('SELECT secure_secret FROM media_storages WHERE id = ?', [$id]) : null;
            $currentSecret = $existing['secure_secret'] ?? '';
            if ($currentSecret === '') {
                $data['secure_secret'] = SecureMedia::generateSecret();
            }
        }

        if ($id > 0) {
            Database::update('media_storages', $data, 'id = ?', [$id]);
        } else {
             
            $id = Database::insert('media_storages', $data);
            $count = Database::count('media_storages');
            if ($count === 1) {
                Database::update('media_storages', ['is_default' => 1], 'id = ?', [$id]);
            } elseif ($driver !== 'local') {
                 
                $localDefault = Database::fetchOne("SELECT id FROM media_storages WHERE driver = 'local' AND is_default = 1");
                if ($localDefault) {
                    Database::query('UPDATE media_storages SET is_default = 0');
                    Database::update('media_storages', ['is_default' => 1], 'id = ?', [$id]);
                    Database::update('media_storages', ['status' => 'readonly'], 'id = ?', [(int)$localDefault['id']]);
                }
            }
        }

        MediaStorage::resetCache();
        Router::json(['success' => true, 'id' => $id]);
    }

    public static function storageDelete(): void
    {
        Auth::requireSuperAdmin();
        if (!Router::verifyCsrf()) Router::json(['error' => 'Session expired.'], 403);

        $input = json_decode(file_get_contents('php://input'), true);
        $id = (int)($input['id'] ?? 0);
        if ($id <= 0) Router::json(['error' => 'Invalid.'], 400);

        $videoCount = Database::count('videos', 'storage_id = ?', [$id]);
        if ($videoCount > 0) Router::json(['error' => "This storage has {$videoCount} video(s). Reassign them first."], 409);

        $totalStorages = Database::count('media_storages');
        if ($totalStorages <= 1) Router::json(['error' => 'Cannot delete the last storage.']);

        $s = Database::fetchOne('SELECT is_default FROM media_storages WHERE id = ?', [$id]);
        if ($s && $s['is_default']) Router::json(['error' => 'Cannot delete the default storage. Set another storage as default first.']);

        Database::delete('media_storages', 'id = ?', [$id]);

        if ($s && $s['is_default']) {
            $first = Database::fetchOne('SELECT id FROM media_storages ORDER BY id LIMIT 1');
            if ($first) Database::update('media_storages', ['is_default' => 1], 'id = ?', [(int)$first['id']]);
        }

        MediaStorage::resetCache();
        Router::json(['success' => true]);
    }

    public static function storageToggleStatus(): void
    {
        Auth::requireSuperAdmin();
        if (!Router::verifyCsrf()) Router::json(['error' => 'Session expired.'], 403);

        $input = json_decode(file_get_contents('php://input'), true);
        $id = (int)($input['id'] ?? 0);
        if ($id <= 0) Router::json(['error' => 'Invalid.'], 400);

        $s = Database::fetchOne('SELECT status, is_default FROM media_storages WHERE id = ?', [$id]);
        if (!$s) Router::json(['error' => 'Storage not found.'], 404);

        $new = (($s['status'] ?? 'active') === 'disabled') ? 'active' : 'disabled';
        if ($new === 'disabled' && $s['is_default']) {
            Router::json(['error' => 'Cannot disable the default storage. Set another storage as default first.']);
        }

        Database::update('media_storages', ['status' => $new], 'id = ?', [$id]);
        MediaStorage::resetCache();
        Router::json(['success' => true, 'status' => $new]);
    }

    public static function storageSetDefault(): void
    {
        Auth::requireSuperAdmin();
        if (!Router::verifyCsrf()) Router::json(['error' => 'Session expired.'], 403);

        $input = json_decode(file_get_contents('php://input'), true);
        $id = (int)($input['id'] ?? 0);
        if ($id <= 0) Router::json(['error' => 'Invalid.'], 400);

        $storage = Database::fetchOne('SELECT * FROM media_storages WHERE id = ?', [$id]);
        if (!$storage) Router::json(['error' => 'Storage not found.'], 404);

         
        if ($storage['driver'] !== 'local') {
            $cfg = json_decode($storage['config'] ?: '{}', true) ?: [];
            $testOk = false;
            try {
                if ($storage['driver'] === 'ftp') {
                    $c = !empty($cfg['ssl']) ? @ftp_ssl_connect($cfg['host'] ?? '', (int)($cfg['port'] ?? 21), 10) : @ftp_connect($cfg['host'] ?? '', (int)($cfg['port'] ?? 21), 10);
                    if ($c && @ftp_login($c, $cfg['user'] ?? '', $cfg['pass'] ?? '')) {
                        if ($cfg['passive'] ?? true) ftp_pasv($c, true);
                        $tmp = tempnam(sys_get_temp_dir(), 'tp'); file_put_contents($tmp, 'test');
                        $testOk = @ftp_put($c, 'tubepress_default_test.tmp', $tmp, FTP_BINARY);
                        if ($testOk) @ftp_delete($c, 'tubepress_default_test.tmp');
                        @unlink($tmp);
                        ftp_close($c);
                    }
                } elseif ($storage['driver'] === 's3' || $storage['driver'] === 'webdav') {
                     
                    $tmp = tempnam(sys_get_temp_dir(), 'tp');
                    file_put_contents($tmp, 'test');
                    $testOk = MediaStorage::put($tmp, 'tubepress_default_test.tmp', $id);
                    @unlink($tmp);
                    if ($testOk) {
                        try { MediaStorage::deleteFile('tubepress_default_test.tmp', $id); } catch (\Throwable) {}
                    }
                }
            } catch (\Throwable) {}

            if (!$testOk) {
                Router::json(['error' => 'Connection test failed. Fix the storage configuration before setting it as default.']);
            }
        }

        Database::query('UPDATE media_storages SET is_default = 0');
        Database::update('media_storages', ['is_default' => 1], 'id = ?', [$id]);

        MediaStorage::resetCache();
        Router::json(['success' => true]);
    }

    public static function storageReassign(): void
    {
        Auth::requireSuperAdmin();
        if (!Router::verifyCsrf()) Router::json(['error' => 'Session expired.'], 403);

        $input = json_decode(file_get_contents('php://input'), true);
        $fromId = (int)($input['from'] ?? 0);
        $toId = (int)($input['to'] ?? 0);
        if ($fromId <= 0 || $toId <= 0 || $fromId === $toId) Router::json(['error' => 'Invalid.'], 400);

        $affected = Database::query('UPDATE videos SET storage_id = ? WHERE storage_id = ?', [$toId, $fromId])->rowCount();

        Database::query('UPDATE media_storages SET file_count = (SELECT COUNT(*) FROM videos WHERE storage_id = media_storages.id)');
        MediaStorage::resetCache();

        Router::json(['success' => true, 'moved' => $affected]);
    }

    public static function storageTest(): void
    {
        Auth::requireSuperAdmin();
        if (!Router::verifyCsrf()) Router::json(['error' => 'Session expired.'], 403);

        $input = json_decode(file_get_contents('php://input'), true);
        $driver = $input['driver'] ?? '';
        $storageId = (int)($input['storage_id'] ?? 0);

         
        $savedConfig = [];
        if ($storageId > 0) {
            $existing = MediaStorage::get($storageId);
            if ($existing) {
                $savedConfig = is_array($existing['config']) ? $existing['config'] : (json_decode($existing['config'] ?: '{}', true) ?: []);
            }
        }

        $checks = [];

        try {
            if ($driver === 'ftp') {
                $host = ($input['ftp_host'] ?? $input['host'] ?? '') !== '' ? ($input['ftp_host'] ?? $input['host'] ?? '') : ($savedConfig['host'] ?? '');
                $port = (int)(($input['ftp_port'] ?? $input['port'] ?? 0) > 0 ? ($input['ftp_port'] ?? $input['port'] ?? 21) : ($savedConfig['port'] ?? 21));
                $user = ($input['ftp_user'] ?? $input['user'] ?? '') !== '' ? ($input['ftp_user'] ?? $input['user'] ?? '') : ($savedConfig['user'] ?? '');
                $pass = ($input['ftp_pass'] ?? $input['pass'] ?? '') !== '' ? ($input['ftp_pass'] ?? $input['pass'] ?? '') : ($savedConfig['pass'] ?? '');
                $ssl = isset($input['ftp_ssl']) ? !empty($input['ftp_ssl']) : (isset($input['ssl']) ? !empty($input['ssl']) : !empty($savedConfig['ssl']));
                $passive = isset($input['ftp_passive']) ? !empty($input['ftp_passive']) : (isset($input['passive']) ? !empty($input['passive']) : ($savedConfig['passive'] ?? true));
                $path = ($input['ftp_path'] ?? $input['path'] ?? '') !== '' ? ($input['ftp_path'] ?? $input['path'] ?? '') : ($savedConfig['path'] ?? '/');

                if ($host === '' || $user === '') Router::json(['ok' => false, 'error' => 'Host and username are required.', 'checks' => []]);

                 
                $c = $ssl ? @ftp_ssl_connect($host, $port, 10) : @ftp_connect($host, $port, 10);
                if (!$c) Router::json(['ok' => false, 'error' => "Cannot connect to {$host}:{$port}. Check host, port, and firewall.", 'checks' => [['name' => 'Connection', 'ok' => false]]]);
                $checks[] = ['name' => 'Connection', 'ok' => true];

                 
                if (!@ftp_login($c, $user, $pass)) { ftp_close($c); $checks[] = ['name' => 'Authentication', 'ok' => false]; Router::json(['ok' => false, 'error' => 'Login failed — check username and password.', 'checks' => $checks]); }
                $checks[] = ['name' => 'Authentication', 'ok' => true];

                 
                if ($passive) ftp_pasv($c, true);

                 
                if ($path !== '' && $path !== '/') {
                    if (!@ftp_chdir($c, $path)) { ftp_close($c); $checks[] = ['name' => 'Path exists', 'ok' => false]; Router::json(['ok' => false, 'error' => "Path not found: {$path}", 'checks' => $checks]); }
                }
                $checks[] = ['name' => 'Path exists', 'ok' => true];

                 
                $list = @ftp_nlist($c, '.');
                $checks[] = ['name' => 'Read permission', 'ok' => $list !== false];

                 
                $testFile = '.tubepress_test_' . time();
                $tmp = tempnam(sys_get_temp_dir(), 'tp');
                file_put_contents($tmp, 'test');
                $writeOk = @ftp_put($c, $testFile, $tmp, FTP_BINARY);
                @unlink($tmp);
                $checks[] = ['name' => 'Write permission', 'ok' => $writeOk];

                 
                $deleteOk = false;
                if ($writeOk) {
                    $deleteOk = @ftp_delete($c, $testFile);
                }
                $checks[] = ['name' => 'Delete permission', 'ok' => $deleteOk];

                 
                $syst = @ftp_raw($c, 'SYST');
                $checks[] = ['name' => 'Server type', 'ok' => true, 'info' => trim(implode(' ', $syst ?: ['Unknown']))];

                ftp_close($c);

                $allOk = !in_array(false, array_column($checks, 'ok'), true);
                Router::json(['ok' => $allOk, 'error' => $allOk ? null : 'Some checks failed — see details.', 'checks' => $checks]);

            } elseif ($driver === 's3') {
                $key = ($input['s3_key'] ?? $input['key'] ?? '') !== '' ? ($input['s3_key'] ?? $input['key'] ?? '') : ($savedConfig['key'] ?? '');
                $secret = ($input['s3_secret'] ?? $input['secret'] ?? '') !== '' ? ($input['s3_secret'] ?? $input['secret'] ?? '') : ($savedConfig['secret'] ?? '');
                $bucket = ($input['s3_bucket'] ?? $input['bucket'] ?? '') !== '' ? ($input['s3_bucket'] ?? $input['bucket'] ?? '') : ($savedConfig['bucket'] ?? '');
                $region = ($input['s3_region'] ?? $input['region'] ?? '') !== '' ? ($input['s3_region'] ?? $input['region'] ?? '') : ($savedConfig['region'] ?? 'us-east-1');
                $endpoint = ($input['s3_endpoint'] ?? $input['endpoint'] ?? '') !== '' ? ($input['s3_endpoint'] ?? $input['endpoint'] ?? '') : ($savedConfig['endpoint'] ?? '');

                if ($key === '' || $secret === '' || $bucket === '') Router::json(['ok' => false, 'error' => 'Key, secret, and bucket are required.', 'checks' => []]);

                $ep = $endpoint !== '' ? rtrim($endpoint, '/') : "https://{$bucket}.s3.{$region}.amazonaws.com";
                $s3Host = parse_url($ep, PHP_URL_HOST);

                 
                $s3sign = function(string $method, string $path, string $qs = '') use ($key, $secret, $region, $s3Host, $ep, $endpoint, $bucket) {
                    $date = gmdate('Ymd\THis\Z'); $ds = gmdate('Ymd');
                    $hdrs = ['host' => $s3Host, 'x-amz-content-sha256' => 'UNSIGNED-PAYLOAD', 'x-amz-date' => $date]; ksort($hdrs);
                    $sh = implode(';', array_keys($hdrs)); $ch = ''; foreach ($hdrs as $k => $v) $ch .= "{$k}:{$v}\n";
                    $cr = "{$method}\n{$path}\n{$qs}\n{$ch}\n{$sh}\nUNSIGNED-PAYLOAD";
                    $scope = "{$ds}/{$region}/s3/aws4_request";
                    $sts = "AWS4-HMAC-SHA256\n{$date}\n{$scope}\n" . hash('sha256', $cr);
                    $sk = hash_hmac('sha256', $ds, 'AWS4' . $secret, true);
                    $sk = hash_hmac('sha256', $region, $sk, true);
                    $sk = hash_hmac('sha256', 's3', $sk, true);
                    $sk = hash_hmac('sha256', 'aws4_request', $sk, true);
                    return ["Authorization: AWS4-HMAC-SHA256 Credential={$key}/{$scope}, SignedHeaders={$sh}, Signature=" . hash_hmac('sha256', $sts, $sk), "x-amz-content-sha256: UNSIGNED-PAYLOAD", "x-amz-date: {$date}"];
                };

                $basePath = $endpoint !== '' ? '/' . $bucket . '/' : '/';

                 
                $headers = $s3sign('GET', $basePath, 'list-type=2&max-keys=1');
                $curl = curl_init($ep . $basePath . '?list-type=2&max-keys=1');
                curl_setopt_array($curl, [CURLOPT_HTTPHEADER => $headers, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 10]);
                curl_exec($curl); $code = (int)curl_getinfo($curl, CURLINFO_HTTP_CODE); curl_close($curl);
                if ($code === 0) Router::json(['ok' => false, 'error' => "Cannot connect to {$s3Host}", 'checks' => [['name' => 'Connection', 'ok' => false]]]);
                $checks[] = ['name' => 'Connection', 'ok' => $code >= 200 && $code < 400];
                $checks[] = ['name' => 'Read (ListBucket)', 'ok' => $code >= 200 && $code < 300, 'info' => $code >= 300 ? "HTTP {$code}" : null];

                 
                $testKey = '.tubepress-test-' . time();
                $putPath = $basePath . $testKey;
                $headers = $s3sign('PUT', $putPath);
                $headers[] = 'Content-Type: text/plain';
                $curl = curl_init($ep . $putPath);
                curl_setopt_array($curl, [CURLOPT_CUSTOMREQUEST => 'PUT', CURLOPT_HTTPHEADER => $headers, CURLOPT_POSTFIELDS => 'test', CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 10]);
                curl_exec($curl); $putCode = (int)curl_getinfo($curl, CURLINFO_HTTP_CODE); curl_close($curl);
                $checks[] = ['name' => 'Write (PutObject)', 'ok' => $putCode >= 200 && $putCode < 300, 'info' => $putCode >= 300 ? "HTTP {$putCode}" : null];

                 
                if ($putCode >= 200 && $putCode < 300) {
                    $headers = $s3sign('DELETE', $putPath);
                    $curl = curl_init($ep . $putPath);
                    curl_setopt_array($curl, [CURLOPT_CUSTOMREQUEST => 'DELETE', CURLOPT_HTTPHEADER => $headers, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 10]);
                    curl_exec($curl); $delCode = (int)curl_getinfo($curl, CURLINFO_HTTP_CODE); curl_close($curl);
                    $checks[] = ['name' => 'Delete', 'ok' => $delCode >= 200 && $delCode < 300];
                }

                $allOk = !in_array(false, array_column($checks, 'ok'), true);
                Router::json(['ok' => $allOk, 'error' => $allOk ? null : 'Some checks failed.', 'checks' => $checks]);

            } elseif ($driver === 'webdav') {
                $url = ($input['dav_url'] ?? $input['url'] ?? '') !== '' ? ($input['dav_url'] ?? $input['url'] ?? '') : ($savedConfig['url'] ?? '');
                $user = ($input['dav_user'] ?? $input['user'] ?? '') !== '' ? ($input['dav_user'] ?? $input['user'] ?? '') : ($savedConfig['user'] ?? '');
                $pass = ($input['dav_pass'] ?? $input['pass'] ?? '') !== '' ? ($input['dav_pass'] ?? $input['pass'] ?? '') : ($savedConfig['pass'] ?? '');

                if ($url === '') Router::json(['ok' => false, 'error' => 'WebDAV URL is required.', 'checks' => []]);

                $davReq = function(string $method, string $reqUrl, ?string $body = null) use ($user, $pass) {
                    $curl = curl_init($reqUrl);
                    $opts = [CURLOPT_CUSTOMREQUEST => $method, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 10, CURLOPT_FOLLOWLOCATION => true];
                    if ($body !== null) $opts[CURLOPT_POSTFIELDS] = $body;
                    if ($method === 'PROPFIND') $opts[CURLOPT_HTTPHEADER] = ['Depth: 0'];
                    curl_setopt_array($curl, $opts);
                    if ($user !== '') { curl_setopt($curl, CURLOPT_USERPWD, $user . ':' . $pass); curl_setopt($curl, CURLOPT_HTTPAUTH, CURLAUTH_BASIC | CURLAUTH_DIGEST); }
                    curl_exec($curl); $code = (int)curl_getinfo($curl, CURLINFO_HTTP_CODE); $err = curl_error($curl); curl_close($curl);
                    return ['code' => $code, 'error' => $err];
                };

                 
                $r = $davReq('PROPFIND', $url);
                if ($r['error'] !== '') Router::json(['ok' => false, 'error' => "Cannot connect: {$r['error']}", 'checks' => [['name' => 'Connection', 'ok' => false]]]);
                $checks[] = ['name' => 'Connection', 'ok' => $r['code'] > 0];
                $checks[] = ['name' => 'Authentication', 'ok' => $r['code'] !== 401, 'info' => $r['code'] === 401 ? 'Check username and password' : null];
                $checks[] = ['name' => 'Read (PROPFIND)', 'ok' => $r['code'] >= 200 && $r['code'] < 300];

                 
                if ($r['code'] >= 200 && $r['code'] < 300) {
                    $testUrl = rtrim($url, '/') . '/.tubepress-test-' . time();
                    $wr = $davReq('PUT', $testUrl, 'test');
                    $checks[] = ['name' => 'Write (PUT)', 'ok' => $wr['code'] >= 200 && $wr['code'] < 300, 'info' => $wr['code'] >= 300 ? "HTTP {$wr['code']}" : null];

                     
                    if ($wr['code'] >= 200 && $wr['code'] < 300) {
                        $dr = $davReq('DELETE', $testUrl);
                        $checks[] = ['name' => 'Delete', 'ok' => $dr['code'] >= 200 && $dr['code'] < 300];
                    }
                }

                $allOk = !in_array(false, array_column($checks, 'ok'), true);
                Router::json(['ok' => $allOk, 'error' => $allOk ? null : 'Some checks failed.', 'checks' => $checks]);
            } else {
                Router::json(['ok' => false, 'error' => 'Unknown driver.', 'checks' => []]);
            }
        } catch (\Throwable $e) {
            Router::json(['ok' => false, 'error' => $e->getMessage(), 'checks' => $checks]);
        }
    }

    public static function storageCreate(): void
    {
        Auth::requireSuperAdmin();
        $storage = null;
        $vc = 0;
        $pageTitle = 'Add Storage';
        $activeMenu = 'settings';
        $contentTemplate = '_storage_form';
        $driverLabels = ['local' => 'Local Server', 'ftp' => 'FTP / FTPS', 's3' => 'S3 Compatible', 'webdav' => 'WebDAV'];
        $driverIcons = ['local' => 'fa-server', 'ftp' => 'fa-folder-open', 's3' => 'fa-cloud', 'webdav' => 'fa-network-wired'];
        require APP_ROOT . '/templates/admin/storage_form.php';
    }

    public static function storageEdit(): void
    {
        Auth::requireSuperAdmin();
        $id = (int)Router::param('id');
        $storage = Database::fetchOne('SELECT * FROM media_storages WHERE id = ?', [$id]);
        if (!$storage) { Router::redirect('/admin/settings?tab=storage'); return; }
        $storage['config'] = json_decode($storage['config'] ?: '{}', true) ?: [];
        $vc = Database::count('videos', 'storage_id = ?', [$id]);
        $pageTitle = 'Edit Storage — ' . $storage['name'];
        $activeMenu = 'settings';
        $contentTemplate = '_storage_form';
        $driverLabels = ['local' => 'Local Server', 'ftp' => 'FTP / FTPS', 's3' => 'S3 Compatible', 'webdav' => 'WebDAV'];
        $driverIcons = ['local' => 'fa-server', 'ftp' => 'fa-folder-open', 's3' => 'fa-cloud', 'webdav' => 'fa-network-wired'];
        require APP_ROOT . '/templates/admin/storage_form.php';
    }

    public static function storageTestMonitor(): void
    {
        Auth::requireSuperAdmin();
        if (!Router::verifyCsrf()) Router::json(['error' => 'Session expired.'], 403);

        $input = json_decode(file_get_contents('php://input'), true);
        $url = trim($input['url'] ?? '');
        $storageId = (int)($input['storage_id'] ?? 0);

        if ($url === '') Router::json(['ok' => false, 'error' => 'No monitor URL provided.']);

         
        $key = '';
        if ($storageId > 0) {
            $s = MediaStorage::get($storageId);
            if ($s) {
                $cfg = is_array($s['config']) ? $s['config'] : (json_decode($s['config'] ?: '{}', true) ?: []);
                $key = $cfg['monitor_key'] ?? '';
            }
        }

         
        $testUrl = rtrim($url, '/') . '?action=ping' . ($key !== '' ? '&key=' . urlencode($key) : '');
        $ch = curl_init($testUrl);
        curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 10, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_SSL_VERIFYPEER => true]);
        $body = curl_exec($ch);
        $code = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $err = curl_error($ch);
        curl_close($ch);

        if ($code === 0) Router::json(['ok' => false, 'error' => 'Cannot reach the script: ' . ($err ?: 'timeout or DNS failure')]);
        if ($code === 404) Router::json(['ok' => false, 'error' => 'Script not found (404) — check the URL and make sure the file is uploaded.']);
        if ($code >= 500) Router::json(['ok' => false, 'error' => "Server error (HTTP {$code}) — the script may have a PHP syntax error."]);

         
        if ($code === 403 && $key === '') {
            Router::json(['ok' => true]);
        }
        if ($code === 403) Router::json(['ok' => false, 'error' => 'Authentication failed — re-download the monitor script to get the correct key.']);

        if ($code >= 200 && $code < 300) {
            Router::json(['ok' => true]);
        }

        Router::json(['ok' => false, 'error' => "Unexpected HTTP {$code}"]);
    }

    public static function testEmail(): void
    {
        Auth::requireSuperAdmin();
        if (!Router::verifyCsrf()) {
            Router::json(['success' => false, 'error' => 'Session expired.'], 403);
            return;
        }

        $email = Setting::get('contact_email', '');
        if ($email === '' || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
            $email = Auth::user()['email'] ?? '';
        }
        if ($email === '') {
            Router::json(['success' => false, 'error' => 'No email address configured. Set a contact email in General settings.']);
            return;
        }

        $result = Mailer::test($email);
        Router::json($result);
    }

    public static function testTelegram(): void
    {
        Auth::requireSuperAdmin();
        if (!Router::verifyCsrf()) {
            Router::json(['success' => false, 'error' => 'Session expired.'], 403);
            return;
        }
        $token  = trim($_POST['telegram_bot_token'] ?? '') ?: (string) Setting::get('telegram_bot_token', '');
        $chatId = trim($_POST['telegram_chat_id'] ?? '') ?: (string) Setting::get('telegram_chat_id', '');
        if ($token === '' || $chatId === '') {
            Router::json(['success' => false, 'error' => 'Enter a bot token and chat ID first.']);
            return;
        }
        try {
            $site = (string) Setting::get('site_name', 'TubePress');
            Telegram::sendMessage('✅ <b>' . Telegram::esc($site) . "</b>\nTelegram notifications are configured correctly.", $token, $chatId);
            Router::json(['success' => true]);
        } catch (\Throwable $e) {
            Router::json(['success' => false, 'error' => $e->getMessage()]);
        }
    }

     
    public static function mailPreview(): void
    {
        Auth::requireSuperAdmin();
        if (!Router::verifyCsrf()) {
            Router::json(['success' => false, 'error' => 'Session expired.'], 403);
            return;
        }
        $key = $_POST['type'] ?? '';
        if (!isset(MailTypes::all()[$key])) {
            Router::json(['success' => false, 'error' => 'Unknown email type.'], 422);
            return;
        }
        $subject = trim($_POST['subject'] ?? '');
        $body    = trim($_POST['body'] ?? '');
        $sample  = MailTypes::sampleVars($key);
        $rendered = Mailer::previewType(
            $key,
            $sample['vars'],
            $sample['html'],
            $subject !== '' ? $subject : null,
            $body !== '' ? $body : null
        );
        [$fromEmail, $fromName] = Mailer::fromInfo();
        $type = MailTypes::all()[$key];
        $to = ($type['audience'] ?? '') === 'Admin'
            ? (trim((string) Setting::get('contact_email', '')) ?: (Auth::user()['email'] ?? 'admin@yoursite.com'))
            : 'jane@example.com';
        Router::json([
            'success'    => true,
            'subject'    => $rendered['subject'],
            'html'       => $rendered['html'],
            'from_name'  => $fromName,
            'from_email' => $fromEmail,
            'to'         => $to,
        ]);
    }

     

    public static function twoFactorSetup(): void
    {
        Auth::requireAdmin();  
        if (!Router::verifyCsrf()) {
            Router::json(['success' => false, 'error' => 'Invalid CSRF token'], 403);
        }

        $secret = TwoFactor::generateSecret();
        $user = Auth::user();
        $siteName = Setting::get('site_name', 'TubePress');
        $qrUri = TwoFactor::getQrUri($user['email'], $secret, $siteName);

        Router::json(['secret' => $secret, 'qrUri' => $qrUri]);
    }

    public static function twoFactorEnable(): void
    {
        Auth::requireAdmin();  
        if (!Router::verifyCsrf()) {
            Router::json(['success' => false, 'error' => 'Invalid CSRF token'], 403);
        }

        $secret = trim($_POST['secret'] ?? '');
        $code = trim($_POST['code'] ?? '');

        if ($secret === '' || $code === '') {
            Router::json(['success' => false, 'error' => 'Missing secret or code.'], 400);
            return;
        }

        $matchedCounter = TwoFactor::verify($secret, $code);
        if (!$matchedCounter) {
            Router::json(['success' => false, 'error' => 'Invalid code. Please check your authenticator app and try again.'], 400);
            return;
        }

         
        Database::update('users', ['totp_secret' => $secret, 'totp_last_counter' => $matchedCounter], 'id = ?', [Auth::id()]);
        Router::json(['success' => true]);
    }

    public static function twoFactorDisable(): void
    {
        Auth::requireAdmin();  
        if (!Router::verifyCsrf()) {
            Router::json(['success' => false, 'error' => 'Invalid CSRF token'], 403);
        }

        $code = trim($_POST['code'] ?? '');
        $user = Auth::user();

        if (empty($user['totp_secret'])) {
            Router::json(['success' => false, 'error' => '2FA is not enabled.'], 400);
            return;
        }

        $matchedCounter = TwoFactor::verify(
            $user['totp_secret'],
            $code,
            1,
            isset($user['totp_last_counter']) ? (int) $user['totp_last_counter'] : null
        );
        if (!$matchedCounter) {
            Router::json(['success' => false, 'error' => 'Invalid code. Please try again.'], 400);
            return;
        }

        Database::update('users', ['totp_secret' => null, 'totp_last_counter' => null], 'id = ?', [Auth::id()]);
        Router::json(['success' => true]);
    }

     

     
     
     
     
     

    private static function purgeRemaining(string $type): int
    {
        if ($type === 'users') {
            return (int) (Database::fetchOne("SELECT COUNT(*) c FROM users WHERE role != 'admin'")['c'] ?? 0);
        }
        $table = $type === 'imports' ? 'import_jobs' : $type;
        return (int) (Database::fetchOne("SELECT COUNT(*) c FROM {$table}")['c'] ?? 0);
    }

    

    private static function purgeVideosChunk(float $deadline): int
    {
        $deleted = 0;
        while (microtime(true) < $deadline) {
            $rows = Database::fetchAll("SELECT id, video_folder, storage_id FROM videos ORDER BY id LIMIT 100");
            if (!$rows) break;

            $done = [];
            foreach ($rows as $r) {
                if (!empty($r['video_folder'])) {
                     
                     
                     
                    try {
                        VideoAsset::deleteFolder($r['video_folder'], !empty($r['storage_id']) ? (int) $r['storage_id'] : null);
                    } catch (\Throwable $e) {
                        error_log('purge: deleteFolder failed for ' . $r['video_folder'] . ': ' . $e->getMessage());
                    }
                }
                $done[] = (int) $r['id'];
                if (microtime(true) >= $deadline) break;  
            }
            if (!$done) break;

            $ph = implode(',', array_fill(0, count($done), '?'));
             
            foreach (['transcode_jobs', 'video_view_log', 'ctr_video_hourly', 'ctr_video_context', 'ctr_homepage_pool'] as $t) {
                try { Database::query("DELETE FROM {$t} WHERE video_id IN ({$ph})", $done); } catch (\Throwable $e) {}
            }
             
            Database::query("DELETE FROM videos WHERE id IN ({$ph})", $done);
            $deleted += count($done);
        }
        return $deleted;
    }

    

    private static function purgeEntityChunk(string $table, ?string $fileCol, ?string $uploadDir, float $deadline): int
    {
        $deleted = 0;
        do {
            $cols = 'id' . ($fileCol ? ", {$fileCol}" : '');
            $rows = Database::fetchAll("SELECT {$cols} FROM {$table} ORDER BY id LIMIT 500");
            if (!$rows) break;

            if ($fileCol !== null) {
                foreach ($rows as $r) {
                    $f = (string) ($r[$fileCol] ?? '');
                    if ($f !== '' && !str_starts_with($f, 'http')) {
                        FileUpload::delete($uploadDir . '/' . $f);
                    }
                }
            }
            $ids = array_map(static fn($r) => (int) $r['id'], $rows);
            $ph  = implode(',', array_fill(0, count($ids), '?'));
            Database::query("DELETE FROM {$table} WHERE id IN ({$ph})", $ids);
            $deleted += count($ids);
        } while (microtime(true) < $deadline);
        return $deleted;
    }

    

    private static function purgeUsersChunk(float $deadline): int
    {
        $deleted = 0;
        do {
            $rows = Database::fetchAll("SELECT id FROM users WHERE role != 'admin' ORDER BY id LIMIT 500");
            if (!$rows) break;
            $ids = array_map(static fn($r) => (int) $r['id'], $rows);
            $ph  = implode(',', array_fill(0, count($ids), '?'));
            Database::query("DELETE FROM users WHERE id IN ({$ph})", $ids);
            $deleted += count($ids);
        } while (microtime(true) < $deadline);
        return $deleted;
    }

     
    private static function purgeSimpleChunk(string $table, float $deadline): int
    {
        $deleted = 0;
        do {
            $affected = Database::query("DELETE FROM {$table} LIMIT 5000")->rowCount();
            $deleted += $affected;
        } while ($affected >= 5000 && microtime(true) < $deadline);
        return $deleted;
    }

    

    private static function purgeAlbumsChunk(float $deadline): int
    {
        $deleted = 0;
        while (microtime(true) < $deadline) {
            $rows = Database::fetchAll("SELECT id, album_folder, storage_id FROM albums ORDER BY id LIMIT 100");
            if (!$rows) break;

            $done = [];
            foreach ($rows as $r) {
                if (!empty($r['album_folder'])) {
                    try {
                        AlbumAsset::deleteFolder($r['album_folder'], !empty($r['storage_id']) ? (int) $r['storage_id'] : null);
                    } catch (\Throwable $e) {
                        error_log('purge: album deleteFolder failed for ' . $r['album_folder'] . ': ' . $e->getMessage());
                    }
                }
                $done[] = (int) $r['id'];
                if (microtime(true) >= $deadline) break;  
            }
            if (!$done) break;

            $ph = implode(',', array_fill(0, count($done), '?'));
             
             
             
            foreach (['album_likes', 'album_view_log', 'ctr_album_hourly', 'ctr_album_context', 'ctr_album_image', 'comments', 'reports'] as $t) {
                try { Database::query("DELETE FROM {$t} WHERE album_id IN ({$ph})", $done); } catch (\Throwable $e) {}
            }
             
            Database::query("DELETE FROM albums WHERE id IN ({$ph})", $done);
            $deleted += count($done);
        }
        return $deleted;
    }

    public static function purge(): void
    {
        Auth::requireSuperAdmin();
        Gate::authorize('manage_settings');  
        if (!Router::verifyCsrf()) {
            Router::json(['success' => false, 'error' => 'Session expired. Please refresh.'], 403);
            return;
        }

        $type = $_POST['type'] ?? '';
        $validTypes = ['videos', 'categories', 'tags', 'performers', 'channels', 'albums', 'album_categories', 'album_tags', 'album_performers', 'album_channels', 'users', 'comments', 'pages', 'imports'];
        if (!in_array($type, $validTypes, true)) {
            Router::json(['success' => false, 'error' => 'Invalid type.'], 400);
            return;
        }

         
         
         
        $gotLock = (int) (Database::fetchOne("SELECT GET_LOCK('cms_purge', 0) AS l")['l'] ?? 0);
        if ($gotLock !== 1) {
            Router::json(['success' => true, 'deleted' => 0, 'remaining' => self::purgeRemaining($type), 'done' => false, 'busy' => true]);
            return;
        }

        try {
            @set_time_limit(0);
            $deadline = microtime(true) + 3.5;  
            $deleted = 0;

            switch ($type) {
                case 'videos':     $deleted = self::purgeVideosChunk($deadline); break;
                case 'categories': $deleted = self::purgeEntityChunk('categories', 'thumbnail', 'thumbnails', $deadline); break;
                case 'performers': $deleted = self::purgeEntityChunk('performers', 'photo', 'performers', $deadline); break;
                case 'channels':    $deleted = self::purgeEntityChunk('channels', 'logo', 'channels', $deadline); break;
                case 'albums':            $deleted = self::purgeAlbumsChunk($deadline); break;
                case 'album_categories':  $deleted = self::purgeEntityChunk('album_categories', 'thumbnail', 'thumbnails', $deadline); break;
                case 'album_performers':  $deleted = self::purgeEntityChunk('album_performers', 'photo', 'performers', $deadline); break;
                case 'album_channels':    $deleted = self::purgeEntityChunk('album_channels', 'logo', 'channels', $deadline); break;
                case 'album_tags':        $deleted = self::purgeEntityChunk('album_tags', null, null, $deadline); break;
                case 'tags':       $deleted = self::purgeEntityChunk('tags', null, null, $deadline); break;
                case 'pages':      $deleted = self::purgeEntityChunk('pages', null, null, $deadline); break;
                case 'users':      $deleted = self::purgeUsersChunk($deadline); break;
                case 'comments':   $deleted = self::purgeSimpleChunk('comments', $deadline); break;
                case 'imports':    $deleted = self::purgeSimpleChunk('import_jobs', $deadline); break;
            }

            $remaining = self::purgeRemaining($type);
            Router::json(['success' => true, 'deleted' => $deleted, 'remaining' => $remaining, 'done' => $remaining === 0]);
        } finally {
            try { Database::query("SELECT RELEASE_LOCK('cms_purge')"); } catch (\Throwable $e) {}
        }
    }

     

    public static function runHealthChecks(): array
    {
        $checks = [];

         
        $phpVer = PHP_VERSION;
        $checks[] = [
            'category' => 'PHP',
            'name'     => 'PHP Version',
            'current'  => $phpVer,
            'recommended' => '>= 8.1',
            'status'   => version_compare($phpVer, '8.1.0', '>=') ? 'pass' : 'fail',
            'fix'      => 'Upgrade PHP to 8.1 or later',
        ];

         
         
         
         

         
        $memLimit = ini_get('memory_limit');
        $memBytes = self::parseBytes($memLimit);
        $checks[] = [
            'category' => 'PHP Config',
            'name'     => 'memory_limit',
            'current'  => $memLimit,
            'recommended' => '>= 128M',
            'status'   => $memBytes >= 128 * 1024 * 1024 || $memBytes === -1 ? 'pass' : 'warn',
            'fix'      => 'Set memory_limit = 128M in php.ini',
        ];

        $maxExec = (int) ini_get('max_execution_time');
        $checks[] = [
            'category' => 'PHP Config',
            'name'     => 'max_execution_time',
            'current'  => (string) $maxExec,
            'recommended' => '>= 120',
            'status'   => $maxExec >= 120 || $maxExec === 0 ? 'pass' : 'warn',
            'fix'      => 'Set max_execution_time = 120 in php.ini',
        ];

        $uploadMax = ini_get('upload_max_filesize');
        $uploadBytes = self::parseBytes($uploadMax);
        $checks[] = [
            'category' => 'PHP Config',
            'name'     => 'upload_max_filesize',
            'current'  => $uploadMax,
            'recommended' => '>= 64M',
             
             
            'status'   => $uploadBytes >= 64 * 1024 * 1024 || $uploadBytes === -1 ? 'pass' : 'warn',
            'fix'      => 'Raise upload_max_filesize in php.ini only if you upload large videos directly from the browser',
        ];

        $postMax = ini_get('post_max_size');
        $postBytes = self::parseBytes($postMax);
        $checks[] = [
            'category' => 'PHP Config',
            'name'     => 'post_max_size',
            'current'  => $postMax,
            'recommended' => '>= upload_max_filesize',
            'status'   => $postBytes >= $uploadBytes || $postBytes === -1 ? 'pass' : 'warn',
            'fix'      => 'Keep post_max_size >= upload_max_filesize in php.ini',
        ];

        $maxInputVars = (int) ini_get('max_input_vars');
        $checks[] = [
            'category' => 'PHP Config',
            'name'     => 'max_input_vars',
            'current'  => (string) $maxInputVars,
            'recommended' => '>= 1000',
            'status'   => $maxInputVars >= 1000 ? 'pass' : 'warn',
            'fix'      => 'Set max_input_vars = 1000 in php.ini',
        ];

        $allowUrlFopen = ini_get('allow_url_fopen');
        $checks[] = [
            'category' => 'PHP Config',
            'name'     => 'allow_url_fopen',
            'current'  => $allowUrlFopen ? 'On' : 'Off',
            'recommended' => 'On',
            'status'   => $allowUrlFopen ? 'pass' : 'warn',
            'fix'      => 'Set allow_url_fopen = On in php.ini',
        ];

        $fileUploads = ini_get('file_uploads');
        $checks[] = [
            'category' => 'PHP Config',
            'name'     => 'file_uploads',
            'current'  => $fileUploads ? 'On' : 'Off',
            'recommended' => 'On',
            'status'   => $fileUploads ? 'pass' : 'warn',
            'fix'      => $fileUploads ? '' : 'Set file_uploads = On in php.ini to allow direct browser uploads (catalogue/feed imports work either way)',
        ];

         
         
        $requiredDirs = [
            'storage/'        => APP_ROOT . '/storage',
            'storage/logs/'   => APP_ROOT . '/storage/logs',
            'storage/cache/'  => APP_ROOT . '/storage/cache',
            'storage/tmp/'    => APP_ROOT . '/storage/tmp',
            'public/uploads/' => APP_ROOT . '/public/uploads',
        ];
        foreach ($requiredDirs as $label => $path) {
            $exists = is_dir($path);
            $writable = $exists && is_writable($path);
            $checks[] = [
                'category' => 'Permissions',
                'name'     => $label,
                'current'  => $writable ? 'Writable' : ($exists ? 'Not writable' : 'Missing'),
                'recommended' => 'Writable',
                'status'   => $writable ? 'pass' : 'fail',
                'fix'      => $writable ? '' : ($exists
                    ? "chmod 775 {$path} && chown www-data:www-data {$path}"
                    : "mkdir -p {$path} && chmod 775 {$path} && chown www-data:www-data {$path}"),
            ];
        }
         
         
        $optionalDirs = [
            'config/'                    => [APP_ROOT . '/config', 'only the installer writes here; runtime settings live in the database'],
            'storage/imports/'           => [APP_ROOT . '/storage/imports', 'CSV/feed import staging'],
            'storage/backups/'           => [APP_ROOT . '/storage/backups', 'the built-in backup feature'],
            'public/uploads/videos/'     => [APP_ROOT . '/public/uploads/videos', 'auto-created on first upload'],
            'public/uploads/performers/' => [APP_ROOT . '/public/uploads/performers', 'auto-created when needed'],
            'public/uploads/channels/'    => [APP_ROOT . '/public/uploads/channels', 'auto-created when needed'],
            'public/uploads/branding/'   => [APP_ROOT . '/public/uploads/branding', 'auto-created when needed'],
            'themes/'                    => [APP_ROOT . '/themes', 'only the in-admin Theme Editor writes here'],
        ];
        foreach ($optionalDirs as $label => [$path, $purpose]) {
            $exists = is_dir($path);
            $writable = $exists && is_writable($path);
            $checks[] = [
                'category' => 'Permissions',
                'name'     => $label,
                'current'  => $writable ? 'Writable' : ($exists ? 'Not writable' : 'Not present'),
                'recommended' => 'Writable (optional)',
                'status'   => $writable ? 'pass' : 'warn',
                'fix'      => $writable ? '' : ('Optional — needed for ' . $purpose . '. Enable with: mkdir -p ' . $path . ' && chown www-data:www-data ' . $path),
            ];
        }

         
        $themesDir = APP_ROOT . '/themes';
        if (is_dir($themesDir)) {
            $notWritable = [];
            $rii = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($themesDir, \FilesystemIterator::SKIP_DOTS));
            foreach ($rii as $fileInfo) {
                if (!$fileInfo->isWritable()) {
                    $notWritable[] = str_replace(APP_ROOT . '/', '', $fileInfo->getPathname());
                }
            }
            $checks[] = [
                'category' => 'Permissions',
                'name'     => 'themes/ files',
                'current'  => empty($notWritable)
                    ? 'All writable'
                    : count($notWritable) . ' file(s) not writable',
                'recommended' => 'Writable for in-admin Theme Editor (optional)',
                 
                 
                'status'   => empty($notWritable) ? 'pass' : 'warn',
                'fix'      => empty($notWritable) ? '' : "Optional — only to edit themes from the admin UI: chown -R www-data:www-data {$themesDir}",
            ];
        }

         
        try {
            Database::fetchOne('SELECT 1');
            $checks[] = [
                'category' => 'Database',
                'name'     => 'Connection',
                'current'  => 'OK',
                'recommended' => 'Connected',
                'status'   => 'pass',
                'fix'      => '',
            ];
        } catch (\Throwable $e) {
            $checks[] = [
                'category' => 'Database',
                'name'     => 'Connection',
                'current'  => 'Failed',
                'recommended' => 'Connected',
                'status'   => 'fail',
                'fix'      => 'Check database credentials in config/database.php',
            ];
        }

        $mysqlVer = '';
        try {
            $row = Database::fetchOne('SELECT VERSION() AS v');
            $mysqlVer = $row['v'] ?? '';
            $cleanVer = preg_replace('/[^0-9.].*/', '', $mysqlVer);
            $checks[] = [
                'category' => 'Database',
                'name'     => 'MySQL Version',
                'current'  => $mysqlVer,
                'recommended' => '>= 5.7',
                'status'   => version_compare($cleanVer, '5.7', '>=') ? 'pass' : 'fail',
                'fix'      => 'Upgrade MySQL to 5.7 or later',
            ];
        } catch (\Throwable $e) {
             
        }

        try {
            $charsetRow = Database::fetchOne("SELECT @@character_set_database AS cs");
            $charset = $charsetRow['cs'] ?? '';
            $checks[] = [
                'category' => 'Database',
                'name'     => 'Charset',
                'current'  => $charset,
                'recommended' => 'utf8mb4',
                'status'   => $charset === 'utf8mb4' ? 'pass' : 'warn',
                'fix'      => 'ALTER DATABASE `' . Database::getName() . '` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci',
            ];
        } catch (\Throwable $e) {
             
        }

         
         
         
        $isHttps = (!empty($_SERVER['HTTPS']) && strtolower((string) $_SERVER['HTTPS']) !== 'off')
                || (($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '') === 'https')
                || (strtolower($_SERVER['HTTP_X_FORWARDED_SSL'] ?? '') === 'on')
                || (strtolower($_SERVER['HTTP_FRONT_END_HTTPS'] ?? '') === 'on')
                || ((int) ($_SERVER['SERVER_PORT'] ?? 0) === 443);
        $checks[] = [
            'category' => 'Security',
            'name'     => 'HTTPS',
            'current'  => $isHttps ? 'Enabled' : 'Not detected',
            'recommended' => 'Enabled',
             
             
            'status'   => $isHttps ? 'pass' : 'warn',
            'fix'      => $isHttps ? '' : 'Serve the site over HTTPS (e.g. Certbot). Behind a proxy, forward the scheme: fastcgi_param HTTPS on; or an X-Forwarded-Proto header.',
        ];

        $displayErrors = ini_get('display_errors');
        $checks[] = [
            'category' => 'Security',
            'name'     => 'display_errors',
            'current'  => $displayErrors ? 'On' : 'Off',
            'recommended' => 'Off',
            'status'   => !$displayErrors ? 'pass' : 'warn',
            'fix'      => 'Set display_errors = Off in php.ini',
        ];

        $exposePhp = ini_get('expose_php');
        $checks[] = [
            'category' => 'Security',
            'name'     => 'expose_php',
            'current'  => $exposePhp ? 'On' : 'Off',
            'recommended' => 'Off',
            'status'   => !$exposePhp ? 'pass' : 'warn',
            'fix'      => 'Set expose_php = Off in php.ini',
        ];

         
        $opcacheEnabled = function_exists('opcache_get_status') && !empty(opcache_get_status(false)['opcache_enabled']);
        $checks[] = [
            'category' => 'Performance',
            'name'     => 'OPcache',
            'current'  => $opcacheEnabled ? 'Enabled' : 'Disabled',
            'recommended' => 'Enabled',
            'status'   => $opcacheEnabled ? 'pass' : 'warn',
            'fix'      => 'Set opcache.enable=1 in php.ini',
        ];

         
        $execAvailable = function_exists('exec') && !in_array('exec', array_map('trim', explode(',', ini_get('disable_functions'))), true);
        $checks[] = [
            'category' => 'Download Queue',
            'name'     => 'exec() function',
            'current'  => $execAvailable ? 'Available' : 'Disabled',
            'recommended' => 'Available',
            'status'   => $execAvailable ? 'pass' : 'warn',
            'fix'      => 'Remove exec from disable_functions in php.ini',
        ];

        $workerScript = APP_ROOT . '/bin/catalogue-worker.php';
        $checks[] = [
            'category' => 'Download Queue',
            'name'     => 'Worker script',
            'current'  => is_file($workerScript) ? 'Present' : 'Missing',
            'recommended' => 'Present',
            'status'   => is_file($workerScript) ? 'pass' : 'fail',
            'fix'      => '',
        ];

         
        $tmpDir = APP_ROOT . '/storage/tmp';
        $activeWorkers = 0;
        for ($w = 0; $w < 3; $w++) {
            $wPid = $tmpDir . '/catalogue-worker-' . $w . '.pid';
            if (file_exists($wPid)) {
                $pid = (int) @file_get_contents($wPid);
                if (Env::processAlive($pid)) {
                    $activeWorkers++;
                }
            }
        }
        $paused = Setting::get('catalogue_service_paused', '0') === '1';
        $pauseReason = Setting::get('catalogue_pause_reason', '');
        try {
            $pendingRow = Database::fetchOne("SELECT COUNT(*) as cnt FROM catalogue_downloads WHERE status IN ('pending','downloading')");
            $pendingCount = (int) ($pendingRow['cnt'] ?? 0);
        } catch (\Throwable $e) {
            $pendingCount = 0;
        }

        $workerStatus = $activeWorkers . ' running';
        if ($paused) {
            $workerStatus .= ' (paused' . ($pauseReason ? ': ' . $pauseReason : '') . ')';
        }
        $workerOk = $paused || $pendingCount === 0 || $activeWorkers > 0;
        $checks[] = [
            'category' => 'Download Queue',
            'name'     => 'Workers',
            'current'  => $workerStatus,
            'recommended' => 'Running when items pending',
            'status'   => $workerOk ? 'pass' : 'warn',
            'fix'      => $workerOk ? '' : 'Visit /admin/import/downloads and click Resume',
        ];

        $checks[] = [
            'category' => 'Download Queue',
            'name'     => 'Pending downloads',
            'current'  => (string) $pendingCount,
            'recommended' => 'Info',
            'status'   => 'pass',
            'fix'      => '',
        ];

         
         
        $phpCliBin = Env::phpCli();
        if ($phpCliBin === 'php') {
             
             
            $resolved = function_exists('exec') ? trim((string) @shell_exec('command -v php 2>/dev/null')) : '';
            $phpCliBin = ($resolved !== '' && @is_executable($resolved)) ? $resolved : '';
        }
        $checks[] = [
            'category' => 'System Tools',
            'name'     => 'PHP CLI',
            'current'  => $phpCliBin !== '' ? 'Found (' . $phpCliBin . ')' : 'Not found',
            'recommended' => 'Available',
            'status'   => $phpCliBin !== '' ? 'pass' : 'warn',
            'fix'      => $phpCliBin !== '' ? '' : 'apt install php-cli',
        ];

        $ffmpegPath = @trim((string) shell_exec('which ffmpeg 2>/dev/null'));
        $checks[] = [
            'category' => 'System Tools',
            'name'     => 'FFmpeg',
            'current'  => $ffmpegPath !== '' ? 'Installed (' . $ffmpegPath . ')' : 'Not found',
            'recommended' => 'Installed',
            'status'   => $ffmpegPath !== '' ? 'pass' : 'warn',
            'fix'      => $ffmpegPath !== '' ? '' : 'apt install ffmpeg',
        ];

        $ffprobePath = @trim((string) shell_exec('which ffprobe 2>/dev/null'));
        $checks[] = [
            'category' => 'System Tools',
            'name'     => 'FFprobe',
            'current'  => $ffprobePath !== '' ? 'Installed' : 'Not found',
            'recommended' => 'Installed',
            'status'   => $ffprobePath !== '' ? 'pass' : 'warn',
            'fix'      => $ffprobePath !== '' ? '' : 'apt install ffmpeg',
        ];

         
         
         
         
         
        $extMatrix = [
            ['pdo_mysql', 'MySQL database driver',                            true],
            ['mbstring',  'Unicode text handling',                            true],
            ['json',      'Core data encoding',                               true],
            ['curl',      'Catalogue downloads, AI rewriting, feed imports',  true],
            ['fileinfo',  'Upload MIME detection (graceful fallback exists)',  false],
            ['intl',      'Best-quality slug transliteration (fallback exists)', false],
            ['zip',       'Plugin / theme / core updates & backups',          false],
            ['zlib',      'Compressed (.gz) sitemaps',                        false],
            ['gd',        'Thumbnail generation & watermarks',                false],
            ['openssl',   'TLS for outbound HTTPS requests',                  false],
            ['ftp',       'FTP remote-storage backend',                       false],
        ];
        $phpv = PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION;
        foreach ($extMatrix as [$ext, $feature, $required]) {
            $loaded = extension_loaded($ext);
            if ($ext === 'gd' && !$loaded) { $loaded = extension_loaded('imagick'); }  
            $checks[] = [
                'category'    => 'PHP Extensions',
                'name'        => $ext,
                'current'     => $loaded ? 'Loaded' : 'Missing',
                'recommended' => $required ? 'Required' : 'Recommended',
                'status'      => $loaded ? 'pass' : ($required ? 'fail' : 'warn'),
                'fix'         => $loaded ? '' : ('Needed for: ' . $feature . '. Install: apt install php' . $phpv . '-' . $ext . ' && systemctl reload php' . $phpv . '-fpm'),
            ];
        }

         
        $uploadsPath = APP_ROOT . '/public/uploads';
        if (is_dir($uploadsPath)) {
            $freeBytes = disk_free_space($uploadsPath);
            $totalBytes = disk_total_space($uploadsPath);
            if ($freeBytes !== false && $totalBytes !== false) {
                $freeGb = round($freeBytes / 1073741824, 1);
                $totalGb = round($totalBytes / 1073741824, 1);
                $usedPct = $totalBytes > 0 ? round(($totalBytes - $freeBytes) / $totalBytes * 100) : 0;
                $checks[] = [
                    'category' => 'Disk Space',
                    'name'     => 'Free space (uploads)',
                    'current'  => $freeGb . ' GB free / ' . $totalGb . ' GB total (' . $usedPct . '% used)',
                    'recommended' => '>= 5 GB free',
                    'status'   => $freeGb >= 5 ? 'pass' : ($freeGb >= 1 ? 'warn' : 'fail'),
                    'fix'      => $freeGb < 5 ? 'Free up disk space or expand storage' : '',
                ];
            }
        }

         
        $serverSoftware = $_SERVER['SERVER_SOFTWARE'] ?? 'Unknown';
        $checks[] = [
            'category' => 'Web Server',
            'name'     => 'Server',
            'current'  => $serverSoftware,
            'recommended' => 'Apache or Nginx',
            'status'   => 'pass',
            'fix'      => '',
        ];

         
        $htaccess = APP_ROOT . '/public/.htaccess';
        if (stripos($serverSoftware, 'apache') !== false) {
            $checks[] = [
                'category' => 'Web Server',
                'name'     => '.htaccess',
                'current'  => is_file($htaccess) ? 'Present' : 'Missing',
                'recommended' => 'Present (Apache)',
                'status'   => is_file($htaccess) ? 'pass' : 'fail',
                'fix'      => is_file($htaccess) ? '' : 'Restore .htaccess in public/',
            ];
        }

         
        $sessionPath = session_save_path() ?: sys_get_temp_dir();
        $sessionWritable = is_writable($sessionPath);
        $checks[] = [
            'category' => 'PHP Config',
            'name'     => 'Session save path',
            'current'  => $sessionWritable ? 'Writable (' . basename($sessionPath) . ')' : 'Not writable',
            'recommended' => 'Writable',
            'status'   => $sessionWritable ? 'pass' : 'fail',
            'fix'      => $sessionWritable ? '' : 'chmod 1733 ' . $sessionPath,
        ];

         
        $tz = date_default_timezone_get();
        $checks[] = [
            'category' => 'PHP Config',
            'name'     => 'Timezone',
            'current'  => $tz,
            'recommended' => 'Set explicitly',
            'status'   => $tz !== 'UTC' || Setting::get('timezone', '') !== '' ? 'pass' : 'warn',
            'fix'      => 'Set timezone in Settings > General or date.timezone in php.ini',
        ];

         
        $lastCron = Setting::get('cron_last_run', '');
        $cronAge = $lastCron !== '' ? time() - strtotime($lastCron) : PHP_INT_MAX;
        $checks[] = [
            'category' => 'System',
            'name'     => 'Pseudo-cron (last run)',
            'current'  => $lastCron !== '' ? $lastCron . ' (' . round($cronAge / 60) . 'm ago)' : 'Never',
            'recommended' => 'Within last 30 minutes',
            'status'   => $cronAge < 1800 ? 'pass' : ($lastCron !== '' ? 'warn' : 'warn'),
            'fix'      => 'Cron runs automatically on page views. Ensure the site receives traffic.',
        ];

        if ($paused) {
            $checks[] = [
                'category' => 'Download Queue',
                'name'     => 'Service status',
                'current'  => 'Paused (' . ($pauseReason ?: 'unknown') . ')',
                'recommended' => 'Running',
                'status'   => $pauseReason === 'manual' ? 'pass' : 'warn',
                'fix'      => $pauseReason === 'network' ? 'Check server connectivity to tubepress.io' : '',
            ];
        }

         
         
         
        $writableTargets = [
            'storage'        => 'Runtime data: logs, cache, sessions, support attachments, backups',
            'storage/logs'   => 'Application & error logs',
            'storage/cache'  => 'Caches (sitemap, i18n, entity translations)',
            'public/uploads' => 'Uploaded media: thumbnails, performer/channel images, intros, watermark, album images',
        ];
        foreach ($writableTargets as $rel => $purpose) {
            $path = APP_ROOT . '/' . $rel;
            $exists = is_dir($path);
            $writable = $exists && is_writable($path);
            $checks[] = [
                'category'    => 'Filesystem',
                'name'        => $rel . '/ writable',
                'current'     => !$exists ? 'Missing' : ($writable ? 'Writable' : 'NOT writable'),
                'recommended' => 'Writable by the web server user',
                'status'      => $writable ? 'pass' : 'fail',
                'fix'         => $writable ? '' : ('Grant write access to your PHP/web user, e.g.  mkdir -p "' . $rel . '" && chmod -R u+rwX "' . $rel . '"  (or chown to the PHP-FPM/Apache user). Purpose: ' . $purpose),
            ];
        }
         
        $probeDir = APP_ROOT . '/storage';
        if (is_dir($probeDir)) {
            $probe = $probeDir . '/.health_write_test';
            $wrote = @file_put_contents($probe, 'ok') !== false;
            if ($wrote) { @unlink($probe); }
            $checks[] = [
                'category'    => 'Filesystem',
                'name'        => 'storage/ write probe',
                'current'     => $wrote ? 'OK (created & removed a temp file)' : 'FAILED to create a file',
                'recommended' => 'Able to create files',
                'status'      => $wrote ? 'pass' : 'fail',
                'fix'         => $wrote ? '' : 'PHP cannot create files in storage/ even though it may look writable (read-only mount, restrictive ACL, SELinux or open_basedir). Grant real write access to the web user.',
            ];
        }
         
        $cfg = APP_ROOT . '/config/app.php';
        $cfgOk = is_file($cfg) && is_readable($cfg);
        $checks[] = [
            'category'    => 'Filesystem',
            'name'        => 'config/app.php readable',
            'current'     => is_file($cfg) ? ($cfgOk ? 'Readable' : 'NOT readable') : 'Missing',
            'recommended' => 'Present & readable',
            'status'      => $cfgOk ? 'pass' : 'fail',
            'fix'         => $cfgOk ? '' : 'Ensure config/app.php exists and is readable by PHP (the installer creates it; do not delete it).',
        ];

        return $checks;
    }

    

    public static function syncInternalMenuLabels(array &$config): void
    {
        $internalKeys = ['home', 'categories', 'tags', 'pornstars', 'channels', 'albums', 'vtype_switcher',
                         'new_videos', 'top_rated', 'most_viewed', 'most_favorited'];
        $defaultLang = Translator::getDefaultCode();
        $availableLangs = array_column(Translator::available(), 'code');

         
        foreach ($config['items'] as &$item) {
            self::syncInternalItem($item, $internalKeys, $defaultLang, $availableLangs);
            if (!empty($item['children'])) {
                foreach ($item['children'] as &$child) {
                    self::syncInternalItem($child, $internalKeys, $defaultLang, $availableLangs);
                }
                unset($child);
            }
        }
        unset($item);
    }

    private static function syncInternalItem(array &$item, array $internalKeys, string $defaultLang, array $availableLangs): void
    {
        if (($item['type'] ?? '') !== 'internal') return;
        $key = $item['id'] ?? '';
        if (!in_array($key, $internalKeys, true)) return;

         
        $label = trim($item['label'] ?? '');
        if ($label !== '') {
            $current = __($key);
            if ($label !== $current) {
                self::upsertTranslationOverride($defaultLang, $key, $label);
            }
        }

         
        if (!empty($item['_translations'])) {
            foreach ($item['_translations'] as $langCode => $transLabel) {
                if ($langCode === $defaultLang) continue;
                if (!in_array($langCode, $availableLangs, true)) continue;
                $transLabel = trim($transLabel);
                if ($transLabel === '') continue;
                self::upsertTranslationOverride($langCode, $key, $transLabel);
            }
             
            unset($item['_translations']);
        }
    }

    private static function upsertTranslationOverride(string $langCode, string $key, string $value): void
    {
        $existing = Database::fetchOne(
            'SELECT id FROM translation_overrides WHERE lang_code = ? AND translation_key = ?',
            [$langCode, $key]
        );
        if ($existing) {
            Database::update('translation_overrides', ['value' => $value], 'id = ?', [(int)$existing['id']]);
        } else {
            Database::insert('translation_overrides', [
                'lang_code' => $langCode,
                'translation_key' => $key,
                'value' => $value,
            ]);
        }
    }

    

    public static function loadInternalMenuTranslations(): array
    {
        $keys = ['home', 'categories', 'tags', 'pornstars', 'channels', 'albums', 'vtype_switcher', 'new_videos', 'top_rated', 'most_viewed', 'most_favorited'];
        $result = [];
        foreach (Translator::available() as $lang) {
            $code = $lang['code'];
            $strings = Translator::getFileStrings($code);
            $overrides = Translator::getOverrides($code);
            foreach ($keys as $key) {
                $result[$key][$code] = $overrides[$key] ?? $strings[$key] ?? '';
            }
        }
        return $result;
    }

    private static function parseBytes(string $val): int
    {
        $val = trim($val);
        if ($val === '-1') return -1;
        $last = strtolower(substr($val, -1));
        $num = (int) $val;
        return match ($last) {
            'g' => $num * 1024 * 1024 * 1024,
            'm' => $num * 1024 * 1024,
            'k' => $num * 1024,
            default => $num,
        };
    }

    public static function transcodeJobsDetail(): void
    {
        Auth::requireSuperAdmin();
        $videoId = (int)($_GET['video_id'] ?? 0);
        if (!$videoId) Router::json(['error' => 'Missing video_id'], 400);

        $jobs = Database::fetchAll(
            "SELECT id, task, status, progress, stage, retry_count, last_worker, error_message, worker, started_at, completed_at, created_at FROM transcode_jobs WHERE video_id = ? ORDER BY id ASC",
            [$videoId]
        );
        $video = Database::fetchOne("SELECT title, source_height FROM videos WHERE id = ?", [$videoId]);

        Router::json([
            'video_id' => $videoId,
            'title' => $video['title'] ?? '#' . $videoId,
            'source_height' => (int)($video['source_height'] ?? 0),
            'jobs' => $jobs,
        ]);
    }

    public static function transcodeQueueData(): void
    {
        Auth::requireSuperAdmin();
        $filter = trim($_GET['filter'] ?? '');
        $page = max(1, (int)($_GET['page'] ?? 1));
        $queue = TranscodeManager::getVideoQueue($filter, $page, 50);
        $stats = TranscodeManager::getStats();
        Router::json(['queue' => $queue, 'stats' => $stats]);
    }
}
