// monorepo/cloud/backend/internal/maplefile/service/file/create_pending_file.go
package file

import (
	"context"
	"fmt"
	"time"

	"go.uber.org/zap"

	"github.com/gocql/gocql"
	"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
	"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config/constants"
	"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/crypto"
	dom_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/collection"
	dom_file "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/file"
	dom_tag "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/tag"
	uc_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/usecase/collection"
	uc_filemetadata "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/usecase/filemetadata"
	uc_fileobjectstorage "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/usecase/fileobjectstorage"
	uc_user "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/usecase/user"
	"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
)

type CreatePendingFileRequestDTO struct {
	ID                gocql.UUID            `json:"id"`
	CollectionID      gocql.UUID            `json:"collection_id"`
	EncryptedMetadata string                `json:"encrypted_metadata"`
	EncryptedFileKey  crypto.EncryptedFileKey `json:"encrypted_file_key"`
	EncryptionVersion string                `json:"encryption_version"`
	EncryptedHash     string                `json:"encrypted_hash"`
	// Optional: expected file size for validation (in bytes)
	ExpectedFileSizeInBytes int64 `json:"expected_file_size_in_bytes,omitempty"`
	// Optional: expected thumbnail size for validation (in bytes)
	ExpectedThumbnailSizeInBytes int64 `json:"expected_thumbnail_size_in_bytes,omitempty"`
	// Optional: content type for file upload validation (e.g., "image/jpeg", "video/mp4")
	// Required for album uploads to enforce photo/video restrictions
	ContentType string `json:"content_type,omitempty"`
	// Optional: tag IDs to embed in file at creation time
	TagIDs []gocql.UUID `json:"tag_ids,omitempty"`
}

type FileResponseDTO struct {
	ID                            gocql.UUID              `json:"id"`
	CollectionID                  gocql.UUID              `json:"collection_id"`
	OwnerID                       gocql.UUID              `json:"owner_id"`
	EncryptedMetadata             string                  `json:"encrypted_metadata"`
	EncryptedFileKey              crypto.EncryptedFileKey `json:"encrypted_file_key"`
	EncryptionVersion             string                  `json:"encryption_version"`
	EncryptedHash                 string                  `json:"encrypted_hash"`
	EncryptedFileSizeInBytes      int64                   `json:"encrypted_file_size_in_bytes"`
	EncryptedThumbnailSizeInBytes int64                   `json:"encrypted_thumbnail_size_in_bytes"`
	Tags                          []dom_tag.EmbeddedTag   `json:"tags"`
	CreatedAt                     time.Time               `json:"created_at"`
	ModifiedAt                    time.Time               `json:"modified_at"`
	Version                       uint64                  `json:"version"`
	State                         string                  `json:"state"`
	TombstoneVersion              uint64                  `json:"tombstone_version"`
	TombstoneExpiry               time.Time               `json:"tombstone_expiry"`
}

type CreatePendingFileResponseDTO struct {
	File                    *FileResponseDTO `json:"file"`
	PresignedUploadURL      string           `json:"presigned_upload_url"`
	PresignedThumbnailURL   string           `json:"presigned_thumbnail_url,omitempty"`
	UploadURLExpirationTime time.Time        `json:"upload_url_expiration_time"`
	Success                 bool             `json:"success"`
	Message                 string           `json:"message"`
}

type CreatePendingFileService interface {
	Execute(ctx context.Context, req *CreatePendingFileRequestDTO) (*CreatePendingFileResponseDTO, error)
}

type createPendingFileServiceImpl struct {
	config                            *config.Configuration
	logger                            *zap.Logger
	getCollectionUseCase              uc_collection.GetCollectionUseCase
	checkCollectionAccessUseCase      uc_collection.CheckCollectionAccessUseCase
	checkFileExistsUseCase            uc_filemetadata.CheckFileExistsUseCase
	createMetadataUseCase             uc_filemetadata.CreateFileMetadataUseCase
	generatePresignedUploadURLUseCase uc_fileobjectstorage.GeneratePresignedUploadURLUseCase
	storageQuotaHelperUseCase         uc_user.UserStorageQuotaHelperUseCase
	tagRepo                           dom_tag.Repository
	fileValidator                     *FileValidator
}

