package tilt

import (
	"context"
	"errors"
	"fmt"
	"net"
	"os"
	"os/exec"
	"path/filepath"
	"runtime"
	"slices"
	"strconv"
	"strings"
	"time"

	"github.com/tidbcloud/controlplane9/internal/verbose"
)

const (
	defaultStopGracePeriod               = 5 * time.Second
	defaultKubeAPITunnelRepairAttempts   = 3
	defaultKubeAPITunnelRepairBackoff    = 250 * time.Millisecond
	defaultKubeAPITunnelRepairMaxBackoff = 2 * time.Second
)

type ProcessStopper struct {
	FindProcess    func(pid int) (processHandle, error)
	ProcessCommand func(ctx context.Context, pid int) (string, error)
	Sleep          func(time.Duration)
	GracePeriod    time.Duration
}

type KubeAPITunnelRepairer struct {
	ProcessCommand func(ctx context.Context, pid int) (string, error)
	ProcessStopper *ProcessStopper
	StartCommand   func(ctx context.Context, command []string, logPath string) (*StartedProcess, error)
	Now            func() time.Time
	Sleep          func(time.Duration)
	MaxAttempts    int
}

type RuntimeReconciler struct {
	ProcessStopper        *ProcessStopper
	KubeAPITunnelRepairer *KubeAPITunnelRepairer
}

type RuntimeReconcileResult struct {
	RestartRequired bool
	Message         string
}

type processHandle interface {
	Signal(signal os.Signal) error
	Kill() error
}

func NewProcessStopper() *ProcessStopper {
	return &ProcessStopper{
		FindProcess:    findOSProcess,
		ProcessCommand: processCommand,
		Sleep:          time.Sleep,
		GracePeriod:    defaultStopGracePeriod,
	}
}

func StopPersistedProcess(ctx context.Context, statusPath, stateDir string) (string, error) {
	return NewProcessStopper().StopPersistedProcess(ctx, statusPath, stateDir)
}

func EnsureKubeAPITunnel(ctx context.Context, statusPath string) (string, error) {
	return (&KubeAPITunnelRepairer{}).EnsureKubeAPITunnel(ctx, statusPath)
}

// StartSupervisedTunnel starts or replaces one exact-session tunnel role.
func StartSupervisedTunnel(ctx context.Context, command []string, logPath string) (*StartedProcess, error) {
	return startKubeAPITunnelCommand(ctx, command, logPath)
}

func ReconcilePersistedRuntime(ctx context.Context, statusPath, stateDir string) (*RuntimeReconcileResult, error) {
	return (&RuntimeReconciler{}).ReconcilePersistedRuntime(ctx, statusPath, stateDir)
}

func (r *RuntimeReconciler) ReconcilePersistedRuntime(
	ctx context.Context,
	statusPath string,
	stateDir string,
) (*RuntimeReconcileResult, error) {
	status, err := LoadStatus(statusPath)
	if err != nil {
		if errors.Is(err, os.ErrNotExist) {
			return &RuntimeReconcileResult{}, nil
		}
		return nil, err
	}
	if status == nil || status.Mode != "up" || !status.Running || status.PID <= 0 {
		return &RuntimeReconcileResult{}, nil
	}
	stopper := r.ProcessStopper
	if stopper == nil {
		stopper = NewProcessStopper()
	}
	command, err := stopper.processCommand(ctx, status.PID)
	switch {
	case err == nil:
		if !statusMatchesCommand(status, stateDir, command) {
			return nil, fmt.Errorf("persisted Tilt PID %d does not match session command %q", status.PID, command)
		}
		repairer := r.KubeAPITunnelRepairer
		if repairer == nil {
			repairer = &KubeAPITunnelRepairer{}
		}
		message, repairErr := repairer.EnsureKubeAPITunnel(ctx, statusPath)
		if repairErr != nil {
			return nil, repairErr
		}
		return &RuntimeReconcileResult{Message: message}, nil
	case errors.Is(err, os.ErrProcessDone):
		message, stopErr := stopper.StopPersistedProcess(ctx, statusPath, stateDir)
		if stopErr != nil {
			return nil, stopErr
		}
		return &RuntimeReconcileResult{RestartRequired: true, Message: message}, nil
	default:
		return nil, fmt.Errorf("lookup persisted Tilt PID %d: %w", status.PID, err)
	}
}

