package stacks

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

// felhomDataDir matches backup.FelhomDataDir — duplicated to avoid circular import via StackDataProvider.
const felhomDataDir = "felhom-data"

// DeleteResponse holds the result of a stack deletion (orphan delete).
type DeleteResponse struct {
	Deleted           string   `json:"deleted"`
	VolumesRemoved    []string `json:"volumes_removed"`
	HDDPathsRemoved   []string `json:"hdd_paths_removed"`
	HDDPathsPreserved []string `json:"hdd_paths_preserved"`
}

// RemoveResponse holds the result of removing a deployed (non-orphaned) stack.
type RemoveResponse struct {
	Removed            string   `json:"removed"`
	VolumesRemoved     []string `json:"volumes_removed"`
	HDDPathsRemoved    []string `json:"hdd_paths_removed"`
	HDDPathsPreserved  []string `json:"hdd_paths_preserved"`
	BackupPathsRemoved []string `json:"backup_paths_removed,omitempty"`
}

// BackupDataResponse holds information about backup data associated with a stack.
type BackupDataResponse struct {
	Stack       string    `json:"stack"`
	BackupPaths []HDDPath `json:"backup_paths"` // reuses HDDPath (path, size, exists)
	HasBackups  bool      `json:"has_backups"`
}

// HDDDataResponse holds information about HDD data associated with a stack.
type HDDDataResponse struct {
	Stack      string    `json:"stack"`
	HDDPaths   []HDDPath `json:"hdd_paths"`
	HasHDDData bool      `json:"has_hdd_data"`
}

// HDDPath represents a single HDD bind mount path and its status.
type HDDPath struct {
	Path      string `json:"path"`
	SizeBytes int64  `json:"size_bytes"`
	SizeHuman string `json:"size_human"`
	Exists    bool   `json:"exists"`
}

// ProtectedHDDPaths returns the set of top-level HDD directories that must never be deleted.
func ProtectedHDDPaths(hddPath string) map[string]bool {
	if hddPath == "" {
		return nil
	}
	return map[string]bool{
		// Model A: the in-guest drive mount IS the felhom-data namespace root, so backups/ and
		// appdata/ sit directly under it (no felhom-data segment).
		hddPath:                                true,
		filepath.Join(hddPath, "appdata"):      true,
		filepath.Join(hddPath, "backups"):      true,
		filepath.Join(hddPath, "media"):        true,
		filepath.Join(hddPath, "Dokumentumok"): true,
		// Legacy pre-Model-A double-nest location; kept protected so any leftover data there is
		// never wiped by a removal.
		filepath.Join(hddPath, felhomDataDir):            true,
		filepath.Join(hddPath, felhomDataDir, "appdata"): true,
		filepath.Join(hddPath, felhomDataDir, "backups"): true,
	}
}

