package openai

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

	edgeservice "iop/apps/edge/internal/service"
	"iop/packages/go/singlerequesttemplate"
)

const (
	singleRequestReviewPrompt  = "Review the task, plan, completed work, and verification evidence. Return exactly one JSON object with decision=pass, non-empty output, non-empty checks, non-empty verification, and non-empty summary when approved. Otherwise make exactly one approved workspace tool call to inspect or repair, with no text content. Every relative_path argument must be canonical and workspace-relative: use README.md, never ./README.md, an absolute path, or a parent traversal. After a tool result with error_code=invalid_request, correct the arguments and make exactly one valid tool call; do not pass. After a tool result with error_code=not_found, do not pass or inspect again; make one repair tool call."
	singleRequestReviewStageID = "review"
)

var errSingleRequestReviewStage = errors.New("single-request review stage: failed")

type singleRequestReviewStage struct {
	provider *singleRequestProviderStage
	bridge   *singleRequestWorkToolBridge
}

func newSingleRequestReviewStage(provider *singleRequestProviderStage, bridge *singleRequestWorkToolBridge) *singleRequestReviewStage {
	return &singleRequestReviewStage{provider: provider, bridge: bridge}
}

type singleRequestReviewStageRequest struct {
	RequestID        string
	Task             string
	StageBinding     edgeservice.SingleRequestStageBinding
	Limits           edgeservice.SingleRequestLimits
	NodeRef          string
	SessionID        string
	UsageAttribution string
	Sequence         uint64
	Quality          *singleRequestQualityGate
}

type singleRequestReviewResult struct {
	Output  []byte
	Summary string
}

type singleRequestReviewDecision struct {
	Decision     string `json:"decision"`
	Output       string `json:"output"`
	Checks       string `json:"checks"`
	Verification string `json:"verification"`
	Summary      string `json:"summary"`
}

func (v *singleRequestReviewDecision) UnmarshalJSON(data []byte) error {
	if err := validateSingleRequestObjectFields(data, "decision", "output", "checks", "verification", "summary"); err != nil {
		return err
	}
	type alias singleRequestReviewDecision
	var decoded alias
	if err := json.Unmarshal(data, &decoded); err != nil {
		return err
	}
	*v = singleRequestReviewDecision(decoded)
	return nil
}

type singleRequestReviewProviderResponse struct {
	pass *singleRequestReviewDecision
	call *singleRequestReviewProviderToolCall
}

