package app

import (
	"encoding/base64"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"os"
	"os/exec"
	"path/filepath"
	sysRuntime "runtime"
	"strings"

	"github.com/wailsapp/wails/v2/pkg/runtime"
	"go.uber.org/zap"

	"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/maplefile/e2ee"
	"codeberg.org/mapleopentech/monorepo/native/desktop/maplefile/internal/domain/file"
	"codeberg.org/mapleopentech/monorepo/native/desktop/maplefile/internal/service/inputvalidation"
)

// =============================================================================
// FILE DOWNLOAD OPERATIONS
// =============================================================================

// tryDecodeBase64 attempts to decode a base64 string using multiple encoding variants.
// The web frontend uses URL-safe base64 without padding (libsodium default),
// while Go typically uses standard base64 with padding.
func tryDecodeBase64(s string) ([]byte, error) {
	var lastErr error

	// Try URL-safe base64 without padding FIRST (libsodium's URLSAFE_NO_PADDING)
	// This is the format used by the web frontend
	if data, err := base64.RawURLEncoding.DecodeString(s); err == nil {
		return data, nil
	} else {
		lastErr = err
	}

	// Try standard base64 with padding (Go's default)
	if data, err := base64.StdEncoding.DecodeString(s); err == nil {
		return data, nil
	} else {
		lastErr = err
	}

	// Try standard base64 without padding
	if data, err := base64.RawStdEncoding.DecodeString(s); err == nil {
		return data, nil
	} else {
		lastErr = err
	}

	// Try URL-safe base64 with padding
	if data, err := base64.URLEncoding.DecodeString(s); err == nil {
		return data, nil
	} else {
		lastErr = err
	}

	return nil, fmt.Errorf("failed to decode base64 with any encoding variant (input length: %d, first 50 chars: %s, last error: %w)", len(s), truncateString(s, 50), lastErr)
}

// truncateString truncates a string to the specified length
func truncateString(s string, maxLen int) string {
	if len(s) <= maxLen {
		return s
	}
	return s[:maxLen] + "..."
}

