<?php
/**
 * Update-Funktionen: Versionen laden, aktuelle Version, Update ausführen.
 * Erwartet APP_ROOT oder Install-Pfad für die Update-Installation.
 */

if (!defined('FUNCTIONS_ROOT')) {
    return;
}

/** Maximal so viele Backup-Ordner (backup_*) behalten – ältere werden beim Update gelöscht */
if (!defined('WECKER_UPDATE_MAX_BACKUPS')) {
    define('WECKER_UPDATE_MAX_BACKUPS', 3);
}

/** Update-Server-URL (in config.php optional definierbar) */
function function_update_server_url(): string {
    return defined('UPDATE_SERVER') ? (string) UPDATE_SERVER : 'https://update.piclock.cylone-network.com';
}

/** HTTP-Context mit Timeout für file_get_contents (gibt stream-context resource zurück) */
function function_update_http_context(int $timeout = 30) {
    return stream_context_create([
        'http' => ['timeout' => $timeout],
        'ssl'  => ['verify_peer' => true]
    ]);
}

/**
 * Letzte N Versionen vom Update-Server holen (sortiert, neueste zuerst).
 * Server: https://update.piclock.cylone-network.com/releases/versions.json
 * @return array<int, array{version?: string, file?: string, date?: string, time?: string, commit?: string}>
 */
function function_update_versionen_letzte_n(int $n = 5): array {
    $server = function_update_server_url();
    $url = rtrim($server, '/') . '/releases/versions.json';
    $json = @file_get_contents($url, false, function_update_http_context(15));
    if ($json === false || $json === '') {
        return [];
    }
    $versions = json_decode($json, true);
    if (!is_array($versions)) {
        return [];
    }
    usort($versions, function ($a, $b) {
        $va = $a['version'] ?? '';
        $vb = $b['version'] ?? '';
        if ($va === $vb) return 0;
        if (preg_match('/^\d+\.\d+/', $va) && preg_match('/^\d+\.\d+/', $vb)) {
            return version_compare($vb, $va);
        }
        $da = $a['time'] ?? $a['date'] ?? '';
        $db = $b['time'] ?? $b['date'] ?? '';
        if ($da !== '' && $db !== '') {
            return strcmp($db, $da);
        }
        return strcmp($vb, $va);
    });
    return array_slice($versions, 0, $n);
}

/**
 * Aktuelle installierte Version anzeigen.
 * Liest version.txt im Wecker-Root (wird beim Update geschrieben), sonst WECKER_VERSION aus config, sonst 0.0.0.
 */
function function_update_aktuelle_version(): string {
    $versionFile = null;
    if (defined('APP_ROOT')) {
        $versionFile = dirname(APP_ROOT) . DIRECTORY_SEPARATOR . 'version.txt';
    }
    if ($versionFile && is_file($versionFile)) {
        $v = trim((string) @file_get_contents($versionFile));
        if ($v !== '') return $v;
    }
    if (defined('WECKER_VERSION') && (string) WECKER_VERSION !== '') {
        return (string) WECKER_VERSION;
    }
    return '0.0.0';
}

/**
 * Update ausführen: ZIP herunterladen, Backup, entpacken, DB-Updates.
 * Es werden alle Einträge aus dem ZIP ins Installationsverzeichnis geschrieben
 * (nach Abzug eines gemeinsamen Präfixes). Das Release-ZIP muss die vollständige
 * Anwendung enthalten: config.php, cronjob.php, functions/, cronjob/, html/, database/, etc.,
 * damit auch Functions und Cronjob mit aktualisiert werden.
 *
 * @param string $version Versionskennung (z.B. "1.0.0")
 * @param string $installDir Absoluter Pfad zum Wecker-Root (z.B. /var/www/wecker)
 * @return array{ok: bool, message: string, log: string[]}
 */
