<?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");
}

if (session_status() === PHP_SESSION_NONE) {
    ini_set("session.use_strict_mode", "1");
    ini_set("session.cookie_httponly", "1");
    ini_set("session.cookie_samesite", "Lax");
    session_start();
}

$messages = [];
$errors = [];

function backupManageEscape($value): string
{
    return htmlspecialchars((string) $value, ENT_QUOTES, "UTF-8");
}

function backupManagePasswordConfigured(): bool
{
    return defined("BACKUP_SERVER_PASSWORD_HASH") || defined("BACKUP_SERVER_PASSWORD");
}

function backupManagePasswordMatches(string $password): bool
{
    if (defined("BACKUP_SERVER_PASSWORD_HASH")) {
        return password_verify($password, (string) BACKUP_SERVER_PASSWORD_HASH);
    }

    if (defined("BACKUP_SERVER_PASSWORD")) {
        return hash_equals((string) BACKUP_SERVER_PASSWORD, $password);
    }

    return false;
}

function backupManageIsLoggedIn(): bool
{
    return !empty($_SESSION["backup_server_logged_in"]);
}

function backupManageCsrfToken(): string
{
    if (empty($_SESSION["backup_server_csrf_token"])) {
        $_SESSION["backup_server_csrf_token"] = bin2hex(random_bytes(32));
    }

    return $_SESSION["backup_server_csrf_token"];
}

function backupManageCsrfIsValid(string $token): bool
{
    return !empty($_SESSION["backup_server_csrf_token"]) &&
        hash_equals($_SESSION["backup_server_csrf_token"], $token);
}

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

    @chmod($dir, 02775);
}

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

    $decoded = json_decode((string) file_get_contents($file), true);
    if (!is_array($decoded)) {
        throw new RuntimeException("JSON file is invalid: " . basename($file));
    }

    return $decoded;
}

