package planmanage

import (
	"context"
	"errors"
	"fmt"
	"strings"
	"time"

	"github.com/tidbcloud/AgentTeam/internal/core/identifiers"
	"github.com/tidbcloud/AgentTeam/internal/coredb"
	"github.com/tidbcloud/AgentTeam/internal/store/agent"
	"github.com/tidbcloud/AgentTeam/internal/store/agentops"
	"github.com/tidbcloud/AgentTeam/internal/store/app"
	"github.com/tidbcloud/AgentTeam/internal/store/message"
	"github.com/tidbcloud/AgentTeam/internal/store/storeerr"
	planstore "github.com/tidbcloud/AgentTeam/internal/store/workflow/plan"
	"github.com/tidbcloud/AgentTeam/internal/workflow/plan"
)

const (
	DefaultMessageLimit = 30
	MaxMessageLimit     = 50
	MaxInstructionBytes = 16 * 1024
	MaxClientMessageID  = 128
)

var (
	ErrAgentWakeActive           = errors.New("workflow Agent wake is active")
	ErrAgentEditRevisionMismatch = errors.New("workflow Agent edit revision does not match the request baseline")
)

type AgentEditService struct {
	db       coredb.DB
	plans    *planstore.Store
	messages *message.Store
	agents   *app.Store
	ops      *agentops.Store
}

func NewAgentEditService(db coredb.DB, plans *planstore.Store, messages *message.Store, agents *app.Store, ops *agentops.Store) *AgentEditService {
	return &AgentEditService{db: db, plans: plans, messages: messages, agents: agents, ops: ops}
}

type MessagePage struct {
	Messages   []message.AppMessage
	Agents     []AgentCandidate
	Wake       *WakeState
	HasMore    bool
	NextCursor int64
}

type AgentCandidate struct {
	ID           string
	Name         string
	DisplayName  string
	Description  string
	AvatarURL    string
	SearchValues []string
}

type AgentPage struct {
	Agents     []AgentCandidate
	NextCursor string
}

type Target struct {
	Kind     string
	ID       string
	NodeType string
}

type RequestInput struct {
	WorkspaceID       string
	PlanID            string
	UserID            string
	MemberID          string
	MemberDisplayName string
	AgentID           string
	ClientMessageID   string
	DraftRevision     int64
	Instruction       string
	Target            Target
}

type RequestResult struct {
	Message           message.AppMessage
	Target            Target
	Agent             AgentCandidate
	BaseDraftRevision int64
	Wake              WakeState
	Inserted          bool
}

type WakeState struct {
	MessageID         string
	AgentID           string
	BaseDraftRevision int64
	Status            string
	Detail            string
	Timestamp         time.Time
	Active            bool
}

func (service *AgentEditService) ListMessages(ctx context.Context, workspaceID, planID string, beforeSeq int64, limit int) (MessagePage, error) {
	if err := service.validate(); err != nil {
		return MessagePage{}, err
	}
	if limit <= 0 {
		limit = DefaultMessageLimit
	}
	if limit > MaxMessageLimit {
		return MessagePage{}, storeerr.BadRequest("limit must not exceed 50")
	}
	plan, err := service.plans.LoadPlan(ctx, workspaceID, planID)
	if err != nil {
		return MessagePage{}, err
	}
	page, err := service.messages.ListWorkflowScopeMessages(ctx, workspaceID, plan.ScopeID, beforeSeq, limit)
	if err != nil {
		return MessagePage{}, mapMessageError(err)
	}
	agentIDs := make([]string, 0, len(page.Messages))
	for _, item := range page.Messages {
		edit, ok := item.Metadata[message.WorkflowAgentEditMetadataKey].(map[string]any)
		if !ok {
			continue
		}
		if agentID, ok := edit["responderAgentId"].(string); ok {
			agentIDs = append(agentIDs, strings.TrimSpace(agentID))
		}
	}
	profiles, err := service.agents.LoadWorkflowAgentEditProfiles(ctx, workspaceID, agentIDs)
	if err != nil {
		return MessagePage{}, err
	}
	agents := make([]AgentCandidate, 0, len(profiles))
	for _, agentID := range agentIDs {
		profile, exists := profiles[agentID]
		if !exists {
			continue
		}
		agents = append(agents, AgentCandidate{
			ID: profile.ID, Name: profile.Name, DisplayName: profile.DisplayName,
			AvatarURL: profile.AvatarURL,
		})
		delete(profiles, agentID)
	}
	var wake *WakeState
	if beforeSeq == 0 {
		wake, err = service.loadLatestWakeState(
			ctx, workspaceID, plan.ID, plan.ScopeID,
		)
		if err != nil {
			return MessagePage{}, err
		}
	}
	return MessagePage{
		Messages: page.Messages, Agents: agents,
		Wake:    wake,
		HasMore: page.HasMore, NextCursor: page.NextCursor,
	}, nil
}

