package s3api

import (
	"bytes"
	"context"
	"encoding/base64"
	"encoding/json"
	"encoding/xml"
	"errors"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"sort"
	"strconv"
	"strings"
	"time"

	"github.com/aws/aws-sdk-go/private/protocol/xml/xmlutil"
	"github.com/seaweedfs/seaweedfs/weed/s3api/s3bucket"

	"github.com/seaweedfs/seaweedfs/weed/filer"
	"github.com/seaweedfs/seaweedfs/weed/s3api/lifecycle_xml"
	"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
	stats_collect "github.com/seaweedfs/seaweedfs/weed/stats"
	"github.com/seaweedfs/seaweedfs/weed/storage/needle"

	"github.com/seaweedfs/seaweedfs/weed/s3api/s3err"

	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/service/s3"

	"github.com/seaweedfs/seaweedfs/weed/glog"
	"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
	util_http "github.com/seaweedfs/seaweedfs/weed/util/http"
)

// A bucket creation lists collections, and a bucket deletion deletes one.
// Neither RPC carried a deadline, so a transient failure anywhere down the chain
// -- gateway to filer, filer to master, master to volume server -- held the S3
// request open until the client gave up on it. Both budgets are taken outside
// the filer failover walk, so they cover the whole walk rather than granting
// each filer a fresh one.
//
// The delete is the shorter of the two: the filer has already spent its own
// budget on this collection, under the bucket entry's delete inside s3a.rm, and
// this call is the follow-up for when that did not happen.
const (
	collectionListTimeout   = 15 * time.Second
	collectionDeleteTimeout = 10 * time.Second
)

func (s3a *S3ApiServer) ListBucketsHandler(w http.ResponseWriter, r *http.Request) {

	glog.V(3).Infof("ListBucketsHandler")

	// Get authenticated identity from context (set by Auth middleware)
	// For unauthenticated requests, this returns empty string
	identityId := s3_constants.GetIdentityNameFromContext(r)

	// Get the full identity object for permission and ownership checks
	// This is especially important for JWT users whose identity is not in the identities list
	// Note: We store the full Identity object in context for simplicity. Future optimization
	// could use a lightweight, credential-free view (name, account, actions, principal ARN)
	// for better data minimization.
	var identity *Identity
	if s3a.iam.isEnabled() {
		// Try to get the full identity from context first (works for all auth types including JWT)
		if identityObj := s3_constants.GetIdentityFromContext(r); identityObj != nil {
			if id, ok := identityObj.(*Identity); ok {
				identity = id
			} else {
				glog.Warningf("ListBucketsHandler: identity object in context has unexpected type: %T", identityObj)
			}
		}
		// Fallback to looking up by name if not in context (backward compatibility)
		if identity == nil && identityId != "" {
			identity = s3a.iam.lookupByIdentityName(identityId)
		}
	}

	maxBuckets, prefix, startAfter, errCode := getListBucketsArgs(r.URL.Query())
	if errCode != s3err.ErrNone {
		s3err.WriteErrorResponse(w, r, errCode)
		return
	}

	var buckets []ListAllMyBucketsEntry
	var nextToken string
	// Unauthenticated users should not see any buckets
	if identity != nil {
		var err error
		if fromIndex, granted := s3a.iam.canListBucketsFromOwnerIndex(r, identity); fromIndex && s3a.bucketOwnerIndexReady() {
			buckets, nextToken, err = s3a.listBucketsFromOwnerIndex(r, identity, granted, prefix, startAfter, maxBuckets)
		} else {
			buckets, nextToken, err = s3a.scanVisibleBuckets(r, identity, prefix, startAfter, maxBuckets)
		}
		if err != nil {
			glog.Errorf("ListBucketsHandler: %v", err)
			s3err.WriteErrorResponse(w, r, s3err.ErrInternalError)
			return
		}
	}

	response := ListAllMyBucketsResult{
		Owner: CanonicalUser{
			ID:          identityId,
			DisplayName: identityId,
		},
		Buckets:           ListAllMyBucketsList{Bucket: buckets},
		ContinuationToken: nextToken,
		Prefix:            prefix,
	}

	glog.V(3).Infof("ListBucketsHandler response: %+v", response)
	writeSuccessResponseXML(w, r, response)
}

const (
	// maxBucketsPerPage caps a single ListBuckets response, matching AWS.
	maxBucketsPerPage = 10000
	// bucketScanPageSize is the filer page size while scanning /buckets.
	bucketScanPageSize = 1000
)

func getListBucketsArgs(values url.Values) (maxBuckets int, prefix string, startAfter string, errCode s3err.ErrorCode) {
	maxBuckets = maxBucketsPerPage
	if v := values.Get("max-buckets"); v != "" {
		n, err := strconv.Atoi(v)
		if err != nil || n < 1 || n > maxBucketsPerPage {
			return 0, "", "", s3err.ErrInvalidMaxBuckets
		}
		maxBuckets = n
	}
	prefix = values.Get("prefix")
	if v := values.Get("continuation-token"); v != "" {
		name, err := decodeContinuationToken(v)
		if err != nil {
			return 0, "", "", s3err.ErrInvalidContinuationToken
		}
		startAfter = name
	}
	return maxBuckets, prefix, startAfter, s3err.ErrNone
}

// The continuation token is the last returned bucket name, base64-encoded so
// clients treat it as opaque per the S3 API contract.
func encodeContinuationToken(bucket string) string {
	return base64.RawURLEncoding.EncodeToString([]byte(bucket))
}

func decodeContinuationToken(token string) (string, error) {
	name, err := base64.RawURLEncoding.DecodeString(token)
	if err != nil || len(name) == 0 {
		return "", fmt.Errorf("invalid continuation token %q", token)
	}
	return string(name), nil
}

// bucketVisibleToIdentity reports whether ListBuckets should include the bucket:
// the identity owns it or has explicit permission to list it.
func (s3a *S3ApiServer) bucketVisibleToIdentity(r *http.Request, bucket, owner string, identity *Identity) bool {
	if isBucketOwnedByIdentity(owner, identity) {
		return true
	}
	return s3a.iam.VerifyActionPermission(r, identity, s3_constants.ACTION_LIST, bucket, "") == s3err.ErrNone
}

// scanVisibleBuckets pages through /buckets and collects up to maxBuckets
// entries visible to the identity, returning a continuation token when more
// remain. It never buffers the full bucket set.
func (s3a *S3ApiServer) scanVisibleBuckets(r *http.Request, identity *Identity, prefix, startAfter string, maxBuckets int) (buckets []ListAllMyBucketsEntry, nextToken string, err error) {
	startFrom := startAfter
	for len(buckets) <= maxBuckets {
		entries, isLast, listErr := s3a.list(s3a.option.BucketsPath, prefix, startFrom, false, bucketScanPageSize)
		if listErr != nil {
			return nil, "", listErr
		}
		if len(entries) == 0 {
			break
		}
		for _, entry := range entries {
			startFrom = entry.Name
			if !entry.IsDirectory || strings.HasPrefix(entry.Name, ".") {
				continue
			}
			if !s3a.bucketVisibleToIdentity(r, entry.Name, bucketEntryOwner(entry), identity) {
				continue
			}
			buckets = append(buckets, ListAllMyBucketsEntry{
				Name:         entry.Name,
				CreationDate: time.Unix(entry.Attributes.Crtime, 0).UTC(),
			})
			if len(buckets) > maxBuckets {
				break
			}
		}
		if isLast || len(buckets) > maxBuckets {
			break
		}
	}
	if len(buckets) > maxBuckets {
		buckets = buckets[:maxBuckets]
		nextToken = encodeContinuationToken(buckets[maxBuckets-1].Name)
	}
	return buckets, nextToken, nil
}