function backupManageWriteJsonFile(string $file, array $data): void
{
    backupManageEnsureDirectory(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 backupManageReadIndex(): array
{
    $index = backupManageReadJsonFile((string) BACKUP_SERVER_INDEX_FILE);
    $backups = isset($index["backups"]) && is_array($index["backups"])
        ? $index["backups"]
        : [];

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

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

function backupManageReadSettings(): array
{
    $settings = backupManageReadJsonFile((string) BACKUP_SERVER_SETTINGS_FILE);
    $retention = isset($settings["retention"])
        ? max(1, (int) $settings["retention"])
        : max(1, (int) BACKUP_SERVER_RETENTION);
    $instances =
        isset($settings["instances"]) && is_array($settings["instances"])
            ? $settings["instances"]
            : [];
    $allowedInstances = [];

    foreach ($instances as $instance) {
        try {
            $allowedInstances[] = backupManageValidateInstance((string) $instance);
        } catch (Throwable $exception) {
            continue;
        }
    }
    $allowedInstances = array_values(array_unique($allowedInstances));
    sort($allowedInstances);

    return [
        "retention" => $retention,
        "instances" => $allowedInstances,
    ];
}

function backupManageWriteSettings(array $settings): void
{
    $instances =
        isset($settings["instances"]) && is_array($settings["instances"])
            ? $settings["instances"]
            : backupManageReadSettings()["instances"];
    $allowedInstances = [];

    foreach ($instances as $instance) {
        $allowedInstances[] = backupManageValidateInstance((string) $instance);
    }
    $allowedInstances = array_values(array_unique($allowedInstances));
    sort($allowedInstances);

    backupManageWriteJsonFile((string) BACKUP_SERVER_SETTINGS_FILE, [
        "retention" => max(1, (int) ($settings["retention"] ?? BACKUP_SERVER_RETENTION)),
        "instances" => $allowedInstances,
    ]);
}

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

function backupManageValidateInstance(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.");
    }

    return $instance;
}

function backupManageValidateFilename(string $filename): string
{
    $filename = basename(trim($filename));
    if (preg_match('/^backup-\d{8}-\d{6}(?:-\d+)?\.zip$/', $filename) !== 1) {
        throw new RuntimeException("Invalid backup filename.");
    }

    return $filename;
}

function backupManageFormatBytes(int $bytes): string
{
    if ($bytes >= 1073741824) {
        return number_format($bytes / 1073741824, 2, ",", ".") . " GB";
    }
    if ($bytes >= 1048576) {
        return number_format($bytes / 1048576, 2, ",", ".") . " MB";
    }
    if ($bytes >= 1024) {
        return number_format($bytes / 1024, 1, ",", ".") . " KB";
    }
    return $bytes . " B";
}

function backupManageFindBackup(string $instance, string $filename): ?array
{
    foreach (backupManageReadIndex()["backups"] as $backup) {
        if (!is_array($backup)) {
            continue;
        }
        if (($backup["instance"] ?? "") === $instance && ($backup["filename"] ?? "") === $filename) {
            return $backup;
        }
    }

    return null;
}

function backupManageBackupPath(string $instance, string $filename): string
{
    return backupManageInstanceDir($instance) . DIRECTORY_SEPARATOR . $filename;
}

function backupManageSendDownload(string $instance, string $filename): void
{
    $backup = backupManageFindBackup($instance, $filename);
    if ($backup === null) {
        throw new RuntimeException("Backup not found.");
    }

    $path = backupManageBackupPath($instance, $filename);
    $size = filesize($path);
    $handle = fopen($path, "rb");
    if ($size === false || $handle === false) {
        throw new RuntimeException("Backup cannot be opened.");
    }

    header("Content-Type: application/zip");
    header("Content-Disposition: attachment; filename=\"" . addcslashes($instance . "-" . $filename, "\"\\") . "\"");
    header("Content-Length: " . (string) $size);
    header("Cache-Control: private, no-store");
    header("X-Content-Type-Options: nosniff");

    fpassthru($handle);
    fclose($handle);
    exit;
}

function backupManageDeleteBackup(string $instance, string $filename): void
{
    $index = backupManageReadIndex();
    $kept = [];
    $found = false;

    foreach ($index["backups"] as $backup) {
        if (
            is_array($backup) &&
            ($backup["instance"] ?? "") === $instance &&
            ($backup["filename"] ?? "") === $filename
        ) {
            $found = true;
            continue;
        }
        $kept[] = $backup;
    }

    if (!$found) {
        throw new RuntimeException("Backup not found.");
    }

    $path = backupManageBackupPath($instance, $filename);
    if (is_file($path)) {
        unlink($path);
    }

    backupManageWriteIndex($kept);
}

function backupManageApplyRetentionForInstance(string $instance): void
{
    $index = backupManageReadIndex();
    $retention = backupManageReadSettings()["retention"];
    $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);

    foreach ($remove as $backup) {
        $filename = basename((string) ($backup["filename"] ?? ""));
        if ($filename !== "") {
            $path = backupManageBackupPath($instance, $filename);
            if (is_file($path)) {
                @unlink($path);
            }
        }
    }

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

function backupManageApplyRetentionAll(): void
{
    $instances = [];
    foreach (backupManageReadIndex()["backups"] as $backup) {
        if (is_array($backup)) {
            $instance = (string) ($backup["instance"] ?? "");
            if ($instance !== "") {
                $instances[$instance] = true;
            }
        }
    }

    foreach (array_keys($instances) as $instance) {
        backupManageApplyRetentionForInstance($instance);
    }
}

function backupManageAddInstance(string $instance): void
{
    $instance = backupManageValidateInstance($instance);
    $settings = backupManageReadSettings();
    $settings["instances"][] = $instance;
    backupManageWriteSettings($settings);
}

function backupManageRemoveInstance(string $instance): void
{
    $instance = backupManageValidateInstance($instance);
    $settings = backupManageReadSettings();
    $settings["instances"] = array_values(
        array_filter($settings["instances"], function ($existing) use ($instance) {
            return $existing !== $instance;
        }),
    );
    backupManageWriteSettings($settings);
}

function backupManageGroupBackupsByInstance(array $backups): array
{
    $grouped = [];
    foreach ($backups as $backup) {
        if (!is_array($backup)) {
            continue;
        }
        $instance = (string) ($backup["instance"] ?? "");
        $filename = basename((string) ($backup["filename"] ?? ""));
        if ($instance === "" || $filename === "" || !is_file(backupManageBackupPath($instance, $filename))) {
            continue;
        }
        $backup["filename"] = $filename;
        $backup["size"] = (int) (filesize(backupManageBackupPath($instance, $filename)) ?: ($backup["size"] ?? 0));
        $grouped[$instance][] = $backup;
    }

    ksort($grouped);
    foreach ($grouped as &$records) {
        usort($records, function ($left, $right) {
            return strcmp((string) ($right["uploaded_at"] ?? ""), (string) ($left["uploaded_at"] ?? ""));
        });
    }
    unset($records);

    return $grouped;
}

if ($_SERVER["REQUEST_METHOD"] === "POST") {
    $action = (string) ($_POST["action"] ?? "");

    if ($action === "login") {
        if (!backupManagePasswordConfigured()) {
            $errors[] = "No password is configured.";
        } elseif (backupManagePasswordMatches((string) ($_POST["password"] ?? ""))) {
            session_regenerate_id(true);
            $_SESSION["backup_server_logged_in"] = true;
            $messages[] = "Logged in.";
        } else {
            $errors[] = "Wrong password.";
        }
    } elseif ($action === "logout") {
        unset($_SESSION["backup_server_logged_in"], $_SESSION["backup_server_csrf_token"]);
        $messages[] = "Logged out.";
    } elseif (!backupManageIsLoggedIn()) {
        $errors[] = "Login required.";
    } elseif (!backupManageCsrfIsValid((string) ($_POST["csrf_token"] ?? ""))) {
        $errors[] = "Invalid token. Please reload the page and try again.";
    } else {
        try {
            if ($action === "update_retention") {
                $retention = max(1, (int) ($_POST["retention"] ?? BACKUP_SERVER_RETENTION));
                $settings = backupManageReadSettings();
                $settings["retention"] = $retention;
                backupManageWriteSettings($settings);
                backupManageApplyRetentionAll();
                $messages[] = "Retention updated.";
            } elseif ($action === "add_instance") {
                backupManageAddInstance((string) ($_POST["instance"] ?? ""));
                $messages[] = "Instance added.";
            } elseif ($action === "remove_instance") {
                backupManageRemoveInstance((string) ($_POST["instance"] ?? ""));
                $messages[] = "Instance removed.";
            } elseif ($action === "download") {
                backupManageSendDownload(
                    backupManageValidateInstance((string) ($_POST["instance"] ?? "")),
                    backupManageValidateFilename((string) ($_POST["filename"] ?? "")),
                );
            } elseif ($action === "delete") {
                backupManageDeleteBackup(
                    backupManageValidateInstance((string) ($_POST["instance"] ?? "")),
                    backupManageValidateFilename((string) ($_POST["filename"] ?? "")),
                );
                $messages[] = "Backup deleted.";
            }
        } catch (Throwable $exception) {
            $errors[] = $exception->getMessage();
        }
    }
}

try {
    $settings = backupManageReadSettings();
    $groupedBackups = backupManageGroupBackupsByInstance(backupManageReadIndex()["backups"]);
} catch (Throwable $exception) {
    $settings = ["retention" => max(1, (int) BACKUP_SERVER_RETENTION)];
    $groupedBackups = [];
    $errors[] = $exception->getMessage();
}

?>
<!DOCTYPE html>
<html lang="de">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Backup Management</title>
</head>
<body>
    <h1>Backup Management</h1>

    <?php foreach ($messages as $message): ?>
        <p><strong><?php echo backupManageEscape($message); ?></strong></p>
    <?php endforeach; ?>

    <?php foreach ($errors as $error): ?>
        <p><strong>Error:</strong> <?php echo backupManageEscape($error); ?></p>
    <?php endforeach; ?>

    <?php if (!backupManageIsLoggedIn()): ?>
        <form method="POST">
            <input type="hidden" name="action" value="login">
            <p>
                <label for="password">Password</label><br>
                <input type="password" id="password" name="password" required>
            </p>
            <button type="submit">Login</button>
        </form>
    <?php else: ?>
        <form method="POST">
            <input type="hidden" name="action" value="logout">
            <button type="submit">Logout</button>
        </form>

        <h2>Settings</h2>
        <form method="POST">
            <input type="hidden" name="action" value="update_retention">
            <input type="hidden" name="csrf_token" value="<?php echo backupManageEscape(backupManageCsrfToken()); ?>">
            <p>
                <label for="retention">Backups retained per instance</label><br>
                <input type="number" id="retention" name="retention" min="1" required value="<?php echo (int) $settings["retention"]; ?>">
            </p>
            <button type="submit">Save retention</button>
        </form>

        <h2>Upload endpoint</h2>
        <p>Distributed instances should upload to <code>upload.php</code>.</p>

        <h2>Allowed instances</h2>
        <form method="POST">
            <input type="hidden" name="action" value="add_instance">
            <input type="hidden" name="csrf_token" value="<?php echo backupManageEscape(backupManageCsrfToken()); ?>">
            <p>
                <label for="instance">Instance identifier</label><br>
                <input type="text" id="instance" name="instance" required pattern="[A-Za-z0-9][A-Za-z0-9._-]*" maxlength="120">
            </p>
            <button type="submit">Add instance</button>
        </form>

        <?php if (empty($settings["instances"])): ?>
            <p>No instances allowed. Uploads will be rejected until an instance is added.</p>
        <?php else: ?>
            <table border="1" cellpadding="6" cellspacing="0">
                <thead>
                    <tr>
                        <th>Instance</th>
                        <th>Actions</th>
                    </tr>
                </thead>
                <tbody>
                    <?php foreach ($settings["instances"] as $instance): ?>
                        <tr>
                            <td><?php echo backupManageEscape($instance); ?></td>
                            <td>
                                <form method="POST" style="display:inline" onsubmit="return confirm('Remove this allowed instance? Existing backups remain visible.');">
                                    <input type="hidden" name="action" value="remove_instance">
                                    <input type="hidden" name="csrf_token" value="<?php echo backupManageEscape(backupManageCsrfToken()); ?>">
                                    <input type="hidden" name="instance" value="<?php echo backupManageEscape($instance); ?>">
                                    <button type="submit">Remove</button>
                                </form>
                            </td>
                        </tr>
                    <?php endforeach; ?>
                </tbody>
            </table>
        <?php endif; ?>

        <h2>Backups</h2>
        <?php if (empty($groupedBackups)): ?>
            <p>No backups uploaded.</p>
        <?php else: ?>
            <?php foreach ($groupedBackups as $instance => $backups): ?>
                <h3><?php echo backupManageEscape($instance); ?></h3>
                <table border="1" cellpadding="6" cellspacing="0">
                    <thead>
                        <tr>
                            <th>Uploaded</th>
                            <th>Filename</th>
                            <th>Size</th>
                            <th>SHA-256</th>
                            <th>Source IP</th>
                            <th>Actions</th>
                        </tr>
                    </thead>
                    <tbody>
                        <?php foreach ($backups as $backup): ?>
                            <tr>
                                <td><?php echo backupManageEscape($backup["uploaded_at"] ?? ""); ?></td>
                                <td><?php echo backupManageEscape($backup["filename"] ?? ""); ?></td>
                                <td><?php echo backupManageEscape(backupManageFormatBytes((int) ($backup["size"] ?? 0))); ?></td>
                                <td><?php echo backupManageEscape($backup["sha256"] ?? ""); ?></td>
                                <td><?php echo backupManageEscape($backup["source_ip"] ?? ""); ?></td>
                                <td>
                                    <form method="POST" style="display:inline">
                                        <input type="hidden" name="action" value="download">
                                        <input type="hidden" name="csrf_token" value="<?php echo backupManageEscape(backupManageCsrfToken()); ?>">
                                        <input type="hidden" name="instance" value="<?php echo backupManageEscape($instance); ?>">
                                        <input type="hidden" name="filename" value="<?php echo backupManageEscape($backup["filename"] ?? ""); ?>">
                                        <button type="submit">Download</button>
                                    </form>
                                    <form method="POST" style="display:inline" onsubmit="return confirm('Delete this backup?');">
                                        <input type="hidden" name="action" value="delete">
                                        <input type="hidden" name="csrf_token" value="<?php echo backupManageEscape(backupManageCsrfToken()); ?>">
                                        <input type="hidden" name="instance" value="<?php echo backupManageEscape($instance); ?>">
                                        <input type="hidden" name="filename" value="<?php echo backupManageEscape($backup["filename"] ?? ""); ?>">
                                        <button type="submit">Delete</button>
                                    </form>
                                </td>
                            </tr>
                        <?php endforeach; ?>
                    </tbody>
                </table>
            <?php endforeach; ?>
        <?php endif; ?>
    <?php endif; ?>
</body>
</html>
