package app

import (
	"encoding/json"
	"fmt"
	"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"
)

// DashboardData contains the formatted dashboard data for the frontend
type DashboardData struct {
	Summary           DashboardSummary        `json:"summary"`
	StorageUsageTrend StorageUsageTrend       `json:"storage_usage_trend"`
	RecentFiles       []DashboardRecentFile   `json:"recent_files"`
}

// DashboardSummary contains summary statistics
type DashboardSummary struct {
	TotalFiles             int    `json:"total_files"`
	TotalFolders           int    `json:"total_folders"`
	StorageUsed            string `json:"storage_used"`
	StorageLimit           string `json:"storage_limit"`
	StorageUsagePercentage int    `json:"storage_usage_percentage"`
}

// StorageUsageTrend contains storage usage trend data
type StorageUsageTrend struct {
	Period     string                  `json:"period"`
	DataPoints []StorageTrendDataPoint `json:"data_points"`
}

// StorageTrendDataPoint represents a single data point in the storage trend
type StorageTrendDataPoint struct {
	Date  string `json:"date"`
	Usage string `json:"usage"`
}

// DashboardRecentFile represents a recent file for dashboard display
type DashboardRecentFile struct {
	ID              string `json:"id"`
	CollectionID    string `json:"collection_id"`
	Name            string `json:"name"`
	Size            string `json:"size"`
	SizeInBytes     int64  `json:"size_in_bytes"`
	MimeType        string `json:"mime_type"`
	CreatedAt       string `json:"created_at"`
	IsDecrypted     bool   `json:"is_decrypted"`
	SyncStatus      string `json:"sync_status"`
	HasLocalContent bool   `json:"has_local_content"`
}

