package workloads

import (
	"context"
	"encoding/json"
	"os/exec"
	"sort"
	"strings"
	"time"
)

// Workload is one container or one systemd unit, agent-side. It mirrors
// models.Workload on the server.
type Workload struct {
	Kind      string
	ID        string
	Name      string
	State     string
	Health    string
	Image     string
	Stack     string
	Ports     []string
	Restarts  int
	StartedAt time.Time
	Protected bool
}

const dockerTimeout = 30 * time.Second

// dockerInspect is the subset of `docker inspect` output we read.
//
// We use inspect rather than `docker ps --format '{{json .}}'` because ps
// reports health and uptime inside a human Status string — "Up 2 hours
// (healthy)" — and anything built on that is parsing English that is
// localised, reworded between releases, and silently different for a paused or
// restarting container. inspect gives typed fields instead.
type dockerInspect struct {
	ID    string `json:"Id"`
	Name  string `json:"Name"`
	State struct {
		Status     string `json:"Status"`
		StartedAt  string `json:"StartedAt"`
		Restarting bool   `json:"Restarting"`
		Health     *struct {
			Status string `json:"Status"`
		} `json:"Health"`
	} `json:"State"`
	Config struct {
		Image  string            `json:"Image"`
		Labels map[string]string `json:"Labels"`
	} `json:"Config"`
	RestartCount    int `json:"RestartCount"`
	NetworkSettings struct {
		Ports map[string][]struct {
			HostIP   string `json:"HostIp"`
			HostPort string `json:"HostPort"`
		} `json:"Ports"`
	} `json:"NetworkSettings"`
}

// collectDocker enumerates containers. It returns ok=false with an empty error
// string when Docker is simply not installed — the common case on this fleet,
// and not a fault.
func collectDocker(ctx context.Context) ([]Workload, bool, string) {
	if _, err := exec.LookPath("docker"); err != nil {
		return nil, false, "" // not installed; not an error
	}

	ctx, cancel := context.WithTimeout(ctx, dockerTimeout)
	defer cancel()

	idsOut, err := exec.CommandContext(ctx, "docker", "ps", "-aq").Output()
	if err != nil {
		// Installed but not answering: a different problem with a different
		// fix, so it carries a message where "not installed" does not.
		return nil, false, "docker ps failed: " + errText(err)
	}

	ids := strings.Fields(string(idsOut))
	if len(ids) == 0 {
		return []Workload{}, true, "" // Docker present, nothing running
	}

	args := append([]string{"inspect", "--format", "{{json .}}"}, ids...)
	out, err := exec.CommandContext(ctx, "docker", args...).Output()
	if err != nil {
		return nil, false, "docker inspect failed: " + errText(err)
	}

	var wls []Workload
	for _, line := range strings.Split(string(out), "\n") {
		line = strings.TrimSpace(line)
		if line == "" {
			continue
		}
		var di dockerInspect
		if err := json.Unmarshal([]byte(line), &di); err != nil {
			continue
		}
		wls = append(wls, dockerToWorkload(di))
	}
	return wls, true, ""
}

func dockerToWorkload(di dockerInspect) Workload {
	w := Workload{
		Kind:     "container",
		ID:       di.ID,
		Name:     strings.TrimPrefix(di.Name, "/"),
		State:    di.State.Status,
		Image:    di.Config.Image,
		Restarts: di.RestartCount,
	}
	if di.State.Health != nil {
		w.Health = strings.ToLower(di.State.Health.Status)
	}
	// The compose project label is what Docker itself treats as authoritative.
	// No YAML is read from disk: a compose file there may not be what is running.
	if v := di.Config.Labels["com.docker.compose.project"]; v != "" {
		w.Stack = v
	}
	if t, err := time.Parse(time.RFC3339Nano, di.State.StartedAt); err == nil {
		w.StartedAt = t
	}
	for container, bindings := range di.NetworkSettings.Ports {
		for _, b := range bindings {
			w.Ports = append(w.Ports, b.HostIP+":"+b.HostPort+"->"+container)
		}
	}
	// Map iteration order is random; sort so a stored snapshot does not reorder
	// its own ports between two otherwise identical reports.
	sort.Strings(w.Ports)
	return w
}

func errText(err error) string {
	if ee, ok := err.(*exec.ExitError); ok && len(ee.Stderr) > 0 {
		return strings.TrimSpace(string(ee.Stderr))
	}
	return err.Error()
}
