package s3tables

import (
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"
	"strconv"
	"strings"
	"time"

	"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
)

// handleCreateTable creates a new table in a namespace
func (h *S3TablesHandler) handleCreateTable(w http.ResponseWriter, r *http.Request, filerClient FilerClient) error {

	var req CreateTableRequest
	if err := h.readRequestBody(r, &req); err != nil {
		h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, err.Error())
		return err
	}

	if req.Format == "" {
		h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, "format is required")
		return fmt.Errorf("format is required")
	}

	// Validate format
	if req.Format != FormatIceberg && req.Format != FormatLance {
		h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, fmt.Sprintf("unsupported format %q", req.Format))
		return fmt.Errorf("invalid format")
	}

	target, err := h.authorizeCreateTable(w, r, filerClient, req.TableBucketARN, req.Namespace, req.Name, req.Tags)
	if err != nil {
		return err
	}
	bucketName, namespaceName, tableName := target.bucketName, target.namespaceName, target.tableName

	// A bucket declares the format it holds, and a table of another format would
	// be invisible to the catalog serving it. A bucket made before the
	// declaration existed has none, and keeps taking anything.
	if target.bucketFormat != "" && target.bucketFormat != req.Format {
		message := fmt.Sprintf("table bucket %s holds %s tables", bucketName, target.bucketFormat)
		h.writeError(w, http.StatusConflict, ErrCodeConflict, message)
		return fmt.Errorf("%s", message)
	}

	tablePath := GetTablePath(bucketName, namespaceName, tableName)

	// Check if a table or view already exists at this name. Names are unique
	// across tables and views in a namespace.
	var existingMetadata tableMetadataInternal
	var existingIsView bool
	err = filerClient.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error {
		entry, err := h.lookupEntry(r.Context(), client, tablePath)
		if err != nil {
			return err
		}
		if EntryType(entry.Extended) == EntryTypeView {
			existingIsView = true
			return nil
		}
		data, ok := entry.Extended[ExtendedKeyMetadata]
		if !ok {
			return fmt.Errorf("%w: %s", ErrAttributeNotFound, ExtendedKeyMetadata)
		}
		if unmarshalErr := json.Unmarshal(data, &existingMetadata); unmarshalErr != nil {
			return fmt.Errorf("failed to parse existing table metadata: %w", unmarshalErr)
		}
		return nil
	})

	if err == nil {
		if existingIsView {
			h.writeError(w, http.StatusConflict, ErrCodeTableAlreadyExists, fmt.Sprintf("a view named %s already exists", tableName))
			return fmt.Errorf("view name conflict: %s", tableName)
		}
		// Creating a table that already exists is idempotent, but only for the
		// same format. Handing a Lance client an Iceberg table's location, or the
		// reverse, has it write one format's files into the other's directory.
		if existingMetadata.Format != "" && existingMetadata.Format != req.Format {
			h.writeError(w, http.StatusConflict, ErrCodeTableAlreadyExists,
				fmt.Sprintf("a %s table named %s already exists", existingMetadata.Format, tableName))
			return fmt.Errorf("format conflict: %s", tableName)
		}
		tableARN := h.generateTableARN(existingMetadata.OwnerAccountID, bucketName, namespaceName+"/"+tableName)
		h.writeJSON(w, http.StatusOK, &CreateTableResponse{
			TableARN:         tableARN,
			VersionToken:     existingMetadata.VersionToken,
			MetadataLocation: existingMetadata.MetadataLocation,
		})
		return nil
	} else if !errors.Is(err, filer_pb.ErrNotFound) && !errors.Is(err, ErrAttributeNotFound) {
		h.writeError(w, http.StatusInternalServerError, ErrCodeInternalError, fmt.Sprintf("failed to check table: %v", err))
		return err
	}

	// Create the table
	now := time.Now()
	versionToken := generateVersionToken()

	metadata := &tableMetadataInternal{
		Name:             tableName,
		Namespace:        namespaceName,
		Format:           req.Format,
		CreatedAt:        now,
		ModifiedAt:       now,
		OwnerAccountID:   target.ownerAccountID, // Inherit namespace owner for consistency
		VersionToken:     versionToken,
		MetadataVersion:  max(req.MetadataVersion, 1),
		MetadataLocation: req.MetadataLocation,
		Metadata:         req.Metadata,
	}

	metadataBytes, err := json.Marshal(metadata)
	if err != nil {
		h.writeError(w, http.StatusInternalServerError, ErrCodeInternalError, "failed to marshal table metadata")
		return fmt.Errorf("failed to marshal metadata: %w", err)
	}

	err = filerClient.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error {
		// Ensure table directory exists (may already be created by object storage clients)
		if err := h.ensureDirectory(r.Context(), client, tablePath); err != nil {
			return err
		}

		// Create data subdirectory for Iceberg files
		dataPath := tablePath + "/data"
		if err := h.ensureDirectory(r.Context(), client, dataPath); err != nil {
			return err
		}

		// Set metadata as extended attribute
		if err := h.setExtendedAttribute(r.Context(), client, tablePath, ExtendedKeyMetadata, metadataBytes); err != nil {
			return err
		}

		// Tag the entry as a table so view listings can exclude it.
		if err := h.setExtendedAttribute(r.Context(), client, tablePath, ExtendedKeyEntryType, []byte(EntryTypeTable)); err != nil {
			return err
		}

		// Set tags if provided
		if len(req.Tags) > 0 {
			tagsBytes, err := json.Marshal(req.Tags)
			if err != nil {
				return fmt.Errorf("failed to marshal tags: %w", err)
			}
			if err := h.setExtendedAttribute(r.Context(), client, tablePath, ExtendedKeyTags, tagsBytes); err != nil {
				return err
			}
		}

		return nil
	})

	if err != nil {
		h.writeError(w, http.StatusInternalServerError, ErrCodeInternalError, "failed to create table")
		return err
	}

	tableARN := h.generateTableARN(metadata.OwnerAccountID, bucketName, namespaceName+"/"+tableName)

	resp := &CreateTableResponse{
		TableARN:     tableARN,
		VersionToken: versionToken,
	}

	h.writeJSON(w, http.StatusOK, resp)
	return nil
}

// metadataVersionFromLocation parses the version N from a metadata location.
// SeaweedFS writes v{N}.metadata.json; Iceberg engines (Spark/Trino/Flink/Java)
// write {NNNNN}-{uuid}.metadata.json with a zero-padded leading version. Returns
// 1 when no version can be parsed.
func metadataVersionFromLocation(metadataLocation string) int {
	name := metadataLocation
	if idx := strings.LastIndex(name, "/"); idx != -1 {
		name = name[idx+1:]
	}
	name = strings.TrimSuffix(name, ".metadata.json")
	// v{N} form
	if v, err := strconv.Atoi(strings.TrimPrefix(name, "v")); err == nil && v > 0 {
		return v
	}
	// v{N}-{unique} form, written when a commit finds v{N} already staged
	if trimmed := strings.TrimPrefix(name, "v"); trimmed != name {
		if idx := strings.IndexByte(trimmed, '-'); idx != -1 {
			if v, err := strconv.Atoi(trimmed[:idx]); err == nil && v > 0 {
				return v
			}
		}
	}
	// {NNNNN}-{uuid} form: the leading integer before the first '-'
	if idx := strings.IndexByte(name, '-'); idx != -1 {
		if v, err := strconv.Atoi(name[:idx]); err == nil && v > 0 {
			return v
		}
	}
	return 1
}