func NewCreatePendingFileService(
	config *config.Configuration,
	logger *zap.Logger,
	getCollectionUseCase uc_collection.GetCollectionUseCase,
	checkCollectionAccessUseCase uc_collection.CheckCollectionAccessUseCase,
	checkFileExistsUseCase uc_filemetadata.CheckFileExistsUseCase,
	createMetadataUseCase uc_filemetadata.CreateFileMetadataUseCase,
	generatePresignedUploadURLUseCase uc_fileobjectstorage.GeneratePresignedUploadURLUseCase,
	storageQuotaHelperUseCase uc_user.UserStorageQuotaHelperUseCase,
	tagRepo dom_tag.Repository,
) CreatePendingFileService {
	logger = logger.Named("CreatePendingFileService")
	return &createPendingFileServiceImpl{
		config:                            config,
		logger:                            logger,
		getCollectionUseCase:              getCollectionUseCase,
		checkCollectionAccessUseCase:      checkCollectionAccessUseCase,
		checkFileExistsUseCase:            checkFileExistsUseCase,
		createMetadataUseCase:             createMetadataUseCase,
		generatePresignedUploadURLUseCase: generatePresignedUploadURLUseCase,
		storageQuotaHelperUseCase:         storageQuotaHelperUseCase,
		tagRepo:                           tagRepo,
		fileValidator:                     NewFileValidator(),
	}
}

