package agent

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"net/http"
	"strconv"
	"strings"
	"time"

	"github.com/gin-gonic/gin"

	"github.com/tidbcloud/AgentTeam/internal/api"
	"github.com/tidbcloud/AgentTeam/internal/api/dto"
	workflowrun "github.com/tidbcloud/AgentTeam/internal/api/workflow/run"
	storeagent "github.com/tidbcloud/AgentTeam/internal/store/agent"
	"github.com/tidbcloud/AgentTeam/internal/store/storeerr"
	workflowrunstore "github.com/tidbcloud/AgentTeam/internal/store/workflow/run"
	invocationlang "github.com/tidbcloud/AgentTeam/internal/workflow/invocation"
	manage "github.com/tidbcloud/AgentTeam/internal/workflow/invocationmanage"
	workflowpayload "github.com/tidbcloud/AgentTeam/internal/workflow/payload"
	"github.com/tidbcloud/AgentTeam/internal/workflow/publicerr"
	"github.com/tidbcloud/AgentTeam/internal/workflow/runartifact"
)

const (
	agentWorkflowRunMaxEnvelopeBytes = int64(2 << 20)
	agentWorkflowRunMaxInputBytes    = int64(1 << 20)
	agentWorkflowRunMaxJSONNesting   = 64
)

var (
	agentWorkflowRunFollowInterval = 500 * time.Millisecond
	agentWorkflowRunFollowWait     = 20 * time.Second
)

type agentWorkflowRunStartInput struct {
	Action        string          `json:"action"`
	PlanID        string          `json:"planId"`
	PlanVersionID json.RawMessage `json:"planVersionId,omitempty"`
	// TeamID optionally names the Team to freeze for an unlinked/dynamic Run.
	// The server authorizes the invoking Agent's membership; omitting it means
	// an ordinary non-Team Run (no automatic inheritance). A Team-linked Plan
	// gets its Team from the approved, activated Plan/Team binding instead.
	TeamID string          `json:"teamId,omitempty"`
	Input  json.RawMessage `json:"input"`
}

type agentWorkflowRunListInput struct {
	Action string `json:"action"`
	PlanID string `json:"planId,omitempty"`
	Status string `json:"status,omitempty"`
	Limit  *int   `json:"limit,omitempty"`
	Cursor string `json:"cursor,omitempty"`
}

type agentWorkflowRunIDInput struct {
	Action  string `json:"action"`
	RunID   string `json:"runId"`
	Section string `json:"section,omitempty"`
	Path    string `json:"path,omitempty"`
	Cursor  string `json:"cursor,omitempty"`
	Limit   *int   `json:"limit,omitempty"`
}

type agentWorkflowRunEventsInput struct {
	Action   string `json:"action"`
	RunID    string `json:"runId"`
	Section  string `json:"section,omitempty"`
	Path     string `json:"path,omitempty"`
	Sequence *int64 `json:"sequence,omitempty"`
	Tail     *int   `json:"tail,omitempty"`
	Limit    *int   `json:"limit,omitempty"`
	Cursor   string `json:"cursor,omitempty"`
	Follow   bool   `json:"follow,omitempty"`
}

type agentWorkflowRunCancelInput struct {
	Action string `json:"action"`
	RunID  string `json:"runId"`
	Reason string `json:"reason,omitempty"`
}

type agentWorkflowRunHandler struct {
	service    workflowrun.Service
	starts     WorkflowInvocationService
	owners     WorkflowInvocationOwnerResolver
	workspaces WorkflowInvocationWorkspaceResolver
}

func registerAgentWorkflowRunRoutes(
	router gin.IRoutes,
	base string,
	auth AgentAuthenticator,
	service workflowrun.Service,
	starts WorkflowInvocationService,
	owners WorkflowInvocationOwnerResolver,
	workspaces WorkflowInvocationWorkspaceResolver,
	recovery ...WorkflowRecoveryResetter,
) {
	if router == nil || auth == nil || service == nil || strings.TrimSpace(base) == "" {
		return
	}
	handler := &agentWorkflowRunHandler{
		service: service, starts: starts, owners: owners, workspaces: workspaces,
	}
	router.POST(
		strings.TrimSuffix(base, "/")+"/workflow/run",
		RequireAgentRuntimePrincipal(auth),
		api.HandleAgentScoped(
			handler.call,
			api.WithErrorClassifier(classifyAgentWorkflowRunError),
			workflowRecoverySuccessOption(recovery...),
		),
	)
}

