package rest

import (
	"bytes"
	"context"
	"crypto/sha256"
	"encoding/hex"
	"errors"
	"fmt"
	"io"
	"log"
	"mime"
	"net/http"
	"net/url"
	"os"
	"path"
	"path/filepath"
	"strings"

	"github.com/gin-gonic/gin"
	filefeature "github.com/nexuscrm/backend/internal/features/files"
	"github.com/nexuscrm/backend/internal/features/mru"
	"github.com/nexuscrm/backend/internal/features/permissions"
	"github.com/nexuscrm/backend/internal/infrastructure/storage"
	"github.com/nexuscrm/backend/pkg/apierr"
	appErrors "github.com/nexuscrm/backend/pkg/errors"
	"github.com/nexuscrm/backend/pkg/utils"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
)

type fileRecordStore interface {
	Insert(ctx context.Context, objectName string, data models.SObject, currentUser *models.UserSession) (models.SObject, error)
	Retrieve(ctx context.Context, objectName string, id string, user *models.UserSession) (models.SObject, error)
	RetrieveWithAccess(ctx context.Context, objectName string, id string, user *models.UserSession) (models.SObject, *permissions.RecordAccessResult, error)
	GetRaw(ctx context.Context, objectName string, id string) (models.SObject, error)
	Update(ctx context.Context, objectName string, id string, updates models.SObject, currentUser *models.UserSession) error
	PhysicalDeleteRaw(ctx context.Context, objectName string, id string) error
}

type filePermissionChecker interface {
	CheckPermissionOrErrorWithUser(ctx context.Context, objectAPIName string, operation string, user *models.UserSession) error
}

type durableFileUploadService interface {
	Upload(ctx context.Context, req filefeature.ArchivedOrderUploadRequest) (filefeature.UploadResult, error)
}

// FileUploadHandler handles file upload operations
type FileUploadHandler struct {
	store            storage.ObjectStore
	legacyUploadRoot string
	storageProvider  string
	storageBucket    string
	records          fileRecordStore
	permissions      filePermissionChecker
	durableUploads   durableFileUploadService
}

// NewFileUploadHandler creates a new FileUploadHandler
func NewFileUploadHandler(
	store storage.ObjectStore,
	storageProvider string,
	storageBucket string,
	legacyUploadRoot string,
	records fileRecordStore,
	permissions filePermissionChecker,
) *FileUploadHandler {
	if store == nil {
		panic("file upload handler requires a storage provider")
	}
	storageProvider = strings.TrimSpace(storageProvider)
	if storageProvider == "" {
		storageProvider = storage.ProviderFilesystem
	}
	legacyUploadRoot = strings.TrimSpace(legacyUploadRoot)
	if legacyUploadRoot == "" {
		legacyUploadRoot = "data/uploads"
	}
	return &FileUploadHandler{
		store:            store,
		legacyUploadRoot: legacyUploadRoot,
		storageProvider:  storageProvider,
		storageBucket:    strings.TrimSpace(storageBucket),
		records:          records,
		permissions:      permissions,
	}
}

func (h *FileUploadHandler) SetDurableUploadService(service durableFileUploadService) {
	h.durableUploads = service
}

const maxFileUploadSize = 25 * 1024 * 1024 // 25MB — matches Salesforce REST API limit

const (
	uploadFieldParentID               = "parentId"
	uploadFieldParentObjectAPIName    = "parentObjectApiName"
	uploadFieldAttachmentKind         = "attachmentKind"
	uploadHeaderIDempotencyKey        = "Idempotency-Key"
	defaultRecordAttachmentKind       = "record"
	fileMetadataWriteSecurityReason   = "file upload metadata write"
	fileMetadataDeleteSecurityReason  = "file delete metadata cleanup"
	systemFileSourceTypeNative        = "native"
	systemFileSourceTypeContentCompat = "content_compat"
	systemFileSourceTypeHrefCompat    = "href_compat"
	systemFileUnknownParentObject     = "Unknown"
)

var allowedExtensions = map[string]bool{
	".pdf": true, ".doc": true, ".docx": true,
	".xls": true, ".xlsx": true, ".ppt": true, ".pptx": true,
	".jpg": true, ".jpeg": true, ".png": true, ".gif": true,
	".csv": true, ".txt": true, ".json": true, ".xml": true,
	".zip": true,
}

// blockedMIMEs are content types that could execute in a browser (XSS vectors).
var blockedMIMEs = map[string]bool{
	"text/html":              true,
	"application/javascript": true,
	"application/xhtml+xml":  true,
	"image/svg+xml":          true,
}

// Upload handles POST /sobjects/ContentVersion
// Accepts multipart/form-data, validates size/type, stores bytes through the
// configured object store, and returns a Salesforce-shaped response.
func (h *FileUploadHandler) Upload(c *gin.Context) {
	h.handleUpload(c, false)
}

