package drive

import (
	"errors"
	"io"
	"log/slog"
	"net/http"
	"strconv"
	"strings"

	"github.com/go-chi/chi/v5"
	"github.com/jackc/pgx/v5/pgxpool"

	"github.com/ultisuite/ulti-backend/internal/api/apiresponse"
	"github.com/ultisuite/ulti-backend/internal/api/apivalidate"
	"github.com/ultisuite/ulti-backend/internal/api/middleware"
	"github.com/ultisuite/ulti-backend/internal/api/query"
	"github.com/ultisuite/ulti-backend/internal/auth"
	"github.com/ultisuite/ulti-backend/internal/mail/rules"
	"github.com/ultisuite/ulti-backend/internal/nextcloud"
	"github.com/ultisuite/ulti-backend/internal/permission"
	"github.com/ultisuite/ulti-backend/internal/realtime"
)

type Handler struct {
	svc          *Service
	publicOffice PublicOfficeAPI
	logger       *slog.Logger
}

func NewHandler(nc *nextcloud.Client, hub *realtime.Hub, db *pgxpool.Pool) *Handler {
	return NewHandlerWithService(NewService(nc, hub, db))
}

func NewHandlerWithService(svc *Service) *Handler {
	return &Handler{
		svc:    svc,
		logger: slog.Default().With("component", "drive-api"),
	}
}

func (h *Handler) SetPublicOffice(api PublicOfficeAPI) {
	h.publicOffice = api
}

func (h *Handler) nextcloudUser(w http.ResponseWriter, r *http.Request, claims *auth.Claims) (string, bool) {
	userID, err := h.svc.EnsureNextcloudUser(r.Context(), claims)
	if err != nil {
		h.logger.Error("ensure nextcloud user", "error", err, "sub", claims.Sub, "email", claims.Email)
		apivalidate.WriteInternal(w, r)
		return "", false
	}
	return userID, true
}

func (h *Handler) Routes() chi.Router {
	r := chi.NewRouter()
	read := middleware.RequirePermission(permission.ResourceDrive, permission.LevelRead)
	write := middleware.RequirePermission(permission.ResourceDrive, permission.LevelWrite)

	r.With(read).Get("/quota", h.GetQuota)
	r.With(read).Get("/trash", h.ListTrash)
	r.With(read).Get("/recent", h.ListRecent)
	r.With(read).Get("/starred", h.ListStarred)
	r.With(read).Get("/starred/*", h.ListStarred)
	r.With(read).Get("/shared", h.ListSharedWithMe)
	r.With(read).Get("/search", h.Search)
	r.With(read).Get("/filter-corpus", h.ListFilterCorpus)
	r.With(read).Get("/filter-corpus/*", h.ListFilterCorpus)
	r.With(read).Get("/shares", h.ListShares)
	r.With(read).Get("/shares/recipients/lookup", h.LookupShareRecipient)
	r.With(read).Get("/download/*", h.Download)
	r.With(read).Get("/preview/*", h.Preview)
	r.With(read).Get("/files/*", h.ListFiles)
	r.With(write).Post("/files/*", h.Upload)
	r.With(write).Post("/files/new", h.CreateNewFile)
	r.With(write).Delete("/files/*", h.DeleteFile)
	r.With(write).Post("/folders/*", h.CreateFolder)
	r.With(write).Post("/move", h.Move)
	r.With(write).Post("/copy", h.Copy)
	r.With(write).Post("/rename", h.Rename)
	r.With(write).Post("/trash/restore", h.RestoreTrash)
	r.With(write).Post("/trash/delete", h.DeleteTrash)
	r.With(write).Delete("/trash", h.EmptyTrash)
	r.With(write).Post("/favorite", h.SetFavorite)
	r.With(write).Post("/shares", h.CreateShare)
	r.With(write).Post("/shares/{shareID}/send-email", h.SendShareEmail)
	r.With(write).Put("/shares/{shareID}", h.UpdateShare)
	r.With(write).Delete("/shares/{shareID}", h.DeleteShare)

	return r
}

