package weed_server

import (
	"bytes"
	"context"
	"encoding/base64"
	"errors"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"path"
	"strconv"
	"strings"

	"github.com/google/uuid"
	"github.com/seaweedfs/seaweedfs/weed/glog"
	"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
	"github.com/seaweedfs/seaweedfs/weed/security"
	"github.com/seaweedfs/seaweedfs/weed/stats"
	"github.com/seaweedfs/seaweedfs/weed/util"
)

// tusHandler is the main entry point for TUS protocol requests
func (fs *FilerServer) tusHandler(w http.ResponseWriter, r *http.Request) {
	// Set common TUS response headers
	w.Header().Set("Tus-Resumable", TusVersion)

	// OPTIONS is capability discovery only and carries no data, so it is left
	// unauthenticated like the main filer OPTIONS handler. Every other TUS method
	// authenticates the credential first, then authorizes the server-stored
	// TargetPath below, once routing has resolved which resource it acts on.
	var claims *security.SeaweedFilerClaims
	if r.Method != http.MethodOptions {
		tusVersion := r.Header.Get("Tus-Resumable")
		if tusVersion != TusVersion {
			http.Error(w, "Unsupported TUS version", http.StatusPreconditionFailed)
			return
		}

		var authenticated bool
		if claims, authenticated = fs.authenticateFilerJwt(r, r.Method != http.MethodHead); !authenticated {
			writeJsonError(w, r, http.StatusUnauthorized, errors.New("wrong jwt"))
			return
		}
	}

	// Route based on method and path
	reqPath := r.URL.Path
	// TusBasePath is pre-normalized in filer_server.go (leading slash, no trailing slash)
	tusPrefix := fs.option.TusBasePath

	// Check if this is an upload location (contains upload ID after {tusPrefix}/.uploads/)
	uploadsPrefix := tusPrefix + "/.uploads/"
	if strings.HasPrefix(reqPath, uploadsPrefix) {
		// Session ids this server mints are canonical UUIDs. Rejecting aliases
		// (a trailing path or any non-canonical spelling) keeps one URL bound to
		// one stored authorization resource.
		uploadID := strings.TrimPrefix(reqPath, uploadsPrefix)
		if !isCanonicalTusUploadID(uploadID) {
			writeTusSessionNotFound(w, r.Method)
			return
		}

		switch r.Method {
		case http.MethodHead, http.MethodPatch, http.MethodDelete:
		default:
			w.WriteHeader(http.StatusMethodNotAllowed)
			return
		}

		ctx := r.Context()
		if r.Method == http.MethodPatch {
			ctx = context.WithoutCancel(ctx)
		}
		session, err := fs.readTusSessionInfo(ctx, uploadID)
		if err != nil {
			// A transient filer error resolves to "not found"; log it so it is
			// distinguishable from a genuinely missing session.
			glog.V(1).Infof("TUS session %s not resolved: %v", uploadID, err)
			writeTusSessionNotFound(w, r.Method)
			return
		}
		if !authorizeFilerJwtPaths(r, claims, []string{session.TargetPath}) {
			writeJsonError(w, r, http.StatusUnauthorized, errors.New("wrong jwt"))
			return
		}
		// One mutating request per session at a time, like tusd: a PATCH retried
		// while its predecessor is still storing a sub-chunk would otherwise
		// record the same range twice, and a DELETE would race the writer. The
		// chunk state is loaded under this claim so the offset check sees every
		// record the previous request left behind. 423 tells the client to retry.
		if r.Method != http.MethodHead {
			if !fs.lockTusUpload(uploadID) {
				http.Error(w, "Upload is locked by another request", http.StatusLocked)
				return
			}
			defer fs.unlockTusUpload(uploadID)
		}
		if err := fs.loadTusSessionChunks(ctx, session); err != nil {
			glog.Errorf("Failed to load TUS session %s chunks: %v", uploadID, err)
			if !errors.Is(err, filer_pb.ErrNotFound) {
				http.Error(w, "Failed to load upload state", http.StatusInternalServerError)
				return
			}
			writeTusSessionNotFound(w, r.Method)
			return
		}

		switch r.Method {
		case http.MethodHead:
			fs.tusHeadHandler(w, session)
		case http.MethodPatch:
			fs.tusPatchHandler(w, r, session)
		case http.MethodDelete:
			fs.tusDeleteHandler(w, r, session)
		}
		return
	}

	// Handle creation endpoints (POST to /.tus/{path})
	switch r.Method {
	case http.MethodOptions:
		fs.tusOptionsHandler(w, r)
	case http.MethodPost:
		if !authorizeFilerJwtPaths(r, claims, []string{fs.tusTargetPath(r)}) {
			writeJsonError(w, r, http.StatusUnauthorized, errors.New("wrong jwt"))
			return
		}
		fs.tusCreateHandler(w, r, claims)
	default:
		w.WriteHeader(http.StatusMethodNotAllowed)
	}
}