// handleRegisterTable registers an existing Iceberg metadata.json under a new
// catalog entry. Unlike CreateTable it does not generate metadata: it points the
// table at the caller-supplied MetadataLocation.
func (h *S3TablesHandler) handleRegisterTable(w http.ResponseWriter, r *http.Request, filerClient FilerClient) error {

	var req RegisterTableRequest
	if err := h.readRequestBody(r, &req); err != nil {
		h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, err.Error())
		return err
	}

	if req.MetadataLocation == "" {
		h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, "metadataLocation is required")
		return fmt.Errorf("metadataLocation is required")
	}

	target, err := h.authorizeCreateTable(w, r, filerClient, req.TableBucketARN, req.Namespace, req.Name, nil)
	if err != nil {
		return err
	}
	bucketName, namespaceName, tableName := target.bucketName, target.namespaceName, target.tableName

	tablePath := GetTablePath(bucketName, namespaceName, tableName)

	// Table must be absent.
	var existingMetadata tableMetadataInternal
	err = filerClient.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error {
		data, err := h.getExtendedAttribute(r.Context(), client, tablePath, ExtendedKeyMetadata)
		if err != nil {
			return err
		}
		return json.Unmarshal(data, &existingMetadata)
	})
	if err == nil {
		h.writeError(w, http.StatusConflict, ErrCodeTableAlreadyExists, fmt.Sprintf("table %s already exists", tableName))
		return fmt.Errorf("table %s already exists", tableName)
	} else if !errors.Is(err, filer_pb.ErrNotFound) && !errors.Is(err, ErrAttributeNotFound) {
		h.writeError(w, http.StatusInternalServerError, ErrCodeInternalError, fmt.Sprintf("failed to check table: %v", err))
		return err
	}

	now := time.Now()
	versionToken := generateVersionToken()
	metadata := &tableMetadataInternal{
		Name:             tableName,
		Namespace:        namespaceName,
		Format:           FormatIceberg,
		CreatedAt:        now,
		ModifiedAt:       now,
		OwnerAccountID:   target.ownerAccountID,
		VersionToken:     versionToken,
		MetadataVersion:  metadataVersionFromLocation(req.MetadataLocation),
		MetadataLocation: req.MetadataLocation,
	}

	metadataBytes, err := json.Marshal(metadata)
	if err != nil {
		h.writeError(w, http.StatusInternalServerError, ErrCodeInternalError, "failed to marshal table metadata")
		return fmt.Errorf("failed to marshal metadata: %w", err)
	}

	err = filerClient.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error {
		if err := h.ensureDirectory(r.Context(), client, tablePath); err != nil {
			return err
		}
		return h.setExtendedAttribute(r.Context(), client, tablePath, ExtendedKeyMetadata, metadataBytes)
	})
	if err != nil {
		h.writeError(w, http.StatusInternalServerError, ErrCodeInternalError, "failed to register table")
		return err
	}

	tableARN := h.generateTableARN(metadata.OwnerAccountID, bucketName, namespaceName+"/"+tableName)
	h.writeJSON(w, http.StatusOK, &RegisterTableResponse{
		TableARN:         tableARN,
		VersionToken:     versionToken,
		MetadataLocation: metadata.MetadataLocation,
	})
	return nil
}

// handleGetTable gets details of a table
func (h *S3TablesHandler) handleGetTable(w http.ResponseWriter, r *http.Request, filerClient FilerClient) error {

	var req GetTableRequest
	if err := h.readRequestBody(r, &req); err != nil {
		h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, err.Error())
		return err
	}

	var bucketName, namespace, tableName string
	var err error

	// Support getting by ARN or by bucket/namespace/name
	if req.TableARN != "" {
		bucketName, namespace, tableName, err = parseTableFromARN(req.TableARN)
		if err != nil {
			h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, err.Error())
			return err
		}
	} else if req.TableBucketARN != "" && len(req.Namespace) > 0 && req.Name != "" {
		bucketName, err = parseBucketNameFromARN(req.TableBucketARN)
		if err != nil {
			h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, err.Error())
			return err
		}
		namespace, err = validateNamespace(req.Namespace)
		if err != nil {
			h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, err.Error())
			return err
		}
		tableName, err = validateTableName(req.Name)
		if err != nil {
			h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, err.Error())
			return err
		}
	} else {
		h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, "either tableARN or (tableBucketARN, namespace, name) is required")
		return fmt.Errorf("missing required parameters")
	}

	tablePath := GetTablePath(bucketName, namespace, tableName)

	var metadata tableMetadataInternal
	err = filerClient.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error {
		entry, err := h.lookupEntry(r.Context(), client, tablePath)
		if err != nil {
			return err
		}
		if EntryType(entry.Extended) == EntryTypeView {
			return filer_pb.ErrNotFound
		}
		data, ok := entry.Extended[ExtendedKeyMetadata]
		if !ok {
			return fmt.Errorf("%w: %s", ErrAttributeNotFound, ExtendedKeyMetadata)
		}
		if err := json.Unmarshal(data, &metadata); err != nil {
			return fmt.Errorf("failed to unmarshal table metadata: %w", err)
		}
		return nil
	})

	if err != nil {
		// A directory without the table-metadata xattr is not a table (e.g. a renamed-away source).
		if errors.Is(err, filer_pb.ErrNotFound) || errors.Is(err, ErrAttributeNotFound) {
			h.writeError(w, http.StatusNotFound, ErrCodeNoSuchTable, fmt.Sprintf("table %s not found", tableName))
		} else {
			h.writeError(w, http.StatusInternalServerError, ErrCodeInternalError, fmt.Sprintf("failed to get table: %v", err))
		}
		return err
	}

	// Authorize access to the table using policy framework
	accountID := h.getAccountID(r)
	bucketPath := GetTableBucketPath(bucketName)
	tablePolicy := ""
	bucketPolicy := ""
	bucketTags := map[string]string{}
	tableTags := map[string]string{}
	var bucketMetadata tableBucketMetadata

	err = filerClient.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error {
		// Fetch bucket metadata to use correct owner for bucket policy evaluation
		data, err := h.getExtendedAttribute(r.Context(), client, bucketPath, ExtendedKeyMetadata)
		if err == nil {
			if err := json.Unmarshal(data, &bucketMetadata); err != nil {
				return fmt.Errorf("failed to unmarshal bucket metadata: %w", err)
			}
		} else if !errors.Is(err, ErrAttributeNotFound) {
			return fmt.Errorf("failed to fetch bucket metadata: %v", err)
		}

		// Fetch table policy if it exists
		policyData, err := h.getExtendedAttribute(r.Context(), client, tablePath, ExtendedKeyPolicy)
		if err == nil {
			tablePolicy = string(policyData)
		} else if !errors.Is(err, ErrAttributeNotFound) {
			return fmt.Errorf("failed to fetch table policy: %v", err)
		}
		if tags, err := h.readTags(r.Context(), client, tablePath); err != nil {
			return err
		} else if tags != nil {
			tableTags = tags
		}

		// Fetch bucket policy if it exists
		policyData, err = h.getExtendedAttribute(r.Context(), client, bucketPath, ExtendedKeyPolicy)
		if err == nil {
			bucketPolicy = string(policyData)
		} else if !errors.Is(err, ErrAttributeNotFound) {
			return fmt.Errorf("failed to fetch bucket policy: %v", err)
		}
		if tags, err := h.readTags(r.Context(), client, bucketPath); err != nil {
			return err
		} else if tags != nil {
			bucketTags = tags
		}

		return nil
	})

	if err != nil {
		h.writeError(w, http.StatusInternalServerError, ErrCodeInternalError, fmt.Sprintf("failed to fetch policies: %v", err))
		return err
	}

	tableARN := h.generateTableARN(metadata.OwnerAccountID, bucketName, namespace+"/"+tableName)
	bucketARN := h.generateTableBucketARN(bucketMetadata.OwnerAccountID, bucketName)
	identityActions := getIdentityActions(r)
	tableAllowed := CheckPermissionWithContext("GetTable", accountID, metadata.OwnerAccountID, tablePolicy, tableARN, &PolicyContext{
		TableBucketName: bucketName,
		Namespace:       namespace,
		TableName:       tableName,
		TableBucketTags: bucketTags,
		ResourceTags:    tableTags,
		IdentityActions: identityActions,
		DefaultAllow:    h.defaultAllowFor(r),
	})
	bucketAllowed := CheckPermissionWithContext("GetTable", accountID, bucketMetadata.OwnerAccountID, bucketPolicy, bucketARN, &PolicyContext{
		TableBucketName: bucketName,
		Namespace:       namespace,
		TableName:       tableName,
		TableBucketTags: bucketTags,
		ResourceTags:    tableTags,
		IdentityActions: identityActions,
		DefaultAllow:    h.defaultAllowFor(r),
	})

	if !tableAllowed && !bucketAllowed {
		h.writeError(w, http.StatusNotFound, ErrCodeNoSuchTable, fmt.Sprintf("table %s not found", tableName))
		return ErrAccessDenied
	}

	resp := &GetTableResponse{
		Name:             metadata.Name,
		TableARN:         tableARN,
		Namespace:        expandNamespace(metadata.Namespace),
		Format:           metadata.Format,
		CreatedAt:        metadata.CreatedAt,
		ModifiedAt:       metadata.ModifiedAt,
		OwnerAccountID:   metadata.OwnerAccountID,
		MetadataLocation: metadata.MetadataLocation,
		MetadataVersion:  metadata.MetadataVersion,
		VersionToken:     metadata.VersionToken,
		Metadata:         metadata.Metadata,
	}

	h.writeJSON(w, http.StatusOK, resp)
	return nil
}

