package app

import (
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"time"

	"go.uber.org/zap"

	"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/maplefile/client"
	"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 QUERY OPERATIONS
// =============================================================================

// EmbeddedTagData represents a tag attached to a file (for display purposes)
type EmbeddedTagData struct {
	ID    string `json:"id"`
	Name  string `json:"name"`
	Color string `json:"color"`
}

// FileDetailData represents detailed file information for the frontend
type FileDetailData struct {
	ID                       string `json:"id"`
	CollectionID             string `json:"collection_id"`
	Filename                 string `json:"filename"`
	MimeType                 string `json:"mime_type"`
	Size                     int64  `json:"size"`
	EncryptedFileSizeInBytes int64  `json:"encrypted_file_size_in_bytes"`
	CreatedAt                string `json:"created_at"`
	ModifiedAt               string `json:"modified_at"`
	Version                  uint64 `json:"version"`
	State                    string `json:"state"`
	// Sync status fields
	SyncStatus      string `json:"sync_status"`
	HasLocalContent bool   `json:"has_local_content"`
	LocalFilePath   string `json:"local_file_path,omitempty"`
	// Tags assigned to this file
	Tags []*EmbeddedTagData `json:"tags"`
}

// ListFilesByCollection lists all files in a collection
func (a *Application) ListFilesByCollection(collectionID string) ([]*FileData, error) {
	// Validate input
	if err := inputvalidation.ValidateCollectionID(collectionID); err != nil {
		return nil, err
	}

	apiClient := a.authService.GetAPIClient()

	files, err := apiClient.ListFilesByCollection(a.ctx, collectionID)
	if err != nil {
		a.logger.Error("Failed to list files",
			zap.String("collection_id", collectionID),
			zap.Error(err))
		return nil, fmt.Errorf("failed to list files: %w", err)
	}

	// Get collection key for decrypting file metadata (needed for cloud-only files)
	var collectionKey []byte
	collectionKeyReady := false

	// Lazy-load collection key only when needed
	getCollectionKey := func() ([]byte, error) {
		if collectionKeyReady {
			return collectionKey, nil
		}

		// Get session for master key
		session, err := a.authService.GetCurrentSession(a.ctx)
		if err != nil {
			return nil, fmt.Errorf("failed to get session: %w", err)
		}

		// Get master key from cache
		masterKey, cleanup, err := a.keyCache.GetMasterKey(session.Email)
		if err != nil {
			return nil, fmt.Errorf("failed to get master key: %w", err)
		}
		defer cleanup()

		// Define a custom response struct that matches the actual backend API
		// (the client SDK's Collection struct has EncryptedCollectionKey as string)
		type collectionAPIResponse struct {
			EncryptedCollectionKey struct {
				Ciphertext string `json:"ciphertext"`
				Nonce      string `json:"nonce"`
			} `json:"encrypted_collection_key"`
		}

		// Make direct HTTP request to get collection
		req, err := http.NewRequestWithContext(a.ctx, "GET", apiClient.GetBaseURL()+"/api/v1/collections/"+collectionID, nil)
		if err != nil {
			return nil, fmt.Errorf("failed to create request: %w", err)
		}
		accessToken, _ := apiClient.GetTokens()
		req.Header.Set("Authorization", "Bearer "+accessToken)

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

		if resp.StatusCode != http.StatusOK {
			return nil, fmt.Errorf("failed to fetch collection: status %d", resp.StatusCode)
		}

		var collection collectionAPIResponse
		if err := json.NewDecoder(resp.Body).Decode(&collection); err != nil {
			return nil, fmt.Errorf("failed to decode collection response: %w", err)
		}

		// Decode collection key components
		// Use tryDecodeBase64 to handle multiple base64 encoding formats
		keyCiphertext, err := tryDecodeBase64(collection.EncryptedCollectionKey.Ciphertext)
		if err != nil {
			return nil, fmt.Errorf("failed to decode collection key ciphertext: %w", err)
		}
		keyNonce, err := tryDecodeBase64(collection.EncryptedCollectionKey.Nonce)
		if err != nil {
			return nil, fmt.Errorf("failed to decode collection key nonce: %w", err)
		}

		// Handle web frontend combined ciphertext format (nonce + encrypted_data)
		actualKeyCiphertext := extractActualCiphertext(keyCiphertext, keyNonce)

		// Decrypt collection key with master key
		collectionKey, err = e2ee.DecryptCollectionKey(&e2ee.EncryptedKey{
			Ciphertext: actualKeyCiphertext,
			Nonce:      keyNonce,
		}, masterKey)
		if err != nil {
			return nil, fmt.Errorf("failed to decrypt collection key: %w", err)
		}

		collectionKeyReady = true
		return collectionKey, nil
	}

	result := make([]*FileData, 0, len(files))
	for _, cloudFile := range files {
		// Skip deleted files - don't show them in the GUI
		if cloudFile.State == file.StateDeleted {
			continue
		}

		// Default values
		filename := "Encrypted File"
		contentType := "application/octet-stream"
		fileSize := cloudFile.EncryptedSizeInBytes

		// Check local repository for sync status
		syncStatus := file.SyncStatusCloudOnly // Default: cloud only (from API)
		hasLocalContent := false
		localFilePath := ""

		localFile, err := a.mustGetFileRepo().Get(cloudFile.ID)
		if err == nil && localFile != nil {
			// Skip if local file is marked as deleted
			if localFile.State == file.StateDeleted {
				continue
			}

			// File exists in local repo - use local data
			syncStatus = localFile.SyncStatus
			hasLocalContent = localFile.HasLocalContent()
			localFilePath = localFile.FilePath

			// Use decrypted data from local storage
			if localFile.Name != "" {
				filename = localFile.Name
			}
			if localFile.MimeType != "" {
				contentType = localFile.MimeType
			}
			if localFile.DecryptedSizeInBytes > 0 {
				fileSize = localFile.DecryptedSizeInBytes
			}
		} else {
			// File not in local repo - decrypt metadata from cloud
			colKey, err := getCollectionKey()
			if err != nil {
				a.logger.Warn("Failed to get collection key for metadata decryption",
					zap.String("file_id", cloudFile.ID),
					zap.Error(err))
				// Continue with placeholder values
			} else {
				// Decrypt file 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
				fileKeyCiphertext, err := tryDecodeBase64(cloudFile.EncryptedFileKey.Ciphertext)
				if err != nil {
					a.logger.Warn("Failed to decode file key ciphertext",
						zap.String("file_id", cloudFile.ID),
						zap.Error(err))
				} else {
					fileKeyNonce, err := tryDecodeBase64(cloudFile.EncryptedFileKey.Nonce)
					if err != nil {
						a.logger.Warn("Failed to decode file key nonce",
							zap.String("file_id", cloudFile.ID),
							zap.Error(err))
					} else {
						// Handle web frontend combined ciphertext format (nonce + encrypted_data)
						actualFileKeyCiphertext := extractActualCiphertext(fileKeyCiphertext, fileKeyNonce)

						fileKey, err := e2ee.DecryptFileKey(&e2ee.EncryptedKey{
							Ciphertext: actualFileKeyCiphertext,
							Nonce:      fileKeyNonce,
						}, colKey)
						if err != nil {
							a.logger.Warn("Failed to decrypt file key",
								zap.String("file_id", cloudFile.ID),
								zap.Error(err))
						} else {
							// Decrypt metadata
							// Use tryDecodeBase64 to handle multiple base64 encoding formats
							encryptedMetadataBytes, err := tryDecodeBase64(cloudFile.EncryptedMetadata)
							if err != nil {
								a.logger.Warn("Failed to decode encrypted metadata",
									zap.String("file_id", cloudFile.ID),
									zap.Error(err))
							} else {
								metadataNonce, metadataCiphertext, err := e2ee.SplitNonceAndCiphertextAuto(encryptedMetadataBytes)
								if err != nil {
									a.logger.Warn("Failed to split metadata nonce/ciphertext",
										zap.String("file_id", cloudFile.ID),
										zap.Error(err))
								} else {
									decryptedMetadata, err := e2ee.DecryptWithAlgorithm(metadataCiphertext, metadataNonce, fileKey)
									if err != nil {
										a.logger.Warn("Failed to decrypt metadata",
											zap.String("file_id", cloudFile.ID),
											zap.Error(err))
									} else {
										// Parse decrypted metadata JSON
										var metadata struct {
											Filename string `json:"name"`
											MimeType string `json:"mime_type"`
											Size     int64  `json:"size"`
										}
										if err := json.Unmarshal(decryptedMetadata, &metadata); err != nil {
											a.logger.Warn("Failed to parse metadata JSON",
												zap.String("file_id", cloudFile.ID),
												zap.Error(err))
										} else {
											// Successfully decrypted - use actual values
											if metadata.Filename != "" {
												filename = metadata.Filename
											}
											if metadata.MimeType != "" {
												contentType = metadata.MimeType
											}
											if metadata.Size > 0 {
												fileSize = metadata.Size
											}
										}
									}
								}
							}
						}
					}
				}
			}
		}

		// Process embedded tags from the API response
		// The backend includes tags in the list response, so we decrypt them here
		// instead of making separate API calls per file
		embeddedTags := make([]*EmbeddedTagData, 0, len(cloudFile.Tags))
		if len(cloudFile.Tags) > 0 {
			// Get master key for tag decryption (we need it for each file with tags)
			// Note: This is inside the file loop, so we get a fresh key reference for each file
			session, err := a.authService.GetCurrentSession(a.ctx)
			if err == nil {
				masterKey, cleanup, err := a.keyCache.GetMasterKey(session.Email)
				if err == nil {
					// Decrypt each embedded tag
					for _, tagData := range cloudFile.Tags {
						// Convert to client.Tag format for decryption
						clientTag := &client.Tag{
							ID:             tagData.ID,
							EncryptedName:  tagData.EncryptedName,
							EncryptedColor: tagData.EncryptedColor,
							EncryptedTagKey: tagData.EncryptedTagKey,
						}

						// Decrypt the tag
						decryptedTag, err := a.decryptTag(clientTag, masterKey)
						if err != nil {
							a.logger.Warn("Failed to decrypt embedded tag for file, skipping",
								zap.String("file_id", cloudFile.ID),
								zap.String("tag_id", tagData.ID),
								zap.Error(err))
							continue
						}

						embeddedTags = append(embeddedTags, &EmbeddedTagData{
							ID:    decryptedTag.ID,
							Name:  decryptedTag.Name,
							Color: decryptedTag.Color,
						})
					}
					cleanup()
				} else {
					a.logger.Debug("Failed to get master key for tag decryption, skipping tags",
						zap.String("file_id", cloudFile.ID),
						zap.Error(err))
				}
			} else {
				a.logger.Debug("Failed to get session for tag decryption, skipping tags",
					zap.String("file_id", cloudFile.ID),
					zap.Error(err))
			}
		}

		result = append(result, &FileData{
			ID:              cloudFile.ID,
			CollectionID:    cloudFile.CollectionID,
			Filename:        filename,
			Size:            fileSize,
			ContentType:     contentType,
			CreatedAt:       cloudFile.CreatedAt.Format(time.RFC3339),
			ModifiedAt:      cloudFile.ModifiedAt.Format(time.RFC3339),
			SyncStatus:      syncStatus.String(),
			HasLocalContent: hasLocalContent,
			LocalFilePath:   localFilePath,
			Tags:            embeddedTags,
		})
	}

	a.logger.Info("Listed files",
		zap.String("collection_id", collectionID),
		zap.Int("count", len(result)))
	return result, nil
}

// GetFile retrieves a single file's details by ID
func (a *Application) GetFile(fileID string) (*FileDetailData, error) {
	// Validate input
	if err := inputvalidation.ValidateFileID(fileID); err != nil {
		return nil, err
	}

	// Get current session
	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 the cached master key for decryption
	masterKey, cleanup, err := a.keyCache.GetMasterKey(session.Email)
	if err != nil {
		a.logger.Error("Failed to get master key", zap.Error(err))
		return nil, fmt.Errorf("failed to get master key: %w", err)
	}
	defer cleanup()

	apiClient := a.authService.GetAPIClient()
	apiClient.SetTokens(session.AccessToken, session.RefreshToken)

	// Make HTTP request to get file details
	// Note: Backend uses /api/v1/file/{id} (singular) not /api/v1/files/{id}
	req, 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 nil, 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 get file request", zap.Error(err))
		return nil, 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 file",
			zap.Int("status", resp.StatusCode),
			zap.String("body", string(body)))
		return nil, fmt.Errorf("failed to get file: %s", string(body))
	}

	// Parse response
	var fileResp 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"`
		EncryptedFileSizeInBytes int64  `json:"encrypted_file_size_in_bytes"`
		CreatedAt                string `json:"created_at"`
		ModifiedAt               string `json:"modified_at"`
		Version                  uint64 `json:"version"`
		State                    string `json:"state"`
		Tags                     []struct {
			ID             string `json:"id"`
			EncryptedName  string `json:"encrypted_name"`
			EncryptedColor string `json:"encrypted_color"`
			EncryptedTagKey struct {
				Ciphertext string `json:"ciphertext"`
				Nonce      string `json:"nonce"`
			} `json:"encrypted_tag_key"`
		} `json:"tags,omitempty"`
	}

	if err := json.NewDecoder(resp.Body).Decode(&fileResp); err != nil {
		a.logger.Error("Failed to decode file response", zap.Error(err))
		return nil, fmt.Errorf("failed to decode response: %w", err)
	}

	// Now we need to get the collection to decrypt the file key
	// First get the collection's encrypted collection key
	collReq, err := http.NewRequestWithContext(a.ctx, "GET", apiClient.GetBaseURL()+"/api/v1/collections/"+fileResp.CollectionID, nil)
	if err != nil {
		a.logger.Error("Failed to create get collection request", zap.Error(err))
		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 {
		a.logger.Error("Failed to get collection for file", zap.Error(err))
		return nil, 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 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 {
		a.logger.Error("Failed to decode collection response", zap.Error(err))
		return nil, fmt.Errorf("failed to decode collection: %w", err)
	}

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

	// 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(fileResp.EncryptedFileKey.Nonce)
	if err != nil {
		a.logger.Error("Failed to decode file key nonce", zap.Error(err))
		return nil, fmt.Errorf("failed to decode file key nonce: %w", err)
	}
	fileKeyCiphertext, err := tryDecodeBase64(fileResp.EncryptedFileKey.Ciphertext)
	if err != nil {
		a.logger.Error("Failed to decode file key ciphertext", zap.Error(err))
		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 {
		a.logger.Error("Failed to decrypt file key", zap.Error(err))
		return nil, fmt.Errorf("failed to decrypt file key: %w", err)
	}

	// Decrypt file metadata with file key
	// Use tryDecodeBase64 to handle multiple base64 encoding formats
	encryptedMetadataBytes, err := tryDecodeBase64(fileResp.EncryptedMetadata)
	if err != nil {
		a.logger.Error("Failed to decode encrypted metadata", zap.Error(err))
		return nil, fmt.Errorf("failed to decode metadata: %w", err)
	}

	// Split nonce and ciphertext (auto-detect nonce size: 12 for ChaCha20, 24 for XSalsa20)
	metadataNonce, metadataCiphertext, err := e2ee.SplitNonceAndCiphertextAuto(encryptedMetadataBytes)
	if err != nil {
		a.logger.Error("Failed to split metadata nonce/ciphertext", zap.Error(err))
		return nil, fmt.Errorf("failed to parse metadata: %w", err)
	}

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

	// Parse decrypted metadata JSON
	var metadata struct {
		Filename string `json:"name"`
		MimeType string `json:"mime_type"`
		Size     int64  `json:"size"`
	}
	if err := json.Unmarshal(decryptedMetadata, &metadata); err != nil {
		a.logger.Error("Failed to parse file metadata", zap.Error(err))
		return nil, fmt.Errorf("failed to parse metadata: %w", err)
	}

	// Check local repository for sync status
	syncStatus := file.SyncStatusCloudOnly // Default: cloud only
	hasLocalContent := false
	localFilePath := ""

	localFile, err := a.mustGetFileRepo().Get(fileResp.ID)
	if err == nil && localFile != nil {
		syncStatus = localFile.SyncStatus
		hasLocalContent = localFile.HasLocalContent()
		localFilePath = localFile.FilePath
	}

	// Process embedded tags from the API response
	embeddedTags := make([]*EmbeddedTagData, 0, len(fileResp.Tags))
	for _, tagData := range fileResp.Tags {
		// Convert the embedded tag structure to client.Tag format for decryption
		clientTag := &client.Tag{
			ID:             tagData.ID,
			EncryptedName:  tagData.EncryptedName,
			EncryptedColor: tagData.EncryptedColor,
			EncryptedTagKey: &client.EncryptedTagKey{
				Ciphertext: tagData.EncryptedTagKey.Ciphertext,
				Nonce:      tagData.EncryptedTagKey.Nonce,
			},
		}

		// Decrypt the tag using the existing decryptTag helper
		decryptedTag, err := a.decryptTag(clientTag, masterKey)
		if err != nil {
			a.logger.Warn("Failed to decrypt embedded tag, skipping",
				zap.String("file_id", fileResp.ID),
				zap.String("tag_id", tagData.ID),
				zap.Error(err))
			continue
		}

		embeddedTags = append(embeddedTags, &EmbeddedTagData{
			ID:    decryptedTag.ID,
			Name:  decryptedTag.Name,
			Color: decryptedTag.Color,
		})

		a.logger.Debug("Decrypted embedded tag for file",
			zap.String("file_id", fileResp.ID),
			zap.String("tag_id", decryptedTag.ID),
			zap.String("name", decryptedTag.Name),
			zap.String("color", decryptedTag.Color))
	}

	return &FileDetailData{
		ID:                       fileResp.ID,
		CollectionID:             fileResp.CollectionID,
		Filename:                 metadata.Filename,
		MimeType:                 metadata.MimeType,
		Size:                     metadata.Size,
		EncryptedFileSizeInBytes: fileResp.EncryptedFileSizeInBytes,
		CreatedAt:                fileResp.CreatedAt,
		ModifiedAt:               fileResp.ModifiedAt,
		Version:                  fileResp.Version,
		State:                    fileResp.State,
		SyncStatus:               syncStatus.String(),
		HasLocalContent:          hasLocalContent,
		LocalFilePath:            localFilePath,
		Tags:                     embeddedTags,
	}, nil
}
