package s3api

import (
	"context"
	"errors"
	"net/http"
	"net/url"
	"strings"

	"github.com/seaweedfs/seaweedfs/weed/glog"
	"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
	"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
	"github.com/seaweedfs/seaweedfs/weed/s3api/s3err"
	stats_collect "github.com/seaweedfs/seaweedfs/weed/stats"
	"github.com/seaweedfs/seaweedfs/weed/util"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
)

var renameSourceConditionalHeaders = sourceConditionalHeaderNames{
	ifMatch:           s3_constants.AmzRenameSourceIfMatch,
	ifNoneMatch:       s3_constants.AmzRenameSourceIfNoneMatch,
	ifModifiedSince:   s3_constants.AmzRenameSourceIfModifiedSince,
	ifUnmodifiedSince: s3_constants.AmzRenameSourceIfUnmodifiedSince,
}

// RenameObjectHandler implements RenameObject:
//
//	PUT /{bucket}/{destination key}?renameObject
//	x-amz-rename-source: /{bucket}/{source key}
//
// The object is moved by the filer's AtomicRenameEntry, so its bytes are never
// read or rewritten and its metadata (ETag, tags, SSE keys) travels unchanged.
// Versioned buckets are rejected: the move would have to rebuild the .versions
// chain, and AWS itself only offers RenameObject on directory buckets, which
// cannot be versioned.
func (s3a *S3ApiServer) RenameObjectHandler(w http.ResponseWriter, r *http.Request) {
	bucket, dstObject := s3_constants.GetBucketAndObject(r)

	candidates, errCode := renameSourceCandidates(r, bucket)
	if errCode != s3err.ErrNone {
		s3err.WriteErrorResponse(w, r, errCode)
		return
	}
	srcObject := s3a.pickRenameSource(bucket, candidates)

	glog.V(3).Infof("RenameObjectHandler %s: %s => %s", bucket, srcObject, dstObject)

	if len(dstObject) > s3_constants.MaxS3ObjectKeyLength {
		s3err.WriteErrorResponse(w, r, s3err.ErrKeyTooLongError)
		return
	}
	if err := s3a.validateTableBucketObjectPath(bucket, dstObject); err != nil {
		s3err.WriteErrorResponse(w, r, s3err.ErrAccessDenied)
		return
	}
	// A trailing slash names a directory, and renaming one would move a whole
	// subtree rather than an object.
	if strings.HasSuffix(dstObject, "/") {
		s3err.WriteErrorResponse(w, r, s3err.ErrInvalidRequest)
		return
	}
	if strings.HasSuffix(srcObject, "/") {
		s3err.WriteErrorResponse(w, r, s3err.ErrNoSuchKey)
		return
	}
	if srcObject == dstObject {
		s3err.WriteErrorResponse(w, r, s3err.ErrRenameDestinationSameAsSource)
		return
	}

	// The route's Auth middleware only authorized the destination, because that
	// is what the request URL names. The source arrives in a header and loses
	// its key, so it needs both read and delete permission checked here.
	if errCode := s3a.authorizeRenameSource(r, bucket, srcObject); errCode != s3err.ErrNone {
		s3err.WriteErrorResponse(w, r, errCode)
		return
	}

	versioningState, err := s3a.getVersioningState(bucket)
	if err != nil {
		if errors.Is(err, filer_pb.ErrNotFound) {
			s3err.WriteErrorResponse(w, r, s3err.ErrNoSuchBucket)
			return
		}
		glog.Errorf("RenameObjectHandler: versioning state for bucket %s: %v", bucket, err)
		s3err.WriteErrorResponse(w, r, s3err.ErrInternalError)
		return
	}
	if versioningState != "" {
		s3err.WriteErrorResponse(w, r, s3err.ErrNotImplemented)
		return
	}

	errCode = s3a.withRenameWriteLocks(bucket, srcObject, dstObject, func() s3err.ErrorCode {
		entry, err := s3a.resolveCopySourceEntry(bucket, srcObject, "", "")
		srcIsPrefixObject := entry.IsPrefixObject()
		entry = prefixObjectSource(entry)
		if errCode := classifyCopySourceError(entry, err); errCode != s3err.ErrNone {
			return errCode
		}
		if errCode := validateSourceConditionalHeaders(r, entry, renameSourceConditionalHeaders); errCode != s3err.ErrNone {
			return errCode
		}
		if errCode := s3a.checkConditionalHeaders(r, bucket, dstObject); errCode != s3err.ErrNone {
			return errCode
		}
		return s3a.renameObjectEntry(r.Context(), bucket, srcObject, dstObject, entry, srcIsPrefixObject)
	})
	if errCode != s3err.ErrNone {
		s3err.WriteErrorResponse(w, r, errCode)
		return
	}

	stats_collect.RecordBucketActiveTime(bucket)
	writeSuccessResponseEmpty(w, r)
}