// handleListTables lists all tables in a namespace or bucket
func (h *S3TablesHandler) handleListTables(w http.ResponseWriter, r *http.Request, filerClient FilerClient) error {

	var req ListTablesRequest
	if err := h.readRequestBody(r, &req); err != nil {
		h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, err.Error())
		return err
	}

	if req.TableBucketARN == "" {
		h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, "tableBucketARN is required")
		return fmt.Errorf("tableBucketARN is required")
	}

	bucketName, err := parseBucketNameFromARN(req.TableBucketARN)
	if err != nil {
		h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, err.Error())
		return err
	}

	maxTables := req.MaxTables
	if maxTables <= 0 {
		maxTables = 100
	}
	// Cap to prevent uint32 overflow when used in uint32(maxTables*2)
	const maxTablesLimit = 1000
	if maxTables > maxTablesLimit {
		h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, "MaxTables exceeds maximum allowed value")
		return fmt.Errorf("invalid maxTables value: %d", maxTables)
	}

	// Pre-validate namespace before calling WithFilerClient to return 400 on validation errors
	var namespaceName string
	if len(req.Namespace) > 0 {
		var err error
		namespaceName, err = validateNamespace(req.Namespace)
		if err != nil {
			h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, err.Error())
			return err
		}
	}

	var tables []TableSummary
	var paginationToken string

	err = filerClient.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error {
		var err error
		accountID := h.getAccountID(r)

		if len(req.Namespace) > 0 {
			// Namespace has already been validated above
			namespacePath := GetNamespacePath(bucketName, namespaceName)
			bucketPath := GetTableBucketPath(bucketName)
			var nsMeta namespaceMetadata
			var bucketMeta tableBucketMetadata
			var namespacePolicy, bucketPolicy string
			bucketTags := map[string]string{}

			// Fetch namespace metadata and policy
			data, err := h.getExtendedAttribute(r.Context(), client, namespacePath, ExtendedKeyMetadata)
			if err != nil {
				return err // Not Found handled by caller
			}
			if err := json.Unmarshal(data, &nsMeta); err != nil {
				return err
			}

			// Fetch namespace policy if it exists
			policyData, err := h.getExtendedAttribute(r.Context(), client, namespacePath, ExtendedKeyPolicy)
			if err == nil {
				namespacePolicy = string(policyData)
			} else if !errors.Is(err, ErrAttributeNotFound) {
				return fmt.Errorf("failed to fetch namespace policy: %w", err)
			}

			// Fetch bucket metadata and policy
			data, err = h.getExtendedAttribute(r.Context(), client, bucketPath, ExtendedKeyMetadata)
			if err == nil {
				if err := json.Unmarshal(data, &bucketMeta); err != nil {
					return fmt.Errorf("failed to unmarshal bucket metadata: %w", err)
				}
			} else if !errors.Is(err, ErrAttributeNotFound) {
				return fmt.Errorf("failed to fetch bucket metadata: %w", err)
			}

			policyData, err = h.getExtendedAttribute(r.Context(), client, bucketPath, ExtendedKeyPolicy)
			if err == nil {
				bucketPolicy = string(policyData)
			} else if !errors.Is(err, ErrAttributeNotFound) {
				return fmt.Errorf("failed to fetch bucket policy: %w", err)
			}
			if tags, err := h.readTags(r.Context(), client, bucketPath); err != nil {
				return fmt.Errorf("failed to read bucket tags: %w", err)
			} else if tags != nil {
				bucketTags = tags
			}

			bucketARN := h.generateTableBucketARN(bucketMeta.OwnerAccountID, bucketName)
			identityActions := getIdentityActions(r)
			nsAllowed := CheckPermissionWithContext("ListTables", accountID, nsMeta.OwnerAccountID, namespacePolicy, bucketARN, &PolicyContext{
				TableBucketName: bucketName,
				Namespace:       namespaceName,
				TableBucketTags: bucketTags,
				IdentityActions: identityActions,
				DefaultAllow:    h.defaultAllowFor(r),
			})
			bucketAllowed := CheckPermissionWithContext("ListTables", accountID, bucketMeta.OwnerAccountID, bucketPolicy, bucketARN, &PolicyContext{
				TableBucketName: bucketName,
				Namespace:       namespaceName,
				TableBucketTags: bucketTags,
				IdentityActions: identityActions,
				DefaultAllow:    h.defaultAllowFor(r),
			})
			if !nsAllowed && !bucketAllowed {
				return ErrAccessDenied
			}

			tables, paginationToken, err = h.listTablesInNamespaceWithClient(r, client, bucketName, namespaceName, req.Prefix, req.ContinuationToken, maxTables)
			if err != nil {
				return fmt.Errorf("list tables in namespace %v: %w", namespaceName, err)
			}
		} else {
			// List tables across all namespaces in bucket
			bucketPath := GetTableBucketPath(bucketName)
			var bucketMeta tableBucketMetadata
			var bucketPolicy string
			bucketTags := map[string]string{}

			// Fetch bucket metadata and policy
			data, err := h.getExtendedAttribute(r.Context(), client, bucketPath, ExtendedKeyMetadata)
			if err != nil {
				return fmt.Errorf("failed to fetch bucket metadata: %w", err)
			}
			if err := json.Unmarshal(data, &bucketMeta); err != nil {
				return fmt.Errorf("failed to unmarshal bucket metadata: %w", err)
			}

			// Fetch bucket policy if it exists
			policyData, err := h.getExtendedAttribute(r.Context(), client, bucketPath, ExtendedKeyPolicy)
			if err == nil {
				bucketPolicy = string(policyData)
			} else if !errors.Is(err, ErrAttributeNotFound) {
				return fmt.Errorf("failed to fetch bucket policy: %w", err)
			}
			if tags, err := h.readTags(r.Context(), client, bucketPath); err != nil {
				return fmt.Errorf("failed to read bucket tags: %w", err)
			} else if tags != nil {
				bucketTags = tags
			}

			bucketARN := h.generateTableBucketARN(bucketMeta.OwnerAccountID, bucketName)
			identityActions := getIdentityActions(r)
			if !CheckPermissionWithContext("ListTables", accountID, bucketMeta.OwnerAccountID, bucketPolicy, bucketARN, &PolicyContext{
				TableBucketName: bucketName,
				TableBucketTags: bucketTags,
				IdentityActions: identityActions,
				DefaultAllow:    h.defaultAllowFor(r),
			}) {
				return ErrAccessDenied
			}

			tables, paginationToken, err = h.listTablesInAllNamespaces(r, client, bucketName, req.Prefix, req.ContinuationToken, maxTables)
			if err != nil {
				return fmt.Errorf("list tables in all namespaces: %w", err)
			}
		}
		return err
	})

	if err != nil {
		if errors.Is(err, filer_pb.ErrNotFound) {
			// If the bucket or namespace directory is not found, return an empty result
			tables = []TableSummary{}
			paginationToken = ""
		} else if isAuthError(err) {
			h.writeError(w, http.StatusForbidden, ErrCodeAccessDenied, "Access Denied")
			return err
		} else {
			h.writeError(w, http.StatusInternalServerError, ErrCodeInternalError, fmt.Sprintf("failed to list tables: %v", err))
			return err
		}
	}

	resp := &ListTablesResponse{
		Tables:            tables,
		ContinuationToken: paginationToken,
	}

	h.writeJSON(w, http.StatusOK, resp)
	return nil
}

// listTablesInNamespaceWithClient lists tables in a specific namespace
func (h *S3TablesHandler) listTablesInNamespaceWithClient(r *http.Request, client filer_pb.SeaweedFilerClient, bucketName, namespaceName, prefix, continuationToken string, maxTables int) ([]TableSummary, string, error) {
	namespacePath := GetNamespacePath(bucketName, namespaceName)
	return h.listTablesWithClient(r, client, namespacePath, bucketName, namespaceName, prefix, continuationToken, maxTables)
}