// GetDashboardData fetches and formats dashboard data from the backend
func (a *Application) GetDashboardData() (*DashboardData, error) {
	// Get API client from auth service
	apiClient := a.authService.GetAPIClient()
	if apiClient == nil {
		return nil, fmt.Errorf("API client not available")
	}

	// Ensure we have a valid session with tokens
	session, err := a.authService.GetCurrentSession(a.ctx)
	if err != nil || session == nil {
		return nil, fmt.Errorf("no active session - please log in")
	}

	if !session.IsValid() {
		return nil, fmt.Errorf("session expired - please log in again")
	}

	// Ensure tokens are set in the API client
	// This is important after app restarts or hot reloads
	apiClient.SetTokens(session.AccessToken, session.RefreshToken)
	a.logger.Debug("Restored tokens to API client for dashboard request",
		zap.String("user_id", session.UserID),
		zap.Time("token_expires_at", session.ExpiresAt))

	// Check if access token is about to expire or already expired
	timeUntilExpiry := time.Until(session.ExpiresAt)
	now := time.Now()
	sessionAge := now.Sub(session.CreatedAt)

	a.logger.Debug("Token status check",
		zap.Time("now", now),
		zap.Time("expires_at", session.ExpiresAt),
		zap.Duration("time_until_expiry", timeUntilExpiry),
		zap.Duration("session_age", sessionAge))

	if timeUntilExpiry < 0 {
		a.logger.Warn("Access token already expired, refresh should happen automatically",
			zap.Duration("expired_since", -timeUntilExpiry))
	} else if timeUntilExpiry < 2*time.Minute {
		a.logger.Info("Access token expiring soon, refresh may be needed",
			zap.Duration("time_until_expiry", timeUntilExpiry))
	}

	// If session is very old (more than 1 day), recommend fresh login
	if sessionAge > 24*time.Hour {
		a.logger.Warn("Session is very old, consider logging out and logging in again",
			zap.Duration("session_age", sessionAge))
	}

	// Fetch dashboard data from backend
	// The client will automatically refresh the token if it gets a 401
	a.logger.Debug("Calling backend API for dashboard data")
	resp, err := apiClient.GetDashboard(a.ctx)
	if err != nil {
		a.logger.Error("Failed to fetch dashboard data",
			zap.Error(err),
			zap.String("error_type", fmt.Sprintf("%T", err)))

		// Check if this is an unauthorized error that should trigger token refresh
		if apiErr, ok := err.(*client.APIError); ok {
			a.logger.Error("API Error details",
				zap.Int("status", apiErr.Status),
				zap.String("title", apiErr.Title),
				zap.String("detail", apiErr.Detail))
		}

		return nil, fmt.Errorf("failed to fetch dashboard: %w", err)
	}

	if resp.Dashboard == nil {
		return nil, fmt.Errorf("dashboard data is empty")
	}

	dashboard := resp.Dashboard

	// Format summary data
	summary := DashboardSummary{
		TotalFiles:             dashboard.Summary.TotalFiles,
		TotalFolders:           dashboard.Summary.TotalFolders,
		StorageUsed:            formatStorageAmount(dashboard.Summary.StorageUsed),
		StorageLimit:           formatStorageAmount(dashboard.Summary.StorageLimit),
		StorageUsagePercentage: dashboard.Summary.StorageUsagePercentage,
	}

	// Format storage usage trend
	dataPoints := make([]StorageTrendDataPoint, len(dashboard.StorageUsageTrend.DataPoints))
	for i, dp := range dashboard.StorageUsageTrend.DataPoints {
		dataPoints[i] = StorageTrendDataPoint{
			Date:  dp.Date,
			Usage: formatStorageAmount(dp.Usage),
		}
	}

	trend := StorageUsageTrend{
		Period:     dashboard.StorageUsageTrend.Period,
		DataPoints: dataPoints,
	}

	// Get master key for decryption (needed for cloud-only files)
	masterKey, cleanup, masterKeyErr := a.keyCache.GetMasterKey(session.Email)
	if masterKeyErr != nil {
		a.logger.Warn("Master key not available for dashboard file decryption",
			zap.Error(masterKeyErr))
	} else {
		defer cleanup()
	}

	// Build a cache of collection keys for efficient decryption
	// First, pre-populate from the dashboard response's collection_keys (if available)
	// This avoids making additional API calls for each collection
	collectionKeyCache := make(map[string][]byte) // collectionID -> decrypted collection key

	if masterKeyErr == nil && len(dashboard.CollectionKeys) > 0 {
		a.logger.Debug("Pre-populating collection key cache from dashboard response",
			zap.Int("collection_keys_count", len(dashboard.CollectionKeys)))

		for _, ck := range dashboard.CollectionKeys {
			// Decode the encrypted collection key
			collKeyCiphertext, decodeErr := tryDecodeBase64(ck.EncryptedCollectionKey)
			if decodeErr != nil {
				a.logger.Warn("Failed to decode collection key ciphertext from dashboard",
					zap.String("collection_id", ck.CollectionID),
					zap.Error(decodeErr))
				continue
			}

			collKeyNonce, decodeErr := tryDecodeBase64(ck.EncryptedCollectionKeyNonce)
			if decodeErr != nil {
				a.logger.Warn("Failed to decode collection key nonce from dashboard",
					zap.String("collection_id", ck.CollectionID),
					zap.Error(decodeErr))
				continue
			}

			// Handle combined ciphertext format (nonce prepended to ciphertext)
			actualCollKeyCiphertext := extractActualCiphertext(collKeyCiphertext, collKeyNonce)

			// Decrypt the collection key with the master key
			collectionKey, decryptErr := e2ee.DecryptCollectionKey(&e2ee.EncryptedKey{
				Ciphertext: actualCollKeyCiphertext,
				Nonce:      collKeyNonce,
			}, masterKey)
			if decryptErr != nil {
				a.logger.Warn("Failed to decrypt collection key from dashboard",
					zap.String("collection_id", ck.CollectionID),
					zap.Error(decryptErr))
				continue
			}

			// Cache the decrypted collection key
			collectionKeyCache[ck.CollectionID] = collectionKey
			a.logger.Debug("Cached collection key from dashboard response",
				zap.String("collection_id", ck.CollectionID))
		}

		a.logger.Info("Pre-populated collection key cache from dashboard",
			zap.Int("cached_keys", len(collectionKeyCache)))
	}

	// Format recent files (use local data if available, otherwise decrypt from cloud)
	recentFiles := make([]DashboardRecentFile, 0, len(dashboard.RecentFiles))
	for _, cloudFile := range dashboard.RecentFiles {
		// Debug: Log what we received from the API
		a.logger.Debug("Processing dashboard recent file",
			zap.String("file_id", cloudFile.ID),
			zap.String("collection_id", cloudFile.CollectionID),
			zap.Int("encrypted_file_key_ciphertext_len", len(cloudFile.EncryptedFileKey.Ciphertext)),
			zap.Int("encrypted_file_key_nonce_len", len(cloudFile.EncryptedFileKey.Nonce)),
			zap.String("encrypted_file_key_ciphertext_preview", truncateForLog(cloudFile.EncryptedFileKey.Ciphertext, 50)),
			zap.Int("encrypted_metadata_len", len(cloudFile.EncryptedMetadata)))

		// Default values for files not in local repository
		filename := "Encrypted File"
		isDecrypted := false
		syncStatus := file.SyncStatusCloudOnly // Default: cloud only
		hasLocalContent := false
		sizeInBytes := cloudFile.EncryptedFileSizeInBytes
		mimeType := "application/octet-stream"

		// Check local repository for this file to get decrypted name and sync status
		localFile, err := a.mustGetFileRepo().Get(cloudFile.ID)
		if err == nil && localFile != nil && localFile.State != file.StateDeleted {
			// File exists locally - use local data
			syncStatus = localFile.SyncStatus
			hasLocalContent = localFile.HasLocalContent()

			// Use decrypted filename if available
			if localFile.Name != "" {
				filename = localFile.Name
				isDecrypted = true
			}

			// Use decrypted mime type if available
			if localFile.MimeType != "" {
				mimeType = localFile.MimeType
			}

			// Use local size (decrypted) if available
			if localFile.DecryptedSizeInBytes > 0 {
				sizeInBytes = localFile.DecryptedSizeInBytes
			}
		} else if masterKeyErr == nil && cloudFile.EncryptedMetadata != "" {
			// File not in local repo, but we have the master key - try to decrypt from cloud data
			decryptedFilename, decryptedMimeType, decryptErr := a.decryptDashboardFileMetadata(
				cloudFile, masterKey, collectionKeyCache, apiClient)
			if decryptErr != nil {
				// Log at Warn level for better visibility during troubleshooting
				a.logger.Warn("Failed to decrypt dashboard file metadata",
					zap.String("file_id", cloudFile.ID),
					zap.String("collection_id", cloudFile.CollectionID),
					zap.Int("encrypted_file_key_ciphertext_len", len(cloudFile.EncryptedFileKey.Ciphertext)),
					zap.Int("encrypted_file_key_nonce_len", len(cloudFile.EncryptedFileKey.Nonce)),
					zap.Error(decryptErr))
			} else {
				filename = decryptedFilename
				mimeType = decryptedMimeType
				isDecrypted = true
			}
		}

		recentFiles = append(recentFiles, DashboardRecentFile{
			ID:              cloudFile.ID,
			CollectionID:    cloudFile.CollectionID,
			Name:            filename,
			Size:            formatFileSize(sizeInBytes),
			SizeInBytes:     sizeInBytes,
			MimeType:        mimeType,
			CreatedAt:       cloudFile.CreatedAt.Format(time.RFC3339),
			IsDecrypted:     isDecrypted,
			SyncStatus:      syncStatus.String(),
			HasLocalContent: hasLocalContent,
		})
	}

	dashboardData := &DashboardData{
		Summary:           summary,
		StorageUsageTrend: trend,
		RecentFiles:       recentFiles,
	}

	a.logger.Info("Dashboard data fetched successfully",
		zap.Int("total_files", summary.TotalFiles),
		zap.Int("recent_files", len(recentFiles)))

	return dashboardData, nil
}

