package cmd

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net"
	"net/http"
	"os"
	"os/exec"
	"os/signal"
	"path/filepath"
	"runtime"
	"strconv"
	"strings"
	"time"

	"github.com/innerr/ticat/pkg/core/model"

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

const (
	starterDashboardAccessAPIVersion = "cp9.starter.dashboard-access/v1alpha1"
	starterDashboardAccessKind       = "StarterDashboardAccess"
	starterDashboardStateFileName    = "starter-dashboard-access.json"
	starterDashboardLogFileName      = "starter-dashboard-port-forward.log"
	starterDashboardInterruptGrace   = 100 * time.Millisecond
)

type starterDashboardAccessState struct {
	APIVersion        string    `json:"apiVersion"`
	Kind              string    `json:"kind"`
	Session           string    `json:"session"`
	PID               int       `json:"pid"`
	ProcessStartToken string    `json:"process_start_token"`
	ServiceUID        string    `json:"service_uid"`
	Address           string    `json:"address"`
	Port              int       `json:"port"`
	URL               string    `json:"url"`
	Kubeconfig        string    `json:"kubeconfig"`
	Context           string    `json:"context"`
	StartedAt         time.Time `json:"started_at"`
}

type starterDashboardResult struct {
	State      string `json:"state"`
	Session    string `json:"session"`
	Address    string `json:"address,omitempty"`
	Port       int    `json:"port,omitempty"`
	URL        string `json:"url,omitempty"`
	PID        int    `json:"pid,omitempty"`
	ServiceUID string `json:"service_uid,omitempty"`
	StatePath  string `json:"state_path,omitempty"`
	Message    string `json:"message"`
}

func (r *starterDashboardResult) String() string {
	if r == nil {
		return "starter dashboard: <nil>"
	}
	if r.URL != "" {
		return fmt.Sprintf("starter dashboard %s: %s\nurl: %s\npid: %d", r.State, r.Message, r.URL, r.PID)
	}
	return fmt.Sprintf("starter dashboard %s: %s", r.State, r.Message)
}

type starterDashboardReadyCallback func(*starterDashboardResult) error
type starterDashboardStatusFunc func(context.Context, string) (*starterStatusResult, error)
type starterDashboardServiceUIDFunc func(context.Context, starterKubeAccess) (string, error)

var (
	starterDashboardCommand                                          = exec.Command
	starterDashboardProbe                                            = probeStarterDashboard
	starterDashboardOpenBrowser                                      = openStarterDashboardBrowser
	starterDashboardStatusRunner      starterDashboardStatusFunc     = runStarterStatus
	starterDashboardServiceUID        starterDashboardServiceUIDFunc = readStarterDashboardServiceUID
	starterDashboardProcessStartToken                                = defaultStarterDashboardProcessStartToken
	starterDashboardProcessRunning                                   = day2ProcessRunning
	starterDashboardProcessMatches                                   = defaultStarterDashboardProcessMatches
	starterDashboardPortAvailable                                    = localTCPPortAvailable
	starterDashboardPlatform                                         = runtime.GOOS
	starterDashboardHTTPClient                                       = verbose.NewHTTPClient(2 * time.Second)
	starterDashboardSignalProcess                                    = func(pid int) error {
		process, err := os.FindProcess(pid)
		if err != nil {
			return err
		}
		return process.Signal(os.Interrupt)
	}
	starterDashboardKillProcess = func(pid int) error {
		process, err := os.FindProcess(pid)
		if err != nil {
			return err
		}
		return process.Kill()
	}
	starterDashboardReadyTimeout = 15 * time.Second
	starterDashboardPollInterval = 100 * time.Millisecond
	starterDashboardStopTimeout  = 3 * time.Second
)

func starterDashboardCmd() model.NormalCmd {
	return func(args model.ArgVals, cc *model.Cli, env *model.Env, _ []model.ParsedCmd) error {
		syncVerbose(env)
		port, err := strconv.Atoi(strings.TrimSpace(args.GetRaw("local-port")))
		if err != nil {
			return cp9errors.Newf(cp9errors.CategoryConfig, "starter dashboard local-port must be an integer: %v", err)
		}
		ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
		defer stop()
		result, runErr := runStarterDashboard(ctx, args.GetRaw("config"), port, func(ready *starterDashboardResult) error {
			return model.Output(cc, env, ready)
		})
		if result != nil {
			if outputErr := model.Output(cc, env, result); outputErr != nil {
				return outputErr
			}
		}
		return runErr
	}
}