func (h *Handler) ListFiles(w http.ResponseWriter, r *http.Request) {
	claims := middleware.ClaimsFromContext(r.Context())
	ncUser, ok := h.nextcloudUser(w, r, claims)
	if !ok {
		return
	}
	params, err := query.ParseListRequest(r)
	if err != nil {
		apivalidate.WriteQueryError(w, r, err)
		return
	}

	path := nextcloud.NormalizeClientPath(chi.URLParam(r, "*"))
	result, err := h.svc.ListFiles(r.Context(), ncUser, path, params)
	if err != nil {
		h.logger.Error("list files", "error", err)
		apivalidate.WriteInternal(w, r)
		return
	}
	h.svc.EnrichSources(r.Context(), claims.Sub, result.Files)
	apiresponse.WriteJSON(w, http.StatusOK, result)
}

func (h *Handler) ListFilterCorpus(w http.ResponseWriter, r *http.Request) {
	claims := middleware.ClaimsFromContext(r.Context())
	ncUser, ok := h.nextcloudUser(w, r, claims)
	if !ok {
		return
	}

	path := nextcloud.NormalizeClientPath(chi.URLParam(r, "*"))
	result, err := h.svc.ListFilterCorpus(r.Context(), ncUser, path)
	if err != nil {
		h.logger.Error("list filter corpus", "error", err, "path", path)
		apivalidate.WriteInternal(w, r)
		return
	}
	h.svc.EnrichSources(r.Context(), claims.Sub, result.Files)
	apiresponse.WriteJSON(w, http.StatusOK, result)
}

func (h *Handler) Upload(w http.ResponseWriter, r *http.Request) {
	claims := middleware.ClaimsFromContext(r.Context())
	ncUser, ok := h.nextcloudUser(w, r, claims)
	if !ok {
		return
	}
	path := chi.URLParam(r, "*")
	if verr := validatePath(path); verr != nil {
		apivalidate.WriteValidationError(w, r, verr)
		return
	}

	if chunk, ok, verr := chunkUploadFromHeaders(r); verr != nil {
		apivalidate.WriteValidationError(w, r, verr)
		return
	} else if ok {
		if err := h.svc.UploadChunk(r.Context(), ncUser, chunk.UploadID, path, chunk.ChunkUpload, r.Body, r.Header.Get("Content-Type")); err != nil {
			h.logger.Error("upload chunk", "error", err)
			writeDriveError(w, r, err)
			return
		}
		status := http.StatusAccepted
		message := "chunk_uploaded"
		if chunk.Complete {
			status = http.StatusCreated
			message = "uploaded"
		}
		apiresponse.WriteJSON(w, status, map[string]any{
			"status":    message,
			"path":      path,
			"upload_id": chunk.UploadID,
			"index":     chunk.Index,
			"total":     chunk.Total,
			"complete":  chunk.Complete,
		})
		return
	}

	if err := h.svc.Upload(r.Context(), ncUser, path, r.Body, r.Header.Get("Content-Type"), r.ContentLength); err != nil {
		h.logger.Error("upload", "error", err)
		writeDriveError(w, r, err)
		return
	}
	h.svc.afterDriveFileEvent(r.Context(), claims.Sub, rules.TriggerDriveFileCreated, path, false)
	apiresponse.WriteJSON(w, http.StatusCreated, map[string]string{"status": "uploaded", "path": path})
}

func (h *Handler) Download(w http.ResponseWriter, r *http.Request) {
	claims := middleware.ClaimsFromContext(r.Context())
	ncUser, ok := h.nextcloudUser(w, r, claims)
	if !ok {
		return
	}
	path := chi.URLParam(r, "*")
	if verr := validatePath(path); verr != nil {
		apivalidate.WriteValidationError(w, r, verr)
		return
	}

	body, contentType, err := h.svc.Download(r.Context(), ncUser, path)
	if err != nil {
		writeDriveError(w, r, err)
		return
	}
	defer body.Close()

	w.Header().Set("Content-Type", contentType)
	io.Copy(w, body)
}

func (h *Handler) Preview(w http.ResponseWriter, r *http.Request) {
	claims := middleware.ClaimsFromContext(r.Context())
	ncUser, ok := h.nextcloudUser(w, r, claims)
	if !ok {
		return
	}
	path := chi.URLParam(r, "*")
	if verr := validatePath(path); verr != nil {
		apivalidate.WriteValidationError(w, r, verr)
		return
	}

	width, _ := strconv.Atoi(r.URL.Query().Get("w"))
	height, _ := strconv.Atoi(r.URL.Query().Get("h"))

	body, contentType, err := h.svc.Preview(r.Context(), ncUser, path, width, height)
	if err != nil {
		writeDriveError(w, r, err)
		return
	}
	defer body.Close()

	w.Header().Set("Content-Type", contentType)
	w.Header().Set("Cache-Control", "private, max-age=300")
	io.Copy(w, body)
}

