package cmd

import (
	"context"
	"fmt"
	"maps"
	"os"
	"os/signal"
	"regexp"
	"slices"
	"strings"
	"sync"
	"time"

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

	"github.com/tidbcloud/controlplane9/internal/cluster"
	"github.com/tidbcloud/controlplane9/internal/config"
	cp9errors "github.com/tidbcloud/controlplane9/internal/errors"
	cptilt "github.com/tidbcloud/controlplane9/internal/tilt"
)

const (
	statusWatchUntilInterrupt statusWatchStopPolicy = iota
	statusWatchUntilTerminal
)

const (
	statusWatchPollInterval      = 5 * time.Second
	statusWatchHeartbeatInterval = 30 * time.Second
)

type statusWatchStopPolicy int

type statusWatchTarget struct {
	ConfigPath    string
	LocalPath     string
	LocalProvided bool
	Version       string
	SessionName   string
	StateDir      string
	Session       *config.SessionConfig
}

type statusWatchOptions struct {
	StopPolicy      statusWatchStopPolicy
	LifecycleResult <-chan error
	Started         chan<- error
}

type lockedStatusWatchScreen struct {
	screen model.Screen
	mu     sync.Mutex
}

func (s *lockedStatusWatchScreen) Print(text string) error {
	s.mu.Lock()
	defer s.mu.Unlock()
	return s.screen.Print(text)
}

func (s *lockedStatusWatchScreen) Error(text string) error {
	s.mu.Lock()
	defer s.mu.Unlock()
	return s.screen.Error(text)
}

func (s *lockedStatusWatchScreen) OutputtedLines() int {
	s.mu.Lock()
	defer s.mu.Unlock()
	return s.screen.OutputtedLines()
}

type statusWatchSnapshot struct {
	Status            string   `json:"status"`
	Lifecycle         string   `json:"lifecycle,omitempty"`
	LifecycleFailure  string   `json:"lifecycle_failure,omitempty"`
	TiltStatus        string   `json:"tilt_status,omitempty"`
	ReadyResources    int      `json:"ready_resources,omitempty"`
	TotalResources    int      `json:"total_resources,omitempty"`
	GCPStatus         string   `json:"gcp_status,omitempty"`
	GCPTiltStatus     string   `json:"gcp_tilt_status,omitempty"`
	GCPReadyResources int      `json:"gcp_ready_resources,omitempty"`
	GCPTotalResources int      `json:"gcp_total_resources,omitempty"`
	NotReadyResources []string `json:"not_ready_resources,omitempty"`
	FailedResources   []string `json:"failed_resources,omitempty"`
	Endpoint          string   `json:"endpoint,omitempty"`
	TiltURL           string   `json:"tilt_url,omitempty"`
	UIURL             string   `json:"ui_url,omitempty"`
	ObservationError  string   `json:"observation_error,omitempty"`
	ProbeError        string   `json:"probe_error,omitempty"`
	ProbeRetryable    bool     `json:"probe_retryable,omitempty"`

	ResourceFailures map[string]string `json:"-"`
	EvidencePath     string            `json:"-"`
}

type statusWatchAction struct {
	Code         string `json:"code"`
	Command      string `json:"command,omitempty"`
	EvidencePath string `json:"evidence_path,omitempty"`
	Message      string `json:"message,omitempty"`
}

type statusWatchEvent struct {
	Type         string              `json:"type"`
	ObservedAt   time.Time           `json:"observed_at"`
	Session      string              `json:"session,omitempty"`
	State        statusWatchSnapshot `json:"state"`
	Delta        []string            `json:"delta,omitempty"`
	Failures     map[string]string   `json:"failures,omitempty"`
	UnchangedFor string              `json:"unchanged_for,omitempty"`
	Message      string              `json:"message,omitempty"`
	Action       statusWatchAction   `json:"action"`
}