func runStarterDashboard(
	ctx context.Context,
	cfgPath string,
	localPort int,
	onReady starterDashboardReadyCallback,
) (*starterDashboardResult, error) {
	resolved, selected, err := resolveDevFullSession(cfgPath, "cp9 starter")
	if err != nil {
		return nil, err
	}
	if !starterObservabilityEnabled(resolved) {
		return nil, cp9errors.New(cp9errors.CategoryConfig,
			"cp9 starter dashboard requires starter.observability.enabled: true; enable it and run cp9 up or cp9 starter activate first")
	}
	if localPort < 1 || localPort > 65535 {
		return nil, cp9errors.New(cp9errors.CategoryConfig, "starter dashboard local-port must be between 1 and 65535")
	}

	activation := starterActivationSession(resolved)
	if matches, matchErr := starterRemoteEKSArtifactMatches(selected.StateDir, activation); matchErr != nil {
		return nil, matchErr
	} else if !matches {
		return nil, cp9errors.New(cp9errors.CategoryResource, "Starter remote EKS is not active; run cp9 up or cp9 starter activate first")
	}
	remote, err := loadStarterRemoteEKSArtifact(selected.StateDir)
	if err != nil {
		return nil, err
	}
	kubeconfig, err := filepath.Abs(filepath.Join(selected.StateDir, "remote-eks", "kubeconfig.yaml"))
	if err != nil {
		return nil, fmt.Errorf("resolve Starter remote EKS kubeconfig: %w", err)
	}
	if _, err := os.Stat(kubeconfig); err != nil {
		return nil, cp9errors.Newf(cp9errors.CategoryResource, "Starter remote EKS kubeconfig is unavailable: %v", err)
	}
	kubeContext := firstNonEmpty(remote.KubeconfigContext, remote.ClusterName)
	if err := reconcileStarterDashboardAccessState(selected.StateDir, resolved.Name, kubeconfig, kubeContext); err != nil {
		return nil, err
	}

	status, err := starterDashboardStatusRunner(ctx, cfgPath)
	if err != nil {
		return nil, fmt.Errorf("verify Starter observability readiness: %w", err)
	}
	if status == nil || status.State != "ready" {
		message := "Starter observability is not ready; run cp9 starter status"
		if status != nil && strings.TrimSpace(status.Message) != "" {
			message = status.Message
		}
		return nil, cp9errors.New(cp9errors.CategoryResource, message)
	}
	if !starterDashboardPortAvailable("127.0.0.1", localPort) {
		return nil, cp9errors.Newf(cp9errors.CategoryResource,
			"Starter dashboard localhost port %d is already in use; choose a different local-port", localPort)
	}

	execEnv := starterAWSExecEnv(resolved.Target.AWS.IAM.Profile, firstNonEmpty(remote.Region, resolved.Target.Region))
	access := starterKubeAccess{ExecEnv: execEnv, Kubeconfig: kubeconfig, KubeContext: kubeContext}
	serviceUID, err := starterDashboardServiceUID(ctx, access)
	if err != nil {
		return nil, err
	}
	url := fmt.Sprintf("http://127.0.0.1:%d", localPort)
	statePath := starterDashboardAccessStatePath(selected.StateDir)
	evidenceDir := filepath.Dir(statePath)
	if err := os.MkdirAll(evidenceDir, 0o700); err != nil {
		return nil, fmt.Errorf("create Starter dashboard evidence directory: %w", err)
	}
	logPath := filepath.Join(evidenceDir, starterDashboardLogFileName)
	logFile, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o600)
	if err != nil {
		return nil, fmt.Errorf("open Starter dashboard port-forward log: %w", err)
	}
	cmd := starterDashboardCommand(
		"kubectl",
		"--kubeconfig", kubeconfig,
		"--context", kubeContext,
		"--namespace", starterObservabilityNamespace,
		"port-forward",
		"--address=127.0.0.1",
		"service/grafana",
		fmt.Sprintf("%d:3000", localPort),
	)
	cmd.Env = append(os.Environ(), execEnv...)
	cmd.Stdin = nil
	cmd.Stdout = logFile
	cmd.Stderr = logFile
	if err := verbose.StartCommand(cmd); err != nil {
		_ = logFile.Close()
		return nil, fmt.Errorf("start Starter dashboard localhost port-forward: %w", err)
	}
	waitResult := make(chan error, 1)
	go func() { waitResult <- cmd.Wait() }()
	pid := cmd.Process.Pid
	processStartToken, err := starterDashboardProcessStartToken(pid)
	if err != nil {
		stopErr := forceStopStarterDashboardProcess(pid, waitResult)
		_ = logFile.Close()
		return nil, errors.Join(fmt.Errorf("read Starter dashboard process start token: %w", err), stopErr)
	}

	ready, earlyExitErr := waitStarterDashboardReady(ctx, url+"/api/health", waitResult)
	if !ready {
		stopErr := forceStopStarterDashboardProcess(pid, waitResult)
		_ = logFile.Close()
		if earlyExitErr != nil {
			return nil, errors.Join(
				fmt.Errorf("starter dashboard port-forward exited before readiness: %w%s", earlyExitErr, starterDashboardLogTail(logPath)),
				stopErr,
			)
		}
		if ctx.Err() != nil {
			return nil, errors.Join(ctx.Err(), stopErr)
		}
		return nil, errors.Join(
			fmt.Errorf("starter dashboard port-forward did not become ready%s", starterDashboardLogTail(logPath)),
			stopErr,
		)
	}

	state := &starterDashboardAccessState{
		APIVersion:        starterDashboardAccessAPIVersion,
		Kind:              starterDashboardAccessKind,
		Session:           resolved.Name,
		PID:               pid,
		ProcessStartToken: processStartToken,
		ServiceUID:        serviceUID,
		Address:           "127.0.0.1",
		Port:              localPort,
		URL:               url,
		Kubeconfig:        kubeconfig,
		Context:           kubeContext,
		StartedAt:         time.Now().UTC(),
	}
	if err := saveStarterDashboardAccessState(selected.StateDir, state); err != nil {
		stopErr := forceStopStarterDashboardProcess(pid, waitResult)
		_ = logFile.Close()
		return nil, errors.Join(err, stopErr)
	}

	readyResult := starterDashboardResultFromState(state, statePath, "ready")
	if starterDashboardPlatform == "darwin" {
		if err := starterDashboardOpenBrowser(url); err != nil {
			_, _ = stopStarterDashboardForeground(selected.StateDir, state, waitResult)
			_ = logFile.Close()
			return nil, fmt.Errorf("open Starter dashboard browser: %w", err)
		}
		readyResult.Message = "session-owned Grafana access is ready and the browser was opened; keep this command running and press Ctrl-C to close"
	} else {
		readyResult.Message = "session-owned Grafana access is ready; open the printed URL, keep this command running, and press Ctrl-C to close"
	}
	if onReady != nil {
		if err := onReady(readyResult); err != nil {
			_, _ = stopStarterDashboardForeground(selected.StateDir, state, waitResult)
			_ = logFile.Close()
			return nil, err
		}
	}

	select {
	case processErr := <-waitResult:
		_ = logFile.Close()
		_ = removeStarterDashboardAccessState(selected.StateDir, state)
		if ctx.Err() != nil || (processErr != nil && starterDashboardContextCanceledWithin(ctx, starterDashboardInterruptGrace)) {
			return starterDashboardResultFromState(state, statePath, "closed"), nil
		}
		if processErr != nil {
			return nil, fmt.Errorf("starter dashboard port-forward exited: %w%s", processErr, starterDashboardLogTail(logPath))
		}
		return starterDashboardResultFromState(state, statePath, "closed"), nil
	case <-ctx.Done():
		message, stopErr := stopStarterDashboardForeground(selected.StateDir, state, waitResult)
		_ = logFile.Close()
		result := starterDashboardResultFromState(state, statePath, "closed")
		result.Message = firstNonEmpty(message, "Starter dashboard access closed")
		return result, stopErr
	}
}

