package agent

import (
	"context"
	"crypto/sha256"
	"encoding/hex"
	"errors"
	"fmt"
	"log/slog"
	"net/http"
	"strings"
	"time"

	"github.com/tidbcloud/AgentTeam/internal/api"
	"github.com/tidbcloud/AgentTeam/internal/api/dto"
	"github.com/tidbcloud/AgentTeam/internal/service/agentapproval"
	"github.com/tidbcloud/AgentTeam/internal/service/approvalapp"
	"github.com/tidbcloud/AgentTeam/internal/store/agent"
	"github.com/tidbcloud/AgentTeam/internal/store/agentops"
	"github.com/tidbcloud/AgentTeam/internal/store/approval"
	"github.com/tidbcloud/AgentTeam/internal/store/storeerr"
)

const (
	agentManagementMaxRequestIDLen = 128

	agentManagementResultStarted        = "started"
	agentManagementResultAlreadyStarted = "already_started"
	agentManagementResultStopping       = "stopping"
	agentManagementResultAlreadyStopped = "already_stopped"
	agentManagementResultRestarted      = "restarted"
	// agentManagementResultRuntimeUnknown is the receipt result_status recorded
	// when a dispatch may or may not have applied (a crash mid-dispatch, or a
	// dispatch whose result could not be durably recorded). It is never returned
	// as a success status: the matching error envelope is returned instead.
	agentManagementResultRuntimeUnknown = "runtime_outcome_unknown"
)

// agentManagementError carries a stable Exec-facing code so the dedicated
// classifier can map each lifecycle outcome to a distinct 409 envelope.
type agentManagementError struct {
	code    string
	message string
	status  int
}

func (err *agentManagementError) Error() string { return err.message }

var (
	errAgentMachineOffline        = &agentManagementError{code: "machine_offline", status: http.StatusConflict, message: "agent machine is offline"}
	errAgentNotRunning            = &agentManagementError{code: "agent_not_running", status: http.StatusConflict, message: "agent is not running"}
	errAgentIdempotencyConflict   = &agentManagementError{code: "idempotency_conflict", status: http.StatusConflict, message: "agent lifecycle requestId was reused with a different payload"}
	errAgentRuntimeOutcomeUnknown = &agentManagementError{code: "runtime_outcome_unknown", status: http.StatusConflict, message: "agent lifecycle runtime outcome could not be determined"}
	errAgentRequestInProgress     = &agentManagementError{code: "request_in_progress", status: http.StatusConflict, message: "agent lifecycle request is already in progress"}
)

// agentManagementErrorClassifier maps the typed lifecycle errors to stable
// envelopes, explicitly maps the store error sentinels (the task
// classifier's HTTPStatus does not recognize storeerr.*), and finally falls
// back to the agent task classifier.
func agentManagementErrorClassifier(err error) (api.ErrorResponse, bool) {
	var managementErr *agentManagementError
	if errors.As(err, &managementErr) {
		return api.ErrorResponse{Status: managementErr.status, Message: managementErr.message, Code: managementErr.code}, true
	}
	switch {
	case errors.Is(err, storeerr.ErrNotFound):
		return api.ErrorResponse{Status: http.StatusNotFound, Message: err.Error(), Code: "not_found"}, true
	case errors.Is(err, storeerr.ErrForbidden):
		return api.ErrorResponse{Status: http.StatusForbidden, Message: err.Error(), Code: "forbidden"}, true
	case errors.Is(err, storeerr.ErrUnauthorized):
		return api.ErrorResponse{Status: http.StatusUnauthorized, Message: err.Error(), Code: "unauthorized"}, true
	case errors.Is(err, storeerr.ErrBadRequest):
		return api.ErrorResponse{Status: http.StatusBadRequest, Message: err.Error(), Code: "bad_request"}, true
	case errors.Is(err, storeerr.ErrUnprocessable):
		return api.ErrorResponse{Status: http.StatusUnprocessableEntity, Message: err.Error(), Code: "unprocessable"}, true
	case errors.Is(err, storeerr.ErrConflict):
		return api.ErrorResponse{Status: http.StatusConflict, Message: err.Error(), Code: "conflict"}, true
	}
	return agentTaskErrorClassifier(err)
}