// UploadRecordFile handles POST /files.
// It stores bytes and creates the _System_File row atomically enough for a
// single request, compensating by deleting the blob if metadata insert fails.
func (h *FileUploadHandler) UploadRecordFile(c *gin.Context) {
	h.handleUpload(c, true)
}

func (h *FileUploadHandler) handleUpload(c *gin.Context, requireParentContext bool) {
	// 1. Enforce size limit at HTTP layer
	c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, maxFileUploadSize)

	fileHeader, err := c.FormFile("file")
	if err != nil {
		if err.Error() == "http: request body too large" {
			c.JSON(http.StatusRequestEntityTooLarge, apierr.SFErrorResponse(
				apierr.SFCodeInvalidRequest, fmt.Sprintf("File exceeds %dMB limit", maxFileUploadSize/(1024*1024))))
			return
		}
		c.JSON(http.StatusBadRequest, apierr.SFErrorResponse(apierr.SFCodeInvalidRequest, "No file provided"))
		return
	}

	if fileHeader.Size > maxFileUploadSize {
		c.JSON(http.StatusRequestEntityTooLarge, apierr.SFErrorResponse(
			apierr.SFCodeInvalidRequest, fmt.Sprintf("File exceeds %dMB limit", maxFileUploadSize/(1024*1024))))
		return
	}

	// 2. Validate extension
	ext := strings.ToLower(filepath.Ext(fileHeader.Filename))
	if !allowedExtensions[ext] {
		c.JSON(http.StatusBadRequest, apierr.SFErrorResponse(apierr.SFCodeInvalidRequest,
			fmt.Sprintf("File type '%s' not allowed", ext)))
		return
	}

	// 3. Detect MIME from actual content (not client header)
	src, err := fileHeader.Open()
	if err != nil {
		c.JSON(http.StatusInternalServerError, apierr.SFErrorResponse(apierr.SFCodeInternalError, "Failed to read file"))
		return
	}
	defer src.Close()

	sniffBuf := make([]byte, 512)
	n, _ := src.Read(sniffBuf)
	detectedMime := http.DetectContentType(sniffBuf[:n])
	if blockedMIMEs[normalizeMediaType(detectedMime)] {
		c.JSON(http.StatusBadRequest, apierr.SFErrorResponse(apierr.SFCodeInvalidRequest, "File content type not allowed"))
		return
	}
	if _, err := src.Seek(0, io.SeekStart); err != nil {
		c.JSON(http.StatusInternalServerError, apierr.SFErrorResponse(apierr.SFCodeInternalError, "Failed to process file"))
		return
	}

	parentID := strings.TrimSpace(c.PostForm(uploadFieldParentID))
	parentObjectAPIName := strings.TrimSpace(c.PostForm(uploadFieldParentObjectAPIName))
	attachmentKind := strings.TrimSpace(c.PostForm(uploadFieldAttachmentKind))
	if attachmentKind == "" {
		attachmentKind = defaultRecordAttachmentKind
	}

	if requireParentContext || parentID != "" || parentObjectAPIName != "" {
		if parentID == "" || parentObjectAPIName == "" {
			c.JSON(http.StatusBadRequest, apierr.SFErrorResponse(apierr.SFCodeInvalidRequest, "parentId and parentObjectApiName are required"))
			return
		}

		uploaded, err := h.uploadRecordFile(c, fileHeader.Filename, ext, detectedMime, src, fileHeader.Size, parentID, parentObjectAPIName, attachmentKind)
		if err != nil {
			h.writeFileOperationError(c, err, "Failed to upload record file")
			return
		}

		c.JSON(http.StatusCreated, gin.H{"data": uploaded})
		return
	}

	if err := h.authorizeLegacyFileCreate(c); err != nil {
		h.writeFileOperationError(c, err, "Failed to store file")
		return
	}

	uploaded, err := h.uploadLegacyFile(c.Request.Context(), fileHeader.Filename, ext, detectedMime, src, fileHeader.Size)
	if err != nil {
		h.writeFileOperationError(c, err, "Failed to store file")
		return
	}

	c.JSON(http.StatusOK, gin.H{"data": uploaded})
}

func (h *FileUploadHandler) uploadLegacyFile(
	ctx context.Context,
	filename string,
	ext string,
	detectedMime string,
	src io.Reader,
	contentLength int64,
) (gin.H, error) {
	// 4. Store with UUID-based object key
	id := utils.GenerateID()
	fileID := id + ext
	objectKey := legacyUploadObjectKey(fileID)

	meta, err := h.store.PutObject(ctx, objectKey, io.LimitReader(src, maxFileUploadSize+1), storage.PutObjectOptions{
		ContentType:   detectedMime,
		ContentLength: &contentLength,
	})
	if err != nil {
		return nil, err
	}

	// 5. Return ID (not path) — client uses download endpoint to retrieve
	return gin.H{
		"Id":          fileID,
		"Name":        sanitizeUploadedFileName(filename),
		"Size":        meta.Size,
		"Mime":        detectedMime,
		"DownloadUrl": legacyContentVersionDownloadURL(fileID),
	}, nil
}