// isBucketOwnedByIdentity checks if a bucket with the given owner id (its
// AmzIdentityId metadata) is owned by the given identity.
//
// Ownership rules:
// - Admin users: considered owners of all buckets
// - Non-admin users: own buckets whose owner id matches identity.Name
// - Buckets without owner metadata are not owned by anyone (except admins)
func isBucketOwnedByIdentity(owner string, identity *Identity) bool {
	if identity == nil {
		return false
	}

	// Admin users are considered owners of all buckets
	if identity.isAdmin() {
		return true
	}

	// Non-admin users with no name cannot own buckets.
	// This prevents misconfigured identities from matching buckets with empty owner IDs.
	return identity.Name != "" && identity.Name == owner
}

func (s3a *S3ApiServer) PutBucketHandler(w http.ResponseWriter, r *http.Request) {

	// collect parameters
	bucket, _ := s3_constants.GetBucketAndObject(r)

	// validate the bucket name
	err := s3bucket.VerifyS3BucketName(bucket)
	if err != nil {
		glog.Errorf("put invalid bucket name: %v %v", bucket, err)
		s3err.WriteErrorResponse(w, r, s3err.ErrInvalidBucketName)
		return
	}

	// Check if bucket already exists and handle ownership/settings
	// Get authenticated identity from context (secure, cannot be spoofed)
	currentIdentityId := s3_constants.GetIdentityNameFromContext(r)

	// Parse any requested bucket ACL (canned ACL or grant headers) up front so it
	// can be validated, persisted on creation, and factored into the already-exists
	// response. A "private" canned ACL is the default and counts as no explicit ACL.
	requestHasACL := hasExplicitBucketACL(r)
	var aclGrantsBytes []byte
	if requestHasACL {
		accountId := getAccountId(r)
		_, grants, errCode := ParseAndValidateAclHeaders(r, s3a.iam, "", accountId, accountId, false)
		if errCode != s3err.ErrNone {
			s3err.WriteErrorResponse(w, r, errCode)
			return
		}
		if len(grants) > 0 {
			grantsBytes, err := json.Marshal(grants)
			if err != nil {
				glog.Errorf("PutBucketHandler: marshal ACL grants for %s: %v", bucket, err)
				s3err.WriteErrorResponse(w, r, s3err.ErrInternalError)
				return
			}
			aclGrantsBytes = grantsBytes
		}
	}

	// Check collection existence first
	collectionExists := false
	if s3a.isTableBucket(bucket) {
		s3err.WriteErrorResponse(w, r, s3err.ErrBucketAlreadyExists)
		return
	}
	listCtx, cancelList := context.WithTimeout(r.Context(), collectionListTimeout)
	if err := s3a.withFilerClient(listCtx, false, func(client filer_pb.SeaweedFilerClient) error {
		if resp, err := client.CollectionList(listCtx, &filer_pb.CollectionListRequest{
			IncludeEcVolumes:     true,
			IncludeNormalVolumes: true,
		}); err != nil {
			return fmt.Errorf("list collections: %w", err)
		} else {
			for _, c := range resp.Collections {
				if s3a.getCollectionName(bucket) == c.Name {
					collectionExists = true
					break
				}
			}
		}
		return nil
	}); err != nil {
		// Advisory: the answer decides nothing below except whether to log that a
		// leftover collection is being reused. s3a.exists is what decides whether
		// the bucket already exists, so a listing that failed is no reason to
		// refuse the creation.
		glog.Warningf("PutBucketHandler: list collections for %s: %v", bucket, err)
	}
	cancelList()

	// Bucket already exists: report whether the caller already owns it or the
	// name is taken / the request conflicts.
	if exist, err := s3a.exists(s3a.option.BucketsPath, bucket, true); err == nil && exist {
		errCode := s3a.existingBucketError(r, bucket, currentIdentityId, requestHasACL)
		if errCode == s3err.ErrBucketAlreadyOwnedByYou {
			s3a.healBucketOwnerIndex(bucket, currentIdentityId)
		}
		s3err.WriteErrorResponse(w, r, errCode)
		return
	}

	// If collection exists but bucket directory doesn't, this is an inconsistent state
	// that can occur when a previous bucket deletion partially completed (collection
	// deletion failed but directory deletion succeeded, or volumes were recreated).
	// Recover by proceeding to create the missing bucket directory.
	if collectionExists {
		glog.Warningf("PutBucketHandler: collection exists but bucket directory missing for %s, recovering by creating bucket directory", bucket)
	}

	// Check for x-amz-bucket-object-lock-enabled header BEFORE creating bucket
	// This allows us to create the bucket with Object Lock configuration atomically
	objectLockEnabled := strings.EqualFold(r.Header.Get(s3_constants.AmzBucketObjectLockEnabled), "true")

	// Capture any Object Lock configuration error from within the callback
	// The mkdir callback doesn't support returning errors, so we capture it here
	var objectLockSetupError error

	// Create the folder for bucket with all settings atomically
	// This ensures Object Lock configuration is set in the same CreateEntry call,
	// preventing race conditions where the bucket exists without Object Lock enabled
	var bucketCrtime int64
	if err := s3a.mkdir(s3a.option.BucketsPath, bucket, func(entry *filer_pb.Entry) {
		bucketCrtime = entry.Attributes.Crtime

		// Set bucket owner
		setBucketOwner(r)(entry)

		// Persist a requested non-default ACL so GetBucketAcl and idempotent
		// recreation observe it (private is the default and is not stored).
		if len(aclGrantsBytes) > 0 {
			if entry.Extended == nil {
				entry.Extended = make(map[string][]byte)
			}
			entry.Extended[s3_constants.ExtAmzAclKey] = aclGrantsBytes
		}

		// Set Object Lock configuration atomically during bucket creation
		if objectLockEnabled {
			glog.V(3).Infof("PutBucketHandler: enabling Object Lock and Versioning for bucket %s atomically", bucket)

			if entry.Extended == nil {
				entry.Extended = make(map[string][]byte)
			}

			// Enable versioning (required for Object Lock)
			entry.Extended[s3_constants.ExtVersioningKey] = []byte(s3_constants.VersioningEnabled)

			// Create and store Object Lock configuration
			objectLockConfig := &ObjectLockConfiguration{
				ObjectLockEnabled: s3_constants.ObjectLockEnabled,
			}
			if err := StoreObjectLockConfigurationInExtended(entry, objectLockConfig); err != nil {
				glog.Errorf("PutBucketHandler: failed to store Object Lock config for bucket %s: %v", bucket, err)
				objectLockSetupError = err
				// Note: The entry will still be created, but we'll roll it back below
			} else {
				glog.V(3).Infof("PutBucketHandler: set ObjectLockConfig for bucket %s: %+v", bucket, objectLockConfig)
			}
		}
	}); err != nil {
		// If mkdir failed because another request created the bucket concurrently,
		// return the appropriate already-exists error instead of InternalError.
		if exist, checkErr := s3a.exists(s3a.option.BucketsPath, bucket, true); checkErr == nil && exist {
			glog.V(3).Infof("PutBucketHandler: bucket %s was created concurrently", bucket)
			errCode := s3a.existingBucketError(r, bucket, currentIdentityId, requestHasACL)
			if errCode == s3err.ErrBucketAlreadyOwnedByYou {
				s3a.healBucketOwnerIndex(bucket, currentIdentityId)
			}
			s3err.WriteErrorResponse(w, r, errCode)
			return
		}
		glog.Errorf("PutBucketHandler mkdir: %v", err)
		s3err.WriteErrorResponse(w, r, s3err.ErrInternalError)
		return
	}

	// If Object Lock setup failed, roll back the bucket creation
	// This ensures we don't leave a bucket without the requested Object Lock configuration
	if objectLockSetupError != nil {
		glog.Errorf("PutBucketHandler: rolling back bucket %s creation due to Object Lock setup failure: %v", bucket, objectLockSetupError)
		if deleteErr := s3a.rm(context.Background(), s3a.option.BucketsPath, bucket, true, true); deleteErr != nil {
			glog.Errorf("PutBucketHandler: failed to rollback bucket %s after Object Lock setup failure: %v", bucket, deleteErr)
		}
		s3err.WriteErrorResponse(w, r, s3err.ErrInternalError)
		return
	}

	// Remove bucket from negative cache after successful creation
	if s3a.bucketConfigCache != nil {
		s3a.bucketConfigCache.RemoveNegativeCache(bucket)
	}

	// Index the bucket under its owner; the metadata subscription would catch
	// up eventually, but writing here keeps ListBuckets read-your-writes.
	if currentIdentityId != "" {
		if err := s3a.addBucketToOwnerIndex(currentIdentityId, bucket, bucketCrtime); err != nil {
			glog.Warningf("PutBucketHandler: owner index add %s/%s: %v", currentIdentityId, bucket, err)
		}
	}

	w.Header().Set("Location", "/"+bucket)
	writeSuccessResponseEmpty(w, r)
}