func (s *singleRequestReviewStage) run(ctx context.Context, req singleRequestReviewStageRequest, ctrl edgeservice.SingleRequestController) (*singleRequestReviewResult, error) {
	quality := singleRequestQualityGateOrNew(req.Quality)
	if s == nil || s.provider == nil || s.provider.service == nil || s.bridge == nil || ctrl == nil || req.RequestID == "" || req.Task == "" || req.Sequence == 0 || req.StageBinding.Dispatch == nil || req.NodeRef == "" {
		return nil, quality.validation(errSingleRequestReviewStage)
	}
	if req.StageBinding.Options["reasoning_effort"] != "high" {
		return nil, quality.validation(errSingleRequestReviewStage)
	}
	binding := ctrl.Binding()
	if binding == nil || binding.Workspace == nil || binding.Workspace.NodeID == "" || binding.Workspace.NodeID != req.NodeRef || binding.Templates.Review == "" {
		return nil, quality.validation(errSingleRequestReviewStage)
	}
	plan, err := ctrl.ReadInternalArtifact(ctx, edgeservice.SingleRequestArtifactPlan)
	if err != nil {
		return nil, quality.serviceFailure(ctx, err, errSingleRequestReviewStage)
	}
	if len(plan) == 0 {
		return nil, quality.malformed(errSingleRequestReviewStage)
	}
	if len(plan) > req.Limits.MaxOutputBytes {
		return nil, quality.length(errSingleRequestReviewStage)
	}
	// Enforce the frozen effective Plan template before extracting IDs. A
	// stored PLAN that fails the exact parser cannot reach provider dispatch,
	// closing the grammar bypass identified in the handoff.
	if _, err := singlerequesttemplate.ParsePlan(binding.Templates.Plan, string(plan), req.Limits.MaxOutputBytes); err != nil {
		return nil, quality.malformed(errSingleRequestReviewStage)
	}
	planIDs, err := singlerequesttemplate.PlanItemIDs(plan)
	if err != nil {
		return nil, quality.malformed(errSingleRequestReviewStage)
	}
	handoff, err := ctrl.ReadInternalArtifact(ctx, edgeservice.SingleRequestArtifactReview)
	if err != nil {
		return nil, quality.serviceFailure(ctx, err, errSingleRequestReviewStage)
	}
	if len(handoff) == 0 {
		return nil, quality.malformed(errSingleRequestReviewStage)
	}
	if len(handoff) > req.Limits.MaxOutputBytes {
		return nil, quality.length(errSingleRequestReviewStage)
	}
	if err := singlerequesttemplate.ValidateReviewHandoff(handoff, planIDs); err != nil {
		return nil, quality.malformed(errSingleRequestReviewStage)
	}
	tools, err := singleRequestWorkTools(binding.Workspace)
	if err != nil {
		return nil, quality.validation(errSingleRequestReviewStage)
	}

	sequence := req.Sequence
	if err := ctrl.SubmitEnvelope(edgeservice.SingleRequestEnvelope{RequestID: req.RequestID, Sequence: sequence, Stage: edgeservice.SingleRequestStateReviewing}); err != nil {
		return nil, quality.serviceFailure(ctx, err, errSingleRequestReviewStage)
	}
	messages := []chatMessage{
		{Role: "system", Content: singleRequestReviewPrompt},
		{Role: "user", Content: "Task:\n" + strings.TrimSpace(req.Task) + "\n\nPLAN:\n" + string(plan) + "\n\nREVIEW HANDOFF:\n" + string(handoff)},
	}
	repairRequired := false
	mutationOccurred := false
	verifiedAfterMutation := false
	invalidToolCorrections := 0
	toolAttempts := 0
	for {
		response, err := s.submit(ctx, req, messages, tools, repairRequired)
		if err != nil {
			return nil, quality.reclassify(err, errSingleRequestReviewStage)
		}
		if response.pass != nil {
			if repairRequired || (mutationOccurred && !verifiedAfterMutation) {
				return nil, quality.malformed(errSingleRequestReviewStage)
			}
			result, err := singleRequestReviewResultFromDecision(*response.pass, req.Limits.MaxOutputBytes)
			if err != nil {
				return nil, quality.malformed(errSingleRequestReviewStage)
			}
			sequence++
			if err := ctrl.SubmitEnvelope(edgeservice.SingleRequestEnvelope{RequestID: req.RequestID, Sequence: sequence, Stage: edgeservice.SingleRequestStateFinalizing, Result: &edgeservice.SingleRequestResult{Output: string(result.Output), Terminal: edgeservice.SingleRequestTerminalDisposition{Kind: edgeservice.SingleRequestTerminalEndTurn}}}); err != nil {
				return nil, quality.serviceFailure(ctx, err, errSingleRequestReviewStage)
			}
			return result, nil
		}
		if response.call == nil || toolAttempts >= req.Limits.MaxToolIterations {
			if response.call != nil {
				return nil, quality.budget(errSingleRequestReviewStage)
			}
			return nil, quality.malformed(errSingleRequestReviewStage)
		}
		arguments, err := decodeSingleRequestWorkToolArguments(response.call.Function.Arguments)
		if err != nil {
			return nil, quality.malformed(errSingleRequestReviewStage)
		}
		arguments = normalizeSingleRequestProviderToolArguments(response.call.Function.Name, arguments)
		isInspection := response.call.Function.Name == edgeservice.InternalWorkspaceToolRead || response.call.Function.Name == edgeservice.InternalWorkspaceToolList
		if !isInspection && !isSingleRequestReviewRepairTool(response.call.Function.Name) {
			return nil, quality.malformed(errSingleRequestReviewStage)
		}
		if repairRequired && isInspection {
			return nil, quality.malformed(errSingleRequestReviewStage)
		}
		current := ctrl.State()
		if current != edgeservice.SingleRequestStateReviewing && current != edgeservice.SingleRequestStateRepairing {
			return nil, quality.validation(errSingleRequestReviewStage)
		}
		// Once a mutation has entered repairing, every later inspection remains
		// in repairing too. The service deliberately rejects repairing ->
		// reviewing, so re-review is a provider dispatch in the saved repairing
		// state rather than another lifecycle transition.
		stage := current
		if !isInspection {
			stage = edgeservice.SingleRequestStateRepairing
		}
		if current != stage {
			sequence++
			if err := ctrl.SubmitEnvelope(edgeservice.SingleRequestEnvelope{RequestID: req.RequestID, Sequence: sequence, Stage: stage}); err != nil {
				return nil, quality.serviceFailure(ctx, err, errSingleRequestReviewStage)
			}
		}
		key := singleRequestWorkToolKey{requestID: req.RequestID, stageID: singleRequestReviewStageID, toolCallID: response.call.ID}
		call := &edgeservice.InternalWorkspaceToolCall{RequestID: req.RequestID, StageID: key.stageID, ToolCallID: key.toolCallID, Name: response.call.Function.Name, Arguments: arguments}
		if err := edgeservice.ValidateInternalWorkspaceToolCall(call); err != nil {
			if invalidToolCorrections >= 1 {
				return nil, quality.malformed(errSingleRequestReviewStage)
			}
			invalidToolCorrections++
			messages = append(messages,
				chatMessage{Role: "assistant", ToolCalls: []any{response.call.asChatToolCall()}},
				chatMessage{Role: "tool", ToolCallID: response.call.ID, ToolName: response.call.Function.Name, Content: singleRequestWorkToolResultContent(edgeservice.InternalWorkspaceToolResult{Status: "invalid", ErrorCode: "invalid_request"}, req.Limits.MaxOutputBytes)},
			)
			continue
		}
		toolAttempts++
		resultCh, err := s.bridge.register(key)
		if err != nil {
			return nil, quality.internalTool(errSingleRequestReviewStage)
		}
		sequence++
		if err := ctrl.SubmitEnvelope(edgeservice.SingleRequestEnvelope{RequestID: req.RequestID, Sequence: sequence, Stage: edgeservice.SingleRequestStateInternalTool, SavedStage: stage, ToolCall: call}); err != nil {
			s.bridge.unregister(key)
			return nil, quality.serviceFailure(ctx, err, errSingleRequestReviewStage)
		}
		toolResult, err := s.bridge.wait(ctx, key, resultCh)
		if err != nil {
			return nil, quality.serviceFailure(ctx, err, errSingleRequestReviewStage)
		}
		if err := quality.observeToolCycle(singleRequestReviewStageID, response.call.Function.Name, arguments, toolResult, errSingleRequestReviewStage); err != nil {
			return nil, err
		}
		if toolResult.Status == "error" && toolResult.ErrorCode == "not_found" {
			repairRequired = true
		}
		// Ledger semantics are separated from tool admission/lifecycle
		// classification. write/delete record mutation and clear verification;
		// a successful command with a prior mutation records post-mutation
		// verification. A command that repairs an outstanding not_found clears
		// that repair gate, but remains a mutation and cannot immediately PASS
		// without a later successful read/list/command verification.
		isWriteDelete := response.call.Function.Name == edgeservice.InternalWorkspaceToolWrite || response.call.Function.Name == edgeservice.InternalWorkspaceToolDelete
		if isWriteDelete && toolResult.Status == "success" && toolResult.ErrorCode == "" {
			mutationOccurred = true
			verifiedAfterMutation = false
			if repairRequired {
				repairRequired = false
			}
		}
		if response.call.Function.Name == edgeservice.InternalWorkspaceToolCommand && toolResult.Status == "success" && toolResult.ErrorCode == "" {
			if mutationOccurred {
				verifiedAfterMutation = true
			} else {
				mutationOccurred = true
				verifiedAfterMutation = false
				if repairRequired {
					repairRequired = false
				}
			}
		}
		if (response.call.Function.Name == edgeservice.InternalWorkspaceToolRead || response.call.Function.Name == edgeservice.InternalWorkspaceToolList) && mutationOccurred && toolResult.Status == "success" && toolResult.ErrorCode == "" {
			verifiedAfterMutation = true
		}
		messages = append(messages,
			chatMessage{Role: "assistant", ToolCalls: []any{response.call.asChatToolCall()}},
			chatMessage{Role: "tool", ToolCallID: response.call.ID, ToolName: response.call.Function.Name, Content: singleRequestWorkToolResultContent(toolResult, req.Limits.MaxOutputBytes)},
		)
		sequence++
		if err := ctrl.SubmitEnvelope(edgeservice.SingleRequestEnvelope{RequestID: req.RequestID, Sequence: sequence, Stage: stage, SavedStage: stage}); err != nil {
			return nil, quality.serviceFailure(ctx, err, errSingleRequestReviewStage)
		}
	}
	return nil, quality.budget(errSingleRequestReviewStage)
}