// DeleteStack removes an orphaned stack: stops containers, removes volumes,
// optionally removes HDD data, and deletes the stack directory.
func (m *Manager) DeleteStack(name string, removeHDDData bool) (*DeleteResponse, error) {
	if m.isDebug() {
		m.logger.Printf("[DEBUG] [stacks] DeleteStack called: name=%q, removeHDDData=%v", name, removeHDDData)
	}

	// Safety: never delete protected stacks
	if m.cfg.IsProtectedStack(name) {
		return nil, fmt.Errorf("stack %q is protected and cannot be deleted", name)
	}

	stack, ok := m.GetStack(name)
	if !ok {
		return nil, fmt.Errorf("stack %q not found", name)
	}

	if m.isDebug() {
		m.logger.Printf("[DEBUG] [stacks] DeleteStack %s: state=%s, deployed=%v, orphaned=%v, deploying=%v",
			name, stack.State, stack.Deployed, stack.Orphaned, stack.Deploying)
	}

	// Must be orphaned
	if !stack.Orphaned {
		return nil, fmt.Errorf("stack %q is not orphaned — only orphaned stacks can be deleted", name)
	}

	// Must not be deploying (H2 fix)
	if stack.Deploying {
		return nil, fmt.Errorf("stack %q is currently being deployed — wait for deployment to finish", name)
	}

	// Must be stopped (not running)
	if stack.State == StateRunning || stack.State == StateStarting || stack.State == StateRestarting {
		return nil, fmt.Errorf("stack %q is still running — stop it first before deleting", name)
	}

	stackDir := filepath.Dir(stack.ComposePath)
	hddPath := m.cfg.Paths.HDDPath

	m.logger.Printf("[INFO] Deleting orphaned stack: %s (removeHDDData=%v)", name, removeHDDData)
	start := time.Now()

	resp := &DeleteResponse{
		Deleted: name,
	}

	// Step 1: Parse compose file for HDD bind mounts
	hddMounts := ParseComposeHDDMounts(stack.ComposePath, hddPath)
	if m.isDebug() {
		m.logger.Printf("[DEBUG] [stacks] DeleteStack %s: found %d HDD mounts from compose file", name, len(hddMounts))
		for i, mount := range hddMounts {
			m.logger.Printf("[DEBUG] [stacks] DeleteStack %s: HDD mount[%d]=%s", name, i, mount)
		}
	}

	// Step 2: Run docker compose down --rmi local --volumes
	// H14: Return error if docker compose down fails — continuing would leave orphaned containers.
	env := m.stackEnv(stackDir)
	output, err := m.composeExecCustomEnv(stackDir, env, "down", "--rmi", "local", "--volumes")
	if m.isDebug() {
		m.logger.Printf("[DEBUG] [stacks] DeleteStack %s: compose down output: %s", name, truncateStr(output, 500))
	}
	if err != nil {
		m.logger.Printf("[ERROR] docker compose down for %s failed: %v (output: %s)", name, err, truncateStr(output, 200))
		return resp, fmt.Errorf("docker compose down failed for %s: %w", name, err)
	}

	// Step 3: Identify removed volumes from compose output
	for _, line := range strings.Split(output, "\n") {
		line = strings.TrimSpace(line)
		if strings.Contains(line, "Removing volume") || strings.Contains(line, "Volume") {
			resp.VolumesRemoved = append(resp.VolumesRemoved, line)
		}
	}

	// Step 4: Handle HDD data
	protected := ProtectedHDDPaths(hddPath)
	for _, mount := range hddMounts {
		// Safety: never delete protected top-level dirs
		cleanPath := filepath.Clean(mount)
		if protected != nil && protected[cleanPath] {
			m.logger.Printf("[WARN] Refusing to delete protected HDD path: %s", cleanPath)
			continue
		}

		if _, err := os.Stat(cleanPath); os.IsNotExist(err) {
			if m.isDebug() {
				m.logger.Printf("[DEBUG] [stacks] DeleteStack %s: HDD path does not exist, skipping: %s", name, cleanPath)
			}
			continue // path doesn't exist, nothing to do
		}

		if removeHDDData {
			// Get size before removal
			sizeHuman := getDirSizeHuman(cleanPath)
			if m.isDebug() {
				m.logger.Printf("[DEBUG] [stacks] DeleteStack %s: removing HDD path %s (%s)", name, cleanPath, sizeHuman)
			}
			if err := os.RemoveAll(cleanPath); err != nil {
				m.logger.Printf("[ERROR] Failed to remove HDD data %s: %v", cleanPath, err)
			} else {
				m.logger.Printf("[INFO] Removed HDD data: %s (%s)", cleanPath, sizeHuman)
				resp.HDDPathsRemoved = append(resp.HDDPathsRemoved, fmt.Sprintf("%s (%s)", cleanPath, sizeHuman))
			}
		} else {
			sizeHuman := getDirSizeHuman(cleanPath)
			if m.isDebug() {
				m.logger.Printf("[DEBUG] [stacks] DeleteStack %s: preserving HDD path %s (%s)", name, cleanPath, sizeHuman)
			}
			resp.HDDPathsPreserved = append(resp.HDDPathsPreserved, fmt.Sprintf("%s (%s)", cleanPath, sizeHuman))
		}
	}

	// Step 5: Remove stack directory
	if m.isDebug() {
		m.logger.Printf("[DEBUG] [stacks] DeleteStack %s: removing stack directory %s", name, stackDir)
	}
	if err := os.RemoveAll(stackDir); err != nil {
		m.logger.Printf("[ERROR] Failed to remove stack directory %s: %v", stackDir, err)
		return resp, fmt.Errorf("failed to remove stack directory: %w", err)
	}

	m.logger.Printf("[INFO] Stack %s deleted successfully (took %.1fs)", name, time.Since(start).Seconds())

	// Step 6: Remove from in-memory map and rescan
	m.mu.Lock()
	delete(m.stacks, name)
	m.mu.Unlock()

	if err := m.ScanStacks(); err != nil {
		m.logger.Printf("[WARN] Rescan after delete failed: %v", err)
	}

	return resp, nil
}

