package platform

import (
	"context"
	"encoding/json"
	"encoding/xml"
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"time"

	"git.casaderoll.de/michael/urbm/internal/model"
)

type WorkloadManager struct {
	RuntimeDir  string
	FindmntPath string
	LsblkPath   string
	Stat        func(string) (os.FileInfo, error)
}

type Workload struct {
	ID     string `json:"id"`
	Name   string `json:"name"`
	Status string `json:"status"`
}

type WorkloadDiscovery struct {
	Available bool       `json:"available"`
	Message   string     `json:"message,omitempty"`
	Items     []Workload `json:"items"`
}

type Prepared struct {
	Sources     []string
	Stopped     []model.Source
	MetadataDir string
	Kind        model.JobType
}

func (w *WorkloadManager) Discover(ctx context.Context, kind model.JobType) (WorkloadDiscovery, error) {
	switch kind {
	case model.JobDocker:
		if _, err := os.Stat("/var/run/docker.sock"); err != nil {
			return WorkloadDiscovery{Available: false, Message: "Docker is not enabled or is not running on this Unraid server.", Items: []Workload{}}, nil
		}
		output, err := exec.CommandContext(ctx, "docker", "ps", "-a", "--format", "{{json .}}").Output()
		if err != nil {
			return WorkloadDiscovery{}, fmt.Errorf("environment: list Docker containers: %w", err)
		}
		var result []Workload
		for _, line := range strings.Split(strings.TrimSpace(string(output)), "\n") {
			if strings.TrimSpace(line) == "" {
				continue
			}
			var item struct {
				ID     string `json:"ID"`
				Names  string `json:"Names"`
				Status string `json:"Status"`
			}
			if err := json.Unmarshal([]byte(line), &item); err != nil {
				return WorkloadDiscovery{}, fmt.Errorf("environment: decode Docker container list: %w", err)
			}
			result = append(result, Workload{ID: item.Names, Name: item.Names, Status: item.Status})
		}
		message := ""
		if len(result) == 0 {
			message = "Docker is active, but no containers are configured."
		}
		return WorkloadDiscovery{Available: true, Message: message, Items: result}, nil
	case model.JobVM:
		if !unraidServiceEnabled("/boot/config/domain.cfg") {
			return WorkloadDiscovery{Available: false, Message: "The Unraid VM Manager is not enabled. Enable VMs under Settings > VM Manager to select virtual machines.", Items: []Workload{}}, nil
		}
		output, err := exec.CommandContext(ctx, "virsh", "list", "--all", "--name").Output()
		if err != nil {
			return WorkloadDiscovery{}, fmt.Errorf("environment: VM Manager is enabled but virtual machines could not be listed: %w", err)
		}
		var result []Workload
		for _, name := range strings.Split(strings.TrimSpace(string(output)), "\n") {
			name = strings.TrimSpace(name)
			if name == "" {
				continue
			}
			state, _ := exec.CommandContext(ctx, "virsh", "domstate", name).Output()
			result = append(result, Workload{ID: name, Name: name, Status: strings.TrimSpace(string(state))})
		}
		message := ""
		if len(result) == 0 {
			message = "The VM Manager is active, but no virtual machines are configured."
		}
		return WorkloadDiscovery{Available: true, Message: message, Items: result}, nil
	default:
		return WorkloadDiscovery{}, fmt.Errorf("validation: unsupported workload type %q", kind)
	}
}

func unraidServiceEnabled(path string) bool {
	data, err := os.ReadFile(path)
	if err != nil {
		return false
	}
	for _, line := range strings.Split(string(data), "\n") {
		parts := strings.SplitN(strings.TrimSpace(line), "=", 2)
		if len(parts) == 2 && parts[0] == "SERVICE" {
			return strings.Trim(strings.TrimSpace(parts[1]), "\"'") == "enable"
		}
	}
	return false
}

