<?php
declare(strict_types=1);

set_time_limit(0);
ignore_user_abort(true);

$source = __DIR__ . '/backup.zip';
$prefix = __DIR__ . '/backup.zip.part-';
$partSize = 3 * 1024 * 1024 * 1024; // 3 GiB
$chunkSize = 8 * 1024 * 1024;       // 8 MiB

if (PHP_INT_SIZE < 8) {
    exit("ERROR: PHP must support 64-bit integers.\n");
}

if (!is_file($source) || !is_readable($source)) {
    exit("ERROR: backup.zip not found or not readable.\n");
}

$sourceSize = filesize($source);
$parts = glob($prefix . '*') ?: [];
sort($parts, SORT_STRING);

if (!$parts) {
    exit("ERROR: No existing split parts found.\n");
}

// Validate names and sizes before resuming.
$expectedIndex = 0;
$totalExisting = 0;
$lastPart = null;

foreach ($parts as $part) {
    if (!preg_match('/\.part-(\d{3,})$/', $part, $match)) {
        exit("ERROR: Unexpected part filename: " . basename($part) . "\n");
    }

    $index = (int)$match[1];

    if ($index !== $expectedIndex) {
        exit("ERROR: Missing or out-of-order part at index $expectedIndex.\n");
    }

    $size = filesize($part);

    if ($size === false || $size > $partSize) {
        exit("ERROR: Invalid part size: " . basename($part) . "\n");
    }

    // Every part except the last must be complete.
    if ($lastPart !== null && filesize($lastPart) !== $partSize) {
        exit("ERROR: Earlier part is incomplete: " . basename($lastPart) . "\n");
    }

    $totalExisting += $size;
    $lastPart = $part;
    $expectedIndex++;
}

if ($totalExisting > $sourceSize) {
    exit("ERROR: Existing parts exceed source ZIP size.\n");
}

if ($totalExisting === $sourceSize) {
    echo "All bytes are already split. Nothing to resume.\n";
    exit;
}

$input = fopen($source, 'rb');

if (!$input || fseek($input, $totalExisting) !== 0) {
    exit("ERROR: Could not seek to the resume position.\n");
}

echo "Source size: $sourceSize bytes\n";
echo "Already split: $totalExisting bytes\n";
echo "Resuming from byte: $totalExisting\n";

$index = count($parts) - 1;
$currentPart = $lastPart;
$currentSize = filesize($currentPart);

// Continue filling the existing partial part, if necessary.
$output = fopen($currentPart, 'ab');

if (!$output) {
    fclose($input);
    exit("ERROR: Cannot append to existing part.\n");
}

while ($totalExisting < $sourceSize) {
    if ($currentSize >= $partSize) {
        fclose($output);

        $index++;
        $currentPart = $prefix . str_pad((string)$index, 3, '0', STR_PAD_LEFT);
        $output = fopen($currentPart, 'wb');
        $currentSize = 0;

        if (!$output) {
            fclose($input);
            exit("ERROR: Cannot create " . basename($currentPart) . "\n");
        }
    }

    $remainingPart = $partSize - $currentSize;
    $remainingSource = $sourceSize - $totalExisting;
    $readSize = (int)min(
        $chunkSize,
        $remainingPart,
        $remainingSource
    );

    $data = fread($input, $readSize);

    if ($data === false || $data === '') {
        fclose($output);
        fclose($input);
        exit("ERROR: Read failed before reaching end of source.\n");
    }

    $length = strlen($data);
    $written = 0;

    while ($written < $length) {
        $n = fwrite($output, substr($data, $written));

        if ($n === false || $n === 0) {
            fclose($output);
            fclose($input);
            exit("ERROR: Disk write failed. Run again to resume.\n");
        }

        $written += $n;
    }

    $currentSize += $length;
    $totalExisting += $length;

    echo "\rProgress: " .
        round(($totalExisting / $sourceSize) * 100, 2) .
        "% (" . basename($currentPart) . ")";

    if (!fflush($output)) {
        fclose($output);
        fclose($input);
        exit("\nERROR: Could not flush output. Check disk space.\n");
    }
}

fclose($output);
fclose($input);

echo "\nSplit completed successfully.\n";
echo "Verify the parts before deleting the original ZIP.\n";
