package app

import (
	"encoding/json"
	"fmt"
	"os"
	"path/filepath"
	"time"

	"go.uber.org/zap"

	"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/maplefile/e2ee"
)

// =============================================================================
// EXPORT FILE CONTENT OPERATIONS
// =============================================================================

// ExportFileContent exports a single file - copies from local if available, otherwise downloads from cloud
func (a *Application) ExportFileContent(fileID string, collectionName string, exportPath string) (*FileExportResult, error) {
	a.logger.Debug("Exporting file content",
		zap.String("file_id", fileID),
		zap.String("collection_name", collectionName))

	result := &FileExportResult{
		FileID:  fileID,
		Success: false,
	}

	// Create collection folder in export path
	// Sanitize collection name for filesystem
	safeName := sanitizeFilename(collectionName)
	collectionDir := filepath.Join(exportPath, "files", safeName)
	if err := os.MkdirAll(collectionDir, 0755); err != nil {
		result.ErrorMessage = fmt.Sprintf("failed to create directory: %v", err)
		return result, nil
	}

	// Check if file exists locally with decrypted content (FilePath must be non-empty)
	// Note: HasLocalContent() returns true for encrypted-only files, but we need the
	// decrypted FilePath to copy, not EncryptedFilePath
	localFile, err := a.mustGetFileRepo().Get(fileID)
	if err == nil && localFile != nil && localFile.FilePath != "" {
		// File exists locally with decrypted content - copy it
		result.Filename = localFile.Name
		result.SourceType = "local"
		result.SizeBytes = localFile.DecryptedSizeInBytes

		destPath := filepath.Join(collectionDir, sanitizeFilename(localFile.Name))
		result.DestPath = destPath

		// Copy the file
		if err := copyFile(localFile.FilePath, destPath); err != nil {
			result.ErrorMessage = fmt.Sprintf("failed to copy local file: %v", err)
			return result, nil
		}

		result.Success = true
		a.logger.Debug("File copied from local storage",
			zap.String("file_id", fileID),
			zap.String("dest", destPath))
		return result, nil
	}

	// File not available locally - download from cloud and save directly to export path
	result.SourceType = "cloud"

	// Download and decrypt file directly to export directory
	filename, fileSize, err := a.downloadFileToPath(fileID, collectionDir)
	if err != nil {
		result.ErrorMessage = fmt.Sprintf("failed to download file: %v", err)
		return result, nil
	}

	result.Filename = filename
	result.SizeBytes = fileSize
	result.DestPath = filepath.Join(collectionDir, sanitizeFilename(filename))
	result.Success = true

	a.logger.Debug("File downloaded from cloud",
		zap.String("file_id", fileID),
		zap.String("dest", result.DestPath))

	return result, nil
}