func (handler *agentWorkflowRunHandler) call(request api.AgentScopedRequest) (api.Response, error) {
	envelope, err := decodeWorkflowCLIEnvelope(
		request.Gin, agentWorkflowRunMaxEnvelopeBytes, agentWorkflowRunMaxJSONNesting, agentWorkflowRunCLIJSONError,
	)
	if err != nil {
		return api.Response{}, err
	}
	if err := validateWorkflowCLIContext(request, envelope.Context, "Run", "run"); err != nil {
		return api.Response{}, err
	}
	action, err := agentWorkflowRunAction(envelope.Input)
	if err != nil {
		return api.Response{}, err
	}
	workspaceID := strings.TrimSpace(request.Principal.WorkspaceID)
	agentPrincipal := workflowrun.AgentPrincipal{
		WorkspaceID: workspaceID,
		AgentID:     strings.TrimSpace(request.Principal.AgentID),
		MemberID:    strings.TrimSpace(request.Principal.MemberID),
	}

	switch action {
	case "start":
		if handler.starts == nil || handler.owners == nil || handler.workspaces == nil {
			return api.Response{}, publicError(
				http.StatusServiceUnavailable, "run_unavailable", "Workflow Run start is not enabled",
			)
		}
		idempotencyKey, err := verifyWorkflowCLIIdempotency(
			request, envelope.Context, workflowCLIObjectRun, action, "run",
		)
		if err != nil {
			return api.Response{}, err
		}
		input, err := decodeAgentWorkflowRunInput[agentWorkflowRunStartInput](envelope.Input)
		if err != nil {
			return api.Response{}, err
		}
		planID, err := requiredAgentInvocationID("planId", input.PlanID)
		if err != nil {
			return api.Response{}, err
		}
		planVersionID, err := optionalAgentInvocationID("planVersionId", input.PlanVersionID)
		if err != nil {
			return api.Response{}, err
		}
		callerInput, err := requiredAgentInvocationInput(input.Input)
		if err != nil {
			return api.Response{}, err
		}
		actor := invocationlang.Actor{
			Type: invocationlang.ActorTypeAgent, ID: strings.TrimSpace(request.Principal.AgentID),
			MemberID: strings.TrimSpace(request.Principal.MemberID),
		}
		replayed, found, err := handler.starts.ReplayManual(request.Context(), manage.ReplayInput{
			WorkspaceID: workspaceID, PlanID: planID, PlanVersionID: planVersionID, Input: callerInput,
			InvokedBy: actor, IdempotencyKey: idempotencyKey, TeamID: strings.TrimSpace(input.TeamID),
		})
		if err != nil {
			return api.Response{}, err
		}
		if found {
			return agentWorkflowRunStartResponse(replayed, true), nil
		}
		principal, err := handler.resolveStartPrincipal(request.Context(), request.Principal)
		if err != nil {
			return api.Response{}, err
		}
		result, err := handler.starts.InvokeManual(request.Context(), manage.ManualInput{
			WorkspaceID: workspaceID, PlanID: planID, PlanVersionID: planVersionID, Input: callerInput,
			Principal: principal, InvokedBy: actor, IdempotencyKey: idempotencyKey, TeamID: strings.TrimSpace(input.TeamID),
		})
		if err != nil {
			return api.Response{}, err
		}
		return agentWorkflowRunStartResponse(result, result.Replayed), nil

	case "list":
		input, err := decodeAgentWorkflowRunInput[agentWorkflowRunListInput](envelope.Input)
		if err != nil {
			return api.Response{}, err
		}
		limit, err := agentWorkflowRunLimit(input.Limit)
		if err != nil {
			return api.Response{}, err
		}
		agentReader, err := requireAgentWorkflowRunReader(handler.service)
		if err != nil {
			return api.Response{}, err
		}
		page, err := agentReader.ListRunsForAgent(request.Context(), workflowrun.ListRunsInput{
			WorkspaceID: workspaceID, PlanID: strings.TrimSpace(input.PlanID), Status: strings.TrimSpace(input.Status),
			Limit: limit, Cursor: strings.TrimSpace(input.Cursor),
		}, agentPrincipal)
		if err != nil {
			return api.Response{}, err
		}
		items := make([]map[string]any, 0, len(page.Items))
		for _, value := range page.Items {
			items = append(items, agentWorkflowRunResponseForWorkspace(workspaceID, workflowrun.RunSummaryResponse(value)))
		}
		return api.OK(map[string]any{"items": items, "nextCursor": page.NextCursor}), nil

	case "show":
		input, err := decodeAgentWorkflowRunInput[agentWorkflowRunIDInput](envelope.Input)
		if err != nil {
			return api.Response{}, err
		}
		runID, err := requiredAgentWorkflowRunID(input.RunID)
		if err != nil {
			return api.Response{}, err
		}
		agentReader, err := requireAgentWorkflowRunReader(handler.service)
		if err != nil {
			return api.Response{}, err
		}
		value, err := agentReader.GetRunForAgent(request.Context(), workspaceID, runID, agentPrincipal)
		if err != nil {
			return api.Response{}, err
		}
		section := strings.TrimSpace(input.Section)
		if section == "compact" {
			compact := agentWorkflowRunResponseForWorkspace(workspaceID, workflowrun.RunDetailResponse(value))
			for _, key := range []string{"input", "output", "error", "inputRef", "outputRef"} {
				delete(compact, key)
			}
			return api.OK(map[string]any{"run": compact}), nil
		}
		if section != "" && section != "compact" {
			if section != "input" && section != "output" {
				return api.Response{}, publicError(http.StatusBadRequest, "run_invalid_request", "section must be compact, input, or output")
			}
			var raw json.RawMessage
			switch section {
			case "input":
				raw = value.InputJSON
			case "output":
				raw = value.OutputJSON
				if len(bytes.TrimSpace(raw)) == 0 {
					var state struct {
						Output      json.RawMessage `json:"output"`
						Activations []struct {
							Output json.RawMessage `json:"output"`
						} `json:"activations"`
					}
					if json.Unmarshal(value.StateJSON, &state) == nil {
						raw = state.Output
						for index := len(state.Activations) - 1; len(bytes.TrimSpace(raw)) == 0 && index >= 0; index-- {
							raw = state.Activations[index].Output
						}
					}
				}
			}
			limit, limitErr := agentWorkflowPayloadLimit(input.Limit)
			if limitErr != nil {
				return api.Response{}, publicError(http.StatusBadRequest, "run_invalid_request", limitErr.Error())
			}
			if artifactPayload, handled, artifactErr := handler.readRunArtifactPage(request.Context(), agentReader, agentPrincipal, workspaceID, runID, section, input.Path, strings.TrimSpace(input.Cursor), limit, raw, agentWorkflowRunResponseForWorkspace(workspaceID, workflowrun.RunDetailResponse(value))); handled {
				if artifactErr != nil {
					return api.Response{}, agentWorkflowArtifactError(artifactErr)
				}
				return api.OK(artifactPayload), nil
			}
			page, pageErr := workflowPayloadPageForJSONWithSnapshot("run", workspaceID, value.PlanID, "", runID, "", section, input.Path, strings.TrimSpace(input.Cursor), limit, raw, workflowpayload.Snapshot{})
			if pageErr != nil {
				return api.Response{}, agentWorkflowPayloadError("run", pageErr)
			}
			return api.OK(map[string]any{
				"run": agentWorkflowRunResponseForWorkspace(workspaceID, workflowrun.RunDetailResponse(value)),
				"payload": map[string]any{
					"section": page.Section, "encoding": page.Encoding,
					"value": page.Value, "nextCursor": page.NextCursor,
				},
			}), nil
		}
		activations, err := agentReader.ListActivationsForAgent(request.Context(), workflowrun.ListChildrenInput{
			WorkspaceID: workspaceID, RunID: runID, Limit: 100,
		}, agentPrincipal)
		if err != nil {
			return api.Response{}, err
		}
		effects, err := agentReader.ListEffectsForAgent(request.Context(), workflowrun.ListChildrenInput{
			WorkspaceID: workspaceID, RunID: runID, Limit: 100,
		}, agentPrincipal)
		if err != nil {
			return api.Response{}, err
		}
		show := workflowrun.RunShowResponse(value, activations.Items, effects.Items)
		response := map[string]any{"run": agentWorkflowRunResponseForWorkspace(workspaceID, show.Run)}
		if show.CurrentStep != nil {
			response["currentStep"] = show.CurrentStep
		}
		if show.WaitingOn != nil {
			response["waitingOn"] = show.WaitingOn
		}
		return api.OK(response), nil

	case "logs":
		input, err := decodeAgentWorkflowRunInput[agentWorkflowRunEventsInput](envelope.Input)
		if err != nil {
			return api.Response{}, err
		}
		if strings.TrimSpace(input.Section) == "details" {
			return handler.workflowLogDetails(request.Context(), workspaceID, input, agentPrincipal)
		}
		runID, err := requiredAgentWorkflowRunID(input.RunID)
		if err != nil {
			return api.Response{}, err
		}
		limitInput := input.Limit
		if input.Tail != nil {
			limitInput = input.Tail
		}
		limit, err := agentWorkflowRunLimit(limitInput)
		if err != nil {
			return api.Response{}, err
		}
		page, status, err := handler.runLogs(
			request.Context(), workspaceID, runID, limit, strings.TrimSpace(input.Cursor), input.Follow, agentPrincipal,
		)
		if err != nil {
			return api.Response{}, err
		}
		agentReader, err := requireAgentWorkflowRunReader(handler.service)
		if err != nil {
			return api.Response{}, err
		}
		run, err := agentReader.GetRunForAgent(request.Context(), workspaceID, runID, agentPrincipal)
		if err != nil {
			return api.Response{}, err
		}
		items := make([]agentWorkflowRunLogDTO, 0, len(page.Items))
		inlineBytesRemaining := 12 << 10
		for _, value := range page.Items {
			kind := value.Type
			level := "info"
			if strings.Contains(kind, "failed") {
				level = "error"
			} else if strings.Contains(kind, "canceled") {
				level = "warn"
			}
			message := workflowrun.RunLogMessage(kind)
			inline, ref, inlineErr := inlineWorkflowJSONWithSnapshot("run-log", workspaceID, run.PlanID, "", runID, strconv.FormatInt(value.Seq, 10), "details", "", value.PayloadJSON, workflowpayload.Snapshot{})
			if inlineErr != nil {
				return api.Response{}, inlineErr
			}
			if len(inline) > inlineBytesRemaining {
				newRef, refErr := workflowPayloadRefForJSONWithSnapshot("run-log", workspaceID, run.PlanID, "", runID, strconv.FormatInt(value.Seq, 10), "details", "", len(value.PayloadJSON), workflowpayload.Snapshot{
					Digest: workflowpayload.ContentDigest(value.PayloadJSON),
				})
				if refErr != nil {
					return api.Response{}, refErr
				}
				inline = nil
				ref = newRef
			} else {
				inlineBytesRemaining -= len(inline)
			}
			items = append(items, agentWorkflowRunLogDTO{
				Sequence: value.Seq, Time: value.CreatedAt, Level: level,
				Kind: kind, Message: message, Details: inline, DetailsRef: ref,
			})
		}
		return api.OK(agentWorkflowRunLogsDTO{
			Items: items, NextCursor: page.NextCursor, RunStatus: status, Terminal: workflowRunTerminal(status),
		}), nil

	case "cancel":
		input, err := decodeAgentWorkflowRunInput[agentWorkflowRunCancelInput](envelope.Input)
		if err != nil {
			return api.Response{}, err
		}
		runID, err := requiredAgentWorkflowRunID(input.RunID)
		if err != nil {
			return api.Response{}, err
		}
		agentReader, err := requireAgentWorkflowRunReader(handler.service)
		if err != nil {
			return api.Response{}, err
		}
		result, err := agentReader.CancelForAgent(request.Context(), workflowrun.CancelInput{
			WorkspaceID: workspaceID, RunID: runID, Reason: input.Reason,
		}, agentPrincipal)
		if err != nil {
			return api.Response{}, err
		}
		return api.OK(map[string]any{
			"run": agentWorkflowRunResponseForWorkspace(workspaceID, workflowrun.RunDetailResponse(result.Run)), "replayed": result.Replayed,
		}), nil

	default:
		return api.Response{}, publicError(
			http.StatusNotFound, "run_command_not_found", "unknown Workflow Run command action",
		)
	}
}