// AgentManagementOwner resolves the caller's owner Human, loads a target Agent
// under ownership, and authorizes target machine management. Satisfied by *app.Store.
type AgentManagementOwner interface {
	AgentManagedByUser(ctx context.Context, workspaceID string, agentID string) (string, error)
	GetAgentForUser(ctx context.Context, userID string, workspaceID string, agentID string) (agent.AgentView, error)
	RequireMachineManagementForUser(ctx context.Context, userID string, workspaceID string, machineID string) error
}

// AgentLifecycleCommands orchestrates auto-wake-aware start/stop. Satisfied by
// *agentruntime.Service. These swallow ErrMachineOffline, so the handler gates
// dispatch on AgentRuntimeControl.MachineConnected rather than the return value.
type AgentLifecycleCommands interface {
	StartAgentRuntimeForUser(ctx context.Context, userID string, workspaceID string, agentID string) (agent.AgentView, error)
	StopAgentRuntimeForUser(ctx context.Context, userID string, workspaceID string, agentID string) (agent.AgentView, error)
}

// AgentRuntimeControl drives low-level runtime state. Satisfied by the daemon
// hub. MachineConnected is the authoritative offline truth used before dispatch.
type AgentRuntimeControl interface {
	MachineConnected(machineID string) bool
	AgentRuntimeRunning(machineID string, agentID string) bool
	RestartAgent(ctx context.Context, machineID string, agentID string) error
}

// AgentLifecycleReceipts is the direct-lifecycle idempotency/audit ledger.
// Satisfied by *agentops.Store.
type AgentLifecycleReceipts interface {
	ReserveLifecycleReceipt(ctx context.Context, intent agentops.LifecycleReceiptIntent) (agentops.LifecycleReceipt, bool, error)
	MarkLifecycleDispatching(ctx context.Context, applicantAgentID, action, requestID string) (bool, error)
	RecordLifecycleResult(ctx context.Context, applicantAgentID, action, requestID, expectedCurrentState, state, resultStatus string) (bool, error)
}

type AgentManagementHandler struct {
	owners          AgentManagementOwner
	runtimeCommands AgentLifecycleCommands
	runtimeControl  AgentRuntimeControl
	receipts        AgentLifecycleReceipts
	approvals       ApprovalRuntime
	logger          *slog.Logger
}

type approvalReplayRuntime interface {
	LookupRequestReplay(context.Context, string, approvalapp.SubmitCommand) (approvalapp.SubmitResult, bool, error)
}

type agentManagementLifecycleRequest struct {
	RequestID string `json:"requestId"`
	AgentID   string `json:"agentId"`
}

type agentManagementLifecycleResponse struct {
	Status string `json:"status"`
}

func (handler *AgentManagementHandler) runLifecycle(request api.AgentScopedJSON[agentManagementLifecycleRequest], action string) (api.Response, error) {
	if handler == nil || handler.owners == nil || handler.runtimeCommands == nil ||
		handler.runtimeControl == nil || handler.receipts == nil {
		return api.Response{}, api.NewHTTPError(http.StatusInternalServerError, "agent management is not configured")
	}
	requestID := strings.TrimSpace(request.Body.RequestID)
	agentID := strings.TrimSpace(request.Body.AgentID)
	if requestID == "" || agentID == "" {
		return api.Response{}, api.NewHTTPError(http.StatusBadRequest, "requestId and agentId are required")
	}
	status, err := handler.execute(request.Context(), request.Principal, action, requestID, agentID,
		strings.TrimSpace(request.Gin.GetHeader("X-Request-ID")))
	if err != nil {
		return api.Response{}, mapAgentLifecycleError(err)
	}
	return api.OK(agentManagementLifecycleResponse{Status: status}), nil
}