func (r *KubeAPITunnelRepairer) EnsureKubeAPITunnel(ctx context.Context, statusPath string) (string, error) {
	status, err := LoadStatus(statusPath)
	if err != nil {
		if errors.Is(err, os.ErrNotExist) {
			return "", nil
		}
		return "", err
	}
	if status == nil || status.KubeAPITunnelPID <= 0 {
		return "", nil
	}
	command, err := r.processCommand(ctx, status.KubeAPITunnelPID)
	if err == nil {
		if !kubeAPITunnelMatchesCommand(status, command) {
			repairErr := fmt.Errorf("persisted kube API tunnel PID %d does not match session command %q", status.KubeAPITunnelPID, command)
			return "", r.saveRepairFailure(statusPath, status, "kube-api-tunnel-owner-mismatch", repairErr)
		}
		return "", nil
	}
	if !errors.Is(err, os.ErrProcessDone) {
		repairErr := fmt.Errorf("lookup persisted kube API tunnel PID %d: %w", status.KubeAPITunnelPID, err)
		return "", r.saveRepairFailure(statusPath, status, "kube-api-tunnel-lookup", repairErr)
	}
	if !kubeAPITunnelMatchesCommand(status, strings.Join(status.KubeAPITunnelCommand, " ")) {
		repairErr := fmt.Errorf("persisted kube API tunnel command is not a restartable ssh -L tunnel")
		return "", r.saveRepairFailure(statusPath, status, "kube-api-tunnel-command", repairErr)
	}
	stateDir := stateDirFromStatusPath(statusPath)
	stopper := r.ProcessStopper
	if stopper == nil {
		stopper = NewProcessStopper()
		if r.ProcessCommand != nil {
			stopper.ProcessCommand = r.ProcessCommand
		}
	}
	if _, stopErr := stopper.stopRecordedKubeAPITunnelChild(ctx, status, stateDir); stopErr != nil {
		repairErr := fmt.Errorf("stop recorded kube API tunnel child before repair: %w", stopErr)
		return "", r.saveRepairFailure(statusPath, status, "kube-api-tunnel-orphan-child", repairErr)
	}
	logPath := resolveTunnelLogPath(stateDir, status)
	oldPID := status.KubeAPITunnelPID
	attempts := r.maxAttempts()
	startedAt := r.now()
	status.KubeAPITunnelRepair = &TunnelRepair{
		Status:    "failed",
		OldPID:    oldPID,
		StartedAt: &startedAt,
	}
	var lastErr error
	var started *StartedProcess
	for attempt := 1; attempt <= attempts; attempt++ {
		status.KubeAPITunnelRepair.Attempts = attempt
		started, lastErr = r.startCommand(ctx, status.KubeAPITunnelCommand, logPath)
		if lastErr == nil {
			break
		}
		if attempt < attempts {
			r.sleep(kubeAPITunnelRepairBackoff(attempt))
		}
	}
	completedAt := r.now()
	status.KubeAPITunnelRepair.CompletedAt = &completedAt
	if lastErr != nil {
		status.KubeAPITunnelRepair.Message = lastErr.Error()
		status.KubeAPITunnelRepair.FailureClass = "kube-api-tunnel-restart"
		if saveErr := SaveStatus(statusPath, status); saveErr != nil {
			return "", saveErr
		}
		return "", lastErr
	}
	status.KubeAPITunnelPID = started.PID
	status.KubeAPITunnelCommand = append([]string(nil), started.Command...)
	status.KubeAPITunnelLogPath = SessionRelativePath(stateDir, started.LogPath)
	if strings.TrimSpace(started.ChildPIDPath) != "" {
		status.KubeAPITunnelChildPIDPath = SessionRelativePath(stateDir, started.ChildPIDPath)
	}
	tunnelStartedAt := started.StartedAt.UTC()
	status.KubeAPITunnelStartedAt = &tunnelStartedAt
	status.KubeAPITunnelRepair.Status = "passed"
	status.KubeAPITunnelRepair.NewPID = started.PID
	status.KubeAPITunnelRepair.Message = fmt.Sprintf("repaired kube API tunnel %d -> %d", oldPID, started.PID)
	if err := SaveStatus(statusPath, status); err != nil {
		return "", err
	}
	return status.KubeAPITunnelRepair.Message, nil
}