func isSingleRequestReviewRepairTool(name string) bool {
	switch name {
	case edgeservice.InternalWorkspaceToolWrite, edgeservice.InternalWorkspaceToolDelete, edgeservice.InternalWorkspaceToolCommand:
		return true
	default:
		return false
	}
}

func (s *singleRequestReviewStage) submit(ctx context.Context, req singleRequestReviewStageRequest, messages []chatMessage, tools []any, repairRequired bool) (*singleRequestReviewProviderResponse, error) {
	quality := singleRequestQualityGateOrNew(req.Quality)
	dispatch := req.StageBinding.Dispatch
	stageCtx, cancel := providerStageContext(ctx, req.Limits.StageTimeoutMS)
	defer cancel()
	poolReq := edgeservice.ProviderPoolDispatchRequest{
		Run: edgeservice.SubmitRunRequest{NodeRef: req.NodeRef, ModelGroupKey: dispatch.ModelGroupKey, ProviderID: dispatch.ProviderID, UsageAttribution: req.UsageAttribution, SessionID: req.SessionID, TimeoutSec: dispatch.TimeoutSec, MaxQueue: dispatch.MaxQueue, QueueTimeoutMS: dispatch.QueueTimeoutMS, ProviderPool: true},
		Tunnel: edgeservice.SubmitProviderTunnelRequest{CredentialBinding: dispatch.CredentialBindingSnapshot(), NodeRef: req.NodeRef, ModelGroupKey: dispatch.ModelGroupKey, ProviderID: dispatch.ProviderID, UsageAttribution: req.UsageAttribution, Adapter: "openai_compat", Target: dispatch.UpstreamModel, SessionID: req.SessionID, Method: http.MethodPost, Path: "/v1/chat/completions", Stream: false, TimeoutSec: dispatch.TimeoutSec, MaxQueue: dispatch.MaxQueue, QueueTimeoutMS: dispatch.QueueTimeoutMS, ProviderPool: true, BuildBody: func(target string) ([]byte, error) {
			return buildSingleRequestReviewBody(messages, req.StageBinding.Options, tools, target, repairRequired)
		}},
	}
	requirements := singleRequestProviderRequirements(req.StageBinding.Options, tools, singleRequestReviewResponseFormat())
	poolReq.AcceptCandidate = singleRequestProviderCandidatePredicate(dispatch, requirements)
	poolReq.PrepareProtocolTunnel = singleRequestProviderTunnelPreparer(requirements, func(target string) ([]byte, error) {
		return buildSingleRequestReviewBody(messages, req.StageBinding.Options, tools, target, repairRequired)
	})
	result, err := s.provider.service.SubmitProviderPool(stageCtx, poolReq)
	if err != nil || result == nil || result.Tunnel == nil {
		return nil, quality.providerFailure(stageCtx, err, errSingleRequestReviewStage)
	}
	if !providerStageDispatchMatches(result, dispatch) {
		return nil, quality.validation(errSingleRequestReviewStage)
	}
	defer result.Tunnel.Close()
	body, err := collectProviderStageFrames(stageCtx, result.Tunnel.Stream().Frames, req.Limits.MaxOutputBytes)
	if err != nil {
		return nil, quality.providerFailure(stageCtx, err, errSingleRequestReviewStage)
	}
	body, err = normalizeSingleRequestProviderResponse(body, result.DispatchInfo)
	if err != nil {
		return nil, quality.providerFailure(stageCtx, err, errSingleRequestReviewStage)
	}
	response, err := decodeSingleRequestReviewProviderResponse(body, req.Limits.MaxOutputBytes)
	if err != nil {
		if !errors.Is(err, errProviderStageOutputLimit) && !errors.Is(err, errProviderStageContextLimit) {
			return nil, quality.malformed(errSingleRequestReviewStage)
		}
		return nil, quality.providerFailure(stageCtx, err, errSingleRequestReviewStage)
	}
	return response, nil
}