// downloadFileToPath downloads and decrypts a file directly to a specified directory.
// Returns the filename, file size, and error. This is used for bulk exports without user dialog.
func (a *Application) downloadFileToPath(fileID string, destDir string) (string, int64, error) {
	// Get current session for authentication
	session, err := a.authService.GetCurrentSession(a.ctx)
	if err != nil {
		return "", 0, fmt.Errorf("not authenticated: %w", err)
	}

	// Get master key from cache
	email := session.Email
	masterKey, cleanupMasterKey, err := a.keyCache.GetMasterKey(email)
	if err != nil {
		return "", 0, fmt.Errorf("encryption key not available: %w", err)
	}
	defer cleanupMasterKey()

	// Use SDK client which has automatic token refresh on 401
	apiClient := a.authService.GetAPIClient()

	// Step 1: Get file metadata using SDK (has automatic 401 retry)
	fileData, err := apiClient.GetFile(a.ctx, fileID)
	if err != nil {
		return "", 0, fmt.Errorf("failed to get file: %w", err)
	}

	// Step 2: Get collection to decrypt collection key using SDK (has automatic 401 retry)
	collData, err := apiClient.GetCollection(a.ctx, fileData.CollectionID)
	if err != nil {
		return "", 0, fmt.Errorf("failed to get collection: %w", err)
	}

	// Step 3: Decrypt collection key with master key
	// SDK returns EncryptedCollectionKey as an object with Ciphertext and Nonce fields
	// Use tryDecodeBase64 to handle multiple base64 encoding formats
	collKeyNonce, err := tryDecodeBase64(collData.EncryptedCollectionKey.Nonce)
	if err != nil {
		return "", 0, fmt.Errorf("failed to decode collection key nonce: %w", err)
	}
	collKeyCiphertext, err := tryDecodeBase64(collData.EncryptedCollectionKey.Ciphertext)
	if err != nil {
		return "", 0, fmt.Errorf("failed to decode collection key ciphertext: %w", err)
	}

	// Handle web frontend combined ciphertext format (nonce + encrypted_data)
	actualCollKeyCiphertext := extractActualCiphertext(collKeyCiphertext, collKeyNonce)

	collectionKey, err := e2ee.DecryptCollectionKey(&e2ee.EncryptedKey{
		Ciphertext: actualCollKeyCiphertext,
		Nonce:      collKeyNonce,
	}, masterKey)
	if err != nil {
		return "", 0, fmt.Errorf("failed to decrypt collection key: %w", err)
	}

	// Step 4: Decrypt file key with collection key
	// NOTE: The web frontend may send combined ciphertext (nonce + encrypted_data)
	// or separate fields. We handle both formats.
	// Use tryDecodeBase64 to handle multiple base64 encoding formats
	fileKeyNonce, err := tryDecodeBase64(fileData.EncryptedFileKey.Nonce)
	if err != nil {
		return "", 0, fmt.Errorf("failed to decode file key nonce: %w", err)
	}
	fileKeyCiphertext, err := tryDecodeBase64(fileData.EncryptedFileKey.Ciphertext)
	if err != nil {
		return "", 0, fmt.Errorf("failed to decode file key ciphertext: %w", err)
	}

	// Handle web frontend combined ciphertext format (nonce + encrypted_data)
	actualFileKeyCiphertext := extractActualCiphertext(fileKeyCiphertext, fileKeyNonce)

	fileKey, err := e2ee.DecryptFileKey(&e2ee.EncryptedKey{
		Ciphertext: actualFileKeyCiphertext,
		Nonce:      fileKeyNonce,
	}, collectionKey)
	if err != nil {
		return "", 0, fmt.Errorf("failed to decrypt file key: %w", err)
	}

	// Step 5: Decrypt metadata to get filename
	// Use tryDecodeBase64 to handle multiple base64 encoding formats
	encryptedMetadataBytes, err := tryDecodeBase64(fileData.EncryptedMetadata)
	if err != nil {
		return "", 0, fmt.Errorf("failed to decode metadata: %w", err)
	}

	metadataNonce, metadataCiphertext, err := e2ee.SplitNonceAndCiphertextAuto(encryptedMetadataBytes)
	if err != nil {
		return "", 0, fmt.Errorf("failed to parse metadata: %w", err)
	}

	decryptedMetadata, err := e2ee.DecryptWithAlgorithm(metadataCiphertext, metadataNonce, fileKey)
	if err != nil {
		return "", 0, fmt.Errorf("failed to decrypt metadata: %w", err)
	}

	var metadata struct {
		Filename string `json:"name"`
		MimeType string `json:"mime_type"`
	}
	if err := json.Unmarshal(decryptedMetadata, &metadata); err != nil {
		return "", 0, fmt.Errorf("failed to parse metadata: %w", err)
	}

	// Step 6: Get presigned download URL using SDK (has automatic 401 retry)
	downloadResp, err := apiClient.GetPresignedDownloadURL(a.ctx, fileID)
	if err != nil {
		return "", 0, fmt.Errorf("failed to get download URL: %w", err)
	}

	// Step 7: Download encrypted file from S3 using SDK helper
	encryptedContent, err := apiClient.DownloadFromPresignedURL(a.ctx, downloadResp.FileURL)
	if err != nil {
		return "", 0, fmt.Errorf("failed to download file: %w", err)
	}

	// Step 8: Decrypt file content
	decryptedContent, err := e2ee.DecryptFile(encryptedContent, fileKey)
	if err != nil {
		return "", 0, fmt.Errorf("failed to decrypt file: %w", err)
	}

	// Step 9: Write decrypted content to destination
	destPath := filepath.Join(destDir, sanitizeFilename(metadata.Filename))
	if err := os.WriteFile(destPath, decryptedContent, 0600); err != nil {
		return "", 0, fmt.Errorf("failed to save file: %w", err)
	}

	return metadata.Filename, int64(len(decryptedContent)), nil
}

