package daemon

import (
	"context"
	"encoding/json"
	"errors"
	"strings"
	"sync"

	"github.com/tidbcloud/AgentTeam/internal/observability/logging"
	"github.com/tidbcloud/AgentTeam/internal/runtimecontrol"
	"github.com/tidbcloud/AgentTeam/internal/store/agent"
)

type reviewSummaryRemoteRequestPayload struct {
	AgentID        string `json:"agentId"`
	TargetPath     string `json:"targetPath,omitempty"`
	BaseRef        string `json:"baseRef,omitempty"`
	HeadRef        string `json:"headRef,omitempty"`
	ComparisonMode string `json:"comparisonMode,omitempty"`
}

type reviewSummaryRemoteRequestResponse struct {
	AgentID string                    `json:"agentId"`
	Summary *agent.AgentReviewSummary `json:"summary,omitempty"`
}

type reviewSnapshotRemoteRequestResponse struct {
	AgentID  string                     `json:"agentId"`
	Snapshot *agent.AgentReviewSnapshot `json:"snapshot,omitempty"`
}

type reviewFingerprintRemoteRequestResponse struct {
	AgentID     string                        `json:"agentId"`
	Fingerprint *agent.AgentReviewFingerprint `json:"fingerprint,omitempty"`
}

type reviewFilesRemoteRequestPayload struct {
	AgentID        string `json:"agentId"`
	TargetPath     string `json:"targetPath,omitempty"`
	Scope          string `json:"scope"`
	BaseRef        string `json:"baseRef,omitempty"`
	HeadRef        string `json:"headRef,omitempty"`
	ComparisonMode string `json:"comparisonMode,omitempty"`
}

type reviewFilesRemoteRequestResponse struct {
	AgentID string                  `json:"agentId"`
	Files   []agent.AgentReviewFile `json:"files"`
}

type reviewDiffRemoteRequestPayload struct {
	AgentID        string `json:"agentId"`
	TargetPath     string `json:"targetPath,omitempty"`
	Scope          string `json:"scope"`
	Path           string `json:"path"`
	BaseRef        string `json:"baseRef,omitempty"`
	HeadRef        string `json:"headRef,omitempty"`
	ComparisonMode string `json:"comparisonMode,omitempty"`
}

type reviewDiffRemoteRequestResponse struct {
	AgentID string                 `json:"agentId"`
	Diff    *agent.AgentReviewDiff `json:"diff,omitempty"`
}

// reviewDiffBatchRemoteRequestPayload is the wire shape of review.diff_batch.
// Paths is the raw caller-supplied list: entries are NOT trimmed because Git
// paths may legitimately contain leading/trailing whitespace. Empty paths are
// kept and surfaced as per-path errors by the desktop handler so the caller
// can correlate with its input ordering. Tree-level product limitations still
// travel as the batch's top-level outcome (for example review_target_unsupported).
type reviewDiffBatchRemoteRequestPayload struct {
	AgentID        string   `json:"agentId"`
	TargetPath     string   `json:"targetPath,omitempty"`
	Scope          string   `json:"scope"`
	Paths          []string `json:"paths"`
	BaseRef        string   `json:"baseRef,omitempty"`
	HeadRef        string   `json:"headRef,omitempty"`
	ComparisonMode string   `json:"comparisonMode,omitempty"`
}

// reviewDiffBatchEntry is one path's outcome in a review.diff_batch response.
// Status mirrors the single-path API: "ok" carries the diff; "error" carries
// a human-readable error message; "not_changed" is a soft skip used when the
// path is not part of the selected review scope (the desktop already returns
// a types.ReviewDiff with Status="not_changed" for this case, which is
// preserved as a normal ok entry below).
type reviewDiffBatchEntry struct {
	Diff  *agent.AgentReviewDiff `json:"diff,omitempty"`
	Error string                 `json:"error,omitempty"`
}

type reviewDiffBatchRemoteRequestResponse struct {
	AgentID string                          `json:"agentId"`
	Diffs   map[string]reviewDiffBatchEntry `json:"diffs"`
}

type outputTargetValidateRemoteRequestPayload struct {
	AgentID    string `json:"agentId"`
	Kind       string `json:"kind"`
	TargetPath string `json:"targetPath"`
}

type outputTargetValidateRemoteRequestResponse struct {
	AgentID string `json:"agentId"`
	Valid   bool   `json:"valid"`
	Error   string `json:"error,omitempty"`
}

