async function createBox() {
    const response = await fetch("/box", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
            retention_key: el.expiry?.value || defaultRetention,
            password: el.password?.value || "",
            allow_zip: isOneTimeDownloadSelected() || !el.allowZip || el.allowZip.checked,
            files: files.map((item) => ({ name: item.displayName, size: item.file.size })),
        }),
    });

    const result = await readJSON(response);
    if (!response.ok) throw new Error(result.error || "Could not create upload box");
    return result;
}

async function readJSON(response) {
    try {
        return await response.json();
    } catch (_) {
        return {};
    }
}

async function markFileStatus(item, status) {
    if (!item.boxID || !item.boxFile) return;
    try {
        await fetch(`/box/${item.boxID}/files/${item.boxFile.id}/status`, {
            method: "POST",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify({ status }),
        });
    } catch (_) {
        // Best effort only. The upload endpoint also marks hard failures.
    }
}

function setFileFailed(item, message) {
    item.failed = true;
    item.uploaded = false;
    item.error = message || "Failed to upload";
    item.loaded = item.file.size;
    item.row?.classList.remove("is-working", "is-uploaded");
    item.row?.classList.add("is-failed");
    if (item.row) item.row.title = item.error;
    setRowProgress(item, 100);
    updateOverallProgress();
}

function markCompletedImpact(item) {
    const key = item.boxFile?.id || item.displayName;
    if (completedImpactKeys.has(key)) return;
    completedImpactKeys.add(key);
    flashProgressBar(item.row?.querySelector(".upload-progress-bar"));
}

function uploadFile(item, onComplete) {
    return new Promise((resolve, reject) => {
        const xhr = new XMLHttpRequest();
        const formData = new FormData();
        formData.append("file", item.file, item.displayName);

        xhr.open("POST", item.boxFile.upload_path);

        xhr.upload.addEventListener("loadstart", () => {
            item.loaded = 0;
            item.failed = false;
            item.uploaded = false;
            item.row?.classList.remove("is-failed", "is-uploaded");
            item.row?.classList.add("is-working");
            setRowProgress(item, 2);
            updateOverallProgress();
        });

        xhr.upload.addEventListener("progress", (event) => {
            if (!event.lengthComputable) return;
            item.loaded = Math.min(event.loaded, item.file.size);
            const percent = (event.loaded / event.total) * 100;
            setRowProgress(item, percent >= 100 ? 99 : percent);
            updateOverallProgress();
        });

        xhr.addEventListener("load", async () => {
            if (xhr.status < 200 || xhr.status >= 300) {
                let message = "Upload failed";
                try {
                    message = JSON.parse(xhr.responseText).error || message;
                } catch (_) {}
                setFileFailed(item, message);
                await markFileStatus(item, "failed");
                reject(new Error(message));
                return;
            }

            item.uploaded = true;
            item.failed = false;
            item.loaded = item.file.size;
            item.row?.classList.remove("is-working", "is-failed");
            item.row?.classList.add("is-uploaded");
            if (item.row) item.row.title = "Uploaded";
            setRowProgress(item, 100);
            markCompletedImpact(item);

            try {
                const result = JSON.parse(xhr.responseText);
                if (result.file) {
                    item.boxFile = result.file;
                    const icon = item.row?.querySelector(".upload-file-icon");
                    if (icon && result.file.thumbnail_path) {
                        item.row.classList.add("has-thumbnail");
                        icon.src = result.file.thumbnail_path;
                    } else if (icon && result.file.icon_path && !item.previewURL) {
                        icon.src = result.file.icon_path;
                    }
                }
            } catch (_) {}

            updateOverallProgress();
            onComplete();
            resolve();
        });

        xhr.addEventListener("error", async () => {
            setFileFailed(item, "Network error while uploading");
            await markFileStatus(item, "failed");
            reject(new Error("Network error while uploading"));
        });

        xhr.addEventListener("abort", async () => {
            setFileFailed(item, "Upload cancelled");
            await markFileStatus(item, "failed");
            reject(new Error("Upload cancelled"));
        });

        markFileStatus(item, "uploading");
        xhr.send(formData);
    });
}