func (r *KubeAPITunnelRepairer) processCommand(ctx context.Context, pid int) (string, error) {
	if r != nil && r.ProcessCommand != nil {
		return r.ProcessCommand(ctx, pid)
	}
	return processCommand(ctx, pid)
}

func (r *KubeAPITunnelRepairer) startCommand(ctx context.Context, command []string, logPath string) (*StartedProcess, error) {
	if r != nil && r.StartCommand != nil {
		return r.StartCommand(ctx, command, logPath)
	}
	return startKubeAPITunnelCommand(ctx, command, logPath)
}

func (r *KubeAPITunnelRepairer) now() time.Time {
	if r != nil && r.Now != nil {
		return r.Now().UTC()
	}
	return time.Now().UTC()
}

func (r *KubeAPITunnelRepairer) maxAttempts() int {
	if r != nil && r.MaxAttempts > 0 {
		return r.MaxAttempts
	}
	return defaultKubeAPITunnelRepairAttempts
}

func (r *KubeAPITunnelRepairer) sleep(delay time.Duration) {
	if r != nil && r.Sleep != nil {
		r.Sleep(delay)
		return
	}
	time.Sleep(delay)
}

func kubeAPITunnelRepairBackoff(attempt int) time.Duration {
	if attempt < 1 {
		attempt = 1
	}
	delay := defaultKubeAPITunnelRepairBackoff
	for i := 1; i < attempt && delay < defaultKubeAPITunnelRepairMaxBackoff; i++ {
		delay *= 2
		if delay > defaultKubeAPITunnelRepairMaxBackoff {
			delay = defaultKubeAPITunnelRepairMaxBackoff
		}
	}
	return delay
}

func (r *KubeAPITunnelRepairer) saveRepairFailure(statusPath string, status *Status, failureClass string, repairErr error) error {
	now := r.now()
	if status != nil {
		oldPID := status.KubeAPITunnelPID
		status.KubeAPITunnelRepair = &TunnelRepair{
			Status:       "failed",
			Message:      repairErr.Error(),
			FailureClass: failureClass,
			Attempts:     0,
			OldPID:       oldPID,
			StartedAt:    &now,
			CompletedAt:  &now,
		}
		if saveErr := SaveStatus(statusPath, status); saveErr != nil {
			return saveErr
		}
	}
	return repairErr
}

func startKubeAPITunnelCommand(ctx context.Context, command []string, logPath string) (*StartedProcess, error) {
	if len(command) == 0 || strings.TrimSpace(command[0]) == "" {
		return nil, fmt.Errorf("kube API tunnel repair command is empty")
	}
	if strings.TrimSpace(logPath) == "" {
		return nil, fmt.Errorf("kube API tunnel repair log path is empty")
	}
	command, childPIDPath, supervisorStatePath, release, err := prepareTunnelStart(ctx, command)
	if err != nil {
		return nil, fmt.Errorf("prepare kube API tunnel repair command: %w", err)
	}
	defer release()
	if err := os.MkdirAll(filepath.Dir(logPath), 0o755); err != nil {
		return nil, fmt.Errorf("create kube API tunnel repair log dir: %w", err)
	}
	logFile, err := os.Create(logPath)
	if err != nil {
		return nil, fmt.Errorf("create kube API tunnel repair log: %w", err)
	}
	defer logFile.Close()
	if err := ctx.Err(); err != nil {
		return nil, fmt.Errorf("start kube API tunnel repair command: %w", err)
	}
	cmd := exec.Command(
		command[0],
		command[1:]...) // #nosec G204 -- command is persisted from cp9-owned status and validated as ssh -L before restart.
	if childPIDPath != "" {
		cmd.SysProcAttr = tunnelStartSysProcAttr()
	}
	cmd.Stdout = logFile
	cmd.Stderr = logFile
	if err := verbose.StartCommand(cmd); err != nil {
		return nil, fmt.Errorf("start kube API tunnel repair command: %w", err)
	}
	started := time.Now().UTC()
	waitCh := make(chan error, 1)
	go func() {
		waitCh <- cmd.Wait()
	}()
	startedProcess := &StartedProcess{
		PID:          cmd.Process.Pid,
		Command:      append([]string(nil), command...),
		LogPath:      logPath,
		ChildPIDPath: childPIDPath,
		StartedAt:    started,
		Exit:         waitCh,
	}
	if supervisorStatePath != "" {
		if err := waitForTunnelSupervisorIdentity(ctx, supervisorStatePath, startedProcess.PID, waitCh); err != nil {
			StopStartedProcess(startedProcess)
			return nil, err
		}
	}
	if port, ok := kubeAPITunnelLocalForwardPort(command); ok {
		if err := waitForKubeAPITunnelPort(ctx, command, port, waitCh); err != nil {
			StopStartedProcess(startedProcess)
			return nil, err
		}
	}
	return startedProcess, nil
}