func (svc *createPendingFileServiceImpl) Execute(ctx context.Context, req *CreatePendingFileRequestDTO) (*CreatePendingFileResponseDTO, error) {
	//
	// STEP 1: Validation
	//
	if req == nil {
		svc.logger.Warn("⚠️ Failed validation with nil request")
		return nil, httperror.NewForBadRequestWithSingleField("non_field_error", "File creation details are required")
	}

	e := make(map[string]string)
	if req.ID.String() == "" {
		e["id"] = "Client-side generated ID is required"
	}
	doesExist, err := svc.checkFileExistsUseCase.Execute(req.ID)
	if err != nil {
		e["id"] = fmt.Sprintf("Client-side generated ID causes error: %v", req.ID)
	}
	if doesExist {
		e["id"] = "Client-side generated ID already exists"
	}
	if req.CollectionID.String() == "" {
		e["collection_id"] = "Collection ID is required"
	}
	if req.EncryptedMetadata == "" {
		e["encrypted_metadata"] = "Encrypted metadata is required"
	}
	if req.EncryptedFileKey.Ciphertext == nil || len(req.EncryptedFileKey.Ciphertext) == 0 {
		e["encrypted_file_key"] = "Encrypted file key is required"
	}
	if req.EncryptionVersion == "" {
		e["encryption_version"] = "Encryption version is required"
	}
	if req.EncryptedHash == "" {
		e["encrypted_hash"] = "Encrypted hash is required"
	}
	if req.ExpectedFileSizeInBytes <= 0 {
		e["expected_file_size_in_bytes"] = "Expected file size must be greater than 0"
	}

	if len(e) != 0 {
		svc.logger.Warn("⚠️ Failed validation",
			zap.Any("error", e))
		return nil, httperror.NewForBadRequest(&e)
	}

	//
	// STEP 2: Get user ID from context
	//
	userID, ok := ctx.Value(constants.SessionUserID).(gocql.UUID)
	if !ok {
		svc.logger.Error("❌ Failed getting user ID from context")
		return nil, httperror.NewForInternalServerErrorWithSingleField("message", "Authentication context error")
	}

	//
	// STEP 3: Check storage quota BEFORE creating file
	//
	totalExpectedSize := req.ExpectedFileSizeInBytes + req.ExpectedThumbnailSizeInBytes
	err = svc.storageQuotaHelperUseCase.CheckAndReserveQuota(ctx, userID, totalExpectedSize)
	if err != nil {
		svc.logger.Warn("⚠️ Storage quota check failed",
			zap.String("user_id", userID.String()),
			zap.Int64("requested_size", totalExpectedSize),
			zap.Error(err))
		return nil, err // This will be a proper HTTP error from the quota helper
	}

	svc.logger.Info("✅ Storage quota reserved successfully",
		zap.String("user_id", userID.String()),
		zap.Int64("reserved_size", totalExpectedSize))

	//
	// STEP 4: Check if user has write access to the collection
	//
	hasAccess, err := svc.checkCollectionAccessUseCase.Execute(ctx, req.CollectionID, userID, dom_collection.CollectionPermissionReadWrite)
	if err != nil {
		// Release reserved quota on error
		if releaseErr := svc.storageQuotaHelperUseCase.ReleaseQuota(ctx, userID, totalExpectedSize); releaseErr != nil {
			svc.logger.Error("❌ Failed to release quota after collection access check error", zap.Error(releaseErr))
		}

		svc.logger.Error("❌ Failed to check collection access",
			zap.Any("error", err),
			zap.Any("collection_id", req.CollectionID),
			zap.Any("user_id", userID))
		return nil, err
	}

	if !hasAccess {
		// Release reserved quota on access denied
		if releaseErr := svc.storageQuotaHelperUseCase.ReleaseQuota(ctx, userID, totalExpectedSize); releaseErr != nil {
			svc.logger.Error("❌ Failed to release quota after access denied", zap.Error(releaseErr))
		}

		svc.logger.Warn("⚠️ Unauthorized file creation attempt",
			zap.Any("user_id", userID),
			zap.Any("collection_id", req.CollectionID))
		return nil, httperror.NewForForbiddenWithSingleField("message", "You don't have permission to create files in this collection")
	}

	//
	// STEP 5: Get collection details and validate file upload
	//
	// CWE-434: Unrestricted Upload of File with Dangerous Type
	// OWASP A04:2021: Insecure Design - File upload validation
	collection, err := svc.getCollectionUseCase.Execute(ctx, req.CollectionID)
	if err != nil {
		// Release reserved quota on error
		if releaseErr := svc.storageQuotaHelperUseCase.ReleaseQuota(ctx, userID, totalExpectedSize); releaseErr != nil {
			svc.logger.Error("❌ Failed to release quota after collection retrieval error", zap.Error(releaseErr))
		}

		svc.logger.Error("❌ Failed to get collection details",
			zap.Error(err),
			zap.Any("collection_id", req.CollectionID))
		return nil, err
	}

	// Validate file upload based on collection type
	if err := svc.fileValidator.ValidateFileUpload(
		collection.CollectionType,
		req.ExpectedFileSizeInBytes,
		req.ExpectedThumbnailSizeInBytes,
		req.ContentType,
	); err != nil {
		// Release reserved quota on validation error
		if releaseErr := svc.storageQuotaHelperUseCase.ReleaseQuota(ctx, userID, totalExpectedSize); releaseErr != nil {
			svc.logger.Error("❌ Failed to release quota after validation error", zap.Error(releaseErr))
		}

		svc.logger.Warn("⚠️ File upload validation failed",
			zap.Error(err),
			zap.String("collection_type", collection.CollectionType),
			zap.Int64("file_size", req.ExpectedFileSizeInBytes),
			zap.String("content_type", req.ContentType))
		return nil, httperror.NewForBadRequestWithSingleField("file", err.Error())
	}

	svc.logger.Info("✅ File upload validated successfully",
		zap.String("collection_type", collection.CollectionType),
		zap.Int64("file_size", req.ExpectedFileSizeInBytes),
		zap.String("content_type", req.ContentType))

	//
	// STEP 6: Generate storage paths.
	//
	storagePath := generateStoragePath(userID.String(), req.ID.String())
	thumbnailStoragePath := generateThumbnailStoragePath(userID.String(), req.ID.String())

	//
	// STEP 6: Generate presigned upload URLs
	//
	uploadURLDuration := 1 * time.Hour // URLs valid for 1 hour
	expirationTime := time.Now().Add(uploadURLDuration)

	presignedUploadURL, err := svc.generatePresignedUploadURLUseCase.Execute(ctx, storagePath, uploadURLDuration)
	if err != nil {
		// Release reserved quota on error
		if releaseErr := svc.storageQuotaHelperUseCase.ReleaseQuota(ctx, userID, totalExpectedSize); releaseErr != nil {
			svc.logger.Error("❌ Failed to release quota after presigned URL generation error", zap.Error(releaseErr))
		}

		svc.logger.Error("❌ Failed to generate presigned upload URL",
			zap.Any("error", err),
			zap.Any("file_id", req.ID),
			zap.String("storage_path", storagePath))
		return nil, err
	}

	// Generate thumbnail upload URL (optional)
	var presignedThumbnailURL string
	if req.ExpectedThumbnailSizeInBytes > 0 {
		presignedThumbnailURL, err = svc.generatePresignedUploadURLUseCase.Execute(ctx, thumbnailStoragePath, uploadURLDuration)
		if err != nil {
			svc.logger.Warn("⚠️ Failed to generate thumbnail presigned upload URL, continuing without it",
				zap.Any("error", err),
				zap.Any("file_id", req.ID),
				zap.String("thumbnail_storage_path", thumbnailStoragePath))
		}
	}

	//
	// STEP 7: Look up and embed tags if TagIDs were provided
	//
	var embeddedTags []dom_tag.EmbeddedTag
	if len(req.TagIDs) > 0 {
		svc.logger.Debug("🏷️ Looking up tags to embed in file",
			zap.Int("tagCount", len(req.TagIDs)))

		for _, tagID := range req.TagIDs {
			tagObj, err := svc.tagRepo.GetByID(ctx, tagID)
			if err != nil {
				svc.logger.Warn("Failed to get tag for embedding, skipping",
					zap.String("tagID", tagID.String()),
					zap.Error(err))
				continue
			}
			// Verify tag belongs to the user
			if tagObj.UserID != userID {
				svc.logger.Warn("Tag does not belong to user, skipping",
					zap.String("tagID", tagID.String()),
					zap.String("userID", userID.String()))
				continue
			}
			embeddedTags = append(embeddedTags, *tagObj.ToEmbeddedTag())
		}

		svc.logger.Info("✅ Tags embedded in file",
			zap.Int("embeddedCount", len(embeddedTags)),
			zap.Int("requestedCount", len(req.TagIDs)))
	}

	//
	// STEP 8: Create pending file metadata record
	//
	now := time.Now()
	file := &dom_file.File{
		ID:                            req.ID,
		CollectionID:                  req.CollectionID,
		OwnerID:                       userID,
		EncryptedMetadata:             req.EncryptedMetadata,
		EncryptedFileKey:              req.EncryptedFileKey,
		EncryptionVersion:             req.EncryptionVersion,
		EncryptedHash:                 req.EncryptedHash,
		EncryptedFileObjectKey:        storagePath,
		EncryptedFileSizeInBytes:      req.ExpectedFileSizeInBytes, // Will be updated when upload completes
		EncryptedThumbnailObjectKey:   thumbnailStoragePath,
		EncryptedThumbnailSizeInBytes: req.ExpectedThumbnailSizeInBytes, // Will be updated when upload completes
		Tags:                          embeddedTags,
		CreatedAt:                     now,
		CreatedByUserID:               userID,
		ModifiedAt:                    now,
		ModifiedByUserID:              userID,
		Version:                       1,                         // File creation always starts mutation version at 1.
		State:                         dom_file.FileStatePending, // File creation always starts state in a pending upload.
	}

	err = svc.createMetadataUseCase.Execute(file)
	if err != nil {
		// Release reserved quota on error
		if releaseErr := svc.storageQuotaHelperUseCase.ReleaseQuota(ctx, userID, totalExpectedSize); releaseErr != nil {
			svc.logger.Error("❌ Failed to release quota after metadata creation error", zap.Error(releaseErr))
		}

		svc.logger.Error("❌ Failed to create pending file metadata",
			zap.Any("error", err),
			zap.Any("file_id", req.ID))
		return nil, err
	}

	//
	// STEP 9: Prepare response
	//
	response := &CreatePendingFileResponseDTO{
		File:                    mapFileToDTO(file),
		PresignedUploadURL:      presignedUploadURL,
		PresignedThumbnailURL:   presignedThumbnailURL,
		UploadURLExpirationTime: expirationTime,
		Success:                 true,
		Message:                 "Pending file created successfully. Storage quota reserved. Use the presigned URL to upload your file.",
	}

	svc.logger.Info("✅ Pending file created successfully with quota reservation",
		zap.Any("file_id", req.ID),
		zap.Any("collection_id", req.CollectionID),
		zap.Any("owner_id", userID),
		zap.String("storage_path", storagePath),
		zap.Int64("reserved_size", totalExpectedSize),
		zap.Time("url_expiration", expirationTime))

	return response, nil
}

// Helper function to generate consistent storage path
func generateStoragePath(ownerID, fileID string) string {
	return fmt.Sprintf("users/%s/files/%s", ownerID, fileID)
}

// Helper function to generate consistent thumbnail storage path
func generateThumbnailStoragePath(ownerID, fileID string) string {
	return fmt.Sprintf("users/%s/files/%s_thumb", ownerID, fileID)
}