func newStatusWatchEvent(
	eventType string,
	observedAt time.Time,
	target *statusWatchTarget,
	previous, current *statusWatchSnapshot,
	lastChange time.Time,
	lifecycleRunning bool,
) *statusWatchEvent {
	event := &statusWatchEvent{
		Type:       eventType,
		ObservedAt: observedAt,
		Session:    target.SessionName,
		State:      *current,
		Delta:      statusWatchDelta(previous, current),
		Failures:   maps.Clone(current.ResourceFailures),
		Action:     statusWatchNextAction(target, current, lifecycleRunning),
	}
	if eventType == "heartbeat" {
		event.UnchangedFor = observedAt.Sub(lastChange).Round(time.Second).String()
		event.Delta = nil
	}
	if eventType == "baseline" {
		event.Delta = nil
	}
	return event
}

func statusWatchDelta(previous, current *statusWatchSnapshot) []string {
	var delta []string
	if previous.Status != current.Status {
		delta = append(delta, transition("status", previous.Status, current.Status))
	}
	if previous.Lifecycle != current.Lifecycle {
		delta = append(delta, transition("lifecycle", previous.Lifecycle, current.Lifecycle))
	}
	if previous.TiltStatus != current.TiltStatus {
		delta = append(delta, transition("tilt", previous.TiltStatus, current.TiltStatus))
	}
	if previous.GCPStatus != current.GCPStatus {
		delta = append(delta, transition("gcp", previous.GCPStatus, current.GCPStatus))
	}
	if previous.GCPTiltStatus != current.GCPTiltStatus {
		delta = append(delta, transition("gcp_tilt", previous.GCPTiltStatus, current.GCPTiltStatus))
	}
	if previous.GCPReadyResources != current.GCPReadyResources || previous.GCPTotalResources != current.GCPTotalResources {
		delta = append(delta, fmt.Sprintf("gcp_readiness=%d/%d", current.GCPReadyResources, current.GCPTotalResources))
	}
	if previous.ReadyResources != current.ReadyResources || previous.TotalResources != current.TotalResources {
		change := current.ReadyResources - previous.ReadyResources
		delta = append(delta, fmt.Sprintf(
			"readiness=%d/%d (%+d)",
			current.ReadyResources,
			current.TotalResources,
			change,
		))
	}
	delta = append(delta, setDelta("not_ready", previous.NotReadyResources, current.NotReadyResources)...)
	delta = append(delta, setDelta("failed", previous.FailedResources, current.FailedResources)...)
	if !maps.Equal(previous.ResourceFailures, current.ResourceFailures) {
		delta = append(delta, "failure_details_changed")
	}
	if previous.Endpoint != current.Endpoint {
		delta = append(delta, transition("endpoint", previous.Endpoint, current.Endpoint))
	}
	if previous.TiltURL != current.TiltURL {
		delta = append(delta, transition("tilt_url", previous.TiltURL, current.TiltURL))
	}
	if previous.UIURL != current.UIURL {
		delta = append(delta, transition("ui_url", previous.UIURL, current.UIURL))
	}
	if previous.ObservationError != current.ObservationError {
		delta = append(delta, transition("observation_error", previous.ObservationError, current.ObservationError))
	}
	if previous.ProbeError != current.ProbeError {
		delta = append(delta, transition("probe_error", previous.ProbeError, current.ProbeError))
	}
	if previous.LifecycleFailure != current.LifecycleFailure {
		delta = append(delta, transition("lifecycle_failure", previous.LifecycleFailure, current.LifecycleFailure))
	}
	return delta
}

func transition(name, before, after string) string {
	before = firstNonEmptyString(strings.TrimSpace(before), "none")
	after = firstNonEmptyString(strings.TrimSpace(after), "none")
	return fmt.Sprintf("%s=%s->%s", name, before, after)
}

func setDelta(name string, previous, current []string) []string {
	var delta []string
	for _, item := range difference(current, previous) {
		delta = append(delta, name+"+="+item)
	}
	for _, item := range difference(previous, current) {
		delta = append(delta, name+"-="+item)
	}
	return delta
}

func difference(left, right []string) []string {
	var out []string
	for _, item := range left {
		if !slices.Contains(right, item) {
			out = append(out, item)
		}
	}
	slices.Sort(out)
	return out
}