func (service *AgentEditService) ListAgents(ctx context.Context, userID, workspaceID, planID, query, cursor string, limit int) (AgentPage, error) {
	if err := service.validate(); err != nil {
		return AgentPage{}, err
	}
	plan, err := service.plans.LoadPlan(ctx, workspaceID, planID)
	if err != nil {
		return AgentPage{}, err
	}
	if plan.State != planstore.PlanStateActive {
		return AgentPage{}, storeerr.Conflict("workflow plan is archived")
	}
	if limit <= 0 {
		limit = MaxMessageLimit
	}
	if limit > MaxMessageLimit {
		return AgentPage{}, storeerr.BadRequest("limit must not exceed 50")
	}
	page, err := service.agents.ListAgentDirectoryForWorkspace(ctx, userID, workspaceID, agent.AgentDirectoryOptions{
		Scope: "owned", Limit: limit, Cursor: strings.TrimSpace(cursor), Query: strings.TrimSpace(query),
	})
	if err != nil {
		return AgentPage{}, err
	}
	items := make([]AgentCandidate, 0, len(page.Agents))
	for _, item := range page.Agents {
		items = append(items, AgentCandidate{
			ID: item.ID, Name: item.Name, DisplayName: item.DisplayName, AvatarURL: item.AvatarURL,
			SearchValues: nonEmptyStrings(item.Name, item.DisplayName),
		})
	}
	return AgentPage{Agents: items, NextCursor: page.NextCursor}, nil
}

func (service *AgentEditService) Request(ctx context.Context, input RequestInput) (RequestResult, error) {
	if err := service.validate(); err != nil {
		return RequestResult{}, err
	}
	normalized, err := normalizeRequest(input)
	if err != nil {
		return RequestResult{}, err
	}
	tx, err := service.db.BeginTx(ctx, coredb.TxOptions{})
	if err != nil {
		return RequestResult{}, fmt.Errorf("begin workflow Agent edit request: %w", err)
	}
	defer func() { _ = tx.Rollback(ctx) }()

	result, err := service.requestWorkflowEditTx(ctx, tx, normalized)
	if err != nil {
		return RequestResult{}, err
	}
	if err := tx.Commit(ctx); err != nil {
		return RequestResult{}, fmt.Errorf("commit Workflow Agent edit request: %w", err)
	}
	return result, nil
}