// GetStackHDDData returns information about HDD bind mounts for a stack.
func (m *Manager) GetStackHDDData(name string) (*HDDDataResponse, error) {
	stack, ok := m.GetStack(name)
	if !ok {
		return nil, fmt.Errorf("stack %q not found", name)
	}

	hddPath := m.cfg.Paths.HDDPath
	resp := &HDDDataResponse{
		Stack: name,
	}

	if hddPath == "" {
		if m.isDebug() {
			m.logger.Printf("[DEBUG] [stacks] GetStackHDDData %s: no HDD path configured, returning empty", name)
		}
		return resp, nil
	}

	mounts := ParseComposeHDDMounts(stack.ComposePath, hddPath)
	protected := ProtectedHDDPaths(hddPath)

	if m.isDebug() {
		m.logger.Printf("[DEBUG] [stacks] GetStackHDDData %s: found %d raw HDD mounts from compose", name, len(mounts))
	}

	for _, mount := range mounts {
		cleanPath := filepath.Clean(mount)

		// Skip protected top-level dirs
		if protected != nil && protected[cleanPath] {
			continue
		}

		hddItem := HDDPath{
			Path: cleanPath,
		}

		info, err := os.Stat(cleanPath)
		if err != nil {
			hddItem.Exists = false
		} else {
			hddItem.Exists = true
			if info.IsDir() {
				hddItem.SizeBytes = getDirSizeBytes(cleanPath)
				hddItem.SizeHuman = getDirSizeHuman(cleanPath)
			}
		}

		resp.HDDPaths = append(resp.HDDPaths, hddItem)
	}

	resp.HasHDDData = len(resp.HDDPaths) > 0

	if m.isDebug() {
		for _, p := range resp.HDDPaths {
			m.logger.Printf("[DEBUG] [stacks] GetStackHDDData %s: path=%s exists=%v size=%s", name, p.Path, p.Exists, p.SizeHuman)
		}
		m.logger.Printf("[DEBUG] [stacks] GetStackHDDData %s: hasHDDData=%v, %d paths returned", name, resp.HasHDDData, len(resp.HDDPaths))
	}

	return resp, nil
}