func (h *Handler) DeleteFile(w http.ResponseWriter, r *http.Request) {
	claims := middleware.ClaimsFromContext(r.Context())
	ncUser, ok := h.nextcloudUser(w, r, claims)
	if !ok {
		return
	}
	path := chi.URLParam(r, "*")
	if verr := validatePath(path); verr != nil {
		apivalidate.WriteValidationError(w, r, verr)
		return
	}

	if err := h.svc.Delete(r.Context(), ncUser, path); err != nil {
		h.logger.Error("delete file", "error", err)
		writeDriveError(w, r, err)
		return
	}
	h.svc.afterDriveFileEvent(r.Context(), claims.Sub, rules.TriggerDriveFileDeleted, path, false)
	w.WriteHeader(http.StatusNoContent)
}

func (h *Handler) CreateFolder(w http.ResponseWriter, r *http.Request) {
	claims := middleware.ClaimsFromContext(r.Context())
	ncUser, ok := h.nextcloudUser(w, r, claims)
	if !ok {
		return
	}
	path := chi.URLParam(r, "*")
	if verr := validatePath(path); verr != nil {
		apivalidate.WriteValidationError(w, r, verr)
		return
	}

	if err := h.svc.CreateFolder(r.Context(), ncUser, path); err != nil {
		h.logger.Error("create folder", "error", err)
		writeDriveError(w, r, err)
		return
	}
	h.svc.afterDriveFileEvent(r.Context(), claims.Sub, rules.TriggerDriveFileCreated, path, true)
	w.WriteHeader(http.StatusCreated)
}

func (h *Handler) Move(w http.ResponseWriter, r *http.Request) {
	claims := middleware.ClaimsFromContext(r.Context())
	ncUser, ok := h.nextcloudUser(w, r, claims)
	if !ok {
		return
	}

	var req moveRequest
	if err := apivalidate.DecodeJSON(w, r, maxJSONRequestBody, &req); err != nil {
		return
	}
	if verr := validateMoveRequest(&req); verr != nil {
		apivalidate.WriteValidationError(w, r, verr)
		return
	}
	if middleware.DenyIfDrivePathOutOfScope(w, r, req.Source, req.Destination) {
		return
	}

	if err := h.svc.Move(r.Context(), ncUser, req.Source, req.Destination); err != nil {
		h.logger.Error("move", "error", err)
		writeDriveError(w, r, err)
		return
	}
	h.svc.afterDriveFileEvent(r.Context(), claims.Sub, rules.TriggerDriveFileMoved, req.Destination, false)
	w.WriteHeader(http.StatusNoContent)
}

func (h *Handler) Copy(w http.ResponseWriter, r *http.Request) {
	claims := middleware.ClaimsFromContext(r.Context())
	ncUser, ok := h.nextcloudUser(w, r, claims)
	if !ok {
		return
	}

	var req copyRequest
	if err := apivalidate.DecodeJSON(w, r, maxJSONRequestBody, &req); err != nil {
		return
	}
	if verr := validateCopyRequest(&req); verr != nil {
		apivalidate.WriteValidationError(w, r, verr)
		return
	}
	if middleware.DenyIfDrivePathOutOfScope(w, r, req.Source, req.Destination) {
		return
	}

	if err := h.svc.Copy(r.Context(), ncUser, req.Source, req.Destination); err != nil {
		h.logger.Error("copy", "error", err)
		writeDriveError(w, r, err)
		return
	}
	h.svc.afterDriveFileEvent(r.Context(), claims.Sub, rules.TriggerDriveFileCreated, req.Destination, false)
	w.WriteHeader(http.StatusNoContent)
}