// lockTusUpload claims a session for one mutating request; it reports false
// while another request holds the claim.
func (fs *FilerServer) lockTusUpload(uploadID string) bool {
	_, loaded := fs.tusActiveUploads.LoadOrStore(uploadID, struct{}{})
	return !loaded
}

func (fs *FilerServer) unlockTusUpload(uploadID string) {
	fs.tusActiveUploads.Delete(uploadID)
}

// writeTusSessionNotFound answers a request whose session cannot be resolved.
// DELETE is idempotent and returns 204 for a missing session; other verbs 404.
func writeTusSessionNotFound(w http.ResponseWriter, method string) {
	if method == http.MethodDelete {
		w.WriteHeader(http.StatusNoContent)
		return
	}
	http.Error(w, "Upload not found", http.StatusNotFound)
}

// isCanonicalTusUploadID reports whether uploadID is a canonical UUID, the only
// form this server mints, so an aliased or crafted id cannot address a session.
func isCanonicalTusUploadID(uploadID string) bool {
	id, err := uuid.Parse(uploadID)
	return err == nil && id.String() == uploadID
}

// tusTargetPath resolves the filer path a TUS create request targets from the
// request URL. It guarantees a leading slash so the result matches stored
// absolute paths and JWT AllowedPrefixes even if TusBasePath were misconfigured
// with a trailing slash.
func (fs *FilerServer) tusTargetPath(r *http.Request) string {
	target := strings.TrimPrefix(r.URL.Path, fs.option.TusBasePath)
	if target != "" && !strings.HasPrefix(target, "/") {
		target = "/" + target
	}
	return canonicalTusTargetPath(target)
}

// canonicalTusTargetPath normalises an absolute filer target, or returns "" when
// the input is empty or not absolute, so authorization and the final write agree
// on one path.
func canonicalTusTargetPath(target string) string {
	if target == "" || !strings.HasPrefix(target, "/") {
		return ""
	}
	return path.Clean(target)
}

// writeTusCompleteError maps a completeTusUpload failure to the same HTTP status
// the normal write path uses: a read-only prefix returns 507 and a WORM-protected
// target returns 403, rather than a generic 500.
func writeTusCompleteError(w http.ResponseWriter, err error) {
	switch {
	case errors.Is(err, ErrReadOnly):
		http.Error(w, err.Error(), http.StatusInsufficientStorage)
	case errors.Is(err, ErrWormEnforced):
		http.Error(w, err.Error(), http.StatusForbidden)
	default:
		http.Error(w, "Failed to complete upload", http.StatusInternalServerError)
	}
}

// tusOptionsHandler handles OPTIONS requests for capability discovery
func (fs *FilerServer) tusOptionsHandler(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Tus-Version", TusVersion)
	w.Header().Set("Tus-Extension", TusExtensions)
	w.Header().Set("Tus-Max-Size", strconv.FormatInt(fs.option.TusMaxSize, 10))
	w.WriteHeader(http.StatusOK)
}