func (hub *Hub) reviewSummary(ctx context.Context, client *clientState, agentID string, targetPath string, baseRef string, headRef string, comparisonMode string) (agent.AgentReviewSummary, error) {
	if client == nil {
		return agent.AgentReviewSummary{}, errors.New("desktop_unavailable")
	}
	descriptor, ok := hub.remoteEndpointDescriptor(remoteEndpointReviewSummary, 1)
	if !ok || hub.elasticRequestLinks == nil {
		return client.reviewSummary(ctx, agentID, targetPath, baseRef, headRef, comparisonMode)
	}
	return invokeRemoteEndpoint(RemoteEndpointInvocation[agent.AgentReviewSummary]{
		Context:      ctx,
		Client:       client,
		Manager:      hub.elasticRequestLinks,
		Descriptor:   descriptor,
		RouteOptions: remoteRequestRouteOptionsFromContext(ctx),
		Fallback: func() (agent.AgentReviewSummary, error) {
			return client.reviewSummary(ctx, agentID, targetPath, baseRef, headRef, comparisonMode)
		},
		BuildSpec: func() (RemoteRequestAssignmentSpec, error) {
			return newReviewSummaryRemoteRequestSpec(ctx, descriptor, agentID, targetPath, baseRef, headRef, comparisonMode)
		},
		MapResult: reviewSummaryFromRemoteResult,
	})
}

func (hub *Hub) reviewSnapshot(ctx context.Context, client *clientState, agentID string, targetPath string, baseRef string, headRef string, comparisonMode string) (agent.AgentReviewSnapshot, error) {
	if client == nil {
		return agent.AgentReviewSnapshot{}, errors.New("desktop_unavailable")
	}
	descriptor, ok := hub.remoteEndpointDescriptor(remoteEndpointReviewSnapshot, 1)
	if !ok || hub.elasticRequestLinks == nil || !client.supportsRemoteEndpoint(remoteEndpointReviewSnapshot, 1, descriptor.RegistryVersion) {
		return agent.AgentReviewSnapshot{}, runtimecontrol.ErrDaemonReviewSnapshotUnavailable
	}
	return invokeRemoteEndpoint(RemoteEndpointInvocation[agent.AgentReviewSnapshot]{
		Context:      ctx,
		Client:       client,
		Manager:      hub.elasticRequestLinks,
		Descriptor:   descriptor,
		RouteOptions: remoteRequestRouteOptionsFromContext(ctx),
		Fallback: func() (agent.AgentReviewSnapshot, error) {
			return agent.AgentReviewSnapshot{}, runtimecontrol.ErrDaemonReviewSnapshotUnavailable
		},
		BuildSpec: func() (RemoteRequestAssignmentSpec, error) {
			return newReviewSnapshotRemoteRequestSpec(ctx, descriptor, agentID, targetPath, baseRef, headRef, comparisonMode)
		},
		MapResult: reviewSnapshotFromRemoteResult,
	})
}

func (hub *Hub) reviewFingerprint(ctx context.Context, client *clientState, agentID string, targetPath string, baseRef string, headRef string, comparisonMode string) (agent.AgentReviewFingerprint, error) {
	if client == nil {
		return agent.AgentReviewFingerprint{}, errors.New("desktop_unavailable")
	}
	descriptor, ok := hub.remoteEndpointDescriptor(remoteEndpointReviewFingerprint, 1)
	if !ok || hub.elasticRequestLinks == nil || !client.supportsRemoteEndpoint(remoteEndpointReviewFingerprint, 1, descriptor.RegistryVersion) {
		return agent.AgentReviewFingerprint{}, runtimecontrol.ErrDaemonReviewFingerprintUnavailable
	}
	return invokeRemoteEndpoint(RemoteEndpointInvocation[agent.AgentReviewFingerprint]{
		Context:      ctx,
		Client:       client,
		Manager:      hub.elasticRequestLinks,
		Descriptor:   descriptor,
		RouteOptions: remoteRequestRouteOptionsFromContext(ctx),
		Fallback: func() (agent.AgentReviewFingerprint, error) {
			return agent.AgentReviewFingerprint{}, runtimecontrol.ErrDaemonReviewFingerprintUnavailable
		},
		BuildSpec: func() (RemoteRequestAssignmentSpec, error) {
			return newReviewFingerprintRemoteRequestSpec(ctx, descriptor, agentID, targetPath, baseRef, headRef, comparisonMode)
		},
		MapResult: reviewFingerprintFromRemoteResult,
	})
}