// execute owns the lifecycle orchestration. Order matters:
//  1. owner + target resolution (hard ownership boundary);
//  2. durable receipt reserve + replay-by-state (a volatile runtime blip must
//     not override a durable prior result, so replay precedes the runtime gate);
//  3. authoritative offline + running gate (Start/Stop swallow ErrMachineOffline);
//  4. dispatch under a CAS, with a terminal CAS guard so a success is never
//     returned unless durably recorded.
func (handler *AgentManagementHandler) execute(ctx context.Context, principal agent.AuthenticatedAgent, action, requestID, agentID, correlationID string) (string, error) {
	if len(requestID) > agentManagementMaxRequestIDLen {
		return "", storeerr.BadRequest("agent lifecycle requestId must be 1-128 characters")
	}
	ownerUserID, err := handler.owners.AgentManagedByUser(ctx, principal.WorkspaceID, principal.AgentID)
	if err != nil {
		return "", err
	}
	target, err := handler.owners.GetAgentForUser(ctx, ownerUserID, principal.WorkspaceID, agentID)
	if err != nil {
		return "", err
	}

	intent := agentops.LifecycleReceiptIntent{
		WorkspaceID: principal.WorkspaceID, ApplicantAgentID: principal.AgentID,
		OwnerUserID: ownerUserID, Action: action, TargetAgentID: agentID,
		RequestID: requestID, PayloadDigest: agentLifecyclePayloadDigest(action, agentID),
		CorrelationID: correlationID,
	}
	receipt, replayed, err := handler.receipts.ReserveLifecycleReceipt(ctx, intent)
	if err != nil {
		return "", err
	}
	if replayed {
		switch receipt.State {
		case agentops.LifecycleReceiptStateSucceeded:
			return receipt.ResultStatus, nil
		case agentops.LifecycleReceiptStateIndeterminate:
			return "", errAgentRuntimeOutcomeUnknown
		case agentops.LifecycleReceiptStateDispatching:
			return "", errAgentRequestInProgress
		case agentops.LifecycleReceiptStatePrepared:
			// Reserved before but never dispatched (a crash or offline before
			// dispatch); fall through to the dispatch flow.
		}
	}

	// Authoritative offline gate BEFORE dispatch: Start/StopAgentRuntimeForUser
	// swallow ErrMachineOffline, so the service return value cannot be trusted to
	// surface offline. Offline must never be normalized into stopped/already_*.
	if !handler.runtimeControl.MachineConnected(target.MachineID) {
		return "", errAgentMachineOffline
	}
	running := handler.runtimeControl.AgentRuntimeRunning(target.MachineID, agentID)
	if action == agentops.LifecycleActionRestart && !running {
		return "", errAgentNotRunning
	}

	transitioned, err := handler.receipts.MarkLifecycleDispatching(ctx, principal.AgentID, action, requestID)
	if err != nil {
		return "", err
	}
	if !transitioned {
		return "", errAgentRequestInProgress
	}

	status, dispatchErr := handler.dispatch(ctx, action, ownerUserID, principal.WorkspaceID, target.MachineID, agentID, running, target.AutoWakeEnabled)
	if dispatchErr != nil {
		// After MarkDispatching, any service/transport error may have sent the
		// command. Record indeterminate (a replay returns runtime_outcome_unknown);
		// this request returns the original error so the caller can act on it.
		handler.recordTerminal(ctx, principal.AgentID, action, requestID,
			agentops.LifecycleReceiptStateIndeterminate, agentManagementResultRuntimeUnknown)
		return "", dispatchErr
	}
	// Terminal CAS guard: a success must be durably recorded, else the outcome is
	// ambiguous (command sent, result not recorded) and must not return success.
	recorded, recErr := handler.receipts.RecordLifecycleResult(ctx, principal.AgentID, action, requestID,
		agentops.LifecycleReceiptStateDispatching, agentops.LifecycleReceiptStateSucceeded, status)
	if recErr != nil || !recorded {
		return "", errAgentRuntimeOutcomeUnknown
	}
	return status, nil
}

