<?php
require_once __DIR__.'/../app/bootstrap.php';
require_once __DIR__.'/_layout.php';

$user = require_login($pdo);
$GLOBALS['user'] = $user;
if (!is_superadmin($user)) {
    http_response_code(403);
    exit('Keine Berechtigung.');
}

if (function_exists('qrator_update_refresh_status')) { qrator_update_refresh_status(false, 1800); }


function kuhrator_normalize_version(string $version): string {
    $version = trim($version);
    $version = ltrim($version, "vV");
    return preg_replace('/[^0-9.\-]/', '', $version) ?: '0';
}

function kuhrator_current_version(): string {
    return defined('QRATOR_VERSION') ? (string)QRATOR_VERSION : '0';
}

function kuhrator_package_regex(): string {
    return '/^(?:qr-code-manager|qrator)-v[0-9]+(?:[.-][0-9]+)*\\.zip$/';
}

function kuhrator_expected_package_label(): string {
    return (defined('QRATOR_UPDATE_CHANNEL') && QRATOR_UPDATE_CHANNEL === 'selfhosting') ? 'qrator-vXX.zip' : 'qr-code-manager-vXX.zip';
}

function kuhrator_package_name_from_url(string $downloadUrl, string $latest): string {
    $path = parse_url($downloadUrl, PHP_URL_PATH);
    $name = basename((string)$path);
    if (preg_match(kuhrator_package_regex(), $name)) {
        return $name;
    }
    $safeVersion = $latest !== '0' ? str_replace('.', '-', $latest) : date('YmdHis');
    $prefix = (defined('QRATOR_UPDATE_CHANNEL') && QRATOR_UPDATE_CHANNEL === 'selfhosting') ? 'qrator' : 'qr-code-manager';
    return $prefix.'-v'.$safeVersion.'.zip';
}

function kuhrator_remote_fetch(string $url): array {
    $url = trim($url);
    if ($url === '' || !preg_match('#^https://#i', $url)) {
        return [false, 'Keine gültige HTTPS-Adresse für den Update-Check konfiguriert.', null];
    }
    $body = false;
    if (function_exists('curl_init')) {
        $ch = curl_init($url);
        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_FOLLOWLOCATION => false,
            CURLOPT_CONNECTTIMEOUT => 4,
            CURLOPT_TIMEOUT => 8,
            CURLOPT_USERAGENT => 'QRator/'.kuhrator_current_version(),
        ]);
        $body = curl_exec($ch);
        $status = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $err = curl_error($ch);
        curl_close($ch);
        if ($body === false || $status < 200 || $status >= 300) {
            return [false, 'Update-Check fehlgeschlagen'.($err ? ': '.$err : '. HTTP-Status '.$status).'. Geprüfte URL: '.$url, null];
        }
    } else {
        $context = stream_context_create(['http' => ['timeout' => 8, 'user_agent' => 'QRator/'.kuhrator_current_version()]]);
        $body = @file_get_contents($url, false, $context);
        if ($body === false) { return [false, 'Update-Check fehlgeschlagen. Der Server konnte das Update-Manifest nicht lesen: '.$url, null]; }
    }
    if (strlen((string)$body) > 64 * 1024) { return [false, 'Update-Manifest ist zu gross und wurde abgelehnt.', null]; }
    $data = json_decode((string)$body, true);
    if (!is_array($data)) { return [false, 'Update-Manifest ist kein gültiges JSON.', null]; }
    return [true, '', $data];
}

function kuhrator_remote_update_info(array $config): array {
    $url = defined('QRATOR_UPDATE_MANIFEST_URL') ? (string)QRATOR_UPDATE_MANIFEST_URL : (string)($config['update_manifest_url'] ?? 'https://qrator.web2use.ch/update.json');
    list($ok, $message, $data) = kuhrator_remote_fetch($url);
    if (!$ok || !is_array($data)) {
        return ['ok' => false, 'message' => $message, 'manifest_url' => $url];
    }
    $expectedChannel = defined('QRATOR_UPDATE_CHANNEL') ? (string)QRATOR_UPDATE_CHANNEL : '';
    $manifestChannel = (string)($data['channel'] ?? '');
    if ($expectedChannel !== '' && $manifestChannel !== '' && $manifestChannel !== $expectedChannel) {
        return ['ok' => false, 'message' => 'Update-Manifest gehört zum Kanal '.htmlspecialchars($manifestChannel, ENT_QUOTES, 'UTF-8').' und passt nicht zu dieser Installation.', 'manifest_url' => $url];
    }

    $latest = kuhrator_normalize_version((string)($data['latest_version'] ?? ''));
    $current = kuhrator_normalize_version(kuhrator_current_version());
    if ($latest === '0') {
        return ['ok' => false, 'message' => 'Update-Manifest enthält keine gültige latest_version.', 'manifest_url' => $url];
    }
    $download = trim((string)($data['download_url'] ?? ''));
    $changelog = trim((string)($data['changelog_url'] ?? ''));
    if ($download !== '' && !preg_match('#^https://#i', $download)) { $download = ''; }
    if ($changelog !== '' && !preg_match('#^https://#i', $changelog)) { $changelog = ''; }
    return [
        'ok' => true,
        'manifest_url' => $url,
        'current_version' => $current,
        'latest_version' => $latest,
        'channel' => $manifestChannel,
        'update_available' => version_compare($latest, $current, '>'),
        'download_url' => $download,
        'changelog_url' => $changelog,
        'sha256' => preg_match('/^[a-f0-9]{64}$/i', (string)($data['sha256'] ?? '')) ? strtolower((string)$data['sha256']) : '',
        'minimum_php' => (string)($data['minimum_php'] ?? ''),
        'release_date' => (string)($data['release_date'] ?? ''),
        'notes' => is_array($data['notes'] ?? null) ? array_values(array_filter(array_map('strval', $data['notes']))) : [],
    ];
}