func (hub *Hub) reviewFiles(ctx context.Context, client *clientState, agentID string, targetPath string, scope string, baseRef string, headRef string, comparisonMode string) ([]agent.AgentReviewFile, error) {
	if client == nil {
		return nil, errors.New("desktop_unavailable")
	}
	descriptor, ok := hub.remoteEndpointDescriptor(remoteEndpointReviewFiles, 1)
	if !ok || hub.elasticRequestLinks == nil {
		return client.reviewFiles(ctx, agentID, targetPath, scope, baseRef, headRef, comparisonMode)
	}
	return invokeRemoteEndpoint(RemoteEndpointInvocation[[]agent.AgentReviewFile]{
		Context:      ctx,
		Client:       client,
		Manager:      hub.elasticRequestLinks,
		Descriptor:   descriptor,
		RouteOptions: remoteRequestRouteOptionsFromContext(ctx),
		Fallback: func() ([]agent.AgentReviewFile, error) {
			return client.reviewFiles(ctx, agentID, targetPath, scope, baseRef, headRef, comparisonMode)
		},
		BuildSpec: func() (RemoteRequestAssignmentSpec, error) {
			return newReviewFilesRemoteRequestSpec(ctx, descriptor, agentID, targetPath, scope, baseRef, headRef, comparisonMode)
		},
		MapResult: reviewFilesFromRemoteResult,
	})
}

func (hub *Hub) reviewDiff(ctx context.Context, client *clientState, agentID string, targetPath string, scope string, path string, baseRef string, headRef string, comparisonMode string) (agent.AgentReviewDiff, error) {
	if client == nil {
		return agent.AgentReviewDiff{}, errors.New("desktop_unavailable")
	}
	descriptor, ok := hub.remoteEndpointDescriptor(remoteEndpointReviewDiff, 1)
	if !ok || hub.elasticRequestLinks == nil {
		return client.reviewDiff(ctx, agentID, targetPath, scope, path, baseRef, headRef, comparisonMode)
	}
	return invokeRemoteEndpoint(RemoteEndpointInvocation[agent.AgentReviewDiff]{
		Context:      ctx,
		Client:       client,
		Manager:      hub.elasticRequestLinks,
		Descriptor:   descriptor,
		RouteOptions: remoteRequestRouteOptionsFromContext(ctx),
		Fallback: func() (agent.AgentReviewDiff, error) {
			return client.reviewDiff(ctx, agentID, targetPath, scope, path, baseRef, headRef, comparisonMode)
		},
		BuildSpec: func() (RemoteRequestAssignmentSpec, error) {
			return newReviewDiffRemoteRequestSpec(ctx, descriptor, agentID, targetPath, scope, path, baseRef, headRef, comparisonMode)
		},
		MapResult: reviewDiffFromRemoteResult,
	})
}

// AgentReviewDiffBatchResult is one path's outcome from GetAgentReviewDiffBatch.
// Per-path errors are surfaced here rather than as the batch's top-level error
// so a single bad path does not discard the rest of the batch.
type AgentReviewDiffBatchResult struct {
	Diff  agent.AgentReviewDiff
	Error error
}

const reviewDiffBatchFallbackConcurrency = 8

type reviewDiffBatchFallbackLoader func(context.Context, string) (agent.AgentReviewDiff, error)

type reviewDiffBatchFallbackJob struct {
	index int
	path  string
}

func resolveReviewDiffBatchFallbackJob(
	ctx context.Context,
	job reviewDiffBatchFallbackJob,
	outcomes []AgentReviewDiffBatchResult,
	load reviewDiffBatchFallbackLoader,
) {
	if err := ctx.Err(); err != nil {
		outcomes[job.index] = AgentReviewDiffBatchResult{Error: err}
		return
	}
	diff, err := load(ctx, job.path)
	if err != nil {
		outcomes[job.index] = AgentReviewDiffBatchResult{Error: err}
		return
	}
	outcomes[job.index] = AgentReviewDiffBatchResult{Diff: diff}
}