// RemoveStack removes a deployed (non-orphaned) stack: stops containers, removes
// volumes, optionally removes HDD data and backup data, then removes app.yaml
// so the stack reverts to "not deployed" state. The template files (docker-compose.yml,
// .felhom.yml) are preserved so the user can redeploy.
func (m *Manager) RemoveStack(name string, removeHDDData bool, backupPathsToRemove []string) (*RemoveResponse, error) {
	if m.isDebug() {
		m.logger.Printf("[DEBUG] [stacks] RemoveStack called: name=%q, removeHDDData=%v, backupPathsToRemove=%d", name, removeHDDData, len(backupPathsToRemove))
	}

	// Safety: never remove protected stacks
	if m.cfg.IsProtectedStack(name) {
		return nil, fmt.Errorf("stack %q is protected and cannot be removed", name)
	}

	stack, ok := m.GetStack(name)
	if !ok {
		return nil, fmt.Errorf("stack %q not found", name)
	}

	if m.isDebug() {
		m.logger.Printf("[DEBUG] [stacks] RemoveStack %s: state=%s, deployed=%v, orphaned=%v, deploying=%v",
			name, stack.State, stack.Deployed, stack.Orphaned, stack.Deploying)
	}

	// Must be deployed
	if !stack.Deployed {
		return nil, fmt.Errorf("stack %q is not deployed", name)
	}

	// Must not be deploying (H2 fix)
	if stack.Deploying {
		return nil, fmt.Errorf("stack %q is currently being deployed — wait for deployment to finish", name)
	}

	// Must be stopped (not running)
	if stack.State == StateRunning || stack.State == StateStarting || stack.State == StateRestarting {
		return nil, fmt.Errorf("stack %q is still running — stop it first before removing", name)
	}

	stackDir := filepath.Dir(stack.ComposePath)
	hddPath := m.cfg.Paths.HDDPath

	m.logger.Printf("[INFO] Removing deployed stack: %s (removeHDDData=%v, backupPaths=%d)", name, removeHDDData, len(backupPathsToRemove))
	start := time.Now()

	resp := &RemoveResponse{
		Removed: name,
	}

	// Step 1: Parse compose file for HDD bind mounts
	hddMounts := ParseComposeHDDMounts(stack.ComposePath, hddPath)
	if m.isDebug() {
		m.logger.Printf("[DEBUG] [stacks] RemoveStack %s: found %d HDD mounts from compose file", name, len(hddMounts))
		for i, mount := range hddMounts {
			m.logger.Printf("[DEBUG] [stacks] RemoveStack %s: HDD mount[%d]=%s", name, i, mount)
		}
	}

	// Step 2: Run docker compose down --volumes (keep images for potential redeploy)
	env := m.stackEnv(stackDir)
	output, err := m.composeExecCustomEnv(stackDir, env, "down", "--volumes")
	if m.isDebug() {
		m.logger.Printf("[DEBUG] [stacks] RemoveStack %s: compose down output: %s", name, truncateStr(output, 500))
	}
	if err != nil {
		m.logger.Printf("[ERROR] docker compose down for %s failed: %v (output: %s)", name, err, truncateStr(output, 200))
		return resp, fmt.Errorf("docker compose down failed for %s: %w", name, err)
	}

	// Step 3: Identify removed volumes from compose output
	for _, line := range strings.Split(output, "\n") {
		line = strings.TrimSpace(line)
		if strings.Contains(line, "Removing volume") || strings.Contains(line, "Volume") {
			resp.VolumesRemoved = append(resp.VolumesRemoved, line)
		}
	}

	// Step 4: Handle HDD data
	protected := ProtectedHDDPaths(hddPath)
	for _, mount := range hddMounts {
		cleanPath := filepath.Clean(mount)
		if protected != nil && protected[cleanPath] {
			m.logger.Printf("[WARN] Refusing to delete protected HDD path: %s", cleanPath)
			continue
		}

		if _, err := os.Stat(cleanPath); os.IsNotExist(err) {
			if m.isDebug() {
				m.logger.Printf("[DEBUG] [stacks] RemoveStack %s: HDD path does not exist, skipping: %s", name, cleanPath)
			}
			continue
		}

		if removeHDDData {
			sizeHuman := getDirSizeHuman(cleanPath)
			if m.isDebug() {
				m.logger.Printf("[DEBUG] [stacks] RemoveStack %s: removing HDD path %s (%s)", name, cleanPath, sizeHuman)
			}
			if err := os.RemoveAll(cleanPath); err != nil {
				m.logger.Printf("[ERROR] Failed to remove HDD data %s: %v", cleanPath, err)
			} else {
				m.logger.Printf("[INFO] Removed HDD data: %s (%s)", cleanPath, sizeHuman)
				resp.HDDPathsRemoved = append(resp.HDDPathsRemoved, fmt.Sprintf("%s (%s)", cleanPath, sizeHuman))
			}
		} else {
			sizeHuman := getDirSizeHuman(cleanPath)
			if m.isDebug() {
				m.logger.Printf("[DEBUG] [stacks] RemoveStack %s: preserving HDD path %s (%s)", name, cleanPath, sizeHuman)
			}
			resp.HDDPathsPreserved = append(resp.HDDPathsPreserved, fmt.Sprintf("%s (%s)", cleanPath, sizeHuman))
		}
	}

	// Step 5: Handle backup data cleanup. Model A: backups/ sits directly under the namespace-root
	// drive mount (no felhom-data segment).
	backupsBase := filepath.Join(hddPath, "backups")
	if m.isDebug() {
		m.logger.Printf("[DEBUG] [stacks] RemoveStack %s: processing %d backup paths for removal (base=%s)", name, len(backupPathsToRemove), backupsBase)
	}
	for _, bkPath := range backupPathsToRemove {
		cleanPath := filepath.Clean(bkPath)
		// Validate path is under the expected backups directory
		if hddPath == "" || !strings.HasPrefix(cleanPath, backupsBase+string(filepath.Separator)) {
			m.logger.Printf("[WARN] Refusing to remove backup path outside expected directory: %s", cleanPath)
			continue
		}
		if _, err := os.Stat(cleanPath); os.IsNotExist(err) {
			continue
		}
		sizeHuman := getDirSizeHuman(cleanPath)
		if err := os.RemoveAll(cleanPath); err != nil {
			m.logger.Printf("[ERROR] Failed to remove backup data %s: %v", cleanPath, err)
		} else {
			m.logger.Printf("[INFO] Removed backup data: %s (%s)", cleanPath, sizeHuman)
			resp.BackupPathsRemoved = append(resp.BackupPathsRemoved, fmt.Sprintf("%s (%s)", cleanPath, sizeHuman))
		}
	}

	// Step 6: Remove app.yaml only (keep template files for redeploy)
	appYAMLPath := filepath.Join(stackDir, "app.yaml")
	if m.isDebug() {
		m.logger.Printf("[DEBUG] [stacks] RemoveStack %s: removing app.yaml at %s", name, appYAMLPath)
	}
	if err := os.Remove(appYAMLPath); err != nil && !os.IsNotExist(err) {
		m.logger.Printf("[ERROR] Failed to remove %s: %v", appYAMLPath, err)
		return resp, fmt.Errorf("failed to remove app.yaml: %w", err)
	}

	m.logger.Printf("[INFO] Stack %s removed successfully (took %.1fs)", name, time.Since(start).Seconds())

	// Step 7: Update in-memory state and rescan
	m.mu.Lock()
	if s, ok := m.stacks[name]; ok {
		s.Deployed = false
		s.AppConfig = nil
	}
	m.mu.Unlock()

	if err := m.ScanStacks(); err != nil {
		m.logger.Printf("[WARN] Rescan after remove failed: %v", err)
	}

	return resp, nil
}