func supervisedTunnelChildPIDPath(command []string) string {
	if len(command) < 3 {
		return ""
	}
	switch filepath.Base(strings.TrimSpace(command[0])) {
	case "kube-api-tunnel-supervisor.sh", "controller-tunnel-supervisor.sh":
	default:
		return ""
	}
	return strings.TrimSpace(command[1])
}

func kubeAPITunnelLocalForwardPort(command []string) (int, bool) {
	for i := range command {
		part := strings.TrimSpace(command[i])
		if part == "-L" && i+1 < len(command) {
			return localForwardPort(command[i+1])
		}
		if strings.HasPrefix(part, "-L") && len(part) > len("-L") {
			return localForwardPort(strings.TrimPrefix(part, "-L"))
		}
	}
	return 0, false
}

func localForwardPort(spec string) (int, bool) {
	parts := strings.Split(strings.TrimSpace(spec), ":")
	if len(parts) < 3 {
		return 0, false
	}
	portPart := parts[0]
	if net.ParseIP(strings.Trim(portPart, "[]")) != nil || strings.EqualFold(portPart, "localhost") {
		if len(parts) < 4 {
			return 0, false
		}
		portPart = parts[1]
	}
	port, err := strconv.Atoi(portPart)
	if err != nil || port <= 0 || port > 65535 {
		return 0, false
	}
	return port, true
}

func waitForKubeAPITunnelPort(ctx context.Context, command []string, port int, waitCh <-chan error) error {
	address := net.JoinHostPort("127.0.0.1", strconv.Itoa(port))
	deadline := time.Now().Add(sshCommandStartupTimeout(command))
	dialer := &net.Dialer{Timeout: 250 * time.Millisecond}
	for time.Now().Before(deadline) {
		select {
		case <-ctx.Done():
			return ctx.Err()
		case err := <-waitCh:
			if err != nil {
				return fmt.Errorf("kube API tunnel repair command exited before ready: %w", err)
			}
			return fmt.Errorf("kube API tunnel repair command exited before ready")
		default:
		}
		probeCtx, cancel := context.WithTimeout(ctx, 250*time.Millisecond)
		conn, err := dialer.DialContext(probeCtx, "tcp", address)
		cancel()
		if err == nil {
			_ = conn.Close()
			return nil
		}
		time.Sleep(100 * time.Millisecond)
	}
	return fmt.Errorf("kube API tunnel repair did not become reachable at %s", address)
}

func sshCommandStartupTimeout(command []string) time.Duration {
	for i := 0; i+1 < len(command); i++ {
		if command[i] != "-o" || !strings.HasPrefix(command[i+1], "ConnectTimeout=") {
			continue
		}
		seconds, err := strconv.Atoi(strings.TrimPrefix(command[i+1], "ConnectTimeout="))
		if err == nil && seconds > 0 {
			return time.Duration(seconds) * time.Second
		}
	}
	return 10 * time.Second
}