func statusWatchNextAction(
	target *statusWatchTarget,
	snapshot *statusWatchSnapshot,
	lifecycleRunning bool,
) statusWatchAction {
	commandArgs := statusWatchCommandArgs(target)
	upCommand := "cp9 up " + commandArgs
	if snapshot.ProbeRetryable {
		return statusWatchAction{Code: "wait", Message: "remote status probe timed out; watching for the next observation"}
	}

	if len(snapshot.FailedResources) > 0 {
		// cp9 persists no resource-to-service mapping: the Session Tiltfile owns
		// the resource graph, so the evidence file is what there is to point at.
		return statusWatchAction{
			Code:         "inspect_evidence",
			EvidencePath: snapshot.EvidencePath,
			Message:      "inspect the failed resource evidence; no service mapping is available",
		}
	}
	if snapshot.LifecycleFailure != "" {
		if lifecycleRunning {
			return statusWatchAction{Code: "wait", Message: "cp9 up is resuming the persisted lifecycle"}
		}
		return statusWatchAction{
			Code:    "rerun_up",
			Command: upCommand,
			Message: "fix the lifecycle blocker, then rerun the same cp9 up command",
		}
	}
	if snapshot.ObservationError != "" {
		return statusWatchAction{
			Code:         "inspect_evidence",
			EvidencePath: snapshot.EvidencePath,
			Message:      "inspect the closest persisted evidence without guessing a repair",
		}
	}
	if snapshot.ProbeError != "" ||
		snapshot.TiltStatus == "unavailable" ||
		snapshot.TiltStatus == "unreachable" ||
		snapshot.TiltStatus == "stopped" {
		if lifecycleRunning {
			return statusWatchAction{Code: "wait", Message: "cp9 up is reconciling the session-owned runtime"}
		}
		return statusWatchAction{
			Code:    "repair_runtime",
			Command: upCommand,
			Message: "resume the session-owned runtime and tunnel repair path",
		}
	}
	if snapshot.GCPStatus == "runtime_failed" {
		if lifecycleRunning {
			return statusWatchAction{Code: "wait", Message: "cp9 up is reconciling the GCP remote Tilt runtime"}
		}
		return statusWatchAction{
			Code:    "repair_runtime",
			Command: upCommand,
			Message: "resume the GCP remote Tilt runtime without restarting the healthy primary runtime",
		}
	}
	switch snapshot.Status {
	case "ready":
		if lifecycleRunning {
			return statusWatchAction{Code: "wait", Message: "cp9 up is finishing lifecycle reconciliation"}
		}
		return statusWatchAction{Code: "none", Message: "environment is ready"}
	case "no_state":
		if lifecycleRunning {
			return statusWatchAction{Code: "wait", Message: "waiting for the lifecycle to create session state"}
		}
		return statusWatchAction{Code: "run_up", Command: upCommand, Message: "create or resume the environment"}
	case "unknown":
		return statusWatchAction{
			Code:         "inspect_evidence",
			EvidencePath: snapshot.EvidencePath,
			Message:      "inspect the closest persisted evidence without guessing a repair",
		}
	case "failed", "partial":
		if lifecycleRunning {
			return statusWatchAction{Code: "wait", Message: "waiting for the active lifecycle result"}
		}
		return statusWatchAction{
			Code:         "inspect_evidence",
			EvidencePath: snapshot.EvidencePath,
			Message:      "inspect the persisted lifecycle and runtime evidence",
		}
	default:
		return statusWatchAction{Code: "wait", Message: "watching for the next actionable change"}
	}
}

var safeStatusWatchCommandValue = regexp.MustCompile(`^[A-Za-z0-9_./:@+-]+$`)

func statusWatchCommandValue(value string) string {
	if safeStatusWatchCommandValue.MatchString(value) {
		return value
	}
	return shellQuote(value)
}

func statusWatchCommandArgs(target *statusWatchTarget) string {
	args := "config=" + statusWatchCommandValue(target.ConfigPath)
	if target.LocalProvided {
		args += " local=" + statusWatchCommandValue(target.LocalPath)
	}
	return args
}