func (h *S3TablesHandler) listTablesWithClient(r *http.Request, client filer_pb.SeaweedFilerClient, dirPath, bucketName, namespaceName, prefix, continuationToken string, maxTables int) ([]TableSummary, string, error) {
	var tables []TableSummary
	lastFileName := continuationToken
	ctx := r.Context()

	for len(tables) < maxTables {
		resp, err := client.ListEntries(ctx, &filer_pb.ListEntriesRequest{
			Directory:          dirPath,
			Limit:              uint32(maxTables * 2),
			StartFromFileName:  lastFileName,
			InclusiveStartFrom: lastFileName == "" || lastFileName == continuationToken,
		})
		if err != nil {
			return nil, "", err
		}

		hasMore := false
		for {
			entry, respErr := resp.Recv()
			if respErr != nil {
				if respErr == io.EOF {
					break
				}
				return nil, "", respErr
			}
			if entry.Entry == nil {
				continue
			}

			// Skip the start item if it was included in the previous page
			if len(tables) == 0 && continuationToken != "" && entry.Entry.Name == continuationToken {
				continue
			}

			hasMore = true
			lastFileName = entry.Entry.Name

			if !entry.Entry.IsDirectory {
				continue
			}

			// Skip hidden entries
			if strings.HasPrefix(entry.Entry.Name, ".") {
				continue
			}

			// Apply prefix filter
			if prefix != "" && !strings.HasPrefix(entry.Entry.Name, prefix) {
				continue
			}

			// Views share the table layout; exclude them from table listings.
			if EntryType(entry.Entry.Extended) == EntryTypeView {
				continue
			}

			// Read table metadata from extended attribute
			data, ok := entry.Entry.Extended[ExtendedKeyMetadata]
			if !ok {
				continue
			}

			var metadata tableMetadataInternal
			if err := json.Unmarshal(data, &metadata); err != nil {
				continue
			}

			// Note: Authorization (ownership or policy-based access) is checked at the handler level
			// before calling this function. This filter is removed to allow policy-based sharing.
			// The caller has already been verified to have ListTables permission for this namespace/bucket.

			tableARN := h.generateTableARN(metadata.OwnerAccountID, bucketName, namespaceName+"/"+entry.Entry.Name)

			tables = append(tables, TableSummary{
				Name:             entry.Entry.Name,
				TableARN:         tableARN,
				Namespace:        expandNamespace(namespaceName),
				Format:           metadata.Format,
				CreatedAt:        metadata.CreatedAt,
				ModifiedAt:       metadata.ModifiedAt,
				MetadataLocation: metadata.MetadataLocation,
			})

			if len(tables) >= maxTables {
				return tables, lastFileName, nil
			}
		}

		if !hasMore {
			break
		}
	}

	if len(tables) < maxTables {
		lastFileName = ""
	}
	return tables, lastFileName, nil
}

func (h *S3TablesHandler) listTablesInAllNamespaces(r *http.Request, client filer_pb.SeaweedFilerClient, bucketName, prefix, continuationToken string, maxTables int) ([]TableSummary, string, error) {
	bucketPath := GetTableBucketPath(bucketName)
	ctx := r.Context()

	var continuationNamespace string
	var startTableName string
	if continuationToken != "" {
		if parts := strings.SplitN(continuationToken, "/", 2); len(parts) == 2 {
			continuationNamespace = parts[0]
			startTableName = parts[1]
		} else {
			continuationNamespace = continuationToken
		}
	}

	var tables []TableSummary
	lastNamespace := continuationNamespace
	for {
		// List namespaces in batches
		resp, err := client.ListEntries(ctx, &filer_pb.ListEntriesRequest{
			Directory:          bucketPath,
			Limit:              100,
			StartFromFileName:  lastNamespace,
			InclusiveStartFrom: (lastNamespace == continuationNamespace && startTableName != "") || (lastNamespace == "" && continuationNamespace == ""),
		})
		if err != nil {
			return nil, "", err
		}

		hasMore := false
		for {
			entry, respErr := resp.Recv()
			if respErr != nil {
				if respErr == io.EOF {
					break
				}
				return nil, "", respErr
			}
			if entry.Entry == nil {
				continue
			}

			hasMore = true
			lastNamespace = entry.Entry.Name

			if !entry.Entry.IsDirectory || strings.HasPrefix(entry.Entry.Name, ".") {
				continue
			}

			namespace := entry.Entry.Name
			tableNameFilter := ""
			if namespace == continuationNamespace {
				tableNameFilter = startTableName
			}

			nsTables, nsToken, err := h.listTablesInNamespaceWithClient(r, client, bucketName, namespace, prefix, tableNameFilter, maxTables-len(tables))
			if err != nil {
				return nil, "", fmt.Errorf("list tables in namespace %s: %w", namespace, err)
			}

			tables = append(tables, nsTables...)

			if namespace == continuationNamespace {
				startTableName = ""
			}

			if len(tables) >= maxTables {
				paginationToken := namespace + "/" + nsToken
				if nsToken == "" {
					// If we hit the limit exactly at the end of a namespace, the next token should be the next namespace
					paginationToken = namespace // This will start from the NEXT namespace in the outer loop
				}
				return tables, paginationToken, nil
			}
		}

		if !hasMore {
			break
		}
	}

	return tables, "", nil
}