function kuhrator_update_root(): string {
    $root = realpath(__DIR__.'/..');
    if (!$root) { throw new RuntimeException('Projektordner wurde nicht gefunden.'); }
    return $root;
}

function kuhrator_update_dir(): string {
    $dir = __DIR__.'/../update';
    if (!is_dir($dir)) { @mkdir($dir, 0755, true); }
    $real = realpath($dir);
    return $real ?: $dir;
}


function kuhrator_backup_dir(): string {
    $dir = kuhrator_update_dir().'/backups';
    if (!is_dir($dir)) { @mkdir($dir, 0755, true); }
    $real = realpath($dir);
    return $real ?: $dir;
}

function kuhrator_backup_skip(string $relative): bool {
    $relative = trim(str_replace('\\', '/', $relative), '/');
    if ($relative === '') { return true; }
    if ($relative === 'update' || strpos($relative, 'update/') === 0) { return true; }
    if ($relative === 'storage' || strpos($relative, 'storage/') === 0) { return true; }
    return false;
}

function kuhrator_backup_add_path(ZipArchive $zip, string $path, string $root): void {
    $relative = trim(str_replace('\\', '/', substr($path, strlen($root) + 1)), '/');
    if (kuhrator_backup_skip($relative)) { return; }
    if (is_dir($path)) {
        if ($relative !== '') { $zip->addEmptyDir($relative); }
        $items = scandir($path);
        if (!$items) { return; }
        foreach ($items as $item) {
            if ($item === '.' || $item === '..') { continue; }
            kuhrator_backup_add_path($zip, $path.'/'.$item, $root);
        }
        return;
    }
    if (is_file($path)) { $zip->addFile($path, $relative); }
}