func (e *statusWatchEvent) FormatText() string {
	if e == nil {
		return ""
	}
	timestamp := e.ObservedAt.Local().Format("15:04:05")
	state := fmt.Sprintf(
		"status=%s lifecycle=%s tilt=%d/%d",
		firstNonEmptyString(e.State.Status, "unknown"),
		firstNonEmptyString(e.State.Lifecycle, "unknown"),
		e.State.ReadyResources,
		e.State.TotalResources,
	)
	if e.State.GCPStatus != "" {
		state += fmt.Sprintf(
			" gcp=%s gcp_tilt=%d/%d",
			e.State.GCPStatus,
			e.State.GCPReadyResources,
			e.State.GCPTotalResources,
		)
	}
	if e.State.Endpoint != "" {
		state += " endpoint=" + e.State.Endpoint
	}
	if e.State.TiltURL != "" {
		state += " tilt_url=" + e.State.TiltURL
	}
	if e.State.UIURL != "" {
		state += " ui=" + e.State.UIURL
	}
	if len(e.State.FailedResources) > 0 {
		resource := e.State.FailedResources[0]
		reason := compactStatusWatchText(e.Failures[resource])
		line := fmt.Sprintf("%s FAILED %s", timestamp, resource)
		if reason != "" {
			line += " reason=" + reason
		}
		switch {
		case e.Action.Command != "":
			return line + "\n       next: " + e.Action.Command + "\n"
		case e.Action.EvidencePath != "":
			return line + "\n       evidence: " + e.Action.EvidencePath + "\n"
		default:
			return line + "\n"
		}
	}
	if e.Action.Code == "resume_watch" {
		return fmt.Sprintf(
			"%s DETACHED environment remains running\n         next: %s\n",
			timestamp,
			e.Action.Command,
		)
	}
	if e.State.Status == "failed" && e.State.LifecycleFailure != "" {
		line := fmt.Sprintf(
			"%s FAILED lifecycle=%s reason=%s",
			timestamp,
			firstNonEmptyString(e.State.Lifecycle, "unknown"),
			compactStatusWatchText(e.State.LifecycleFailure),
		)
		if e.Action.Command != "" {
			line += "\n       next: " + e.Action.Command
		} else {
			line += " action=" + e.Action.Code
		}
		return line + "\n"
	}
	if e.State.ObservationError != "" {
		return fmt.Sprintf(
			"%s OBSERVATION_ERROR %s\n       evidence: %s\n",
			timestamp,
			compactStatusWatchText(e.State.ObservationError),
			firstNonEmptyString(e.Action.EvidencePath, "unavailable"),
		)
	}
	if e.State.ProbeError != "" {
		line := fmt.Sprintf(
			"%s PROBE_ERROR %s",
			timestamp,
			compactStatusWatchText(e.State.ProbeError),
		)
		if e.Action.Command != "" {
			line += "\n       next: " + e.Action.Command
		} else {
			line += " action=" + e.Action.Code
		}
		return line + "\n"
	}
	if e.State.Status == "failed" || e.State.Status == "partial" {
		label := strings.ToUpper(e.State.Status)
		if e.Action.Code == "wait" {
			return fmt.Sprintf("%s %s %s action=wait\n", timestamp, label, state)
		}
		return fmt.Sprintf(
			"%s %s %s\n       evidence: %s\n",
			timestamp,
			label,
			state,
			firstNonEmptyString(e.Action.EvidencePath, "unavailable"),
		)
	}
	switch e.Type {
	case "baseline":
		label := "WATCH "
		if e.State.Status == "ready" {
			label = "READY "
		}
		line := fmt.Sprintf("%s %s %s failed=%d", timestamp, label, state, len(e.State.FailedResources))
		switch {
		case e.Action.Code == "none":
			line += " action=none"
		case e.Action.Command != "" && e.Action.Code != "wait":
			line += "\n       next: " + e.Action.Command
		default:
			line += " action=" + e.Action.Code
		}
		return line + "\n"
	case "heartbeat":
		return fmt.Sprintf(
			"%s HEARTBEAT unchanged %s unchanged_for=%s action=%s\n",
			timestamp,
			state,
			e.UnchangedFor,
			e.Action.Code,
		)
	default:
		label := "CHANGE"
		if e.State.Status == "ready" {
			label = "READY "
		}
		line := fmt.Sprintf("%s %s %s", timestamp, label, state)
		if len(e.Delta) > 0 {
			line += " delta=" + strings.Join(e.Delta, ",")
		}
		switch {
		case e.Action.Code == "none":
			line += " action=none"
		case e.Action.Command != "":
			line += "\n       next: " + e.Action.Command
		case e.Action.EvidencePath != "":
			line += "\n       evidence: " + e.Action.EvidencePath
		default:
			line += " action=" + e.Action.Code
		}
		return line + "\n"
	}
}