func starterDashboardContextCanceledWithin(ctx context.Context, grace time.Duration) bool {
	if ctx.Err() != nil {
		return true
	}
	if grace <= 0 {
		return false
	}
	timer := time.NewTimer(grace)
	defer timer.Stop()
	select {
	case <-ctx.Done():
		return true
	case <-timer.C:
		return false
	}
}

func starterDashboardWaitResultWithin(waitResult <-chan error, grace time.Duration) bool {
	if grace <= 0 {
		select {
		case <-waitResult:
			return true
		default:
			return false
		}
	}
	timer := time.NewTimer(grace)
	defer timer.Stop()
	select {
	case <-waitResult:
		return true
	case <-timer.C:
		return false
	}
}

func forceStopStarterDashboardProcess(pid int, waitResult <-chan error) error {
	if !starterDashboardProcessRunning(pid) {
		return nil
	}
	if err := starterDashboardKillProcess(pid); err != nil && starterDashboardProcessRunning(pid) {
		return fmt.Errorf("kill Starter dashboard port-forward PID %d: %w", pid, err)
	}
	timer := time.NewTimer(starterDashboardStopTimeout)
	defer timer.Stop()
	select {
	case <-waitResult:
		return nil
	case <-timer.C:
		if starterDashboardProcessRunning(pid) {
			return fmt.Errorf("starter dashboard port-forward PID %d did not exit after kill", pid)
		}
		return nil
	}
}