// healBucketOwnerIndex re-creates the owner index entry for a bucket the
// caller owns, repairing holes left by crashes between bucket creation and
// index write.
func (s3a *S3ApiServer) healBucketOwnerIndex(bucket, identityId string) {
	if identityId == "" {
		return
	}
	config, errCode := s3a.getBucketConfig(bucket)
	if errCode != s3err.ErrNone || config.IdentityId != identityId {
		return
	}
	if err := s3a.addBucketToOwnerIndex(identityId, bucket, config.Crtime); err != nil {
		glog.V(1).Infof("owner index heal %s/%s: %v", identityId, bucket, err)
	}
}

func (s3a *S3ApiServer) DeleteBucketHandler(w http.ResponseWriter, r *http.Request) {

	bucket, _ := s3_constants.GetBucketAndObject(r)
	glog.V(3).Infof("DeleteBucketHandler %s", bucket)
	// The teardown below retries, and a failover walk repeats it once per
	// filer, so the backoff comes out of one allowance held here.
	r = r.WithContext(withFilerRetryBudget(r.Context(), filerRetryRequestBudget))

	if s3a.isTableBucket(bucket) {
		s3err.WriteErrorResponse(w, r, s3err.ErrAccessDenied)
		return
	}

	if err := s3a.checkBucket(r, bucket); err != s3err.ErrNone {
		s3err.WriteErrorResponse(w, r, err)
		return
	}

	// Check if bucket has object lock enabled
	bucketConfig, errCode := s3a.getBucketConfig(bucket)
	if errCode != s3err.ErrNone {
		s3err.WriteErrorResponse(w, r, errCode)
		return
	}

	// If object lock is enabled, check for objects with active locks
	if bucketConfig.ObjectLockConfig != nil {
		hasLockedObjects, checkErr := s3a.hasObjectsWithActiveLocks(r.Context(), bucket)
		if checkErr != nil {
			glog.Errorf("DeleteBucketHandler: failed to check for locked objects in bucket %s: %v", bucket, checkErr)
			s3err.WriteErrorResponse(w, r, s3err.ErrInternalError)
			return
		}
		if hasLockedObjects {
			glog.V(3).Infof("DeleteBucketHandler: bucket %s has objects with active object locks, cannot delete", bucket)
			s3err.WriteErrorResponse(w, r, s3err.ErrBucketNotEmpty)
			return
		}
	}

	if !s3a.option.AllowDeleteBucketNotEmpty {
		if hasUserObjects, err := s3a.bucketHasUserObjects(bucket); err != nil {
			glog.Errorf("failed to list bucket %s: %v", bucket, err)
			s3err.WriteErrorResponse(w, r, s3err.ErrInternalError)
			return
		} else if hasUserObjects {
			s3err.WriteErrorResponse(w, r, s3err.ErrBucketNotEmpty)
			return
		}
	}

	// Delete bucket directory first, then collection. This order ensures that if
	// collection deletion fails, the bucket directory is already gone, preventing
	// the "collection exists but bucket directory missing" inconsistency that blocks
	// bucket recreation. An orphaned collection is harmless and will be cleaned up
	// or reused when the bucket is recreated.
	err := s3a.rm(r.Context(), s3a.option.BucketsPath, bucket, false, true)
	if err != nil {
		s3err.WriteErrorResponse(w, r, s3err.ErrInternalError)
		return
	}

	if owner := bucketConfig.IdentityId; owner != "" {
		if err := s3a.removeBucketFromOwnerIndex(owner, bucket); err != nil {
			glog.Warningf("DeleteBucketHandler: owner index remove %s/%s: %v", owner, bucket, err)
		}
	}

	// Bounded on a background context: the bucket directory is already gone, so
	// this follow-up must survive a client disconnect, but it must not outlive the
	// client by an unbounded amount either.
	deleteCtx, cancelDelete := context.WithTimeout(context.Background(), collectionDeleteTimeout)
	err = s3a.withFilerClient(deleteCtx, false, func(client filer_pb.SeaweedFilerClient) error {
		deleteCollectionRequest := &filer_pb.DeleteCollectionRequest{
			Collection: s3a.getCollectionName(bucket),
		}

		glog.V(1).Infof("delete collection: %v", deleteCollectionRequest)
		if _, err := client.DeleteCollection(deleteCtx, deleteCollectionRequest); err != nil {
			return fmt.Errorf("delete collection %s: %v", bucket, err)
		}

		return nil
	})
	timedOut := deleteCtx.Err() != nil
	cancelDelete()

	if err != nil {
		// Log but don't fail — the bucket directory is already removed, so the bucket
		// is effectively deleted. The orphaned collection will be cleaned up or reused.
		if timedOut {
			// Our own budget, not a refusal: the master carries on deleting once asked.
			glog.Warningf("DeleteBucketHandler: stopped waiting for the collection delete for bucket %s: %v", bucket, err)
		} else {
			glog.Errorf("DeleteBucketHandler: failed to delete collection for bucket %s: %v", bucket, err)
		}
	}

	// Clean up bucket-related caches, locks, and metrics after successful deletion
	s3a.invalidateBucketConfigCache(bucket)
	stats_collect.DeleteBucketMetrics(bucket)

	// Prune identity actions that were scoped to this bucket via s3.configure.
	// Use a bounded background context so the cleanup survives client disconnect;
	// the bucket is already gone and this is best-effort bookkeeping.
	if s3a.iam != nil {
		pruneCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
		if _, err := s3a.iam.PruneBucketFromConfiguration(pruneCtx, bucket); err != nil {
			glog.Errorf("DeleteBucketHandler: failed to prune IAM actions for bucket %s: %v", bucket, err)
		}
		cancel()
	}

	s3err.WriteEmptyResponse(w, r, http.StatusNoContent)
}

// bucketHasUserObjects checks whether a bucket contains any non-special entries.
// Special entries (.uploads, *.versions) are internal to S3 and don't count as user objects.
func (s3a *S3ApiServer) bucketHasUserObjects(bucket string) (bool, error) {
	bucketPath := s3a.option.BucketsPath + "/" + bucket
	startFrom := ""
	// Start with a small batch — most non-empty buckets have a real object early.
	// If we only find special entries, switch to larger batches to page through quickly.
	limit := uint32(10)
	for {
		entries, isLast, err := s3a.list(bucketPath, "", startFrom, false, limit)
		if err != nil {
			return false, err
		}
		for _, entry := range entries {
			if entry.Name != s3_constants.MultipartUploadsFolder &&
				!strings.HasSuffix(entry.Name, s3_constants.VersionsFolder) {
				return true, nil
			}
			startFrom = entry.Name
		}
		if isLast {
			return false, nil
		}
		limit = 1000
	}
}