// handleDeleteTable deletes a table from a namespace
func (h *S3TablesHandler) handleDeleteTable(w http.ResponseWriter, r *http.Request, filerClient FilerClient) error {

	var req DeleteTableRequest
	if err := h.readRequestBody(r, &req); err != nil {
		h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, err.Error())
		return err
	}

	if req.TableBucketARN == "" || len(req.Namespace) == 0 || req.Name == "" {
		h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, "tableBucketARN, namespace, and name are required")
		return fmt.Errorf("missing required parameters")
	}

	namespaceName, err := validateNamespace(req.Namespace)
	if err != nil {
		h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, err.Error())
		return err
	}

	bucketName, err := parseBucketNameFromARN(req.TableBucketARN)
	if err != nil {
		h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, err.Error())
		return err
	}

	tableName, err := validateTableName(req.Name)
	if err != nil {
		h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, err.Error())
		return err
	}

	tablePath := GetTablePath(bucketName, namespaceName, tableName)

	// Check if table exists and enforce VersionToken if provided
	var metadata tableMetadataInternal
	var tablePolicy string
	var bucketPolicy string
	var bucketTags map[string]string
	var tableTags map[string]string
	var bucketMetadata tableBucketMetadata
	err = filerClient.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error {
		data, err := h.getExtendedAttribute(r.Context(), client, tablePath, ExtendedKeyMetadata)
		if err != nil {
			return err
		}

		if err := json.Unmarshal(data, &metadata); err != nil {
			return fmt.Errorf("failed to unmarshal table metadata: %w", err)
		}

		if req.VersionToken != "" {
			if metadata.VersionToken != req.VersionToken {
				return ErrVersionTokenMismatch
			}
		}

		// Fetch table policy if it exists
		policyData, err := h.getExtendedAttribute(r.Context(), client, tablePath, ExtendedKeyPolicy)
		if err != nil {
			if errors.Is(err, ErrAttributeNotFound) {
				// No table policy set; proceed with empty policy
			} else {
				return fmt.Errorf("failed to fetch table policy: %w", err)
			}
		} else {
			tablePolicy = string(policyData)
		}

		tableTags, err = h.readTags(r.Context(), client, tablePath)
		if err != nil {
			return err
		}

		bucketPath := GetTableBucketPath(bucketName)
		data, err = h.getExtendedAttribute(r.Context(), client, bucketPath, ExtendedKeyMetadata)
		if err == nil {
			if err := json.Unmarshal(data, &bucketMetadata); err != nil {
				return fmt.Errorf("failed to unmarshal bucket metadata: %w", err)
			}
		} else if !errors.Is(err, ErrAttributeNotFound) {
			return fmt.Errorf("failed to fetch bucket metadata: %w", err)
		}
		policyData, err = h.getExtendedAttribute(r.Context(), client, bucketPath, ExtendedKeyPolicy)
		if err != nil {
			if !errors.Is(err, ErrAttributeNotFound) {
				return fmt.Errorf("failed to fetch bucket policy: %w", err)
			}
		} else {
			bucketPolicy = string(policyData)
		}
		bucketTags, err = h.readTags(r.Context(), client, bucketPath)
		if err != nil {
			return err
		}

		return nil
	})

	if err != nil {
		if errors.Is(err, filer_pb.ErrNotFound) {
			h.writeError(w, http.StatusNotFound, ErrCodeNoSuchTable, fmt.Sprintf("table %s not found", tableName))
		} else if errors.Is(err, ErrVersionTokenMismatch) {
			h.writeError(w, http.StatusConflict, ErrCodeConflict, "version token mismatch")
		} else {
			h.writeError(w, http.StatusInternalServerError, ErrCodeInternalError, fmt.Sprintf("failed to check table: %v", err))
		}
		return err
	}

	tableARN := h.generateTableARN(metadata.OwnerAccountID, bucketName, namespaceName+"/"+tableName)
	bucketARN := h.generateTableBucketARN(bucketMetadata.OwnerAccountID, bucketName)
	principal := h.getAccountID(r)
	identityActions := getIdentityActions(r)
	tableAllowed := CheckPermissionWithContext("DeleteTable", principal, metadata.OwnerAccountID, tablePolicy, tableARN, &PolicyContext{
		TableBucketName: bucketName,
		Namespace:       namespaceName,
		TableName:       tableName,
		TableBucketTags: bucketTags,
		ResourceTags:    tableTags,
		IdentityActions: identityActions,
		DefaultAllow:    h.defaultAllowFor(r),
	})
	bucketAllowed := CheckPermissionWithContext("DeleteTable", principal, bucketMetadata.OwnerAccountID, bucketPolicy, bucketARN, &PolicyContext{
		TableBucketName: bucketName,
		Namespace:       namespaceName,
		TableName:       tableName,
		TableBucketTags: bucketTags,
		ResourceTags:    tableTags,
		IdentityActions: identityActions,
		DefaultAllow:    h.defaultAllowFor(r),
	})
	if !tableAllowed && !bucketAllowed {
		h.writeError(w, http.StatusForbidden, ErrCodeAccessDenied, "not authorized to delete table")
		return NewAuthError("DeleteTable", principal, "not authorized to delete table")
	}

	// Delete the table
	err = filerClient.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error {
		dataPath := TableDataDirFromMetadataLocation(metadata.MetadataLocation)
		if dataPath != "" && dataPath != tablePath && strings.HasPrefix(dataPath+"/", GetTableBucketPath(bucketName)+"/") {
			// Refuse to purge a data path that is an ancestor of the table's own
			// name path (e.g. corrupt metadata resolving to the bucket or
			// namespace root): the bucket-scope check above still admits the
			// bucket root, and a recursive delete there would take out unrelated
			// tables.
			if strings.HasPrefix(tablePath+"/", dataPath+"/") {
				return fmt.Errorf("refusing to delete table %s: data path %q is an ancestor of catalog path %q", tableName, dataPath, tablePath)
			}
			// The location is caller-supplied, so it may name a sibling that is
			// still a live catalog entry. Only this table's authorization was
			// checked; a decoupled location has had its catalog attributes
			// stripped, so a surviving marker means the path belongs elsewhere.
			switch _, markerErr := h.getExtendedAttribute(r.Context(), client, dataPath, ExtendedKeyMetadata); {
			case markerErr == nil:
				return fmt.Errorf("refusing to delete table %s: data path %q is another catalog entry", tableName, dataPath)
			case !errors.Is(markerErr, ErrAttributeNotFound) && !errors.Is(markerErr, filer_pb.ErrNotFound):
				return fmt.Errorf("refusing to delete table %s: cannot read data path %q: %w", tableName, dataPath, markerErr)
			}
			// Decoupled table (renamed, or created over a leftover): its data
			// lives elsewhere. Purge the data, then clear the catalog marker
			// without deleting the name path -- it may still hold another
			// table's data that was left when this name was reused.
			if err := h.deleteDirectory(r.Context(), client, dataPath); err != nil {
				return err
			}
			return h.removeExtendedAttributes(r.Context(), client, tablePath,
				ExtendedKeyMetadata, ExtendedKeyMetadataVersion, ExtendedKeyPolicy, ExtendedKeyTags, ExtendedKeyEntryType,
				ExtendedKeyMaintenance, ExtendedKeyMaintenanceStatus)
		}
		// Colocated table: the name path holds the data.
		return h.deleteDirectory(r.Context(), client, tablePath)
	})

	if err != nil {
		h.writeError(w, http.StatusInternalServerError, ErrCodeInternalError, "failed to delete table")
		return err
	}

	h.writeJSON(w, http.StatusOK, nil)
	return nil
}

// renamedTableAttributes are the catalog attributes a rename carries to the new
// name and clears from the old one.
var renamedTableAttributes = []string{
	ExtendedKeyMetadata,
	ExtendedKeyMetadataVersion,
	ExtendedKeyPolicy,
	ExtendedKeyTags,
	ExtendedKeyMaintenance,
	ExtendedKeyMaintenanceStatus,
	ExtendedKeyEntryType,
}

// catalogEntryKind describes the entry a rename operates on, so tables and
// views share one implementation of the catalog-only move.
type catalogEntryKind struct {
	entryType    string
	noun         string
	renameOp     string
	createOp     string
	notFoundCode string
	existsCode   string
	// resourceARN builds the ARN a policy scoped to this entry would name, so a
	// view is authorized against its view ARN and not a table ARN.
	resourceARN func(h *S3TablesHandler, ownerAccountID, bucketName, id string) string
}

var (
	tableEntryKind = catalogEntryKind{
		entryType:    EntryTypeTable,
		noun:         "table",
		renameOp:     "RenameTable",
		createOp:     "CreateTable",
		notFoundCode: ErrCodeNoSuchTable,
		existsCode:   ErrCodeTableAlreadyExists,
		resourceARN: func(h *S3TablesHandler, ownerAccountID, bucketName, id string) string {
			return h.generateTableARN(ownerAccountID, bucketName, id)
		},
	}
	viewEntryKind = catalogEntryKind{
		entryType:    EntryTypeView,
		noun:         "view",
		renameOp:     "RenameView",
		createOp:     "CreateView",
		notFoundCode: ErrCodeNoSuchView,
		existsCode:   ErrCodeViewAlreadyExists,
		resourceARN: func(h *S3TablesHandler, ownerAccountID, bucketName, id string) string {
			return h.generateViewARN(ownerAccountID, bucketName, id)
		},
	}
)

// handleRenameTable moves a table's catalog entry to a new namespace/name within
// the same bucket. It is catalog-only: the metadata.json and data files stay put,
// the destination keeps the source's MetadataLocation, and the source name is
// soft-deleted in place (its catalog xattrs are dropped, its data is left intact).
func (h *S3TablesHandler) handleRenameTable(w http.ResponseWriter, r *http.Request, filerClient FilerClient) error {
	return h.renameCatalogEntry(w, r, filerClient, tableEntryKind)
}

// handleRenameView is handleRenameTable for views, which live in the same
// namespace directory under the same name rules.
func (h *S3TablesHandler) handleRenameView(w http.ResponseWriter, r *http.Request, filerClient FilerClient) error {
	return h.renameCatalogEntry(w, r, filerClient, viewEntryKind)
}

