package server

import (
	"archive/zip"
	"fmt"
	"io"
	"net/http"
	"os"
	"strings"
	"sync"
	"time"

	"github.com/gin-gonic/gin"

	"warpbox/lib/boxstore"
	"warpbox/lib/helpers"
	"warpbox/lib/models"
)

const boxAuthCookiePrefix = "warpbox_box_"

var oneTimeDownloadLocks sync.Map

func formatBrowserTime(value time.Time) string {
	if value.IsZero() {
		return ""
	}
	return value.UTC().Format(time.RFC3339)
}

func (app *App) handleIndex(ctx *gin.Context) {
	ctx.HTML(http.StatusOK, "index.html", gin.H{
		"RetentionOptions": app.retentionOptions(),
		"DefaultRetention": app.defaultRetentionOption().Key,
		"UploadsEnabled":   app.config.GuestUploadsEnabled && app.config.APIEnabled,
		"MaxFileSizeBytes": app.config.GlobalMaxFileSizeBytes,
		"MaxBoxSizeBytes":  app.config.GlobalMaxBoxSizeBytes,
	})
}

func (app *App) handleShowBox(ctx *gin.Context) {
	boxID := ctx.Param("id")
	if !boxstore.ValidBoxID(boxID) {
		ctx.String(http.StatusBadRequest, "Invalid box id")
		return
	}

	manifest, hasManifest, ok := app.authorizeBoxRequest(ctx, boxID, true)
	if !ok {
		return
	}

	files, err := boxstore.ListFiles(boxID)
	if err != nil {
		ctx.String(http.StatusNotFound, "Box not found")
		return
	}

	downloadAll := "/box/" + boxID + "/download"
	if !app.config.ZipDownloadsEnabled || hasManifest && manifest.DisableZip {
		downloadAll = ""
	}

	ctx.HTML(http.StatusOK, "box.html", gin.H{
		"BoxID":          boxID,
		"Files":          files,
		"FileCount":      len(files),
		"DownloadAll":    downloadAll,
		"ZipOnly":        hasManifest && manifest.OneTimeDownload,
		"PollMS":         app.config.BoxPollIntervalMS,
		"RetentionLabel": manifest.RetentionLabel,
		"ExpiresAt":      manifest.ExpiresAt,
		"ExpiresAtISO":   formatBrowserTime(manifest.ExpiresAt),
	})
}

func handleBoxLogin(ctx *gin.Context) {
	boxID := ctx.Param("id")
	if !boxstore.ValidBoxID(boxID) {
		ctx.String(http.StatusBadRequest, "Invalid box id")
		return
	}

	manifest, err := boxstore.ReadManifest(boxID)
	if err != nil {
		ctx.String(http.StatusNotFound, "Box not found")
		return
	}

	if boxstore.IsExpired(manifest) {
		boxstore.DeleteBox(boxID)
		ctx.String(http.StatusGone, "Box expired")
		return
	}

	if !boxstore.IsPasswordProtected(manifest) || isBoxAuthorized(ctx, boxID, manifest) {
		ctx.Redirect(http.StatusSeeOther, "/box/"+boxID)
		return
	}

	renderBoxLogin(ctx, boxID, "")
}

func handleBoxLoginPost(ctx *gin.Context) {
	boxID := ctx.Param("id")
	if !boxstore.ValidBoxID(boxID) {
		ctx.String(http.StatusBadRequest, "Invalid box id")
		return
	}

	manifest, err := boxstore.ReadManifest(boxID)
	if err != nil {
		ctx.String(http.StatusNotFound, "Box not found")
		return
	}

	if boxstore.IsExpired(manifest) {
		boxstore.DeleteBox(boxID)
		ctx.String(http.StatusGone, "Box expired")
		return
	}

	if !boxstore.VerifyPassword(manifest, ctx.PostForm("password")) {
		renderBoxLogin(ctx, boxID, "The password was not accepted.")
		return
	}

	maxAge := 24 * 60 * 60
	if !manifest.ExpiresAt.IsZero() {
		seconds := int(time.Until(manifest.ExpiresAt).Seconds())
		if seconds > 0 {
			maxAge = seconds
		}
	}

	ctx.SetCookie(boxAuthCookieName(boxID), manifest.AuthToken, maxAge, "/box/"+boxID, "", false, true)
	ctx.Redirect(http.StatusSeeOther, "/box/"+boxID)
}