// tusCreateHandler handles POST requests to create new uploads
func (fs *FilerServer) tusCreateHandler(w http.ResponseWriter, r *http.Request, claims *security.SeaweedFilerClaims) {
	// Use a context that ignores cancellation from the request context.
	// Internal operations (creating TUS session, writing data, completing uploads)
	// may exceed the filer's client connection inactivity timeout.
	ctx := context.WithoutCancel(r.Context())

	concat := r.Header.Get("Upload-Concat")
	if strings.HasPrefix(concat, TusConcatFinalPrefix) {
		fs.tusConcatFinalHandler(w, r, claims, concat)
		return
	}
	if concat != "" && concat != TusConcatPartial {
		http.Error(w, "Invalid Upload-Concat", http.StatusBadRequest)
		return
	}

	// Parse Upload-Length header (required)
	uploadLengthStr := r.Header.Get("Upload-Length")
	if uploadLengthStr == "" {
		http.Error(w, "Upload-Length header required", http.StatusBadRequest)
		return
	}
	uploadLength, err := strconv.ParseInt(uploadLengthStr, 10, 64)
	if err != nil || uploadLength < 0 {
		http.Error(w, "Invalid Upload-Length", http.StatusBadRequest)
		return
	}
	if uploadLength > fs.option.TusMaxSize {
		http.Error(w, "Upload-Length exceeds maximum", http.StatusRequestEntityTooLarge)
		return
	}

	// Parse Upload-Metadata header (optional)
	metadata := parseTusMetadata(r.Header.Get("Upload-Metadata"))

	// Determine target path from request URL (leading slash guaranteed)
	targetPath := fs.tusTargetPath(r)
	if targetPath == "" || targetPath == "/" {
		http.Error(w, "Target path required", http.StatusBadRequest)
		return
	}

	// Reject writes to a read-only prefix up front, before creating a session or
	// uploading any chunks, matching the normal write path.
	if fs.filer.FilerConf.MatchStorageRule(targetPath).ReadOnly {
		http.Error(w, ErrReadOnly.Error(), http.StatusInsufficientStorage)
		return
	}

	// Generate upload ID
	uploadID := uuid.New().String()

	// Create upload session
	session, err := fs.createTusSession(ctx, uploadID, targetPath, uploadLength, metadata, concat)
	if err != nil {
		glog.Errorf("Failed to create TUS session: %v", err)
		http.Error(w, "Failed to create upload", http.StatusInternalServerError)
		return
	}

	uploadLocation := fs.tusUploadLocation(uploadID)

	// Handle creation-with-upload extension
	// TUS requires Content-Length for uploads; reject chunked encoding
	if r.Header.Get("Content-Type") == "application/offset+octet-stream" {
		if r.ContentLength < 0 {
			fs.deleteTusSession(ctx, uploadID)
			http.Error(w, "Content-Length header required for creation-with-upload", http.StatusBadRequest)
			return
		}
		if r.ContentLength > 0 {
			// Upload data in the creation request
			bytesWritten, uploadErr := fs.tusWriteData(ctx, r, session, 0, r.Body, r.ContentLength)
			if uploadErr != nil {
				// Cleanup session on failure
				fs.deleteTusSession(ctx, uploadID)
				if errors.Is(uploadErr, ErrContentTooLarge) {
					http.Error(w, "Content-Length exceeds declared upload size", http.StatusRequestEntityTooLarge)
					return
				}
				glog.Errorf("Failed to write initial TUS data: %v", uploadErr)
				http.Error(w, "Failed to write data", http.StatusInternalServerError)
				return
			}

			// Update offset in response header
			w.Header().Set("Upload-Offset", strconv.FormatInt(bytesWritten, 10))

			// Check if upload is complete; a partial upload keeps its chunks for
			// a later concatenation instead of landing at the target path.
			if bytesWritten == session.Size && !session.isPartial() {
				// Ensure the pinned session still exists, then refresh its chunks.
				if err = fs.refreshTusSessionChunks(ctx, session); err != nil {
					glog.Errorf("Failed to get updated TUS session: %v", err)
					http.Error(w, "Failed to complete upload", http.StatusInternalServerError)
					return
				}
				if err := fs.completeTusUpload(ctx, session); err != nil {
					glog.Errorf("Failed to complete TUS upload: %v", err)
					writeTusCompleteError(w, err)
					return
				}
			}
		}
		// ContentLength == 0 is allowed, just proceed to respond
	}

	w.Header().Set("Location", uploadLocation)
	w.WriteHeader(http.StatusCreated)
}