func (h *S3TablesHandler) renameCatalogEntry(w http.ResponseWriter, r *http.Request, filerClient FilerClient, kind catalogEntryKind) error {
	var req RenameTableRequest
	if err := h.readRequestBody(r, &req); err != nil {
		h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, err.Error())
		return err
	}

	if req.TableBucketARN == "" || len(req.SourceNamespace) == 0 || req.SourceName == "" || len(req.DestNamespace) == 0 || req.DestName == "" {
		h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, "tableBucketARN, sourceNamespace, sourceName, destNamespace, and destName are required")
		return fmt.Errorf("missing required parameters")
	}

	bucketName, err := parseBucketNameFromARN(req.TableBucketARN)
	if err != nil {
		h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, err.Error())
		return err
	}

	srcNamespace, err := validateNamespace(req.SourceNamespace)
	if err != nil {
		h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, err.Error())
		return err
	}
	srcName, err := validateTableName(req.SourceName)
	if err != nil {
		h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, err.Error())
		return err
	}
	destNamespace, err := validateNamespace(req.DestNamespace)
	if err != nil {
		h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, err.Error())
		return err
	}
	destName, err := validateTableName(req.DestName)
	if err != nil {
		h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, err.Error())
		return err
	}

	srcPath := GetTablePath(bucketName, srcNamespace, srcName)
	destPath := GetTablePath(bucketName, destNamespace, destName)

	var metadata tableMetadataInternal
	var metadataVersionXattr []byte
	var maintenanceXattr []byte
	var maintenanceStatusXattr []byte
	// The values the rename copies. The source is only cleared while it still
	// holds exactly these, so a write that lands mid-rename is not deleted here
	// after having missed the copy to the destination.
	var copiedFromSource map[string][]byte
	var tablePolicy string
	var bucketPolicy string
	var bucketTags map[string]string
	var tableTags map[string]string
	var bucketMetadata tableBucketMetadata
	var srcExtended map[string][]byte
	err = filerClient.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error {
		data, err := h.getExtendedAttribute(r.Context(), client, srcPath, ExtendedKeyMetadata)
		if err != nil {
			return err
		}
		if err := json.Unmarshal(data, &metadata); err != nil {
			return fmt.Errorf("failed to unmarshal table metadata: %w", err)
		}

		if versionData, err := h.getExtendedAttribute(r.Context(), client, srcPath, ExtendedKeyMetadataVersion); err == nil {
			metadataVersionXattr = versionData
		} else if !errors.Is(err, ErrAttributeNotFound) {
			return fmt.Errorf("failed to fetch metadata version: %w", err)
		}

		policyData, err := h.getExtendedAttribute(r.Context(), client, srcPath, ExtendedKeyPolicy)
		if err == nil {
			tablePolicy = string(policyData)
		} else if !errors.Is(err, ErrAttributeNotFound) {
			return fmt.Errorf("failed to fetch table policy: %w", err)
		}
		tableTags, err = h.readTags(r.Context(), client, srcPath)
		if err != nil {
			return err
		}

		srcEntry, err := h.lookupEntry(r.Context(), client, srcPath)
		if err != nil {
			return err
		}
		srcExtended = srcEntry.Extended
		copiedFromSource = make(map[string][]byte, len(renamedTableAttributes))
		for _, key := range renamedTableAttributes {
			copiedFromSource[key] = srcEntry.Extended[key]
		}

		// The maintenance configuration and its last-run status belong to the
		// table, so they move with it; leaving them behind re-enables
		// maintenance under the new name and leaks the old settings onto
		// whatever is created at the old one.
		for _, attr := range []struct {
			key  string
			dest *[]byte
		}{
			{ExtendedKeyMaintenance, &maintenanceXattr},
			{ExtendedKeyMaintenanceStatus, &maintenanceStatusXattr},
		} {
			data, err := h.getExtendedAttribute(r.Context(), client, srcPath, attr.key)
			if err == nil {
				*attr.dest = data
			} else if !errors.Is(err, ErrAttributeNotFound) {
				return fmt.Errorf("failed to fetch %s: %w", attr.key, err)
			}
		}

		bucketPath := GetTableBucketPath(bucketName)
		data, err = h.getExtendedAttribute(r.Context(), client, bucketPath, ExtendedKeyMetadata)
		if err == nil {
			if err := json.Unmarshal(data, &bucketMetadata); err != nil {
				return fmt.Errorf("failed to unmarshal bucket metadata: %w", err)
			}
		} else if !errors.Is(err, ErrAttributeNotFound) {
			return fmt.Errorf("failed to fetch bucket metadata: %w", err)
		}
		policyData, err = h.getExtendedAttribute(r.Context(), client, bucketPath, ExtendedKeyPolicy)
		if err == nil {
			bucketPolicy = string(policyData)
		} else if !errors.Is(err, ErrAttributeNotFound) {
			return fmt.Errorf("failed to fetch bucket policy: %w", err)
		}
		bucketTags, err = h.readTags(r.Context(), client, bucketPath)
		if err != nil {
			return err
		}

		return nil
	})

	if err != nil {
		if errors.Is(err, filer_pb.ErrNotFound) || errors.Is(err, ErrAttributeNotFound) {
			h.writeError(w, http.StatusNotFound, kind.notFoundCode, fmt.Sprintf("%s %s not found", kind.noun, srcName))
		} else {
			h.writeError(w, http.StatusInternalServerError, ErrCodeInternalError, fmt.Sprintf("failed to check %s: %v", kind.noun, err))
		}
		return err
	}

	// Tables and views share the namespace directory, so a rename must not pick
	// up the other kind under the same name.
	if EntryType(srcExtended) != kind.entryType {
		h.writeError(w, http.StatusNotFound, kind.notFoundCode, fmt.Sprintf("%s %s not found", kind.noun, srcName))
		return fmt.Errorf("%s %s not found", kind.noun, srcName)
	}

	tableARN := kind.resourceARN(h, metadata.OwnerAccountID, bucketName, srcNamespace+"/"+srcName)
	bucketARN := h.generateTableBucketARN(bucketMetadata.OwnerAccountID, bucketName)
	principal := h.getAccountID(r)
	identityActions := getIdentityActions(r)
	tableAllowed := CheckPermissionWithContext(kind.renameOp, principal, metadata.OwnerAccountID, tablePolicy, tableARN, &PolicyContext{
		TableBucketName: bucketName,
		Namespace:       srcNamespace,
		TableName:       srcName,
		TableBucketTags: bucketTags,
		ResourceTags:    tableTags,
		IdentityActions: identityActions,
		DefaultAllow:    h.defaultAllowFor(r),
	})
	bucketAllowed := CheckPermissionWithContext(kind.renameOp, principal, bucketMetadata.OwnerAccountID, bucketPolicy, bucketARN, &PolicyContext{
		TableBucketName: bucketName,
		Namespace:       srcNamespace,
		TableName:       srcName,
		TableBucketTags: bucketTags,
		ResourceTags:    tableTags,
		IdentityActions: identityActions,
		DefaultAllow:    h.defaultAllowFor(r),
	})
	if !tableAllowed && !bucketAllowed {
		h.writeError(w, http.StatusForbidden, ErrCodeAccessDenied, "not authorized to rename "+kind.noun)
		return NewAuthError(kind.renameOp, principal, "not authorized to rename "+kind.noun)
	}

	// Require the destination namespace to exist and the destination table to be free.
	destNamespacePath := GetNamespacePath(bucketName, destNamespace)
	var destNamespaceMetadata namespaceMetadata
	var destNamespacePolicy string
	err = filerClient.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error {
		data, err := h.getExtendedAttribute(r.Context(), client, destNamespacePath, ExtendedKeyMetadata)
		if err != nil {
			return err
		}
		if err := json.Unmarshal(data, &destNamespaceMetadata); err != nil {
			return fmt.Errorf("failed to unmarshal destination namespace metadata: %w", err)
		}
		policyData, err := h.getExtendedAttribute(r.Context(), client, destNamespacePath, ExtendedKeyPolicy)
		if err == nil {
			destNamespacePolicy = string(policyData)
		} else if !errors.Is(err, ErrAttributeNotFound) {
			return fmt.Errorf("failed to fetch destination namespace policy: %w", err)
		}
		if _, err := h.getExtendedAttribute(r.Context(), client, destPath, ExtendedKeyMetadata); err == nil {
			return ErrTableAlreadyExists
		} else if !errors.Is(err, filer_pb.ErrNotFound) && !errors.Is(err, ErrAttributeNotFound) {
			return err
		}
		return nil
	})

	if err != nil {
		if errors.Is(err, ErrTableAlreadyExists) {
			h.writeError(w, http.StatusConflict, kind.existsCode, fmt.Sprintf("%s %s already exists", kind.noun, destName))
		} else if errors.Is(err, filer_pb.ErrNotFound) {
			h.writeError(w, http.StatusNotFound, ErrCodeNoSuchNamespace, fmt.Sprintf("namespace %s not found", destNamespace))
		} else {
			h.writeError(w, http.StatusInternalServerError, ErrCodeInternalError, fmt.Sprintf("failed to check destination: %v", err))
		}
		return err
	}

	// Renaming places the table into the destination namespace, so the principal
	// must also be allowed to create a table there (the source check alone lets a
	// caller move tables into namespaces they don't control).
	destNamespaceAllowed := CheckPermissionWithContext(kind.createOp, principal, destNamespaceMetadata.OwnerAccountID, destNamespacePolicy, bucketARN, &PolicyContext{
		TableBucketName: bucketName,
		Namespace:       destNamespace,
		TableName:       destName,
		TableBucketTags: bucketTags,
		IdentityActions: identityActions,
		DefaultAllow:    h.defaultAllowFor(r),
	})
	destBucketAllowed := CheckPermissionWithContext(kind.createOp, principal, bucketMetadata.OwnerAccountID, bucketPolicy, bucketARN, &PolicyContext{
		TableBucketName: bucketName,
		Namespace:       destNamespace,
		TableName:       destName,
		TableBucketTags: bucketTags,
		IdentityActions: identityActions,
		DefaultAllow:    h.defaultAllowFor(r),
	})
	if !destNamespaceAllowed && !destBucketAllowed {
		h.writeError(w, http.StatusForbidden, ErrCodeAccessDenied, "not authorized to create "+kind.noun+" in the destination namespace")
		return NewAuthError(kind.renameOp, principal, "not authorized to create "+kind.noun+" in the destination namespace")
	}

	metadata.Name = destName
	metadata.Namespace = destNamespace
	metadata.ModifiedAt = time.Now()

	metadataBytes, err := json.Marshal(&metadata)
	if err != nil {
		h.writeError(w, http.StatusInternalServerError, ErrCodeInternalError, "failed to marshal "+kind.noun+" metadata")
		return fmt.Errorf("failed to marshal metadata: %w", err)
	}

	// Write the destination entry before deleting the source so a mid-rename
	// failure can never lose the table.
	err = filerClient.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error {
		if err := h.createDirectory(r.Context(), client, destPath); err != nil {
			return err
		}
		if err := h.setExtendedAttribute(r.Context(), client, destPath, ExtendedKeyMetadata, metadataBytes); err != nil {
			return err
		}
		if err := h.setExtendedAttribute(r.Context(), client, destPath, ExtendedKeyEntryType, []byte(kind.entryType)); err != nil {
			return err
		}
		if len(metadataVersionXattr) > 0 {
			if err := h.setExtendedAttribute(r.Context(), client, destPath, ExtendedKeyMetadataVersion, metadataVersionXattr); err != nil {
				return err
			}
		}
		if len(tableTags) > 0 {
			tagsBytes, err := json.Marshal(tableTags)
			if err != nil {
				return fmt.Errorf("failed to marshal tags: %w", err)
			}
			if err := h.setExtendedAttribute(r.Context(), client, destPath, ExtendedKeyTags, tagsBytes); err != nil {
				return err
			}
		}
		if tablePolicy != "" {
			if err := h.setExtendedAttribute(r.Context(), client, destPath, ExtendedKeyPolicy, []byte(tablePolicy)); err != nil {
				return err
			}
		}
		if len(maintenanceXattr) > 0 {
			if err := h.setExtendedAttribute(r.Context(), client, destPath, ExtendedKeyMaintenance, maintenanceXattr); err != nil {
				return err
			}
		}
		if len(maintenanceStatusXattr) > 0 {
			if err := h.setExtendedAttribute(r.Context(), client, destPath, ExtendedKeyMaintenanceStatus, maintenanceStatusXattr); err != nil {
				return err
			}
		}
		// Soft-delete the source catalog identity in place: drop its catalog xattrs
		// so the name stops resolving while the metadata/ and data/ children stay put
		// (manifests embed absolute paths, so the data must not move).
		return h.removeExtendedAttributesIf(r.Context(), client, srcPath, copiedFromSource,
			renamedTableAttributes...)
	})

	if err != nil {
		if errors.Is(err, ErrConcurrentUpdate) {
			h.writeError(w, http.StatusConflict, ErrCodeConflict, kind.noun+" changed during rename, retry the request")
			return err
		}
		h.writeError(w, http.StatusInternalServerError, ErrCodeInternalError, "failed to rename "+kind.noun)
		return err
	}

	h.writeJSON(w, http.StatusOK, &RenameTableResponse{
		TableARN:         kind.resourceARN(h, metadata.OwnerAccountID, bucketName, destNamespace+"/"+destName),
		MetadataLocation: metadata.MetadataLocation,
	})
	return nil
}