// resolveReviewDiffBatchFallback runs the old-Desktop per-path RPC fallback
// with bounded concurrency. Every worker writes to a distinct indexed result
// slot; callers assemble the path-keyed map only after all workers stop.
func resolveReviewDiffBatchFallback(ctx context.Context, paths []string, concurrency int, load reviewDiffBatchFallbackLoader) []AgentReviewDiffBatchResult {
	outcomes := make([]AgentReviewDiffBatchResult, len(paths))
	if len(paths) == 0 {
		return outcomes
	}
	if concurrency <= 0 || concurrency > len(paths) {
		concurrency = len(paths)
	}

	jobs := make(chan reviewDiffBatchFallbackJob)
	var workers sync.WaitGroup
	workers.Add(concurrency)
	for range concurrency {
		logging.Go(nil, func() {
			defer workers.Done()
			for job := range jobs {
				resolveReviewDiffBatchFallbackJob(ctx, job, outcomes, load)
			}
		})
	}

	next := 0
producer:
	for ; next < len(paths); next++ {
		if ctx.Err() != nil {
			break
		}
		select {
		case <-ctx.Done():
			break producer
		case jobs <- reviewDiffBatchFallbackJob{index: next, path: paths[next]}:
		}
	}
	close(jobs)
	if err := ctx.Err(); err != nil {
		for index := next; index < len(paths); index++ {
			outcomes[index] = AgentReviewDiffBatchResult{Error: err}
		}
	}
	workers.Wait()
	return outcomes
}

func reviewDiffBatchFallbackResults(paths []string, outcomes []AgentReviewDiffBatchResult) map[string]AgentReviewDiffBatchResult {
	results := make(map[string]AgentReviewDiffBatchResult, len(paths))
	for index, path := range paths {
		results[path] = outcomes[index]
	}
	return results
}

// reviewDiffBatch fan-outs every requested path through the same desktop
// review.diff_batch endpoint when the connected daemon advertises it, so the
// desktop resolves the repo state once and produces all per-path diffs in a
// single call. If the daemon does NOT advertise review.diff_batch — i.e. an
// older desktop — the function transparently falls back to one review.diff
// call per path. This capability check is the ONLY fallback trigger: any
// error returned by an advertised batch endpoint (timeout, internal error,
// malformed payload) is propagated as-is so a broken batch never silently
// degrades into per-path fan-out.
func (hub *Hub) reviewDiffBatch(ctx context.Context, client *clientState, agentID string, targetPath string, scope string, paths []string, baseRef string, headRef string, comparisonMode string) (map[string]AgentReviewDiffBatchResult, error) {
	if client == nil {
		return nil, errors.New("desktop_unavailable")
	}
	descriptor, ok := hub.remoteEndpointDescriptor(remoteEndpointReviewDiffBatch, 1)
	canUseBatch := ok &&
		hub.elasticRequestLinks != nil &&
		client.supportsRemoteEndpoint(remoteEndpointReviewDiffBatch, 1, descriptor.RegistryVersion)
	if !canUseBatch {
		return hub.reviewDiffBatchFallback(ctx, client, agentID, targetPath, scope, paths, baseRef, headRef, comparisonMode)
	}
	result, err := invokeRemoteEndpoint(RemoteEndpointInvocation[map[string]AgentReviewDiffBatchResult]{
		Context:      ctx,
		Client:       client,
		Manager:      hub.elasticRequestLinks,
		Descriptor:   descriptor,
		RouteOptions: remoteRequestRouteOptionsFromContext(ctx),
		// BuildSpec runs AFTER the capability check, only when the client
		// advertised the endpoint. There is no per-path fallback here on
		// purpose: a real failure inside the batch path must surface instead
		// of silently collapsing to per-path fan-out, which would mask bugs.
		Fallback: nil,
		BuildSpec: func() (RemoteRequestAssignmentSpec, error) {
			return newReviewDiffBatchRemoteRequestSpec(ctx, descriptor, agentID, targetPath, scope, paths, baseRef, headRef, comparisonMode)
		},
		MapResult: reviewDiffBatchFromRemoteResult,
	})
	if err != nil {
		return nil, err
	}
	if result == nil {
		return map[string]AgentReviewDiffBatchResult{}, nil
	}
	return result, nil
}