func (h *FileUploadHandler) uploadRecordFile(
	c *gin.Context,
	filename string,
	ext string,
	detectedMime string,
	src io.Reader,
	contentLength int64,
	parentID string,
	parentObjectAPIName string,
	attachmentKind string,
) (gin.H, error) {
	parentObjectAPIName = filefeature.NormalizeParentObjectAPIName(parentID, parentObjectAPIName)
	durableArchivedUpload := isDurableArchivedAttachment(attachmentKind) && h.durableUploads != nil

	if h.records == nil {
		return nil, fmt.Errorf("record-file upload requires persistence dependency")
	}

	user := MustGetUser(c)
	ctx := c.Request.Context()

	parentOperation := constants.PermRead
	if durableArchivedUpload {
		parentOperation = constants.PermEdit
	}
	if err := h.authorizeParentAccess(ctx, parentObjectAPIName, parentID, parentOperation, user); err != nil {
		return nil, err
	}
	if durableArchivedUpload {
		payload, err := io.ReadAll(io.LimitReader(src, maxFileUploadSize+1))
		if err != nil {
			return nil, err
		}
		checksum := sha256.Sum256(payload)
		result, err := h.durableUploads.Upload(ctx, filefeature.ArchivedOrderUploadRequest{
			IDempotencyKey:      strings.TrimSpace(c.GetHeader(uploadHeaderIDempotencyKey)),
			ParentID:            parentID,
			ParentObjectAPIName: parentObjectAPIName,
			FileName:            sanitizeUploadedFileName(filename),
			Extension:           ext,
			MimeType:            detectedMime,
			AttachmentKind:      attachmentKind,
			StorageProvider:     h.storageProvider,
			StorageBucket:       h.storageBucket,
			Body:                bytes.NewReader(payload),
			ContentLength:       contentLength,
			ContentSHA256:       hex.EncodeToString(checksum[:]),
			CreatedByID:         user.ID,
		})
		if err != nil {
			return nil, err
		}
		return gin.H{
			"Id":          result.ID,
			"Name":        result.Name,
			"Size":        result.Size,
			"Mime":        result.Mime,
			"DownloadUrl": result.DownloadURL,
		}, nil
	}

	systemFileID := utils.GenerateID()
	originalFileName := sanitizeUploadedFileName(filename)
	objectKey := systemFileObjectKey(parentObjectAPIName, parentID, systemFileID, originalFileName)

	meta, err := h.store.PutObject(ctx, objectKey, io.LimitReader(src, maxFileUploadSize+1), storage.PutObjectOptions{
		ContentType:   detectedMime,
		ContentLength: &contentLength,
	})
	if err != nil {
		return nil, err
	}

	// The service owns _System_File fields. Upload authorization comes from
	// parent record/action checks; the internal metadata write runs in system
	// context so _System_File is not exposed as a business CRUD surface.
	createCtx := constants.WithSecurityContext(ctx, constants.SystemContext, fileMetadataWriteSecurityReason)
	record := models.SObject{
		constants.FieldID:                          systemFileID,
		constants.FieldSysFile_ParentID:            parentID,
		constants.FieldSysFile_ParentObjectAPIName: parentObjectAPIName,
		constants.FieldSysFile_Name:                originalFileName,
		constants.FieldSysFile_OriginalFileName:    originalFileName,
		constants.FieldSysFile_Extension:           strings.TrimPrefix(ext, "."),
		constants.FieldSysFile_MimeType:            detectedMime,
		constants.FieldSysFile_SizeBytes:           meta.Size,
		constants.FieldSysFile_StoragePath:         objectKey,
		constants.FieldSysFile_StorageProvider:     h.storageProvider,
		constants.FieldSysFile_StorageBucket:       h.storageBucket,
		constants.FieldSysFile_StorageKey:          objectKey,
		constants.FieldSysFile_Status:              "Active",
		constants.FieldSysFile_SourceType:          systemFileSourceTypeNative,
		constants.FieldSysFile_AttachmentKind:      attachmentKind,
	}

	if _, err := h.records.Insert(createCtx, constants.TableFile, record, user); err != nil {
		_ = h.cleanupStoredObject(ctx, record, objectKey)
		return nil, err
	}

	return gin.H{
		"Id":          systemFileID,
		"Name":        originalFileName,
		"Size":        meta.Size,
		"Mime":        detectedMime,
		"DownloadUrl": recordFileDownloadURL(systemFileID),
	}, nil
}