func (w *WorkloadManager) Prepare(ctx context.Context, job model.Job) (Prepared, error) {
	prepared := Prepared{Kind: job.Type}
	for _, source := range job.Sources {
		if source.Path != "" {
			prepared.Sources = append(prepared.Sources, source.Path)
		}
	}
	if job.Type != model.JobDocker && job.Type != model.JobVM {
		return prepared, nil
	}
	metadataDir, err := os.MkdirTemp(w.RuntimeDir, "metadata-"+job.ID+"-")
	if err != nil {
		return prepared, err
	}
	prepared.MetadataDir = metadataDir
	prepared.Sources = append(prepared.Sources, metadataDir)
	if job.Type == model.JobDocker {
		if _, err := os.Stat("/boot/config/plugins/dockerMan/templates-user"); err == nil {
			prepared.Sources = append(prepared.Sources, "/boot/config/plugins/dockerMan/templates-user")
		}
	}
	for _, source := range job.Sources {
		if source.WorkloadID == "" {
			continue
		}
		discovered, err := w.captureMetadata(ctx, job.Type, source.WorkloadID, metadataDir)
		if err != nil {
			w.Cleanup(context.Background(), prepared)
			return Prepared{}, err
		}
		prepared.Sources = append(prepared.Sources, discovered...)
		if job.Consistency.Mode == "stop" {
			if err := w.stop(ctx, job.Type, source.WorkloadID, time.Duration(job.ShutdownSecs)*time.Second); err != nil {
				w.Cleanup(context.Background(), prepared)
				return Prepared{}, err
			}
			prepared.Stopped = append(prepared.Stopped, source)
		}
	}
	prepared.Sources = uniqueStrings(prepared.Sources)
	return prepared, nil
}

func (w *WorkloadManager) FlashDevice(ctx context.Context) (string, error) {
	findmnt := w.FindmntPath
	if findmnt == "" {
		findmnt = "findmnt"
	}
	output, err := exec.CommandContext(ctx, findmnt, "--noheadings", "--output", "SOURCE", "--target", "/boot").Output()
	if err != nil {
		return "", fmt.Errorf("source: determine USB flash partition: %w", err)
	}
	partition := strings.TrimSpace(string(output))
	if index := strings.IndexByte(partition, '['); index >= 0 {
		partition = partition[:index]
	}
	partition = filepath.Clean(partition)
	if !strings.HasPrefix(partition, "/dev/") || strings.ContainsAny(partition, "\r\n\x00") {
		return "", fmt.Errorf("source: /boot is not backed by a device: %q", partition)
	}

	lsblk := w.LsblkPath
	if lsblk == "" {
		lsblk = "lsblk"
	}
	output, err = exec.CommandContext(ctx, lsblk, "--noheadings", "--output", "PKNAME", "--", partition).Output()
	if err != nil {
		return "", fmt.Errorf("source: determine USB flash device for %s: %w", partition, err)
	}
	parent := strings.TrimSpace(string(output))
	device := partition
	if parent != "" {
		if strings.ContainsAny(parent, "/\r\n\x00") {
			return "", fmt.Errorf("source: invalid USB flash parent device %q", parent)
		}
		device = filepath.Join("/dev", parent)
	}
	stat := w.Stat
	if stat == nil {
		stat = os.Stat
	}
	info, err := stat(device)
	if err != nil {
		return "", fmt.Errorf("source: access USB flash device %s: %w", device, err)
	}
	if info.Mode()&os.ModeDevice == 0 {
		return "", fmt.Errorf("source: USB flash source %s is not a block device", device)
	}
	return device, nil
}

func (w *WorkloadManager) Cleanup(ctx context.Context, prepared Prepared) error {
	var first error
	for i := len(prepared.Stopped) - 1; i >= 0; i-- {
		source := prepared.Stopped[i]
		if err := w.start(ctx, prepared.Kind, source.WorkloadID); err != nil && first == nil {
			first = err
		}
	}
	if prepared.MetadataDir != "" {
		_ = os.RemoveAll(prepared.MetadataDir)
	}
	return first
}