// tusUploadLocation builds the upload URL path for a session id (single leading /)
func (fs *FilerServer) tusUploadLocation(uploadID string) string {
	// TusBasePath is pre-normalized in filer_server.go (leading slash, no trailing slash)
	uploadLocation := path.Clean(fmt.Sprintf("%s/.uploads/%s", fs.option.TusBasePath, uploadID))
	if !strings.HasPrefix(uploadLocation, "/") {
		uploadLocation = "/" + uploadLocation
	}
	return uploadLocation
}

// parseTusConcatFinal extracts the partial upload ids from an Upload-Concat
// final header. Each reference may be an absolute URL or a path, must point at
// this server's upload location, and may appear only once since concatenation
// consumes the partial.
func (fs *FilerServer) parseTusConcatFinal(concat string) ([]string, error) {
	uploadsPrefix := fs.option.TusBasePath + "/.uploads/"
	var partialIDs []string
	seen := make(map[string]bool)
	for _, ref := range strings.Fields(strings.TrimPrefix(concat, TusConcatFinalPrefix)) {
		refURL, err := url.Parse(ref)
		if err != nil {
			return nil, fmt.Errorf("invalid partial upload reference %q", ref)
		}
		partialID := strings.TrimPrefix(refURL.Path, uploadsPrefix)
		if partialID == refURL.Path || !isCanonicalTusUploadID(partialID) {
			return nil, fmt.Errorf("invalid partial upload reference %q", ref)
		}
		if seen[partialID] {
			return nil, fmt.Errorf("duplicate partial upload reference %q", ref)
		}
		seen[partialID] = true
		partialIDs = append(partialIDs, partialID)
	}
	if len(partialIDs) == 0 {
		return nil, errors.New("no partial uploads listed")
	}
	return partialIDs, nil
}