func buildSingleRequestReviewBody(messages []chatMessage, options map[string]any, tools []any, target string, repairRequired bool) ([]byte, error) {
	if target == "" || len(messages) == 0 || len(tools) == 0 || options["reasoning_effort"] != "high" {
		return nil, errSingleRequestReviewStage
	}
	toolChoice := "auto"
	if repairRequired {
		toolChoice = "required"
	}
	body := map[string]any{"model": target, "messages": messages, "tools": tools, "tool_choice": toolChoice, "parallel_tool_calls": false, "response_format": singleRequestReviewResponseFormat(), "stream": false}
	for key, value := range options {
		folded := strings.ToLower(key)
		if isSingleRequestReviewReservedOption(folded) && key != folded {
			return nil, errSingleRequestReviewStage
		}
		if isSingleRequestReviewReservedOption(folded) {
			if folded == "reasoning_effort" && value != "high" {
				return nil, errSingleRequestReviewStage
			}
			continue
		}
		body[key] = value
	}
	body["reasoning_effort"] = "high"
	return json.Marshal(body)
}

func isSingleRequestReviewReservedOption(key string) bool {
	switch key {
	case "model", "messages", "tools", "tool_choice", "parallel_tool_calls", "response_format", "stream", "credential", "credential_binding", "reasoning_effort":
		return true
	default:
		return false
	}
}