// hasObjectsWithActiveLocks checks if any objects in the bucket have active retention or legal hold
// Delegates to the shared HasObjectsWithActiveLocks function in object_lock_utils.go
func (s3a *S3ApiServer) hasObjectsWithActiveLocks(ctx context.Context, bucket string) (bool, error) {
	bucketPath := s3a.option.BucketsPath + "/" + bucket
	var hasLocks bool
	var checkErr error

	err := s3a.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error {
		hasLocks, checkErr = HasObjectsWithActiveLocks(ctx, client, bucketPath)
		return checkErr
	})
	if err != nil {
		return false, err
	}
	return hasLocks, nil
}

func (s3a *S3ApiServer) HeadBucketHandler(w http.ResponseWriter, r *http.Request) {

	bucket, _ := s3_constants.GetBucketAndObject(r)
	glog.V(3).Infof("HeadBucketHandler %s", bucket)

	if _, err := s3a.getBucketEntry(bucket); err != nil {
		if errors.Is(err, filer_pb.ErrNotFound) {
			s3err.WriteErrorResponse(w, r, s3err.ErrNoSuchBucket)
			return
		}
		glog.Errorf("HeadBucketHandler: failed to get bucket entry for %s: %v", bucket, err)
		s3err.WriteErrorResponse(w, r, s3err.ErrInternalError)
		return
	}

	writeSuccessResponseEmpty(w, r)
}

func (s3a *S3ApiServer) checkBucket(r *http.Request, bucket string) s3err.ErrorCode {
	// Use cached bucket config instead of direct getEntry call (optimization)
	config, errCode := s3a.getBucketConfig(bucket)
	if errCode != s3err.ErrNone {
		return errCode
	}

	//if iam is enabled, the access was already checked before
	if s3a.iam.isEnabled() {
		return s3err.ErrNone
	}
	if !s3a.hasAccess(r, config.IdentityId) {
		return s3err.ErrAccessDenied
	}
	return s3err.ErrNone
}

// ErrAutoCreatePermissionDenied is returned when a user lacks permission to auto-create buckets
var ErrAutoCreatePermissionDenied = errors.New("permission denied - requires Admin permission")

// ErrAutoCreateDisabled is returned when bucket auto-creation is disabled by configuration
var ErrAutoCreateDisabled = errors.New("bucket auto-creation is disabled")

// ErrInvalidBucketName is returned when a bucket name doesn't meet S3 naming requirements
var ErrInvalidBucketName = errors.New("invalid bucket name")

// existingBucketError returns the error for a PutBucket whose target bucket
// already exists: BucketAlreadyOwnedByYou for an idempotent recreate by the
// owner, or BucketAlreadyExists when the name is owned by someone else or the
// request conflicts with the existing bucket (a different Object Lock setting,
// or an ACL on the request or the existing bucket).
func (s3a *S3ApiServer) existingBucketError(r *http.Request, bucket, currentIdentityId string, requestHasACL bool) s3err.ErrorCode {
	entry, err := s3a.getEntry(s3a.option.BucketsPath, bucket)
	if err != nil {
		// We just observed the bucket exists but can't read it; report it as taken.
		glog.Errorf("PutBucketHandler: failed to read existing bucket %s: %v", bucket, err)
		return s3err.ErrBucketAlreadyExists
	}

	var existingOwnerId string
	if entry.Extended != nil {
		if id, ok := entry.Extended[s3_constants.AmzIdentityId]; ok {
			existingOwnerId = string(id)
		}
	}

	// Different owner: the name is taken in the shared namespace.
	if existingOwnerId != "" && existingOwnerId != currentIdentityId {
		glog.V(3).Infof("PutBucketHandler: bucket %s owned by %s, requested by %s", bucket, existingOwnerId, currentIdentityId)
		return s3err.ErrBucketAlreadyExists
	}

	// Same owner (or an unowned bucket the caller can claim). Recreating your own
	// bucket is idempotent and returns BucketAlreadyOwnedByYou, unless the request
	// conflicts with the existing bucket: a different Object Lock setting, or an ACL
	// on the request or the existing bucket.
	// (s3-tests: test_bucket_create_exists vs test_bucket_recreate_*_acl.)
	if requestHasACL {
		return s3err.ErrBucketAlreadyExists
	}

	objectLockRequested := strings.EqualFold(r.Header.Get(s3_constants.AmzBucketObjectLockEnabled), "true")
	bucketConfig, errCode := s3a.getBucketConfig(bucket)
	if errCode != s3err.ErrNone {
		// Can't read the existing bucket's settings, so we can't tell whether this
		// recreate conflicts; surface the failure instead of assuming idempotency.
		glog.Errorf("PutBucketHandler: failed to get bucket config for %s: %v", bucket, errCode)
		return errCode
	}
	currentObjectLockEnabled := bucketConfig.ObjectLockConfig != nil &&
		bucketConfig.ObjectLockConfig.ObjectLockEnabled == s3_constants.ObjectLockEnabled
	if objectLockRequested != currentObjectLockEnabled || len(bucketConfig.ACL) > 0 {
		glog.V(3).Infof("PutBucketHandler: bucket %s already exists", bucket)
		return s3err.ErrBucketAlreadyExists
	}

	glog.V(3).Infof("PutBucketHandler: bucket %s already owned by requester", bucket)
	return s3err.ErrBucketAlreadyOwnedByYou
}

// hasExplicitBucketACL reports whether the request carries an explicit, non-default
// bucket ACL via a canned ACL header (other than "private") or grant headers.
func hasExplicitBucketACL(r *http.Request) bool {
	if canned := r.Header.Get(s3_constants.AmzCannedAcl); canned != "" && !strings.EqualFold(canned, s3_constants.CannedAclPrivate) {
		return true
	}
	for _, h := range []string{s3_constants.AmzAclFullControl, s3_constants.AmzAclRead, s3_constants.AmzAclReadAcp, s3_constants.AmzAclWrite, s3_constants.AmzAclWriteAcp} {
		if r.Header.Get(h) != "" {
			return true
		}
	}
	return false
}

// setBucketOwner creates a function that sets the bucket owner from the request context
func setBucketOwner(r *http.Request) func(entry *filer_pb.Entry) {
	currentIdentityId := s3_constants.GetIdentityNameFromContext(r)
	// Record the canonical account id too so GetBucketAcl can report the bucket
	// owner instead of whoever is reading (e.g. an admin or another account).
	accountId := r.Header.Get(s3_constants.AmzAccountId)
	return func(entry *filer_pb.Entry) {
		if currentIdentityId == "" && accountId == "" {
			return
		}
		if entry.Extended == nil {
			entry.Extended = make(map[string][]byte)
		}
		if currentIdentityId != "" {
			entry.Extended[s3_constants.AmzIdentityId] = []byte(currentIdentityId)
		}
		if accountId != "" {
			entry.Extended[s3_constants.ExtAmzOwnerKey] = []byte(accountId)
		}
	}
}

