package app

import (
	"bytes"
	"crypto/sha256"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"os"
	"path/filepath"
	"time"

	"github.com/google/uuid"
	"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"
)

// =============================================================================
// FILE UPLOAD OPERATIONS
// =============================================================================

// SelectFile opens a native file dialog and returns the selected file path
func (a *Application) SelectFile() (string, error) {
	selection, err := runtime.OpenFileDialog(a.ctx, runtime.OpenDialogOptions{
		Title: "Select File to Upload",
		Filters: []runtime.FileFilter{
			{DisplayName: "All Files", Pattern: "*.*"},
			{DisplayName: "Images", Pattern: "*.jpg;*.jpeg;*.png;*.gif;*.webp;*.bmp"},
			{DisplayName: "Documents", Pattern: "*.pdf;*.doc;*.docx;*.txt;*.md"},
			{DisplayName: "Videos", Pattern: "*.mp4;*.mov;*.avi;*.mkv;*.webm"},
		},
	})
	if err != nil {
		a.logger.Error("Failed to open file dialog", zap.Error(err))
		return "", fmt.Errorf("failed to open file dialog: %w", err)
	}
	return selection, nil
}

// FileUploadInput represents the input for uploading a file
type FileUploadInput struct {
	FilePath     string   `json:"file_path"`
	CollectionID string   `json:"collection_id"`
	TagIDs       []string `json:"tag_ids,omitempty"` // Tag IDs to assign to this file
}

// FileUploadResult represents the result of a file upload
type FileUploadResult struct {
	FileID   string `json:"file_id"`
	Filename string `json:"filename"`
	Size     int64  `json:"size"`
	Success  bool   `json:"success"`
	Message  string `json:"message"`
}