func (service *AgentEditService) requestWorkflowEditTx(
	ctx context.Context,
	tx coredb.Tx,
	input RequestInput,
) (RequestResult, error) {
	plan, err := service.plans.InTx(tx).LoadPlanForUpdate(ctx, input.WorkspaceID, input.PlanID)
	if err != nil {
		return RequestResult{}, err
	}
	if strings.TrimSpace(plan.ScopeID) == "" {
		return RequestResult{}, storeerr.Conflict("workflow plan scope is unavailable")
	}
	if err := message.RequireWorkflowScope(ctx, tx, input.WorkspaceID, plan.ScopeID); err != nil {
		return RequestResult{}, mapMessageError(err)
	}

	if replay, found, err := message.FindWorkflowScopeMessageByClientID(
		ctx, tx, plan.ScopeID, input.MemberID, input.ClientMessageID,
	); err != nil {
		return RequestResult{}, mapMessageError(err)
	} else if found {
		return service.replayWorkflowEditRequest(ctx, tx, input, replay)
	}

	if plan.State != planstore.PlanStateActive {
		return RequestResult{}, storeerr.Conflict("workflow plan is archived")
	}
	if latest, found, err := message.LoadLatestWorkflowEditRequestForUpdate(ctx, tx, input.WorkspaceID, plan.ScopeID); err != nil {
		return RequestResult{}, err
	} else if found {
		agentID := workflowEditResponderAgentID(latest.Metadata)
		if agentID == "" {
			return RequestResult{}, storeerr.Conflict("previous Workflow Agent wake state is ambiguous")
		}
		wake, err := service.ops.InTx(tx).LoadMessageWakeState(ctx, input.WorkspaceID, agentID, latest.ID)
		if err != nil {
			return RequestResult{}, err
		}
		if wake.Active {
			return RequestResult{}, fmt.Errorf("%w: %w", ErrAgentWakeActive, storeerr.ErrConflict)
		}
	}

	draft, err := service.plans.InTx(tx).LoadDraftForUpdate(ctx, input.WorkspaceID, input.PlanID)
	if err != nil {
		return RequestResult{}, err
	}
	if draft.Revision != input.DraftRevision {
		return RequestResult{}, storeerr.Conflict("workflow draft revision is stale")
	}
	target, diagnostics, err := validateTarget(draft.Source, input.Target)
	if err != nil {
		return RequestResult{}, err
	}
	selected, err := service.agents.LoadOwnedWorkflowAgentEditAgent(
		ctx, tx, input.UserID, input.WorkspaceID, input.AgentID,
	)
	if err != nil {
		return RequestResult{}, err
	}
	participatedAt := time.Now().UTC()
	if err := message.EnsureWorkflowScopeParticipantMembership(
		ctx, tx, plan.ScopeID, input.MemberID, participatedAt,
	); err != nil {
		return RequestResult{}, mapMessageError(err)
	}
	if err := message.EnsureWorkflowScopeParticipantMembership(
		ctx, tx, plan.ScopeID, selected.MemberID, participatedAt,
	); err != nil {
		return RequestResult{}, mapMessageError(err)
	}
	return service.writeWorkflowEditRequest(
		ctx, tx, input, plan, draft, target, diagnostics, selected,
	)
}

func (service *AgentEditService) replayWorkflowEditRequest(
	ctx context.Context,
	tx coredb.Tx,
	input RequestInput,
	replay message.ScopeMessageRecord,
) (RequestResult, error) {
	requestedDigest, edit, matches, err := replayRequestPayloadDigest(replay.Metadata, input)
	if err != nil {
		return RequestResult{}, err
	}
	if !matches || !strings.EqualFold(replay.IdempotencyPayloadDigest, requestedDigest) {
		return RequestResult{}, storeerr.Conflict("clientMessageId was already used for a different Workflow edit request")
	}
	projected, err := message.WorkflowAppMessageFromRecord(ctx, tx, input.WorkspaceID, replay)
	if err != nil {
		return RequestResult{}, err
	}
	agent := AgentCandidate{
		ID: edit.ResponderAgentID, Name: edit.ResponderAgentID,
		DisplayName: edit.ResponderAgentID,
	}
	selected, loadErr := service.agents.LoadOwnedWorkflowAgentEditAgent(
		ctx, tx, input.UserID, input.WorkspaceID, input.AgentID,
	)
	if loadErr == nil {
		agent = AgentCandidate{
			ID: selected.ID, Name: selected.Name, DisplayName: selected.DisplayName,
			Description: selected.Description, AvatarURL: selected.AvatarURL,
			SearchValues: nonEmptyStrings(selected.Name, selected.DisplayName),
		}
	} else if !errors.Is(loadErr, app.ErrNotFound) {
		return RequestResult{}, loadErr
	}
	wakeState, err := service.ops.InTx(tx).LoadMessageWakeState(
		ctx, input.WorkspaceID, edit.ResponderAgentID, replay.ID,
	)
	if err != nil {
		return RequestResult{}, err
	}
	return RequestResult{
		Message: projected,
		Target: Target{
			Kind: edit.TargetKind, ID: edit.TargetID, NodeType: edit.TargetNodeType,
		},
		Agent:             agent,
		BaseDraftRevision: input.DraftRevision,
		Wake:              wakeStateFromRequest(replay.ID, edit, projected.CreatedAt, wakeState),
		Inserted:          false,
	}, nil
}