func stateDirFromStatusPath(statusPath string) string {
	reportsDir := filepath.Dir(statusPath)
	tiltDir := filepath.Dir(reportsDir)
	stateDir := filepath.Dir(tiltDir)
	if strings.TrimSpace(stateDir) == "." {
		return ""
	}
	return stateDir
}

func resolveTunnelLogPath(stateDir string, status *Status) string {
	logPath := ""
	if status != nil {
		logPath = strings.TrimSpace(status.KubeAPITunnelLogPath)
	}
	if logPath == "" {
		return filepath.Join(stateDir, "tilt", "reports", "kube-api-tunnel.log")
	}
	if filepath.IsAbs(logPath) {
		return logPath
	}
	return filepath.Join(stateDir, logPath)
}

func (s *ProcessStopper) StopPersistedProcess(ctx context.Context, statusPath, stateDir string) (string, error) {
	status, err := LoadStatus(statusPath)
	if err != nil {
		if errors.Is(err, os.ErrNotExist) {
			controllerPIDs, controllerErr := stopExactTunnelSupervisorsForRole(
				ctx,
				stateDir,
				"controller-tunnel",
			)
			kubePIDs, kubeErr := stopExactTunnelSupervisorsForRole(ctx, stateDir, "kube-api-tunnel")
			if stopErr := errors.Join(controllerErr, kubeErr); stopErr != nil {
				return "", stopErr
			}
			return fmt.Sprintf(
				"tilt process stop skipped: no persisted Tilt status; exact tunnel supervisors stopped: controller=%v kube-api=%v",
				controllerPIDs,
				kubePIDs,
			), nil
		}
		return "", err
	}
	if status.Controller == ControllerRemote {
		tunnelMessage, tunnelErr := s.stopKubeAPITunnel(ctx, status, stateDir)
		forwardMessage, forwardErr := StopRemoteControllerForward(ctx, statusPath, stateDir)
		if stopErr := errors.Join(tunnelErr, forwardErr); stopErr != nil {
			return "", stopErr
		}
		reloaded, reloadErr := LoadStatus(statusPath)
		if reloadErr == nil {
			status = reloaded
		}
		if err := markStatusStopped(statusPath, status); err != nil {
			return "", err
		}
		return strings.Join(compactStopMessages(
			"remote Tilt controller closeout recorded",
			tunnelMessage,
			forwardMessage,
		), "; "), nil
	}
	if status.PID <= 0 {
		tunnelMsg, err := s.stopKubeAPITunnel(ctx, status, stateDir)
		if err != nil {
			return "", err
		}
		if err := markStatusStopped(statusPath, status); err != nil {
			return "", err
		}
		return strings.Join(compactStopMessages("tilt process stop skipped: no persisted PID", tunnelMsg), "; "), nil
	}
	command, err := s.processCommand(ctx, status.PID)
	if err != nil {
		tunnelMsg, err := s.stopKubeAPITunnel(ctx, status, stateDir)
		if err != nil {
			return "", err
		}
		if err := markStatusStopped(statusPath, status); err != nil {
			return "", err
		}
		return strings.Join(compactStopMessages(fmt.Sprintf("tilt process %d already stopped", status.PID), tunnelMsg), "; "), nil
	}
	if !statusMatchesCommand(status, stateDir, command) {
		return "", fmt.Errorf("persisted Tilt PID %d does not match session command %q", status.PID, command)
	}
	process, err := s.findProcess(status.PID)
	if err != nil {
		if err := markStatusStopped(statusPath, status); err != nil {
			return "", err
		}
		return fmt.Sprintf("tilt process %d already stopped", status.PID), nil
	}
	_ = process.Signal(os.Interrupt)
	if !s.waitUntilStopped(ctx, status.PID) {
		_ = process.Kill()
	}
	pid := status.PID
	tunnelMsg, err := s.stopKubeAPITunnel(ctx, status, stateDir)
	if err != nil {
		return "", err
	}
	if err := markStatusStopped(statusPath, status); err != nil {
		return "", err
	}
	return strings.Join(compactStopMessages(fmt.Sprintf("tilt process %d stopped", pid), tunnelMsg), "; "), nil
}