// renameSourceCandidates reads x-amz-rename-source into the source keys it may
// mean, best guess first.
//
// AWS spells the source both ways: its CLI, Java and Rust examples pass a bare
// key, while a second CLI example and the boto3 conditional example pass
// bucket/key. A value is therefore read as a literal key first — that is the
// form AWS leads with, and it is the only reading that can never name the wrong
// object — and, when it is prefixed with the request's own bucket, as that
// bucket-qualified form second. There is no cross-bucket reading: RenameObject
// moves within one bucket, and the filer refuses to move an entry between two.
func renameSourceCandidates(r *http.Request, bucket string) ([]string, s3err.ErrorCode) {
	rawSource := r.Header.Get(s3_constants.AmzRenameSource)
	if rawSource == "" {
		return nil, s3err.ErrInvalidRenameSource
	}
	// PathUnescape, not QueryUnescape: the value is a path, where '+' is a
	// literal plus and not a space.
	source, err := url.PathUnescape(rawSource)
	if err != nil {
		source = rawSource
	}

	// NormalizeObjectKey drops the leading slash both forms may carry.
	source = s3_constants.NormalizeObjectKey(source)
	if source == "" {
		return nil, s3err.ErrInvalidRenameSource
	}

	candidates := []string{source}
	if qualified := strings.TrimPrefix(source, bucket+"/"); qualified != source && qualified != "" {
		candidates = append(candidates, qualified)
	}
	// `.`/`..` segments are collapsed by the filer's path join, so reject them
	// here as the request URL's own key already is.
	for _, candidate := range candidates {
		if !s3_constants.IsValidObjectKey(candidate) {
			return nil, s3err.ErrInvalidRenameSource
		}
	}
	return candidates, s3err.ErrNone
}

// pickRenameSource resolves which reading of the source header the bucket
// actually holds. A single candidate is returned unprobed, so the common bare
// key costs no extra lookup; when both readings are possible the one the bucket
// holds wins, and when neither does the last is reported missing.
//
// Only a proven absence moves on to the next reading. A path that holds
// something the rename cannot move — a directory, say — is still the path the
// caller named, and answering for it beats renaming a different object under
// the other reading; so is a path whose lookup merely failed, since a blip must
// not be able to redirect a rename.
func (s3a *S3ApiServer) pickRenameSource(bucket string, candidates []string) string {
	for _, candidate := range candidates[:len(candidates)-1] {
		// A trailing slash never names an object, and never reaches a usable
		// directory/name split either.
		if strings.HasSuffix(candidate, "/") {
			continue
		}
		if !renameSourceAbsent(s3a.resolveCopySourceEntry(bucket, candidate, "", "")) {
			return candidate
		}
	}
	return candidates[len(candidates)-1]
}

// renameSourceAbsent reports whether a lookup proved the candidate absent. Only
// the filer saying so counts; a lookup that failed for any other reason is not
// a proof of absence.
func renameSourceAbsent(entry *filer_pb.Entry, err error) bool {
	if entry != nil {
		return false
	}
	return err == nil || errors.Is(err, filer_pb.ErrNotFound) || status.Code(err) == codes.NotFound
}

func (s3a *S3ApiServer) authorizeRenameSource(r *http.Request, bucket, srcObject string) s3err.ErrorCode {
	if s3a.iam == nil || !s3a.iam.isEnabled() {
		return s3err.ErrNone
	}
	var identity *Identity
	if id, ok := s3_constants.GetIdentityFromContext(r).(*Identity); ok {
		identity = id
	}
	// The rename both reads the source object and removes it from its key.
	if errCode := s3a.iam.AuthorizeCopySource(r, identity, bucket, srcObject, ""); errCode != s3err.ErrNone {
		return errCode
	}
	return s3a.iam.AuthorizeObjectDelete(r, identity, bucket, srcObject, "")
}