func (handler *agentWorkflowRunHandler) readRunArtifactPage(ctx context.Context, reader workflowrun.AgentRunReader, principal workflowrun.AgentPrincipal, workspaceID, runID, section, path, cursor string, limit int, raw json.RawMessage, runResponse map[string]any) (map[string]any, bool, error) {
	segments, err := runartifact.PathSegments(path)
	if err != nil {
		return nil, false, err
	}
	ref, artifactPath, marker, err := runartifact.FindMarkerAtPath(raw, segments)
	if err != nil || !marker {
		return nil, marker, err
	}
	if ref.WorkspaceID != workspaceID || ref.RunID != runID {
		return nil, true, runartifact.ErrUnauthorizedReference
	}
	page, err := reader.ReadArtifactPageForAgent(ctx, workflowrun.ArtifactReadInput{
		WorkspaceID: workspaceID, RunID: runID, Reference: ref, Section: section,
		Path: artifactPath, Cursor: cursor, Limit: limit,
	}, principal)
	if err != nil {
		return nil, true, err
	}
	return map[string]any{
		"run":     runResponse,
		"payload": map[string]any{"section": page.Section, "encoding": "json", "value": string(page.Bytes), "nextCursor": page.NextCursor},
	}, true, nil
}

func requireAgentWorkflowRunReader(service workflowrun.Service) (workflowrun.AgentRunReader, error) {
	return requireAgentWorkflowRunReaderFromService(service)
}