func waitStarterDashboardReady(ctx context.Context, target string, waitResult <-chan error) (bool, error) {
	deadline := time.NewTimer(starterDashboardReadyTimeout)
	defer deadline.Stop()
	ticker := time.NewTicker(starterDashboardPollInterval)
	defer ticker.Stop()
	for {
		probeCtx, cancel := context.WithTimeout(ctx, time.Second)
		probeErr := starterDashboardProbe(probeCtx, target)
		cancel()
		if probeErr == nil {
			return true, nil
		}
		select {
		case err := <-waitResult:
			return false, err
		case <-ctx.Done():
			return false, nil
		case <-deadline.C:
			return false, nil
		case <-ticker.C:
		}
	}
}

func stopStarterDashboardForeground(
	stateDir string,
	state *starterDashboardAccessState,
	waitResult <-chan error,
) (string, error) {
	if state == nil {
		return "", errors.New("starter dashboard state is nil")
	}
	if starterDashboardWaitResultWithin(waitResult, starterDashboardInterruptGrace) {
		if err := removeStarterDashboardAccessState(stateDir, state); err != nil {
			return "", err
		}
		return fmt.Sprintf("Starter dashboard port-forward PID %d stopped", state.PID), nil
	}
	if starterDashboardProcessRunning(state.PID) {
		if !starterDashboardProcessMatches(state.PID, state) {
			return "", cp9errors.Newf(cp9errors.CategoryResource,
				"refusing to signal PID %d because it does not match the session-owned Starter dashboard forward", state.PID)
		}
		_ = starterDashboardSignalProcess(state.PID)
		timer := time.NewTimer(starterDashboardStopTimeout)
		select {
		case <-waitResult:
			timer.Stop()
		case <-timer.C:
			if err := starterDashboardKillProcess(state.PID); err != nil && starterDashboardProcessRunning(state.PID) {
				return "", fmt.Errorf("stop Starter dashboard port-forward PID %d: %w", state.PID, err)
			}
			select {
			case <-waitResult:
			case <-time.After(starterDashboardStopTimeout):
				if starterDashboardProcessRunning(state.PID) {
					return "", fmt.Errorf("starter dashboard port-forward PID %d did not exit", state.PID)
				}
			}
		}
	}
	if err := removeStarterDashboardAccessState(stateDir, state); err != nil {
		return "", err
	}
	return fmt.Sprintf("Starter dashboard port-forward PID %d stopped", state.PID), nil
}

