<?php

declare(strict_types=1);

/**
 * One-time FULL-bundle applier — rescue a stuck or drifted install.
 *
 * WHY THIS EXISTS: over-the-air updates ship tiny DELTA patches, and a delta is
 * refused if a file it expects unchanged actually differs on this server (drift).
 * Modern builds fall back to the full bundle automatically, but an OLDER install
 * (built before that fallback) has no way to self-heal — its delta is rejected
 * with "this patch expects X to be unchanged, but it differs on this server", and
 * the local-upload box is capped by PHP's upload_max_filesize (often 2 MB) so a
 * 30 MB full can't be uploaded through the browser.
 *
 * A FULL bundle carries every file, so it NEVER hits the drift guard. This script
 * applies one with the platform's own safe path: it makes a restore point, writes
 * only the files that actually change, and prunes files removed since. After it
 * runs once the install is current and normal OTA (with the drift->full self-heal)
 * takes over.
 *
 * USAGE (CLI only — run from cPanel Terminal, SSH, or a one-off cron):
 *
 *   # From a LOCAL path (best — no download, no upload limit). The full bundle is
 *   # already on your account if you published it; point at it directly:
 *   php bin/apply-full-bundle.php /home/USER/UPDATE-HOST-DIR/full/ppphp-server-1.0.25.zip
 *
 *   # …or fetch it over HTTPS from the update host (needs the public URL to work):
 *   php bin/apply-full-bundle.php https://ppphp-updates.example.com/full/ppphp-server-1.0.25.zip
 *
 * It refuses to run over the web. Exit 0 on success, 1 on any failure.
 */

if (PHP_SAPI !== 'cli') {
    http_response_code(404);
    exit("This script runs from the command line only (cPanel Terminal / SSH / cron).\n");
}

$source = $argv[1] ?? '';
if ($source === '') {
    fwrite(STDERR, "Usage: php bin/apply-full-bundle.php <path-or-URL-to-full-bundle.zip>\n");
    exit(1);
}

define('BASE_PATH', dirname(__DIR__));
error_reporting(E_ALL & ~E_DEPRECATED);
@set_time_limit(0);
@ignore_user_abort(true);

$__tz = (string) (((array) (is_file(BASE_PATH . '/config/app.php') ? require BASE_PATH . '/config/app.php' : []))['timezone'] ?? 'Asia/Kathmandu');
if ($__tz !== '') { @date_default_timezone_set($__tz); if (!defined('PP_TIMEZONE')) define('PP_TIMEZONE', $__tz); }

require_once BASE_PATH . '/core/Autoloader.php';
$autoloader = new \PpPhp\Core\Autoloader();
$autoloader->addNamespace('PpPhp\\Core\\', BASE_PATH . '/core/');
$autoloader->addNamespace('PpPhp\\Plugins\\', BASE_PATH . '/plugins/');
$autoloader->register();
require_once BASE_PATH . '/core/Helpers.php';

// DB is only needed to stamp the installed version afterwards; tolerate its absence.
try {
    $dbConfig = require BASE_PATH . '/config/database.php';
    $container = new \PpPhp\Core\Container();
    $container->set(\PpPhp\Core\Config::class, new \PpPhp\Core\Config([
        'base_url' => '', 'assets_url' => '/assets', 'db_config' => new \PpPhp\Core\Config($dbConfig),
    ]));
    \PpPhp\Core\DB::Init();
    \PpPhp\Core\DB::SetConnection();
} catch (\Throwable $e) {
    fwrite(STDERR, "[warn] could not open the database (version stamp will be skipped): " . $e->getMessage() . "\n");
}

$log = static function (string $m): void { echo '[' . date('H:i:s') . '] ' . $m . "\n"; };

// 1. Resolve the bundle to a local file (download if a URL was given).
$zipPath = $source;
$temp    = null;
if (preg_match('#^https?://#i', $source)) {
    $log('Downloading the full bundle…');
    $dir = BASE_PATH . '/storage/updates/incoming';
    if (!is_dir($dir)) @mkdir($dir, 0775, true);
    $temp = $dir . '/manual-full-' . date('Ymd-His') . '.zip';
    $ok = false;
    if (function_exists('curl_init')) {
        $fh = @fopen($temp, 'wb');
        if ($fh) {
            $ch = curl_init($source);
            curl_setopt_array($ch, [
                CURLOPT_FILE => $fh, CURLOPT_FOLLOWLOCATION => true,
                CURLOPT_CONNECTTIMEOUT => 15, CURLOPT_TIMEOUT => 600,
                CURLOPT_USERAGENT => 'PPPHP-Update/1',
            ]);
            $ok = curl_exec($ch) !== false && (int) curl_getinfo($ch, CURLINFO_HTTP_CODE) === 200;
            curl_close($ch);
            fclose($fh);
        }
    }
    if (!$ok) {
        @unlink($temp);
        fwrite(STDERR, "[error] Download failed. If the public URL 404s, fix it in Update host (a common mistake is including 'public_html' in the URL), or pass a LOCAL file path instead.\n");
        exit(1);
    }
    $zipPath = $temp;
    $log('Downloaded ' . number_format((int) @filesize($zipPath) / 1048576, 1) . ' MB.');
}

if (!is_file($zipPath)) {
    fwrite(STDERR, "[error] Not a file: {$zipPath}\n");
    exit(1);
}

// 2. Inspect + apply with the platform's safe updater.
try {
    $updater = new \PpPhp\Core\Update\PlatformUpdater(BASE_PATH);
    $log('Installed version: ' . $updater->installedVersion());

    $report = $updater->inspect($zipPath);
    if (!empty($report['delta'])) {
        if ($temp) @unlink($temp);
        fwrite(STDERR, "[error] That is a DELTA bundle, not a full one. This tool needs the FULL bundle (the file under full/ on the update host, ~30 MB).\n");
        exit(1);
    }
    if (empty($report['ok'])) {
        if ($temp) @unlink($temp);
        fwrite(STDERR, "[error] The bundle was rejected: " . implode(' ', (array) ($report['errors'] ?? ['unknown'])) . "\n");
        exit(1);
    }
    $sum = $report['summary'] ?? [];
    $log("Plan: {$report['version']} — add " . (int) ($sum['add'] ?? 0) . ', update ' . (int) ($sum['update'] ?? 0)
        . ', remove ' . (int) ($sum['remove'] ?? 0) . ', unchanged ' . (int) ($sum['unchanged'] ?? 0) . '.');

    $log('Applying (a restore point is created first)…');
    $result = $updater->apply($zipPath);
    $version = (string) ($result['version'] ?? $report['version']);

    if (class_exists('\PpPhp\Core\PlatformSettings')) {
        try { \PpPhp\Core\PlatformSettings::setVersion($version); } catch (\Throwable $e) {}
    }
    if ($temp) @unlink($temp);

    $log('DONE — now running ' . $version . '. Restore point: ' . (string) ($result['restoreId'] ?? '?') . '.');
    $log('Open the site and confirm it loads. Future updates will apply over-the-air normally.');
    exit(0);
} catch (\Throwable $e) {
    if ($temp) @unlink($temp);
    fwrite(STDERR, "[error] " . $e->getMessage() . "\n");
    exit(1);
}