// withRenameWriteLocks holds the object write lock of both keys across the
// precondition checks and the move. The keys are locked in a fixed order so a
// rename in the opposite direction cannot deadlock against this one.
func (s3a *S3ApiServer) withRenameWriteLocks(bucket, srcObject, dstObject string, fn func() s3err.ErrorCode) s3err.ErrorCode {
	first, second := srcObject, dstObject
	if second < first {
		first, second = second, first
	}
	return s3a.withObjectWriteLock(bucket, first, nil, func() s3err.ErrorCode {
		return s3a.withObjectWriteLock(bucket, second, nil, fn)
	})
}

func (s3a *S3ApiServer) renameObjectEntry(ctx context.Context, bucket, srcObject, dstObject string, srcEntry *filer_pb.Entry, srcIsPrefixObject bool) s3err.ErrorCode {
	srcDir, srcName := util.FullPath(s3a.toFilerPath(bucket, srcObject)).DirAndName()
	dstDir, dstName := util.FullPath(s3a.toFilerPath(bucket, dstObject)).DirAndName()

	// The move overwrites an existing destination object. A directory there is not a
	// conflict: it means other keys are nested under the destination key, and the
	// object goes onto the directory they live in, the way a PutObject of that key
	// would put it there.
	dstHoldsNestedKeys := false
	if existing, err := s3a.getEntry(dstDir, dstName); err == nil {
		dstHoldsNestedKeys = existing.IsDirectory
	} else if !errors.Is(err, filer_pb.ErrNotFound) {
		glog.Errorf("RenameObject %s: destination %s: %v", bucket, dstObject, err)
		return s3err.ErrInternalError
	}

	// AtomicRenameEntry moves a directory by moving everything under it, and the keys
	// nested under either end of this rename are not part of what is being renamed.
	if srcIsPrefixObject || dstHoldsNestedKeys {
		return s3a.renameKeyHoldingNestedKeys(bucket, srcObject, dstObject, srcEntry)
	}

	err := s3a.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error {
		_, err := client.AtomicRenameEntry(ctx, &filer_pb.AtomicRenameEntryRequest{
			OldDirectory: srcDir,
			OldName:      srcName,
			NewDirectory: dstDir,
			NewName:      dstName,
		})
		return err
	})
	if err != nil {
		glog.Errorf("RenameObject %s: %s => %s: %v", bucket, srcObject, dstObject, err)
		if isTransientFilerError(err) {
			return s3err.ErrServiceUnavailable
		}
		return s3err.ErrInternalError
	}
	return s3err.ErrNone
}

// renameKeyHoldingNestedKeys moves an object when either key of the rename is one
// other keys are nested under. Such a key is stored on the directory those keys live
// in, which has to stay where it is, so the object's own data is written at the
// destination and then stripped off the source key - the entry survives as the plain
// directory it also is. Both keys are held under their write locks for the whole
// move, so no other S3 write interleaves; a crash between the two steps leaves the
// destination written and the source still there, which a retry settles.
func (s3a *S3ApiServer) renameKeyHoldingNestedKeys(bucket, srcObject, dstObject string, srcEntry *filer_pb.Entry) s3err.ErrorCode {
	dstPath := util.FullPath(s3a.toFilerPath(bucket, dstObject))
	dstDir, dstName := dstPath.DirAndName()

	chunks, err := s3a.copyChunks(srcEntry, string(dstPath))
	if err != nil {
		glog.Errorf("RenameObject %s: copy chunks of %s: %v", bucket, srcObject, err)
		return s3err.ErrInternalError
	}

	if err := s3a.mkFile(dstDir, dstName, chunks, func(entry *filer_pb.Entry) {
		copyEntryToTarget(entry, srcEntry)
		entry.Chunks = chunks
	}); err != nil {
		glog.Errorf("RenameObject %s: write %s: %v", bucket, dstObject, err)
		s3a.deleteOrphanedChunks(chunks)
		return filerErrorToS3Error(err)
	}

	// The destination holds copies now, so the source's own chunks go with it.
	srcDir, srcName := util.FullPath(s3a.toFilerPath(bucket, srcObject)).DirAndName()
	if err := s3a.rmObject(context.Background(), srcDir, srcName, true, false); err != nil {
		glog.Errorf("RenameObject %s: strip %s: %v", bucket, srcObject, err)
		return s3err.ErrInternalError
	}
	return s3err.ErrNone
}
