package stacks

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

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

// 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{
		hddPath:                                true,
		filepath.Join(hddPath, "media"):        true,
		filepath.Join(hddPath, "storage"):      true,
		filepath.Join(hddPath, "Dokumentumok"): true,
		filepath.Join(hddPath, "appdata"):      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) {
	// 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)
	}

	// Must be orphaned
	if !stack.Orphaned {
		return nil, fmt.Errorf("stack %q is not orphaned — only orphaned stacks can be deleted", 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)

	// Step 2: Run docker compose down --rmi local --volumes
	env := m.stackEnv(stackDir)
	output, err := m.composeExecCustomEnv(stackDir, env, "down", "--rmi", "local", "--volumes")
	if err != nil {
		m.logger.Printf("[WARN] docker compose down for %s had errors: %v (output: %s)", name, err, truncateStr(output, 200))
		// Continue anyway — the stack dir will be removed
	}

	// 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) {
			continue // path doesn't exist, nothing to do
		}

		if removeHDDData {
			// Get size before removal
			sizeHuman := getDirSizeHuman(cleanPath)
			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)
			resp.HDDPathsPreserved = append(resp.HDDPathsPreserved, fmt.Sprintf("%s (%s)", cleanPath, sizeHuman))
		}
	}

	// Step 5: Remove stack directory
	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 == "" {
		return resp, nil
	}

	mounts := parseComposeHDDMounts(stack.ComposePath, hddPath)
	protected := protectedHDDPaths(hddPath)

	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
	return resp, nil
}

// 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)

		// Check if this is an HDD mount
		if !strings.HasPrefix(hostPath, hddPath) {
			continue
		}

		cleanPath := filepath.Clean(hostPath)
		if !seen[cleanPath] {
			seen[cleanPath] = true
			mounts = append(mounts, cleanPath)
		}
	}

	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 {
	cmd := exec.Command("du", "-sb", path)
	output, err := cmd.Output()
	if err != nil {
		return 0
	}
	fields := strings.Fields(string(output))
	if len(fields) > 0 {
		var size int64
		fmt.Sscanf(fields[0], "%d", &size)
		return size
	}
	return 0
}
