<?php

declare(strict_types=1);

$baseDir = __DIR__;
$configFile = $baseDir . "/config.php";

if (is_file($configFile)) {
    require_once $configFile;
}

if (!defined("BACKUP_SERVER_RETENTION")) {
    define("BACKUP_SERVER_RETENTION", 30);
}
if (!defined("BACKUP_SERVER_BACKUP_DIR")) {
    define("BACKUP_SERVER_BACKUP_DIR", $baseDir . "/backups/");
}
if (!defined("BACKUP_SERVER_INDEX_FILE")) {
    define("BACKUP_SERVER_INDEX_FILE", rtrim((string) BACKUP_SERVER_BACKUP_DIR, "/\\") . "/index.json");
}
if (!defined("BACKUP_SERVER_SETTINGS_FILE")) {
    define("BACKUP_SERVER_SETTINGS_FILE", rtrim((string) BACKUP_SERVER_BACKUP_DIR, "/\\") . "/settings.json");
}

header("Content-Type: application/json; charset=utf-8");
header("Cache-Control: no-store");
header("X-Content-Type-Options: nosniff");

function backupUploadRespond(int $status, array $payload): void
{
    http_response_code($status);
    echo json_encode(
        $payload,
        JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE,
    );
    exit;
}

function backupUploadEnsureDirectory(string $dir): void
{
    if (!is_dir($dir) && !mkdir($dir, 02775, true) && !is_dir($dir)) {
        throw new RuntimeException("Directory cannot be created.");
    }

    @chmod($dir, 02775);
}

function backupUploadNormalizeInstance(string $instance): string
{
    $instance = trim($instance);
    if (
        $instance === "" ||
        strlen($instance) > 120 ||
        preg_match('/^[A-Za-z0-9][A-Za-z0-9._-]*$/', $instance) !== 1
    ) {
        throw new RuntimeException("Invalid instance identifier.");
    }

    return $instance;
}

function backupUploadReadJsonFile(string $file): array
{
    if (!is_file($file)) {
        return [];
    }

    $decoded = json_decode((string) file_get_contents($file), true);
    return is_array($decoded) ? $decoded : [];
}

function backupUploadWriteJsonFile(string $file, array $data): void
{
    backupUploadEnsureDirectory(dirname($file));

    $json = json_encode(
        $data,
        JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE,
    );
    if ($json === false) {
        throw new RuntimeException("JSON cannot be encoded.");
    }

    $tmpFile = $file . ".tmp";
    if (file_put_contents($tmpFile, $json . PHP_EOL, LOCK_EX) === false) {
        throw new RuntimeException("JSON cannot be written.");
    }

    @chmod($tmpFile, 0664);
    if (!rename($tmpFile, $file)) {
        @unlink($tmpFile);
        throw new RuntimeException("JSON cannot be saved.");
    }

    @chmod($file, 0664);
}

function backupUploadReadIndex(): array
{
    $index = backupUploadReadJsonFile((string) BACKUP_SERVER_INDEX_FILE);
    $backups = isset($index["backups"]) && is_array($index["backups"])
        ? $index["backups"]
        : [];

    return ["backups" => array_values($backups)];
}

function backupUploadWriteIndex(array $backups): void
{
    backupUploadWriteJsonFile((string) BACKUP_SERVER_INDEX_FILE, [
        "backups" => array_values($backups),
    ]);
}

function backupUploadGetRetention(): int
{
    $settings = backupUploadReadJsonFile((string) BACKUP_SERVER_SETTINGS_FILE);
    if (isset($settings["retention"])) {
        return max(1, (int) $settings["retention"]);
    }

    return max(1, (int) BACKUP_SERVER_RETENTION);
}

function backupUploadGetAllowedInstances(): array
{
    $settings = backupUploadReadJsonFile((string) BACKUP_SERVER_SETTINGS_FILE);
    $instances =
        isset($settings["instances"]) && is_array($settings["instances"])
            ? $settings["instances"]
            : [];
    $allowed = [];

    foreach ($instances as $instance) {
        $instance = (string) $instance;
        if (
            $instance !== "" &&
            preg_match('/^[A-Za-z0-9][A-Za-z0-9._-]*$/', $instance) === 1
        ) {
            $allowed[$instance] = true;
        }
    }

    return $allowed;
}

function backupUploadInstanceIsAllowed(string $instance): bool
{
    $allowed = backupUploadGetAllowedInstances();
    return isset($allowed[$instance]);
}

function backupUploadInstanceDir(string $instance): string
{
    return rtrim((string) BACKUP_SERVER_BACKUP_DIR, "/\\") . DIRECTORY_SEPARATOR . $instance;
}

function backupUploadIsZipFile(string $path): bool
{
    $handle = fopen($path, "rb");
    if ($handle === false) {
        return false;
    }

    $signature = fread($handle, 4);
    fclose($handle);

    return $signature === "PK\x03\x04" ||
        $signature === "PK\x05\x06" ||
        $signature === "PK\x07\x08";
}