// formatStorageAmount converts StorageAmount to human-readable string
func formatStorageAmount(amount client.StorageAmount) string {
	if amount.Value == 0 {
		return "0 B"
	}
	return fmt.Sprintf("%.2f %s", amount.Value, amount.Unit)
}

// formatFileSize converts bytes to human-readable format
func formatFileSize(bytes int64) string {
	if bytes == 0 {
		return "0 B"
	}

	const unit = 1024
	if bytes < unit {
		return fmt.Sprintf("%d B", bytes)
	}

	div, exp := int64(unit), 0
	for n := bytes / unit; n >= unit; n /= unit {
		div *= unit
		exp++
	}

	units := []string{"B", "KB", "MB", "GB", "TB"}
	return fmt.Sprintf("%.1f %s", float64(bytes)/float64(div), units[exp+1])
}

// decryptDashboardFileMetadata decrypts file metadata for a dashboard recent file
// Collection keys should already be pre-populated in the cache from the dashboard API response
func (a *Application) decryptDashboardFileMetadata(
	cloudFile client.RecentFileDashboard,
	masterKey []byte,
	collectionKeyCache map[string][]byte,
	apiClient *client.Client,
) (filename string, mimeType string, err error) {

	// Step 1: Get the collection key from cache (should be pre-populated from dashboard API response)
	collectionKey, exists := collectionKeyCache[cloudFile.CollectionID]
	if !exists {
		// Collection key was not provided by the dashboard API - this shouldn't happen
		// but we log a warning for debugging
		a.logger.Warn("Collection key not found in cache - dashboard API should have provided it",
			zap.String("collection_id", cloudFile.CollectionID),
			zap.String("file_id", cloudFile.ID))
		return "", "", fmt.Errorf("collection key not available for collection %s", cloudFile.CollectionID)
	}

	// Step 2: Get the file's encrypted_file_key
	// First try using the dashboard data, but if empty, fetch from the file endpoint directly
	var fileKeyCiphertext, fileKeyNonce []byte

	if cloudFile.EncryptedFileKey.Ciphertext != "" && cloudFile.EncryptedFileKey.Nonce != "" {
		// Use data from dashboard response
		var decodeErr error
		fileKeyCiphertext, decodeErr = tryDecodeBase64(cloudFile.EncryptedFileKey.Ciphertext)
		if decodeErr != nil {
			return "", "", fmt.Errorf("failed to decode file key ciphertext: %w", decodeErr)
		}

		fileKeyNonce, decodeErr = tryDecodeBase64(cloudFile.EncryptedFileKey.Nonce)
		if decodeErr != nil {
			return "", "", fmt.Errorf("failed to decode file key nonce: %w", decodeErr)
		}
	} else {
		// Dashboard response has empty encrypted_file_key, fetch from file endpoint
		// This endpoint properly deserializes the encrypted_file_key through the repository
		a.logger.Debug("Dashboard encrypted_file_key is empty, fetching from file endpoint",
			zap.String("file_id", cloudFile.ID))

		file, fetchErr := apiClient.GetFile(a.ctx, cloudFile.ID)
		if fetchErr != nil {
			return "", "", fmt.Errorf("failed to fetch file %s: %w", cloudFile.ID, fetchErr)
		}

		if file.EncryptedFileKey.Ciphertext == "" || file.EncryptedFileKey.Nonce == "" {
			return "", "", fmt.Errorf("file endpoint also returned empty encrypted_file_key for file %s", cloudFile.ID)
		}

		var decodeErr error
		fileKeyCiphertext, decodeErr = tryDecodeBase64(file.EncryptedFileKey.Ciphertext)
		if decodeErr != nil {
			return "", "", fmt.Errorf("failed to decode file key ciphertext from file endpoint: %w", decodeErr)
		}

		fileKeyNonce, decodeErr = tryDecodeBase64(file.EncryptedFileKey.Nonce)
		if decodeErr != nil {
			return "", "", fmt.Errorf("failed to decode file key nonce from file endpoint: %w", decodeErr)
		}
	}

	// Handle combined ciphertext format for file key
	actualFileKeyCiphertext := extractActualCiphertext(fileKeyCiphertext, fileKeyNonce)

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

	// Step 3: Decrypt the file metadata with the file key
	// Use tryDecodeBase64 to handle multiple base64 encoding formats
	encryptedMetadataBytes, err := tryDecodeBase64(cloudFile.EncryptedMetadata)
	if err != nil {
		return "", "", fmt.Errorf("failed to decode encrypted metadata: %w", err)
	}

	// Split nonce and ciphertext from the combined metadata (auto-detect nonce size)
	metadataNonce, metadataCiphertext, err := e2ee.SplitNonceAndCiphertextAuto(encryptedMetadataBytes)
	if err != nil {
		return "", "", fmt.Errorf("failed to split metadata nonce/ciphertext: %w", err)
	}

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

	// Step 4: Parse the decrypted metadata JSON
	var metadata struct {
		Name     string `json:"name"`
		MimeType string `json:"mime_type"`
	}
	if err := json.Unmarshal(decryptedMetadata, &metadata); err != nil {
		return "", "", fmt.Errorf("failed to parse metadata JSON: %w", err)
	}

	return metadata.Name, metadata.MimeType, nil
}

// truncateForLog truncates a string for logging purposes
func truncateForLog(s string, maxLen int) string {
	if len(s) <= maxLen {
		return s
	}
	return s[:maxLen] + "..."
}