func compactStatusWatchText(value string) string {
	return strings.Join(strings.Fields(value), " ")
}

func runStatusWatch(
	ctx context.Context,
	target *statusWatchTarget,
	options statusWatchOptions,
	cc *model.Cli,
	env *model.Env,
) error {
	var previous statusWatchSnapshot
	lastChange := time.Now()
	lastEmit := lastChange
	first := true
	terminalEnabled := options.StopPolicy == statusWatchUntilTerminal && options.LifecycleResult == nil
	lifecycleResult := options.LifecycleResult
	forceEvent := false
	stopAfterFinalEvent := false
	started := false
	reportStarted := func(err error) {
		if started || options.Started == nil {
			return
		}
		options.Started <- err
		started = true
	}
	for {
		_, current, err := observeStatus(ctx, target)
		if ctx.Err() != nil {
			reportStarted(ctx.Err())
			return finishCanceledStatusWatch(target, &previous, lastChange, cc, env)
		}
		observedAt := time.Now()
		if err != nil {
			current = statusWatchSnapshot{
				Status:           "unknown",
				ObservationError: strings.TrimSpace(err.Error()),
				EvidencePath:     target.StateDir,
			}
		}
		eventType := ""
		switch {
		case first:
			eventType = "baseline"
			lastChange = observedAt
			lastEmit = observedAt
		case forceEvent:
			eventType = "change"
			forceEvent = false
			lastChange = observedAt
			lastEmit = observedAt
		case len(statusWatchDelta(&previous, &current)) > 0:
			eventType = "change"
			lastChange = observedAt
			lastEmit = observedAt
		case observedAt.Sub(lastEmit) >= statusWatchHeartbeatInterval:
			eventType = "heartbeat"
			lastEmit = observedAt
		}
		if eventType != "" {
			event := newStatusWatchEvent(
				eventType,
				observedAt,
				target,
				&previous,
				&current,
				lastChange,
				lifecycleResult != nil,
			)
			if err := model.Output(cc, env, event); err != nil {
				reportStarted(err)
				return err
			}
			reportStarted(nil)
			if stopAfterFinalEvent {
				return nil
			}
		}
		previous = current
		first = false
		if !terminalEnabled && lifecycleResult != nil {
			select {
			case lifecycleErr := <-lifecycleResult:
				terminalEnabled = true
				lifecycleResult = nil
				if lifecycleErr != nil {
					stopAfterFinalEvent = true
					forceEvent = true
				} else if isStatusWatchTerminal(current.Status) {
					forceEvent = true
				}
			default:
			}
		}
		if forceEvent {
			continue
		}
		if terminalEnabled {
			switch current.Status {
			case "ready":
				return nil
			case "failed", "partial", "no_state":
				return cp9errors.Newf(
					cp9errors.CategoryResource,
					"cp9 up watch reached status=%s",
					current.Status,
				)
			}
		}

		timer := time.NewTimer(statusWatchPollInterval)
		select {
		case <-ctx.Done():
			if !timer.Stop() {
				<-timer.C
			}
			return finishCanceledStatusWatch(target, &previous, lastChange, cc, env)
		case lifecycleErr := <-lifecycleResult:
			if !timer.Stop() {
				<-timer.C
			}
			terminalEnabled = true
			lifecycleResult = nil
			if lifecycleErr != nil {
				stopAfterFinalEvent = true
				forceEvent = true
			} else if isStatusWatchTerminal(previous.Status) {
				forceEvent = true
			}
		case <-timer.C:
		}
	}
}

func isStatusWatchTerminal(status string) bool {
	switch status {
	case "ready", "failed", "partial", "no_state":
		return true
	default:
		return false
	}
}

func finishCanceledStatusWatch(
	target *statusWatchTarget,
	snapshot *statusWatchSnapshot,
	lastChange time.Time,
	cc *model.Cli,
	env *model.Env,
) error {
	return model.Output(cc, env, newStatusWatchDetachedEvent(time.Now(), target, snapshot, lastChange))
}