// tusConcatFinalHandler handles POST requests carrying Upload-Concat final. It
// stitches the listed completed partial uploads, in order, into one entry at
// the request's target path.
func (fs *FilerServer) tusConcatFinalHandler(w http.ResponseWriter, r *http.Request, claims *security.SeaweedFilerClaims, concat string) {
	ctx := context.WithoutCancel(r.Context())

	// The final upload's length is the sum of the partial lengths.
	if r.Header.Get("Upload-Length") != "" {
		http.Error(w, "Upload-Length not allowed for a final upload", http.StatusBadRequest)
		return
	}
	if r.ContentLength > 0 {
		http.Error(w, "Cannot upload data to a final upload", http.StatusForbidden)
		return
	}

	partialIDs, err := fs.parseTusConcatFinal(concat)
	if err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}

	targetPath := fs.tusTargetPath(r)
	if targetPath == "" || targetPath == "/" {
		http.Error(w, "Target path required", http.StatusBadRequest)
		return
	}
	if fs.filer.FilerConf.MatchStorageRule(targetPath).ReadOnly {
		http.Error(w, ErrReadOnly.Error(), http.StatusInsufficientStorage)
		return
	}

	// Every partial must exist, be authorized for this credential, and be
	// complete; concatenation-unfinished is not offered. Each one is claimed
	// with an exclusive consumed marker before its chunks are read, so a
	// concurrent final cannot consume the same partial and a concurrent DELETE
	// or expiry cleanup cannot free chunks that move to the final entry.
	var partials []*TusSession
	var claimedIDs []string
	var totalSize int64
	releaseClaims := func() {
		for _, claimedID := range claimedIDs {
			fs.rollbackTusSessionConsumed(ctx, claimedID)
		}
	}
	for _, partialID := range partialIDs {
		partial, err := fs.readTusSessionInfo(ctx, partialID)
		if err != nil {
			glog.V(1).Infof("TUS partial %s not resolved: %v", partialID, err)
			releaseClaims()
			http.Error(w, "Partial upload not found", http.StatusNotFound)
			return
		}
		if !authorizeFilerJwtPaths(r, claims, []string{partial.TargetPath}) {
			releaseClaims()
			writeJsonError(w, r, http.StatusUnauthorized, errors.New("wrong jwt"))
			return
		}
		if !partial.isPartial() {
			releaseClaims()
			http.Error(w, "Not a partial upload: "+partialID, http.StatusBadRequest)
			return
		}
		totalSize += partial.Size
		if totalSize > fs.option.TusMaxSize {
			releaseClaims()
			http.Error(w, "Combined upload size exceeds maximum", http.StatusRequestEntityTooLarge)
			return
		}
		if err := fs.claimTusPartial(ctx, partial); err != nil {
			releaseClaims()
			switch {
			case errors.Is(err, filer_pb.ErrEntryAlreadyExists):
				http.Error(w, "Partial upload already consumed: "+partialID, http.StatusConflict)
			case errors.Is(err, filer_pb.ErrNotFound):
				glog.V(1).Infof("TUS partial %s changed before concatenation: %v", partialID, err)
				http.Error(w, "Partial upload not found", http.StatusNotFound)
			default:
				glog.Errorf("Failed to claim TUS partial %s: %v", partialID, err)
				http.Error(w, "Failed to create upload", http.StatusInternalServerError)
			}
			return
		}
		claimedIDs = append(claimedIDs, partialID)
		if partial.Offset != partial.Size {
			releaseClaims()
			http.Error(w, "Partial upload not finished: "+partialID, http.StatusBadRequest)
			return
		}
		partials = append(partials, partial)
	}

	metadata := parseTusMetadata(r.Header.Get("Upload-Metadata"))
	uploadID := uuid.New().String()
	session, err := fs.createTusSession(ctx, uploadID, targetPath, totalSize, metadata, concat)
	if err != nil {
		glog.Errorf("Failed to create TUS session: %v", err)
		releaseClaims()
		http.Error(w, "Failed to create upload", http.StatusInternalServerError)
		return
	}

	// Re-base each partial's chunks onto the final upload's offset space.
	for _, partial := range partials {
		for _, chunk := range partial.Chunks {
			session.Chunks = append(session.Chunks, &TusChunkInfo{
				Offset:   session.Offset + chunk.Offset,
				Size:     chunk.Size,
				FileId:   chunk.FileId,
				UploadAt: chunk.UploadAt,
			})
		}
		session.Offset += partial.Size
	}

	if err := fs.completeTusUpload(ctx, session); err != nil {
		fs.deleteTusSession(ctx, uploadID)
		releaseClaims()
		glog.Errorf("Failed to complete TUS concatenation: %v", err)
		writeTusCompleteError(w, err)
		return
	}

	// The chunks now belong to the final entry, so remove only the partials'
	// session metadata.
	for _, partial := range partials {
		if err := fs.filer.DeleteEntryMetaAndData(ctx, util.FullPath(fs.tusSessionPath(partial.ID)), true, false, false, false, nil, 0); err != nil {
			glog.V(1).Infof("Failed to cleanup TUS partial session %s: %v", partial.ID, err)
		}
	}

	w.Header().Set("Location", fs.tusUploadLocation(uploadID))
	w.WriteHeader(http.StatusCreated)
}

// tusHeadHandler handles HEAD requests to get current upload offset
func (fs *FilerServer) tusHeadHandler(w http.ResponseWriter, session *TusSession) {
	if session.Concat != "" {
		w.Header().Set("Upload-Concat", session.Concat)
	}
	w.Header().Set("Upload-Offset", strconv.FormatInt(session.Offset, 10))
	w.Header().Set("Upload-Length", strconv.FormatInt(session.Size, 10))
	w.Header().Set("Cache-Control", "no-store")
	w.WriteHeader(http.StatusOK)
}