func singleRequestReviewResponseFormat() *singleRequestProviderResponseFormat {
	return &singleRequestProviderResponseFormat{
		Type: "json_schema",
		JSONSchema: singleRequestProviderResponseJSONSchema{
			Name:   "single_request_review",
			Strict: true,
			Schema: singleRequestProviderOutputSchema{
				Type: "object",
				Properties: map[string]singleRequestProviderOutputProperty{
					"decision":     {Type: "string", Enum: []string{"pass"}},
					"output":       {Type: "string", MinLength: 1},
					"checks":       {Type: "string", MinLength: 1},
					"verification": {Type: "string", MinLength: 1},
					"summary":      {Type: "string", MinLength: 1},
				},
				Required:             []string{"decision", "output", "checks", "verification", "summary"},
				AdditionalProperties: false,
			},
		},
	}
}

type singleRequestReviewProviderEnvelope struct {
	ID      string                              `json:"id"`
	Object  string                              `json:"object"`
	Created int64                               `json:"created"`
	Model   string                              `json:"model"`
	Choices []singleRequestReviewProviderChoice `json:"choices"`
	Usage   *singleRequestChatUsage             `json:"usage,omitempty"`
}

type singleRequestReviewProviderChoice struct {
	Index        int                                `json:"index"`
	FinishReason string                             `json:"finish_reason"`
	Message      singleRequestReviewProviderMessage `json:"message"`
}

type singleRequestReviewProviderMessage struct {
	Role             string                                `json:"role"`
	Content          *string                               `json:"content"`
	ToolCalls        []singleRequestReviewProviderToolCall `json:"tool_calls"`
	ReasoningContent *string                               `json:"reasoning_content,omitempty"`
	ExtraContent     singleRequestGeminiExtraContent       `json:"extra_content"`
}

type singleRequestReviewProviderToolCall struct {
	ID           string                            `json:"id"`
	Type         string                            `json:"type"`
	Function     singleRequestWorkProviderFunction `json:"function"`
	ExtraContent singleRequestGeminiExtraContent   `json:"extra_content"`
}

func (c singleRequestReviewProviderToolCall) asChatToolCall() map[string]any {
	result := map[string]any{"id": c.ID, "type": c.Type, "function": map[string]any{"name": c.Function.Name, "arguments": c.Function.Arguments}}
	if c.ExtraContent.present {
		result["extra_content"] = c.ExtraContent
	}
	return result
}

func (v *singleRequestReviewProviderEnvelope) UnmarshalJSON(data []byte) error {
	if err := validateSingleRequestObjectFields(data, "id", "object", "created", "model", "choices", "usage"); err != nil {
		return err
	}
	type alias singleRequestReviewProviderEnvelope
	var decoded alias
	if err := json.Unmarshal(data, &decoded); err != nil {
		return err
	}
	*v = singleRequestReviewProviderEnvelope(decoded)
	return nil
}

func (v *singleRequestReviewProviderChoice) UnmarshalJSON(data []byte) error {
	if err := validateSingleRequestObjectFields(data, "index", "finish_reason", "message"); err != nil {
		return err
	}
	type alias singleRequestReviewProviderChoice
	var decoded alias
	if err := json.Unmarshal(data, &decoded); err != nil {
		return err
	}
	*v = singleRequestReviewProviderChoice(decoded)
	return nil
}