func starterDashboardResultFromState(state *starterDashboardAccessState, statePath, resultState string) *starterDashboardResult {
	message := "session-owned Grafana localhost access is ready"
	if resultState == "closed" {
		message = "session-owned Grafana localhost access closed"
	}
	return &starterDashboardResult{
		State:      resultState,
		Session:    state.Session,
		Address:    state.Address,
		Port:       state.Port,
		URL:        state.URL,
		PID:        state.PID,
		ServiceUID: state.ServiceUID,
		StatePath:  statePath,
		Message:    message,
	}
}

func starterDashboardAccessStatePath(stateDir string) string {
	return filepath.Join(stateDir, "reports", "evidence", starterDashboardStateFileName)
}

func loadStarterDashboardAccessState(stateDir string) (*starterDashboardAccessState, error) {
	path := starterDashboardAccessStatePath(stateDir)
	data, err := os.ReadFile(path)
	if err != nil {
		return nil, err
	}
	var state starterDashboardAccessState
	if err := json.Unmarshal(data, &state); err != nil {
		return nil, fmt.Errorf("parse Starter dashboard state: %w", err)
	}
	return &state, nil
}

func saveStarterDashboardAccessState(stateDir string, state *starterDashboardAccessState) error {
	if state == nil {
		return errors.New("starter dashboard state is nil")
	}
	path := starterDashboardAccessStatePath(stateDir)
	if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
		return fmt.Errorf("create Starter dashboard state directory: %w", err)
	}
	data, err := json.MarshalIndent(state, "", "  ")
	if err != nil {
		return fmt.Errorf("marshal Starter dashboard state: %w", err)
	}
	data = append(data, '\n')
	tmp := path + ".tmp"
	if err := os.WriteFile(tmp, data, 0o600); err != nil {
		return fmt.Errorf("write Starter dashboard state: %w", err)
	}
	if err := os.Chmod(tmp, 0o600); err != nil {
		_ = os.Remove(tmp)
		return fmt.Errorf("protect Starter dashboard state: %w", err)
	}
	if err := os.Rename(tmp, path); err != nil {
		_ = os.Remove(tmp)
		return fmt.Errorf("publish Starter dashboard state: %w", err)
	}
	return nil
}

func validateStarterDashboardAccessState(state *starterDashboardAccessState, session, kubeconfig, kubeContext string) error {
	if state == nil || state.APIVersion != starterDashboardAccessAPIVersion || state.Kind != starterDashboardAccessKind {
		return cp9errors.New(cp9errors.CategoryResource, "persisted Starter dashboard state has an unsupported contract")
	}
	if state.Session != session || state.Address != "127.0.0.1" || state.Kubeconfig != kubeconfig || state.Context != kubeContext ||
		state.PID <= 0 || strings.TrimSpace(state.ProcessStartToken) == "" || strings.TrimSpace(state.ServiceUID) == "" ||
		state.Port <= 0 || state.Port > 65535 || state.URL != fmt.Sprintf("http://127.0.0.1:%d", state.Port) || state.StartedAt.IsZero() {
		return cp9errors.New(cp9errors.CategoryResource, "persisted Starter dashboard state does not match this session's localhost access contract")
	}
	return nil
}

func reconcileStarterDashboardAccessState(stateDir, session, kubeconfig, kubeContext string) error {
	state, err := loadStarterDashboardAccessState(stateDir)
	if errors.Is(err, os.ErrNotExist) {
		return nil
	}
	if err != nil {
		return err
	}
	if err := validateStarterDashboardAccessState(state, session, kubeconfig, kubeContext); err != nil {
		return err
	}
	if starterDashboardProcessRunning(state.PID) {
		if !starterDashboardProcessMatches(state.PID, state) {
			return cp9errors.Newf(cp9errors.CategoryResource,
				"refusing to replace PID %d because it does not match the session-owned Starter dashboard forward", state.PID)
		}
		return cp9errors.Newf(cp9errors.CategoryResource,
			"Starter dashboard is already active on localhost port %d; return to its foreground command and press Ctrl-C", state.Port)
	}
	return removeStarterDashboardAccessState(stateDir, state)
}