// handleUpdateTable updates table metadata
func (h *S3TablesHandler) handleUpdateTable(w http.ResponseWriter, r *http.Request, filerClient FilerClient) error {
	var req UpdateTableRequest
	if err := h.readRequestBody(r, &req); err != nil {
		h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, err.Error())
		return err
	}

	if req.TableBucketARN == "" || len(req.Namespace) == 0 || req.Name == "" {
		h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, "tableBucketARN, namespace, and name are required")
		return fmt.Errorf("missing required parameters")
	}

	namespaceName, err := validateNamespace(req.Namespace)
	if err != nil {
		h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, err.Error())
		return err
	}

	bucketName, err := parseBucketNameFromARN(req.TableBucketARN)
	if err != nil {
		h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, err.Error())
		return err
	}

	tableName, err := validateTableName(req.Name)
	if err != nil {
		h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, err.Error())
		return err
	}

	tablePath := GetTablePath(bucketName, namespaceName, tableName)

	// Load existing metadata and policies for authorization
	var metadata tableMetadataInternal
	var storedMetadata []byte
	var storedPolicy []byte
	var tablePolicy string
	var bucketPolicy string
	var bucketTags map[string]string
	var tableTags map[string]string
	var bucketMetadata tableBucketMetadata

	err = filerClient.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error {
		// 1. Get Table Metadata
		data, err := h.getExtendedAttribute(r.Context(), client, tablePath, ExtendedKeyMetadata)
		if err != nil {
			return err
		}
		if err := json.Unmarshal(data, &metadata); err != nil {
			return fmt.Errorf("failed to unmarshal table metadata: %w", err)
		}
		storedMetadata = data

		// 2. Get Table Policy & Tags
		policyData, err := h.getExtendedAttribute(r.Context(), client, tablePath, ExtendedKeyPolicy)
		if err == nil {
			tablePolicy = string(policyData)
			storedPolicy = policyData
		} else if !errors.Is(err, ErrAttributeNotFound) {
			return fmt.Errorf("failed to fetch table policy: %w", err)
		}
		tableTags, err = h.readTags(r.Context(), client, tablePath)
		if err != nil {
			return err
		}

		// 3. Get Bucket Metadata, Policy & Tags
		bucketPath := GetTableBucketPath(bucketName)
		data, err = h.getExtendedAttribute(r.Context(), client, bucketPath, ExtendedKeyMetadata)
		if err == nil {
			if err := json.Unmarshal(data, &bucketMetadata); err != nil {
				return fmt.Errorf("failed to unmarshal bucket metadata: %w", err)
			}
		} else if !errors.Is(err, ErrAttributeNotFound) {
			return fmt.Errorf("failed to fetch bucket metadata: %w", err)
		}
		policyData, err = h.getExtendedAttribute(r.Context(), client, bucketPath, ExtendedKeyPolicy)
		if err == nil {
			bucketPolicy = string(policyData)
		} else if !errors.Is(err, ErrAttributeNotFound) {
			return fmt.Errorf("failed to fetch bucket policy: %w", err)
		}
		bucketTags, err = h.readTags(r.Context(), client, bucketPath)
		if err != nil {
			return err
		}

		return nil
	})

	if err != nil {
		if errors.Is(err, filer_pb.ErrNotFound) {
			h.writeError(w, http.StatusNotFound, ErrCodeNoSuchTable, "table not found")
		} else {
			h.writeError(w, http.StatusInternalServerError, ErrCodeInternalError, err.Error())
		}
		return err
	}

	// Authorization Check
	tableARN := h.generateTableARN(metadata.OwnerAccountID, bucketName, namespaceName+"/"+tableName)
	bucketARN := h.generateTableBucketARN(bucketMetadata.OwnerAccountID, bucketName)
	principal := h.getAccountID(r)
	identityActions := getIdentityActions(r)

	tableAllowed := CheckPermissionWithContext("UpdateTable", principal, metadata.OwnerAccountID, tablePolicy, tableARN, &PolicyContext{
		TableBucketName: bucketName,
		Namespace:       namespaceName,
		TableName:       tableName,
		TableBucketTags: bucketTags,
		ResourceTags:    tableTags,
		IdentityActions: identityActions,
		DefaultAllow:    h.defaultAllowFor(r),
	})
	bucketAllowed := CheckPermissionWithContext("UpdateTable", principal, bucketMetadata.OwnerAccountID, bucketPolicy, bucketARN, &PolicyContext{
		TableBucketName: bucketName,
		Namespace:       namespaceName,
		TableName:       tableName,
		TableBucketTags: bucketTags,
		ResourceTags:    tableTags,
		IdentityActions: identityActions,
		DefaultAllow:    h.defaultAllowFor(r),
	})

	if !tableAllowed && !bucketAllowed {
		h.writeError(w, http.StatusForbidden, ErrCodeAccessDenied, "not authorized to update table")
		return NewAuthError("UpdateTable", principal, "not authorized to update table")
	}

	// Check version token if provided
	if req.VersionToken != "" && req.VersionToken != metadata.VersionToken {
		h.writeError(w, http.StatusConflict, ErrCodeConflict, "Version token mismatch")
		return ErrVersionTokenMismatch
	}

	// Update metadata
	if req.Metadata != nil {
		if metadata.Metadata == nil {
			metadata.Metadata = &TableMetadata{}
		}
		if req.Metadata.Iceberg != nil {
			if metadata.Metadata.Iceberg == nil {
				metadata.Metadata.Iceberg = &IcebergMetadata{}
			}
			if req.Metadata.Iceberg.TableUUID != "" {
				metadata.Metadata.Iceberg.TableUUID = req.Metadata.Iceberg.TableUUID
			}
		}
		if len(req.Metadata.FullMetadata) > 0 {
			metadata.Metadata.FullMetadata = req.Metadata.FullMetadata
		}
	}
	if req.MetadataLocation != "" {
		metadata.MetadataLocation = req.MetadataLocation
	}
	if req.MetadataVersion > 0 {
		metadata.MetadataVersion = req.MetadataVersion
	} else if metadata.MetadataVersion == 0 {
		metadata.MetadataVersion = 1
	}
	metadata.ModifiedAt = time.Now()
	metadata.VersionToken = generateVersionToken()

	metadataBytes, err := json.Marshal(metadata)
	if err != nil {
		h.writeError(w, http.StatusInternalServerError, ErrCodeInternalError, "failed to marshal metadata")
		return err
	}

	// Conditional on the metadata this request read and authorized against:
	// two commits that both passed the version-token check would otherwise each
	// write their own metadata and the later one would drop the earlier
	// snapshot. mutateEntryExtended retries on a changed entry, so the check
	// lives in the mutation, where it sees the value that is current now.
	err = filerClient.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error {
		return h.mutateEntryExtended(r.Context(), client, tablePath, func(extended map[string][]byte) error {
			if !bytes.Equal(extended[ExtendedKeyMetadata], storedMetadata) {
				return fmt.Errorf("%w: %s", ErrConcurrentUpdate, ExtendedKeyMetadata)
			}
			// The policy this request was authorized against must still be the
			// one in force: an administrator restricting it mid-commit should
			// send the caller back through authorization, not have its decision
			// applied afterwards.
			if !bytes.Equal(extended[ExtendedKeyPolicy], storedPolicy) {
				return fmt.Errorf("%w: %s", ErrConcurrentUpdate, ExtendedKeyPolicy)
			}
			extended[ExtendedKeyMetadata] = metadataBytes
			return nil
		})
	})

	if err != nil {
		if errors.Is(err, ErrConcurrentUpdate) {
			h.writeError(w, http.StatusConflict, ErrCodeConflict, "table was updated concurrently")
			return ErrVersionTokenMismatch
		}
		h.writeError(w, http.StatusInternalServerError, ErrCodeInternalError, "failed to update metadata")
		return err
	}

	h.writeJSON(w, http.StatusOK, &UpdateTableResponse{
		TableARN:         tableARN,
		MetadataLocation: metadata.MetadataLocation,
		VersionToken:     metadata.VersionToken,
	})
	return nil
}