func (h *Handler) Rename(w http.ResponseWriter, r *http.Request) {
	claims := middleware.ClaimsFromContext(r.Context())
	ncUser, ok := h.nextcloudUser(w, r, claims)
	if !ok {
		return
	}

	var req renameRequest
	if err := apivalidate.DecodeJSON(w, r, maxJSONRequestBody, &req); err != nil {
		return
	}
	if verr := validateRenameRequest(&req); verr != nil {
		apivalidate.WriteValidationError(w, r, verr)
		return
	}
	if middleware.DenyIfDrivePathOutOfScope(w, r, req.Path) {
		return
	}

	if err := h.svc.Rename(r.Context(), ncUser, req.Path, req.NewName); err != nil {
		h.logger.Error("rename", "error", err)
		writeDriveError(w, r, err)
		return
	}
	h.svc.afterDriveFileEvent(r.Context(), claims.Sub, rules.TriggerDriveFileUpdated, renamedPath(req.Path, req.NewName), false)
	w.WriteHeader(http.StatusNoContent)
}

func (h *Handler) ListTrash(w http.ResponseWriter, r *http.Request) {
	claims := middleware.ClaimsFromContext(r.Context())
	ncUser, ok := h.nextcloudUser(w, r, claims)
	if !ok {
		return
	}
	params, err := query.ParseListRequest(r)
	if err != nil {
		apivalidate.WriteQueryError(w, r, err)
		return
	}
	result, err := h.svc.ListTrash(r.Context(), ncUser, params)
	if err != nil {
		h.logger.Error("list trash", "error", err)
		writeDriveError(w, r, err)
		return
	}
	h.svc.EnrichSources(r.Context(), claims.Sub, result.Files)
	apiresponse.WriteJSON(w, http.StatusOK, result)
}

func (h *Handler) ListRecent(w http.ResponseWriter, r *http.Request) {
	claims := middleware.ClaimsFromContext(r.Context())
	ncUser, ok := h.nextcloudUser(w, r, claims)
	if !ok {
		return
	}
	params, err := query.ParseListRequest(r)
	if err != nil {
		apivalidate.WriteQueryError(w, r, err)
		return
	}
	result, err := h.svc.ListRecent(r.Context(), ncUser, params)
	if err != nil {
		h.logger.Error("list recent", "error", err)
		writeDriveError(w, r, err)
		return
	}
	h.svc.EnrichSources(r.Context(), claims.Sub, result.Files)
	apiresponse.WriteJSON(w, http.StatusOK, result)
}

func (h *Handler) ListStarred(w http.ResponseWriter, r *http.Request) {
	claims := middleware.ClaimsFromContext(r.Context())
	ncUser, ok := h.nextcloudUser(w, r, claims)
	if !ok {
		return
	}
	params, err := query.ParseListRequest(r)
	if err != nil {
		apivalidate.WriteQueryError(w, r, err)
		return
	}
	basePath := strings.TrimPrefix(chi.URLParam(r, "*"), "/")
	result, err := h.svc.ListStarred(r.Context(), ncUser, basePath, params)
	if err != nil {
		h.logger.Error("list starred", "error", err)
		writeDriveError(w, r, err)
		return
	}
	h.svc.EnrichSources(r.Context(), claims.Sub, result.Files)
	apiresponse.WriteJSON(w, http.StatusOK, result)
}

func (h *Handler) ListSharedWithMe(w http.ResponseWriter, r *http.Request) {
	claims := middleware.ClaimsFromContext(r.Context())
	ncUser, ok := h.nextcloudUser(w, r, claims)
	if !ok {
		return
	}
	params, err := query.ParseListRequest(r)
	if err != nil {
		apivalidate.WriteQueryError(w, r, err)
		return
	}
	result, err := h.svc.ListSharedWithMe(r.Context(), ncUser, params)
	if err != nil {
		h.logger.Error("list shared with me", "error", err)
		writeDriveError(w, r, err)
		return
	}
	h.svc.EnrichSources(r.Context(), claims.Sub, result.Files)
	apiresponse.WriteJSON(w, http.StatusOK, result)
}

func (h *Handler) CreateShare(w http.ResponseWriter, r *http.Request) {
	claims := middleware.ClaimsFromContext(r.Context())
	ncUser, ok := h.nextcloudUser(w, r, claims)
	if !ok {
		return
	}

	var req createShareRequest
	if err := apivalidate.DecodeJSON(w, r, maxJSONRequestBody, &req); err != nil {
		return
	}
	if verr := validateCreateShareRequest(&req); verr != nil {
		apivalidate.WriteValidationError(w, r, verr)
		return
	}

	permissions := req.Permissions
	if permissions == 0 && strings.TrimSpace(req.Role) != "" {
		if mapped, ok := sharePermissionsForRole(req.Role); ok {
			permissions = mapped
		}
	}

	share, err := h.svc.CreateShare(r.Context(), ncUser, req.Path, req, permissions)
	if err != nil {
		h.logger.Error("create share", "error", err)
		writeDriveError(w, r, err)
		return
	}
	h.svc.afterDriveShareEvent(r.Context(), claims.Sub, req.Path)
	apiresponse.WriteJSON(w, http.StatusCreated, share)
}