func (app *App) handleBoxStatus(ctx *gin.Context) {
	if !app.requireAPI(ctx) {
		return
	}

	boxID := ctx.Param("id")
	if !boxstore.ValidBoxID(boxID) {
		ctx.JSON(http.StatusBadRequest, gin.H{"error": "Invalid box id"})
		return
	}

	manifest, _, ok := app.authorizeBoxRequest(ctx, boxID, false)
	if !ok {
		return
	}

	files, err := boxstore.ListFiles(boxID)
	if err != nil {
		ctx.JSON(http.StatusNotFound, gin.H{"error": "Box not found"})
		return
	}

	ctx.JSON(http.StatusOK, gin.H{"box_id": boxID, "expires_at": formatBrowserTime(manifest.ExpiresAt), "files": files})
}

func (app *App) handleDownloadBox(ctx *gin.Context) {
	boxID := ctx.Param("id")
	if !boxstore.ValidBoxID(boxID) {
		ctx.String(http.StatusBadRequest, "Invalid box id")
		return
	}

	if !app.config.ZipDownloadsEnabled {
		ctx.String(http.StatusForbidden, "Zip downloads are disabled")
		return
	}

	manifest, hasManifest, ok := app.authorizeBoxRequest(ctx, boxID, true)
	if !ok {
		return
	}
	if hasManifest && manifest.OneTimeDownload {
		app.handleOneTimeDownloadBox(ctx, boxID)
		return
	}

	if hasManifest && manifest.DisableZip {
		ctx.String(http.StatusForbidden, "Zip download disabled for this box")
		return
	}

	files, err := boxstore.ListFiles(boxID)
	if err != nil {
		ctx.String(http.StatusNotFound, "Box not found")
		return
	}
	if !app.writeBoxZip(ctx, boxID, files) {
		return
	}
	if hasManifest && app.config.RenewOnDownloadEnabled {
		boxstore.RenewManifest(boxID, manifest.RetentionSecs)
	}
}

func (app *App) handleOneTimeDownloadBox(ctx *gin.Context, boxID string) {
	lock := oneTimeDownloadLock(boxID)
	lock.Lock()
	defer lock.Unlock()
	defer oneTimeDownloadLocks.Delete(boxID)

	manifest, hasManifest, ok := app.authorizeBoxRequest(ctx, boxID, true)
	if !ok {
		return
	}
	if !hasManifest || !manifest.OneTimeDownload {
		ctx.String(http.StatusNotFound, "Box not found")
		return
	}

	files, err := boxstore.ListFiles(boxID)
	if err != nil {
		ctx.String(http.StatusNotFound, "Box not found")
		return
	}
	if !allFilesComplete(files) {
		ctx.String(http.StatusConflict, "Box is not ready yet")
		return
	}
	if !app.writeBoxZip(ctx, boxID, files) {
		return
	}
	boxstore.DeleteBox(boxID)
}

func (app *App) writeBoxZip(ctx *gin.Context, boxID string, files []models.BoxFile) bool {
	ctx.Header("Content-Type", "application/zip")
	ctx.Header("Content-Disposition", fmt.Sprintf(`attachment; filename="warpbox-%s.zip"`, boxID))

	zipWriter := zip.NewWriter(ctx.Writer)
	zipClosed := false
	defer func() {
		if !zipClosed {
			zipWriter.Close()
		}
	}()

	for _, file := range files {
		if !file.IsComplete {
			continue
		}
		if err := boxstore.AddFileToZip(zipWriter, boxID, file.Name); err != nil {
			ctx.Status(http.StatusInternalServerError)
			return false
		}
	}

	if err := zipWriter.Close(); err != nil {
		zipClosed = true
		ctx.Status(http.StatusInternalServerError)
		return false
	}
	zipClosed = true
	return true
}