// createTableTarget is the namespace a create or register resolved to, once the
// caller has been authorized to put a table there.
type createTableTarget struct {
	bucketName     string
	namespaceName  string
	tableName      string
	bucketFormat   string
	ownerAccountID string
}

// authorizeCreateTable validates the names a create names and checks the caller
// may create a table in that namespace, writing the error response itself.
// Deferred creates (Iceberg stage-create) write into the table bucket before any
// table is registered, so they run this same gate first through
// Manager.AuthorizeCreateTable.
func (h *S3TablesHandler) authorizeCreateTable(w http.ResponseWriter, r *http.Request, filerClient FilerClient, tableBucketARN string, namespace []string, name string, requestTags map[string]string) (*createTableTarget, error) {
	if tableBucketARN == "" {
		h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, "tableBucketARN is required")
		return nil, fmt.Errorf("tableBucketARN is required")
	}

	namespaceName, err := validateNamespace(namespace)
	if err != nil {
		h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, err.Error())
		return nil, err
	}

	if name == "" {
		h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, "name is required")
		return nil, fmt.Errorf("name is required")
	}

	bucketName, err := parseBucketNameFromARN(tableBucketARN)
	if err != nil {
		h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, err.Error())
		return nil, err
	}

	tableName, err := validateTableName(name)
	if err != nil {
		h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, err.Error())
		return nil, err
	}

	// Check if namespace exists
	namespacePath := GetNamespacePath(bucketName, namespaceName)
	namespaceMetadata, err := h.loadNamespaceMetadata(r.Context(), filerClient, bucketName, namespaceName)
	if err != nil {
		if errors.Is(err, filer_pb.ErrNotFound) {
			h.writeError(w, http.StatusNotFound, ErrCodeNoSuchNamespace, fmt.Sprintf("namespace %s not found", namespaceName))
		} else {
			h.writeError(w, http.StatusInternalServerError, ErrCodeInternalError, fmt.Sprintf("failed to check namespace: %v", err))
		}
		return nil, err
	}

	// Authorize table creation using policy framework (namespace + bucket policies)
	accountID := h.getAccountID(r)
	bucketPath := GetTableBucketPath(bucketName)
	namespacePolicy := ""
	bucketPolicy := ""
	bucketTags := map[string]string{}
	var bucketMetadata tableBucketMetadata

	err = filerClient.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error {
		// Fetch bucket metadata to use correct owner for bucket policy evaluation
		data, err := h.getExtendedAttribute(r.Context(), client, bucketPath, ExtendedKeyMetadata)
		if err == nil {
			if err := json.Unmarshal(data, &bucketMetadata); err != nil {
				return fmt.Errorf("failed to unmarshal bucket metadata: %w", err)
			}
		} else if !errors.Is(err, ErrAttributeNotFound) {
			return fmt.Errorf("failed to fetch bucket metadata: %v", err)
		}

		// Fetch namespace policy if it exists
		policyData, err := h.getExtendedAttribute(r.Context(), client, namespacePath, ExtendedKeyPolicy)
		if err == nil {
			namespacePolicy = string(policyData)
		} else if !errors.Is(err, ErrAttributeNotFound) {
			return fmt.Errorf("failed to fetch namespace policy: %v", err)
		}

		// Fetch bucket policy if it exists
		policyData, err = h.getExtendedAttribute(r.Context(), client, bucketPath, ExtendedKeyPolicy)
		if err == nil {
			bucketPolicy = string(policyData)
		} else if !errors.Is(err, ErrAttributeNotFound) {
			return fmt.Errorf("failed to fetch bucket policy: %v", err)
		}
		if tags, err := h.readTags(r.Context(), client, bucketPath); err != nil {
			return err
		} else if tags != nil {
			bucketTags = tags
		}

		return nil
	})

	if err != nil {
		h.writeError(w, http.StatusInternalServerError, ErrCodeInternalError, fmt.Sprintf("failed to fetch policies: %v", err))
		return nil, err
	}

	bucketARN := h.generateTableBucketARN(bucketMetadata.OwnerAccountID, bucketName)
	identityActions := getIdentityActions(r)
	policyContext := &PolicyContext{
		TableBucketName: bucketName,
		Namespace:       namespaceName,
		TableName:       tableName,
		RequestTags:     requestTags,
		TagKeys:         mapKeys(requestTags),
		TableBucketTags: bucketTags,
		IdentityActions: identityActions,
		DefaultAllow:    h.defaultAllowFor(r),
	}
	nsAllowed := CheckPermissionWithContext("CreateTable", accountID, namespaceMetadata.OwnerAccountID, namespacePolicy, bucketARN, policyContext)
	bucketAllowed := CheckPermissionWithContext("CreateTable", accountID, bucketMetadata.OwnerAccountID, bucketPolicy, bucketARN, policyContext)
	if !nsAllowed && !bucketAllowed {
		h.writeError(w, http.StatusForbidden, ErrCodeAccessDenied, "not authorized to create table in this namespace")
		return nil, ErrAccessDenied
	}

	return &createTableTarget{
		bucketName:     bucketName,
		namespaceName:  namespaceName,
		tableName:      tableName,
		bucketFormat:   bucketMetadata.Format,
		ownerAccountID: namespaceMetadata.OwnerAccountID,
	}, nil
}