// autoCreateBucket creates a bucket if it doesn't exist, setting the owner from the request context
// Only users with admin permissions are allowed to auto-create buckets
func (s3a *S3ApiServer) autoCreateBucket(r *http.Request, bucket string) error {
	// Validate the bucket name before auto-creating
	if err := s3bucket.VerifyS3BucketName(bucket); err != nil {
		return fmt.Errorf("auto-create bucket %s: %w", bucket, errors.Join(ErrInvalidBucketName, err))
	}

	if !s3a.option.AutoCreateBucket {
		return fmt.Errorf("auto-create bucket %s: %w", bucket, ErrAutoCreateDisabled)
	}

	// Check if user has admin permissions
	if !s3a.isUserAdmin(r) {
		return fmt.Errorf("auto-create bucket %s: %w", bucket, ErrAutoCreatePermissionDenied)
	}

	identityId := s3_constants.GetIdentityNameFromContext(r)
	var bucketCrtime int64
	if err := s3a.mkdir(s3a.option.BucketsPath, bucket, func(entry *filer_pb.Entry) {
		bucketCrtime = entry.Attributes.Crtime
		setBucketOwner(r)(entry)
	}); err != nil {
		// In case of a race condition where another request created the bucket
		// in the meantime, check for existence before returning an error.
		if exist, err2 := s3a.exists(s3a.option.BucketsPath, bucket, true); err2 != nil {
			glog.Warningf("autoCreateBucket: failed to check existence for bucket %s: %v", bucket, err2)
			return fmt.Errorf("failed to auto-create bucket %s: %w", bucket, errors.Join(err, err2))
		} else if exist {
			// The bucket exists, which is fine. However, we should ensure it has an owner.
			// If it was created by a concurrent request that didn't set an owner,
			// we'll set it here to ensure consistency.
			if entry, getErr := s3a.getEntry(s3a.option.BucketsPath, bucket); getErr == nil {
				if entry.Extended == nil || len(entry.Extended[s3_constants.AmzIdentityId]) == 0 {
					// No owner set, assign current admin as owner
					setBucketOwner(r)(entry)
					if updateErr := s3a.updateEntry(s3a.option.BucketsPath, entry); updateErr != nil {
						glog.Warningf("autoCreateBucket: failed to set owner for existing bucket %s: %v", bucket, updateErr)
					} else {
						glog.V(1).Infof("Set owner for existing bucket %s (created by concurrent request)", bucket)
						if owner := bucketEntryOwner(entry); owner != "" {
							if indexErr := s3a.addBucketToOwnerIndex(owner, bucket, entry.Attributes.Crtime); indexErr != nil {
								glog.Warningf("autoCreateBucket: owner index add %s/%s: %v", owner, bucket, indexErr)
							}
						}
					}
				}
			} else {
				glog.Warningf("autoCreateBucket: failed to get entry for existing bucket %s: %v", bucket, getErr)
			}
			// Remove bucket from negative cache — it exists now
			if s3a.bucketConfigCache != nil {
				s3a.bucketConfigCache.RemoveNegativeCache(bucket)
			}
			return nil
		}
		return fmt.Errorf("failed to auto-create bucket %s: %w", bucket, err)
	}

	// Remove bucket from negative cache after successful creation
	if s3a.bucketConfigCache != nil {
		s3a.bucketConfigCache.RemoveNegativeCache(bucket)
	}

	if identityId != "" {
		if err := s3a.addBucketToOwnerIndex(identityId, bucket, bucketCrtime); err != nil {
			glog.Warningf("autoCreateBucket: owner index add %s/%s: %v", identityId, bucket, err)
		}
	}

	glog.V(1).Infof("Auto-created bucket %s", bucket)
	return nil
}

// handleAutoCreateBucket attempts to auto-create a bucket and writes appropriate error responses
// Returns true if the bucket was created successfully or already exists, false if an error was written
func (s3a *S3ApiServer) handleAutoCreateBucket(w http.ResponseWriter, r *http.Request, bucket, handlerName string) bool {
	if err := s3a.autoCreateBucket(r, bucket); err != nil {
		glog.Warningf("%s: %v", handlerName, err)
		// Check for specific errors to return appropriate S3 error codes
		if errors.Is(err, ErrAutoCreateDisabled) {
			s3err.WriteErrorResponse(w, r, s3err.ErrNoSuchBucket)
		} else if errors.Is(err, ErrInvalidBucketName) {
			s3err.WriteErrorResponse(w, r, s3err.ErrInvalidBucketName)
		} else if errors.Is(err, ErrAutoCreatePermissionDenied) {
			s3err.WriteErrorResponse(w, r, s3err.ErrAccessDenied)
		} else {
			s3err.WriteErrorResponse(w, r, s3err.ErrInternalError)
		}
		return false
	}
	return true
}

// hasAccess checks the caller against the identity recorded at bucket
// creation; buckets with no recorded identity are open to any caller.
func (s3a *S3ApiServer) hasAccess(r *http.Request, bucketIdentityId string) bool {
	// Check if user is properly authenticated as admin through IAM system
	if s3a.isUserAdmin(r) {
		return true
	}

	if bucketIdentityId == "" {
		return true
	}

	// Get authenticated identity from context (secure, cannot be spoofed)
	identityId := s3_constants.GetIdentityNameFromContext(r)
	if identityId != bucketIdentityId {
		glog.V(3).Infof("hasAccess: %s != %s", identityId, bucketIdentityId)
		return false
	}
	return true
}

// isUserAdmin securely checks if the authenticated user is an admin
// This validates admin status through proper IAM authentication, not spoofable headers
func (s3a *S3ApiServer) isUserAdmin(r *http.Request) bool {
	// Reuse the identity the Auth middleware stored; re-authenticating here would
	// fail once the request body has been read.
	if identityObj := s3_constants.GetIdentityFromContext(r); identityObj != nil {
		if identity, ok := identityObj.(*Identity); ok {
			return identity != nil && identity.isAdmin()
		}
	}
	if s3a.iam == nil {
		return false
	}
	// Use a minimal admin action to authenticate and check admin status
	adminAction := Action("Admin")
	identity, errCode := s3a.iam.authRequest(r, adminAction)
	if errCode != s3err.ErrNone {
		return false
	}

	// Check if the authenticated identity has admin privileges
	return identity != nil && identity.isAdmin()
}

// isBucketPublicRead checks if a bucket allows anonymous read access based on its cached ACL status
func (s3a *S3ApiServer) isBucketPublicRead(bucket string) bool {
	// Get bucket configuration which contains cached public-read status
	config, errCode := s3a.getBucketConfig(bucket)
	if errCode != s3err.ErrNone {
		glog.V(4).Infof("isBucketPublicRead: failed to get bucket config for %s: %v", bucket, errCode)
		return false
	}

	glog.V(4).Infof("isBucketPublicRead: bucket=%s, IsPublicRead=%v", bucket, config.IsPublicRead)
	// Return the cached public-read status (no JSON parsing needed)
	return config.IsPublicRead
}

// isPublicReadGrants checks if the grants allow public read access
func isPublicReadGrants(grants []*s3.Grant) bool {
	for _, grant := range grants {
		if grant.Grantee != nil && grant.Grantee.URI != nil && grant.Permission != nil {
			// Check for AllUsers group with Read permission
			if *grant.Grantee.URI == s3_constants.GranteeGroupAllUsers &&
				(*grant.Permission == s3_constants.PermissionRead || *grant.Permission == s3_constants.PermissionFullControl) {
				return true
			}
		}
	}
	return false
}

// buildResourceARN builds a resource ARN from bucket and object
// Used by the policy engine wrapper
func buildResourceARN(bucket, object string) string {
	if object == "" || object == "/" {
		return fmt.Sprintf("arn:aws:s3:::%s", bucket)
	}
	// Remove leading slash if present
	object = strings.TrimPrefix(object, "/")
	return fmt.Sprintf("arn:aws:s3:::%s/%s", bucket, object)
}