// tusPatchHandler handles PATCH requests to upload data
func (fs *FilerServer) tusPatchHandler(w http.ResponseWriter, r *http.Request, session *TusSession) {
	if session.isFinal() {
		http.Error(w, "Cannot PATCH a final upload", http.StatusForbidden)
		return
	}

	// Use a context that ignores cancellation from the request context.
	// The filer's connection has an inactivity timeout: after the request body is fully read,
	// internal operations (assigning file IDs, uploading to volume servers, completing uploads)
	// may exceed the timeout, causing the request context to be canceled.
	ctx := context.WithoutCancel(r.Context())

	// Validate Content-Type
	contentType := r.Header.Get("Content-Type")
	if contentType != "application/offset+octet-stream" {
		http.Error(w, "Content-Type must be application/offset+octet-stream", http.StatusUnsupportedMediaType)
		return
	}

	// Validate Upload-Offset header
	uploadOffsetStr := r.Header.Get("Upload-Offset")
	if uploadOffsetStr == "" {
		http.Error(w, "Upload-Offset header required", http.StatusBadRequest)
		return
	}
	uploadOffset, err := strconv.ParseInt(uploadOffsetStr, 10, 64)
	if err != nil || uploadOffset < 0 {
		http.Error(w, "Invalid Upload-Offset", http.StatusBadRequest)
		return
	}

	// Check offset matches current position
	if uploadOffset != session.Offset {
		http.Error(w, fmt.Sprintf("Offset mismatch: expected %d, got %d", session.Offset, uploadOffset), http.StatusConflict)
		return
	}

	// TUS requires Content-Length header for PATCH requests
	if r.ContentLength < 0 {
		http.Error(w, "Content-Length header required", http.StatusBadRequest)
		return
	}

	// Write data
	bytesWritten, err := fs.tusWriteData(ctx, r, session, uploadOffset, r.Body, r.ContentLength)
	if err != nil {
		if errors.Is(err, ErrContentTooLarge) {
			http.Error(w, "Content-Length exceeds remaining upload size", http.StatusRequestEntityTooLarge)
			return
		}
		glog.Errorf("Failed to write TUS data: %v", err)
		http.Error(w, "Failed to write data", http.StatusInternalServerError)
		return
	}

	newOffset := uploadOffset + bytesWritten

	// Check if upload is complete; a partial upload keeps its chunks for a later
	// concatenation instead of landing at the target path.
	if newOffset == session.Size && !session.isPartial() {
		// Ensure the authorized session still exists, then refresh its chunks.
		if err = fs.refreshTusSessionChunks(ctx, session); err != nil {
			glog.Errorf("Failed to get updated TUS session: %v", err)
			http.Error(w, "Failed to complete upload", http.StatusInternalServerError)
			return
		}

		if err := fs.completeTusUpload(ctx, session); err != nil {
			glog.Errorf("Failed to complete TUS upload: %v", err)
			writeTusCompleteError(w, err)
			return
		}
	}

	w.Header().Set("Upload-Offset", strconv.FormatInt(newOffset, 10))
	w.WriteHeader(http.StatusNoContent)
}

// tusDeleteHandler handles DELETE requests to cancel uploads
func (fs *FilerServer) tusDeleteHandler(w http.ResponseWriter, r *http.Request, session *TusSession) {
	ctx := r.Context()

	if err := fs.deleteTusSession(ctx, session.ID); err != nil {
		glog.Errorf("Failed to delete TUS session: %v", err)
		http.Error(w, "Failed to delete upload", http.StatusInternalServerError)
		return
	}

	w.WriteHeader(http.StatusNoContent)
}

// tusChunkSize is the size of sub-chunks used when streaming uploads to volume servers.
// 4MB balances memory usage (avoiding buffering large TUS chunks) with upload efficiency
// (minimizing the number of volume server requests). Smaller values reduce memory but
// increase request overhead; larger values do the opposite.
const tusChunkSize = 4 * 1024 * 1024 // 4MB

// ErrContentTooLarge is returned when Content-Length exceeds remaining upload space
var ErrContentTooLarge = fmt.Errorf("content length exceeds remaining upload size")