func (s *ProcessStopper) findProcess(pid int) (processHandle, error) {
	if s != nil && s.FindProcess != nil {
		return s.FindProcess(pid)
	}
	return findOSProcess(pid)
}

func (s *ProcessStopper) processCommand(ctx context.Context, pid int) (string, error) {
	if s != nil && s.ProcessCommand != nil {
		return s.ProcessCommand(ctx, pid)
	}
	return processCommand(ctx, pid)
}

func (s *ProcessStopper) waitUntilStopped(ctx context.Context, pid int) bool {
	grace := defaultStopGracePeriod
	if s != nil && s.GracePeriod > 0 {
		grace = s.GracePeriod
	}
	sleep := time.Sleep
	if s != nil && s.Sleep != nil {
		sleep = s.Sleep
	}
	deadline := time.Now().Add(grace)
	for time.Now().Before(deadline) {
		if _, err := s.processCommand(ctx, pid); err != nil {
			return true
		}
		select {
		case <-ctx.Done():
			return false
		default:
		}
		sleep(100 * time.Millisecond)
	}
	return false
}

func processCommand(ctx context.Context, pid int) (string, error) {
	if pid <= 0 {
		return "", fmt.Errorf("invalid process id %d", pid)
	}
	if runtime.GOOS == "windows" {
		return "", fmt.Errorf("process command lookup is unsupported on Windows")
	}
	// #nosec G204 -- pid is validated as positive and passed as one ps argument, not shell-interpreted input.
	cmd := exec.CommandContext(ctx, "ps", "-p", strconv.Itoa(pid), "-o", "command=")
	output, err := verbose.RunCommand(cmd)
	if err != nil {
		var exitErr *exec.ExitError
		if errors.As(err, &exitErr) && strings.TrimSpace(string(output)) == "" {
			return "", fmt.Errorf("%w: process %d not found", os.ErrProcessDone, pid)
		}
		return "", err
	}
	command := strings.TrimSpace(string(output))
	if command == "" {
		return "", fmt.Errorf("%w: process %d not found", os.ErrProcessDone, pid)
	}
	return command, nil
}

func findOSProcess(pid int) (processHandle, error) {
	return os.FindProcess(pid)
}

func statusMatchesCommand(status *Status, stateDir, command string) bool {
	if status == nil {
		return false
	}
	command = strings.TrimSpace(command)
	if command == "" || !strings.Contains(command, "tilt") {
		return false
	}
	tiltfile := strings.TrimSpace(status.TiltfilePath)
	if tiltfile == "" {
		return true
	}
	return strings.Contains(command, tiltfile) ||
		strings.Contains(command, SessionAbsolutePath(stateDir, tiltfile))
}

func markStatusStopped(path string, status *Status) error {
	status.Running = false
	status.PID = 0
	status.StartedAt = nil
	status.KubeAPITunnelPID = 0
	status.KubeAPITunnelCommand = nil
	status.KubeAPITunnelLogPath = ""
	status.KubeAPITunnelChildPIDPath = ""
	status.KubeAPITunnelStartedAt = nil
	status.ControllerTunnelPID = 0
	status.ControllerTunnelCommand = nil
	status.ControllerTunnelLogPath = ""
	status.ControllerTunnelChildPID = ""
	status.ControllerTunnelStartedAt = nil
	return SaveStatus(path, status)
}

func (s *ProcessStopper) stopKubeAPITunnel(ctx context.Context, status *Status, stateDir string) (string, error) {
	if status == nil {
		return "", nil
	}
	childMessage, err := s.stopRecordedKubeAPITunnelChild(ctx, status, stateDir)
	if err != nil {
		return "", err
	}
	pids, err := stopExactTunnelSupervisorsForRole(ctx, stateDir, "kube-api-tunnel")
	if err != nil {
		return "", err
	}
	stopMessage := ""
	if len(pids) > 0 {
		stopMessage = fmt.Sprintf("kube API tunnels stopped: %v", pids)
	}
	return strings.Join(compactStopMessages(stopMessage, childMessage), "; "), nil
}

