package s3api

import (
	"context"
	"errors"
	"fmt"
	"net/http"

	"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"
)

// GetObjectAclHandler Get object ACL
// https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetObjectAcl.html
func (s3a *S3ApiServer) GetObjectAclHandler(w http.ResponseWriter, r *http.Request) {
	// collect parameters
	bucket, object := s3_constants.GetBucketAndObject(r)
	glog.V(3).Infof("GetObjectAclHandler %s %s", bucket, object)
	if err := s3a.validateTableBucketObjectPath(bucket, object); err != nil {
		s3err.WriteErrorResponse(w, r, s3err.ErrAccessDenied)
		return
	}

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

	// Check for specific version ID in query parameters
	versionId := r.URL.Query().Get("versionId")

	// Check if versioning is configured for the bucket (Enabled or Suspended)
	versioningConfigured, err := s3a.isVersioningConfigured(bucket)
	if err != nil {
		if err == filer_pb.ErrNotFound {
			s3err.WriteErrorResponse(w, r, s3err.ErrNoSuchBucket)
			return
		}
		glog.Errorf("GetObjectAclHandler: Error checking versioning status for bucket %s: %v", bucket, err)
		s3err.WriteErrorResponse(w, r, s3err.ErrInternalError)
		return
	}

	var entry *filer_pb.Entry

	if versioningConfigured {
		// Handle versioned object ACL retrieval - use same logic as GetObjectHandler
		if versionId != "" {
			// Request for specific version
			glog.V(2).Infof("GetObjectAclHandler: requesting ACL for specific version %s of %s/%s", versionId, bucket, object)
			entry, err = s3a.getSpecificObjectVersion(bucket, object, versionId)
		} else {
			// Request for latest version
			glog.V(2).Infof("GetObjectAclHandler: requesting ACL for latest version of %s/%s", bucket, object)
			entry, err = s3a.getLatestObjectVersion(bucket, object)
		}

		if err != nil {
			glog.Errorf("GetObjectAclHandler: Failed to get object version %s for %s/%s: %v", versionId, bucket, object, err)
			s3err.WriteErrorResponse(w, r, s3err.ErrNoSuchKey)
			return
		}

		// Check if this is a delete marker
		if entry.Extended != nil {
			if deleteMarker, exists := entry.Extended[s3_constants.ExtDeleteMarkerKey]; exists && string(deleteMarker) == "true" {
				s3err.WriteErrorResponse(w, r, s3err.ErrNoSuchKey)
				return
			}
		}
	} else {
		// Handle regular (non-versioned) object ACL retrieval
		entry, err = s3a.fetchObjectEntryRequired(bucket, object)
		if err != nil {
			if errors.Is(err, filer_pb.ErrNotFound) {
				s3err.WriteErrorResponse(w, r, s3err.ErrNoSuchKey)
				return
			}
			glog.Errorf("GetObjectAclHandler: error checking object %s/%s: %v", bucket, object, err)
			s3err.WriteErrorResponse(w, r, s3err.ErrInternalError)
			return
		}
	}

	if entry == nil {
		s3err.WriteErrorResponse(w, r, s3err.ErrNoSuchKey)
		return
	}

	// Get object owner from metadata, fallback to request account
	var objectOwner string
	var objectOwnerDisplayName string
	amzAccountId := r.Header.Get(s3_constants.AmzAccountId)

	if entry.Extended != nil {
		if ownerBytes, exists := entry.Extended[s3_constants.ExtAmzOwnerKey]; exists {
			objectOwner = string(ownerBytes)
		}
	}

	// Fallback to current account if no owner stored
	if objectOwner == "" {
		objectOwner = amzAccountId
	}

	objectOwnerDisplayName = s3a.iam.GetAccountNameById(objectOwner)

	// Build ACL response from stored ACL metadata (or the owner's default grant).
	response := AccessControlPolicy{
		Owner: CanonicalUser{
			ID:          objectOwner,
			DisplayName: objectOwnerDisplayName,
		},
		AccessControlList: buildAccessControlList(s3a.iam, GetAcpGrants(entry.Extended), objectOwner, objectOwnerDisplayName),
	}

	writeSuccessResponseXML(w, r, response)
}