func (h *Handler) GetQuota(w http.ResponseWriter, r *http.Request) {
	claims := middleware.ClaimsFromContext(r.Context())
	ncUser, ok := h.nextcloudUser(w, r, claims)
	if !ok {
		return
	}
	quota, err := h.svc.GetQuota(r.Context(), ncUser)
	if err != nil {
		h.logger.Error("get quota", "error", err, "nc_user", ncUser)
		writeDriveError(w, r, err)
		return
	}
	apiresponse.WriteJSON(w, http.StatusOK, quota)
}

func (h *Handler) Search(w http.ResponseWriter, r *http.Request) {
	claims := middleware.ClaimsFromContext(r.Context())
	ncUser, ok := h.nextcloudUser(w, r, claims)
	if !ok {
		return
	}
	params, err := query.ParseListRequest(r)
	if err != nil {
		apivalidate.WriteQueryError(w, r, err)
		return
	}
	result, err := h.svc.Search(r.Context(), ncUser, SearchOptions{
		Scope:    r.URL.Query().Get("scope"),
		BasePath: r.URL.Query().Get("path"),
		Suggest:  r.URL.Query().Get("suggest") == "1",
	}, params)
	if err != nil {
		writeDriveError(w, r, err)
		return
	}
	h.svc.EnrichSources(r.Context(), claims.Sub, result.Files)
	apiresponse.WriteJSON(w, http.StatusOK, result)
}

func (h *Handler) ListShares(w http.ResponseWriter, r *http.Request) {
	claims := middleware.ClaimsFromContext(r.Context())
	ncUser, ok := h.nextcloudUser(w, r, claims)
	if !ok {
		return
	}
	filePath := r.URL.Query().Get("path")
	if filePath == "" {
		apivalidate.WriteValidationError(w, r, apivalidate.NewValidationError(
			apivalidate.FieldDetail{Field: "path", Message: "required"},
		))
		return
	}
	shares, err := h.svc.ListShares(r.Context(), ncUser, filePath)
	if err != nil {
		writeDriveError(w, r, err)
		return
	}
	apiresponse.WriteJSON(w, http.StatusOK, map[string]any{"shares": shares})
}

func (h *Handler) LookupShareRecipient(w http.ResponseWriter, r *http.Request) {
	claims := middleware.ClaimsFromContext(r.Context())
	ncUser, ok := h.nextcloudUser(w, r, claims)
	if !ok {
		return
	}
	_ = ncUser
	email := strings.TrimSpace(strings.ToLower(r.URL.Query().Get("email")))
	if email == "" {
		apivalidate.WriteValidationError(w, r, apivalidate.NewValidationError(
			apivalidate.FieldDetail{Field: "email", Message: "required"},
		))
		return
	}
	registered, err := h.svc.UserExists(r.Context(), email)
	if err != nil {
		h.logger.Error("lookup share recipient", "error", err, "email", email)
		writeDriveError(w, r, err)
		return
	}
	apiresponse.WriteJSON(w, http.StatusOK, map[string]any{
		"email":      email,
		"registered": registered,
	})
}

func (h *Handler) SendShareEmail(w http.ResponseWriter, r *http.Request) {
	claims := middleware.ClaimsFromContext(r.Context())
	ncUser, ok := h.nextcloudUser(w, r, claims)
	if !ok {
		return
	}
	shareID := chi.URLParam(r, "shareID")
	var req struct {
		Password string `json:"password"`
	}
	if err := apivalidate.DecodeJSON(w, r, maxJSONRequestBody, &req); err != nil {
		return
	}
	if err := h.svc.SendShareEmail(r.Context(), ncUser, shareID, req.Password); err != nil {
		h.logger.Error("send share email", "error", err, "share_id", shareID)
		writeDriveError(w, r, err)
		return
	}
	w.WriteHeader(http.StatusNoContent)
}