func requireAgentWorkflowRunReaderFromService(service workflowrun.Service) (workflowrun.AgentRunReader, error) {
	reader, ok := service.(workflowrun.AgentRunReader)
	if !ok {
		// A production Agent route without the capability is never allowed to
		// fall back to workspace-only reads. Keep the selector non-disclosing.
		return nil, publicError(http.StatusNotFound, "run_not_found", "Workflow Run not found")
	}
	return reader, nil
}

func agentWorkflowArtifactError(err error) error {
	switch {
	case errors.Is(err, storeerr.ErrNotFound), errors.Is(err, storeerr.ErrForbidden):
		return publicError(http.StatusNotFound, "run_not_found", "Workflow Run not found")
	case errors.Is(err, runartifact.ErrUnauthorizedReference), errors.Is(err, runartifact.ErrInvalidReference):
		return publicError(http.StatusForbidden, "run_artifact_forbidden", "workflow artifact reference is not authorized")
	case errors.Is(err, runartifact.ErrStaleReference), errors.Is(err, runartifact.ErrInvalidCursor), errors.Is(err, runartifact.ErrInvalidRead):
		return publicError(http.StatusConflict, "run_artifact_conflict", "workflow artifact reference or cursor is stale")
	default:
		return publicError(http.StatusBadRequest, "run_artifact_invalid", "workflow artifact read is invalid")
	}
}