func (service *AgentEditService) writeWorkflowEditRequest(
	ctx context.Context,
	tx coredb.Tx,
	input RequestInput,
	plan planstore.Plan,
	draft planstore.PlanDraft,
	target Target,
	diagnostics []plan.Diagnostic,
	selected app.WorkflowAgentEditAgent,
) (RequestResult, error) {
	metadata := buildRequestMetadata(plan.ID, draft.Revision, selected, target)
	deliveryMetadata := buildRequestDeliveryMetadata(
		plan.ID, draft.Revision, selected, target, diagnostics,
	)
	encodedMetadata, _, err := message.EncodeMessageMetadata(metadata)
	if err != nil {
		return RequestResult{}, err
	}
	payloadDigest, err := message.ComputeMessageCreatePayloadDigest(message.MessageCreateDigestInput{
		MessageType: "text", Content: input.Instruction, Metadata: encodedMetadata,
	})
	if err != nil {
		return RequestResult{}, err
	}
	messageID, err := identifiers.NewMessageID()
	if err != nil {
		return RequestResult{}, fmt.Errorf("generate Workflow edit request message: %w", err)
	}
	recipient := message.AgentRecipient{
		AgentID: selected.ID, MachineID: selected.MachineID, AgentMemberID: selected.MemberID,
		AutoWakeEnabled: selected.AutoWakeEnabled,
		DeliveryReason:  message.AgentDeliveryReasonDirectMention,
		MentionToken:    "@" + selected.Name,
	}
	written, err := message.WriteWorkflowScopeMessageTx(ctx, tx, message.WorkflowScopeMessageWrite{
		MessageID: messageID, WorkspaceID: input.WorkspaceID, ScopeID: plan.ScopeID, ChannelName: plan.ID,
		AuthorMemberID: input.MemberID, AuthorType: message.AuthorActorTypeHuman,
		ClientMessageID: input.ClientMessageID, PayloadDigest: payloadDigest,
		Content: input.Instruction, Metadata: metadata, DeliveryMetadata: deliveryMetadata,
		Recipient: &recipient, RequireWorkflowEditMetadata: true,
		AuthorProfile: message.AuthorProfile{
			ActorType: message.AuthorActorTypeHuman, PrincipalID: input.UserID,
			MemberID: input.MemberID, TargetID: input.MemberID,
			DisplayName: input.MemberDisplayName,
		},
	})
	if err != nil {
		return RequestResult{}, mapMessageError(err)
	}
	return RequestResult{
		Message: written.Message,
		Target:  target,
		Agent: AgentCandidate{
			ID: selected.ID, Name: selected.Name, DisplayName: selected.DisplayName,
			Description: selected.Description, AvatarURL: selected.AvatarURL,
			SearchValues: nonEmptyStrings(selected.Name, selected.DisplayName),
		},
		BaseDraftRevision: draft.Revision,
		Wake: WakeState{
			MessageID: messageID, AgentID: selected.ID,
			BaseDraftRevision: draft.Revision,
			Status:            "received", Timestamp: written.Message.CreatedAt, Active: true,
		},
		Inserted: written.Inserted,
	}, nil
}

// GetWake returns the latest durable Agent-edit wake for one Plan without
// loading its message history. The Plan lookup owns workspace scoping and the
// Workflow Scope remains an internal transport detail.
func (service *AgentEditService) GetWake(ctx context.Context, workspaceID, planID string) (*WakeState, error) {
	if err := service.validate(); err != nil {
		return nil, err
	}
	workspaceID = strings.TrimSpace(workspaceID)
	planID = strings.TrimSpace(planID)
	plan, err := service.plans.LoadPlan(ctx, workspaceID, planID)
	if err != nil {
		return nil, err
	}
	return service.loadLatestWakeState(ctx, workspaceID, planID, plan.ScopeID)
}

func (service *AgentEditService) loadLatestWakeState(ctx context.Context, workspaceID, planID, scopeID string) (*WakeState, error) {
	request, found, err := message.LoadLatestWorkflowEditRequest(ctx, service.db, workspaceID, scopeID)
	if err != nil || !found {
		return nil, err
	}
	edit, err := message.DecodeWorkflowEditRequestMetadata(request.Metadata)
	if err != nil {
		return nil, err
	}
	if edit.PlanID != strings.TrimSpace(planID) {
		return nil, nil
	}
	var state agentops.MessageWakeState
	err = coredb.WithTx(ctx, service.db, func(tx coredb.Tx) error {
		var loadErr error
		state, loadErr = service.ops.InTx(tx).LoadMessageWakeState(
			ctx, workspaceID, edit.ResponderAgentID, request.ID,
		)
		return loadErr
	})
	if err != nil {
		return nil, err
	}
	wake := wakeStateFromRequest(request.ID, edit, request.CreatedAt, state)
	return &wake, nil
}