// AuthWithPublicRead creates an auth wrapper that allows anonymous access for public-read buckets
func (s3a *S3ApiServer) AuthWithPublicRead(handler http.HandlerFunc, action Action) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		bucket, object := s3_constants.GetBucketAndObject(r)
		authType := getRequestAuthType(r)
		isAnonymous := authType == authTypeAnonymous

		glog.V(4).Infof("AuthWithPublicRead: bucket=%s, object=%s, authType=%v, isAnonymous=%v", bucket, object, authType, isAnonymous)

		// For anonymous requests, check if bucket allows public read via ACLs or bucket policies
		if isAnonymous {
			// First check ACL-based public access
			isPublic := s3a.isBucketPublicRead(bucket)
			glog.V(4).Infof("AuthWithPublicRead: bucket=%s, isPublicACL=%v", bucket, isPublic)
			if isPublic {
				glog.V(3).Infof("AuthWithPublicRead: allowing anonymous access to public-read bucket %s (ACL)", bucket)
				handler(w, r)
				return
			}

			// Check bucket policy for anonymous access using the policy engine
			principal := "*" // Anonymous principal
			// Evaluate bucket policy (objectEntry nil - not yet fetched)
			allowed, evaluated, err := s3a.policyEngine.EvaluatePolicy(bucket, object, string(action), principal, r, nil, nil)
			if err != nil {
				// SECURITY: Fail-close on policy evaluation errors
				// If we can't evaluate the policy, deny access rather than falling through to IAM
				glog.Errorf("AuthWithPublicRead: error evaluating bucket policy for %s/%s: %v - denying access", bucket, object, err)
				s3err.WriteErrorResponse(w, r, s3err.ErrAccessDenied)
				return
			} else if evaluated {
				// A bucket policy exists and was evaluated with a matching statement
				if allowed {
					// Policy explicitly allows anonymous access
					glog.V(3).Infof("AuthWithPublicRead: allowing anonymous access to bucket %s (bucket policy)", bucket)
					handler(w, r)
					return
				} else {
					// Policy explicitly denies anonymous access
					glog.V(3).Infof("AuthWithPublicRead: bucket policy explicitly denies anonymous access to %s/%s", bucket, object)
					s3err.WriteErrorResponse(w, r, s3err.ErrAccessDenied)
					return
				}
			}
			// No matching policy statement - fall through to check ACLs and then IAM auth
			glog.V(3).Infof("AuthWithPublicRead: no bucket policy match for %s, checking ACLs", bucket)
		}

		// For all authenticated requests and anonymous requests to non-public buckets,
		// use normal IAM auth to enforce policies
		s3a.iam.Auth(handler, action)(w, r)
	}
}

// GetBucketAclHandler Get Bucket ACL
// https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetBucketAcl.html
func (s3a *S3ApiServer) GetBucketAclHandler(w http.ResponseWriter, r *http.Request) {
	// collect parameters
	bucket, _ := s3_constants.GetBucketAndObject(r)
	glog.V(3).Infof("GetBucketAclHandler %s", bucket)

	if err := s3a.checkBucket(r, bucket); err != s3err.ErrNone {
		s3err.WriteErrorResponse(w, r, err)
		return
	}

	// Report the bucket's owner (recorded at creation or by PutBucketAcl), not the
	// caller; fall back to the caller only when no owner was persisted. Likewise
	// return any stored ACL, defaulting to the owner's full-control grant.
	ownerId := r.Header.Get(s3_constants.AmzAccountId)
	var storedGrants []*s3.Grant
	if bucketConfig, errCode := s3a.getBucketConfig(bucket); errCode == s3err.ErrNone {
		storedGrants = parseAclGrants(bucketConfig.ACL)
		if bucketConfig.Owner != "" {
			ownerId = bucketConfig.Owner
		}
	}
	ownerDisplayName := s3a.iam.GetAccountNameById(ownerId)
	response := AccessControlPolicy{
		Owner: CanonicalUser{
			ID:          ownerId,
			DisplayName: ownerDisplayName,
		},
		AccessControlList: buildAccessControlList(s3a.iam, storedGrants, ownerId, ownerDisplayName),
	}
	writeSuccessResponseXML(w, r, response)
}

// PutBucketAclHandler Put bucket ACL
// https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutBucketAcl.html //
func (s3a *S3ApiServer) PutBucketAclHandler(w http.ResponseWriter, r *http.Request) {
	// collect parameters
	bucket, _ := s3_constants.GetBucketAndObject(r)
	glog.V(3).Infof("PutBucketAclHandler %s", bucket)

	if err := s3a.checkBucket(r, bucket); err != s3err.ErrNone {
		s3err.WriteErrorResponse(w, r, err)
		return
	}

	// Get account information for ACL processing
	amzAccountId := r.Header.Get(s3_constants.AmzAccountId)

	// Get bucket ownership settings (these would be used for ownership validation in a full implementation)
	bucketOwnership := ""         // Default/simplified for now - in a full implementation this would be retrieved from bucket config
	bucketOwnerId := amzAccountId // Simplified - bucket owner is current account

	// Use the existing ACL parsing logic to handle both canned ACLs and XML body
	grants, errCode := ExtractAcl(r, s3a.iam, bucketOwnership, bucketOwnerId, amzAccountId, amzAccountId)
	if errCode != s3err.ErrNone {
		s3err.WriteErrorResponse(w, r, errCode)
		return
	}

	glog.V(3).Infof("PutBucketAclHandler: bucket=%s, extracted %d grants", bucket, len(grants))
	isPublic := isPublicReadGrants(grants)
	glog.V(3).Infof("PutBucketAclHandler: bucket=%s, isPublicReadGrants=%v", bucket, isPublic)

	// Store the bucket ACL in bucket metadata
	errCode = s3a.updateBucketConfig(bucket, func(config *BucketConfig) error {
		if len(grants) > 0 {
			grantsBytes, err := json.Marshal(grants)
			if err != nil {
				glog.Errorf("PutBucketAclHandler: failed to marshal grants: %v", err)
				return err
			}
			config.ACL = grantsBytes
			// Cache the public-read status to avoid JSON parsing on every request
			config.IsPublicRead = isPublicReadGrants(grants)
			glog.V(4).Infof("PutBucketAclHandler: bucket=%s, setting IsPublicRead=%v", bucket, config.IsPublicRead)
		} else {
			config.ACL = nil
			config.IsPublicRead = false
		}
		config.Owner = amzAccountId
		return nil
	})

	if errCode != s3err.ErrNone {
		s3err.WriteErrorResponse(w, r, errCode)
		return
	}

	glog.V(3).Infof("PutBucketAclHandler: Successfully stored ACL for bucket %s with %d grants", bucket, len(grants))

	// Small delay to ensure ACL propagation across distributed caches
	// This prevents race conditions in tests where anonymous access is attempted immediately after ACL change
	time.Sleep(50 * time.Millisecond)

	writeSuccessResponseEmpty(w, r)
}

// GetBucketLifecycleConfigurationHandler Get Bucket Lifecycle configuration
// https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetBucketLifecycleConfiguration.html
func (s3a *S3ApiServer) GetBucketLifecycleConfigurationHandler(w http.ResponseWriter, r *http.Request) {
	// collect parameters
	bucket, _ := s3_constants.GetBucketAndObject(r)
	glog.V(3).Infof("GetBucketLifecycleConfigurationHandler %s", bucket)

	if err := s3a.checkBucket(r, bucket); err != s3err.ErrNone {
		s3err.WriteErrorResponse(w, r, err)
		return
	}
	if lifecycleXML, transitionMinimumObjectSize, found, errCode := s3a.getStoredBucketLifecycleConfiguration(bucket); errCode != s3err.ErrNone {
		s3err.WriteErrorResponse(w, r, errCode)
		return
	} else if found {
		w.Header().Set(bucketLifecycleTransitionMinimumObjectSizeHeader, transitionMinimumObjectSize)
		writeSuccessResponseXMLBytes(w, r, lifecycleXML)
		return
	}
	// ReadFilerConfFromFilers provides multi-filer failover
	fc, err := filer.ReadFilerConfFromFilers(s3a.option.Filers, s3a.option.GrpcDialOption, nil)
	if err != nil {
		glog.Errorf("GetBucketLifecycleConfigurationHandler: %s", err)
		s3err.WriteErrorResponse(w, r, s3err.ErrInternalError)
		return
	}
	ttls := fc.GetCollectionTtls(s3a.getCollectionName(bucket))
	if len(ttls) == 0 {
		s3err.WriteErrorResponse(w, r, s3err.ErrNoSuchLifecycleConfiguration)
		return
	}

	response := lifecycle_xml.Lifecycle{}
	// Sort locationPrefixes to ensure consistent ordering of lifecycle rules
	var locationPrefixes []string
	for locationPrefix := range ttls {
		locationPrefixes = append(locationPrefixes, locationPrefix)
	}
	sort.Strings(locationPrefixes)

	for _, locationPrefix := range locationPrefixes {
		internalTtl := ttls[locationPrefix]
		ttl, _ := needle.ReadTTL(internalTtl)
		days := int(ttl.Minutes() / 60 / 24)
		if days == 0 {
			continue
		}
		prefix, found := strings.CutPrefix(locationPrefix, fmt.Sprintf("%s/%s/", s3a.option.BucketsPath, bucket))
		if !found {
			continue
		}
		response.Rules = append(response.Rules, lifecycle_xml.Rule{
			ID:         prefix,
			Status:     lifecycle_xml.Enabled,
			Prefix:     lifecycle_xml.NewPrefix(prefix),
			Expiration: lifecycle_xml.NewExpirationDays(days),
		})
	}

	if len(response.Rules) > 0 {
		w.Header().Set(bucketLifecycleTransitionMinimumObjectSizeHeader, defaultLifecycleTransitionMinimumObjectSize)
	}
	writeSuccessResponseXML(w, r, response)
}