// reviewDiffBatchFallback issues one review.diff per path when the connected
// daemon does not support review.diff_batch. It preserves per-path error
// isolation: a failure on one path is captured as that path's
// AgentReviewDiffBatchResult.Error and the remaining paths still resolve.
func (hub *Hub) reviewDiffBatchFallback(ctx context.Context, client *clientState, agentID string, targetPath string, scope string, paths []string, baseRef string, headRef string, comparisonMode string) (map[string]AgentReviewDiffBatchResult, error) {
	outcomes := resolveReviewDiffBatchFallback(
		ctx,
		paths,
		reviewDiffBatchFallbackConcurrency,
		func(ctx context.Context, path string) (agent.AgentReviewDiff, error) {
			return hub.reviewDiff(ctx, client, agentID, targetPath, scope, path, baseRef, headRef, comparisonMode)
		},
	)
	return reviewDiffBatchFallbackResults(paths, outcomes), nil
}

func (hub *Hub) validateOutputTarget(ctx context.Context, client *clientState, agentID string, kind string, targetPath string) error {
	if client == nil {
		return errors.New("desktop_unavailable")
	}
	descriptor, ok := hub.remoteEndpointDescriptor(remoteEndpointOutputTargetValidate, 1)
	if !ok || hub.elasticRequestLinks == nil {
		return client.validateOutputTarget(ctx, agentID, kind, targetPath)
	}
	_, err := invokeRemoteEndpoint(RemoteEndpointInvocation[struct{}]{
		Context:      ctx,
		Client:       client,
		Manager:      hub.elasticRequestLinks,
		Descriptor:   descriptor,
		RouteOptions: remoteRequestRouteOptionsFromContext(ctx),
		Fallback: func() (struct{}, error) {
			return struct{}{}, client.validateOutputTarget(ctx, agentID, kind, targetPath)
		},
		BuildSpec: func() (RemoteRequestAssignmentSpec, error) {
			return newOutputTargetValidateRemoteRequestSpec(ctx, descriptor, agentID, kind, targetPath)
		},
		MapResult: outputTargetValidateFromRemoteResult,
	})
	return err
}

func newReviewSummaryRemoteRequestSpec(ctx context.Context, descriptor RemoteEndpointDescriptor, agentID string, targetPath string, baseRef string, headRef string, comparisonMode string) (RemoteRequestAssignmentSpec, error) {
	payload, err := json.Marshal(reviewSummaryRemoteRequestPayload{
		AgentID: strings.TrimSpace(agentID),
		// TargetPath is a raw filesystem path: do not TrimSpace. Two distinct
		// worktrees whose roots differ only by trailing whitespace must reach
		// the Desktop handler as distinct paths, and an empty string is the
		// signal for "use the agent's default workdir" — that contract is
		// preserved by checking == "" downstream, not by trimming here.
		TargetPath:     targetPath,
		BaseRef:        strings.TrimSpace(baseRef),
		HeadRef:        strings.TrimSpace(headRef),
		ComparisonMode: strings.TrimSpace(comparisonMode),
	})
	if err != nil {
		return RemoteRequestAssignmentSpec{}, err
	}
	return newRemoteRequestAssignmentSpec(ctx, descriptor, "review_summary", json.RawMessage(payload))
}

func newReviewSnapshotRemoteRequestSpec(ctx context.Context, descriptor RemoteEndpointDescriptor, agentID string, targetPath string, baseRef string, headRef string, comparisonMode string) (RemoteRequestAssignmentSpec, error) {
	payload, err := json.Marshal(reviewSummaryRemoteRequestPayload{
		AgentID:        strings.TrimSpace(agentID),
		TargetPath:     targetPath,
		BaseRef:        strings.TrimSpace(baseRef),
		HeadRef:        strings.TrimSpace(headRef),
		ComparisonMode: strings.TrimSpace(comparisonMode),
	})
	if err != nil {
		return RemoteRequestAssignmentSpec{}, err
	}
	return newRemoteRequestAssignmentSpec(ctx, descriptor, "review_snapshot", json.RawMessage(payload))
}

func newReviewFingerprintRemoteRequestSpec(ctx context.Context, descriptor RemoteEndpointDescriptor, agentID string, targetPath string, baseRef string, headRef string, comparisonMode string) (RemoteRequestAssignmentSpec, error) {
	payload, err := json.Marshal(reviewSummaryRemoteRequestPayload{
		AgentID:        strings.TrimSpace(agentID),
		TargetPath:     targetPath,
		BaseRef:        strings.TrimSpace(baseRef),
		HeadRef:        strings.TrimSpace(headRef),
		ComparisonMode: strings.TrimSpace(comparisonMode),
	})
	if err != nil {
		return RemoteRequestAssignmentSpec{}, err
	}
	return newRemoteRequestAssignmentSpec(ctx, descriptor, "review_fingerprint", json.RawMessage(payload))
}