func agentWorkflowRunResponse(value dto.WorkflowRunResponse) map[string]any {
	return agentWorkflowRunResponseForWorkspace("", value)
}

func agentWorkflowRunResponseForWorkspace(workspaceID string, value dto.WorkflowRunResponse) map[string]any {
	response := map[string]any{
		"id": value.ID, "planId": value.PlanID, "planVersionId": value.PlanVersionID,
		"status": value.Status, "queuedAt": value.QueuedAt,
	}
	optionalString := map[string]string{
		"planKey": value.PlanKey, "planDisplayName": value.PlanDisplayName, "errorCode": value.ErrorCode,
	}
	for key, candidate := range optionalString {
		if strings.TrimSpace(candidate) != "" {
			response[key] = candidate
		}
	}
	optionalJSON := map[string]struct {
		value   json.RawMessage
		section string
	}{
		"input":  {value: value.Input, section: "input"},
		"output": {value: value.Output, section: "output"},
	}
	for key, candidate := range optionalJSON {
		if len(candidate.value) == 0 {
			continue
		}
		inline, ref, err := inlineWorkflowJSONWithSnapshot("run", workspaceID, value.PlanID, "", value.ID, "", candidate.section, "", candidate.value, workflowpayload.Snapshot{})
		if err != nil {
			continue
		}
		if len(inline) > 0 {
			response[key] = inline
		}
		if ref != nil {
			response[key+"Ref"] = ref
		}
	}
	// Operational error JSON retains its existing sensitive boundary: it is
	// never given a continuation ref. Small values remain compatible; an
	// oversized error is omitted rather than made recoverable through Exec.
	if len(value.Error) > 0 && len(value.Error) <= workflowpayload.InlineBytes {
		response["error"] = value.Error
	}
	if value.StartedAt != nil {
		response["startedAt"] = value.StartedAt
	}
	if value.CompletedAt != nil {
		response["completedAt"] = value.CompletedAt
	}
	if value.CanceledAt != nil {
		response["canceledAt"] = value.CanceledAt
	}
	return response
}