// PutBucketLifecycleConfigurationHandler Put Bucket Lifecycle configuration
// https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutBucketLifecycleConfiguration.html
func (s3a *S3ApiServer) PutBucketLifecycleConfigurationHandler(w http.ResponseWriter, r *http.Request) {
	// collect parameters
	bucket, _ := s3_constants.GetBucketAndObject(r)
	glog.V(3).Infof("PutBucketLifecycleConfigurationHandler %s", bucket)

	if err := s3a.checkBucket(r, bucket); err != s3err.ErrNone {
		s3err.WriteErrorResponse(w, r, err)
		return
	}

	r.Body = http.MaxBytesReader(w, r.Body, maxBucketLifecycleConfigurationSize)
	lifecycleXML, err := io.ReadAll(r.Body)
	if err != nil {
		glog.Warningf("PutBucketLifecycleConfigurationHandler read body: %s", err)
		var maxBytesErr *http.MaxBytesError
		if errors.As(err, &maxBytesErr) {
			s3err.WriteErrorResponse(w, r, s3err.ErrEntityTooLarge)
			return
		}
		s3err.WriteErrorResponse(w, r, s3err.ErrInvalidRequest)
		return
	}

	lifeCycleConfig := lifecycle_xml.Lifecycle{}
	if err := xmlDecoder(bytes.NewReader(lifecycleXML), &lifeCycleConfig, int64(len(lifecycleXML))); err != nil {
		glog.Warningf("PutBucketLifecycleConfigurationHandler xml decode: %s", err)
		s3err.WriteErrorResponse(w, r, s3err.ErrMalformedXML)
		return
	}

	// Reject Transition rules — they require storage class migration
	// infrastructure that does not exist yet. Validate before touching
	// any backing state so a malformed PUT can't half-apply.
	for _, rule := range lifeCycleConfig.Rules {
		if rule.Status != lifecycle_xml.Enabled {
			continue
		}
		if rule.Transition.Set() || rule.NoncurrentVersionTransition.Set() {
			s3err.WriteErrorResponse(w, r, s3err.ErrNotImplemented)
			return
		}
	}

	// Migration: clear any day-TTL filer.conf entries this handler
	// installed in older builds. Per-write TTL is now driven by the
	// LifecycleTTLResolver constructed off the stored XML, so leaving a
	// stale day-TTL entry under /buckets/<bucket>/ would double-stamp
	// (volume server expires under the old rule) or contradict the new
	// XML after a rule change. The add path is gone — this loop only
	// shrinks the conf, never grows it.
	if err := s3a.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error {
		return filer.ClearBucketLifecycleDayTTLs(context.Background(), client, s3a.option.BucketsPath, bucket, s3a.getCollectionName(bucket))
	}); err != nil {
		glog.Errorf("PutBucketLifecycleConfigurationHandler clear legacy day-TTLs: %s", err)
		s3err.WriteErrorResponse(w, r, s3err.ErrInternalError)
		return
	}

	if errCode := s3a.storeBucketLifecycleConfiguration(bucket, lifecycleXML, r.Header.Get(bucketLifecycleTransitionMinimumObjectSizeHeader)); errCode != s3err.ErrNone {
		s3err.WriteErrorResponse(w, r, errCode)
		return
	}

	writeSuccessResponseEmpty(w, r)
}

// DeleteBucketLifecycleHandler Delete Bucket Lifecycle
// https://docs.aws.amazon.com/AmazonS3/latest/API/API_DeleteBucketLifecycle.html
func (s3a *S3ApiServer) DeleteBucketLifecycleHandler(w http.ResponseWriter, r *http.Request) {
	// collect parameters
	bucket, _ := s3_constants.GetBucketAndObject(r)
	glog.V(3).Infof("DeleteBucketLifecycleHandler %s", bucket)

	if err := s3a.checkBucket(r, bucket); err != s3err.ErrNone {
		s3err.WriteErrorResponse(w, r, err)
		return
	}

	// Same legacy day-TTL migration as the PUT handler.
	if err := s3a.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error {
		return filer.ClearBucketLifecycleDayTTLs(context.Background(), client, s3a.option.BucketsPath, bucket, s3a.getCollectionName(bucket))
	}); err != nil {
		glog.Errorf("DeleteBucketLifecycleHandler clear legacy day-TTLs: %s", err)
		s3err.WriteErrorResponse(w, r, s3err.ErrInternalError)
		return
	}

	if errCode := s3a.clearStoredBucketLifecycleConfiguration(bucket); errCode != s3err.ErrNone {
		s3err.WriteErrorResponse(w, r, errCode)
		return
	}

	s3err.WriteEmptyResponse(w, r, http.StatusNoContent)
}

// GetBucketLocationHandler Get bucket location
// https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetBucketLocation.html
func (s3a *S3ApiServer) GetBucketLocationHandler(w http.ResponseWriter, r *http.Request) {
	bucket, _ := s3_constants.GetBucketAndObject(r)

	if err := s3a.checkBucket(r, bucket); err != s3err.ErrNone {
		s3err.WriteErrorResponse(w, r, err)
		return
	}

	writeSuccessResponseXML(w, r, CreateBucketConfiguration{})
}

// GetBucketRequestPaymentHandler Get bucket location
// https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetBucketRequestPayment.html
func (s3a *S3ApiServer) GetBucketRequestPaymentHandler(w http.ResponseWriter, r *http.Request) {
	writeSuccessResponseXML(w, r, RequestPaymentConfiguration{Payer: "BucketOwner"})
}

// PutBucketOwnershipControls https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutBucketOwnershipControls.html
func (s3a *S3ApiServer) PutBucketOwnershipControls(w http.ResponseWriter, r *http.Request) {
	bucket, _ := s3_constants.GetBucketAndObject(r)
	glog.V(3).Infof("PutBucketOwnershipControls %s", bucket)

	errCode := s3a.checkAccessByOwnership(r, bucket)
	if errCode != s3err.ErrNone {
		s3err.WriteErrorResponse(w, r, errCode)
		return
	}

	if r.Body == nil || r.Body == http.NoBody {
		s3err.WriteErrorResponse(w, r, s3err.ErrInvalidRequest)
		return
	}

	var v s3.OwnershipControls
	defer util_http.CloseRequest(r)

	err := xmlutil.UnmarshalXML(&v, xml.NewDecoder(r.Body), "")
	if err != nil {
		s3err.WriteErrorResponse(w, r, s3err.ErrInvalidRequest)
		return
	}

	if len(v.Rules) != 1 || v.Rules[0] == nil || v.Rules[0].ObjectOwnership == nil {
		s3err.WriteErrorResponse(w, r, s3err.ErrInvalidRequest)
		return
	}

	printOwnership := true
	ownership := *v.Rules[0].ObjectOwnership
	switch ownership {
	case s3_constants.OwnershipObjectWriter:
	case s3_constants.OwnershipBucketOwnerPreferred:
	case s3_constants.OwnershipBucketOwnerEnforced:
		printOwnership = false
	default:
		s3err.WriteErrorResponse(w, r, s3err.ErrInvalidRequest)
		return
	}

	// Persist even when it matches the implicit default, so a later delete has something to remove.
	if errCode := s3a.setBucketOwnership(bucket, ownership); errCode != s3err.ErrNone {
		s3err.WriteErrorResponse(w, r, errCode)
		return
	}

	if printOwnership {
		result := &s3.PutBucketOwnershipControlsInput{
			OwnershipControls: &v,
		}
		s3err.WriteAwsXMLResponse(w, r, http.StatusOK, result)
	} else {
		writeSuccessResponseEmpty(w, r)
	}
}