func removeStarterDashboardAccessState(stateDir string, expected *starterDashboardAccessState) error {
	path := starterDashboardAccessStatePath(stateDir)
	current, err := loadStarterDashboardAccessState(stateDir)
	if errors.Is(err, os.ErrNotExist) {
		return nil
	}
	if err != nil {
		return err
	}
	if expected != nil &&
		(current.PID != expected.PID || current.ProcessStartToken != expected.ProcessStartToken || current.Session != expected.Session) {
		return cp9errors.New(cp9errors.CategoryResource, "refusing to remove Starter dashboard state because its process identity changed")
	}
	if err := os.Remove(path); err != nil && !errors.Is(err, os.ErrNotExist) {
		return fmt.Errorf("remove Starter dashboard state: %w", err)
	}
	return nil
}

func stopStarterDashboardAccess(stateDir, session string) (string, error) {
	state, err := loadStarterDashboardAccessState(stateDir)
	if errors.Is(err, os.ErrNotExist) {
		return "", nil
	}
	if err != nil {
		return "", err
	}
	expectedKubeconfig, absErr := filepath.Abs(filepath.Join(stateDir, "remote-eks", "kubeconfig.yaml"))
	if absErr != nil {
		return "", absErr
	}
	expectedContext := state.Context
	if remote, remoteErr := loadStarterRemoteEKSArtifact(stateDir); remoteErr == nil {
		expectedContext = firstNonEmpty(remote.KubeconfigContext, remote.ClusterName)
	} else if !errors.Is(remoteErr, os.ErrNotExist) {
		return "", remoteErr
	}
	if err := validateStarterDashboardAccessState(state, session, expectedKubeconfig, expectedContext); err != nil {
		return "", err
	}
	if starterDashboardProcessRunning(state.PID) {
		if !starterDashboardProcessMatches(state.PID, state) {
			return "", cp9errors.Newf(cp9errors.CategoryResource,
				"refusing to kill PID %d because it does not match the session-owned Starter dashboard forward", state.PID)
		}
		if err := starterDashboardKillProcess(state.PID); err != nil && starterDashboardProcessRunning(state.PID) {
			return "", fmt.Errorf("stop Starter dashboard port-forward PID %d: %w", state.PID, err)
		}
		if err := waitStarterDashboardPIDExit(state.PID); err != nil {
			return "", err
		}
	}
	if err := removeStarterDashboardAccessState(stateDir, state); err != nil {
		return "", err
	}
	return fmt.Sprintf("Starter dashboard port-forward PID %d stopped", state.PID), nil
}

func waitStarterDashboardPIDExit(pid int) error {
	deadline := time.NewTimer(starterDashboardStopTimeout)
	defer deadline.Stop()
	ticker := time.NewTicker(starterDashboardPollInterval)
	defer ticker.Stop()
	for starterDashboardProcessRunning(pid) {
		select {
		case <-deadline.C:
			return fmt.Errorf("starter dashboard port-forward PID %d did not exit after kill", pid)
		case <-ticker.C:
		}
	}
	return nil
}

func starterDashboardAccessStatusCheck(
	ctx context.Context,
	input starterLiveCheckInput,
	access starterKubeAccess,
) starterStatusCheck {
	check := starterStatusCheck{
		Name:    "starter-dashboard-access",
		Status:  "passed",
		Message: "dashboard access is inactive; run cp9 starter dashboard when local access is needed",
	}
	state, err := loadStarterDashboardAccessState(input.StateDir)
	if errors.Is(err, os.ErrNotExist) {
		return check
	}
	if err != nil {
		check.Status = "not_ready"
		check.Message = "dashboard_access_stale: " + err.Error()
		return check
	}
	session := ""
	if input.Session != nil {
		session = input.Session.Name
	}
	absKubeconfig, absErr := filepath.Abs(access.Kubeconfig)
	if absErr != nil {
		check.Status = "not_ready"
		check.Message = "dashboard_access_stale: " + absErr.Error()
		return check
	}
	if err := validateStarterDashboardAccessState(state, session, absKubeconfig, access.KubeContext); err != nil {
		check.Status = "not_ready"
		check.Message = "dashboard_access_stale: " + err.Error()
		return check
	}
	if !starterDashboardProcessRunning(state.PID) || !starterDashboardProcessMatches(state.PID, state) {
		check.Status = "not_ready"
		check.Message = fmt.Sprintf("dashboard_access_stale: PID %d is absent or its process identity changed", state.PID)
		return check
	}
	access.Kubeconfig = absKubeconfig
	serviceUID, err := starterDashboardServiceUID(ctx, access)
	if err != nil || serviceUID != state.ServiceUID {
		check.Status = "not_ready"
		if err != nil {
			check.Message = "dashboard_access_stale: " + err.Error()
		} else {
			check.Message = fmt.Sprintf("dashboard_access_stale: Grafana Service UID changed from %s to %s", state.ServiceUID, serviceUID)
		}
		return check
	}
	check.Message = fmt.Sprintf("dashboard access is active on %s with PID %d and Grafana Service UID %s", state.URL, state.PID, state.ServiceUID)
	return check
}