type agentWorkflowRunLogDTO struct {
	Sequence   int64               `json:"sequence"`
	Time       time.Time           `json:"time"`
	Level      string              `json:"level"`
	Kind       string              `json:"kind"`
	StepID     string              `json:"stepId,omitempty"`
	Message    string              `json:"message"`
	Details    json.RawMessage     `json:"details,omitempty"`
	DetailsRef *workflowPayloadRef `json:"detailsRef,omitempty"`
}

type agentWorkflowRunLogsDTO struct {
	Items      []agentWorkflowRunLogDTO `json:"items"`
	NextCursor string                   `json:"nextCursor,omitempty"`
	RunStatus  string                   `json:"runStatus"`
	Terminal   bool                     `json:"terminal"`
}

// runLogs implements one bounded long-poll. The desktop CLI repeats these
// requests while terminal is false, avoiding both tight polling and an HTTP
// request that can outlive the client's transport timeout.
func (handler *agentWorkflowRunHandler) workflowLogDetails(
	ctx context.Context,
	workspaceID string,
	input agentWorkflowRunEventsInput,
	principal workflowrun.AgentPrincipal,
) (api.Response, error) {
	if input.Sequence == nil || *input.Sequence < 1 {
		return api.Response{}, publicError(http.StatusBadRequest, "run_invalid_request", "details requires a positive sequence")
	}
	sequence := *input.Sequence
	agentReader, err := requireAgentWorkflowRunReaderFromService(handler.service)
	if err != nil {
		return api.Response{}, err
	}
	event, err := agentReader.GetEventForAgent(ctx, workspaceID, strings.TrimSpace(input.RunID), sequence, principal)
	if err != nil {
		return api.Response{}, err
	}
	run, err := agentReader.GetRunForAgent(ctx, workspaceID, strings.TrimSpace(input.RunID), principal)
	if err != nil {
		return api.Response{}, err
	}
	limit, err := agentWorkflowPayloadLimit(input.Limit)
	if err != nil {
		return api.Response{}, publicError(http.StatusBadRequest, "run_invalid_request", err.Error())
	}
	page, err := workflowPayloadPageForJSONWithSnapshot(
		"run-log", workspaceID, run.PlanID, "", strings.TrimSpace(input.RunID), strconv.FormatInt(sequence, 10),
		"details", input.Path, strings.TrimSpace(input.Cursor), limit, event.PayloadJSON, workflowpayload.Snapshot{},
	)
	if err != nil {
		return api.Response{}, agentWorkflowPayloadError("run", err)
	}
	return api.OK(map[string]any{
		"items": []map[string]any{{
			"sequence": event.Seq, "time": event.CreatedAt, "kind": event.Type,
			"details": page.Value, "encoding": page.Encoding, "cursor": page.Cursor,
			"nextCursor": page.NextCursor, "bytes": page.Bytes, "totalBytes": page.TotalBytes,
			"complete": page.Complete,
		}},
		"runStatus": run.Status, "terminal": workflowRunTerminal(run.Status),
	}), nil
}