// GetBucketOwnershipControls https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetBucketOwnershipControls.html
func (s3a *S3ApiServer) GetBucketOwnershipControls(w http.ResponseWriter, r *http.Request) {
	bucket, _ := s3_constants.GetBucketAndObject(r)
	glog.V(3).Infof("GetBucketOwnershipControls %s", bucket)

	errCode := s3a.checkAccessByOwnership(r, bucket)
	if errCode != s3err.ErrNone {
		s3err.WriteErrorResponse(w, r, errCode)
		return
	}

	// Get ownership using new bucket config system
	ownership, errCode := s3a.getBucketOwnership(bucket)
	if errCode == s3err.ErrNoSuchBucket {
		s3err.WriteErrorResponse(w, r, s3err.ErrNoSuchBucket)
		return
	} else if errCode != s3err.ErrNone {
		s3err.WriteErrorResponse(w, r, s3err.OwnershipControlsNotFoundError)
		return
	}

	result := &s3.PutBucketOwnershipControlsInput{
		OwnershipControls: &s3.OwnershipControls{
			Rules: []*s3.OwnershipControlsRule{
				{
					ObjectOwnership: &ownership,
				},
			},
		},
	}

	s3err.WriteAwsXMLResponse(w, r, http.StatusOK, result)
}

// DeleteBucketOwnershipControls https://docs.aws.amazon.com/AmazonS3/latest/API/API_DeleteBucketOwnershipControls.html
func (s3a *S3ApiServer) DeleteBucketOwnershipControls(w http.ResponseWriter, r *http.Request) {
	bucket, _ := s3_constants.GetBucketAndObject(r)
	glog.V(3).Infof("PutBucketOwnershipControls %s", bucket)

	errCode := s3a.checkAccessByOwnership(r, bucket)
	if errCode != s3err.ErrNone {
		s3err.WriteErrorResponse(w, r, errCode)
		return
	}

	bucketEntry, err := s3a.getEntry(s3a.option.BucketsPath, bucket)
	if err != nil {
		if errors.Is(err, filer_pb.ErrNotFound) {
			s3err.WriteErrorResponse(w, r, s3err.ErrNoSuchBucket)
			return
		}
		s3err.WriteErrorResponse(w, r, s3err.ErrInternalError)
		return
	}

	_, ok := bucketEntry.Extended[s3_constants.ExtOwnershipKey]
	if !ok {
		s3err.WriteErrorResponse(w, r, s3err.OwnershipControlsNotFoundError)
		return
	}

	delete(bucketEntry.Extended, s3_constants.ExtOwnershipKey)
	err = s3a.updateEntry(s3a.option.BucketsPath, bucketEntry)
	if err != nil {
		s3err.WriteErrorResponse(w, r, s3err.ErrInternalError)
		return
	}

	emptyOwnershipControls := &s3.OwnershipControls{
		Rules: []*s3.OwnershipControlsRule{},
	}
	s3err.WriteAwsXMLResponse(w, r, http.StatusOK, emptyOwnershipControls)
}

// GetBucketVersioningHandler Get Bucket Versioning status
// https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetBucketVersioning.html
func (s3a *S3ApiServer) GetBucketVersioningHandler(w http.ResponseWriter, r *http.Request) {
	bucket, _ := s3_constants.GetBucketAndObject(r)
	glog.V(3).Infof("GetBucketVersioning %s", bucket)

	if err := s3a.checkBucket(r, bucket); err != s3err.ErrNone {
		s3err.WriteErrorResponse(w, r, err)
		return
	}

	// Get versioning status using new bucket config system
	versioningStatus, errCode := s3a.getBucketVersioningStatus(bucket)
	if errCode != s3err.ErrNone {
		s3err.WriteErrorResponse(w, r, errCode)
		return
	}

	// AWS S3 behavior: If versioning was never configured, don't return Status field
	var response *s3.PutBucketVersioningInput
	if versioningStatus == "" {
		// No versioning configuration - return empty response (no Status field)
		response = &s3.PutBucketVersioningInput{
			VersioningConfiguration: &s3.VersioningConfiguration{},
		}
	} else {
		// Versioning was explicitly configured - return the status
		response = &s3.PutBucketVersioningInput{
			VersioningConfiguration: &s3.VersioningConfiguration{
				Status: aws.String(versioningStatus),
			},
		}
	}
	s3err.WriteAwsXMLResponse(w, r, http.StatusOK, response)
}

// PutBucketVersioningHandler Put bucket Versioning
// https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutBucketVersioning.html
func (s3a *S3ApiServer) PutBucketVersioningHandler(w http.ResponseWriter, r *http.Request) {
	bucket, _ := s3_constants.GetBucketAndObject(r)
	glog.V(3).Infof("PutBucketVersioning %s", bucket)

	if err := s3a.checkBucket(r, bucket); err != s3err.ErrNone {
		s3err.WriteErrorResponse(w, r, err)
		return
	}

	if r.Body == nil || r.Body == http.NoBody {
		s3err.WriteErrorResponse(w, r, s3err.ErrInvalidRequest)
		return
	}

	var versioningConfig s3.VersioningConfiguration
	defer util_http.CloseRequest(r)

	err := xmlutil.UnmarshalXML(&versioningConfig, xml.NewDecoder(r.Body), "")
	if err != nil {
		glog.Warningf("PutBucketVersioningHandler xml decode: %s", err)
		s3err.WriteErrorResponse(w, r, s3err.ErrMalformedXML)
		return
	}

	if versioningConfig.Status == nil {
		s3err.WriteErrorResponse(w, r, s3err.ErrInvalidRequest)
		return
	}

	status := *versioningConfig.Status
	if status != s3_constants.VersioningEnabled && status != s3_constants.VersioningSuspended {
		glog.Errorf("PutBucketVersioningHandler: invalid status '%s' for bucket %s", status, bucket)
		s3err.WriteErrorResponse(w, r, s3err.ErrInvalidRequest)
		return
	}

	// Check if trying to suspend versioning on a bucket with object lock enabled
	if status == s3_constants.VersioningSuspended {
		// Get bucket configuration to check for object lock
		bucketConfig, errCode := s3a.getBucketConfig(bucket)
		if errCode == s3err.ErrNone && bucketConfig.ObjectLockConfig != nil {
			// Object lock is enabled, cannot suspend versioning
			s3err.WriteErrorResponse(w, r, s3err.ErrInvalidBucketState)
			return
		}
	}

	// Update bucket versioning configuration using new bucket config system
	if errCode := s3a.setBucketVersioningStatus(bucket, status); errCode != s3err.ErrNone {
		glog.Errorf("PutBucketVersioningHandler save config: bucket=%s, status='%s', errCode=%d", bucket, status, errCode)
		s3err.WriteErrorResponse(w, r, errCode)
		return
	}

	writeSuccessResponseEmpty(w, r)
}
