package app

import (
	"fmt"
	"io"
	"os"
	"os/exec"
	"path/filepath"
	sysRuntime "runtime"
	"strings"
	"time"
	"unicode"

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

// =============================================================================
// EXPORT TYPES AND UTILITIES
// =============================================================================

// ExportError represents an error that occurred during export
type ExportError struct {
	FileID       string `json:"file_id"`
	Filename     string `json:"filename"`
	CollectionID string `json:"collection_id"`
	ErrorMessage string `json:"error_message"`
	Timestamp    string `json:"timestamp"`
}

// ExportEstimate provides an estimate of what will be exported
type ExportEstimate struct {
	TotalCollections  int    `json:"total_collections"`
	OwnedCollections  int    `json:"owned_collections"`
	SharedCollections int    `json:"shared_collections"`
	TotalFiles        int    `json:"total_files"`
	TotalSizeBytes    int64  `json:"total_size_bytes"`
	LocalFilesCount   int    `json:"local_files_count"`
	CloudOnlyCount    int    `json:"cloud_only_count"`
	EstimatedTime     string `json:"estimated_time"`
}

// UserProfileExport represents exported user profile data
type UserProfileExport struct {
	ID         string `json:"id"`
	Email      string `json:"email"`
	FirstName  string `json:"first_name"`
	LastName   string `json:"last_name"`
	Name       string `json:"name"`
	Phone      string `json:"phone,omitempty"`
	Country    string `json:"country,omitempty"`
	Timezone   string `json:"timezone,omitempty"`
	CreatedAt  string `json:"created_at"`
	ExportedAt string `json:"exported_at"`
}

// CollectionExportData represents a single collection in the export
type CollectionExportData struct {
	ID             string `json:"id"`
	Name           string `json:"name"`
	CollectionType string `json:"collection_type"`
	ParentID       string `json:"parent_id,omitempty"`
	FileCount      int    `json:"file_count"`
	CreatedAt      string `json:"created_at"`
	ModifiedAt     string `json:"modified_at"`
	IsShared       bool   `json:"is_shared"`
}

// CollectionsExport represents all exported collections
type CollectionsExport struct {
	OwnedCollections  []*CollectionExportData `json:"owned_collections"`
	SharedCollections []*CollectionExportData `json:"shared_collections"`
	TotalCount        int                     `json:"total_count"`
	ExportedAt        string                  `json:"exported_at"`
}

// FileExportData represents a single file's metadata in the export
type FileExportData struct {
	ID             string `json:"id"`
	Filename       string `json:"filename"`
	MimeType       string `json:"mime_type"`
	SizeBytes      int64  `json:"size_bytes"`
	CreatedAt      string `json:"created_at"`
	ModifiedAt     string `json:"modified_at"`
	CollectionID   string `json:"collection_id"`
	CollectionName string `json:"collection_name"`
}

// FilesMetadataExport represents all exported file metadata
type FilesMetadataExport struct {
	Files      []*FileExportData `json:"files"`
	TotalCount int               `json:"total_count"`
	TotalSize  int64             `json:"total_size_bytes"`
	ExportedAt string            `json:"exported_at"`
}

// FileExportResult represents the result of exporting a single file
type FileExportResult struct {
	FileID       string `json:"file_id"`
	Filename     string `json:"filename"`
	SourceType   string `json:"source_type"`
	SizeBytes    int64  `json:"size_bytes"`
	DestPath     string `json:"dest_path"`
	Success      bool   `json:"success"`
	ErrorMessage string `json:"error_message,omitempty"`
}

// ExportSummary is the final summary of the export operation
type ExportSummary struct {
	ExportedAt        string        `json:"exported_at"`
	ExportPath        string        `json:"export_path"`
	TotalCollections  int           `json:"total_collections"`
	OwnedCollections  int           `json:"owned_collections"`
	SharedCollections int           `json:"shared_collections"`
	TotalFiles        int           `json:"total_files"`
	FilesExported     int           `json:"files_exported"`
	FilesCopiedLocal  int           `json:"files_copied_local"`
	FilesDownloaded   int           `json:"files_downloaded"`
	FilesFailed       int           `json:"files_failed"`
	TotalSizeBytes    int64         `json:"total_size_bytes"`
	Errors            []ExportError `json:"errors,omitempty"`
}

// =============================================================================
// EXPORT SETUP OPERATIONS
// =============================================================================

// SelectExportDirectory opens a dialog for the user to select an export directory
func (a *Application) SelectExportDirectory() (string, error) {
	// Get user's home directory as default
	homeDir, err := os.UserHomeDir()
	if err != nil {
		homeDir = ""
	}

	dir, err := runtime.OpenDirectoryDialog(a.ctx, runtime.OpenDialogOptions{
		DefaultDirectory:           homeDir,
		Title:                      "Select Export Directory",
		CanCreateDirectories:       true,
		ShowHiddenFiles:            false,
		TreatPackagesAsDirectories: false,
	})
	if err != nil {
		a.logger.Error("Failed to open directory dialog", zap.Error(err))
		return "", fmt.Errorf("failed to open directory dialog: %w", err)
	}

	return dir, nil
}

// GetExportEstimate returns an estimate of what will be exported
func (a *Application) GetExportEstimate() (*ExportEstimate, error) {
	a.logger.Info("Getting export estimate")

	// Get current session
	session, err := a.authService.GetCurrentSession(a.ctx)
	if err != nil {
		return nil, fmt.Errorf("not authenticated: %w", err)
	}

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

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

	// Get dashboard for storage stats
	dashResp, err := apiClient.GetDashboard(a.ctx)
	if err != nil {
		a.logger.Warn("Failed to get dashboard for estimate", zap.Error(err))
	}

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

	// Get shared collections
	sharedCollections, err := a.listSharedCollections()
	if err != nil {
		a.logger.Warn("Failed to list shared collections", zap.Error(err))
		sharedCollections = []*CollectionData{}
	}

	// Count files and check local availability
	totalFiles := 0
	localFilesCount := 0
	cloudOnlyCount := 0
	var totalSizeBytes int64 = 0

	allCollections := append(ownedCollections, sharedCollections...)
	for _, coll := range allCollections {
		totalFiles += coll.TotalFiles
	}

	// Check local file repository for files with decrypted content available
	// We check for FilePath (decrypted file) since that's what we copy during export
	localFiles, err := a.mustGetFileRepo().List()
	if err == nil {
		for _, f := range localFiles {
			if f.FilePath != "" {
				localFilesCount++
				totalSizeBytes += f.DecryptedSizeInBytes
			}
		}
	}
	cloudOnlyCount = totalFiles - localFilesCount
	if cloudOnlyCount < 0 {
		cloudOnlyCount = 0
	}

	// Note: Dashboard has storage in formatted units (e.g., "1.5 GB")
	// We use our calculated totalSizeBytes instead for accuracy
	_ = dashResp // Suppress unused variable warning if dashboard call failed

	// Estimate time based on file count and sizes
	estimatedTime := "Less than a minute"
	if cloudOnlyCount > 0 {
		// Rough estimate: 1 file per second for cloud downloads
		seconds := cloudOnlyCount
		if seconds > 60 {
			minutes := seconds / 60
			if minutes > 60 {
				estimatedTime = fmt.Sprintf("About %d hours", minutes/60)
			} else {
				estimatedTime = fmt.Sprintf("About %d minutes", minutes)
			}
		} else {
			estimatedTime = fmt.Sprintf("About %d seconds", seconds)
		}
	}

	estimate := &ExportEstimate{
		TotalCollections:  len(allCollections),
		OwnedCollections:  len(ownedCollections),
		SharedCollections: len(sharedCollections),
		TotalFiles:        totalFiles,
		TotalSizeBytes:    totalSizeBytes,
		LocalFilesCount:   localFilesCount,
		CloudOnlyCount:    cloudOnlyCount,
		EstimatedTime:     estimatedTime,
	}

	a.logger.Info("Export estimate calculated",
		zap.Int("total_collections", estimate.TotalCollections),
		zap.Int("total_files", estimate.TotalFiles),
		zap.Int("local_files", estimate.LocalFilesCount),
		zap.Int("cloud_only", estimate.CloudOnlyCount))

	return estimate, nil
}

// CreateExportDirectory creates the export directory with timestamp
func (a *Application) CreateExportDirectory(basePath string) (string, error) {
	timestamp := time.Now().Format("2006-01-02_15-04-05")
	exportDir := filepath.Join(basePath, fmt.Sprintf("MapleFile_Export_%s", timestamp))

	if err := os.MkdirAll(exportDir, 0755); err != nil {
		return "", fmt.Errorf("failed to create export directory: %w", err)
	}

	// Create subdirectories
	filesDir := filepath.Join(exportDir, "files")
	if err := os.MkdirAll(filesDir, 0755); err != nil {
		return "", fmt.Errorf("failed to create files directory: %w", err)
	}

	return exportDir, nil
}

// OpenExportFolder opens the export folder in the system file manager
func (a *Application) OpenExportFolder(path string) error {
	// Security: Validate the path before passing to exec.Command
	if path == "" {
		return fmt.Errorf("path cannot be empty")
	}

	// Get absolute path and clean it
	absPath, err := filepath.Abs(path)
	if err != nil {
		return fmt.Errorf("invalid path: %w", err)
	}
	absPath = filepath.Clean(absPath)

	// Verify the path exists and is a directory
	info, err := os.Stat(absPath)
	if err != nil {
		if os.IsNotExist(err) {
			return fmt.Errorf("path does not exist: %s", absPath)
		}
		return fmt.Errorf("failed to access path: %w", err)
	}

	if !info.IsDir() {
		return fmt.Errorf("path is not a directory: %s", absPath)
	}

	a.logger.Info("Opening export folder",
		zap.String("path", absPath))

	var cmd *exec.Cmd
	switch sysRuntime.GOOS {
	case "darwin":
		cmd = exec.Command("open", absPath)
	case "windows":
		cmd = exec.Command("explorer", absPath)
	case "linux":
		cmd = exec.Command("xdg-open", absPath)
	default:
		return fmt.Errorf("unsupported operating system: %s", sysRuntime.GOOS)
	}

	return cmd.Start()
}

// =============================================================================
// HELPER FUNCTIONS
// =============================================================================

// sanitizeFilename removes or replaces characters that are invalid in filenames.
// This function provides defense-in-depth against path traversal attacks by:
// 1. Extracting only the base filename (removing any path components)
// 2. Handling special directory references (. and ..)
// 3. Removing control characters
// 4. Replacing invalid filesystem characters
// 5. Handling Windows reserved names
// 6. Limiting filename length
func sanitizeFilename(name string) string {
	// Step 1: Extract only the base filename to prevent path traversal
	// This handles cases like "../../../etc/passwd" -> "passwd"
	name = filepath.Base(name)

	// Step 2: Handle special directory references
	if name == "." || name == ".." || name == "" {
		return "unnamed"
	}

	// Step 3: Trim leading/trailing whitespace and dots
	// Windows doesn't allow filenames ending with dots or spaces
	name = strings.TrimSpace(name)
	name = strings.Trim(name, ".")

	if name == "" {
		return "unnamed"
	}

	// Step 4: Remove control characters (ASCII 0-31)
	result := make([]rune, 0, len(name))
	for _, r := range name {
		if r < 32 || !unicode.IsPrint(r) {
			continue // Skip control characters
		}
		result = append(result, r)
	}
	name = string(result)

	// Step 5: Replace invalid filesystem characters
	// These are invalid on Windows: \ / : * ? " < > |
	// Forward/back slashes are also dangerous for path traversal
	replacer := map[rune]rune{
		'/':  '-',
		'\\': '-',
		':':  '-',
		'*':  '-',
		'?':  '-',
		'"':  '\'',
		'<':  '(',
		'>':  ')',
		'|':  '-',
	}

	result = make([]rune, 0, len(name))
	for _, r := range name {
		if replacement, ok := replacer[r]; ok {
			result = append(result, replacement)
		} else {
			result = append(result, r)
		}
	}
	name = string(result)

	// Step 6: Handle Windows reserved names
	// These names are reserved regardless of extension: CON, PRN, AUX, NUL,
	// COM1-COM9, LPT1-LPT9
	upperName := strings.ToUpper(name)
	// Extract name without extension for comparison
	nameWithoutExt := upperName
	if idx := strings.LastIndex(upperName, "."); idx > 0 {
		nameWithoutExt = upperName[:idx]
	}

	reservedNames := map[string]bool{
		"CON": true, "PRN": true, "AUX": true, "NUL": true,
		"COM1": true, "COM2": true, "COM3": true, "COM4": true,
		"COM5": true, "COM6": true, "COM7": true, "COM8": true, "COM9": true,
		"LPT1": true, "LPT2": true, "LPT3": true, "LPT4": true,
		"LPT5": true, "LPT6": true, "LPT7": true, "LPT8": true, "LPT9": true,
	}

	if reservedNames[nameWithoutExt] {
		name = "_" + name
	}

	// Step 7: Limit filename length
	// Most filesystems support 255 bytes; we use 200 to leave room for path
	const maxFilenameLength = 200
	if len(name) > maxFilenameLength {
		// Try to preserve the extension
		ext := filepath.Ext(name)
		if len(ext) < maxFilenameLength-10 {
			nameWithoutExt := name[:len(name)-len(ext)]
			if len(nameWithoutExt) > maxFilenameLength-len(ext) {
				nameWithoutExt = nameWithoutExt[:maxFilenameLength-len(ext)]
			}
			name = nameWithoutExt + ext
		} else {
			name = name[:maxFilenameLength]
		}
	}

	// Final check
	if name == "" {
		return "unnamed"
	}

	return name
}

// copyFile copies a file from src to dst
func copyFile(src, dst string) error {
	sourceFile, err := os.Open(src)
	if err != nil {
		return err
	}
	defer sourceFile.Close()

	destFile, err := os.Create(dst)
	if err != nil {
		return err
	}
	defer destFile.Close()

	_, err = io.Copy(destFile, sourceFile)
	if err != nil {
		return err
	}

	return destFile.Sync()
}