func (v *singleRequestReviewProviderMessage) UnmarshalJSON(data []byte) error {
	if err := validateSingleRequestObjectFields(data, "role", "content", "tool_calls", "reasoning_content", "extra_content"); err != nil {
		return err
	}
	type alias singleRequestReviewProviderMessage
	var decoded alias
	if err := json.Unmarshal(data, &decoded); err != nil {
		return err
	}
	*v = singleRequestReviewProviderMessage(decoded)
	return nil
}

func (v *singleRequestReviewProviderToolCall) UnmarshalJSON(data []byte) error {
	if err := validateSingleRequestObjectFields(data, "id", "type", "function", "extra_content"); err != nil {
		return err
	}
	type alias singleRequestReviewProviderToolCall
	var decoded alias
	if err := json.Unmarshal(data, &decoded); err != nil {
		return err
	}
	*v = singleRequestReviewProviderToolCall(decoded)
	return nil
}

func decodeSingleRequestReviewProviderResponse(body []byte, maximum int) (*singleRequestReviewProviderResponse, error) {
	if len(body) == 0 || len(body) > maximum || validateSingleRequestJSON(body) != nil {
		return nil, errSingleRequestReviewStage
	}
	var envelope singleRequestReviewProviderEnvelope
	decoder := json.NewDecoder(bytes.NewReader(body))
	decoder.DisallowUnknownFields()
	if err := decoder.Decode(&envelope); err != nil || len(envelope.Choices) != 1 {
		return nil, errSingleRequestReviewStage
	}
	var extra any
	if err := decoder.Decode(&extra); err != io.EOF {
		return nil, errSingleRequestReviewStage
	}
	choice := envelope.Choices[0]
	message := choice.Message
	if choice.Index != 0 || message.Role != "assistant" {
		return nil, errSingleRequestReviewStage
	}
	if choice.FinishReason == "length" {
		return nil, errors.Join(errSingleRequestReviewStage, errProviderStageOutputLimit)
	}
	if choice.FinishReason == "context_length" || choice.FinishReason == "context_length_exceeded" {
		return nil, errors.Join(errSingleRequestReviewStage, errProviderStageContextLimit)
	}
	if choice.FinishReason == "stop" && message.Content != nil && len(message.ToolCalls) == 0 {
		decision, err := decodeSingleRequestReviewDecision(*message.Content, maximum)
		if err != nil {
			return nil, errSingleRequestReviewStage
		}
		return &singleRequestReviewProviderResponse{pass: decision}, nil
	}
	if choice.FinishReason == "tool_calls" && message.Content == nil && len(message.ToolCalls) == 1 {
		call := message.ToolCalls[0]
		if call.ID == "" || call.Type != "function" || call.Function.Name == "" || call.Function.Arguments == "" {
			return nil, errSingleRequestReviewStage
		}
		return &singleRequestReviewProviderResponse{call: &call}, nil
	}
	return nil, errSingleRequestReviewStage
}

func decodeSingleRequestReviewDecision(raw string, maximum int) (*singleRequestReviewDecision, error) {
	if len(raw) == 0 || len(raw) > maximum || validateSingleRequestJSON([]byte(raw)) != nil {
		return nil, errSingleRequestReviewStage
	}
	decoder := json.NewDecoder(strings.NewReader(raw))
	decoder.DisallowUnknownFields()
	var decision singleRequestReviewDecision
	if err := decoder.Decode(&decision); err != nil {
		return nil, errSingleRequestReviewStage
	}
	var extra any
	if err := decoder.Decode(&extra); err != io.EOF || decision.Decision != "pass" || strings.TrimSpace(decision.Output) == "" || strings.TrimSpace(decision.Checks) == "" || strings.TrimSpace(decision.Verification) == "" || strings.TrimSpace(decision.Summary) == "" {
		return nil, errSingleRequestReviewStage
	}
	return &decision, nil
}

func singleRequestReviewResultFromDecision(decision singleRequestReviewDecision, maximum int) (*singleRequestReviewResult, error) {
	if decision.Decision != "pass" || maximum < 1 {
		return nil, errSingleRequestReviewStage
	}
	if strings.TrimSpace(decision.Output) == "" || len(decision.Output) > maximum {
		return nil, errSingleRequestReviewStage
	}
	return &singleRequestReviewResult{Output: append([]byte(nil), []byte(decision.Output)...), Summary: strings.TrimSpace(decision.Summary)}, nil
}