func (h *Handler) UpdateShare(w http.ResponseWriter, r *http.Request) {
	claims := middleware.ClaimsFromContext(r.Context())
	ncUser, ok := h.nextcloudUser(w, r, claims)
	if !ok {
		return
	}
	shareID := chi.URLParam(r, "shareID")
	var req updateShareRequest
	if err := apivalidate.DecodeJSON(w, r, maxJSONRequestBody, &req); err != nil {
		return
	}
	permissions := req.Permissions
	if permissions == 0 && strings.TrimSpace(req.Role) != "" {
		if mapped, ok := sharePermissionsForRole(req.Role); ok {
			permissions = mapped
		}
	}
	share, err := h.svc.UpdateShare(r.Context(), ncUser, shareID, permissions, req.ExpireDate, req.Password)
	if err != nil {
		writeDriveError(w, r, err)
		return
	}
	h.svc.afterDriveShareEvent(r.Context(), claims.Sub, share.Path)
	apiresponse.WriteJSON(w, http.StatusOK, share)
}

func (h *Handler) DeleteShare(w http.ResponseWriter, r *http.Request) {
	claims := middleware.ClaimsFromContext(r.Context())
	ncUser, ok := h.nextcloudUser(w, r, claims)
	if !ok {
		return
	}
	shareID := chi.URLParam(r, "shareID")
	if err := h.svc.DeleteShare(r.Context(), ncUser, shareID); err != nil {
		writeDriveError(w, r, err)
		return
	}
	h.svc.afterDriveShareEvent(r.Context(), claims.Sub, "")
	w.WriteHeader(http.StatusNoContent)
}

func (h *Handler) RestoreTrash(w http.ResponseWriter, r *http.Request) {
	claims := middleware.ClaimsFromContext(r.Context())
	ncUser, ok := h.nextcloudUser(w, r, claims)
	if !ok {
		return
	}
	var req restoreTrashRequest
	if err := apivalidate.DecodeJSON(w, r, maxJSONRequestBody, &req); err != nil {
		return
	}
	if verr := validateRestoreTrashRequest(&req); verr != nil {
		apivalidate.WriteValidationError(w, r, verr)
		return
	}
	if err := h.svc.RestoreTrash(r.Context(), ncUser, req.Name); err != nil {
		writeDriveError(w, r, err)
		return
	}
	h.svc.afterDriveFileEvent(r.Context(), claims.Sub, rules.TriggerDriveFileCreated, req.Name, false)
	w.WriteHeader(http.StatusNoContent)
}

func (h *Handler) DeleteTrash(w http.ResponseWriter, r *http.Request) {
	claims := middleware.ClaimsFromContext(r.Context())
	ncUser, ok := h.nextcloudUser(w, r, claims)
	if !ok {
		return
	}
	var req deleteTrashRequest
	if err := apivalidate.DecodeJSON(w, r, maxJSONRequestBody, &req); err != nil {
		return
	}
	if verr := validateDeleteTrashRequest(&req); verr != nil {
		apivalidate.WriteValidationError(w, r, verr)
		return
	}
	if err := h.svc.DeleteTrash(r.Context(), ncUser, req.Name); err != nil {
		writeDriveError(w, r, err)
		return
	}
	w.WriteHeader(http.StatusNoContent)
}

func (h *Handler) EmptyTrash(w http.ResponseWriter, r *http.Request) {
	claims := middleware.ClaimsFromContext(r.Context())
	ncUser, ok := h.nextcloudUser(w, r, claims)
	if !ok {
		return
	}
	if err := h.svc.EmptyTrash(r.Context(), ncUser); err != nil {
		writeDriveError(w, r, err)
		return
	}
	w.WriteHeader(http.StatusNoContent)
}

func (h *Handler) SetFavorite(w http.ResponseWriter, r *http.Request) {
	claims := middleware.ClaimsFromContext(r.Context())
	ncUser, ok := h.nextcloudUser(w, r, claims)
	if !ok {
		return
	}
	var req favoriteRequest
	if err := apivalidate.DecodeJSON(w, r, maxJSONRequestBody, &req); err != nil {
		return
	}
	if verr := validateFavoriteRequest(&req); verr != nil {
		apivalidate.WriteValidationError(w, r, verr)
		return
	}
	if err := h.svc.SetFavorite(r.Context(), ncUser, req.Path, req.Favorite); err != nil {
		writeDriveError(w, r, err)
		return
	}
	h.svc.afterDriveFileEvent(r.Context(), claims.Sub, rules.TriggerDriveFileUpdated, req.Path, false)
	w.WriteHeader(http.StatusNoContent)
}