func newReviewFilesRemoteRequestSpec(ctx context.Context, descriptor RemoteEndpointDescriptor, agentID string, targetPath string, scope string, baseRef string, headRef string, comparisonMode string) (RemoteRequestAssignmentSpec, error) {
	payload, err := json.Marshal(reviewFilesRemoteRequestPayload{
		AgentID:        strings.TrimSpace(agentID),
		TargetPath:     targetPath,
		Scope:          strings.TrimSpace(scope),
		BaseRef:        strings.TrimSpace(baseRef),
		HeadRef:        strings.TrimSpace(headRef),
		ComparisonMode: strings.TrimSpace(comparisonMode),
	})
	if err != nil {
		return RemoteRequestAssignmentSpec{}, err
	}
	return newRemoteRequestAssignmentSpec(ctx, descriptor, "review_files", json.RawMessage(payload))
}

func newReviewDiffRemoteRequestSpec(ctx context.Context, descriptor RemoteEndpointDescriptor, agentID string, targetPath string, scope string, path string, baseRef string, headRef string, comparisonMode string) (RemoteRequestAssignmentSpec, error) {
	payload, err := json.Marshal(reviewDiffRemoteRequestPayload{
		AgentID: strings.TrimSpace(agentID),
		// TargetPath (worktree root) and Path (Git file path) are both raw —
		// do not TrimSpace. Both may legitimately contain surrounding
		// whitespace or newlines and trimming would point the diff at a
		// different file or worktree. Only emptiness is meaningful, checked
		// downstream via == "".
		TargetPath:     targetPath,
		Scope:          strings.TrimSpace(scope),
		Path:           path,
		BaseRef:        strings.TrimSpace(baseRef),
		HeadRef:        strings.TrimSpace(headRef),
		ComparisonMode: strings.TrimSpace(comparisonMode),
	})
	if err != nil {
		return RemoteRequestAssignmentSpec{}, err
	}
	return newRemoteRequestAssignmentSpec(ctx, descriptor, "review_diff", json.RawMessage(payload))
}

// newReviewDiffBatchRemoteRequestSpec mirrors newReviewDiffRemoteRequestSpec
// for the multi-path payload. Paths are passed through verbatim — Git paths
// may legitimately contain leading/trailing whitespace and trimming here
// would request a different file's diff.
func newReviewDiffBatchRemoteRequestSpec(ctx context.Context, descriptor RemoteEndpointDescriptor, agentID string, targetPath string, scope string, paths []string, baseRef string, headRef string, comparisonMode string) (RemoteRequestAssignmentSpec, error) {
	payload, err := json.Marshal(reviewDiffBatchRemoteRequestPayload{
		AgentID:        strings.TrimSpace(agentID),
		TargetPath:     targetPath,
		Scope:          strings.TrimSpace(scope),
		Paths:          append([]string(nil), paths...),
		BaseRef:        strings.TrimSpace(baseRef),
		HeadRef:        strings.TrimSpace(headRef),
		ComparisonMode: strings.TrimSpace(comparisonMode),
	})
	if err != nil {
		return RemoteRequestAssignmentSpec{}, err
	}
	return newRemoteRequestAssignmentSpec(ctx, descriptor, "review_diff_batch", json.RawMessage(payload))
}

func newOutputTargetValidateRemoteRequestSpec(ctx context.Context, descriptor RemoteEndpointDescriptor, agentID string, kind string, targetPath string) (RemoteRequestAssignmentSpec, error) {
	payload, err := json.Marshal(outputTargetValidateRemoteRequestPayload{
		AgentID:    strings.TrimSpace(agentID),
		Kind:       strings.TrimSpace(kind),
		TargetPath: strings.TrimSpace(targetPath),
	})
	if err != nil {
		return RemoteRequestAssignmentSpec{}, err
	}
	return newRemoteRequestAssignmentSpec(ctx, descriptor, "output_target_validate", json.RawMessage(payload))
}