func readStarterDashboardServiceUID(
	ctx context.Context,
	access starterKubeAccess,
) (string, error) {
	output, err := access.run(ctx,
		"--kubeconfig", access.Kubeconfig,
		"--context", access.KubeContext,
		"--namespace", starterObservabilityNamespace,
		"get", "service/grafana",
		"-o", "jsonpath={.metadata.uid}",
	)
	if err != nil {
		return "", fmt.Errorf("read Starter Grafana Service UID: %s", trimmedCommandError("kubectl", output, err))
	}
	uid := strings.TrimSpace(string(output))
	if uid == "" {
		return "", cp9errors.New(cp9errors.CategoryResource, "Starter Grafana Service UID is empty")
	}
	return uid, nil
}

func localTCPPortAvailable(address string, port int) bool {
	listener, err := net.Listen("tcp", net.JoinHostPort(address, strconv.Itoa(port)))
	if err != nil {
		return false
	}
	_ = listener.Close()
	return true
}

func defaultStarterDashboardProcessStartToken(pid int) (string, error) {
	if pid <= 0 {
		return "", fmt.Errorf("invalid PID %d", pid)
	}
	//nolint:gosec // pid is validated positive and passed as one decimal ps argument.
	output, err := verbose.RunCommand(exec.Command("ps", "-p", strconv.Itoa(pid), "-o", "lstart="))
	if err != nil {
		return "", err
	}
	token := strings.Join(strings.Fields(string(output)), " ")
	if token == "" {
		return "", fmt.Errorf("pid %d has no process start token", pid)
	}
	return token, nil
}

func defaultStarterDashboardProcessMatches(pid int, state *starterDashboardAccessState) bool {
	if pid <= 0 || state == nil {
		return false
	}
	startToken, err := starterDashboardProcessStartToken(pid)
	if err != nil || startToken != state.ProcessStartToken {
		return false
	}
	//nolint:gosec // pid is validated positive and passed as one decimal ps argument.
	output, err := verbose.RunCommand(exec.Command("ps", "-p", strconv.Itoa(pid), "-o", "command="))
	if err != nil {
		return false
	}
	command := string(output)
	for _, fragment := range []string{
		"kubectl",
		state.Kubeconfig,
		state.Context,
		"port-forward",
		"--address=127.0.0.1",
		"service/grafana",
		fmt.Sprintf("%d:3000", state.Port),
	} {
		if !strings.Contains(command, fragment) {
			return false
		}
	}
	return true
}

func probeStarterDashboard(ctx context.Context, target string) error {
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, target, http.NoBody)
	if err != nil {
		return err
	}
	response, err := starterDashboardHTTPClient.Do(req)
	if err != nil {
		return err
	}
	defer response.Body.Close()
	if response.StatusCode < 200 || response.StatusCode >= 300 {
		_, _ = io.Copy(io.Discard, io.LimitReader(response.Body, 4<<10))
		return fmt.Errorf("grafana health returned HTTP %d", response.StatusCode)
	}
	return nil
}

func openStarterDashboardBrowser(target string) error {
	if runtime.GOOS != "darwin" {
		return nil
	}
	cmd := exec.Command("open", target)
	_, err := verbose.RunCommand(cmd)
	return err
}

func starterDashboardLogTail(path string) string {
	data, err := os.ReadFile(path)
	if err != nil || len(data) == 0 {
		return ""
	}
	const limit = 4096
	if len(data) > limit {
		data = data[len(data)-limit:]
	}
	return ": " + strings.TrimSpace(string(data))
}