func isDurableArchivedAttachment(kind string) bool {
	switch strings.ToLower(strings.TrimSpace(kind)) {
	case filefeature.AttachmentKindArchivedOrder, filefeature.AttachmentKindArchivedContract:
		return true
	default:
		return false
	}
}

// Download handles GET /sobjects/ContentVersion/:fileId/VersionData
// Streams file content to client with proper headers.
func (h *FileUploadHandler) Download(c *gin.Context) {
	fileId := c.Param("fileId")

	if reader, meta, fileName, handled, err := h.openSystemFileForDownload(c, fileId); handled {
		if err != nil {
			h.writeDownloadError(c, err)
			return
		}
		defer reader.Close()
		h.writeDownloadResponse(c, fileName, reader, meta)
		return
	}

	// Validate: no path separators
	if strings.Contains(fileId, "/") || strings.Contains(fileId, "\\") || strings.Contains(fileId, "..") {
		c.JSON(http.StatusBadRequest, apierr.SFErrorResponse(apierr.SFCodeInvalidRequest, "Invalid file ID"))
		return
	}

	ext := strings.ToLower(filepath.Ext(fileId))
	if !allowedExtensions[ext] {
		c.JSON(http.StatusBadRequest, apierr.SFErrorResponse(apierr.SFCodeInvalidRequest, "Invalid file ID"))
		return
	}

	// Validate UUID stem
	stem := strings.TrimSuffix(fileId, ext)
	if !utils.IsValidUUID(stem) {
		c.JSON(http.StatusBadRequest, apierr.SFErrorResponse(apierr.SFCodeInvalidRequest, "Invalid file ID"))
		return
	}

	if err := h.authorizeLegacyFileRead(c); err != nil {
		h.writeDownloadError(c, err)
		return
	}

	reader, meta, err := h.openForDownload(c, fileId)
	if err != nil {
		h.writeDownloadError(c, err)
		return
	}
	defer reader.Close()

	h.writeDownloadResponse(c, fileId, reader, meta)
}

// DownloadRecordFile handles GET /files/:id/download
func (h *FileUploadHandler) DownloadRecordFile(c *gin.Context) {
	fileID := c.Param("id")
	reader, meta, fileName, handled, err := h.openSystemFileForDownload(c, fileID)
	if !handled {
		c.JSON(http.StatusNotFound, apierr.SFErrorResponse(apierr.SFCodeNotFound, "File not found"))
		return
	}
	if err != nil {
		h.writeDownloadError(c, err)
		return
	}
	defer reader.Close()
	h.writeDownloadResponse(c, fileName, reader, meta)
}

// DeleteRecordFile handles DELETE /files/:id.
// It is idempotent: missing metadata rows or already-missing blobs still return 204.
func (h *FileUploadHandler) DeleteRecordFile(c *gin.Context) {
	if h.records == nil || h.permissions == nil {
		c.JSON(http.StatusInternalServerError, apierr.SFErrorResponse(apierr.SFCodeInternalError, "File delete service unavailable"))
		return
	}

	user := MustGetUser(c)
	ctx := c.Request.Context()
	fileID := c.Param("id")

	if err := h.permissions.CheckPermissionOrErrorWithUser(ctx, constants.TableFile, constants.PermDelete, user); err != nil {
		h.writeFileOperationError(c, err, "Failed to delete file")
		return
	}

	record, err := h.records.GetRaw(ctx, constants.TableFile, fileID)
	if err != nil {
		h.writeFileOperationError(c, err, "Failed to delete file")
		return
	}
	if record == nil {
		c.Status(http.StatusNoContent)
		return
	}

	parentID, _ := record[constants.FieldSysFile_ParentID].(string)
	parentObjectAPIName, _ := record[constants.FieldSysFile_ParentObjectAPIName].(string)
	if err := h.authorizeParentAccess(ctx, parentObjectAPIName, parentID, constants.PermRead, user); err != nil {
		h.writeFileOperationError(c, err, "Failed to delete file")
		return
	}

	if status, _ := record[constants.FieldSysFile_Status].(string); strings.EqualFold(status, "Deleted") {
		if hasPendingStoredObject(record) {
			if err := h.deleteStoredObjectForRecord(ctx, record); err != nil {
				h.writeFileOperationError(c, err, "Failed to delete file")
				return
			}
		}
		c.Status(http.StatusNoContent)
		return
	}

	if sourceType, _ := record[constants.FieldSysFile_SourceType].(string); isCompatSystemFileSourceType(sourceType) {
		h.writeFileOperationError(c, apierr.Invalid("compatibility files cannot be deleted"), "Failed to delete file")
		return
	}

	deleteCtx := constants.WithSecurityContext(ctx, constants.SystemContext, fileMetadataDeleteSecurityReason)
	if err := h.softDeleteRecordFile(deleteCtx, fileID, user); err != nil {
		h.writeFileOperationError(c, err, "Failed to delete file")
		return
	}

	if err := h.deleteStoredObjectForRecord(ctx, record); err != nil {
		h.writeFileOperationError(c, err, "Failed to delete file")
		return
	}

	c.Status(http.StatusNoContent)
}