func (handler *agentWorkflowRunHandler) runLogs(
	ctx context.Context,
	workspaceID string,
	runID string,
	limit int,
	cursor string,
	follow bool,
	principals ...workflowrun.AgentPrincipal,
) (workflowrun.EventPage, string, error) {
	var principal workflowrun.AgentPrincipal
	if len(principals) > 0 {
		principal = principals[0]
	}
	load := func() (workflowrun.EventPage, string, error) {
		var (
			page workflowrun.EventPage
			run  workflowrunstore.Run
			err  error
		)
		if len(principals) == 0 {
			page, err = handler.service.ListEvents(ctx, workflowrun.ListEventsInput{
				WorkspaceID: workspaceID, RunID: runID, Limit: limit, Cursor: cursor, ContinueCursor: follow,
			})
			if err != nil {
				return workflowrun.EventPage{}, "", err
			}
			run, err = handler.service.GetRun(ctx, workspaceID, runID)
		} else {
			agentReader, readerErr := requireAgentWorkflowRunReaderFromService(handler.service)
			if readerErr != nil {
				return workflowrun.EventPage{}, "", readerErr
			}
			page, err = agentReader.ListEventsForAgent(ctx, workflowrun.ListEventsInput{
				WorkspaceID: workspaceID, RunID: runID, Limit: limit, Cursor: cursor, ContinueCursor: follow,
			}, principal)
			if err != nil {
				return workflowrun.EventPage{}, "", err
			}
			run, err = agentReader.GetRunForAgent(ctx, workspaceID, runID, principal)
		}
		if err != nil {
			return workflowrun.EventPage{}, "", err
		}
		if follow && len(page.Items) == 0 {
			page.NextCursor = cursor
		}
		return page, run.Status, nil
	}

	page, status, err := load()
	if err != nil || !follow || len(page.Items) > 0 || workflowRunTerminal(status) {
		return page, status, err
	}
	deadline := time.NewTimer(agentWorkflowRunFollowWait)
	defer deadline.Stop()
	ticker := time.NewTicker(agentWorkflowRunFollowInterval)
	defer ticker.Stop()
	for {
		select {
		case <-ctx.Done():
			return workflowrun.EventPage{}, "", ctx.Err()
		case <-deadline.C:
			return page, status, nil
		case <-ticker.C:
			page, status, err = load()
			if err != nil || len(page.Items) > 0 || workflowRunTerminal(status) {
				return page, status, err
			}
		}
	}
}

func workflowRunTerminal(status string) bool {
	switch status {
	case workflowrunstore.RunStatusSucceeded, workflowrunstore.RunStatusFailed, workflowrunstore.RunStatusCanceled:
		return true
	default:
		return false
	}
}

type agentWorkflowRunStartReceipt struct {
	RunID         string    `json:"runId"`
	Status        string    `json:"status"`
	PlanID        string    `json:"planId"`
	PlanVersionID string    `json:"planVersionId"`
	AcceptedAt    time.Time `json:"acceptedAt"`
	Replayed      bool      `json:"replayed,omitempty"`
}

func classifyAgentWorkflowRunError(err error) (api.ErrorResponse, bool) {
	// Run-start authorization and acceptance failures originate in the
	// invocation manage domain and keep their invocation stage. Every other
	// failure (strict-JSON, command dispatch, Run service) projects with the
	// run stage.
	if _, _, ok := manage.ErrorDetails(err); ok {
		return classifyWorkflowInvocationError(err)
	}
	var httpError api.HTTPError
	if errors.As(err, &httpError) {
		return workflowrun.ClassifyError(err)
	}
	classified, ok := publicerr.Classify(err, publicerr.Options{
		Stage: publicerr.StageRun, DefaultCode: "run_request_failed", Unredacted: true,
	})
	if !ok {
		return api.ErrorResponse{}, false
	}
	return workflowErrorResponse(classified, nil), true
}