// GetStackBackupData returns information about backup data for a stack.
// drivePath is the app's home drive (HDD or system data path).
func (m *Manager) GetStackBackupData(name string, drivePath string) (*BackupDataResponse, error) {
	_, ok := m.GetStack(name)
	if !ok {
		return nil, fmt.Errorf("stack %q not found", name)
	}

	resp := &BackupDataResponse{
		Stack: name,
	}

	if drivePath == "" {
		if m.isDebug() {
			m.logger.Printf("[DEBUG] [stacks] GetStackBackupData %s: no drive path provided, returning empty", name)
		}
		return resp, nil
	}

	// Check DB dump directory. drivePath is the felhom-data namespace ROOT (Model A: the in-guest
	// drive mount itself), so backups/ sits directly under it: <nsRoot>/backups/primary/<stack>/db-dumps
	dbDumpPath := filepath.Join(drivePath, "backups", "primary", name, "db-dumps")
	resp.BackupPaths = append(resp.BackupPaths, buildPathInfo(dbDumpPath))

	// Check cross-drive rsync directory: <nsRoot>/backups/secondary/<stack>/rsync
	rsyncPath := filepath.Join(drivePath, "backups", "secondary", name, "rsync")
	resp.BackupPaths = append(resp.BackupPaths, buildPathInfo(rsyncPath))

	if m.isDebug() {
		for _, p := range resp.BackupPaths {
			m.logger.Printf("[DEBUG] [stacks] GetStackBackupData %s: checked path=%s exists=%v size=%s", name, p.Path, p.Exists, p.SizeHuman)
		}
	}

	for _, p := range resp.BackupPaths {
		if p.Exists {
			resp.HasBackups = true
			break
		}
	}

	if m.isDebug() {
		m.logger.Printf("[DEBUG] [stacks] GetStackBackupData %s: hasBackups=%v", name, resp.HasBackups)
	}

	return resp, nil
}

// buildPathInfo creates an HDDPath with size info for a given path.
func buildPathInfo(path string) HDDPath {
	item := HDDPath{Path: path}
	info, err := os.Stat(path)
	if err != nil {
		item.Exists = false
		return item
	}
	item.Exists = true
	if info.IsDir() {
		item.SizeBytes = getDirSizeBytes(path)
		item.SizeHuman = getDirSizeHuman(path)
	}
	return item
}