func (h *FileUploadHandler) writeDownloadResponse(c *gin.Context, fileName string, reader io.Reader, meta storage.ObjectMetadata) {
	contentType := meta.ContentType
	if blockedMIMEs[normalizeMediaType(contentType)] {
		contentType = "application/octet-stream"
	}

	safeName := strings.ReplaceAll(filepath.Base(fileName), "\"", "_")
	c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", safeName))
	c.Header("Content-Type", contentType)
	c.Header("Content-Length", fmt.Sprintf("%d", meta.Size))
	c.Status(http.StatusOK)
	io.Copy(c.Writer, reader)
}

func (h *FileUploadHandler) openSystemFileForDownload(c *gin.Context, fileID string) (io.ReadCloser, storage.ObjectMetadata, string, bool, error) {
	if h.records == nil || h.permissions == nil {
		return nil, storage.ObjectMetadata{}, "", false, nil
	}

	record, err := h.records.GetRaw(c.Request.Context(), constants.TableFile, fileID)
	if err != nil {
		return nil, storage.ObjectMetadata{}, "", true, err
	}
	if record == nil {
		return nil, storage.ObjectMetadata{}, "", false, nil
	}

	status, _ := record[constants.FieldSysFile_Status].(string)
	if strings.EqualFold(status, "Deleted") {
		return nil, storage.ObjectMetadata{}, "", true, storage.ErrObjectNotFound
	}

	user := MustGetUser(c)
	if err := h.permissions.CheckPermissionOrErrorWithUser(c.Request.Context(), constants.TableFile, constants.PermRead, user); err != nil {
		return nil, storage.ObjectMetadata{}, "", true, err
	}
	parentID, _ := record[constants.FieldSysFile_ParentID].(string)
	parentObjectAPIName, _ := record[constants.FieldSysFile_ParentObjectAPIName].(string)
	if err := h.authorizeParentAccess(c.Request.Context(), parentObjectAPIName, parentID, constants.PermRead, user); err != nil {
		return nil, storage.ObjectMetadata{}, "", true, err
	}

	if storageKey, _ := record[constants.FieldSysFile_StorageKey].(string); strings.TrimSpace(storageKey) != "" {
		reader, meta, err := h.openStoredObject(c.Request.Context(), record, storageKey)
		if err != nil {
			return nil, storage.ObjectMetadata{}, "", true, err
		}
		return reader, meta, resolveSystemFileName(record, fileID), true, nil
	}

	if storagePath, _ := record[constants.FieldSysFile_StoragePath].(string); strings.TrimSpace(storagePath) != "" {
		if bucket, key, ok := parseS3ObjectReference(storagePath); ok {
			shadowRecord := cloneFileRecord(record)
			shadowRecord[constants.FieldSysFile_StorageProvider] = storage.ProviderS3
			shadowRecord[constants.FieldSysFile_StorageBucket] = bucket
			reader, meta, err := h.openStoredObject(c.Request.Context(), shadowRecord, key)
			return reader, meta, resolveSystemFileName(record, fileID), true, err
		}
		legacyFileID := resolveLegacyContentVersionReference(storagePath)
		reader, meta, err := h.openForDownload(c, legacyFileID)
		return reader, meta, resolveSystemFileName(record, legacyFileID), true, err
	}

	return nil, storage.ObjectMetadata{}, "", true, storage.ErrObjectNotFound
}

func legacyUploadObjectKey(fileID string) string {
	return path.Join("contentversion", fileID)
}

func resolveLegacyContentVersionReference(fileRef string) string {
	fileRef = strings.TrimSpace(fileRef)
	if fileRef == "" {
		return fileRef
	}

	const routePrefix = "/sobjects/ContentVersion/"
	const routeSuffix = "/VersionData"

	if idx := strings.Index(fileRef, routePrefix); idx >= 0 {
		candidate := fileRef[idx+len(routePrefix):]
		if cut := strings.Index(candidate, routeSuffix); cut >= 0 {
			return strings.TrimSpace(candidate[:cut])
		}
	}

	return fileRef
}

func systemFileObjectKey(parentObjectAPIName, parentID, systemFileID, originalFileName string) string {
	return path.Join(
		"system-file",
		parentObjectAPIName,
		parentID,
		systemFileID,
		sanitizeUploadedFileName(originalFileName),
	)
}