func oneTimeDownloadLock(boxID string) *sync.Mutex {
	lock, _ := oneTimeDownloadLocks.LoadOrStore(boxID, &sync.Mutex{})
	return lock.(*sync.Mutex)
}

func allFilesComplete(files []models.BoxFile) bool {
	if len(files) == 0 {
		return false
	}

	for _, file := range files {
		if !file.IsComplete {
			return false
		}
	}

	return true
}

func (app *App) handleDownloadFile(ctx *gin.Context) {
	boxID := ctx.Param("id")
	filename, ok := helpers.SafeFilename(ctx.Param("filename"))
	if !boxstore.ValidBoxID(boxID) || !ok {
		ctx.String(http.StatusBadRequest, "Invalid file")
		return
	}

	manifest, hasManifest, authorized := app.authorizeBoxRequest(ctx, boxID, true)
	if !authorized {
		return
	}
	if hasManifest && manifest.OneTimeDownload {
		ctx.String(http.StatusForbidden, "Individual downloads disabled for this box")
		return
	}

	path, ok := boxstore.SafeBoxFilePath(boxID, filename)
	if !ok {
		ctx.String(http.StatusBadRequest, "Invalid file")
		return
	}

	if _, err := os.Stat(path); err != nil {
		ctx.String(http.StatusNotFound, "File not found")
		return
	}
	if !boxstore.IsSafeRegularBoxFile(boxID, filename) {
		ctx.String(http.StatusBadRequest, "Invalid file")
		return
	}

	ctx.FileAttachment(path, filename)
	if hasManifest && app.config.RenewOnDownloadEnabled {
		boxstore.RenewManifest(boxID, manifest.RetentionSecs)
	}
}

func (app *App) handleDownloadThumbnail(ctx *gin.Context) {
	boxID := ctx.Param("id")
	fileID := ctx.Param("file_id")
	if !boxstore.ValidBoxID(boxID) {
		ctx.String(http.StatusBadRequest, "Invalid box id")
		return
	}

	if _, _, authorized := app.authorizeBoxRequest(ctx, boxID, true); !authorized {
		return
	}

	path, ok := boxstore.ThumbnailFilePath(boxID, fileID)
	if !ok {
		ctx.String(http.StatusBadRequest, "Invalid thumbnail")
		return
	}

	if _, err := os.Stat(path); err != nil {
		ctx.String(http.StatusNotFound, "Thumbnail not found")
		return
	}

	ctx.Header("Content-Type", "image/jpeg")
	ctx.File(path)
}