function backupUploadChooseFilename(string $clientFilename, string $instanceDir): string
{
    $clientFilename = trim($clientFilename);
    if ($clientFilename === "") {
        $filename = "backup-" . gmdate("Ymd-His") . ".zip";
    } elseif (
        basename($clientFilename) !== $clientFilename ||
        preg_match('/^backup-\d{8}-\d{6}(?:-\d+)?\.zip$/', $clientFilename) !== 1
    ) {
        throw new RuntimeException("Invalid backup filename.");
    } else {
        $filename = $clientFilename;
    }

    $base = substr($filename, 0, -4);
    $counter = 2;
    while (is_file($instanceDir . DIRECTORY_SEPARATOR . $filename)) {
        $filename = $base . "-" . $counter . ".zip";
        $counter++;
    }

    return $filename;
}

function backupUploadApplyRetention(string $instance): void
{
    $index = backupUploadReadIndex();
    $retention = backupUploadGetRetention();
    $instanceBackups = [];
    $otherBackups = [];

    foreach ($index["backups"] as $backup) {
        if (!is_array($backup)) {
            continue;
        }
        if (($backup["instance"] ?? "") === $instance) {
            $instanceBackups[] = $backup;
        } else {
            $otherBackups[] = $backup;
        }
    }

    usort($instanceBackups, function ($left, $right) {
        return strcmp((string) ($right["uploaded_at"] ?? ""), (string) ($left["uploaded_at"] ?? ""));
    });

    $keep = array_slice($instanceBackups, 0, $retention);
    $remove = array_slice($instanceBackups, $retention);
    $instanceDir = backupUploadInstanceDir($instance);

    foreach ($remove as $backup) {
        $filename = basename((string) ($backup["filename"] ?? ""));
        if ($filename !== "" && is_file($instanceDir . DIRECTORY_SEPARATOR . $filename)) {
            @unlink($instanceDir . DIRECTORY_SEPARATOR . $filename);
        }
    }

    backupUploadWriteIndex(array_merge($otherBackups, $keep));
}

if ($_SERVER["REQUEST_METHOD"] !== "POST") {
    backupUploadRespond(405, ["success" => false, "error" => "POST required."]);
}

try {
    $instance = backupUploadNormalizeInstance((string) ($_POST["instance"] ?? ""));
    if (!backupUploadInstanceIsAllowed($instance)) {
        throw new RuntimeException("Instance is not allowed.");
    }

    $file = $_FILES["backup"] ?? null;
    if (!is_array($file)) {
        throw new RuntimeException("Backup file is missing.");
    }

    if (($file["error"] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) {
        throw new RuntimeException("Upload failed with error code " . (string) ($file["error"] ?? "unknown") . ".");
    }

    $tmpName = (string) ($file["tmp_name"] ?? "");
    if ($tmpName === "" || !is_uploaded_file($tmpName)) {
        throw new RuntimeException("Upload is invalid.");
    }

    if (!backupUploadIsZipFile($tmpName)) {
        throw new RuntimeException("Uploaded file must be a ZIP file.");
    }

    $instanceDir = backupUploadInstanceDir($instance);
    backupUploadEnsureDirectory($instanceDir);

    $requestedFilename = (string) ($_POST["filename"] ?? "");
    $clientFilename = $requestedFilename !== "" ? $requestedFilename : (string) ($file["name"] ?? "");
    $filename = backupUploadChooseFilename($requestedFilename, $instanceDir);
    $targetPath = $instanceDir . DIRECTORY_SEPARATOR . $filename;

    if (!move_uploaded_file($tmpName, $targetPath)) {
        throw new RuntimeException("Uploaded backup cannot be stored.");
    }

    @chmod($targetPath, 0664);

    $size = filesize($targetPath);
    $sha256 = strtolower(hash_file("sha256", $targetPath) ?: "");
    if ($size === false || $size <= 0 || !preg_match('/^[a-f0-9]{64}$/', $sha256)) {
        @unlink($targetPath);
        throw new RuntimeException("Stored backup could not be verified.");
    }

    $postedSha256 = strtolower(trim((string) ($_POST["sha256"] ?? "")));
    if ($postedSha256 !== "" && (!preg_match('/^[a-f0-9]{64}$/', $postedSha256) || $postedSha256 !== $sha256)) {
        @unlink($targetPath);
        throw new RuntimeException("Backup checksum mismatch.");
    }

    $index = backupUploadReadIndex();
    $record = [
        "instance" => $instance,
        "filename" => $filename,
        "client_filename" => basename($clientFilename),
        "size" => $size,
        "sha256" => $sha256,
        "uploaded_at" => date(DATE_ATOM),
        "source_ip" => $_SERVER["REMOTE_ADDR"] ?? "unknown",
    ];
    $index["backups"][] = $record;
    backupUploadWriteIndex($index["backups"]);
    backupUploadApplyRetention($instance);

    backupUploadRespond(200, [
        "success" => true,
        "instance" => $instance,
        "filename" => $filename,
        "size" => $size,
        "sha256" => $sha256,
        "retention" => backupUploadGetRetention(),
    ]);
} catch (Throwable $exception) {
    backupUploadRespond(400, [
        "success" => false,
        "error" => $exception->getMessage(),
    ]);
}