func (h *Handler) CreateNewFile(w http.ResponseWriter, r *http.Request) {
	claims := middleware.ClaimsFromContext(r.Context())
	ncUser, ok := h.nextcloudUser(w, r, claims)
	if !ok {
		return
	}
	var req newFileRequest
	if err := apivalidate.DecodeJSON(w, r, maxJSONRequestBody, &req); err != nil {
		return
	}
	if verr := validateNewFileRequest(&req); verr != nil {
		apivalidate.WriteValidationError(w, r, verr)
		return
	}
	kind := NewFileKind(strings.TrimSpace(strings.ToLower(req.Kind)))
	target, err := h.svc.CreateNewFile(r.Context(), ncUser, req.ParentPath, req.Name, kind)
	if err != nil {
		writeDriveError(w, r, err)
		return
	}
	h.svc.afterDriveFileEvent(r.Context(), claims.Sub, rules.TriggerDriveFileCreated, target, false)
	apiresponse.WriteJSON(w, http.StatusCreated, map[string]string{"path": target})
}

func writeDriveError(w http.ResponseWriter, r *http.Request, err error) {
	switch {
	case errors.Is(err, ErrNotFound):
		apivalidate.WriteNotFound(w, r, "not found")
	case errors.Is(err, ErrConflict):
		apiresponse.WriteError(w, r, http.StatusConflict, "drive.conflict", "resource conflict", nil)
	case errors.Is(err, ErrForbidden):
		apiresponse.WriteError(w, r, http.StatusForbidden, apiresponse.CodeAuthForbidden, "forbidden", nil)
	case errors.Is(err, ErrQuotaExceeded):
		apiresponse.WriteError(w, r, http.StatusInsufficientStorage, "drive.quota_exceeded", "quota exceeded", nil)
	case errors.Is(err, ErrMalware):
		apiresponse.WriteError(w, r, http.StatusUnprocessableEntity, "drive.malware_detected", "malware detected in file", nil)
	case errors.Is(err, ErrInvalid):
		apiresponse.WriteError(w, r, http.StatusBadRequest, apiresponse.CodeInvalidRequest, "invalid request body", nil)
	default:
		apivalidate.WriteInternal(w, r)
	}
}

type chunkRequest struct {
	UploadID string
	ChunkUpload
}

func chunkUploadFromHeaders(r *http.Request) (chunkRequest, bool, *apivalidate.ValidationError) {
	uploadID := strings.TrimSpace(r.Header.Get("X-Upload-ID"))
	if uploadID == "" {
		return chunkRequest{}, false, nil
	}
	var details []apivalidate.FieldDetail

	index, err := strconv.Atoi(strings.TrimSpace(r.Header.Get("X-Chunk-Index")))
	if err != nil || index < 0 {
		details = append(details, apivalidate.FieldDetail{Field: "X-Chunk-Index", Message: "required positive integer"})
	}
	total, err := strconv.Atoi(strings.TrimSpace(r.Header.Get("X-Chunk-Total")))
	if err != nil || total <= 0 {
		details = append(details, apivalidate.FieldDetail{Field: "X-Chunk-Total", Message: "required positive integer"})
	}
	totalSize := int64(-1)
	if raw := strings.TrimSpace(r.Header.Get("X-Upload-Total-Size")); raw != "" {
		parsed, parseErr := strconv.ParseInt(raw, 10, 64)
		if parseErr != nil || parsed < 0 {
			details = append(details, apivalidate.FieldDetail{Field: "X-Upload-Total-Size", Message: "invalid"})
		} else {
			totalSize = parsed
		}
	}
	complete := strings.EqualFold(strings.TrimSpace(r.Header.Get("X-Upload-Complete")), "true")
	if total > 0 && index == total-1 {
		complete = true
	}

	if len(details) > 0 {
		return chunkRequest{}, true, apivalidate.NewValidationError(details...)
	}
	return chunkRequest{
		UploadID: uploadID,
		ChunkUpload: ChunkUpload{
			Index:     index,
			Total:     total,
			TotalSize: totalSize,
			Complete:  complete,
		},
	}, true, nil
}