// GetFileDownloadURL gets a presigned download URL for a file
func (a *Application) GetFileDownloadURL(fileID string) (string, error) {
	// Validate input
	if err := inputvalidation.ValidateFileID(fileID); err != nil {
		return "", err
	}

	// Get current session for authentication
	session, err := a.authService.GetCurrentSession(a.ctx)
	if err != nil {
		a.logger.Error("Failed to get current session", zap.Error(err))
		return "", fmt.Errorf("not authenticated: %w", err)
	}

	apiClient := a.authService.GetAPIClient()

	// Make the HTTP GET request for download URL
	// Note: Backend uses singular "file" not plural "files" in the path
	req, err := http.NewRequestWithContext(a.ctx, "GET", apiClient.GetBaseURL()+"/api/v1/file/"+fileID+"/download-url", nil)
	if err != nil {
		a.logger.Error("Failed to create download URL request", zap.Error(err))
		return "", fmt.Errorf("failed to create request: %w", err)
	}

	req.Header.Set("Authorization", "Bearer "+session.AccessToken)

	resp, err := a.httpClient.Do(req)
	if err != nil {
		a.logger.Error("Failed to send download URL request", zap.Error(err))
		return "", fmt.Errorf("failed to send request: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		body, _ := io.ReadAll(resp.Body)
		a.logger.Error("Failed to get download URL",
			zap.Int("status", resp.StatusCode),
			zap.String("body", string(body)))
		return "", fmt.Errorf("failed to get download URL: %s", string(body))
	}

	// Response structure matches backend's GetPresignedDownloadURLResponseDTO
	var urlResp struct {
		PresignedDownloadURL      string `json:"presigned_download_url"`
		DownloadURLExpirationTime string `json:"download_url_expiration_time"`
		Success                   bool   `json:"success"`
		Message                   string `json:"message"`
	}
	if err := json.NewDecoder(resp.Body).Decode(&urlResp); err != nil {
		a.logger.Error("Failed to decode download URL response", zap.Error(err))
		return "", fmt.Errorf("failed to decode response: %w", err)
	}

	return urlResp.PresignedDownloadURL, nil
}

// DownloadFile downloads, decrypts, and saves a file to the user's chosen location.
// If the file already exists locally, it copies from local storage instead of re-downloading.
func (a *Application) DownloadFile(fileID string) (string, error) {
	// Validate input
	if err := inputvalidation.ValidateFileID(fileID); err != nil {
		return "", err
	}

	a.logger.Info("Starting file download", zap.String("file_id", fileID))

	// First, check if file already exists locally
	localFile, err := a.mustGetFileRepo().Get(fileID)
	if err == nil && localFile != nil && localFile.FilePath != "" {
		// Check if local file actually exists on disk
		if _, statErr := os.Stat(localFile.FilePath); statErr == nil {
			a.logger.Info("File exists locally, using local copy",
				zap.String("file_id", fileID),
				zap.String("local_path", localFile.FilePath))

			// Open save dialog for user to choose location
			savePath, dialogErr := runtime.SaveFileDialog(a.ctx, runtime.SaveDialogOptions{
				Title:           "Save File As",
				DefaultFilename: localFile.Name,
			})
			if dialogErr != nil {
				return "", fmt.Errorf("failed to open save dialog: %w", dialogErr)
			}

			// User cancelled the dialog
			if savePath == "" {
				a.logger.Info("User cancelled save dialog")
				return "", nil
			}

			// Copy local file to chosen location
			srcFile, copyErr := os.Open(localFile.FilePath)
			if copyErr != nil {
				return "", fmt.Errorf("failed to open local file: %w", copyErr)
			}
			defer srcFile.Close()

			dstFile, copyErr := os.Create(savePath)
			if copyErr != nil {
				return "", fmt.Errorf("failed to create destination file: %w", copyErr)
			}
			defer dstFile.Close()

			if _, copyErr := io.Copy(dstFile, srcFile); copyErr != nil {
				return "", fmt.Errorf("failed to copy file: %w", copyErr)
			}

			a.logger.Info("File saved from local copy",
				zap.String("file_id", fileID),
				zap.String("save_path", savePath))

			return savePath, nil
		}
	}

	// File not available locally, download from cloud
	a.logger.Info("File not available locally, downloading from cloud", zap.String("file_id", fileID))

	// Get current session for authentication
	session, err := a.authService.GetCurrentSession(a.ctx)
	if err != nil {
		a.logger.Error("Failed to get current session", zap.Error(err))
		return "", fmt.Errorf("not authenticated: %w", err)
	}

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

	apiClient := a.authService.GetAPIClient()

	// Step 1: Get file metadata
	fileReq, err := http.NewRequestWithContext(a.ctx, "GET", apiClient.GetBaseURL()+"/api/v1/file/"+fileID, nil)
	if err != nil {
		a.logger.Error("Failed to create get file request", zap.Error(err))
		return "", fmt.Errorf("failed to create request: %w", err)
	}
	fileReq.Header.Set("Authorization", "Bearer "+session.AccessToken)

	fileResp, err := a.httpClient.Do(fileReq)
	if err != nil {
		a.logger.Error("Failed to get file metadata", zap.Error(err))
		return "", fmt.Errorf("failed to get file: %w", err)
	}
	defer fileResp.Body.Close()

	if fileResp.StatusCode != http.StatusOK {
		body, _ := io.ReadAll(fileResp.Body)
		a.logger.Error("Failed to get file", zap.Int("status", fileResp.StatusCode), zap.String("body", string(body)))
		return "", fmt.Errorf("failed to get file: %s", string(body))
	}

	var fileData struct {
		ID                string `json:"id"`
		CollectionID      string `json:"collection_id"`
		EncryptedMetadata string `json:"encrypted_metadata"`
		EncryptedFileKey  struct {
			Ciphertext string `json:"ciphertext"`
			Nonce      string `json:"nonce"`
		} `json:"encrypted_file_key"`
	}
	if err := json.NewDecoder(fileResp.Body).Decode(&fileData); err != nil {
		a.logger.Error("Failed to decode file response", zap.Error(err))
		return "", fmt.Errorf("failed to decode response: %w", err)
	}

	// Step 2: Get collection to decrypt collection key
	collReq, err := http.NewRequestWithContext(a.ctx, "GET", apiClient.GetBaseURL()+"/api/v1/collections/"+fileData.CollectionID, nil)
	if err != nil {
		a.logger.Error("Failed to create get collection request", zap.Error(err))
		return "", fmt.Errorf("failed to create request: %w", err)
	}
	collReq.Header.Set("Authorization", "Bearer "+session.AccessToken)

	collResp, err := a.httpClient.Do(collReq)
	if err != nil {
		a.logger.Error("Failed to get collection", zap.Error(err))
		return "", fmt.Errorf("failed to get collection: %w", err)
	}
	defer collResp.Body.Close()

	if collResp.StatusCode != http.StatusOK {
		body, _ := io.ReadAll(collResp.Body)
		a.logger.Error("Failed to get collection", zap.Int("status", collResp.StatusCode), zap.String("body", string(body)))
		return "", fmt.Errorf("failed to get collection: %s", string(body))
	}

	var collData struct {
		EncryptedCollectionKey struct {
			Ciphertext string `json:"ciphertext"`
			Nonce      string `json:"nonce"`
		} `json:"encrypted_collection_key"`
	}
	if err := json.NewDecoder(collResp.Body).Decode(&collData); err != nil {
		a.logger.Error("Failed to decode collection response", zap.Error(err))
		return "", fmt.Errorf("failed to decode collection: %w", err)
	}

	// Step 3: Decrypt collection key with master key
	// Use tryDecodeBase64 to handle multiple base64 encoding formats
	collKeyNonce, err := tryDecodeBase64(collData.EncryptedCollectionKey.Nonce)
	if err != nil {
		return "", fmt.Errorf("failed to decode collection key nonce: %w", err)
	}
	collKeyCiphertext, err := tryDecodeBase64(collData.EncryptedCollectionKey.Ciphertext)
	if err != nil {
		return "", 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 {
		a.logger.Error("Failed to decrypt collection key", zap.Error(err))
		return "", 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 "", fmt.Errorf("failed to decode file key nonce: %w", err)
	}
	fileKeyCiphertext, err := tryDecodeBase64(fileData.EncryptedFileKey.Ciphertext)
	if err != nil {
		return "", fmt.Errorf("failed to decode file key ciphertext: %w", err)
	}

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

	a.logger.Info("Decrypting file key",
		zap.Int("nonce_size", len(fileKeyNonce)),
		zap.Int("ciphertext_size", len(actualFileKeyCiphertext)),
		zap.Int("collection_key_size", len(collectionKey)))

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

	a.logger.Info("File key decrypted successfully", zap.Int("file_key_size", len(fileKey)))

	// Step 5: Decrypt metadata to get filename
	// Use tryDecodeBase64 to handle URL-safe base64 without padding (libsodium format)
	encryptedMetadataBytes, err := tryDecodeBase64(fileData.EncryptedMetadata)
	if err != nil {
		return "", fmt.Errorf("failed to decode metadata: %w", err)
	}

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

	decryptedMetadata, err := e2ee.DecryptWithAlgorithm(metadataCiphertext, metadataNonce, fileKey)
	if err != nil {
		return "", 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 "", fmt.Errorf("failed to parse metadata: %w", err)
	}

	// Step 6: Get presigned download URL
	downloadURL, err := a.GetFileDownloadURL(fileID)
	if err != nil {
		return "", fmt.Errorf("failed to get download URL: %w", err)
	}

	// Step 6.5: Validate download URL before use (SSRF protection)
	if err := inputvalidation.ValidateDownloadURL(downloadURL); err != nil {
		a.logger.Error("Download URL validation failed",
			zap.String("file_id", fileID),
			zap.Error(err))
		return "", fmt.Errorf("download URL validation failed: %w", err)
	}

	// Step 7: Download encrypted file from S3 (use large download client - no timeout for big files)
	a.logger.Info("Downloading encrypted file from S3", zap.String("filename", metadata.Filename))
	downloadResp, err := a.httpClient.GetLargeDownload(downloadURL)
	if err != nil {
		a.logger.Error("Failed to download file from S3", zap.Error(err))
		return "", fmt.Errorf("failed to download file: %w", err)
	}
	defer downloadResp.Body.Close()

	if downloadResp.StatusCode != http.StatusOK {
		body, _ := io.ReadAll(downloadResp.Body)
		a.logger.Error("S3 download failed", zap.Int("status", downloadResp.StatusCode), zap.String("body", string(body)))
		return "", fmt.Errorf("failed to download file from storage: status %d", downloadResp.StatusCode)
	}

	encryptedContent, err := io.ReadAll(downloadResp.Body)
	if err != nil {
		a.logger.Error("Failed to read encrypted content", zap.Error(err))
		return "", fmt.Errorf("failed to read file content: %w", err)
	}

	a.logger.Info("Downloaded encrypted file", zap.Int("encrypted_size", len(encryptedContent)))

	// Step 8: Decrypt file content
	a.logger.Info("Decrypting file content",
		zap.Int("encrypted_size", len(encryptedContent)),
		zap.Int("file_key_size", len(fileKey)),
		zap.Int("first_bytes_of_content", int(encryptedContent[0])))

	decryptedContent, err := e2ee.DecryptFile(encryptedContent, fileKey)
	if err != nil {
		a.logger.Error("Failed to decrypt file content",
			zap.Error(err),
			zap.Int("encrypted_size", len(encryptedContent)),
			zap.Int("file_key_size", len(fileKey)))
		return "", fmt.Errorf("failed to decrypt file: %w", err)
	}

	a.logger.Info("File content decrypted successfully",
		zap.Int("decrypted_size", len(decryptedContent)))

	a.logger.Info("Decrypted file", zap.Int("decrypted_size", len(decryptedContent)))

	// Step 9: Open save dialog for user to choose location
	savePath, err := runtime.SaveFileDialog(a.ctx, runtime.SaveDialogOptions{
		Title:           "Save File As",
		DefaultFilename: metadata.Filename,
	})
	if err != nil {
		a.logger.Error("Failed to open save dialog", zap.Error(err))
		return "", fmt.Errorf("failed to open save dialog: %w", err)
	}

	// User cancelled the dialog
	if savePath == "" {
		a.logger.Info("User cancelled save dialog")
		return "", nil
	}

	// Step 10: Write decrypted content to file (0600 = owner read/write only for security)
	if err := os.WriteFile(savePath, decryptedContent, 0600); err != nil {
		a.logger.Error("Failed to write file", zap.Error(err), zap.String("path", savePath))
		return "", fmt.Errorf("failed to save file: %w", err)
	}

	a.logger.Info("File downloaded and decrypted successfully",
		zap.String("file_id", fileID),
		zap.String("filename", metadata.Filename),
		zap.String("save_path", savePath),
		zap.Int("size", len(decryptedContent)))

	return savePath, nil
}

// OnloadFileResult represents the result of onloading a file for offline access
type OnloadFileResult struct {
	FileID        string `json:"file_id"`
	Filename      string `json:"filename"`
	LocalFilePath string `json:"local_file_path"`
	Size          int64  `json:"size"`
	Success       bool   `json:"success"`
	Message       string `json:"message"`
}

// OnloadFile downloads and stores a file locally for offline access (no save dialog)
func (a *Application) OnloadFile(fileID string) (*OnloadFileResult, error) {
	// Validate input
	if err := inputvalidation.ValidateFileID(fileID); err != nil {
		return nil, err
	}

	a.logger.Info("Onloading file for offline access", zap.String("file_id", fileID))

	// Get current session for authentication
	session, err := a.authService.GetCurrentSession(a.ctx)
	if err != nil {
		a.logger.Error("Failed to get current session", zap.Error(err))
		return nil, fmt.Errorf("not authenticated: %w", err)
	}

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

	apiClient := a.authService.GetAPIClient()

	// Step 1: Get file metadata
	fileReq, err := http.NewRequestWithContext(a.ctx, "GET", apiClient.GetBaseURL()+"/api/v1/file/"+fileID, nil)
	if err != nil {
		return nil, fmt.Errorf("failed to create request: %w", err)
	}
	fileReq.Header.Set("Authorization", "Bearer "+session.AccessToken)

	fileResp, err := a.httpClient.Do(fileReq)
	if err != nil {
		return nil, fmt.Errorf("failed to get file: %w", err)
	}
	defer fileResp.Body.Close()

	if fileResp.StatusCode != http.StatusOK {
		body, _ := io.ReadAll(fileResp.Body)
		return nil, fmt.Errorf("failed to get file: %s", string(body))
	}

	var fileData struct {
		ID                   string `json:"id"`
		CollectionID         string `json:"collection_id"`
		EncryptedMetadata    string `json:"encrypted_metadata"`
		FileNonce            string `json:"file_nonce"`
		EncryptedSizeInBytes int64  `json:"encrypted_file_size_in_bytes"`
		EncryptedFileKey     struct {
			Ciphertext string `json:"ciphertext"`
			Nonce      string `json:"nonce"`
		} `json:"encrypted_file_key"`
	}
	if err := json.NewDecoder(fileResp.Body).Decode(&fileData); err != nil {
		return nil, fmt.Errorf("failed to decode response: %w", err)
	}

	// Step 2: Get collection to decrypt collection key
	collReq, err := http.NewRequestWithContext(a.ctx, "GET", apiClient.GetBaseURL()+"/api/v1/collections/"+fileData.CollectionID, nil)
	if err != nil {
		return nil, fmt.Errorf("failed to create request: %w", err)
	}
	collReq.Header.Set("Authorization", "Bearer "+session.AccessToken)

	collResp, err := a.httpClient.Do(collReq)
	if err != nil {
		return nil, fmt.Errorf("failed to get collection: %w", err)
	}
	defer collResp.Body.Close()

	if collResp.StatusCode != http.StatusOK {
		body, _ := io.ReadAll(collResp.Body)
		return nil, fmt.Errorf("failed to get collection: %s", string(body))
	}

	var collData struct {
		EncryptedCollectionKey struct {
			Ciphertext string `json:"ciphertext"`
			Nonce      string `json:"nonce"`
		} `json:"encrypted_collection_key"`
	}
	if err := json.NewDecoder(collResp.Body).Decode(&collData); err != nil {
		return nil, fmt.Errorf("failed to decode collection: %w", err)
	}

	// Step 3: Decrypt collection key with master key
	// Use tryDecodeBase64 to handle multiple base64 encoding formats
	collKeyNonce, err := tryDecodeBase64(collData.EncryptedCollectionKey.Nonce)
	if err != nil {
		return nil, fmt.Errorf("failed to decode collection key nonce: %w", err)
	}
	collKeyCiphertext, err := tryDecodeBase64(collData.EncryptedCollectionKey.Ciphertext)
	if err != nil {
		return nil, 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 nil, 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 nil, fmt.Errorf("failed to decode file key nonce: %w", err)
	}
	fileKeyCiphertext, err := tryDecodeBase64(fileData.EncryptedFileKey.Ciphertext)
	if err != nil {
		return nil, 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 nil, fmt.Errorf("failed to decrypt file key: %w", err)
	}

	// Step 5: Decrypt metadata to get filename
	// Use tryDecodeBase64 to handle URL-safe base64 without padding (libsodium format)
	encryptedMetadataBytes, err := tryDecodeBase64(fileData.EncryptedMetadata)
	if err != nil {
		return nil, fmt.Errorf("failed to decode metadata: %w", err)
	}

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

	decryptedMetadata, err := e2ee.DecryptWithAlgorithm(metadataCiphertext, metadataNonce, fileKey)
	if err != nil {
		return nil, 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 nil, fmt.Errorf("failed to parse metadata: %w", err)
	}

	// Step 6: Get presigned download URL
	downloadURL, err := a.GetFileDownloadURL(fileID)
	if err != nil {
		return nil, fmt.Errorf("failed to get download URL: %w", err)
	}

	// Step 6.5: Validate download URL before use (SSRF protection)
	if err := inputvalidation.ValidateDownloadURL(downloadURL); err != nil {
		a.logger.Error("Download URL validation failed",
			zap.String("file_id", fileID),
			zap.Error(err))
		return nil, fmt.Errorf("download URL validation failed: %w", err)
	}

	// Step 7: Download encrypted file from S3 (use large download client - no timeout for big files)
	a.logger.Info("Downloading encrypted file from S3", zap.String("filename", metadata.Filename))
	downloadResp, err := a.httpClient.GetLargeDownload(downloadURL)
	if err != nil {
		return nil, fmt.Errorf("failed to download file: %w", err)
	}
	defer downloadResp.Body.Close()

	if downloadResp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("failed to download file from storage: status %d", downloadResp.StatusCode)
	}

	encryptedContent, err := io.ReadAll(downloadResp.Body)
	if err != nil {
		return nil, fmt.Errorf("failed to read file content: %w", err)
	}

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

	// Step 9: Create local storage directory structure
	dataDir, err := a.config.GetAppDataDirPath(a.ctx)
	if err != nil {
		return nil, fmt.Errorf("failed to get data directory: %w", err)
	}

	// Create files directory: <data_dir>/files/<collection_id>/
	filesDir := filepath.Join(dataDir, "files", fileData.CollectionID)
	if err := os.MkdirAll(filesDir, 0700); err != nil {
		return nil, fmt.Errorf("failed to create files directory: %w", err)
	}

	// Save decrypted file
	localFilePath := filepath.Join(filesDir, metadata.Filename)
	if err := os.WriteFile(localFilePath, decryptedContent, 0600); err != nil {
		return nil, fmt.Errorf("failed to save file: %w", err)
	}

	// Step 10: Update local file repository with sync status
	localFile := &file.File{
		ID:                   fileID,
		CollectionID:         fileData.CollectionID,
		Name:                 metadata.Filename,
		MimeType:             metadata.MimeType,
		FilePath:             localFilePath,
		DecryptedSizeInBytes: int64(len(decryptedContent)),
		EncryptedSizeInBytes: fileData.EncryptedSizeInBytes,
		SyncStatus:           file.SyncStatusSynced,
		EncryptedFileKey: file.EncryptedFileKeyData{
			Ciphertext: fileData.EncryptedFileKey.Ciphertext,
			Nonce:      fileData.EncryptedFileKey.Nonce,
		},
		EncryptedMetadata: fileData.EncryptedMetadata,
		FileNonce:         fileData.FileNonce,
	}

	// Check if file already exists in local repo
	existingFile, _ := a.mustGetFileRepo().Get(fileID)
	if existingFile != nil {
		if err := a.mustGetFileRepo().Update(localFile); err != nil {
			a.logger.Warn("Failed to update local file record", zap.Error(err))
		}
	} else {
		if err := a.mustGetFileRepo().Create(localFile); err != nil {
			a.logger.Warn("Failed to create local file record", zap.Error(err))
		}
	}

	a.logger.Info("File onloaded successfully",
		zap.String("file_id", fileID),
		zap.String("filename", metadata.Filename),
		zap.String("local_path", localFilePath),
		zap.Int("size", len(decryptedContent)))

	return &OnloadFileResult{
		FileID:        fileID,
		Filename:      metadata.Filename,
		LocalFilePath: localFilePath,
		Size:          int64(len(decryptedContent)),
		Success:       true,
		Message:       "File downloaded for offline access",
	}, nil
}

// OffloadFile removes the local copy of a file while keeping it in the cloud
func (a *Application) OffloadFile(fileID string) error {
	// Validate input
	if err := inputvalidation.ValidateFileID(fileID); err != nil {
		return err
	}

	a.logger.Info("Offloading file to cloud-only", zap.String("file_id", fileID))

	// Get the file from local repository
	localFile, err := a.mustGetFileRepo().Get(fileID)
	if err != nil {
		a.logger.Error("Failed to get file from local repo", zap.Error(err))
		return fmt.Errorf("file not found locally: %w", err)
	}

	if localFile == nil {
		return fmt.Errorf("file not found in local storage")
	}

	if !localFile.HasLocalContent() {
		a.logger.Info("File already cloud-only, nothing to offload")
		return nil
	}

	// Delete the local file from disk
	if localFile.FilePath != "" {
		if err := os.Remove(localFile.FilePath); err != nil && !os.IsNotExist(err) {
			a.logger.Warn("Failed to delete local file", zap.Error(err), zap.String("path", localFile.FilePath))
			// Continue anyway - we'll update the metadata
		} else {
			a.logger.Info("Deleted local file", zap.String("path", localFile.FilePath))
		}
	}

	// Delete encrypted file if it exists
	if localFile.EncryptedFilePath != "" {
		if err := os.Remove(localFile.EncryptedFilePath); err != nil && !os.IsNotExist(err) {
			a.logger.Warn("Failed to delete encrypted file", zap.Error(err), zap.String("path", localFile.EncryptedFilePath))
		}
	}

	// Delete thumbnail if it exists
	if localFile.ThumbnailPath != "" {
		if err := os.Remove(localFile.ThumbnailPath); err != nil && !os.IsNotExist(err) {
			a.logger.Warn("Failed to delete thumbnail", zap.Error(err), zap.String("path", localFile.ThumbnailPath))
		}
	}

	// Update the local file record to cloud-only status
	localFile.FilePath = ""
	localFile.EncryptedFilePath = ""
	localFile.ThumbnailPath = ""
	localFile.SyncStatus = file.SyncStatusCloudOnly

	if err := a.mustGetFileRepo().Update(localFile); err != nil {
		a.logger.Error("Failed to update file record", zap.Error(err))
		return fmt.Errorf("failed to update file record: %w", err)
	}

	a.logger.Info("File offloaded successfully",
		zap.String("file_id", fileID),
		zap.String("filename", localFile.Name))

	return nil
}

// OpenFile opens a locally stored file with the system's default application
func (a *Application) OpenFile(fileID string) error {
	// Validate input
	if err := inputvalidation.ValidateFileID(fileID); err != nil {
		return err
	}

	a.logger.Info("Opening file", zap.String("file_id", fileID))

	// Get the file from local repository
	localFile, err := a.mustGetFileRepo().Get(fileID)
	if err != nil {
		a.logger.Error("Failed to get file from local repo", zap.Error(err))
		return fmt.Errorf("file not found locally: %w", err)
	}

	if localFile == nil {
		return fmt.Errorf("file not found in local storage")
	}

	if localFile.FilePath == "" {
		return fmt.Errorf("file has not been downloaded for offline access")
	}

	// Security: Validate file path is within expected application data directory
	appDataDir, err := a.config.GetAppDataDirPath(a.ctx)
	if err != nil {
		a.logger.Error("Failed to get app data directory", zap.Error(err))
		return fmt.Errorf("failed to validate file path: %w", err)
	}

	if err := validatePathWithinDirectory(localFile.FilePath, appDataDir); err != nil {
		a.logger.Error("File path validation failed",
			zap.String("file_path", localFile.FilePath),
			zap.String("expected_dir", appDataDir),
			zap.Error(err))
		return fmt.Errorf("invalid file path: %w", err)
	}

	// Check if file exists on disk
	if _, err := os.Stat(localFile.FilePath); os.IsNotExist(err) {
		return fmt.Errorf("file no longer exists at %s", localFile.FilePath)
	}

	// Open the file with the system's default application
	var cmd *exec.Cmd
	switch sysRuntime.GOOS {
	case "darwin":
		cmd = exec.Command("open", localFile.FilePath)
	case "windows":
		cmd = exec.Command("cmd", "/c", "start", "", localFile.FilePath)
	case "linux":
		cmd = exec.Command("xdg-open", localFile.FilePath)
	default:
		return fmt.Errorf("unsupported operating system: %s", sysRuntime.GOOS)
	}

	if err := cmd.Start(); err != nil {
		a.logger.Error("Failed to open file", zap.Error(err), zap.String("path", localFile.FilePath))
		return fmt.Errorf("failed to open file: %w", err)
	}

	a.logger.Info("File opened successfully",
		zap.String("file_id", fileID),
		zap.String("path", localFile.FilePath))

	return nil
}

// validatePathWithinDirectory checks that a file path is within the expected directory.
// This is a defense-in-depth measure to prevent path traversal attacks.
func validatePathWithinDirectory(filePath, expectedDir string) error {
	// Get absolute paths to handle any relative path components
	absFilePath, err := filepath.Abs(filePath)
	if err != nil {
		return fmt.Errorf("failed to resolve file path: %w", err)
	}

	absExpectedDir, err := filepath.Abs(expectedDir)
	if err != nil {
		return fmt.Errorf("failed to resolve expected directory: %w", err)
	}

	// Clean paths to remove any . or .. components
	absFilePath = filepath.Clean(absFilePath)
	absExpectedDir = filepath.Clean(absExpectedDir)

	// Ensure the expected directory ends with a separator to prevent partial matches
	// e.g., /app/data should not match /app/data-other/file
	if !strings.HasSuffix(absExpectedDir, string(filepath.Separator)) {
		absExpectedDir = absExpectedDir + string(filepath.Separator)
	}

	// Check if the file path starts with the expected directory
	if !strings.HasPrefix(absFilePath, absExpectedDir) && absFilePath != strings.TrimSuffix(absExpectedDir, string(filepath.Separator)) {
		return fmt.Errorf("path is outside application data directory")
	}

	return nil
}
