package workloads

import (
	"context"
	"errors"
	"fmt"
	"os"
	"os/exec"
	"regexp"
	"strings"
	"time"
)

// ErrProtected is returned for a workload the agent will not act on.
var ErrProtected = errors.New("workload is protected")

// AgentUnit is the systemd unit this agent runs as.
const AgentUnit = "vantage-agent.service"

// controlTimeout bounds a stop that may never finish on its own. `docker stop`
// waits on a container that may ignore SIGTERM, and `systemctl stop` on a unit
// with a long TimeoutStopSec blocks for exactly as long as that says. A
// timeout must return a real error rather than an ack implying success.
const controlTimeout = 90 * time.Second

// ownContainerID is read once: the container this agent runs in, if any.
var ownContainerID = detectOwnContainer()

var cgroupContainerRe = regexp.MustCompile(`[0-9a-f]{64}`)

// detectOwnContainer returns this process's container ID, or "" on a host
// install. The agent is normally a systemd service, so "" is the common case;
// this exists so containerising it later cannot silently remove the guard.
func detectOwnContainer() string {
	b, err := os.ReadFile("/proc/self/cgroup")
	if err != nil {
		return ""
	}
	if m := cgroupContainerRe.FindString(string(b)); m != "" {
		return m
	}
	return ""
}

// isProtected reports whether the agent refuses to act on this workload.
//
// The refusal lives here, in the agent, and not in the control plane. As with
// the console relay hardcoding 127.0.0.1 agent-side: the control plane may name
// a target, but the agent decides what it will do to itself. A server-side
// denylist alone would be bypassed by the next dispatch path someone adds.
func isProtected(kind, id, name string) bool {
	if kind == "unit" {
		return id == AgentUnit || name == strings.TrimSuffix(AgentUnit, ".service")
	}
	if ownContainerID == "" {
		return false
	}
	// Container IDs are commonly abbreviated to 12 characters; compare on the
	// shorter of the two so a short id still matches a full one.
	return strings.HasPrefix(ownContainerID, id) || strings.HasPrefix(id, ownContainerID)
}

// markProtected stamps the flag onto a collected list so the UI can render the
// action disabled with a reason.
func markProtected(wls []Workload) {
	for i := range wls {
		wls[i].Protected = isProtected(wls[i].Kind, wls[i].ID, wls[i].Name)
	}
}

// Control starts, stops or restarts a workload.
func Control(ctx context.Context, kind, id, action string) error {
	switch action {
	case "start", "stop", "restart":
	default:
		return fmt.Errorf("unknown action %q", action)
	}

	// Checked before anything else happens, and checked here rather than only
	// on the server. See isProtected.
	if isProtected(kind, id, strings.TrimSuffix(id, ".service")) {
		return fmt.Errorf("%w: %s", ErrProtected, id)
	}

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

	var cmd *exec.Cmd
	switch kind {
	case "container":
		cmd = exec.CommandContext(ctx, "docker", action, id)
	case "unit":
		cmd = exec.CommandContext(ctx, "systemctl", action, id)
	default:
		return fmt.Errorf("unknown workload kind %q", kind)
	}

	if out, err := cmd.CombinedOutput(); err != nil {
		if ctx.Err() == context.DeadlineExceeded {
			return fmt.Errorf("%s %s timed out after %s", action, id, controlTimeout)
		}
		return fmt.Errorf("%s %s: %s", action, id, strings.TrimSpace(string(out)))
	}
	return nil
}