func parseS3ObjectReference(fileRef string) (bucket string, key string, ok bool) {
	parsed, err := url.Parse(strings.TrimSpace(fileRef))
	if err != nil || !strings.EqualFold(parsed.Scheme, "s3") {
		return "", "", false
	}

	bucket = strings.TrimSpace(parsed.Host)
	key = strings.TrimPrefix(strings.TrimSpace(parsed.Path), "/")
	if bucket == "" || key == "" {
		return "", "", false
	}
	return bucket, key, true
}

func (h *FileUploadHandler) openStoredObject(ctx context.Context, record models.SObject, objectKey string) (io.ReadCloser, storage.ObjectMetadata, error) {
	provider, _ := record[constants.FieldSysFile_StorageProvider].(string)
	provider = strings.ToLower(strings.TrimSpace(provider))
	if provider == "" {
		provider = strings.ToLower(strings.TrimSpace(h.storageProvider))
	}

	bucket, _ := record[constants.FieldSysFile_StorageBucket].(string)
	bucket = strings.TrimSpace(bucket)
	if bucket == "" {
		bucket = strings.TrimSpace(h.storageBucket)
	}

	if provider == strings.ToLower(strings.TrimSpace(h.storageProvider)) && bucket == strings.TrimSpace(h.storageBucket) {
		return h.store.GetObject(ctx, objectKey)
	}

	cfg := storage.LoadConfigFromEnv()
	cfg.Provider = provider
	cfg.Bucket = bucket
	ephemeralStore, err := storage.NewStore(cfg)
	if err != nil {
		return nil, storage.ObjectMetadata{}, err
	}
	return ephemeralStore.GetObject(ctx, objectKey)
}

func (h *FileUploadHandler) openForDownload(c *gin.Context, fileID string) (io.ReadCloser, storage.ObjectMetadata, error) {
	reader, meta, err := h.store.GetObject(c.Request.Context(), legacyUploadObjectKey(fileID))
	if err == nil {
		return reader, meta, nil
	}
	if !errors.Is(err, storage.ErrObjectNotFound) {
		return nil, storage.ObjectMetadata{}, err
	}
	return h.openLegacyUploadFile(fileID)
}

func (h *FileUploadHandler) openLegacyUploadFile(fileID string) (io.ReadCloser, storage.ObjectMetadata, error) {
	fullPath := filepath.Join(h.legacyUploadRoot, filepath.Base(fileID))
	file, err := os.Open(fullPath)
	if err != nil {
		if os.IsNotExist(err) {
			return nil, storage.ObjectMetadata{}, storage.ErrObjectNotFound
		}
		return nil, storage.ObjectMetadata{}, fmt.Errorf("open legacy upload: %w", err)
	}

	info, err := file.Stat()
	if err != nil {
		_ = file.Close()
		return nil, storage.ObjectMetadata{}, fmt.Errorf("stat legacy upload: %w", err)
	}

	contentType, err := sniffOpenFileContentType(file)
	if err != nil {
		_ = file.Close()
		return nil, storage.ObjectMetadata{}, err
	}

	return file, storage.ObjectMetadata{
		Key:          fullPath,
		Size:         info.Size(),
		ContentType:  contentType,
		LastModified: info.ModTime(),
	}, nil
}

func sniffOpenFileContentType(file *os.File) (string, error) {
	sniffBuf := make([]byte, 512)
	n, err := file.Read(sniffBuf)
	if err != nil && err != io.EOF {
		return "", fmt.Errorf("sniff content type: %w", err)
	}
	if _, err := file.Seek(0, io.SeekStart); err != nil {
		return "", fmt.Errorf("rewind after sniffing: %w", err)
	}
	if n == 0 {
		return "application/octet-stream", nil
	}
	return http.DetectContentType(sniffBuf[:n]), nil
}

func normalizeMediaType(contentType string) string {
	mediaType, _, err := mime.ParseMediaType(contentType)
	if err != nil {
		return strings.ToLower(strings.TrimSpace(contentType))
	}
	return strings.ToLower(strings.TrimSpace(mediaType))
}

func sanitizeUploadedFileName(filename string) string {
	name := strings.TrimSpace(filepath.Base(filename))
	if name == "." || name == "/" || name == "" {
		return "file"
	}
	return name
}

func recordFileDownloadURL(fileID string) string {
	return fmt.Sprintf("/services/data/v63.0/files/%s/download", fileID)
}

func cloneFileRecord(src models.SObject) models.SObject {
	if src == nil {
		return nil
	}
	dst := make(models.SObject, len(src))
	for key, value := range src {
		dst[key] = value
	}
	return dst
}

func legacyContentVersionDownloadURL(fileID string) string {
	return fmt.Sprintf("/services/data/v63.0/sobjects/ContentVersion/%s/VersionData", fileID)
}

func resolveSystemFileName(record models.SObject, fallback string) string {
	for _, key := range []string{constants.FieldSysFile_OriginalFileName, constants.FieldSysFile_Name} {
		if value, ok := record[key].(string); ok && strings.TrimSpace(value) != "" {
			return value
		}
	}
	return fallback
}