func runUpWithStatusWatch(
	target *statusWatchTarget,
	runUp func() error,
	cc *model.Cli,
	env *model.Env,
) error {
	originalScreen := cc.Screen
	cc.Screen = &lockedStatusWatchScreen{screen: originalScreen}
	defer func() {
		cc.Screen = originalScreen
	}()
	watchCtx, cancelWatch := context.WithCancel(context.Background())
	defer cancelWatch()
	lifecycleResult := make(chan error, 1)
	started := make(chan error, 1)
	watchDone := make(chan error, 1)
	go func() {
		watchDone <- runStatusWatch(watchCtx, target, statusWatchOptions{
			StopPolicy:      statusWatchUntilTerminal,
			LifecycleResult: lifecycleResult,
			Started:         started,
		}, cc, env)
	}()

	if err := <-started; err != nil {
		return <-watchDone
	}
	if err := runUp(); err != nil {
		lifecycleResult <- err
		<-watchDone
		return err
	}

	signalCtx, stopSignals := signal.NotifyContext(context.Background(), os.Interrupt)
	defer stopSignals()
	lifecycleResult <- nil
	select {
	case err := <-watchDone:
		return err
	case <-signalCtx.Done():
		cancelWatch()
		return <-watchDone
	}
}

func newStatusWatchDetachedEvent(
	observedAt time.Time,
	target *statusWatchTarget,
	snapshot *statusWatchSnapshot,
	lastChange time.Time,
) *statusWatchEvent {
	detached := newStatusWatchEvent("change", observedAt, target, snapshot, snapshot, lastChange, false)
	detached.Delta = []string{"watch=attached->detached"}
	detached.Message = "watch detached; environment remains running"
	detached.Action = statusWatchAction{
		Code:    "resume_watch",
		Command: "cp9 status --watch " + statusWatchCommandArgs(target),
		Message: "reattach the read-only watcher",
	}
	return detached
}

func resolveStatusWatchTarget(cfgPath, localPath string, localProvided bool) (statusWatchTarget, error) {
	target := statusWatchTarget{
		ConfigPath:    cfgPath,
		LocalPath:     localPath,
		LocalProvided: localProvided,
	}
	version, err := config.DetectAPIVersion(cfgPath)
	if err != nil {
		return target, err
	}
	target.Version = version
	if version == config.APIVersionV1Alpha2 {
		if err := rejectV1Alpha2LocalConfigOverride(localPath, localProvided); err != nil {
			return target, err
		}
		session, err := config.LoadSession(cfgPath)
		if err != nil {
			return target, err
		}
		session, err = config.ResolveSession(session, cfgPath)
		if err != nil {
			return target, err
		}
		target.Session = session
		target.SessionName = session.Name
		target.StateDir = cluster.SessionStateDir(session.Name)
		return target, nil
	}
	cfg, _, err := config.Load(cfgPath, localPath)
	if err != nil {
		return target, err
	}
	target.SessionName = cfg.Name
	target.StateDir = cluster.SessionStateDir(cfg.Name)
	return target, nil
}

func observeStatus(ctx context.Context, target *statusWatchTarget) (any, statusWatchSnapshot, error) {
	exists, err := cluster.StateExists(target.StateDir)
	if err != nil {
		return nil, statusWatchSnapshot{}, cp9errors.Newf(cp9errors.CategoryResource, "check state: %v", err)
	}
	if !exists {
		result := &noClusterResult{}
		if target.Version == config.APIVersionV1Alpha2 {
			result = buildV1Alpha2NoClusterResult(ctx, target.StateDir, target.Session)
		}
		return result, statusWatchSnapshot{
			Status:       "no_state",
			Lifecycle:    "no_state",
			EvidencePath: target.StateDir,
		}, nil
	}

	state, err := cluster.LoadState(target.StateDir)
	if err != nil {
		return nil, statusWatchSnapshot{}, cp9errors.Newf(cp9errors.CategoryResource, "read state: %v", err)
	}
	result := cluster.BuildStatus(state)
	if target.Version == config.APIVersionV1Alpha2 {
		enrichV1Alpha2Status(ctx, result, target.StateDir, target.Session, state)
	}
	return result, statusWatchSnapshotFromResult(result, target.StateDir), nil
}

