package models

import (
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
	"strings"

	"gopkg.in/yaml.v3"
)

var composeFileNames = []string{
	"docker-compose.yml",
	"docker-compose.yaml",
	"compose.yml",
	"compose.yaml",
}

// Host paths excluded from backup folder deduction.
var excludedPrefixes = []string{"/var/run/", "/proc/", "/sys/", "/dev/", "/run/", "/etc/"}
var excludedExact = []string{
	"/var/run/docker.sock",
	"/etc/timezone", "/etc/localtime", "/etc/hosts", "/etc/hostname", "/etc/resolv.conf",
}

// ScanAll discovers compose-based services under all default paths plus an optional
// extra folder. Existing services matched by compose file path are updated (status,
// and backup folder if previously unset). New services are created.
func ScanAll(extraFolder string) ScanResult {
	paths := resolveScanPaths(extraFolder)
	result := ScanResult{
		ScannedPaths: paths,
		Created:      []ServiceScanInfo{},
		Updated:      []ServiceScanInfo{},
		Errors:       []string{},
	}
	for _, p := range paths {
		r := scanFolder(p)
		result.Found += r.Found
		result.Created = append(result.Created, r.Created...)
		result.Updated = append(result.Updated, r.Updated...)
		result.Errors = append(result.Errors, r.Errors...)
	}
	return result
}

// resolveScanPaths returns the directories to scan: /opt/compose, /root/compose,
// /home/*/compose, and any explicitly supplied extra path.
func resolveScanPaths(extra string) []string {
	var paths []string
	for _, candidate := range []string{"/opt/compose", "/root/compose"} {
		if isAccessibleDir(candidate) {
			paths = append(paths, candidate)
		}
	}
	if entries, err := os.ReadDir("/home"); err == nil {
		for _, e := range entries {
			if !e.IsDir() {
				continue
			}
			p := filepath.Join("/home", e.Name(), "compose")
			if isAccessibleDir(p) {
				paths = append(paths, p)
			}
		}
	}
	if extra != "" && isAccessibleDir(extra) {
		paths = append(paths, extra)
	}
	return paths
}

func isAccessibleDir(path string) bool {
	info, err := os.Stat(path)
	return err == nil && info.IsDir()
}

// scanFolder scans the immediate subdirectories of folder for compose files.
func scanFolder(folder string) ScanResult {
	result := ScanResult{Created: []ServiceScanInfo{}, Updated: []ServiceScanInfo{}}
	entries, err := os.ReadDir(folder)
	if err != nil {
		result.Errors = []string{fmt.Sprintf("cannot read %s: %v", folder, err)}
		return result
	}
	for _, entry := range entries {
		if !entry.IsDir() {
			continue
		}
		serviceDir := filepath.Join(folder, entry.Name())
		composeFile := findComposeFile(serviceDir)
		if composeFile == "" {
			continue
		}
		result.Found++
		info, err := processComposeFile(entry.Name(), composeFile)
		if err != nil {
			result.Errors = append(result.Errors, fmt.Sprintf("%s: %v", composeFile, err))
			continue
		}
		created, isNew := upsertService(info)
		if created {
			if isNew {
				result.Created = append(result.Created, info)
			} else {
				result.Updated = append(result.Updated, info)
			}
		}
	}
	return result
}

func findComposeFile(dir string) string {
	for _, name := range composeFileNames {
		p := filepath.Join(dir, name)
		if _, err := os.Stat(p); err == nil {
			return p
		}
	}
	return ""
}

// processComposeFile parses a compose file and produces a ServiceScanInfo.
func processComposeFile(name, composeFile string) (ServiceScanInfo, error) {
	hostPaths, err := extractHostPaths(composeFile)
	if err != nil {
		return ServiceScanInfo{}, err
	}
	return ServiceScanInfo{
		Name:         name,
		ComposeFile:  composeFile,
		BackupFolder: deduceBackupFolder(hostPaths, name),
		Status:       probeServiceStatus(composeFile),
	}, nil
}

// extractHostPaths parses all volume bind-mount host paths from a compose YAML file,
// filtering out system/runtime paths that should not be backed up.
// If yaml.v3 strict parsing fails (e.g. duplicate mapping keys in real-world compose
// files), it falls back to a lenient text-based extraction.
func extractHostPaths(composeFile string) ([]string, error) {
	data, err := os.ReadFile(composeFile)
	if err != nil {
		return nil, err
	}

	var doc map[string]interface{}
	if err := yaml.Unmarshal(data, &doc); err != nil {
		// Malformed YAML (duplicate keys etc.) — fall back to text scan.
		return extractHostPathsFallback(data), nil
	}

	servicesRaw, ok := doc["services"]
	if !ok {
		return nil, nil
	}
	services, _ := servicesRaw.(map[string]interface{})

	seen := map[string]bool{}
	var paths []string

	for _, svcRaw := range services {
		svc, ok := svcRaw.(map[string]interface{})
		if !ok {
			continue
		}
		volumes, _ := svc["volumes"].([]interface{})
		for _, v := range volumes {
			var hostPath string
			switch vv := v.(type) {
			case string:
				hostPath = hostPathFromShortForm(vv)
			case map[string]interface{}:
				if t, _ := vv["type"].(string); t == "bind" {
					hostPath, _ = vv["source"].(string)
				}
			}
			hostPath = filepath.Clean(hostPath)
			if hostPath != "" && hostPath != "." && isBackupablePath(hostPath) && !seen[hostPath] {
				seen[hostPath] = true
				paths = append(paths, hostPath)
			}
		}
	}
	return paths, nil
}