// WasWakeActiveAt reports whether the latest Workflow Agent request was still
// active at one server-recorded Plan lifecycle timestamp.
func (service *AgentEditService) WasWakeActiveAt(ctx context.Context, workspaceID, planID string, at time.Time) (bool, error) {
	_, state, found, err := service.loadLatestAgentEditWake(
		ctx, workspaceID, planID,
	)
	if err != nil {
		return false, err
	}
	if !found {
		return false, nil
	}
	if state.Active {
		return true, nil
	}
	at = at.UTC()
	return !at.IsZero() && !state.RecordedAt.IsZero() && !state.RecordedAt.Before(at), nil
}

// RequireAgentLifecycleAllowed blocks only the responder of the latest active
// Builder-edit request for this exact Plan. The lookup is bounded by one Plan,
// one latest request, and one wake state; unrelated workspace history is never
// enumerated.
func (service *AgentEditService) RequireAgentLifecycleAllowed(
	ctx context.Context,
	workspaceID string,
	planID string,
	agentID string,
) error {
	_, state, applies, err := service.loadMatchingAgentEditWake(
		ctx, workspaceID, planID, agentID,
	)
	if err != nil || !applies || !state.Active {
		return err
	}
	return fmt.Errorf(
		"%w: Plan lifecycle actions are unavailable during this Builder edit",
		ErrAgentWakeActive,
	)
}

// RequireAgentDraftUpdateAllowed keeps a matching active Builder edit on the
// request's confirmed baseline revision. The ordinary Draft CAS then permits
// at most one changed save from that revision; a retry from the baseline is
// stale after the first save, and rebasing to the new revision is rejected
// here.
func (service *AgentEditService) RequireAgentDraftUpdateAllowed(
	ctx context.Context,
	workspaceID string,
	planID string,
	agentID string,
	expectedRevision int64,
) error {
	edit, state, applies, err := service.loadMatchingAgentEditWake(
		ctx, workspaceID, planID, agentID,
	)
	if err != nil || !applies || !state.Active {
		return err
	}
	if expectedRevision != edit.BaseDraftRevision {
		return fmt.Errorf(
			"%w: expected revision must remain %d for this Builder edit",
			ErrAgentEditRevisionMismatch,
			edit.BaseDraftRevision,
		)
	}
	return nil
}

func (service *AgentEditService) loadMatchingAgentEditWake(
	ctx context.Context,
	workspaceID string,
	planID string,
	agentID string,
) (message.WorkflowEditRequestMetadata, agentops.MessageWakeState, bool, error) {
	edit, state, found, err := service.loadLatestAgentEditWake(
		ctx, workspaceID, planID,
	)
	if err != nil || !found {
		return message.WorkflowEditRequestMetadata{}, agentops.MessageWakeState{}, false, err
	}
	if edit.ResponderAgentID != strings.TrimSpace(agentID) {
		return edit, agentops.MessageWakeState{}, false, nil
	}
	return edit, state, true, nil
}

func (service *AgentEditService) loadLatestAgentEditWake(
	ctx context.Context,
	workspaceID string,
	planID string,
) (message.WorkflowEditRequestMetadata, agentops.MessageWakeState, bool, error) {
	if err := service.validate(); err != nil {
		return message.WorkflowEditRequestMetadata{}, agentops.MessageWakeState{}, false, err
	}
	workspaceID = strings.TrimSpace(workspaceID)
	planID = strings.TrimSpace(planID)
	plan, err := service.plans.LoadPlan(ctx, workspaceID, planID)
	if err != nil {
		return message.WorkflowEditRequestMetadata{}, agentops.MessageWakeState{}, false, err
	}
	request, found, err := message.LoadLatestWorkflowEditRequest(
		ctx, service.db, workspaceID, plan.ScopeID,
	)
	if err != nil || !found {
		return message.WorkflowEditRequestMetadata{}, agentops.MessageWakeState{}, false, err
	}
	edit, err := message.DecodeWorkflowEditRequestMetadata(request.Metadata)
	if err != nil {
		return message.WorkflowEditRequestMetadata{}, agentops.MessageWakeState{}, false, err
	}
	if edit.PlanID != planID {
		return edit, agentops.MessageWakeState{}, false, nil
	}
	var state agentops.MessageWakeState
	err = coredb.WithTx(ctx, service.db, func(tx coredb.Tx) error {
		var loadErr error
		state, loadErr = service.ops.InTx(tx).LoadMessageWakeState(
			ctx, workspaceID, edit.ResponderAgentID, request.ID,
		)
		return loadErr
	})
	if err != nil {
		return message.WorkflowEditRequestMetadata{}, agentops.MessageWakeState{}, false, err
	}
	return edit, state, true, nil
}