func agentWorkflowRunStartResponse(result invocationlang.AcceptResult, replayed bool) api.Response {
	return api.Response{
		Status: http.StatusCreated,
		Body: agentWorkflowRunStartReceipt{
			RunID: result.Run.ID, Status: result.Run.Status, PlanID: result.Invocation.PlanID,
			PlanVersionID: result.Invocation.PlanVersionID, AcceptedAt: result.Invocation.AcceptedAt,
			Replayed: replayed,
		},
	}
}

func (handler *agentWorkflowRunHandler) resolveStartPrincipal(
	ctx context.Context,
	authenticated storeagent.AuthenticatedAgent,
) (manage.Principal, error) {
	workspaceID := strings.TrimSpace(authenticated.WorkspaceID)
	ownerUserID, err := handler.owners.AgentManagedByUser(ctx, workspaceID, strings.TrimSpace(authenticated.AgentID))
	if err != nil {
		return manage.Principal{}, err
	}
	ownerUserID = strings.TrimSpace(ownerUserID)
	if ownerUserID == "" {
		return manage.Principal{}, agentWorkflowInvocationPrincipalUnavailableError()
	}
	membership, err := handler.workspaces.GetWorkspaceForUser(ctx, ownerUserID, workspaceID)
	if err != nil {
		return manage.Principal{}, err
	}
	if strings.TrimSpace(membership.WorkspaceID) != workspaceID ||
		strings.TrimSpace(membership.MemberUserID) != ownerUserID ||
		strings.TrimSpace(membership.MemberID) == "" || strings.TrimSpace(membership.WorkspaceRole) == "" {
		return manage.Principal{}, agentWorkflowInvocationPrincipalUnavailableError()
	}
	return manage.Principal{
		UserID: ownerUserID, MemberID: strings.TrimSpace(membership.MemberID), Role: strings.TrimSpace(membership.WorkspaceRole),
	}, nil
}

func agentWorkflowRunAction(raw json.RawMessage) (string, error) {
	object, err := decodeWorkflowCLIJSONBytes[map[string]json.RawMessage](
		raw, agentWorkflowRunMaxInputBytes, agentWorkflowRunMaxJSONNesting, agentWorkflowRunCLIJSONError,
	)
	if err != nil {
		return "", err
	}
	var action string
	if err := json.Unmarshal(object["action"], &action); err != nil || strings.TrimSpace(action) == "" || action != strings.TrimSpace(action) {
		return "", publicError(http.StatusBadRequest, "run_invalid_action", "action is required and must be canonical")
	}
	return action, nil
}

func decodeAgentWorkflowRunInput[T any](raw json.RawMessage) (T, error) {
	return decodeWorkflowCLIJSONBytes[T](
		raw, agentWorkflowRunMaxInputBytes, agentWorkflowRunMaxJSONNesting, agentWorkflowRunCLIJSONError,
	)
}

func agentWorkflowRunLimit(raw *int) (int, error) {
	if raw == nil {
		return 0, nil
	}
	if *raw < 1 || *raw > 100 {
		return 0, publicError(http.StatusBadRequest, "run_invalid_request", "limit must be between 1 and 100 when provided")
	}
	return *raw, nil
}

func requiredAgentWorkflowRunID(raw string) (string, error) {
	value := strings.TrimSpace(raw)
	if value == "" {
		return "", publicError(http.StatusBadRequest, "run_invalid_request", "runId is required")
	}
	return value, nil
}

func agentWorkflowRunCLIJSONError(err error) error {
	var maxBytesError *http.MaxBytesError
	if errors.As(err, &maxBytesError) {
		return publicError(http.StatusRequestEntityTooLarge, "run_request_too_large", "invalid request body")
	}
	return publicError(http.StatusBadRequest, "run_invalid_json", "invalid request body")
}