function kuhrator_create_backup(string $targetVersion): string {
    if (!class_exists('ZipArchive')) {
        throw new RuntimeException('Backup nicht möglich: ZipArchive ist nicht aktiv.');
    }
    $root = kuhrator_update_root();
    $backupDir = kuhrator_backup_dir();
    if (!is_writable($backupDir)) {
        throw new RuntimeException('Backup-Ordner ist nicht beschreibbar: /update/backups');
    }
    $safeVersion = preg_replace('/[^0-9A-Za-z._-]/', '-', $targetVersion) ?: 'update';
    $file = $backupDir.'/qrator-backup-before-'.$safeVersion.'-'.date('Ymd-His').'.zip';
    $zip = new ZipArchive();
    if ($zip->open($file, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) {
        throw new RuntimeException('Backup-ZIP konnte nicht erstellt werden.');
    }
    $items = scandir($root);
    if (!$items) { $zip->close(); throw new RuntimeException('Projektordner konnte für Backup nicht gelesen werden.'); }
    foreach ($items as $item) {
        if ($item === '.' || $item === '..') { continue; }
        kuhrator_backup_add_path($zip, $root.'/'.$item, $root);
    }
    $zip->close();
    @chmod($file, 0644);
    kuhrator_log('Backup erstellt: '.basename($file));
    return $file;
}

function kuhrator_log(string $message): void {
    @file_put_contents(kuhrator_update_dir().'/last-update.log', '['.date('Y-m-d H:i:s').'] '.$message.PHP_EOL, FILE_APPEND);
}

function kuhrator_package_version(string $name): string {
    if (preg_match('/^(?:qr-code-manager|qrator)-v([0-9]+(?:[.-][0-9]+)*)\.zip$/', $name, $m)) {
        return 'v'.str_replace('-', '.', $m[1]);
    }
    return 'neue Version';
}

function kuhrator_clean_changelog_html(string $html): string {
    $html = preg_replace('/<\\?php.*?\\?>/s', '', $html);
    $html = preg_replace('/<\\?=.*?\\?>/s', '', $html);
    $html = preg_replace('/<script\\b[^>]*>.*?<\\/script>/is', '', $html);
    $html = preg_replace('/<style\\b[^>]*>.*?<\\/style>/is', '', $html);
    $allowed = '<h2><h3><ul><ol><li><p><code><strong><em><br>';
    return trim(strip_tags($html, $allowed));
}

function kuhrator_package_changelog(string $zipPath): string {
    if (!class_exists('ZipArchive') || !is_file($zipPath)) { return ''; }
    $zip = new ZipArchive();
    if ($zip->open($zipPath) !== true) { return ''; }
    $content = '';
    for ($i = 0; $i < $zip->numFiles; $i++) {
        $name = trim(str_replace('\\\\', '/', (string)$zip->getNameIndex($i)), '/');
        if ($name === 'admin/changelog.php' || preg_match('#^[^/]+/admin/changelog\\.php$#', $name)) {
            $content = (string)$zip->getFromIndex($i);
            break;
        }
    }
    $zip->close();
    if ($content === '') { return ''; }
    if (preg_match('/<h2[^>]*>\\s*Version\\s+[^<]+<\\/h2>\\s*(?:<ul[\\s\\S]*?<\\/ul>|<p[\\s\\S]*?<\\/p>)/i', $content, $m)) {
        return kuhrator_clean_changelog_html($m[0]);
    }
    return '';
}

function kuhrator_latest_package_changelog(array $packages): string {
    if (!$packages || empty($packages[0]['path'])) { return ''; }
    return kuhrator_package_changelog((string)$packages[0]['path']);
}


function kuhrator_update_packages(): array {
    $dir = kuhrator_update_dir();
    $files = array_merge(glob($dir.'/qr-code-manager-v*.zip') ?: [], glob($dir.'/qrator-v*.zip') ?: []);
    $packages = [];
    foreach ($files as $file) {
        $name = basename($file);
        if (!preg_match('/^(?:qr-code-manager|qrator)-v([0-9]+(?:[.-][0-9]+)*)\.zip$/', $name, $m)) { continue; }
        $packages[] = [
            'name' => $name,
            'path' => $file,
            'version' => str_replace('-', '.', $m[1]),
            'size' => @filesize($file) ?: 0,
            'mtime' => @filemtime($file) ?: time(),
        ];
    }
    usort($packages, function($a, $b) { return version_compare($b['version'], $a['version']); });
    return $packages;
}

function kuhrator_zip_has_project_structure(ZipArchive $zip): bool {
    $hasAdmin = false;
    $hasApp = false;
    for ($i = 0; $i < $zip->numFiles; $i++) {
        $name = trim(str_replace('\\\\', '/', (string)$zip->getNameIndex($i)), '/');
        if ($name === '') { continue; }
        if (strpos($name, 'admin/') === 0) { $hasAdmin = true; }
        if (strpos($name, 'app/') === 0) { $hasApp = true; }
        $parts = explode('/', $name);
        if (count($parts) > 2) {
            if ($parts[1] === 'admin') { $hasAdmin = true; }
            if ($parts[1] === 'app') { $hasApp = true; }
        }
    }
    return $hasAdmin && $hasApp;
}

function kuhrator_upload_update_package(): array {
    if (empty($_FILES['update_zip']) || !is_array($_FILES['update_zip'])) {
        return [false, 'Keine Datei ausgewählt.'];
    }
    $file = $_FILES['update_zip'];
    if (($file['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) {
        return [false, 'Upload fehlgeschlagen. Bitte Dateigrösse und Server-Limits prüfen.'];
    }
    $name = basename((string)($file['name'] ?? ''));
    if (!preg_match('/^(?:qr-code-manager|qrator)-v[0-9]+(?:[.-][0-9]+)*\\.zip$/', $name)) {
        return [false, 'Ungültiger Dateiname. Erlaubt ist für diesen Kanal: '.kuhrator_expected_package_label().'.'];
    }
    if (($file['size'] ?? 0) < 1000 || ($file['size'] ?? 0) > 50 * 1024 * 1024) {
        return [false, 'Die Update-Datei hat eine ungültige Grösse.'];
    }
    $tmpName = (string)($file['tmp_name'] ?? '');
    if (!is_uploaded_file($tmpName)) {
        return [false, 'Die Datei wurde nicht als gültiger Upload erkannt.'];
    }
    if (!class_exists('ZipArchive')) {
        return [false, 'Die PHP-Erweiterung ZipArchive ist nicht aktiv.'];
    }
    $zip = new ZipArchive();
    if ($zip->open($tmpName) !== true) {
        return [false, 'Die hochgeladene Datei ist kein gültiges ZIP-Archiv.'];
    }
    for ($i = 0; $i < $zip->numFiles; $i++) {
        $entry = (string)$zip->getNameIndex($i);
        if (!kuhrator_is_safe_zip_path($entry)) {
            $zip->close();
            return [false, 'Das ZIP enthält unsichere Pfade und wurde abgelehnt.'];
        }
    }
    if (!kuhrator_zip_has_project_structure($zip)) {
        $zip->close();
        return [false, 'Das ZIP hat keine gültige KuhRator-Struktur. Erwartet werden /admin und /app.'];
    }
    $zip->close();
    $updateDir = kuhrator_update_dir();
    if (!is_writable($updateDir)) {
        return [false, 'Der Ordner /update ist nicht beschreibbar.'];
    }
    $target = $updateDir.'/'.$name;
    if (file_exists($target)) {
        return [false, 'Diese Update-Datei liegt bereits im Ordner /update.'];
    }
    if (!move_uploaded_file($tmpName, $target)) {
        return [false, 'Die Update-Datei konnte nicht nach /update verschoben werden.'];
    }
    @chmod($target, 0644);
    kuhrator_log('Update-Datei hochgeladen: '.$name);
    return [true, 'Update-Datei wurde hochgeladen und ist bereit zur Installation.'];
}

function kuhrator_delete_update_package(string $zipName): array {
    if (!preg_match('/^(?:qr-code-manager|qrator)-v[0-9]+(?:[.-][0-9]+)*\.zip$/', $zipName)) {
        return [false, 'Ungültiger Dateiname.'];
    }
    $updateDir = kuhrator_update_dir();
    $path = realpath($updateDir.'/'.$zipName);
    if (!$path || !is_file($path)) {
        return [false, 'Update-Datei wurde nicht gefunden.'];
    }
    if (strpos($path, $updateDir) !== 0) {
        return [false, 'Update-Datei liegt nicht im erlaubten Ordner.'];
    }
    if (!is_writable($path)) {
        return [false, 'Update-Datei ist nicht beschreibbar und konnte nicht gelöscht werden.'];
    }
    if (!@unlink($path)) {
        $err = error_get_last();
        return [false, 'Update-Datei konnte nicht gelöscht werden.'.(!empty($err['message']) ? ' '.$err['message'] : '')];
    }
    kuhrator_log('Update-Datei gelöscht: '.$zipName);
    return [true, 'Update-Datei wurde gelöscht.'];
}

function kuhrator_download_remote_update(array $remoteUpdate): array {
    $downloadUrl = trim((string)($remoteUpdate['download_url'] ?? ''));
    $expectedHash = strtolower(trim((string)($remoteUpdate['sha256'] ?? '')));
    $latest = kuhrator_normalize_version((string)($remoteUpdate['latest_version'] ?? ''));
    if ($downloadUrl === '' || !preg_match('#^https://#i', $downloadUrl)) {
        return [false, 'Keine gültige HTTPS-Download-Adresse im Update-Manifest gefunden.'];
    }
    if (!preg_match('/^[a-f0-9]{64}$/', $expectedHash)) {
        return [false, 'Update blockiert: Im Manifest fehlt ein gültiger SHA-256-Hash.'];
    }
    $name = kuhrator_package_name_from_url($downloadUrl, $latest);
    $updateDir = kuhrator_update_dir();
    if (!is_writable($updateDir)) {
        return [false, 'Der Ordner /update ist nicht beschreibbar.'];
    }
    $target = $updateDir.'/'.$name;
    if (file_exists($target)) {
        $existingHash = strtolower((string)hash_file('sha256', $target));
        if (hash_equals($expectedHash, $existingHash)) {
            return [true, 'Update-Datei liegt bereits geprüft im Ordner /update.', $name];
        }
        return [false, 'Eine vorhandene Update-Datei mit gleichem Namen hat einen abweichenden SHA-256-Hash. Bitte im lokalen Update-Tab löschen und erneut versuchen.'];
    }
    $tmp = $updateDir.'/_download_'.bin2hex(random_bytes(8)).'.zip';
    try {
        $in = false;
        if (function_exists('curl_init')) {
            $fp = fopen($tmp, 'wb');
            if (!$fp) { throw new RuntimeException('Temporäre Download-Datei konnte nicht erstellt werden.'); }
            $ch = curl_init($downloadUrl);
            curl_setopt_array($ch, [
                CURLOPT_FILE => $fp,
                CURLOPT_FOLLOWLOCATION => false,
                CURLOPT_CONNECTTIMEOUT => 8,
                CURLOPT_TIMEOUT => 60,
                CURLOPT_USERAGENT => 'QRator/'.kuhrator_current_version(),
            ]);
            $ok = curl_exec($ch);
            $status = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
            $err = curl_error($ch);
            curl_close($ch);
            fclose($fp);
            if ($ok === false || $status < 200 || $status >= 300) {
                throw new RuntimeException('Download fehlgeschlagen'.($err ? ': '.$err : '. HTTP-Status '.$status));
            }
        } else {
            $context = stream_context_create(['http' => ['timeout' => 60, 'user_agent' => 'QRator/'.kuhrator_current_version()]]);
            $in = @fopen($downloadUrl, 'rb', false, $context);
            if (!$in) { throw new RuntimeException('Download fehlgeschlagen. Der Server konnte die ZIP-Datei nicht lesen.'); }
            $out = fopen($tmp, 'wb');
            if (!$out) { fclose($in); throw new RuntimeException('Temporäre Download-Datei konnte nicht erstellt werden.'); }
            stream_copy_to_stream($in, $out, 50 * 1024 * 1024 + 1);
            fclose($in);
            fclose($out);
        }
        $size = @filesize($tmp) ?: 0;
        if ($size < 1000 || $size > 50 * 1024 * 1024) {
            throw new RuntimeException('Die heruntergeladene Update-Datei hat eine ungültige Grösse.');
        }
        $actualHash = strtolower((string)hash_file('sha256', $tmp));
        if (!hash_equals($expectedHash, $actualHash)) {
            throw new RuntimeException('Update blockiert: SHA-256 stimmt nicht. Erwartet: '.$expectedHash.' / erhalten: '.$actualHash);
        }
        if (!class_exists('ZipArchive')) {
            throw new RuntimeException('Die PHP-Erweiterung ZipArchive ist nicht aktiv.');
        }
        $zip = new ZipArchive();
        if ($zip->open($tmp) !== true) {
            throw new RuntimeException('Die heruntergeladene Datei ist kein gültiges ZIP-Archiv.');
        }
        for ($i = 0; $i < $zip->numFiles; $i++) {
            $entry = (string)$zip->getNameIndex($i);
            if (!kuhrator_is_safe_zip_path($entry)) {
                $zip->close();
                throw new RuntimeException('Das ZIP enthält unsichere Pfade und wurde abgelehnt.');
            }
        }
        if (!kuhrator_zip_has_project_structure($zip)) {
            $zip->close();
            throw new RuntimeException('Das ZIP hat keine gültige QRator-Struktur. Erwartet werden /admin und /app.');
        }
        $zip->close();
        if (!@rename($tmp, $target)) {
            throw new RuntimeException('Die geprüfte Update-Datei konnte nicht nach /update verschoben werden.');
        }
        @chmod($target, 0644);
        kuhrator_log('Remote-Update heruntergeladen und per SHA-256 geprüft: '.$name);
        return [true, 'Update-Datei wurde heruntergeladen und per SHA-256 geprüft.', $name];
    } catch (Throwable $e) {
        if (is_file($tmp)) { @unlink($tmp); }
        kuhrator_log('Remote-Download fehlgeschlagen: '.$e->getMessage());
        return [false, $e->getMessage()];
    }
}

function kuhrator_delete_dir(string $dir): void {
    if (!is_dir($dir)) { return; }
    $items = scandir($dir);
    if (!$items) { return; }
    foreach ($items as $item) {
        if ($item === '.' || $item === '..') { continue; }
        $path = $dir.'/'.$item;
        if (is_dir($path)) { kuhrator_delete_dir($path); }
        else { @unlink($path); }
    }
    @rmdir($dir);
}

function kuhrator_is_safe_zip_path(string $path): bool {
    $path = str_replace('\\', '/', $path);
    if ($path === '' || $path[0] === '/') { return false; }
    if (strpos($path, '../') !== false || strpos($path, '/..') !== false || $path === '..') { return false; }
    return true;
}

function kuhrator_should_skip(string $relative): bool {
    $relative = trim(str_replace('\\', '/', $relative), '/');
    if ($relative === '') { return true; }
    if ($relative === 'app/config.php') { return true; }
    if ($relative === 'update' || strpos($relative, 'update/') === 0) { return true; }
    if ($relative === 'storage' || strpos($relative, 'storage/') === 0) { return true; }
    return false;
}

function kuhrator_copy_tree(string $source, string $target, string $base): void {
    $items = scandir($source);
    if (!$items) { throw new RuntimeException('Update-Ordner konnte nicht gelesen werden: '.$source); }
    foreach ($items as $item) {
        if ($item === '.' || $item === '..') { continue; }
        $src = $source.'/'.$item;
        $relative = str_replace('\\', '/', substr($src, strlen($base) + 1));
        if (kuhrator_should_skip($relative)) { continue; }
        $dst = $target.'/'.$relative;
        if (is_dir($src)) {
            if (!is_dir($dst) && !@mkdir($dst, 0755, true)) {
                throw new RuntimeException('Ordner konnte nicht erstellt werden: '.$relative);
            }
            kuhrator_copy_tree($src, $target, $base);
        } else {
            $dstDir = dirname($dst);
            if (!is_dir($dstDir) && !@mkdir($dstDir, 0755, true)) {
                throw new RuntimeException('Zielordner konnte nicht erstellt werden: '.$dstDir);
            }
            if (!is_writable($dstDir)) {
                throw new RuntimeException('Zielordner ist nicht beschreibbar: '.str_replace(kuhrator_update_root().'/', '', $dstDir));
            }
            if (file_exists($dst) && !is_writable($dst)) {
                throw new RuntimeException('Datei ist nicht beschreibbar: '.$relative);
            }
            if (!@copy($src, $dst)) {
                $err = error_get_last();
                throw new RuntimeException('Datei konnte nicht kopiert werden: '.$relative.(!empty($err['message']) ? ' ('.$err['message'].')' : ''));
            }
            @chmod($dst, 0644);
        }
    }
}

function kuhrator_find_source_root(string $tmp): string {
    if (is_dir($tmp.'/admin') && is_dir($tmp.'/app')) { return $tmp; }
    $children = array_values(array_filter(glob($tmp.'/*') ?: [], 'is_dir'));
    if (count($children) === 1 && is_dir($children[0].'/admin') && is_dir($children[0].'/app')) { return $children[0]; }
    throw new RuntimeException('Update-ZIP hat keine gültige KuhRator-Struktur. Erwartet werden die Ordner /admin und /app.');
}

function kuhrator_restore_backup(string $backupFile, string $root): bool {
    if ($backupFile === '' || !is_file($backupFile) || !class_exists('ZipArchive')) { return false; }
    $zip = new ZipArchive();
    if ($zip->open($backupFile) !== true) { return false; }
    for ($i = 0; $i < $zip->numFiles; $i++) {
        $entry = (string)$zip->getNameIndex($i);
        if (!kuhrator_is_safe_zip_path($entry)) { $zip->close(); return false; }
    }
    $ok = $zip->extractTo($root);
    $zip->close();
    if ($ok) { kuhrator_log('Rollback aus Backup ausgeführt: '.basename($backupFile)); }
    return (bool)$ok;
}

function kuhrator_run_update(PDO $pdo, string $zipName): array {
    $tmp = null;
    $backupFile = '';
    $backupCreated = false;
    try {
        kuhrator_log('Update gestartet: '.$zipName);
        if (!class_exists('ZipArchive')) {
            throw new RuntimeException('Die PHP-Erweiterung ZipArchive ist nicht aktiv. Bitte im Hosting/cPanel aktivieren.');
        }
        if (!preg_match('/^(?:qr-code-manager|qrator)-v[0-9]+(?:[.-][0-9]+)*\.zip$/', $zipName)) {
            throw new RuntimeException('Ungültiger Dateiname. Erlaubt ist für diesen Kanal: '.kuhrator_expected_package_label().'.');
        }
        $root = kuhrator_update_root();
        $updateDir = kuhrator_update_dir();
        if (!is_writable($updateDir)) {
            throw new RuntimeException('Der Ordner /update ist nicht beschreibbar.');
        }
        $zipPath = realpath($updateDir.'/'.$zipName);
        if (!$zipPath || !is_file($zipPath)) {
            throw new RuntimeException('Update-Datei wurde nicht gefunden.');
        }
        if (strpos($zipPath, $updateDir) !== 0) {
            throw new RuntimeException('Update-Datei liegt nicht im erlaubten Ordner.');
        }

        $zip = new ZipArchive();
        if ($zip->open($zipPath) !== true) {
            throw new RuntimeException('Update-ZIP konnte nicht geöffnet werden.');
        }
        for ($i = 0; $i < $zip->numFiles; $i++) {
            $name = $zip->getNameIndex($i);
            if (!kuhrator_is_safe_zip_path($name)) {
                $zip->close();
                throw new RuntimeException('Update-ZIP enthält unsichere Pfade.');
            }
        }

        $tmp = $updateDir.'/_tmp_'.bin2hex(random_bytes(8));
        if (!@mkdir($tmp, 0755, true)) {
            $zip->close();
            throw new RuntimeException('Temporärer Update-Ordner konnte nicht erstellt werden.');
        }
        if (!$zip->extractTo($tmp)) {
            $zip->close();
            throw new RuntimeException('Update-ZIP konnte nicht entpackt werden.');
        }
        $zip->close();

        $source = kuhrator_find_source_root($tmp);
        $backupFile = kuhrator_create_backup(kuhrator_package_version($zipName));
        $backupCreated = true;
        kuhrator_copy_tree($source, $root, $source);

        $migrations = $root.'/app/migrations.php';
        if (is_file($migrations)) { require_once $migrations; }
        if (function_exists('run_migrations')) { run_migrations($pdo); }

        kuhrator_delete_dir($tmp);
        $tmp = null;

        $installedName = $updateDir.'/installed-'.$zipName;
        if (!@rename($zipPath, $installedName)) { @unlink($zipPath); }
        kuhrator_log('Update erfolgreich: '.$zipName);
        if (function_exists('qrator_update_clear_status')) { qrator_update_clear_status(); }
        return [true, 'Die neue Version wurde erfolgreich installiert.'];
    } catch (Throwable $e) {
        if ($tmp && is_dir($tmp)) { kuhrator_delete_dir($tmp); }
        $rollback = '';
        if ($backupCreated && $backupFile !== '') {
            $rollback = kuhrator_restore_backup($backupFile, kuhrator_update_root()) ? ' Rollback wurde aus dem Datei-Backup ausgeführt.' : ' Rollback konnte nicht automatisch ausgeführt werden; Backup liegt unter /update/backups.';
        }
        kuhrator_log('Update fehlgeschlagen: '.$e->getMessage().$rollback);
        return [false, 'Update fehlgeschlagen: '.$e->getMessage().$rollback];
    }
}

function kuhrator_render_success(string $version, string $message): void {
    page_header('Update erfolgreich'); ?>
    <meta http-equiv="refresh" content="5;url=dashboard.php">
    <section class="card update-success-card update-success-centered">
        <div class="update-success-icon">✅</div>
        <h2>Update erfolgreich</h2>
        <p class="muted"><?= e($message) ?></p>
        <p class="notice ok">Aktuelle Version: <strong><?= e($version) ?></strong></p>
        <p class="muted">Du wirst in 5 Sekunden automatisch zum Dashboard weitergeleitet.</p>
        <a href="dashboard.php" class="btn update-success-button">Zurück zum Dashboard</a>
    </section>
    <?php page_footer();
}

$error = null;
$notice = null;
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    csrf_check();
    $action = (string)($_POST['action'] ?? 'install');
    if ($action === 'upload') {
        list($ok, $message) = kuhrator_upload_update_package();
        if ($ok) { $notice = $message; } else { $error = $message; }
    } elseif ($action === 'remote-install') {
        $remoteForDownload = kuhrator_remote_update_info($config);
        if (empty($remoteForDownload['ok']) || empty($remoteForDownload['update_available'])) {
            $error = 'Es ist kein gültiges Remote-Update verfügbar.';
        } else {
            $downloadResult = kuhrator_download_remote_update($remoteForDownload);
            $ok = (bool)($downloadResult[0] ?? false);
            $message = (string)($downloadResult[1] ?? '');
            $packageName = (string)($downloadResult[2] ?? '');
            if ($ok && $packageName !== '') {
                list($installOk, $installMessage) = kuhrator_run_update($pdo, $packageName);
                if ($installOk) {
                    kuhrator_render_success('v'.kuhrator_normalize_version((string)$remoteForDownload['latest_version']), $installMessage);
                    exit;
                }
                $error = $installMessage;
            } else {
                $error = $message ?: 'Remote-Update konnte nicht installiert werden.';
            }
        }
    } elseif ($action === 'delete') {
        $selectedPackage = (string)($_POST['package'] ?? '');
        list($ok, $message) = kuhrator_delete_update_package($selectedPackage);
        if ($ok) { $notice = $message; } else { $error = $message; }
    } else {
        $selectedPackage = (string)($_POST['package'] ?? '');
        list($ok, $message) = kuhrator_run_update($pdo, $selectedPackage);
        if ($ok) {
            kuhrator_render_success(kuhrator_package_version($selectedPackage), $message);
            exit;
        }
        $error = $message;
    }
}

$packages = kuhrator_update_packages();
$remoteUpdate = kuhrator_remote_update_info($config);
page_header('Updates');
?>
<div class="update-tabs" data-update-tabs>
  <input class="update-tab-input" type="radio" name="update_tab" id="update-tab-online" checked>
  <input class="update-tab-input" type="radio" name="update_tab" id="update-tab-local">
  <div class="update-tab-nav" role="tablist" aria-label="Update-Bereich">
    <label class="update-tab-button" for="update-tab-online" role="tab">Online-Update</label>
    <label class="update-tab-button" for="update-tab-local" role="tab">Lokales Update</label>
  </div>

  <section class="update-tab-panel update-tab-panel-online" role="tabpanel" aria-labelledby="update-tab-online">
    <section class="card remote-update-card">
      <div class="remote-update-grid">
        <div>
          <p class="eyebrow">Online-Update-Check</p>
          <h2>Aktuelle Version: <?= e(defined('QRATOR_VERSION_LABEL') ? QRATOR_VERSION_LABEL : 'v?') ?></h2>
          <?php if (!empty($remoteUpdate['ok'])): ?>
            <?php if (!empty($remoteUpdate['update_available'])): ?>
              <p class="notice ok">Neue Version verfügbar: <strong>v<?= e($remoteUpdate['latest_version']) ?></strong></p>
            <?php else: ?>
              <p class="notice">Du verwendest die aktuellste veröffentlichte Version.</p>
            <?php endif; ?>
            <div class="remote-update-meta">
              <span>Manifest: <code><?= e($remoteUpdate['manifest_url']) ?></code></span>
              <?php if (!empty($remoteUpdate['release_date'])): ?><span>Release: <?= e($remoteUpdate['release_date']) ?></span><?php endif; ?>
              <?php if (!empty($remoteUpdate['minimum_php'])): ?><span>PHP ≥ <?= e($remoteUpdate['minimum_php']) ?></span><?php endif; ?>
              <?php if (!empty($remoteUpdate['sha256'])): ?><span>SHA-256 hinterlegt</span><?php endif; ?>
            </div>
          <?php else: ?>
            <p class="notice error">Update-Check nicht möglich: <?= e($remoteUpdate['message'] ?? 'Unbekannter Fehler') ?></p>
          <?php endif; ?>
        </div>
        <div class="remote-update-actions">
          <?php if (!empty($remoteUpdate['download_url']) && !empty($remoteUpdate['update_available'])): ?><form method="post" class="inline-form"><input type="hidden" name="csrf" value="<?= e(csrf_token()) ?>"><input type="hidden" name="action" value="remote-install"><button class="btn" type="submit">Update installieren</button></form><?php elseif (!empty($remoteUpdate['download_url'])): ?><a class="btn secondary" href="<?= e($remoteUpdate['download_url']) ?>" target="_blank" rel="noopener noreferrer">Update-Datei öffnen</a><?php endif; ?>
          <?php if (!empty($remoteUpdate['changelog_url'])): ?><a class="btn secondary" href="<?= e($remoteUpdate['changelog_url']) ?>" target="_blank" rel="noopener noreferrer">Changelog öffnen</a><?php endif; ?>
        </div>
      </div>
      <?php if (!empty($remoteUpdate['notes'])): ?><ul class="update-notes"><?php foreach ($remoteUpdate['notes'] as $note): ?><li><?= e($note) ?></li><?php endforeach; ?></ul><?php endif; ?>
      <?php if ($notice): ?><p class="notice ok"><?= e($notice) ?></p><?php endif; ?>
      <?php if ($error): ?><p class="notice error"><?= e($error) ?></p><?php endif; ?>
    </section>
  </section>

  <section class="update-tab-panel update-tab-panel-local" role="tabpanel" aria-labelledby="update-tab-local">
    <section class="card">
      <h2>Lokales Update-Paket installieren</h2>
      <p class="muted">Lade ein QRator-ZIP manuell hoch oder installiere ein Paket, das bereits im Ordner <code>/update</code> liegt.</p>
      <?php $latestChangelog = kuhrator_latest_package_changelog($packages); ?>
      <?php if ($latestChangelog): ?>
        <div class="update-changelog-preview">
          <p class="eyebrow">Änderungen im neuesten Update</p>
          <?= $latestChangelog ?>
        </div>
      <?php endif; ?>

      <form method="post" enctype="multipart/form-data" class="update-upload-card">
        <input type="hidden" name="csrf" value="<?= e(csrf_token()) ?>">
        <input type="hidden" name="action" value="upload">
        <div class="update-upload-copy">
          <h3>Update-Paket hochladen</h3>
          <p class="muted">Erlaubt sind geprüfte ZIP-Dateien im Format <code><?= e(kuhrator_expected_package_label()) ?></code>. Für ältere manuelle Pakete werden beide Namensmuster weiterhin erkannt.</p>
        </div>
        <label class="upload-dropzone">
          <span class="upload-icon">⬆</span>
          <span><strong>Datei auswählen</strong><small>ZIP-Datei bis 50 MB</small></span>
          <input type="file" name="update_zip" accept=".zip" required>
        </label>
        <button class="btn" type="submit">Update-Datei hochladen</button>
      </form>
      <?php if (!$packages): ?>
        <p class="notice">Aktuell liegt kein Update-Paket im Ordner <code>/update</code>.</p>
      <?php else: ?>
        <div class="table-wrap"><table><thead><tr><th>Version</th><th>Datei</th><th>Grösse</th><th>Datum</th><th></th></tr></thead><tbody>
        <?php foreach ($packages as $p): ?><tr>
          <td><strong>v<?= e($p['version']) ?></strong></td>
          <td><code><?= e($p['name']) ?></code></td>
          <td><?= number_format($p['size']/1024, 1, '.', "'") ?> KB</td>
          <td><?= e(date('d.m.Y H:i', $p['mtime'])) ?></td>
          <td class="update-actions"><form method="post" data-confirm='Update wirklich installieren? Bitte stelle sicher, dass vorher ein Datenbank-Backup erstellt wurde.'><input type="hidden" name="csrf" value="<?= e(csrf_token()) ?>"><input type="hidden" name="action" value="install"><input type="hidden" name="package" value="<?= e($p['name']) ?>"><button class="btn" type="submit">Update installieren</button></form><form method="post" data-confirm='Update-Datei wirklich löschen?'><input type="hidden" name="csrf" value="<?= e(csrf_token()) ?>"><input type="hidden" name="action" value="delete"><input type="hidden" name="package" value="<?= e($p['name']) ?>"><button class="btn secondary" type="submit">Löschen</button></form></td>
        </tr><?php endforeach; ?>
        </tbody></table></div>
      <?php endif; ?>
    </section>
  </section>
</div>
<section class="card subtle-card">
  <h2>Wichtig</h2>
  <ul>
    <li>Vor jedem Update bitte die Datenbank via phpMyAdmin sichern.</li>
    <li>Beim Online- und lokalen Update wird automatisch ein Datei-Backup unter <code>/update/backups</code> erstellt.</li>
    <li><code>app/config.php</code>, <code>/storage</code> und <code>/update</code> werden beim Update nicht überschrieben.</li>
    <li>Nach einem Fehler findest du Details in <code>/update/last-update.log</code>.</li>
    <li>Nach erfolgreicher Installation wird die ZIP-Datei automatisch entfernt oder in <code>installed-...</code> umbenannt.</li>
  </ul>
</section>
<?php page_footer(); ?>