func wakeStateFromRequest(messageID string, edit message.WorkflowEditRequestMetadata, createdAt time.Time, state agentops.MessageWakeState) WakeState {
	status := "received"
	switch state.EventClass {
	case agentops.AgentRuntimeEventClassVisibleReplySent:
		status = "reply_sent"
	case agentops.AgentRuntimeEventClassCompletedWithoutVisibleReply:
		status = "completed_without_visible_reply"
	case agentops.AgentRuntimeEventClassWakeFailed:
		status = "failed"
	default:
		if !state.Active {
			status = "failed"
		}
	}
	timestamp := state.OccurredAt
	if timestamp.IsZero() {
		timestamp = createdAt
	}
	return WakeState{
		MessageID:         strings.TrimSpace(messageID),
		AgentID:           edit.ResponderAgentID,
		BaseDraftRevision: edit.BaseDraftRevision,
		Status:            status,
		Detail:            state.Detail,
		Timestamp:         timestamp.UTC(),
		Active:            state.Active,
	}
}

func (service *AgentEditService) validate() error {
	if service == nil || service.db == nil || service.plans == nil || service.messages == nil || service.agents == nil || service.ops == nil {
		return fmt.Errorf("Workflow Agent edit service is not configured")
	}
	return nil
}

func normalizeRequest(input RequestInput) (RequestInput, error) {
	input.WorkspaceID = strings.TrimSpace(input.WorkspaceID)
	input.PlanID = strings.TrimSpace(input.PlanID)
	input.UserID = strings.TrimSpace(input.UserID)
	input.MemberID = strings.TrimSpace(input.MemberID)
	input.MemberDisplayName = strings.TrimSpace(input.MemberDisplayName)
	input.AgentID = strings.TrimSpace(input.AgentID)
	input.ClientMessageID = strings.TrimSpace(input.ClientMessageID)
	input.Instruction = strings.TrimSpace(input.Instruction)
	input.Target.Kind = strings.TrimSpace(input.Target.Kind)
	input.Target.ID = strings.TrimSpace(input.Target.ID)
	input.Target.NodeType = strings.TrimSpace(input.Target.NodeType)
	if input.WorkspaceID == "" || input.PlanID == "" || input.UserID == "" || input.MemberID == "" || input.AgentID == "" {
		return RequestInput{}, storeerr.BadRequest("workspace, plan, actor, and agentId are required")
	}
	if input.MemberDisplayName == "" {
		input.MemberDisplayName = input.MemberID
	}
	if input.ClientMessageID == "" || len(input.ClientMessageID) > MaxClientMessageID {
		return RequestInput{}, storeerr.BadRequest("clientMessageId is required and must be at most 128 bytes")
	}
	if input.DraftRevision <= 0 {
		return RequestInput{}, storeerr.BadRequest("draftRevision must be positive")
	}
	if input.Instruction == "" || len(input.Instruction) > MaxInstructionBytes {
		return RequestInput{}, storeerr.BadRequest("instruction is required and must be at most 16384 bytes")
	}
	switch input.Target.Kind {
	case "plan", "entry", "node", "edge":
	default:
		return RequestInput{}, storeerr.BadRequest("target.kind must be plan, entry, node, or edge")
	}
	if input.Target.ID == "" {
		return RequestInput{}, storeerr.BadRequest("target.id is required")
	}
	if input.Target.Kind == "plan" && input.Target.ID != input.PlanID {
		return RequestInput{}, storeerr.BadRequest("plan target id must match planId")
	}
	return input, nil
}