func (handler *AgentManagementHandler) dispatch(ctx context.Context, action, ownerUserID, workspaceID, machineID, agentID string, running, autoWake bool) (string, error) {
	switch action {
	case agentops.LifecycleActionStart:
		// Full target state is (running AND auto-wake on). The handler short-
		// circuits only when both hold; otherwise it delegates to the lifecycle
		// service, which converges both dimensions and sends the runtime command
		// only when the agent is not already running (no duplicate send).
		if running && autoWake {
			return agentManagementResultAlreadyStarted, nil
		}
		if _, err := handler.runtimeCommands.StartAgentRuntimeForUser(ctx, ownerUserID, workspaceID, agentID); err != nil {
			return "", err
		}
		// Status reflects the pre-call run truth: an already-running agent is
		// already_started even when the service enabled auto-wake to converge.
		if running {
			return agentManagementResultAlreadyStarted, nil
		}
		return agentManagementResultStarted, nil
	case agentops.LifecycleActionStop:
		if !running && !autoWake {
			return agentManagementResultAlreadyStopped, nil
		}
		if _, err := handler.runtimeCommands.StopAgentRuntimeForUser(ctx, ownerUserID, workspaceID, agentID); err != nil {
			return "", err
		}
		// Status reflects the pre-call run truth: an already-stopped agent is
		// already_stopped even when the service disabled auto-wake to converge.
		if running {
			return agentManagementResultStopping, nil
		}
		return agentManagementResultAlreadyStopped, nil
	case agentops.LifecycleActionRestart:
		if err := handler.runtimeControl.RestartAgent(ctx, machineID, agentID); err != nil {
			return "", err
		}
		return agentManagementResultRestarted, nil
	}
	return "", fmt.Errorf("unsupported agent lifecycle action %q", action)
}

func (handler *AgentManagementHandler) recordTerminal(ctx context.Context, applicantAgentID, action, requestID, state, resultStatus string) {
	if _, err := handler.receipts.RecordLifecycleResult(ctx, applicantAgentID, action, requestID,
		agentops.LifecycleReceiptStateDispatching, state, resultStatus); err != nil && handler.logger != nil {
		handler.logger.WarnContext(ctx, "record agent lifecycle result", "action", action, "error", err)
	}
}

func mapAgentLifecycleError(err error) error {
	if errors.Is(err, agentops.ErrLifecyclePayloadConflict) {
		return errAgentIdempotencyConflict
	}
	return err
}

func agentLifecyclePayloadDigest(action, agentID string) string {
	sum := sha256.Sum256([]byte(action + "\x00" + agentID))
	return hex.EncodeToString(sum[:])
}

// Structural action DTOs (create/update/delete/duplicate). These mirror the
// frozen agent_management Exec contract and feed the credential-free
// agentmanagement payload; raw LLM credentials are intentionally absent.

type agentManagementCreateRequest struct {
	RequestID       string   `json:"requestId"`
	Name            string   `json:"name"`
	Description     string   `json:"description,omitempty"`
	MachineID       string   `json:"machineId"`
	Runtime         string   `json:"runtime"`
	Model           string   `json:"model,omitempty"`
	ReasoningEffort string   `json:"reasoningEffort,omitempty"`
	ModelProfileID  string   `json:"modelProfileId,omitempty"`
	ChannelIDs      []string `json:"channelIds,omitempty"`
}

type agentManagementUpdateRequest struct {
	RequestID       string             `json:"requestId"`
	AgentID         string             `json:"agentId"`
	Description     dto.OptionalString `json:"description"`
	Model           dto.OptionalString `json:"model"`
	ReasoningEffort dto.OptionalString `json:"reasoningEffort"`
	ModelProfileID  dto.OptionalString `json:"modelProfileId"`
}

type agentManagementDeleteRequest struct {
	RequestID string   `json:"requestId"`
	AgentIDs  []string `json:"agentIds"`
}

type agentManagementDuplicateRequest struct {
	RequestID     string   `json:"requestId"`
	SourceAgentID string   `json:"sourceAgentId"`
	Count         *int     `json:"count,omitempty"`
	Mode          string   `json:"mode,omitempty"`
	MachineID     string   `json:"machineId,omitempty"`
	Runtime       string   `json:"runtime,omitempty"`
	ChannelIDs    []string `json:"channelIds,omitempty"`
}