// extractHostPathsFallback uses simple line scanning to extract volume bind-mounts
// from compose files that fail strict YAML parsing (duplicate keys, etc.).
// It looks for list items that contain an absolute host path followed by a colon.
func extractHostPathsFallback(data []byte) []string {
	seen := map[string]bool{}
	var paths []string
	for _, line := range strings.Split(string(data), "\n") {
		trimmed := strings.TrimSpace(line)
		// Volume list entries: "- /abs/path:/container/path" (with or without quotes)
		if !strings.HasPrefix(trimmed, "- ") {
			continue
		}
		spec := strings.Trim(strings.TrimPrefix(trimmed, "- "), "\"' ")
		if !strings.HasPrefix(spec, "/") {
			continue
		}
		hostPath := hostPathFromShortForm(spec)
		if hostPath == "" {
			continue
		}
		hostPath = filepath.Clean(hostPath)
		if isBackupablePath(hostPath) && !seen[hostPath] {
			seen[hostPath] = true
			paths = append(paths, hostPath)
		}
	}
	return paths
}

// hostPathFromShortForm extracts the host path from "host:container[:opts]" volume spec.
func hostPathFromShortForm(spec string) string {
	parts := strings.SplitN(spec, ":", 3)
	if len(parts) < 2 {
		return ""
	}
	src := parts[0]
	if !filepath.IsAbs(src) {
		return "" // named volume or relative path
	}
	return src
}

// isBackupablePath returns false for system/runtime paths that should not be backed up.
func isBackupablePath(path string) bool {
	for _, exact := range excludedExact {
		if path == exact {
			return false
		}
	}
	for _, prefix := range excludedPrefixes {
		if strings.HasPrefix(path, prefix) {
			return false
		}
	}
	return true
}

// deduceBackupFolder finds the most appropriate backup root for a service.
//
// Priority:
//  1. /opt/data/containers/<serviceName>  — standard layout in the examples
//  2. Longest common directory prefix across all collected host paths
//  3. First collected path as-is
func deduceBackupFolder(hostPaths []string, serviceName string) string {
	if len(hostPaths) == 0 {
		return ""
	}
	// 1. Standard layout
	standard := "/opt/data/containers/" + serviceName
	for _, p := range hostPaths {
		if strings.HasPrefix(p, standard) {
			return standard
		}
	}
	// 2. Common prefix
	prefix := commonPathPrefix(hostPaths)
	if prefix != "" && prefix != "/" {
		return prefix
	}
	// 3. Fallback
	return hostPaths[0]
}

// commonPathPrefix returns the longest common directory prefix shared by all paths.
func commonPathPrefix(paths []string) string {
	if len(paths) == 0 {
		return ""
	}
	if len(paths) == 1 {
		return paths[0]
	}
	sep := string(filepath.Separator)
	parts := strings.Split(paths[0], sep)
	for _, p := range paths[1:] {
		other := strings.Split(p, sep)
		limit := len(parts)
		if len(other) < limit {
			limit = len(other)
		}
		i := 0
		for i < limit && parts[i] == other[i] {
			i++
		}
		parts = parts[:i]
	}
	if len(parts) <= 1 {
		return "/"
	}
	return strings.Join(parts, sep)
}

// probeServiceStatus checks whether the service has any running containers.
// Uses the docker project label set by compose, then falls back to compose CLI.
func probeServiceStatus(composeFile string) string {
	projectName := strings.ToLower(filepath.Base(filepath.Dir(composeFile)))

	// Method 1: docker ps with project label (no compose binary required)
	cmd := exec.Command("docker", "ps", "-q",
		"--filter", "label=com.docker.compose.project="+projectName)
	if out, err := cmd.Output(); err == nil {
		if strings.TrimSpace(string(out)) != "" {
			return "up"
		}
		return "down"
	}

	// Method 2: docker compose ps
	cmd = exec.Command("docker", "compose", "-f", composeFile, "ps", "-q")
	if out, err := cmd.Output(); err == nil {
		if strings.TrimSpace(string(out)) != "" {
			return "up"
		}
		return "down"
	}

	// Method 3: legacy docker-compose
	cmd = exec.Command("docker-compose", "-f", composeFile, "ps", "-q")
	if out, err := cmd.Output(); err == nil {
		if strings.TrimSpace(string(out)) != "" {
			return "up"
		}
		return "down"
	}

	return "unknown"
}

// upsertService creates or updates a service entry indexed by compose file path.
// Returns (true, true) = created; (true, false) = updated; (false, _) = no change.
func upsertService(info ServiceScanInfo) (bool, bool) {
	services := GetAllServices()
	for _, svc := range services {
		if svc.ComposeFile != info.ComposeFile {
			continue
		}
		// Found — update status and backup folder if previously unset
		changed := svc.Status != info.Status
		if svc.BackupFolder == "" && info.BackupFolder != "" {
			svc.BackupFolder = info.BackupFolder
			changed = true
		}
		svc.Status = info.Status
		if changed {
			_, _ = UpdateService(svc.ID, svc)
			return true, false
		}
		return false, false
	}
	// Not found — create
	_, _ = AddService(Service{
		Name:         info.Name,
		ComposeFile:  info.ComposeFile,
		BackupFolder: info.BackupFolder,
		Status:       info.Status,
		ComposeType:  "docker compose",
	})
	return true, true
}