func validateTarget(source string, requested Target) (Target, []plan.Diagnostic, error) {
	sourceBytes := []byte(source)
	document, parseDiagnostics := plan.Parse(sourceBytes)
	if document == nil || plan.HasErrors(parseDiagnostics) {
		return Target{}, parseDiagnostics, storeerr.New(storeerr.ErrUnprocessable, "workflow draft must parse before requesting an Agent edit")
	}
	_, diagnostics, err := plan.Compile(sourceBytes)
	if err != nil || plan.HasErrors(diagnostics) {
		return Target{}, diagnostics, storeerr.New(storeerr.ErrUnprocessable, "workflow draft must compile before requesting an Agent edit")
	}
	target := Target{Kind: requested.Kind, ID: requested.ID}
	switch requested.Kind {
	case "plan":
		return target, diagnostics, nil
	case "entry":
		if requested.ID != "$entry" {
			return Target{}, diagnostics, storeerr.BadRequest("entry target id must be $entry")
		}
	case "node":
		for _, node := range document.Graph.Nodes {
			if node.ID == requested.ID {
				target.NodeType = string(node.Type)
				return target, diagnostics, nil
			}
		}
		return Target{}, diagnostics, storeerr.BadRequest("target node was not found in the saved draft")
	case "edge":
		for _, edge := range document.Graph.Edges {
			if edge.ID == requested.ID {
				return target, diagnostics, nil
			}
		}
		return Target{}, diagnostics, storeerr.BadRequest("target edge was not found in the saved draft")
	}
	return target, diagnostics, nil
}

func buildRequestMetadata(planID string, revision int64, selected app.WorkflowAgentEditAgent, target Target) map[string]any {
	targetValue := map[string]any{"kind": target.Kind, "id": target.ID}
	if target.NodeType != "" {
		targetValue["nodeType"] = target.NodeType
	}
	return map[string]any{
		message.WorkflowAgentEditMetadataKey: map[string]any{
			"kind": "request", "planId": planID,
			"baseDraftRevision": revision, "responderAgentId": selected.ID,
			"target": targetValue,
		},
	}
}

func buildRequestDeliveryMetadata(
	planID string,
	revision int64,
	selected app.WorkflowAgentEditAgent,
	target Target,
	diagnostics []plan.Diagnostic,
) map[string]any {
	metadata := buildRequestMetadata(planID, revision, selected, target)
	edit := metadata[message.WorkflowAgentEditMetadataKey].(map[string]any)
	edit["diagnostics"] = diagnostics
	return metadata
}

func replayRequestPayloadDigest(raw []byte, input RequestInput) (string, message.WorkflowEditRequestMetadata, bool, error) {
	metadata, err := message.DecodeMessageMetadata(raw)
	if err != nil {
		return "", message.WorkflowEditRequestMetadata{}, false, err
	}
	edit, err := message.DecodeWorkflowEditRequestMetadata(raw)
	if err != nil {
		return "", message.WorkflowEditRequestMetadata{}, false, err
	}
	matches :=
		edit.ResponderAgentID == input.AgentID &&
			edit.PlanID == input.PlanID &&
			edit.BaseDraftRevision == input.DraftRevision &&
			edit.TargetKind == input.Target.Kind &&
			edit.TargetID == input.Target.ID
	if !matches {
		return "", edit, false, nil
	}
	encoded, _, err := message.EncodeMessageMetadata(metadata)
	if err != nil {
		return "", message.WorkflowEditRequestMetadata{}, false, err
	}
	digest, err := message.ComputeMessageCreatePayloadDigest(message.MessageCreateDigestInput{
		MessageType: "text", Content: input.Instruction, Metadata: encoded,
	})
	return digest, edit, true, err
}

func workflowEditResponderAgentID(raw []byte) string {
	metadata, err := message.DecodeWorkflowEditRequestMetadata(raw)
	if err != nil {
		return ""
	}
	return metadata.ResponderAgentID
}

func mapMessageError(err error) error {
	switch {
	case errors.Is(err, message.ErrNotFound):
		return storeerr.Wrapped(storeerr.ErrNotFound, "the Workflow Plan edit message was not found", err)
	case errors.Is(err, message.ErrForbidden):
		return storeerr.Wrapped(storeerr.ErrForbidden, "the Workflow Plan edit message is not accessible", err)
	case errors.Is(err, message.ErrConflict):
		return storeerr.Wrapped(storeerr.ErrConflict, "the Workflow Plan edit message conflicts with the current state", err)
	default:
		return err
	}
}

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