func (h *FileUploadHandler) authorizeParentAccess(ctx context.Context, parentObjectAPIName, parentID string, operation string, user *models.UserSession) error {
	if h.records == nil {
		return fmt.Errorf("file parent authorization requires persistence")
	}
	parentID = strings.TrimSpace(parentID)
	if parentID == "" {
		return apierr.Invalid("missing file parent context")
	}

	resolvedParentObjectAPIName, err := h.resolveParentObjectAPIName(ctx, parentObjectAPIName, parentID)
	if err != nil {
		return err
	}

	if strings.EqualFold(resolvedParentObjectAPIName, constants.TableComment) {
		return h.authorizeCommentParentAccess(ctx, parentID, user)
	}

	if operation == constants.PermEdit {
		_, access, err := h.records.RetrieveWithAccess(ctx, resolvedParentObjectAPIName, parentID, user)
		if err != nil {
			return err
		}
		if access == nil || !access.HasEditAccess {
			return apierr.Forbidden(fmt.Sprintf("permission denied: cannot update %s/%s", resolvedParentObjectAPIName, parentID))
		}
		return nil
	}

	_, err = h.records.Retrieve(ctx, resolvedParentObjectAPIName, parentID, user)
	return err
}

func hasPendingStoredObject(record models.SObject) bool {
	if record == nil {
		return false
	}
	if sourceType, _ := record[constants.FieldSysFile_SourceType].(string); isCompatSystemFileSourceType(sourceType) {
		return false
	}
	if storageKey, _ := record[constants.FieldSysFile_StorageKey].(string); strings.TrimSpace(storageKey) != "" {
		return true
	}
	if storagePath, _ := record[constants.FieldSysFile_StoragePath].(string); strings.TrimSpace(storagePath) != "" {
		return true
	}
	return false
}

func (h *FileUploadHandler) authorizeLegacyFileCreate(c *gin.Context) error {
	if h.permissions == nil {
		return nil
	}
	return h.permissions.CheckPermissionOrErrorWithUser(
		c.Request.Context(),
		constants.TableFile,
		constants.PermCreate,
		MustGetUser(c),
	)
}

func (h *FileUploadHandler) authorizeLegacyFileRead(c *gin.Context) error {
	if h.permissions == nil {
		return nil
	}
	return h.permissions.CheckPermissionOrErrorWithUser(
		c.Request.Context(),
		constants.TableFile,
		constants.PermRead,
		MustGetUser(c),
	)
}

func (h *FileUploadHandler) softDeleteRecordFile(ctx context.Context, fileID string, user *models.UserSession) error {
	if h.records == nil {
		return fmt.Errorf("file delete service unavailable")
	}

	updates := models.SObject{
		constants.FieldSysFile_Status:      "Deleted",
		constants.FieldSysFile_DeletedDate: utils.NowTimestamp(),
		constants.FieldSysFile_DeletedByID: constants.SystemUserID,
	}
	if user != nil && strings.TrimSpace(user.ID) != "" {
		updates[constants.FieldSysFile_DeletedByID] = user.ID
	}

	return h.records.Update(ctx, constants.TableFile, fileID, updates, user)
}

func (h *FileUploadHandler) resolveParentObjectAPIName(ctx context.Context, parentObjectAPIName, parentID string) (string, error) {
	parentObjectAPIName = strings.TrimSpace(parentObjectAPIName)
	if parentObjectAPIName != "" && !strings.EqualFold(parentObjectAPIName, systemFileUnknownParentObject) {
		return parentObjectAPIName, nil
	}

	comment, err := h.records.GetRaw(ctx, constants.TableComment, parentID)
	if err != nil {
		return "", err
	}
	if comment != nil {
		return constants.TableComment, nil
	}

	if inferredParentObjectAPIName, ok := mru.ResolveTypeFromID(parentID); ok {
		return inferredParentObjectAPIName, nil
	}

	return "", apierr.Invalid("missing file parent context")
}

func (h *FileUploadHandler) authorizeCommentParentAccess(ctx context.Context, commentID string, user *models.UserSession) error {
	comment, err := h.records.GetRaw(ctx, constants.TableComment, commentID)
	if err != nil {
		return err
	}
	if comment == nil {
		return apierr.NotFound("Comment not found")
	}

	commentObjectAPIName, _ := comment[constants.FieldSysComment_ObjectAPIName].(string)
	commentRecordID, _ := comment[constants.FieldSysComment_RecordID].(string)
	if commentObjectAPIName == "" || commentRecordID == "" {
		return apierr.Invalid("comment is missing parent record context")
	}

	_, err = h.records.Retrieve(ctx, commentObjectAPIName, commentRecordID, user)
	return err
}