// ExportAllFiles exports all files from all collections
func (a *Application) ExportAllFiles(exportPath string, progressCallback func(current, total int, filename string)) (*ExportSummary, error) {
	a.logger.Info("Exporting all files", zap.String("export_path", exportPath))

	summary := &ExportSummary{
		ExportedAt: time.Now().Format(time.RFC3339),
		ExportPath: exportPath,
		Errors:     make([]ExportError, 0),
	}

	// Get all collections
	ownedCollections, err := a.ListCollections()
	if err != nil {
		return nil, fmt.Errorf("failed to list owned collections: %w", err)
	}
	summary.OwnedCollections = len(ownedCollections)

	sharedCollections, err := a.listSharedCollections()
	if err != nil {
		a.logger.Warn("Failed to list shared collections", zap.Error(err))
		sharedCollections = []*CollectionData{}
	}
	summary.SharedCollections = len(sharedCollections)
	summary.TotalCollections = summary.OwnedCollections + summary.SharedCollections

	// Build list of all files to export
	type fileToExport struct {
		fileID         string
		collectionID   string
		collectionName string
	}

	allFilesToExport := make([]fileToExport, 0)

	// Collect files from owned collections
	for _, coll := range ownedCollections {
		files, err := a.ListFilesByCollection(coll.ID)
		if err != nil {
			a.logger.Warn("Failed to list files for collection",
				zap.String("collection_id", coll.ID),
				zap.Error(err))
			continue
		}
		for _, f := range files {
			allFilesToExport = append(allFilesToExport, fileToExport{
				fileID:         f.ID,
				collectionID:   coll.ID,
				collectionName: coll.Name,
			})
		}
	}

	// Collect files from shared collections
	for _, coll := range sharedCollections {
		files, err := a.ListFilesByCollection(coll.ID)
		if err != nil {
			a.logger.Warn("Failed to list files for shared collection",
				zap.String("collection_id", coll.ID),
				zap.Error(err))
			continue
		}
		for _, f := range files {
			// Prefix shared collection names to distinguish them
			collName := "Shared - " + coll.Name
			allFilesToExport = append(allFilesToExport, fileToExport{
				fileID:         f.ID,
				collectionID:   coll.ID,
				collectionName: collName,
			})
		}
	}

	summary.TotalFiles = len(allFilesToExport)

	// Export each file
	for i, f := range allFilesToExport {
		if progressCallback != nil {
			progressCallback(i+1, summary.TotalFiles, f.collectionName)
		}

		result, err := a.ExportFileContent(f.fileID, f.collectionName, exportPath)
		if err != nil || !result.Success {
			summary.FilesFailed++
			errMsg := "unknown error"
			if err != nil {
				errMsg = err.Error()
			} else if result.ErrorMessage != "" {
				errMsg = result.ErrorMessage
			}

			summary.Errors = append(summary.Errors, ExportError{
				FileID:       f.fileID,
				Filename:     result.Filename,
				CollectionID: f.collectionID,
				ErrorMessage: errMsg,
				Timestamp:    time.Now().Format(time.RFC3339),
			})
			continue
		}

		summary.FilesExported++
		summary.TotalSizeBytes += result.SizeBytes

		if result.SourceType == "local" {
			summary.FilesCopiedLocal++
		} else {
			summary.FilesDownloaded++
		}
	}

	// Save export manifest
	manifestPath := filepath.Join(exportPath, "export_manifest.json")
	manifestData, err := json.MarshalIndent(summary, "", "  ")
	if err != nil {
		a.logger.Warn("Failed to marshal export manifest", zap.Error(err))
	} else {
		if err := os.WriteFile(manifestPath, manifestData, 0644); err != nil {
			a.logger.Warn("Failed to write export manifest", zap.Error(err))
		}
	}

	a.logger.Info("Export completed",
		zap.Int("files_exported", summary.FilesExported),
		zap.Int("files_failed", summary.FilesFailed),
		zap.Int("copied_local", summary.FilesCopiedLocal),
		zap.Int("downloaded", summary.FilesDownloaded))

	return summary, nil
}