// PutObjectAclHandler Put object ACL
// https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutObjectAcl.html
func (s3a *S3ApiServer) PutObjectAclHandler(w http.ResponseWriter, r *http.Request) {
	// collect parameters
	bucket, object := s3_constants.GetBucketAndObject(r)
	glog.V(3).Infof("PutObjectAclHandler %s %s", bucket, object)
	if err := s3a.validateTableBucketObjectPath(bucket, object); err != nil {
		s3err.WriteErrorResponse(w, r, s3err.ErrAccessDenied)
		return
	}

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

	// Check for specific version ID in query parameters
	versionId := r.URL.Query().Get("versionId")

	// Check if versioning is configured for the bucket (Enabled or Suspended)
	versioningConfigured, err := s3a.isVersioningConfigured(bucket)
	if err != nil {
		if err == filer_pb.ErrNotFound {
			s3err.WriteErrorResponse(w, r, s3err.ErrNoSuchBucket)
			return
		}
		glog.Errorf("PutObjectAclHandler: Error checking versioning status for bucket %s: %v", bucket, err)
		s3err.WriteErrorResponse(w, r, s3err.ErrInternalError)
		return
	}

	var entry *filer_pb.Entry

	if versioningConfigured {
		// Handle versioned object ACL modification - use same logic as GetObjectHandler
		if versionId != "" {
			// Request for specific version
			glog.V(2).Infof("PutObjectAclHandler: modifying ACL for specific version %s of %s/%s", versionId, bucket, object)
			entry, err = s3a.getSpecificObjectVersion(bucket, object, versionId)
		} else {
			// Request for latest version
			glog.V(2).Infof("PutObjectAclHandler: modifying ACL for latest version of %s/%s", bucket, object)
			entry, err = s3a.getLatestObjectVersion(bucket, object)
		}

		if err != nil {
			glog.Errorf("PutObjectAclHandler: Failed to get object version %s for %s/%s: %v", versionId, bucket, object, err)
			s3err.WriteErrorResponse(w, r, s3err.ErrNoSuchKey)
			return
		}

		// Check if this is a delete marker
		if entry.Extended != nil {
			if deleteMarker, exists := entry.Extended[s3_constants.ExtDeleteMarkerKey]; exists && string(deleteMarker) == "true" {
				s3err.WriteErrorResponse(w, r, s3err.ErrNoSuchKey)
				return
			}
		}
	} else {
		// Handle regular (non-versioned) object ACL modification
		entry, err = s3a.fetchObjectEntryRequired(bucket, object)
		if err != nil {
			if errors.Is(err, filer_pb.ErrNotFound) {
				s3err.WriteErrorResponse(w, r, s3err.ErrNoSuchKey)
				return
			}
			glog.Errorf("PutObjectAclHandler: error checking object %s/%s: %v", bucket, object, err)
			s3err.WriteErrorResponse(w, r, s3err.ErrInternalError)
			return
		}
	}

	if entry == nil {
		s3err.WriteErrorResponse(w, r, s3err.ErrNoSuchKey)
		return
	}

	// Get current object owner from metadata
	var objectOwner string
	amzAccountId := r.Header.Get(s3_constants.AmzAccountId)

	if entry.Extended != nil {
		if ownerBytes, exists := entry.Extended[s3_constants.ExtAmzOwnerKey]; exists {
			objectOwner = string(ownerBytes)
		}
	}

	// Fallback to current account if no owner stored
	if objectOwner == "" {
		objectOwner = amzAccountId
	}

	// **PERMISSION CHECKS**

	// 1. Check if user is admin (admins can modify any ACL)
	if !s3a.isUserAdmin(r) {
		// 2. Check object ownership - only object owner can modify ACL (unless admin)
		if objectOwner != amzAccountId {
			glog.V(3).Infof("PutObjectAclHandler: Access denied - user %s is not owner of object %s/%s (owner: %s)",
				amzAccountId, bucket, object, objectOwner)
			s3err.WriteErrorResponse(w, r, s3err.ErrAccessDenied)
			return
		}

		// 3. Check object-level WRITE_ACP permission
		// Create the specific action for this object
		writeAcpAction := Action(fmt.Sprintf("WriteAcp:%s/%s", bucket, object))
		identity, errCode := s3a.iam.authRequest(r, writeAcpAction)
		if errCode != s3err.ErrNone {
			glog.V(3).Infof("PutObjectAclHandler: Auth failed for WriteAcp action on %s/%s: %v", bucket, object, errCode)
			s3err.WriteErrorResponse(w, r, s3err.ErrAccessDenied)
			return
		}

		// 4. Verify the authenticated identity can perform WriteAcp on this specific object
		if identity == nil || !identity.CanDo(writeAcpAction, bucket, object) {
			glog.V(3).Infof("PutObjectAclHandler: Identity %v cannot perform WriteAcp on %s/%s", identity, bucket, object)
			s3err.WriteErrorResponse(w, r, s3err.ErrAccessDenied)
			return
		}
	} else {
		glog.V(3).Infof("PutObjectAclHandler: Admin user %s granted ACL modification permission for %s/%s", amzAccountId, bucket, object)
	}

	// Get bucket config for ownership settings
	bucketConfig, errCode := s3a.getBucketConfig(bucket)
	if errCode != s3err.ErrNone {
		s3err.WriteErrorResponse(w, r, errCode)
		return
	}

	bucketOwnership := bucketConfig.Ownership
	bucketOwnerId := bucketConfig.Owner

	// Extract ACL from request (either canned ACL or XML body)
	// This function also validates that the owner in the request matches the object owner
	grants, errCode := ExtractAcl(r, s3a.iam, bucketOwnership, bucketOwnerId, objectOwner, amzAccountId)
	if errCode != s3err.ErrNone {
		s3err.WriteErrorResponse(w, r, errCode)
		return
	}

	// Store ACL in object metadata
	if errCode := AssembleEntryWithAcp(entry, objectOwner, grants); errCode != s3err.ErrNone {
		glog.Errorf("PutObjectAclHandler: failed to assemble entry with ACP: %v", errCode)
		s3err.WriteErrorResponse(w, r, errCode)
		return
	}

	// Calculate the correct directory for ACL update
	var updateDirectory string

	if versioningConfigured {
		if versionId != "" && versionId != "null" {
			// Versioned object - update the specific version file in .versions directory
			updateDirectory = s3a.bucketDir(bucket) + "/" + object + s3_constants.VersionsFolder
		} else {
			// Latest version in versioned bucket - could be null version or versioned object
			// Extract version ID from the entry to determine where it's stored
			var actualVersionId string
			if entry.Extended != nil {
				if versionIdBytes, exists := entry.Extended[s3_constants.ExtVersionIdKey]; exists {
					actualVersionId = string(versionIdBytes)
				}
			}

			if actualVersionId == "null" || actualVersionId == "" {
				// Null version (pre-versioning object) - stored as regular file
				updateDirectory = s3a.bucketDir(bucket)
			} else {
				// Versioned object - stored in .versions directory
				updateDirectory = s3a.bucketDir(bucket) + "/" + object + s3_constants.VersionsFolder
			}
		}
	} else {
		// Non-versioned object - stored as regular file
		updateDirectory = s3a.bucketDir(bucket)
	}

	// Update the object with new ACL metadata
	err = s3a.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error {
		request := &filer_pb.UpdateEntryRequest{
			Directory: updateDirectory,
			Entry:     entry,
		}

		if _, err := client.UpdateEntry(context.Background(), request); err != nil {
			return err
		}
		return nil
	})

	if err != nil {
		glog.Errorf("PutObjectAclHandler: failed to update entry: %v", err)
		s3err.WriteErrorResponse(w, r, s3err.ErrInternalError)
		return
	}

	glog.V(3).Infof("PutObjectAclHandler: Successfully updated ACL for %s/%s by user %s", bucket, object, amzAccountId)
	writeSuccessResponseEmpty(w, r)
}