type agentManagementResponse struct {
	Status     string `json:"status"`
	ApprovalID string `json:"approvalId"`
}

func (handler *AgentManagementHandler) Create(request api.AgentScopedJSON[agentManagementCreateRequest]) (api.Response, error) {
	req := request.Body
	requestID, err := validateAgentManagementRequestID(req.RequestID)
	if err != nil {
		return api.Response{}, err
	}
	payload := agentapproval.Payload{Action: agentapproval.ActionCreate, Create: &agentapproval.CreateProposal{
		Name: strings.TrimSpace(req.Name), Description: req.Description, MachineID: strings.TrimSpace(req.MachineID),
		Runtime: strings.TrimSpace(req.Runtime), Model: req.Model, ReasoningEffort: req.ReasoningEffort,
		ModelProfileID: req.ModelProfileID, ChannelIDs: req.ChannelIDs,
	}}
	if err := agentapproval.ValidatePayload(payload); err != nil {
		return api.Response{}, err
	}
	if response, found, err := handler.lookupAgentManagementReplay(
		request.Context(), request.Principal, requestID, payload,
		strings.TrimSpace(request.Gin.GetHeader("X-Request-ID")),
	); err != nil || found {
		return response, err
	}
	ownerUserID, err := handler.resolveApplicantOwner(request.Context(), request.Principal)
	if err != nil {
		return api.Response{}, err
	}
	if err := handler.owners.RequireMachineManagementForUser(request.Context(), ownerUserID, request.Principal.WorkspaceID, payload.Create.MachineID); err != nil {
		return api.Response{}, err
	}
	return handler.submitAgentManagement(request.Context(), request.Principal, agentapproval.SubmitInput{
		ClientRequestID: requestID,
		Payload:         payload,
	}, strings.TrimSpace(request.Gin.GetHeader("X-Request-ID")))
}

func (handler *AgentManagementHandler) Update(request api.AgentScopedJSON[agentManagementUpdateRequest]) (api.Response, error) {
	req := request.Body
	requestID, err := validateAgentManagementRequestID(req.RequestID)
	if err != nil {
		return api.Response{}, err
	}
	payload := agentapproval.Payload{Action: agentapproval.ActionUpdate, Update: &agentapproval.UpdateProposal{
		AgentID: strings.TrimSpace(req.AgentID), Description: optionalStringChange(req.Description), Model: optionalStringChange(req.Model),
		ReasoningEffort: optionalStringChange(req.ReasoningEffort), ModelProfileID: optionalStringChange(req.ModelProfileID),
	}}
	if err := agentapproval.ValidatePayload(payload); err != nil {
		return api.Response{}, err
	}
	if response, found, err := handler.lookupAgentManagementReplay(
		request.Context(), request.Principal, requestID, payload,
		strings.TrimSpace(request.Gin.GetHeader("X-Request-ID")),
	); err != nil || found {
		return response, err
	}
	ownerUserID, err := handler.resolveApplicantOwner(request.Context(), request.Principal)
	if err != nil {
		return api.Response{}, err
	}
	if err := handler.requireAgentOwnedBy(request.Context(), request.Principal.WorkspaceID, payload.Update.AgentID, ownerUserID); err != nil {
		return api.Response{}, err
	}
	return handler.submitAgentManagement(request.Context(), request.Principal, agentapproval.SubmitInput{
		ClientRequestID: requestID,
		Payload:         payload,
	}, strings.TrimSpace(request.Gin.GetHeader("X-Request-ID")))
}