// UploadFile encrypts and uploads a file to a collection
func (a *Application) UploadFile(input FileUploadInput) (*FileUploadResult, error) {
	a.logger.Info("Starting file upload",
		zap.String("file_path", input.FilePath),
		zap.String("collection_id", input.CollectionID))

	// 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 key cache
	masterKey, cleanup, err := a.keyCache.GetMasterKey(session.Email)
	if err != nil {
		a.logger.Error("Failed to get master key from cache", zap.Error(err))
		return nil, fmt.Errorf("master key not available - please log in again: %w", err)
	}
	defer cleanup()

	apiClient := a.authService.GetAPIClient()

	// Step 1: Read the file from disk
	fileContent, err := os.ReadFile(input.FilePath)
	if err != nil {
		a.logger.Error("Failed to read file", zap.Error(err))
		return nil, fmt.Errorf("failed to read file: %w", err)
	}

	filename := filepath.Base(input.FilePath)
	fileSize := int64(len(fileContent))
	mimeType := http.DetectContentType(fileContent)

	a.logger.Info("File read successfully",
		zap.String("filename", filename),
		zap.Int64("size", fileSize),
		zap.String("mime_type", mimeType))

	// Step 2: Get collection key (need to fetch collection first)
	a.logger.Info("Step 2: Fetching collection for upload",
		zap.String("collection_id", input.CollectionID),
		zap.String("api_url", apiClient.GetBaseURL()+"/api/v1/collections/"+input.CollectionID))

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

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

	a.logger.Info("Step 2a: Collection fetch response", zap.Int("status", collectionResp.StatusCode))

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

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

	a.logger.Info("Step 2b: Collection data decoded",
		zap.Int("ciphertext_len", len(collectionData.EncryptedCollectionKey.Ciphertext)),
		zap.Int("nonce_len", len(collectionData.EncryptedCollectionKey.Nonce)))

	// Decrypt collection key
	collectionKeyCiphertext, err := base64.StdEncoding.DecodeString(collectionData.EncryptedCollectionKey.Ciphertext)
	if err != nil {
		a.logger.Error("Failed to decode collection key ciphertext", zap.Error(err))
		return nil, fmt.Errorf("failed to decode collection key ciphertext: %w", err)
	}
	collectionKeyNonce, err := base64.StdEncoding.DecodeString(collectionData.EncryptedCollectionKey.Nonce)
	if err != nil {
		a.logger.Error("Failed to decode collection key nonce", zap.Error(err))
		return nil, fmt.Errorf("failed to decode collection key nonce: %w", err)
	}

	// Handle web frontend combined ciphertext format (nonce + encrypted_data)
	actualCollectionKeyCiphertext := extractActualCiphertext(collectionKeyCiphertext, collectionKeyNonce)

	a.logger.Info("Step 2c: Decrypting collection key",
		zap.Int("ciphertext_bytes", len(actualCollectionKeyCiphertext)),
		zap.Int("nonce_bytes", len(collectionKeyNonce)),
		zap.Int("master_key_bytes", len(masterKey)))

	collectionKey, err := e2ee.DecryptCollectionKey(&e2ee.EncryptedKey{
		Ciphertext: actualCollectionKeyCiphertext,
		Nonce:      collectionKeyNonce,
	}, 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)
	}

	a.logger.Info("Collection key decrypted successfully", zap.Int("key_length", len(collectionKey)))

	// Step 3: Generate a new file key
	fileKey, err := e2ee.GenerateFileKey()
	if err != nil {
		return nil, fmt.Errorf("failed to generate file key: %w", err)
	}

	// Step 4: Encrypt file content using SecretBox (XSalsa20-Poly1305) for web frontend compatibility
	encryptedContent, err := e2ee.EncryptFileSecretBox(fileContent, fileKey)
	if err != nil {
		return nil, fmt.Errorf("failed to encrypt file: %w", err)
	}

	// Step 5: Encrypt metadata using SecretBox (XSalsa20-Poly1305) for web frontend compatibility
	metadata := &e2ee.FileMetadata{
		Name:     filename,
		MimeType: mimeType,
		Size:     fileSize,
	}
	encryptedMetadata, err := e2ee.EncryptMetadataSecretBox(metadata, fileKey)
	if err != nil {
		return nil, fmt.Errorf("failed to encrypt metadata: %w", err)
	}

	// Step 6: Encrypt file key with collection key using SecretBox for web frontend compatibility
	encryptedFileKey, err := e2ee.EncryptFileKeySecretBox(fileKey, collectionKey)
	if err != nil {
		return nil, fmt.Errorf("failed to encrypt file key: %w", err)
	}

	// Step 7: Compute encrypted hash
	hash := sha256.Sum256(encryptedContent)
	encryptedHash := base64.StdEncoding.EncodeToString(hash[:])

	// Step 8: Generate client-side file ID
	fileID := uuid.New().String()

	// Step 9: Create pending file request
	// NOTE: The web frontend sends ciphertext and nonce as SEPARATE fields (not combined).
	// The ciphertext field contains only the encrypted data (from crypto_secretbox_easy),
	// and the nonce field contains the nonce separately.
	pendingFileReq := map[string]interface{}{
		"id":            fileID,
		"collection_id": input.CollectionID,
		"encrypted_metadata": encryptedMetadata,
		"encrypted_file_key": map[string]string{
			"ciphertext": base64.StdEncoding.EncodeToString(encryptedFileKey.Ciphertext),
			"nonce":      base64.StdEncoding.EncodeToString(encryptedFileKey.Nonce),
		},
		"encryption_version":          "xsalsa20-poly1305-v1",
		"encrypted_hash":              encryptedHash,
		"expected_file_size_in_bytes": int64(len(encryptedContent)),
		"content_type":                mimeType,
	}

	// Add tag IDs if provided
	if len(input.TagIDs) > 0 {
		pendingFileReq["tag_ids"] = input.TagIDs
		a.logger.Info("Adding tags to file upload",
			zap.Int("tag_count", len(input.TagIDs)))
	}

	pendingBody, err := json.Marshal(pendingFileReq)
	if err != nil {
		return nil, fmt.Errorf("failed to marshal pending file request: %w", err)
	}

	req, err := http.NewRequestWithContext(a.ctx, "POST",
		apiClient.GetBaseURL()+"/api/v1/files/pending",
		bytes.NewReader(pendingBody))
	if err != nil {
		return nil, fmt.Errorf("failed to create pending file request: %w", err)
	}
	req.Header.Set("Authorization", "Bearer "+session.AccessToken)
	req.Header.Set("Content-Type", "application/json")

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

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

	var pendingResp struct {
		File struct {
			ID string `json:"id"`
		} `json:"file"`
		PresignedUploadURL      string `json:"presigned_upload_url"`
		UploadURLExpirationTime string `json:"upload_url_expiration_time"`
		Success                 bool   `json:"success"`
		Message                 string `json:"message"`
	}
	if err := json.NewDecoder(resp.Body).Decode(&pendingResp); err != nil {
		return nil, fmt.Errorf("failed to decode pending file response: %w", err)
	}

	if !pendingResp.Success {
		return nil, fmt.Errorf("failed to create pending file: %s", pendingResp.Message)
	}

	a.logger.Info("Pending file created, uploading to S3",
		zap.String("file_id", pendingResp.File.ID),
		zap.String("presigned_url", pendingResp.PresignedUploadURL[:50]+"..."))

	// Step 10: Upload encrypted content to S3
	uploadReq, err := http.NewRequestWithContext(a.ctx, "PUT",
		pendingResp.PresignedUploadURL,
		bytes.NewReader(encryptedContent))
	if err != nil {
		return nil, fmt.Errorf("failed to create upload request: %w", err)
	}
	uploadReq.Header.Set("Content-Type", "application/octet-stream")
	uploadReq.ContentLength = int64(len(encryptedContent))

	uploadResp, err := a.httpClient.DoLargeDownload(uploadReq)
	if err != nil {
		return nil, fmt.Errorf("failed to upload to S3: %w", err)
	}
	defer uploadResp.Body.Close()

	if uploadResp.StatusCode != http.StatusOK && uploadResp.StatusCode != http.StatusCreated {
		body, _ := io.ReadAll(uploadResp.Body)
		a.logger.Error("Failed to upload to S3",
			zap.Int("status", uploadResp.StatusCode),
			zap.String("body", string(body)))
		return nil, fmt.Errorf("failed to upload to S3: status %d", uploadResp.StatusCode)
	}

	a.logger.Info("File uploaded to S3, completing upload")

	// Step 11: Complete the upload
	completeReq := map[string]interface{}{
		"actual_file_size_in_bytes": int64(len(encryptedContent)),
		"upload_confirmed":          true,
	}

	completeBody, err := json.Marshal(completeReq)
	if err != nil {
		return nil, fmt.Errorf("failed to marshal complete request: %w", err)
	}

	completeHTTPReq, err := http.NewRequestWithContext(a.ctx, "POST",
		apiClient.GetBaseURL()+"/api/v1/file/"+pendingResp.File.ID+"/complete",
		bytes.NewReader(completeBody))
	if err != nil {
		return nil, fmt.Errorf("failed to create complete request: %w", err)
	}
	completeHTTPReq.Header.Set("Authorization", "Bearer "+session.AccessToken)
	completeHTTPReq.Header.Set("Content-Type", "application/json")

	completeResp, err := a.httpClient.Do(completeHTTPReq)
	if err != nil {
		return nil, fmt.Errorf("failed to complete upload: %w", err)
	}
	defer completeResp.Body.Close()

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

	var completeRespData struct {
		Success bool   `json:"success"`
		Message string `json:"message"`
	}
	if err := json.NewDecoder(completeResp.Body).Decode(&completeRespData); err != nil {
		return nil, fmt.Errorf("failed to decode complete response: %w", err)
	}

	// Save file metadata to local repository so it appears in dashboard and file list
	localFile := &file.File{
		ID:                   pendingResp.File.ID,
		CollectionID:         input.CollectionID,
		OwnerID:              session.UserID,
		Name:                 filename,
		MimeType:             mimeType,
		DecryptedSizeInBytes: fileSize,
		EncryptedSizeInBytes: int64(len(encryptedContent)),
		FilePath:             input.FilePath, // Original file path
		SyncStatus:           file.SyncStatusSynced,
		State:                file.StateActive,
		CreatedAt:            time.Now(),
		ModifiedAt:           time.Now(),
		LastSyncedAt:         time.Now(),
	}

	if err := a.mustGetFileRepo().Create(localFile); err != nil {
		// Log but don't fail - the upload succeeded, just local tracking failed
		a.logger.Warn("Failed to save file to local repository",
			zap.String("file_id", pendingResp.File.ID),
			zap.Error(err))
	} else {
		a.logger.Info("File saved to local repository",
			zap.String("file_id", pendingResp.File.ID),
			zap.String("filename", filename))

		// Index the file in the search index
		if err := a.indexFileForSearch(pendingResp.File.ID, input.CollectionID, filename, input.TagIDs, fileSize); err != nil {
			a.logger.Warn("Failed to index file in search",
				zap.String("file_id", pendingResp.File.ID),
				zap.Error(err))
		}
	}

	a.logger.Info("File upload completed successfully",
		zap.String("file_id", pendingResp.File.ID),
		zap.String("filename", filename),
		zap.Int64("size", fileSize))

	return &FileUploadResult{
		FileID:   pendingResp.File.ID,
		Filename: filename,
		Size:     fileSize,
		Success:  true,
		Message:  "File uploaded successfully",
	}, nil
}