func (app *App) handleCreateBox(ctx *gin.Context) {
	if !app.requireAPI(ctx) || !app.requireGuestUploads(ctx) {
		return
	}
	app.limitRequestBody(ctx)

	boxID, err := boxstore.NewBoxID()
	if err != nil {
		ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Could not create upload box"})
		return
	}

	if err := os.MkdirAll(boxstore.BoxPath(boxID), 0755); err != nil {
		ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Could not prepare upload box"})
		return
	}

	var request models.CreateBoxRequest
	if err := ctx.ShouldBindJSON(&request); err != nil && err != io.EOF {
		ctx.JSON(http.StatusBadRequest, gin.H{"error": "Invalid box payload"})
		return
	}
	if err := app.validateCreateBoxRequest(&request); err != nil {
		ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	files, err := boxstore.CreateManifest(boxID, request)
	if err != nil {
		ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	ctx.JSON(http.StatusOK, gin.H{"box_id": boxID, "box_url": "/box/" + boxID, "files": files})
}

func (app *App) handleManifestFileUpload(ctx *gin.Context) {
	if !app.requireAPI(ctx) || !app.requireGuestUploads(ctx) {
		return
	}
	app.limitRequestBody(ctx)

	boxID := ctx.Param("id")
	fileID := ctx.Param("file_id")
	if !boxstore.ValidBoxID(boxID) {
		ctx.JSON(http.StatusBadRequest, gin.H{"error": "Invalid box id"})
		return
	}

	file, err := ctx.FormFile("file")
	if err != nil {
		boxstore.MarkFileStatus(boxID, fileID, models.FileStatusFailed)
		ctx.JSON(http.StatusBadRequest, gin.H{"error": "No file received"})
		return
	}
	if err := app.validateManifestFileUpload(boxID, fileID, file.Size); err != nil {
		boxstore.MarkFileStatus(boxID, fileID, models.FileStatusFailed)
		ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	savedFile, err := boxstore.SaveManifestUpload(boxID, fileID, file)
	if err != nil {
		boxstore.MarkFileStatus(boxID, fileID, models.FileStatusFailed)
		ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	ctx.JSON(http.StatusOK, gin.H{"box_id": boxID, "box_url": "/box/" + boxID, "file": savedFile})
}

func (app *App) handleFileStatusUpdate(ctx *gin.Context) {
	if !app.requireAPI(ctx) {
		return
	}
	app.limitRequestBody(ctx)

	boxID := ctx.Param("id")
	fileID := ctx.Param("file_id")
	if !boxstore.ValidBoxID(boxID) || !helpers.ValidLowerHexID(fileID, 16) {
		ctx.JSON(http.StatusBadRequest, gin.H{"error": "Invalid file"})
		return
	}

	var request models.UpdateFileStatusRequest
	if err := ctx.ShouldBindJSON(&request); err != nil {
		ctx.JSON(http.StatusBadRequest, gin.H{"error": "Invalid status payload"})
		return
	}
	if request.Status == models.FileStatusReady {
		ctx.JSON(http.StatusBadRequest, gin.H{"error": "Uploads must complete through the upload endpoint"})
		return
	}
	if err := app.rejectExpiredManifestBox(boxID); err != nil {
		ctx.JSON(http.StatusGone, gin.H{"error": err.Error()})
		return
	}

	file, err := boxstore.MarkFileStatus(boxID, fileID, request.Status)
	if err != nil {
		ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	ctx.JSON(http.StatusOK, gin.H{"file": file})
}

func (app *App) handleDirectBoxUpload(ctx *gin.Context) {
	if !app.requireAPI(ctx) || !app.requireGuestUploads(ctx) {
		return
	}
	app.limitRequestBody(ctx)

	boxID := ctx.Param("id")
	if !boxstore.ValidBoxID(boxID) {
		ctx.JSON(http.StatusBadRequest, gin.H{"error": "Invalid box id"})
		return
	}

	file, err := ctx.FormFile("file")
	if err != nil {
		ctx.JSON(http.StatusBadRequest, gin.H{"error": "No file received"})
		return
	}
	if err := app.validateIncomingFile(boxID, file.Size); err != nil {
		ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	savedFile, err := boxstore.SaveUpload(boxID, file)
	if err != nil {
		ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	ctx.JSON(http.StatusOK, gin.H{"box_id": boxID, "box_url": "/box/" + boxID, "file": savedFile})
}

func (app *App) handleLegacyUpload(ctx *gin.Context) {
	if !app.requireAPI(ctx) || !app.requireGuestUploads(ctx) {
		return
	}
	app.limitRequestBody(ctx)

	form, err := ctx.MultipartForm()
	if err != nil {
		ctx.JSON(http.StatusBadRequest, gin.H{"error": "No files received"})
		return
	}

	files := form.File["files"]
	if len(files) == 0 {
		ctx.JSON(http.StatusBadRequest, gin.H{"error": "No files received"})
		return
	}
	totalSize := int64(0)
	for _, file := range files {
		if err := app.validateFileSize(file.Size); err != nil {
			ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
			return
		}
		totalSize += file.Size
	}
	if err := app.validateBoxSize(totalSize); err != nil {
		ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	boxID, err := boxstore.NewBoxID()
	if err != nil {
		ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Could not create upload box"})
		return
	}

	if err := os.MkdirAll(boxstore.BoxPath(boxID), 0755); err != nil {
		ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Could not prepare upload box"})
		return
	}

	retentionKey := strings.TrimSpace(ctx.PostForm("retention_key"))
	if retentionKey == "" {
		retentionKey = strings.TrimSpace(ctx.PostForm("retention"))
	}
	allowZip := true
	if strings.EqualFold(strings.TrimSpace(ctx.PostForm("allow_zip")), "false") {
		allowZip = false
	}
	request := models.CreateBoxRequest{
		RetentionKey: retentionKey,
		Password:     ctx.PostForm("password"),
		AllowZip:     &allowZip,
		Files:        make([]models.CreateBoxFileRequest, 0, len(files)),
	}
	for _, file := range files {
		request.Files = append(request.Files, models.CreateBoxFileRequest{Name: file.Filename, Size: file.Size})
	}
	if err := app.validateCreateBoxRequest(&request); err != nil {
		ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	manifestFiles, err := boxstore.CreateManifest(boxID, request)
	if err != nil {
		ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	savedFiles := make([]models.BoxFile, 0, len(files))
	for index, file := range files {
		savedFile, err := boxstore.SaveManifestUpload(boxID, manifestFiles[index].ID, file)
		if err != nil {
			_, _ = boxstore.MarkFileStatus(boxID, manifestFiles[index].ID, models.FileStatusFailed)
			ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
			return
		}

		savedFiles = append(savedFiles, savedFile)
	}

	ctx.JSON(http.StatusOK, gin.H{"box_id": boxID, "box_url": "/box/" + boxID, "files": savedFiles})
}

func (app *App) authorizeBoxRequest(ctx *gin.Context, boxID string, wantsHTML bool) (models.BoxManifest, bool, bool) {
	manifest, err := boxstore.ReadManifest(boxID)
	if err != nil {
		return models.BoxManifest{}, false, true
	}

	if boxstore.IsExpired(manifest) {
		boxstore.DeleteBox(boxID)
		if wantsHTML {
			ctx.String(http.StatusGone, "Box expired")
		} else {
			ctx.JSON(http.StatusGone, gin.H{"error": "Box expired"})
		}
		return manifest, true, false
	}

	if boxstore.IsPasswordProtected(manifest) && !isBoxAuthorized(ctx, boxID, manifest) {
		if wantsHTML {
			ctx.Redirect(http.StatusSeeOther, "/box/"+boxID+"/login")
		} else {
			ctx.JSON(http.StatusUnauthorized, gin.H{"error": "Password required"})
		}
		return manifest, true, false
	}

	if app.config.RenewOnAccessEnabled {
		if renewed, err := boxstore.RenewManifest(boxID, manifest.RetentionSecs); err == nil {
			manifest = renewed
		}
	}

	return manifest, true, true
}

func isBoxAuthorized(ctx *gin.Context, boxID string, manifest models.BoxManifest) bool {
	token, err := ctx.Cookie(boxAuthCookieName(boxID))
	return err == nil && boxstore.VerifyAuthToken(manifest, token)
}

func boxAuthCookieName(boxID string) string {
	return boxAuthCookiePrefix + boxID
}

func (app *App) requireAPI(ctx *gin.Context) bool {
	if app.config.APIEnabled {
		return true
	}
	ctx.JSON(http.StatusForbidden, gin.H{"error": "API access is disabled"})
	return false
}

func (app *App) requireGuestUploads(ctx *gin.Context) bool {
	if app.config.GuestUploadsEnabled {
		return true
	}
	ctx.JSON(http.StatusForbidden, gin.H{"error": "Guest uploads are disabled"})
	return false
}

func (app *App) validateCreateBoxRequest(request *models.CreateBoxRequest) error {
	if request == nil {
		return nil
	}
	if !app.retentionAllowed(request.RetentionKey) {
		return fmt.Errorf("Retention option is not allowed")
	}
	if !app.config.ZipDownloadsEnabled {
		allowZip := false
		request.AllowZip = &allowZip
	}
	if strings.TrimSpace(request.RetentionKey) == boxstore.OneTimeDownloadRetentionKey && !app.config.OneTimeDownloadsEnabled {
		return fmt.Errorf("One-time downloads are disabled")
	}

	totalSize := int64(0)
	for _, file := range request.Files {
		if err := app.validateFileSize(file.Size); err != nil {
			return err
		}
		totalSize += file.Size
	}
	return app.validateBoxSize(totalSize)
}

func (app *App) validateIncomingFile(boxID string, size int64) error {
	if err := app.validateFileSize(size); err != nil {
		return err
	}
	if app.config.GlobalMaxBoxSizeBytes <= 0 {
		return nil
	}

	files, err := boxstore.ListFiles(boxID)
	if err != nil {
		return nil
	}
	totalSize := size
	for _, file := range files {
		totalSize += file.Size
	}
	return app.validateBoxSize(totalSize)
}

func (app *App) validateManifestFileUpload(boxID string, fileID string, size int64) error {
	if err := app.validateFileSize(size); err != nil {
		return err
	}

	manifest, err := boxstore.ReadManifest(boxID)
	if err != nil {
		return app.validateIncomingFile(boxID, size)
	}
	if boxstore.IsExpired(manifest) {
		_ = boxstore.DeleteBox(boxID)
		return fmt.Errorf("Box expired")
	}
	if app.config.GlobalMaxBoxSizeBytes <= 0 {
		return nil
	}
	totalSize := int64(0)
	found := false
	for _, file := range manifest.Files {
		if file.ID == fileID {
			totalSize += size
			found = true
			continue
		}
		totalSize += file.Size
	}
	if !found {
		totalSize += size
	}
	return app.validateBoxSize(totalSize)
}

func (app *App) validateFileSize(size int64) error {
	if size < 0 {
		return fmt.Errorf("File size cannot be negative")
	}
	if app.config.GlobalMaxFileSizeBytes > 0 && size > app.config.GlobalMaxFileSizeBytes {
		return fmt.Errorf("File exceeds the global max file size")
	}
	return nil
}

func (app *App) validateBoxSize(size int64) error {
	if size < 0 {
		return fmt.Errorf("Box size cannot be negative")
	}
	if app.config.GlobalMaxBoxSizeBytes > 0 && size > app.config.GlobalMaxBoxSizeBytes {
		return fmt.Errorf("Box exceeds the global max box size")
	}
	return nil
}

func (app *App) rejectExpiredManifestBox(boxID string) error {
	manifest, err := boxstore.ReadManifest(boxID)
	if err != nil {
		return nil
	}
	if !boxstore.IsExpired(manifest) {
		return nil
	}
	_ = boxstore.DeleteBox(boxID)
	return fmt.Errorf("Box expired")
}

func (app *App) limitRequestBody(ctx *gin.Context) {
	limit := app.maxRequestBodyBytes()
	if limit <= 0 {
		return
	}
	ctx.Request.Body = http.MaxBytesReader(ctx.Writer, ctx.Request.Body, limit)
}

func (app *App) maxRequestBodyBytes() int64 {
	limit := app.config.GlobalMaxBoxSizeBytes
	if limit <= 0 || app.config.GlobalMaxFileSizeBytes > limit {
		limit = app.config.GlobalMaxFileSizeBytes
	}
	if limit <= 0 {
		return 0
	}
	return limit + 10*1024*1024
}

func (app *App) retentionAllowed(key string) bool {
	key = strings.TrimSpace(key)
	if key == "" {
		return true
	}
	for _, option := range app.retentionOptions() {
		if option.Key == key {
			return true
		}
	}
	return false
}

func (app *App) retentionOptions() []models.RetentionOption {
	allOptions := boxstore.RetentionOptions()
	options := make([]models.RetentionOption, 0, len(allOptions))
	for _, option := range allOptions {
		if option.Key == boxstore.OneTimeDownloadRetentionKey && !app.config.OneTimeDownloadsEnabled {
			continue
		}
		if option.Seconds > 0 && app.config.MaxGuestExpirySeconds > 0 && option.Seconds > app.config.MaxGuestExpirySeconds {
			continue
		}
		options = append(options, option)
	}
	if len(options) == 0 {
		return allOptions[:1]
	}
	return options
}

func (app *App) defaultRetentionOption() models.RetentionOption {
	options := app.retentionOptions()
	for _, option := range options {
		if option.Seconds == app.config.DefaultGuestExpirySeconds {
			return option
		}
	}
	return options[0]
}

func renderBoxLogin(ctx *gin.Context, boxID string, errorMessage string) {
	ctx.HTML(http.StatusOK, "box_login.html", gin.H{
		"BoxID":        boxID,
		"BoxUser":      "WarpBox\\" + boxID,
		"ErrorMessage": errorMessage,
	})
}