func (s *ProcessStopper) stopRecordedKubeAPITunnelChild(
	ctx context.Context,
	status *Status,
	stateDir string,
) (string, error) {
	if status == nil {
		return "", nil
	}
	return s.stopRecordedTunnelChild(
		ctx,
		stateDir,
		status.KubeAPITunnelChildPIDPath,
		"kube API tunnel",
		func(command string) bool {
			return kubeAPITunnelChildMatchesCommand(status, command)
		},
	)
}

func (s *ProcessStopper) stopRecordedTunnelChild(
	ctx context.Context,
	stateDir string,
	childPIDPath string,
	label string,
	matches func(string) bool,
) (string, error) {
	path := strings.TrimSpace(childPIDPath)
	if path == "" {
		return "", nil
	}
	if !filepath.IsAbs(path) {
		path = filepath.Join(stateDir, path)
	}
	data, err := os.ReadFile(path)
	if err != nil {
		if errors.Is(err, os.ErrNotExist) {
			return "", nil
		}
		return "", fmt.Errorf("read persisted %s child PID %s: %w", label, path, err)
	}
	childPID, err := strconv.Atoi(strings.TrimSpace(string(data)))
	if err != nil || childPID <= 0 {
		return "", fmt.Errorf("parse persisted %s child PID %q", label, strings.TrimSpace(string(data)))
	}
	command, err := s.processCommand(ctx, childPID)
	if err != nil {
		if errors.Is(err, os.ErrProcessDone) {
			_ = os.Remove(path)
			return fmt.Sprintf("%s child %d already stopped", label, childPID), nil
		}
		return "", fmt.Errorf("lookup persisted %s child PID %d: %w", label, childPID, err)
	}
	if !matches(command) {
		return "", fmt.Errorf("persisted %s child PID %d does not match session command %q", label, childPID, command)
	}
	process, err := s.findProcess(childPID)
	if err != nil {
		return "", fmt.Errorf("find persisted %s child PID %d: %w", label, childPID, err)
	}
	_ = process.Signal(os.Interrupt)
	if !s.waitUntilStopped(ctx, childPID) {
		_ = process.Kill()
		if !s.waitUntilStopped(ctx, childPID) {
			return "", fmt.Errorf("persisted %s child PID %d did not stop", label, childPID)
		}
	}
	_ = os.Remove(path)
	return fmt.Sprintf("%s child %d stopped", label, childPID), nil
}

func kubeAPITunnelMatchesCommand(status *Status, command string) bool {
	if status == nil {
		return false
	}
	return tunnelMatchesCommand(status.KubeAPITunnelCommand, command)
}

func tunnelMatchesCommand(persisted []string, command string) bool {
	if len(persisted) == 0 {
		return false
	}
	command = strings.TrimSpace(command)
	if !strings.Contains(command, "ssh") || !strings.Contains(command, "-L") {
		return false
	}
	checkedParts := 0
	for _, part := range persisted {
		part = strings.TrimSpace(part)
		if part == "" || part == "ssh" {
			continue
		}
		checkedParts++
		if !strings.Contains(command, part) {
			return false
		}
	}
	return checkedParts > 0
}

func kubeAPITunnelChildMatchesCommand(status *Status, command string) bool {
	if status == nil {
		return false
	}
	return tunnelChildMatchesCommand(status.KubeAPITunnelCommand, command)
}

func tunnelChildMatchesCommand(persisted []string, command string) bool {
	if len(persisted) == 0 {
		return false
	}
	command = strings.TrimSpace(command)
	if !strings.Contains(command, "ssh") || !strings.Contains(command, "-L") {
		return false
	}
	sshIndex := slices.Index(persisted, "ssh")
	if sshIndex < 0 {
		return tunnelMatchesCommand(persisted, command)
	}
	checkedParts := 0
	for _, part := range persisted[sshIndex+1:] {
		part = strings.TrimSpace(part)
		if part == "" {
			continue
		}
		checkedParts++
		if !strings.Contains(command, part) {
			return false
		}
	}
	return checkedParts > 0
}

func compactStopMessages(values ...string) []string {
	out := make([]string, 0, len(values))
	for _, value := range values {
		value = strings.TrimSpace(value)
		if value != "" {
			out = append(out, value)
		}
	}
	return out
}