func (handler *AgentManagementHandler) Delete(request api.AgentScopedJSON[agentManagementDeleteRequest]) (api.Response, error) {
	req := request.Body
	requestID, err := validateAgentManagementRequestID(req.RequestID)
	if err != nil {
		return api.Response{}, err
	}
	agentIDs := make([]string, 0, len(req.AgentIDs))
	for _, id := range req.AgentIDs {
		agentIDs = append(agentIDs, strings.TrimSpace(id))
	}
	payload := agentapproval.Payload{Action: agentapproval.ActionDelete, Delete: &agentapproval.DeleteProposal{AgentIDs: agentIDs}}
	if err := agentapproval.ValidatePayload(payload); err != nil {
		return api.Response{}, err
	}
	if response, found, err := handler.lookupAgentManagementReplay(
		request.Context(), request.Principal, requestID, payload,
		strings.TrimSpace(request.Gin.GetHeader("X-Request-ID")),
	); err != nil || found {
		return response, err
	}
	ownerUserID, err := handler.resolveApplicantOwner(request.Context(), request.Principal)
	if err != nil {
		return api.Response{}, err
	}
	for _, id := range payload.Delete.AgentIDs {
		if err := handler.requireAgentOwnedBy(request.Context(), request.Principal.WorkspaceID, id, ownerUserID); err != nil {
			return api.Response{}, err
		}
	}
	return handler.submitAgentManagement(request.Context(), request.Principal, agentapproval.SubmitInput{
		ClientRequestID: requestID,
		Payload:         payload,
	}, strings.TrimSpace(request.Gin.GetHeader("X-Request-ID")))
}

func (handler *AgentManagementHandler) Duplicate(request api.AgentScopedJSON[agentManagementDuplicateRequest]) (api.Response, error) {
	req := request.Body
	requestID, err := validateAgentManagementRequestID(req.RequestID)
	if err != nil {
		return api.Response{}, err
	}
	count := 1
	if req.Count != nil {
		count = *req.Count
	}
	payload := agentapproval.Payload{Action: agentapproval.ActionDuplicate, Duplicate: &agentapproval.DuplicateProposal{
		SourceAgentID: strings.TrimSpace(req.SourceAgentID), Count: count, Mode: req.Mode,
		MachineID: strings.TrimSpace(req.MachineID), Runtime: strings.TrimSpace(req.Runtime), ChannelIDs: req.ChannelIDs,
	}}
	if err := agentapproval.ValidatePayload(payload); err != nil {
		return api.Response{}, err
	}
	if response, found, err := handler.lookupAgentManagementReplay(
		request.Context(), request.Principal, requestID, payload,
		strings.TrimSpace(request.Gin.GetHeader("X-Request-ID")),
	); err != nil || found {
		return response, err
	}
	ownerUserID, err := handler.resolveApplicantOwner(request.Context(), request.Principal)
	if err != nil {
		return api.Response{}, err
	}
	if err := handler.requireAgentOwnedBy(request.Context(), request.Principal.WorkspaceID, strings.TrimSpace(req.SourceAgentID), ownerUserID); err != nil {
		return api.Response{}, err
	}
	if machineID := strings.TrimSpace(req.MachineID); machineID != "" {
		if err := handler.owners.RequireMachineManagementForUser(request.Context(), ownerUserID, request.Principal.WorkspaceID, machineID); err != nil {
			return api.Response{}, err
		}
	}
	return handler.submitAgentManagement(request.Context(), request.Principal, agentapproval.SubmitInput{
		ClientRequestID: requestID,
		Payload:         payload,
	}, strings.TrimSpace(request.Gin.GetHeader("X-Request-ID")))
}

func validateAgentManagementRequestID(raw string) (string, error) {
	requestID := strings.TrimSpace(raw)
	if requestID == "" || len(requestID) > agentManagementMaxRequestIDLen {
		return "", api.NewHTTPError(http.StatusBadRequest, "agent management requestId must be 1-128 characters")
	}
	return requestID, nil
}

// optionalStringChange converts a transport OptionalString into the frozen
// contract *string change: absent (not Set) -> nil (no change); set with a null
// value -> pointer to empty string (clear); set with a value -> trimmed pointer.
func optionalStringChange(opt dto.OptionalString) *string {
	if !opt.Set {
		return nil
	}
	if opt.Value == nil {
		empty := ""
		return &empty
	}
	trimmed := strings.TrimSpace(*opt.Value)
	return &trimmed
}