func (w *WorkloadManager) captureMetadata(ctx context.Context, kind model.JobType, id, dir string) ([]string, error) {
	var cmd *exec.Cmd
	var name string
	if kind == model.JobDocker {
		cmd, name = exec.CommandContext(ctx, "docker", "inspect", id), "docker-"+safeName(id)+".json"
	} else {
		cmd, name = exec.CommandContext(ctx, "virsh", "dumpxml", id), "vm-"+safeName(id)+".xml"
	}
	output, err := cmd.Output()
	if err != nil {
		return nil, fmt.Errorf("source: capture metadata for %s: %w", id, err)
	}
	if err := os.WriteFile(filepath.Join(dir, name), output, 0600); err != nil {
		return nil, err
	}
	if kind == model.JobDocker {
		var inspected []struct {
			Mounts []struct {
				Type   string `json:"Type"`
				Source string `json:"Source"`
			} `json:"Mounts"`
		}
		if err := json.Unmarshal(output, &inspected); err != nil {
			return nil, fmt.Errorf("source: decode docker metadata: %w", err)
		}
		var paths []string
		if len(inspected) > 0 {
			for _, mount := range inspected[0].Mounts {
				if mount.Type == "bind" && filepath.IsAbs(mount.Source) {
					paths = append(paths, mount.Source)
				}
			}
		}
		return paths, nil
	}
	var domain struct {
		OS struct {
			NVRAM string `xml:"nvram"`
		} `xml:"os"`
		Devices struct {
			Disks []struct {
				Device string `xml:"device,attr"`
				Source struct {
					File string `xml:"file,attr"`
					Dev  string `xml:"dev,attr"`
				} `xml:"source"`
			} `xml:"disk"`
		} `xml:"devices"`
	}
	if err := xml.Unmarshal(output, &domain); err != nil {
		return nil, fmt.Errorf("source: decode vm metadata: %w", err)
	}
	paths := []string{}
	if filepath.IsAbs(strings.TrimSpace(domain.OS.NVRAM)) {
		paths = append(paths, strings.TrimSpace(domain.OS.NVRAM))
	}
	for _, disk := range domain.Devices.Disks {
		if disk.Device != "disk" && disk.Device != "lun" {
			continue
		}
		path := disk.Source.File
		if path == "" {
			path = disk.Source.Dev
		}
		if filepath.IsAbs(path) {
			paths = append(paths, path)
		}
	}
	return paths, nil
}

func (w *WorkloadManager) stop(ctx context.Context, kind model.JobType, id string, timeout time.Duration) error {
	if timeout <= 0 {
		timeout = 120 * time.Second
	}
	if kind == model.JobDocker {
		seconds := fmt.Sprint(int(timeout.Seconds()))
		if err := exec.CommandContext(ctx, "docker", "stop", "--time", seconds, id).Run(); err != nil {
			return fmt.Errorf("source: stop docker %s: %w", id, err)
		}
		return nil
	}
	if err := exec.CommandContext(ctx, "virsh", "shutdown", id).Run(); err != nil {
		return fmt.Errorf("source: shutdown vm %s: %w", id, err)
	}
	deadline := time.Now().Add(timeout)
	for time.Now().Before(deadline) {
		output, _ := exec.CommandContext(ctx, "virsh", "domstate", id).Output()
		if strings.Contains(strings.ToLower(string(output)), "shut off") {
			return nil
		}
		select {
		case <-ctx.Done():
			return ctx.Err()
		case <-time.After(2 * time.Second):
		}
	}
	return fmt.Errorf("source: vm %s did not stop before timeout", id)
}

func (w *WorkloadManager) start(ctx context.Context, kind model.JobType, id string) error {
	if kind == model.JobDocker {
		return exec.CommandContext(ctx, "docker", "start", id).Run()
	}
	return exec.CommandContext(ctx, "virsh", "start", id).Run()
}

func safeName(value string) string {
	value = strings.Map(func(r rune) rune {
		if r >= 'a' && r <= 'z' || r >= 'A' && r <= 'Z' || r >= '0' && r <= '9' || r == '-' || r == '_' {
			return r
		}
		return '_'
	}, value)
	if value == "" {
		return "workload"
	}
	return value
}

func uniqueStrings(values []string) []string {
	seen := make(map[string]struct{}, len(values))
	result := make([]string, 0, len(values))
	for _, value := range values {
		value = filepath.Clean(value)
		if _, exists := seen[value]; exists {
			continue
		}
		seen[value] = struct{}{}
		result = append(result, value)
	}
	return result
}