func reviewSummaryFromRemoteResult(result RemoteRequestResult[json.RawMessage]) (agent.AgentReviewSummary, error) {
	if err := remoteEndpointResultError(result); err != nil {
		return agent.AgentReviewSummary{}, err
	}
	var response reviewSummaryRemoteRequestResponse
	if err := json.Unmarshal(result.Payload, &response); err != nil {
		return agent.AgentReviewSummary{}, err
	}
	if response.Summary == nil {
		return agent.AgentReviewSummary{}, errors.New("review summary result summary is required")
	}
	return *response.Summary, nil
}

func reviewSnapshotFromRemoteResult(result RemoteRequestResult[json.RawMessage]) (agent.AgentReviewSnapshot, error) {
	if err := remoteEndpointResultError(result); err != nil {
		return agent.AgentReviewSnapshot{}, err
	}
	var response reviewSnapshotRemoteRequestResponse
	if err := json.Unmarshal(result.Payload, &response); err != nil {
		return agent.AgentReviewSnapshot{}, err
	}
	if response.Snapshot == nil {
		return agent.AgentReviewSnapshot{}, errors.New("review snapshot result snapshot is required")
	}
	return *response.Snapshot, nil
}

func reviewFingerprintFromRemoteResult(result RemoteRequestResult[json.RawMessage]) (agent.AgentReviewFingerprint, error) {
	if err := remoteEndpointResultError(result); err != nil {
		return agent.AgentReviewFingerprint{}, err
	}
	var response reviewFingerprintRemoteRequestResponse
	if err := json.Unmarshal(result.Payload, &response); err != nil {
		return agent.AgentReviewFingerprint{}, err
	}
	if response.Fingerprint == nil {
		return agent.AgentReviewFingerprint{}, errors.New("review fingerprint result fingerprint is required")
	}
	return *response.Fingerprint, nil
}

func reviewFilesFromRemoteResult(result RemoteRequestResult[json.RawMessage]) ([]agent.AgentReviewFile, error) {
	if err := remoteEndpointResultError(result); err != nil {
		return nil, err
	}
	var response reviewFilesRemoteRequestResponse
	if err := json.Unmarshal(result.Payload, &response); err != nil {
		return nil, err
	}
	if response.Files == nil {
		return []agent.AgentReviewFile{}, nil
	}
	return response.Files, nil
}

func reviewDiffFromRemoteResult(result RemoteRequestResult[json.RawMessage]) (agent.AgentReviewDiff, error) {
	if err := remoteEndpointResultError(result); err != nil {
		return agent.AgentReviewDiff{}, err
	}
	var response reviewDiffRemoteRequestResponse
	if err := json.Unmarshal(result.Payload, &response); err != nil {
		return agent.AgentReviewDiff{}, err
	}
	if response.Diff == nil {
		return agent.AgentReviewDiff{}, errors.New("review diff result diff is required")
	}
	return *response.Diff, nil
}

// reviewDiffBatchFromRemoteResult decodes the desktop's batch response into a
// per-path result map. Each entry's optional Error string is converted to a
// Go error so callers can route it through the same AgentReviewDiffBatchResult
// shape used by the per-path fallback.
func reviewDiffBatchFromRemoteResult(result RemoteRequestResult[json.RawMessage]) (map[string]AgentReviewDiffBatchResult, error) {
	if err := remoteEndpointResultError(result); err != nil {
		return nil, err
	}
	var response reviewDiffBatchRemoteRequestResponse
	if err := json.Unmarshal(result.Payload, &response); err != nil {
		return nil, err
	}
	out := make(map[string]AgentReviewDiffBatchResult, len(response.Diffs))
	for path, entry := range response.Diffs {
		batchResult := AgentReviewDiffBatchResult{}
		if errText := strings.TrimSpace(entry.Error); errText != "" {
			batchResult.Error = errors.New(errText)
		}
		if entry.Diff != nil {
			batchResult.Diff = *entry.Diff
		}
		out[path] = batchResult
	}
	return out, nil
}

func outputTargetValidateFromRemoteResult(result RemoteRequestResult[json.RawMessage]) (struct{}, error) {
	if err := remoteEndpointResultError(result); err != nil {
		return struct{}{}, err
	}
	var response outputTargetValidateRemoteRequestResponse
	if err := json.Unmarshal(result.Payload, &response); err != nil {
		return struct{}{}, err
	}
	if !response.Valid {
		if errText := strings.TrimSpace(response.Error); errText != "" {
			return struct{}{}, errors.New(errText)
		}
		return struct{}{}, errors.New("output target validation failed")
	}
	return struct{}{}, nil
}