func statusWatchSnapshotFromResult(result *cluster.StatusResult, stateDir string) statusWatchSnapshot {
	snapshot := statusWatchSnapshot{
		Status:       "unknown",
		EvidencePath: stateDir,
	}
	if result == nil {
		return snapshot
	}
	snapshot.Status = firstNonEmptyString(strings.TrimSpace(result.Status), "unknown")
	snapshot.Lifecycle = firstNonEmptyString(
		strings.TrimSpace(string(result.ResumeAt)),
		strings.TrimSpace(string(result.Lifecycle)),
	)
	for _, step := range result.Steps {
		if step.Status != cluster.StatusFailed && step.Status != cluster.StatusInterrupted {
			continue
		}
		snapshot.Lifecycle = string(step.Name)
		if step.Status == cluster.StatusInterrupted {
			snapshot.LifecycleFailure = "lifecycle step interrupted; rerun the same cp9 up command"
		} else {
			snapshot.LifecycleFailure = firstNonEmptyString(
				strings.TrimSpace(step.Error),
				"lifecycle step failed",
			)
		}
		break
	}
	if result.Tilt != nil {
		snapshot.TiltStatus = result.Tilt.Status
		snapshot.ReadyResources = result.Tilt.ReadyResources
		snapshot.TotalResources = result.Tilt.TotalResources
		snapshot.NotReadyResources = append([]string(nil), result.Tilt.NotReadyResources...)
		snapshot.FailedResources = append([]string(nil), result.Tilt.FailedResources...)
		snapshot.ResourceFailures = maps.Clone(result.Tilt.FailureReasons)
		snapshot.Endpoint = result.Tilt.Endpoint
		snapshot.ProbeError = result.Tilt.ReadinessError
		snapshot.ProbeRetryable = result.Tilt.Retryable
		if strings.TrimSpace(result.Tilt.LogPath) != "" {
			snapshot.EvidencePath = result.Tilt.LogPath
		}
		if strings.TrimSpace(result.Tilt.URL) != "" {
			snapshot.TiltURL = result.Tilt.URL
		}
		slices.Sort(snapshot.NotReadyResources)
		slices.Sort(snapshot.FailedResources)
	}
	if result.Layers != nil && result.Layers.UI != nil {
		if strings.TrimSpace(result.Layers.UI.URL) != "" {
			snapshot.UIURL = result.Layers.UI.URL
		}
		if snapshot.EvidencePath == stateDir && result.Layers.UI.EvidencePath != "" {
			snapshot.EvidencePath = result.Layers.UI.EvidencePath
		}
	}
	if result.Layers != nil && result.Layers.DedicatedGCP != nil {
		gcp := result.Layers.DedicatedGCP
		snapshot.GCPStatus = gcp.Status
		if gcp.Tilt != nil {
			snapshot.GCPTiltStatus = gcp.Tilt.Status
			snapshot.GCPReadyResources = gcp.Tilt.ReadyResources
			snapshot.GCPTotalResources = gcp.Tilt.TotalResources
			for _, name := range gcp.Tilt.NotReadyResources {
				snapshot.NotReadyResources = append(snapshot.NotReadyResources, cptilt.RuntimeGCPRegional+"/"+name)
			}
			for _, name := range gcp.Tilt.FailedResources {
				prefixed := cptilt.RuntimeGCPRegional + "/" + name
				snapshot.FailedResources = append(snapshot.FailedResources, prefixed)
				if snapshot.ResourceFailures == nil {
					snapshot.ResourceFailures = map[string]string{}
				}
				snapshot.ResourceFailures[prefixed] = gcp.Tilt.FailureReasons[name]
			}
		}
	}
	slices.Sort(snapshot.NotReadyResources)
	slices.Sort(snapshot.FailedResources)
	return snapshot
}

func outputOneShotStatus(
	ctx context.Context,
	target *statusWatchTarget,
	cc *model.Cli,
	env *model.Env,
) error {
	result, _, err := observeStatus(ctx, target)
	if err != nil {
		return err
	}
	return model.Output(cc, env, result)
}

func outputStatusWatch(
	target *statusWatchTarget,
	stopPolicy statusWatchStopPolicy,
	cc *model.Cli,
	env *model.Env,
) error {
	ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
	defer stop()
	return runStatusWatch(ctx, target, statusWatchOptions{StopPolicy: stopPolicy}, cc, env)
}
