package appexport

import (
	"context"
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"time"
)

// ExportEstimate holds pre-export size and space estimation.
type ExportEstimate struct {
	ConfigSizeBytes  int64  `json:"config_size_bytes"`
	ConfigSizeHuman  string `json:"config_size_human"`
	DataSizeBytes    int64  `json:"data_size_bytes"`
	DataSizeHuman    string `json:"data_size_human"`
	TotalSizeBytes   int64  `json:"total_size_bytes"`
	TotalSizeHuman   string `json:"total_size_human"`
	EstimatedMinutes int    `json:"estimated_minutes"`
	DestFreeBytes    int64  `json:"dest_free_bytes"`
	DestFreeHuman    string `json:"dest_free_human"`
	FitsOnDest       bool   `json:"fits_on_dest"`
}

// EstimateExport calculates size estimates for an app export.
func (e *Exporter) EstimateExport(stackName, destDrive string) (*ExportEstimate, error) {
	stackDir, ok := e.provider.GetStackDir(stackName)
	if !ok {
		return nil, fmt.Errorf("stack %q not found", stackName)
	}

	e.debugf("EstimateExport: stack=%s stackDir=%s destDrive=%s", stackName, stackDir, destDrive)
	est := &ExportEstimate{}

	// Config size: sum of all files in the stack directory
	est.ConfigSizeBytes = dirSize(stackDir)
	est.ConfigSizeHuman = humanizeBytes(est.ConfigSizeBytes)
	e.debugf("EstimateExport: configSize=%s (%d bytes)", est.ConfigSizeHuman, est.ConfigSizeBytes)

	// Data size: HDD bind mounts or Docker volumes
	if e.provider.GetStackNeedsHDD(stackName) {
		mounts := e.provider.GetStackHDDMounts(stackName)
		e.debugf("EstimateExport: HDD mounts: %v", mounts)
		for _, mount := range mounts {
			mountSize := duBytes(mount)
			e.debugf("EstimateExport: mount %s = %s", mount, humanizeBytes(mountSize))
			est.DataSizeBytes += mountSize
		}
	} else {
		volumes := e.provider.GetDockerVolumes(stackName)
		e.debugf("EstimateExport: Docker volumes: %v", volumes)
		for _, vol := range volumes {
			volSize := dockerVolumeSize(vol)
			e.debugf("EstimateExport: volume %s = %s", vol, humanizeBytes(volSize))
			est.DataSizeBytes += volSize
		}
	}
	est.DataSizeHuman = humanizeBytes(est.DataSizeBytes)

	est.TotalSizeBytes = est.ConfigSizeBytes + est.DataSizeBytes
	est.TotalSizeHuman = humanizeBytes(est.TotalSizeBytes)

	// Rough time estimate: ~500 MB/min for HDDs, minimum 1 minute
	minutes := int(est.TotalSizeBytes / (500 * 1024 * 1024))
	if minutes < 1 {
		minutes = 1
	}
	est.EstimatedMinutes = minutes

	// Destination free space
	exportDir := ExportDir(destDrive)
	os.MkdirAll(exportDir, 0755)
	est.DestFreeBytes = diskFree(exportDir)
	est.DestFreeHuman = humanizeBytes(est.DestFreeBytes)

	// Need ~10% overhead for tar.gz metadata + compression margin
	needed := est.TotalSizeBytes + est.TotalSizeBytes/10
	est.FitsOnDest = est.DestFreeBytes >= needed

	e.debugf("EstimateExport: total=%s free=%s fits=%v needed=%s minutes=%d",
		est.TotalSizeHuman, est.DestFreeHuman, est.FitsOnDest, humanizeBytes(needed), est.EstimatedMinutes)

	return est, nil
}

// dirSize returns the total size of all files in a directory (non-recursive for config dirs).
func dirSize(dir string) int64 {
	var total int64
	entries, err := os.ReadDir(dir)
	if err != nil {
		return 0
	}
	for _, e := range entries {
		if e.IsDir() {
			continue
		}
		info, err := e.Info()
		if err != nil {
			continue
		}
		total += info.Size()
	}
	return total
}

// duBytes runs du -sb on a path and returns the byte count.
func duBytes(path string) int64 {
	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()
	out, err := exec.CommandContext(ctx, "du", "-sb", path).Output()
	if err != nil {
		return 0
	}
	var size int64
	fmt.Sscanf(strings.Fields(string(out))[0], "%d", &size)
	return size
}

// dockerVolumeSize estimates the size of a Docker named volume.
func dockerVolumeSize(volumeName string) int64 {
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()
	// Use docker system df -v and parse, or inspect the volume mount path
	out, err := exec.CommandContext(ctx, "docker", "volume", "inspect",
		"--format", "{{.Mountpoint}}", volumeName).Output()
	if err != nil {
		return 0
	}
	mountpoint := strings.TrimSpace(string(out))
	if mountpoint == "" {
		return 0
	}
	return duBytes(mountpoint)
}

// diskFree returns available bytes on the filesystem containing path.
func diskFree(path string) int64 {
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	out, err := exec.CommandContext(ctx, "df", "--output=avail", "-B1", path).Output()
	if err != nil {
		return 0
	}
	lines := strings.Split(strings.TrimSpace(string(out)), "\n")
	if len(lines) < 2 {
		return 0
	}
	var size int64
	fmt.Sscanf(strings.TrimSpace(lines[1]), "%d", &size)
	return size
}

// ExportDir returns the exports directory on a drive. Model A (slice 10): a registered drive's
// in-guest mount IS the felhom-data namespace root, so exports/ sits directly under it (no
// felhom-data segment — avoids the .../felhom-data/felhom-data/... double-nest).
func ExportDir(drivePath string) string {
	return filepath.Join(drivePath, "exports")
}

// humanizeBytes converts bytes to human-readable format.
func humanizeBytes(b int64) string {
	const (
		KB = 1024
		MB = KB * 1024
		GB = MB * 1024
	)
	switch {
	case b >= GB:
		return fmt.Sprintf("%.1f GB", float64(b)/float64(GB))
	case b >= MB:
		return fmt.Sprintf("%.1f MB", float64(b)/float64(MB))
	case b >= KB:
		return fmt.Sprintf("%.1f KB", float64(b)/float64(KB))
	default:
		return fmt.Sprintf("%d B", b)
	}
}