func (h *FileUploadHandler) deleteStoredObjectForRecord(ctx context.Context, record models.SObject) error {
	if sourceType, _ := record[constants.FieldSysFile_SourceType].(string); isCompatSystemFileSourceType(sourceType) {
		return apierr.Invalid("compatibility files cannot be deleted")
	}

	if storageKey, _ := record[constants.FieldSysFile_StorageKey].(string); strings.TrimSpace(storageKey) != "" {
		if err := h.cleanupStoredObject(ctx, record, storageKey); err != nil {
			return err
		}
		return nil
	}

	if storagePath, _ := record[constants.FieldSysFile_StoragePath].(string); strings.TrimSpace(storagePath) != "" {
		if bucket, key, ok := parseS3ObjectReference(storagePath); ok {
			shadowRecord := cloneFileRecord(record)
			shadowRecord[constants.FieldSysFile_StorageProvider] = storage.ProviderS3
			shadowRecord[constants.FieldSysFile_StorageBucket] = bucket
			return h.cleanupStoredObject(ctx, shadowRecord, key)
		}
		if err := h.cleanupLegacyStoredObject(ctx, storagePath); err != nil {
			return err
		}
	}
	return nil
}

func isCompatSystemFileSourceType(sourceType string) bool {
	switch strings.ToLower(strings.TrimSpace(sourceType)) {
	case systemFileSourceTypeContentCompat, systemFileSourceTypeHrefCompat:
		return true
	default:
		return false
	}
}

func (h *FileUploadHandler) cleanupStoredObject(ctx context.Context, record models.SObject, objectKey string) error {
	provider, _ := record[constants.FieldSysFile_StorageProvider].(string)
	provider = strings.ToLower(strings.TrimSpace(provider))
	if provider == "" {
		provider = strings.ToLower(strings.TrimSpace(h.storageProvider))
	}

	bucket, _ := record[constants.FieldSysFile_StorageBucket].(string)
	bucket = strings.TrimSpace(bucket)
	if bucket == "" {
		bucket = strings.TrimSpace(h.storageBucket)
	}

	storeToUse := h.store
	if provider != strings.ToLower(strings.TrimSpace(h.storageProvider)) || bucket != strings.TrimSpace(h.storageBucket) {
		cfg := storage.LoadConfigFromEnv()
		cfg.Provider = provider
		cfg.Bucket = bucket
		overrideStore, err := storage.NewStore(cfg)
		if err != nil {
			return err
		}
		storeToUse = overrideStore
	}

	err := storeToUse.DeleteObject(ctx, objectKey)
	if err == nil || errors.Is(err, storage.ErrObjectNotFound) {
		return nil
	}
	return err
}

func (h *FileUploadHandler) cleanupLegacyStoredObject(ctx context.Context, fileID string) error {
	fileID = resolveLegacyContentVersionReference(fileID)
	err := h.store.DeleteObject(ctx, legacyUploadObjectKey(fileID))
	if err == nil || errors.Is(err, storage.ErrObjectNotFound) {
		return nil
	}

	fullPath := filepath.Join(h.legacyUploadRoot, filepath.Base(fileID))
	if removeErr := os.Remove(fullPath); removeErr == nil || os.IsNotExist(removeErr) {
		return nil
	}

	return err
}

func (h *FileUploadHandler) writeDownloadError(c *gin.Context, err error) {
	if errors.Is(err, storage.ErrObjectNotFound) {
		c.JSON(http.StatusNotFound, apierr.SFErrorResponse(apierr.SFCodeNotFound, "File not found"))
		return
	}
	h.writeFileOperationError(c, err, "Failed to read file")
}

func (h *FileUploadHandler) writeFileOperationError(c *gin.Context, err error, defaultMessage string) {
	var permErr *appErrors.PermissionError
	if errors.As(err, &permErr) {
		c.JSON(http.StatusForbidden, apierr.SFErrorResponse(apierr.SFCodeForbidden, err.Error()))
		return
	}

	var apiErr *apierr.Error
	if errors.As(err, &apiErr) {
		switch apiErr.Status {
		case http.StatusForbidden:
			c.JSON(http.StatusForbidden, apierr.SFErrorResponse(apierr.SFCodeForbidden, err.Error()))
			return
		case http.StatusNotFound:
			c.JSON(http.StatusNotFound, apierr.SFErrorResponse(apierr.SFCodeNotFound, err.Error()))
			return
		case http.StatusBadRequest, http.StatusConflict:
			c.JSON(apiErr.Status, apierr.SFErrorResponse(apierr.SFCodeInvalidRequest, err.Error()))
			return
		}
	}

	log.Printf("file operation failed: %s: %v", defaultMessage, err)
	c.JSON(http.StatusInternalServerError, apierr.SFErrorResponse(apierr.SFCodeInternalError, defaultMessage))
}