// resolveApplicantOwner returns the Human owner of the applicant Agent's
// machine; every target must belong to this same owner.
func (handler *AgentManagementHandler) resolveApplicantOwner(ctx context.Context, principal agent.AuthenticatedAgent) (string, error) {
	if handler == nil || handler.owners == nil || handler.approvals == nil {
		return "", api.NewHTTPError(http.StatusInternalServerError, "agent management is not configured")
	}
	return handler.owners.AgentManagedByUser(ctx, principal.WorkspaceID, principal.AgentID)
}

func (handler *AgentManagementHandler) requireAgentOwnedBy(ctx context.Context, workspaceID, agentID, ownerUserID string) error {
	if agentID == "" {
		return api.NewHTTPError(http.StatusBadRequest, "agent management target agentId is required")
	}
	agentOwner, err := handler.owners.AgentManagedByUser(ctx, workspaceID, agentID)
	if err != nil {
		return err
	}
	if agentOwner != ownerUserID {
		return storeerr.Forbidden("agent is not owned by the applicant's owner")
	}
	return nil
}

// lookupAgentManagementReplay checks the immutable approval receipt before any
// mutable target Agent or machine preflight. Runtimes without the optional
// lookup capability simply fall through to the normal validated submit path.
func (handler *AgentManagementHandler) lookupAgentManagementReplay(
	ctx context.Context,
	principal agent.AuthenticatedAgent,
	requestID string,
	payload agentapproval.Payload,
	correlationID string,
) (api.Response, bool, error) {
	replays, ok := handler.approvals.(approvalReplayRuntime)
	if !ok {
		return api.Response{}, false, nil
	}
	now := time.Now().UTC()
	result, found, err := replays.LookupRequestReplay(ctx, agentapproval.RequestType, approvalapp.SubmitCommand{
		Principal: approvalapp.SubjectPrincipal{
			ActorType: "agent", WorkspaceID: principal.WorkspaceID,
			PrincipalID: principal.AgentID, MemberID: principal.MemberID,
			MachineID: principal.MachineID,
		},
		ClientRequestID: requestID,
		Payload: agentapproval.SubmitInput{
			ClientRequestID: requestID, CorrelationID: correlationID,
			Now: now, Payload: payload,
		},
		CorrelationID: correlationID,
		Now:           now,
	})
	if err != nil || !found {
		return api.Response{}, found, err
	}
	response, err := handler.presentAgentManagementResult(ctx, result)
	return response, true, err
}

// submitAgentManagement submits the credential-free proposal to the
// agent_management approval subject, projects the owner-Human pending card, and
// returns the pending approval id. Target ownership must already be validated.
func (handler *AgentManagementHandler) submitAgentManagement(ctx context.Context, principal agent.AuthenticatedAgent, input agentapproval.SubmitInput, correlationID string) (api.Response, error) {
	now := time.Now().UTC()
	input.Now = now
	result, err := handler.approvals.SubmitRequest(ctx, agentapproval.RequestType, approvalapp.SubmitCommand{
		Principal: approvalapp.SubjectPrincipal{
			ActorType: "agent", WorkspaceID: principal.WorkspaceID,
			PrincipalID: principal.AgentID, MemberID: principal.MemberID,
			MachineID: principal.MachineID,
		},
		ClientRequestID: input.ClientRequestID,
		Payload:         input,
		CorrelationID:   correlationID,
		Now:             now,
	})
	if err != nil {
		return api.Response{}, err
	}
	return handler.presentAgentManagementResult(ctx, result)
}

func (handler *AgentManagementHandler) presentAgentManagementResult(ctx context.Context, result approvalapp.SubmitResult) (api.Response, error) {
	if result.Instance.State == approval.StatePending {
		if _, err := handler.approvals.PresentPending(ctx, result); err != nil {
			return api.Response{}, err
		}
	}
	return api.OK(agentManagementResponse{Status: "pending", ApprovalID: result.Instance.ID}), nil
}