// tusWriteData uploads data to volume servers in streaming chunks and updates session
// It reads data in fixed-size sub-chunks to avoid buffering large TUS chunks entirely in memory
func (fs *FilerServer) tusWriteData(ctx context.Context, r *http.Request, session *TusSession, offset int64, reader io.Reader, contentLength int64) (int64, error) {
	if contentLength == 0 {
		return 0, nil
	}

	// Check if content length exceeds remaining size - return error instead of silently truncating
	remaining := session.Size - offset
	if contentLength > remaining {
		return 0, ErrContentTooLarge
	}
	if remaining <= 0 {
		return 0, nil
	}

	// Determine storage options based on target path
	so, err := fs.detectStorageOption0(ctx, session.TargetPath, "", "", "", "", "", "", "", "", "")
	if err != nil {
		return 0, fmt.Errorf("detect storage option: %w", err)
	}

	// When DiskType is empty, use filer's -disk
	if so.DiskType == "" {
		so.DiskType = fs.option.DiskType
	}

	// Read first bytes for MIME type detection
	sniffSize := int64(512)
	if contentLength < sniffSize {
		sniffSize = contentLength
	}
	sniffBuf := make([]byte, sniffSize)
	sniffN, sniffErr := io.ReadFull(reader, sniffBuf)
	if sniffErr != nil && sniffErr != io.EOF && sniffErr != io.ErrUnexpectedEOF {
		return 0, fmt.Errorf("read data for mime detection: %w", sniffErr)
	}
	if sniffN == 0 {
		return 0, nil
	}
	sniffBuf = sniffBuf[:sniffN]
	mimeType := http.DetectContentType(sniffBuf)

	// Create a combined reader with sniffed bytes prepended
	var dataReader io.Reader
	if int64(sniffN) >= contentLength {
		dataReader = bytes.NewReader(sniffBuf)
	} else {
		dataReader = io.MultiReader(bytes.NewReader(sniffBuf), io.LimitReader(reader, contentLength-int64(sniffN)))
	}

	// Upload in streaming chunks to avoid buffering entire content in memory
	var totalWritten int64
	var uploadErr error

	chunkBuf := make([]byte, tusChunkSize)
	currentOffset := offset

	for totalWritten < contentLength {
		// Read up to tusChunkSize bytes
		readSize := int64(tusChunkSize)
		if contentLength-totalWritten < readSize {
			readSize = contentLength - totalWritten
		}

		n, readErr := io.ReadFull(dataReader, chunkBuf[:readSize])
		if readErr != nil && readErr != io.EOF && readErr != io.ErrUnexpectedEOF {
			uploadErr = fmt.Errorf("read chunk data: %w", readErr)
			break
		}
		if n == 0 {
			break
		}

		// Store the sub-chunk through the regular write path's chunk writer,
		// which assigns a fresh file id per attempt; a failed attempt's needle
		// is returned so it can be freed instead of lingering unreferenced.
		chunks, chunkErr := fs.dataToChunkWithSSE(ctx, r, "", mimeType, chunkBuf[:n], currentOffset, so)
		if chunkErr != nil {
			fs.filer.DeleteUncommittedChunks(ctx, chunks)
			uploadErr = fmt.Errorf("upload data: %w", chunkErr)
			break
		}
		if len(chunks) == 0 {
			uploadErr = fmt.Errorf("no chunk stored at offset %d", currentOffset)
			break
		}
		stored := chunks[0]

		chunk := &TusChunkInfo{
			Offset:   stored.Offset,
			Size:     int64(stored.Size),
			FileId:   stored.FileId,
			UploadAt: stored.ModifiedTsNs,
		}

		if saveErr := fs.saveTusChunk(ctx, session.ID, chunk); saveErr != nil {
			fs.deleteTusChunk(ctx, session, chunk)
			uploadErr = fmt.Errorf("update session: %w", saveErr)
			break
		}

		totalWritten += chunk.Size
		currentOffset += chunk.Size
		stats.FilerHandlerCounter.WithLabelValues("tusUploadChunk").Inc()
	}

	// Sub-chunks already recorded stay: the session offset a resuming client
	// reads back covers them, and the completed entry is assembled from them.
	return totalWritten, uploadErr
}

// parseTusMetadata parses the Upload-Metadata header
// Format: key1 base64value1,key2 base64value2,...
func parseTusMetadata(header string) map[string]string {
	metadata := make(map[string]string)
	if header == "" {
		return metadata
	}

	pairs := strings.Split(header, ",")
	for _, pair := range pairs {
		pair = strings.TrimSpace(pair)
		parts := strings.SplitN(pair, " ", 2)
		if len(parts) != 2 {
			continue
		}
		key := strings.TrimSpace(parts[0])
		encodedValue := strings.TrimSpace(parts[1])

		value, err := base64.StdEncoding.DecodeString(encodedValue)
		if err != nil {
			glog.V(1).Infof("Failed to decode TUS metadata value for key %s: %v", key, err)
			continue
		}
		metadata[key] = string(value)
	}

	return metadata
}