// ParseComposeUserdataMounts reads a docker-compose.yml and extracts the host bind-source paths that
// reference ${USERDATA_PATH} (resolved to userdataPath) — the dirs the deploy belt must pre-create
// with the userdata convention. Same scanner shape as ParseComposeHDDMounts.
func ParseComposeUserdataMounts(composePath, userdataPath string) []string {
	if userdataPath == "" {
		return nil
	}
	data, err := os.ReadFile(composePath)
	if err != nil {
		return nil
	}
	var mounts []string
	seen := make(map[string]bool)
	scanner := bufio.NewScanner(strings.NewReader(string(data)))
	inVolumes := false
	for scanner.Scan() {
		line := strings.TrimSpace(scanner.Text())
		if strings.HasPrefix(line, "volumes:") {
			inVolumes = true
			continue
		}
		if inVolumes && !strings.HasPrefix(line, "-") && !strings.HasPrefix(line, "#") && line != "" {
			inVolumes = false
		}
		if !inVolumes || !strings.HasPrefix(line, "- ") {
			continue
		}
		mountStr := strings.Trim(strings.TrimPrefix(line, "- "), "\"'")
		parts := strings.SplitN(mountStr, ":", 3)
		if len(parts) < 2 {
			continue
		}
		hostPath := strings.ReplaceAll(parts[0], "${USERDATA_PATH}", userdataPath)
		cleanPath := filepath.Clean(hostPath)
		cleanUD := filepath.Clean(userdataPath)
		// must be userdataPath itself or a subpath (clean before check — traversal-safe)
		if cleanPath != cleanUD && !strings.HasPrefix(cleanPath, cleanUD+string(filepath.Separator)) {
			continue
		}
		if !seen[cleanPath] {
			seen[cleanPath] = true
			mounts = append(mounts, cleanPath)
		}
	}
	return mounts
}

// ParseComposeHDDMounts reads a docker-compose.yml and extracts host paths
// that reference the HDD path from volume bind mounts.
func ParseComposeHDDMounts(composePath, hddPath string) []string {
	if hddPath == "" {
		return nil
	}

	data, err := os.ReadFile(composePath)
	if err != nil {
		return nil
	}

	var mounts []string
	seen := make(map[string]bool)

	scanner := bufio.NewScanner(strings.NewReader(string(data)))
	inVolumes := false
	for scanner.Scan() {
		line := strings.TrimSpace(scanner.Text())

		// Track when we're in a volumes section (service-level, not top-level)
		if strings.HasPrefix(line, "volumes:") {
			inVolumes = true
			continue
		}
		if inVolumes && !strings.HasPrefix(line, "-") && !strings.HasPrefix(line, "#") && line != "" {
			inVolumes = false
		}

		if !inVolumes || !strings.HasPrefix(line, "- ") {
			continue
		}

		// Parse bind mount: "- /host/path:/container/path:options"
		mountStr := strings.TrimPrefix(line, "- ")
		mountStr = strings.Trim(mountStr, "\"'")

		parts := strings.SplitN(mountStr, ":", 3)
		if len(parts) < 2 {
			continue
		}

		hostPath := parts[0]

		// Resolve ${HDD_PATH} variable reference
		hostPath = strings.ReplaceAll(hostPath, "${HDD_PATH}", hddPath)

		// C10: Clean path BEFORE prefix check to prevent traversal like ${HDD_PATH}/../../etc/passwd.
		cleanPath := filepath.Clean(hostPath)
		cleanHDD := filepath.Clean(hddPath)

		// Check if this is an HDD mount (must be cleanHDD itself or a direct subpath)
		if cleanPath != cleanHDD && !strings.HasPrefix(cleanPath, cleanHDD+string(filepath.Separator)) {
			continue
		}
		if !seen[cleanPath] {
			seen[cleanPath] = true
			mounts = append(mounts, cleanPath)
		}
	}

	log.Printf("[INFO] [stacks] ParseComposeHDDMounts: found %d HDD mounts for %s", len(mounts), composePath)
	return mounts
}

// getDirSizeHuman returns a human-readable size string for a directory using du.
func getDirSizeHuman(path string) string {
	cmd := exec.Command("du", "-sh", path)
	output, err := cmd.Output()
	if err != nil {
		return "unknown"
	}
	fields := strings.Fields(string(output))
	if len(fields) > 0 {
		return fields[0]
	}
	return "unknown"
}

// getDirSizeBytes returns the total size in bytes for a directory.
func getDirSizeBytes(path string) int64 {
	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()
	cmd := exec.CommandContext(ctx, "du", "-sb", path)
	output, err := cmd.Output()
	if err != nil {
		return 0
	}
	fields := strings.Fields(string(output))
	if len(fields) > 0 {
		var size int64
		if n, _ := fmt.Sscanf(fields[0], "%d", &size); n != 1 {
			return 0
		}
		return size
	}
	return 0
}