function function_update_ausfuehren(string $version, string $installDir): array {
    $log = [];
    $log[] = 'Starte Update auf Version: ' . $version;

    if (!preg_match('/^[a-zA-Z0-9._-]+$/', $version)) {
        return ['ok' => false, 'message' => 'Ungültige Version', 'log' => $log];
    }

    $server = function_update_server_url();
    $url = rtrim($server, '/') . '/releases/versions.json';
    $json = @file_get_contents($url, false, function_update_http_context(15));
    if ($json === false) {
        $log[] = 'Versionen konnten nicht geladen werden';
        return ['ok' => false, 'message' => 'Update-Server nicht erreichbar', 'log' => $log];
    }

    $versions = json_decode($json, true);
    if (!is_array($versions)) {
        $log[] = 'Ungültige Antwort vom Update-Server';
        return ['ok' => false, 'message' => 'Ungültige Server-Antwort', 'log' => $log];
    }

    $selected = null;
    foreach ($versions as $v) {
        if (isset($v['version']) && $v['version'] === $version && !empty($v['file'])) {
            $selected = $v;
            break;
        }
    }
    if (!$selected) {
        $log[] = 'Version nicht gefunden: ' . $version;
        return ['ok' => false, 'message' => 'Version nicht gefunden', 'log' => $log];
    }

    $tmpZip = rtrim($installDir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . 'update.zip';
    $backupDir = rtrim($installDir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . 'backup_' . date('YmdHis');
    $downloadUrl = rtrim($server, '/') . '/releases/' . $selected['file'];

    $log[] = 'Lade herunter: ' . $selected['file'];
    $zipData = @file_get_contents($downloadUrl, false, function_update_http_context(120));
    if ($zipData === false || $zipData === '') {
        $log[] = 'Download fehlgeschlagen';
        return ['ok' => false, 'message' => 'Download fehlgeschlagen', 'log' => $log];
    }

    if (file_put_contents($tmpZip, $zipData) === false) {
        $log[] = 'ZIP konnte nicht gespeichert werden';
        return ['ok' => false, 'message' => 'ZIP speichern fehlgeschlagen', 'log' => $log];
    }
    unset($zipData);

    if (!file_exists($tmpZip) || filesize($tmpZip) === 0) {
        @unlink($tmpZip);
        $log[] = 'Download fehlgeschlagen (leere Datei)';
        return ['ok' => false, 'message' => 'Download fehlgeschlagen', 'log' => $log];
    }

    $log[] = 'Backup wird erstellt...';
    $maxBackups = (int) (defined('WECKER_UPDATE_MAX_BACKUPS') ? WECKER_UPDATE_MAX_BACKUPS : 3);
    $maxBackups = max(1, min(10, $maxBackups));
    _function_update_backup_cleanup($installDir, $maxBackups - 1, $log);

    if (!@mkdir($backupDir, 0755, true)) {
        @unlink($tmpZip);
        $log[] = 'Backup-Ordner konnte nicht erstellt werden';
        return ['ok' => false, 'message' => 'Backup fehlgeschlagen', 'log' => $log];
    }

    // Backup: nur Anwendungsdateien kopieren, keine alten Backups und keine update.zip
    _function_update_backup_copy($installDir, $backupDir);

    $log[] = 'Übernahme der Dateien (config.php wird nicht überschrieben)...';
    $zip = new ZipArchive();
    if ($zip->open($tmpZip) !== true) {
        @unlink($tmpZip);
        $log[] = 'ZIP Fehler';
        return ['ok' => false, 'message' => 'ZIP konnte nicht geöffnet werden', 'log' => $log];
    }

    $numFiles = $zip->numFiles;
    $prefix = '';
    $segmentCounts = [];
    for ($i = 0; $i < $numFiles; $i++) {
        $entryName = $zip->getNameIndex($i);
        if ($entryName !== false && strpos($entryName, '/') !== false) {
            $seg = substr($entryName, 0, strpos($entryName, '/') + 1);
            $segmentCounts[$seg] = ($segmentCounts[$seg] ?? 0) + 1;
        }
    }
    if (!empty($segmentCounts)) {
        arsort($segmentCounts);
        $prefix = (string) key($segmentCounts);
    }

    $installDirNorm = rtrim($installDir, DIRECTORY_SEPARATOR);
    $installDirNormSlash = str_replace('\\', '/', $installDirNorm);
    $selfPath = $installDirNormSlash . '/html/update.php';
    $configPath = $installDirNorm . DIRECTORY_SEPARATOR . 'config.php';
    $pendingSelfUpdate = null;

    for ($i = 0; $i < $numFiles; $i++) {
        $entryName = $zip->getNameIndex($i);
        if ($entryName === false) continue;
        if ($prefix !== '' && strpos($entryName, $prefix) !== 0) continue;

        $relativePath = $prefix !== '' ? substr($entryName, strlen($prefix)) : $entryName;
        if ($relativePath === '' || $relativePath === false) continue;

        $targetPath = $installDirNorm . DIRECTORY_SEPARATOR . str_replace('/', DIRECTORY_SEPARATOR, $relativePath);
        $targetPathNorm = str_replace('\\', '/', $targetPath);
        $isSelf = ($targetPathNorm === $selfPath);
        // config.php nicht überschreiben – lokale Anpassungen (DB, Pfade, etc.) bleiben erhalten
        $isConfig = (str_replace('\\', '/', $targetPath) === str_replace('\\', '/', $configPath));

        if (substr($entryName, -1) === '/') {
            if (!is_dir($targetPath)) {
                @mkdir($targetPath, 0755, true);
            }
        } else {
            if ($isConfig) {
                continue;
            }
            $targetDir = dirname($targetPath);
            if (!is_dir($targetDir)) {
                @mkdir($targetDir, 0755, true);
            }
            $content = $zip->getFromIndex($i);
            if ($content !== false) {
                if ($isSelf) {
                    $pendingSelfUpdate = $installDirNormSlash . '/html/update.php.new';
                    file_put_contents($pendingSelfUpdate, $content);
                } else {
                    file_put_contents($targetPath, $content);
                }
            }
        }
    }
    $zip->close();
    @unlink($tmpZip);

    if ($pendingSelfUpdate !== null && file_exists($pendingSelfUpdate)) {
        @rename($pendingSelfUpdate, $installDirNormSlash . '/html/update.php');
    }

    $log[] = 'Alle Tabellen-Updates werden ausgeführt...';
    $updateInc = $installDirNormSlash . '/html/run_database_updates.inc.php';
    $weckerRoot = $installDirNorm;
    $databaseUpdateDir = $weckerRoot . DIRECTORY_SEPARATOR . 'database' . DIRECTORY_SEPARATOR . 'update';
    if (!is_dir($databaseUpdateDir)) {
        $databaseUpdateDir = null;
    }
    if (file_exists($updateInc)) {
        $pdo = $GLOBALS['pdo'] ?? null;
        if ($pdo) {
            $dbResult = require $updateInc;
            if (!empty($dbResult['executed'])) {
                $log[] = 'DB-Updates: ' . $dbResult['executed'] . ' ausgeführt';
            }
            if (!empty($dbResult['errors'])) {
                foreach ($dbResult['errors'] as $err) {
                    $log[] = 'DB-Fehler: ' . $err;
                }
            }
        } else {
            $log[] = 'Hinweis: Keine DB-Verbindung, Tabellen-Updates übersprungen';
        }
    } else {
        $log[] = 'Hinweis: run_database_updates.inc.php nicht gefunden';
    }

    $log[] = 'UPDATE SUCCESS';
    $versionFile = $weckerRoot . DIRECTORY_SEPARATOR . 'version.txt';
    if (@file_put_contents($versionFile, $version) !== false) {
        $log[] = 'Version gespeichert: ' . $version;
    }

    // Alte Backups aufräumen: maximal WECKER_UPDATE_MAX_BACKUPS (z. B. 3) behalten
    _function_update_backup_cleanup($installDir, $maxBackups, $log);

    return ['ok' => true, 'message' => 'Update erfolgreich', 'log' => $log];
}

/**
 * Kopiert Installationsverzeichnis ins Backup – ohne backup_* Ordner und update.zip,
 * damit Backups nicht immer größer werden.
 */
function _function_update_backup_copy(string $installDir, string $backupDir): void {
    $installDir = rtrim($installDir, DIRECTORY_SEPARATOR);
    $list = @scandir($installDir);
    if (!$list) return;
    foreach ($list as $f) {
        if ($f === '.' || $f === '..') continue;
        if ($f === 'update.zip') continue;
        if (strpos($f, 'backup_') === 0) continue;
        $src = $installDir . DIRECTORY_SEPARATOR . $f;
        $dst = $backupDir . DIRECTORY_SEPARATOR . $f;
        if (is_dir($src)) {
            @mkdir($dst, 0755, true);
            _function_update_copydir($src, $dst);
        } else {
            @copy($src, $dst);
        }
    }
}

/**
 * Entfernt alte Backup-Ordner, behält nur die letzten $keep Stück (nach Name sortiert, neueste zuerst).
 */
function _function_update_backup_cleanup(string $installDir, int $keep, array &$log): void {
    $installDir = rtrim($installDir, DIRECTORY_SEPARATOR);
    $list = @scandir($installDir);
    if (!$list || $keep < 1) return;
    $backups = [];
    foreach ($list as $f) {
        if (strpos($f, 'backup_') === 0 && is_dir($installDir . DIRECTORY_SEPARATOR . $f)) {
            $backups[] = $f;
        }
    }
    if (count($backups) <= $keep) return;
    rsort($backups, SORT_STRING);
    $toRemove = array_slice($backups, $keep);
    foreach ($toRemove as $dir) {
        $path = $installDir . DIRECTORY_SEPARATOR . $dir;
        if (_function_update_rmdir_recursive($path)) {
            $log[] = 'Altes Backup entfernt: ' . $dir;
        }
    }
}

function _function_update_rmdir_recursive(string $dir): bool {
    if (!is_dir($dir)) return false;
    $list = @scandir($dir);
    if (!$list) return false;
    foreach ($list as $f) {
        if ($f === '.' || $f === '..') continue;
        $path = $dir . DIRECTORY_SEPARATOR . $f;
        if (is_dir($path)) {
            _function_update_rmdir_recursive($path);
        } else {
            @unlink($path);
        }
    }
    return @rmdir($dir);
}

function _function_update_copydir(string $src, string $dst): void {
    if (!is_dir($src)) return;
    @mkdir($dst, 0755, true);
    $dir = @opendir($src);
    if (!$dir) return;
    while (($f = readdir($dir)) !== false) {
        if ($f === '.' || $f === '..') continue;
        $srcPath = $src . DIRECTORY_SEPARATOR . $f;
        $dstPath = $dst . DIRECTORY_SEPARATOR . $f;
        if (is_dir($srcPath)) {
            _function_update_copydir($srcPath, $dstPath);
        } else {
            @copy($srcPath, $dstPath);
        }
    }
    closedir($dir);
}
