package wire

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"log/slog"
	"strings"
	"time"

	"github.com/tidbcloud/AgentTeam/desktop/internal/agent"
	"github.com/tidbcloud/AgentTeam/desktop/internal/api/dashboard"
	"github.com/tidbcloud/AgentTeam/desktop/internal/api/dto"
	"github.com/tidbcloud/AgentTeam/desktop/internal/memorywatcher"
	"github.com/tidbcloud/AgentTeam/desktop/internal/metrics"
	"github.com/tidbcloud/AgentTeam/desktop/internal/profile"
	"github.com/tidbcloud/AgentTeam/desktop/internal/types"
	"github.com/tidbcloud/AgentTeam/desktop/internal/wire/workspacefs"
	"github.com/tidbcloud/AgentTeam/desktop/internal/workspace"
)

type remoteWorkspaceListPayload struct {
	AgentID    string `json:"agentId,omitempty"`
	TargetPath string `json:"targetPath,omitempty"`
	DirPath    string `json:"dirPath,omitempty"`
	Limit      int    `json:"limit,omitempty"`
	Cursor     string `json:"cursor,omitempty"`
}

type remoteWorkspaceListResponse struct {
	AgentID    string             `json:"agentId"`
	DirPath    string             `json:"dirPath,omitempty"`
	RootPath   string             `json:"rootPath,omitempty"`
	Files      []workspacefs.File `json:"files"`
	Limit      int                `json:"limit,omitempty"`
	Cursor     string             `json:"cursor,omitempty"`
	HasMore    bool               `json:"hasMore,omitempty"`
	NextCursor string             `json:"nextCursor,omitempty"`
}

type remoteReviewSummaryPayload 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 remoteReviewSummaryResponse struct {
	AgentID string               `json:"agentId"`
	Summary *types.ReviewSummary `json:"summary,omitempty"`
}

type remoteReviewSnapshotResponse struct {
	AgentID  string                `json:"agentId"`
	Snapshot *types.ReviewSnapshot `json:"snapshot,omitempty"`
}

type remoteReviewFingerprintResponse struct {
	AgentID     string                   `json:"agentId"`
	Fingerprint *types.ReviewFingerprint `json:"fingerprint,omitempty"`
}

type remoteReviewFilesPayload 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 remoteReviewFilesResponse struct {
	AgentID string             `json:"agentId"`
	Files   []types.ReviewFile `json:"files"`
}

type remoteReviewDiffPayload 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 remoteReviewDiffResponse struct {
	AgentID string            `json:"agentId"`
	Diff    *types.ReviewDiff `json:"diff,omitempty"`
}

// remoteReviewDiffBatchPayload is the wire shape for review.diff_batch. Paths
// is the raw caller-supplied list: entries are NOT trimmed because Git paths
// may legitimately contain leading/trailing whitespace or newlines. Empty
// paths are reported as per-path errors in the response so callers can
// correlate with their input.
type remoteReviewDiffBatchPayload 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"`
}

// remoteReviewDiffBatchEntry is one path's outcome. A non-empty Error is the
// stable per-path failure message; the diff is omitted when Error is set.
type remoteReviewDiffBatchEntry struct {
	Diff  *types.ReviewDiff `json:"diff,omitempty"`
	Error string            `json:"error,omitempty"`
}

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

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

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

type remoteWorkspaceReadPayload struct {
	AgentID    string `json:"agentId,omitempty"`
	TargetPath string `json:"targetPath,omitempty"`
	Path       string `json:"path"`
}

type remoteWorkspaceReadResponse struct {
	AgentID string  `json:"agentId"`
	Content *string `json:"content"`
	Binary  bool    `json:"binary"`
}

type remoteWorkspaceApplySeedFilesPayload struct {
	AgentID   string                    `json:"agentId"`
	SeedFiles []types.WorkspaceSeedFile `json:"seedFiles"`
}

type remoteWorkspaceApplySeedFilesResponse struct {
	AgentID string `json:"agentId"`
	Success bool   `json:"success"`
}

type remoteWorkspaceDeletePayload struct {
	AgentID string `json:"agentId"`
	Path    string `json:"path"`
}

type remoteWorkspaceDeleteResponse struct {
	AgentID string `json:"agentId"`
	Success bool   `json:"success"`
}

type remoteMachineWorkspaceScanPayload struct {
	Limit  int    `json:"limit,omitempty"`
	Cursor string `json:"cursor,omitempty"`
}

type remoteMachineWorkspaceScanResponse struct {
	Directories []workspacefs.DirInfo `json:"directories"`
	Limit       int                   `json:"limit,omitempty"`
	Cursor      string                `json:"cursor,omitempty"`
	HasMore     bool                  `json:"hasMore,omitempty"`
	NextCursor  string                `json:"nextCursor,omitempty"`
}

type remoteMachineWorkspaceDeletePayload struct {
	DirectoryName string `json:"directoryName"`
}

type remoteMachineWorkspaceDeleteResponse struct {
	DirectoryName string `json:"directoryName"`
	Success       bool   `json:"success"`
}

// remoteMachineDirectoryListPayload mirrors the JSON body of the
// machine.directory.list Remote Endpoint. Path is a raw filesystem value: an
// empty string means “start at the daemon user's home directory”, and any
// non-empty value is forwarded byte-exact without trimming.
type remoteMachineDirectoryListPayload struct {
	Path   string `json:"path,omitempty"`
	Limit  int    `json:"limit,omitempty"`
	Cursor string `json:"cursor,omitempty"`
}

// remoteMachineDirectoryListResponse carries the bounded directory page.
// Directory browsing fields are always present so the browser can rely on the
// shape, including empty strings and empty arrays.
type remoteMachineDirectoryListResponse struct {
	CurrentPath string                         `json:"currentPath"`
	ParentPath  string                         `json:"parentPath"`
	Directories []workspacefs.MachineDirectory `json:"directories"`
	Limit       int                            `json:"limit"`
	Cursor      string                         `json:"cursor"`
	HasMore     bool                           `json:"hasMore"`
	NextCursor  string                         `json:"nextCursor"`
	Truncated   bool                           `json:"truncated"`
}

type remoteMachineDashboardPayload struct {
	WorkspaceID string   `json:"workspaceId,omitempty"`
	MachineID   string   `json:"machineId,omitempty"`
	Window      string   `json:"window,omitempty"`
	AgentIDs    []string `json:"agentIds,omitempty"`
}

type remoteMachineDashboardResponse struct {
	MachineID      string                                             `json:"machineId"`
	WorkspaceID    string                                             `json:"workspaceId"`
	Window         string                                             `json:"window"`
	AgentCount     int                                                `json:"agentCount"`
	ActiveAgents   int                                                `json:"activeAgents"`
	MessageSummary metrics.AgentMessageSummary                        `json:"messageSummary"`
	MessageTrend   metrics.ServerMessageDirectionalTimeSeriesResponse `json:"messageTrend"`
	MessageBar     metrics.BarSeriesResponse                          `json:"messageBar"`
	TokenSummary   metrics.AgentTokenSummary                          `json:"tokenSummary"`
	TokenTrend     metrics.AgentTokenTimeSeriesResponse               `json:"tokenTrend"`
	InputTokenBar  metrics.BarSeriesResponse                          `json:"inputTokenBar"`
	CachedTokenBar metrics.BarSeriesResponse                          `json:"cachedTokenBar"`
	OutputTokenBar metrics.BarSeriesResponse                          `json:"outputTokenBar"`
	TokenSources   metrics.TokenSourceResult                          `json:"tokenSources"`
	Agents         []remoteMachineDashboardAgent                      `json:"agents"`
}

type remoteMachineDashboardAgent struct {
	AgentID  string `json:"agentId"`
	Name     string `json:"name"`
	Runtime  string `json:"runtime"`
	Active   bool   `json:"active"`
	Received uint64 `json:"received"`
	Sent     uint64 `json:"sent"`
	Input    uint64 `json:"input"`
	Cached   uint64 `json:"cached"`
	Output   uint64 `json:"output"`
	Total    uint64 `json:"total"`
}

type remoteRuntimeTrajectoryHistoryPayload struct {
	AgentID string `json:"agentId"`
	Limit   int    `json:"limit,omitempty"`
	Before  int64  `json:"before,omitempty"`
}

type remoteRuntimeTrajectoryHistoryResponse struct {
	AgentID string                    `json:"agentId"`
	Items   []types.TrajectoryLogItem `json:"items"`
}

type remoteRuntimeRestoreRecoveryPointPayload struct {
	AgentID     string `json:"agentId"`
	Runtime     string `json:"runtime"`
	Model       string `json:"model,omitempty"`
	SessionID   string `json:"sessionId,omitempty"`
	Strategy    string `json:"strategy"`
	RecoveryRef string `json:"recoveryRef"`
}

type remoteRuntimeRestoreRecoveryPointResponse struct {
	AgentID string `json:"agentId"`
	Success bool   `json:"success"`
}

type remoteMemorySyncRuntime interface {
	MemorySyncStatus(agentID string) (memorywatcher.Status, bool)
	RetryMemorySync(ctx context.Context, agentID string)
}

type remoteMemorySyncPayload struct {
	AgentID string `json:"agentId"`
}

type remoteMemorySyncStatusResponse struct {
	Available bool                    `json:"available"`
	Status    *remoteMemorySyncStatus `json:"status,omitempty"`
}

type remoteMemorySyncStatus struct {
	State   memorywatcher.StatusState  `json:"state"`
	Reason  memorywatcher.StatusReason `json:"reason,omitempty"`
	RetryAt time.Time                  `json:"retryAt,omitzero"`
}

type remoteMemorySyncRetryResponse struct {
	RetryRequested bool `json:"retryRequested"`
}

func handleRemoteRuntimeMemorySyncStatus(ctx context.Context, c *Protocol, assignment remoteRequestAssignmentMsg) (json.RawMessage, string, error) {
	payload, runtime, err := remoteMemorySyncRequest(c, assignment)
	if err != nil {
		return nil, "protocol_error", err
	}
	select {
	case <-ctx.Done():
		return nil, "", ctx.Err()
	default:
	}
	status, available := runtime.MemorySyncStatus(payload.AgentID)
	response := remoteMemorySyncStatusResponse{Available: available}
	if available {
		response.Status = &remoteMemorySyncStatus{State: status.State, Reason: status.Reason, RetryAt: status.RetryAt}
	}
	raw, err := json.Marshal(response)
	if err != nil {
		return nil, "endpoint_error", err
	}
	return raw, "ok", nil
}

func handleRemoteRuntimeMemorySyncRetry(ctx context.Context, c *Protocol, assignment remoteRequestAssignmentMsg) (json.RawMessage, string, error) {
	payload, runtime, err := remoteMemorySyncRequest(c, assignment)
	if err != nil {
		return nil, "protocol_error", err
	}
	if _, available := runtime.MemorySyncStatus(payload.AgentID); !available {
		raw, marshalErr := json.Marshal(remoteMemorySyncRetryResponse{})
		if marshalErr != nil {
			return nil, "endpoint_error", marshalErr
		}
		return raw, "ok", nil
	}
	runtime.RetryMemorySync(ctx, payload.AgentID)
	raw, err := json.Marshal(remoteMemorySyncRetryResponse{RetryRequested: true})
	if err != nil {
		return nil, "endpoint_error", err
	}
	return raw, "ok", nil
}

func remoteMemorySyncRequest(c *Protocol, assignment remoteRequestAssignmentMsg) (remoteMemorySyncPayload, remoteMemorySyncRuntime, error) {
	var payload remoteMemorySyncPayload
	if err := json.Unmarshal(assignment.Payload, &payload); err != nil {
		return payload, nil, err
	}
	payload.AgentID = strings.TrimSpace(payload.AgentID)
	if payload.AgentID == "" {
		return payload, nil, errors.New("Memory sync request requires agentId")
	}
	runtime, ok := c.disp.(remoteMemorySyncRuntime)
	if !ok || runtime == nil {
		return payload, nil, errors.New("Memory sync runtime unavailable")
	}
	return payload, runtime, nil
}

func handleRemoteWorkspaceList(ctx context.Context, _ *Protocol, assignment remoteRequestAssignmentMsg) (json.RawMessage, string, error) {
	var payload remoteWorkspaceListPayload
	if err := json.Unmarshal(assignment.Payload, &payload); err != nil {
		return nil, "protocol_error", err
	}
	if strings.TrimSpace(payload.AgentID) == "" && strings.TrimSpace(payload.TargetPath) == "" {
		return nil, "protocol_error", errors.New("workspace.list requires agentId or targetPath")
	}
	select {
	case <-ctx.Done():
		return nil, "", ctx.Err()
	default:
	}
	files, page, err := workspacefs.ListTargetDirPage(payload.AgentID, payload.TargetPath, payload.DirPath, workspacefs.ListOptions{
		Limit:  payload.Limit,
		Cursor: payload.Cursor,
	})
	if err != nil {
		slog.Warn("AgentTeam: remote workspace.list handler error", "agent_id", payload.AgentID, "reason", "endpoint_error")
		return nil, "endpoint_error", err
	}
	response := remoteWorkspaceListResponse{
		AgentID:    payload.AgentID,
		DirPath:    payload.DirPath,
		RootPath:   page.RootPath,
		Files:      files,
		Limit:      page.Limit,
		Cursor:     page.Cursor,
		HasMore:    page.HasMore,
		NextCursor: page.NextCursor,
	}
	raw, err := json.Marshal(response)
	if err != nil {
		return nil, "endpoint_error", err
	}
	return raw, "ok", nil
}

func handleRemoteWorkspaceRead(ctx context.Context, _ *Protocol, assignment remoteRequestAssignmentMsg) (json.RawMessage, string, error) {
	var payload remoteWorkspaceReadPayload
	if err := json.Unmarshal(assignment.Payload, &payload); err != nil {
		return nil, "protocol_error", err
	}
	if (strings.TrimSpace(payload.AgentID) == "" && strings.TrimSpace(payload.TargetPath) == "") || strings.TrimSpace(payload.Path) == "" {
		return nil, "protocol_error", errors.New("workspace.read requires agentId or targetPath, and path")
	}
	select {
	case <-ctx.Done():
		return nil, "", ctx.Err()
	default:
	}
	content, binary, err := workspacefs.ReadTargetFile(payload.AgentID, payload.TargetPath, payload.Path)
	if err != nil {
		slog.Warn("AgentTeam: remote workspace.read handler error", "agent_id", payload.AgentID, "reason", "endpoint_error")
		return nil, "endpoint_error", err
	}
	var contentPtr *string
	if !binary {
		contentPtr = &content
	}
	raw, err := json.Marshal(remoteWorkspaceReadResponse{
		AgentID: payload.AgentID,
		Content: contentPtr,
		Binary:  binary,
	})
	if err != nil {
		return nil, "endpoint_error", err
	}
	return raw, "ok", nil
}

func handleRemoteWorkspaceApplySeedFiles(ctx context.Context, _ *Protocol, assignment remoteRequestAssignmentMsg) (json.RawMessage, string, error) {
	var payload remoteWorkspaceApplySeedFilesPayload
	if err := json.Unmarshal(assignment.Payload, &payload); err != nil {
		return nil, "protocol_error", err
	}
	if strings.TrimSpace(payload.AgentID) == "" {
		return nil, "protocol_error", errors.New("workspace.apply_seed_files requires agentId")
	}
	select {
	case <-ctx.Done():
		return nil, "", ctx.Err()
	default:
	}
	if err := workspacefs.ApplySeedFiles(payload.AgentID, payload.SeedFiles); err != nil {
		slog.Warn("AgentTeam: remote workspace.apply_seed_files handler error", "agent_id", payload.AgentID, "reason", "endpoint_error")
		return nil, "endpoint_error", err
	}
	raw, err := json.Marshal(remoteWorkspaceApplySeedFilesResponse{AgentID: payload.AgentID, Success: true})
	if err != nil {
		return nil, "endpoint_error", err
	}
	return raw, "ok", nil
}

func handleRemoteWorkspaceDelete(ctx context.Context, _ *Protocol, assignment remoteRequestAssignmentMsg) (json.RawMessage, string, error) {
	var payload remoteWorkspaceDeletePayload
	if err := json.Unmarshal(assignment.Payload, &payload); err != nil {
		return nil, "protocol_error", err
	}
	if strings.TrimSpace(payload.AgentID) == "" || strings.TrimSpace(payload.Path) == "" {
		return nil, "protocol_error", errors.New("workspace.delete requires agentId and path")
	}
	select {
	case <-ctx.Done():
		return nil, "", ctx.Err()
	default:
	}
	if err := workspacefs.DeletePath(payload.AgentID, payload.Path); err != nil {
		slog.Warn("AgentTeam: remote workspace.delete handler error", "agent_id", payload.AgentID, "reason", "endpoint_error")
		return nil, "endpoint_error", err
	}
	raw, err := json.Marshal(remoteWorkspaceDeleteResponse{AgentID: payload.AgentID, Success: true})
	if err != nil {
		return nil, "endpoint_error", err
	}
	return raw, "ok", nil
}

func handleRemoteReviewSummary(ctx context.Context, _ *Protocol, assignment remoteRequestAssignmentMsg) (json.RawMessage, string, error) {
	var payload remoteReviewSummaryPayload
	if err := json.Unmarshal(assignment.Payload, &payload); err != nil {
		return nil, "protocol_error", err
	}
	if strings.TrimSpace(payload.AgentID) == "" && strings.TrimSpace(payload.TargetPath) == "" {
		return nil, "protocol_error", errors.New("review.summary requires agentId or targetPath")
	}
	select {
	case <-ctx.Done():
		return nil, "", ctx.Err()
	default:
	}
	basePath := reviewBasePath(payload.AgentID, payload.TargetPath)
	summary, err := workspace.ReviewSummary(basePath, payload.BaseRef, payload.HeadRef, payload.ComparisonMode, payload.BaseRef)
	if err != nil {
		outcome := reviewOutcomeForError(err)
		slog.Warn("AgentTeam: remote review.summary handler error", "agent_id", payload.AgentID, "reason", outcome)
		return nil, outcome, err
	}
	raw, err := json.Marshal(remoteReviewSummaryResponse{AgentID: payload.AgentID, Summary: &summary})
	if err != nil {
		return nil, "endpoint_error", err
	}
	return raw, "ok", nil
}

func handleRemoteReviewSnapshot(ctx context.Context, _ *Protocol, assignment remoteRequestAssignmentMsg) (json.RawMessage, string, error) {
	var payload remoteReviewSummaryPayload
	if err := json.Unmarshal(assignment.Payload, &payload); err != nil {
		return nil, "protocol_error", err
	}
	if strings.TrimSpace(payload.AgentID) == "" && strings.TrimSpace(payload.TargetPath) == "" {
		return nil, "protocol_error", errors.New("review.snapshot requires agentId or targetPath")
	}
	select {
	case <-ctx.Done():
		return nil, "", ctx.Err()
	default:
	}
	basePath := reviewBasePath(payload.AgentID, payload.TargetPath)
	snapshot, err := workspace.ReviewSnapshotContext(ctx, basePath, "branch", payload.BaseRef, payload.HeadRef, payload.ComparisonMode, payload.BaseRef)
	if err != nil {
		outcome := reviewOutcomeForError(err)
		slog.Warn("AgentTeam: remote review.snapshot handler error", "agent_id", payload.AgentID, "reason", outcome)
		return nil, outcome, err
	}
	raw, err := json.Marshal(remoteReviewSnapshotResponse{AgentID: payload.AgentID, Snapshot: &snapshot})
	if err != nil {
		return nil, "endpoint_error", err
	}
	return raw, "ok", nil
}

func handleRemoteReviewFingerprint(ctx context.Context, _ *Protocol, assignment remoteRequestAssignmentMsg) (json.RawMessage, string, error) {
	var payload remoteReviewSummaryPayload
	if err := json.Unmarshal(assignment.Payload, &payload); err != nil {
		return nil, "protocol_error", err
	}
	if strings.TrimSpace(payload.AgentID) == "" && strings.TrimSpace(payload.TargetPath) == "" {
		return nil, "protocol_error", errors.New("review.fingerprint requires agentId or targetPath")
	}
	select {
	case <-ctx.Done():
		return nil, "", ctx.Err()
	default:
	}
	basePath := reviewBasePath(payload.AgentID, payload.TargetPath)
	fingerprint, err := workspace.ReviewFingerprint(basePath, payload.BaseRef, payload.HeadRef, payload.ComparisonMode, payload.BaseRef)
	if err != nil {
		outcome := reviewOutcomeForError(err)
		slog.Warn("AgentTeam: remote review.fingerprint handler error", "agent_id", payload.AgentID, "reason", outcome)
		return nil, outcome, err
	}
	raw, err := json.Marshal(remoteReviewFingerprintResponse{AgentID: payload.AgentID, Fingerprint: &fingerprint})
	if err != nil {
		return nil, "endpoint_error", err
	}
	return raw, "ok", nil
}

func handleRemoteReviewFiles(ctx context.Context, _ *Protocol, assignment remoteRequestAssignmentMsg) (json.RawMessage, string, error) {
	var payload remoteReviewFilesPayload
	if err := json.Unmarshal(assignment.Payload, &payload); err != nil {
		return nil, "protocol_error", err
	}
	if (strings.TrimSpace(payload.AgentID) == "" && strings.TrimSpace(payload.TargetPath) == "") || strings.TrimSpace(payload.Scope) == "" {
		return nil, "protocol_error", errors.New("review.files requires agentId or targetPath, and scope")
	}
	select {
	case <-ctx.Done():
		return nil, "", ctx.Err()
	default:
	}
	basePath := reviewBasePath(payload.AgentID, payload.TargetPath)
	files, err := workspace.ReviewFiles(basePath, payload.Scope, payload.BaseRef, payload.HeadRef, payload.ComparisonMode, payload.BaseRef)
	if err != nil {
		outcome := reviewOutcomeForError(err)
		slog.Warn("AgentTeam: remote review.files handler error", "agent_id", payload.AgentID, "reason", outcome)
		return nil, outcome, err
	}
	if files == nil {
		files = []types.ReviewFile{}
	}
	raw, err := json.Marshal(remoteReviewFilesResponse{AgentID: payload.AgentID, Files: files})
	if err != nil {
		return nil, "endpoint_error", err
	}
	return raw, "ok", nil
}

func handleRemoteReviewDiff(ctx context.Context, _ *Protocol, assignment remoteRequestAssignmentMsg) (json.RawMessage, string, error) {
	var payload remoteReviewDiffPayload
	if err := json.Unmarshal(assignment.Payload, &payload); err != nil {
		return nil, "protocol_error", err
	}
	// Path is a raw Git path: do not TrimSpace. A whitespace-only path is a
	// legitimate (if unusual) Git path; rejecting it via TrimSpace would
	// silently point the diff at a different file. AgentID and Scope are
	// enum-like control strings and stay trimmed.
	if (strings.TrimSpace(payload.AgentID) == "" && strings.TrimSpace(payload.TargetPath) == "") || strings.TrimSpace(payload.Scope) == "" || payload.Path == "" {
		return nil, "protocol_error", errors.New("review.diff requires agentId or targetPath, scope and path")
	}
	select {
	case <-ctx.Done():
		return nil, "", ctx.Err()
	default:
	}
	basePath := reviewBasePath(payload.AgentID, payload.TargetPath)
	diff, err := workspace.ReviewDiff(basePath, payload.Scope, payload.Path, payload.BaseRef, payload.HeadRef, payload.ComparisonMode, payload.BaseRef)
	if err != nil {
		outcome := reviewOutcomeForError(err)
		slog.Warn("AgentTeam: remote review.diff handler error", "agent_id", payload.AgentID, "reason", outcome)
		return nil, outcome, err
	}
	raw, err := json.Marshal(remoteReviewDiffResponse{AgentID: payload.AgentID, Diff: &diff})
	if err != nil {
		return nil, "endpoint_error", err
	}
	return raw, "ok", nil
}

// handleRemoteReviewDiffBatch resolves diffs for multiple paths in a single
// desktop call. The workspace layer's ReviewDiffBatch opens the repo and
// resolves review refs / merge-base exactly once, then loops over paths to
// produce per-path diffs — this is the loadReviewRepoState-once contract
// callers depend on for batch latency.
//
// Per-path errors do NOT abort the batch. The handler returns "ok" with a
// per-path entry.Error for ordinary path failures; protocol-level errors and
// tree-level product limitations produce a non-"ok" outcome. In particular,
// workspace.ErrReviewControlCharPath is escalated by ReviewDiffBatch and
// mapped here to review_target_unsupported, matching the single diff endpoint
// so the server can return the stable unsupported response instead of a
// retryable per-path error.
func handleRemoteReviewDiffBatch(ctx context.Context, _ *Protocol, assignment remoteRequestAssignmentMsg) (json.RawMessage, string, error) {
	var payload remoteReviewDiffBatchPayload
	if err := json.Unmarshal(assignment.Payload, &payload); err != nil {
		return nil, "protocol_error", err
	}
	if (strings.TrimSpace(payload.AgentID) == "" && strings.TrimSpace(payload.TargetPath) == "") || strings.TrimSpace(payload.Scope) == "" {
		return nil, "protocol_error", errors.New("review.diff_batch requires agentId or targetPath, and scope")
	}
	if payload.Paths == nil {
		payload.Paths = []string{}
	}
	select {
	case <-ctx.Done():
		return nil, "", ctx.Err()
	default:
	}
	basePath := reviewBasePath(payload.AgentID, payload.TargetPath)
	results, err := workspace.ReviewDiffBatch(basePath, payload.Scope, payload.Paths, payload.BaseRef, payload.HeadRef, payload.ComparisonMode, payload.BaseRef)
	if err != nil {
		outcome := reviewOutcomeForError(err)
		slog.Warn("AgentTeam: remote review.diff_batch handler error", "agent_id", payload.AgentID, "reason", outcome)
		return nil, outcome, err
	}
	entries := make(map[string]remoteReviewDiffBatchEntry, len(results))
	for path, result := range results {
		entry := remoteReviewDiffBatchEntry{}
		if result.Error != nil {
			entry.Error = result.Error.Error()
		}
		if result.Diff.Status != "" || len(result.Diff.Sections) > 0 {
			diff := result.Diff
			entry.Diff = &diff
		}
		entries[path] = entry
	}
	raw, err := json.Marshal(remoteReviewDiffBatchResponse{AgentID: payload.AgentID, Diffs: entries})
	if err != nil {
		return nil, "endpoint_error", err
	}
	return raw, "ok", nil
}

func handleRemoteOutputTargetValidate(ctx context.Context, _ *Protocol, assignment remoteRequestAssignmentMsg) (json.RawMessage, string, error) {
	var payload remoteOutputTargetValidatePayload
	if err := json.Unmarshal(assignment.Payload, &payload); err != nil {
		return nil, "protocol_error", err
	}
	if strings.TrimSpace(payload.AgentID) == "" || strings.TrimSpace(payload.Kind) == "" || strings.TrimSpace(payload.TargetPath) == "" {
		return nil, "protocol_error", errors.New("output_target.validate requires agentId, kind and targetPath")
	}
	select {
	case <-ctx.Done():
		return nil, "", ctx.Err()
	default:
	}
	response := remoteOutputTargetValidateResponse{AgentID: payload.AgentID, Valid: true}
	if err := workspace.ValidateOutputTarget(payload.TargetPath, payload.Kind); err != nil {
		response.Valid = false
		response.Error = err.Error()
	}
	raw, err := json.Marshal(response)
	if err != nil {
		return nil, "endpoint_error", err
	}
	return raw, "ok", nil
}

// reviewBasePath resolves the worktree root for a Review request. targetPath
// is a raw filesystem path: an empty string means "use the agent's default
// workdir" and ANY non-empty value — including pure whitespace — is treated
// as an exact worktree root. TrimSpace is intentionally NOT applied: a real
// worktree whose canonical root differs only by trailing whitespace must not
// be silently rewritten to a different directory.
func reviewBasePath(agentID string, targetPath string) string {
	if targetPath == "" {
		return workspacefs.WorkDir(agentID)
	}
	return targetPath
}

// reviewTargetUnsupportedOutcome is the stable wire outcome string emitted
// by all five Review remote-endpoint handlers when the workspace layer
// raises workspace.ErrReviewControlCharPath. The Hub-side mapper
// (internal/api/daemon/remoteEndpointResultError) reconstructs the typed
// runtimecontrol.ErrReviewTargetUnsupported sentinel from this string.
//
// This is a wire contract: the literal value MUST match the constant
// ReviewTargetUnsupportedOutcomeCode in internal/runtimecontrol. The two
// modules are separate Go modules (desktop vs main) and cannot share a
// Go-level constant, so the string is duplicated with cross-referencing
// comments on both sides. Never change the literal without coordinating
// both modules and bumping the protocol version.
const reviewTargetUnsupportedOutcome = "review_target_unsupported"

// reviewTargetUnsupportedLegacyCode is the parallel stable code carried in
// the ErrorCode field of the legacy agentReview*ResultMsg structs. Same
// contract as reviewTargetUnsupportedOutcome; same literal value; never
// change without coordinating both modules.
const reviewTargetUnsupportedLegacyCode = "review_target_unsupported"

// reviewOutcomeForError picks the stable wire outcome string for a workspace
// error returned by any of the five Review remote-endpoint handlers. The
// Hub-side mapper (remoteEndpointResultError) reconstructs a typed sentinel
// from this outcome so the frontend can distinguish "repo content not
// supported" from generic endpoint failures.
//
// The control-char path condition (workspace.ErrReviewControlCharPath) is
// a product limitation, not a transient failure: the diff engine cannot
// process a tree that contains a path with embedded control characters
// (e.g. newlines), and the frontend surfaces a stable "unsupported" state
// instead of retrying. All other workspace errors stay on the legacy
// "endpoint_error" outcome; the original error value still flows back to
// the executor for diagnostics, but the outcome string is the wire signal
// that survives the cross-process boundary.
func reviewOutcomeForError(err error) string {
	if errors.Is(err, workspace.ErrReviewControlCharPath) {
		return reviewTargetUnsupportedOutcome
	}
	return "endpoint_error"
}

func handleRemoteMachineWorkspaceScan(ctx context.Context, c *Protocol, assignment remoteRequestAssignmentMsg) (json.RawMessage, string, error) {
	var payload remoteMachineWorkspaceScanPayload
	if err := json.Unmarshal(assignment.Payload, &payload); err != nil {
		return nil, "protocol_error", err
	}
	select {
	case <-ctx.Done():
		return nil, "", ctx.Err()
	default:
	}
	// Only return agent workspaces belonging to the requesting server: each
	// connection (c) carries the server's id (c.id), and agents are tagged with
	// Source.ID == that serverID. Filter the scanned dirs so a user never sees
	// other groups' workspaces.
	serverID := ""
	if c != nil {
		serverID = c.id
	}
	allow := func(agentID string) bool {
		cfg, ok := profile.Default().Agents().Get(agentID)
		if !ok {
			return false
		}
		return cfg.Source.Type == "agentteam" && cfg.Source.ID == serverID
	}
	// Agent workspace dirs are bounded (one per agent on the machine), so page
	// through all and filter to the caller's own agents, returned as one page.
	// (Filtering a single bounded page would yield short/empty pages.)
	var dirs []workspacefs.DirInfo
	var lastPage workspacefs.PageInfo
	cursor := strings.TrimSpace(payload.Cursor)
	for {
		page, info, err := workspacefs.ScanAllPage(workspacefs.ListOptions{Limit: payload.Limit, Cursor: cursor})
		if err != nil {
			slog.Warn("AgentTeam: remote machine.workspace.scan handler error", "reason", "endpoint_error")
			return nil, "endpoint_error", err
		}
		lastPage = info
		for _, d := range page {
			if allow(d.DirectoryName) {
				dirs = append(dirs, d)
			}
		}
		if !info.HasMore {
			break
		}
		cursor = info.NextCursor
	}
	raw, err := json.Marshal(remoteMachineWorkspaceScanResponse{
		Directories: dirs,
		Limit:       lastPage.Limit,
		HasMore:     false,
	})
	if err != nil {
		return nil, "endpoint_error", err
	}
	return raw, "ok", nil
}

func handleRemoteMachineWorkspaceDelete(ctx context.Context, _ *Protocol, assignment remoteRequestAssignmentMsg) (json.RawMessage, string, error) {
	var payload remoteMachineWorkspaceDeletePayload
	if err := json.Unmarshal(assignment.Payload, &payload); err != nil {
		return nil, "protocol_error", err
	}
	if strings.TrimSpace(payload.DirectoryName) == "" {
		return nil, "protocol_error", errors.New("machine.workspace.delete requires directoryName")
	}
	select {
	case <-ctx.Done():
		return nil, "", ctx.Err()
	default:
	}
	success, err := workspacefs.DeleteDir(payload.DirectoryName)
	if err != nil {
		slog.Warn("AgentTeam: remote machine.workspace.delete handler error", "reason", "endpoint_error")
		return nil, "endpoint_error", err
	}
	raw, err := json.Marshal(remoteMachineWorkspaceDeleteResponse{DirectoryName: payload.DirectoryName, Success: success})
	if err != nil {
		return nil, "endpoint_error", err
	}
	return raw, "ok", nil
}

// machineDirectoryListDep is the workspacefs-backed list dependency used by
// the machine.directory.list handler. Production wires the OS-backed list
// func; tests inject deterministic failures or synthetic pages without
// mutating package globals or relying on chmod behavior.
type machineDirectoryListDep func(rawPath string, opts workspacefs.ListOptions) ([]workspacefs.MachineDirectory, workspacefs.MachineDirectoryPageInfo, error)

// handleRemoteMachineDirectoryList lists one bounded page of directories on
// the daemon host. Path is a raw filesystem value; the workspacefs layer
// canonicalizes and validates it. Filesystem classification outcomes flow
// back as stable wire strings so the Hub can reconstruct a typed sentinel
// without seeing the original error text or path.
func handleRemoteMachineDirectoryList(ctx context.Context, _ *Protocol, assignment remoteRequestAssignmentMsg) (json.RawMessage, string, error) {
	return runMachineDirectoryList(ctx, workspacefs.ListMachineDirectoriesPage, assignment)
}

// runMachineDirectoryList is the factored core of the handler. It accepts an
// injected workspacefs dependency so tests can drive classification and
// near-budget envelopes deterministically.
func runMachineDirectoryList(ctx context.Context, dep machineDirectoryListDep, assignment remoteRequestAssignmentMsg) (json.RawMessage, string, error) {
	var payload remoteMachineDirectoryListPayload
	if err := json.Unmarshal(assignment.Payload, &payload); err != nil {
		return nil, "protocol_error", err
	}
	select {
	case <-ctx.Done():
		return nil, "", ctx.Err()
	default:
	}
	directories, page, err := dep(payload.Path, workspacefs.ListOptions{
		Limit:  payload.Limit,
		Cursor: payload.Cursor,
	})
	if err != nil {
		outcome := machineDirectoryListOutcomeForError(err)
		slog.Warn("AgentTeam: remote machine.directory.list handler error", "reason", outcome)
		return nil, outcome, err
	}
	if directories == nil {
		directories = []workspacefs.MachineDirectory{}
	}
	raw, err := json.Marshal(remoteMachineDirectoryListResponse{
		CurrentPath: page.CurrentPath,
		ParentPath:  page.ParentPath,
		Directories: directories,
		Limit:       page.Limit,
		Cursor:      page.Cursor,
		HasMore:     page.HasMore,
		NextCursor:  page.NextCursor,
		Truncated:   page.Truncated,
	})
	if err != nil {
		return nil, "endpoint_error", err
	}
	return raw, "ok", nil
}

// machineDirectoryListOutcomeForError maps workspacefs classification
// sentinels to the stable wire outcome strings the Hub post-classifier
// interprets. Only the two stable outcomes are exposed; any unexpected
// implementation error stays on the generic endpoint_error outcome.
func machineDirectoryListOutcomeForError(err error) string {
	switch {
	case errors.Is(err, workspacefs.ErrMachineDirectoryInvalid):
		return "machine_directory_invalid"
	case errors.Is(err, workspacefs.ErrMachineDirectoryUnreadable):
		return "machine_directory_unreadable"
	default:
		return "endpoint_error"
	}
}

func handleRemoteMachineDashboard(ctx context.Context, c *Protocol, assignment remoteRequestAssignmentMsg) (json.RawMessage, string, error) {
	var payload remoteMachineDashboardPayload
	if err := json.Unmarshal(assignment.Payload, &payload); err != nil {
		return nil, "protocol_error", err
	}
	select {
	case <-ctx.Done():
		return nil, "", ctx.Err()
	default:
	}
	serverID := ""
	workspaceID := ""
	machineID := ""
	if c != nil {
		serverID = strings.TrimSpace(c.id)
		workspaceID = strings.TrimSpace(c.workspaceID)
		machineID = strings.TrimSpace(c.machineID)
	}
	payloadWorkspaceID := strings.TrimSpace(payload.WorkspaceID)
	if payloadWorkspaceID != "" && workspaceID != "" && payloadWorkspaceID != workspaceID {
		return nil, remoteMachineDashboardInvalidOutcome, errors.New("machine.dashboard workspaceId does not match this connection")
	}
	if payloadWorkspaceID != "" {
		workspaceID = payloadWorkspaceID
	}
	payloadMachineID := strings.TrimSpace(payload.MachineID)
	if payloadMachineID != "" && machineID != "" && payloadMachineID != machineID {
		return nil, remoteMachineDashboardInvalidOutcome, errors.New("machine.dashboard machineId does not match this connection")
	}
	if payloadMachineID != "" {
		machineID = payloadMachineID
	}
	window, err := parseRemoteDashboardWindow(payload.Window)
	if err != nil {
		return nil, remoteMachineDashboardInvalidOutcome, err
	}

	configuredAgents := remoteDashboardAgentsForServer(serverID)
	configuredByID := make(map[string]agent.Config, len(configuredAgents))
	for _, configured := range configuredAgents {
		configuredByID[configured.ID] = configured
	}
	selectedAgentIDs := uniqueTrimmedStrings(payload.AgentIDs)
	if len(payload.AgentIDs) > 0 && len(selectedAgentIDs) == 0 {
		return nil, remoteMachineDashboardInvalidOutcome, errors.New("machine.dashboard requires at least one valid agentId")
	}
	if len(selectedAgentIDs) > 0 {
		configuredAgents = make([]agent.Config, 0, len(selectedAgentIDs))
		for _, agentID := range selectedAgentIDs {
			configured, exists := configuredByID[agentID]
			if !exists {
				return nil, remoteMachineDashboardInvalidOutcome, errors.New("agentId does not belong to this AgentTeam server connection")
			}
			configuredAgents = append(configuredAgents, configured)
		}
	}
	agentIDs := make([]string, 0, len(configuredAgents))
	for _, configured := range configuredAgents {
		agentIDs = append(agentIDs, configured.ID)
	}

	aggregate := emptyRemoteDashboardAggregate(window)
	if collector := metrics.DefaultServerMessageCollector(); collector != nil {
		aggregate = collector.QueryAgentsDashboard(agentIDs, window)
	}
	rowsByID := make(map[string]metrics.AgentDashboardRow, len(aggregate.Agents))
	for _, row := range aggregate.Agents {
		rowsByID[row.AgentID] = row
	}
	activeIDs := make(map[string]struct{})
	if c != nil {
		for _, snapshot := range c.runningAgentTeamSessions() {
			if agentID := strings.TrimSpace(snapshot.AgentConfigID); agentID != "" {
				activeIDs[agentID] = struct{}{}
			}
			if agentID := strings.TrimSpace(snapshot.AgentID); agentID != "" {
				activeIDs[agentID] = struct{}{}
			}
			if agentID := sessionServerAgentID(snapshot); agentID != "" {
				activeIDs[agentID] = struct{}{}
			}
		}
	}
	agents := make([]remoteMachineDashboardAgent, 0, len(configuredAgents))
	activeCount := 0
	for _, configured := range configuredAgents {
		row := rowsByID[configured.ID]
		_, active := activeIDs[configured.ID]
		if active {
			activeCount++
		}
		agents = append(agents, remoteMachineDashboardAgent{
			AgentID:  configured.ID,
			Name:     configured.Name,
			Runtime:  configured.Runtime,
			Active:   active,
			Received: row.Received,
			Sent:     row.Sent,
			Input:    row.Input,
			Cached:   row.Cached,
			Output:   row.Output,
			Total:    row.Total,
		})
	}
	tokenSources := metrics.UnavailableTokenSourceResult(window)
	if querier := metrics.DefaultTokenTrackingQuerier(); querier != nil {
		tokenSources = querier.QueryTokenTracking(metrics.TokenSourceQuery{
			AgentIDs: agentIDs,
			Window:   window,
		})
	}
	raw, err := json.Marshal(remoteMachineDashboardResponse{
		MachineID:      machineID,
		WorkspaceID:    workspaceID,
		Window:         window.String(),
		AgentCount:     len(agents),
		ActiveAgents:   activeCount,
		MessageSummary: aggregate.MessageSummary,
		MessageTrend:   aggregate.MessageTrend,
		MessageBar:     aggregate.MessageBar,
		TokenSummary:   aggregate.TokenSummary,
		TokenTrend:     aggregate.TokenTrend,
		InputTokenBar:  aggregate.InputTokenBar,
		CachedTokenBar: aggregate.CachedTokenBar,
		OutputTokenBar: aggregate.OutputTokenBar,
		TokenSources:   tokenSources,
		Agents:         agents,
	})
	if err != nil {
		return nil, "endpoint_error", err
	}
	return raw, "ok", nil
}

// remoteMachineDashboardInvalidOutcome mirrors runtimecontrol.MachineDashboardInvalidOutcome
// in the Hub module. The Desktop module cannot import that package, so keep
// this wire literal in sync with the Hub-side result mapper.
const remoteMachineDashboardInvalidOutcome = "machine_dashboard_invalid"

func parseRemoteDashboardWindow(value string) (metrics.TimeWindow, error) {
	switch strings.TrimSpace(value) {
	case "", "1d":
		return metrics.TimeWindow1Day, nil
	case "6h":
		return metrics.TimeWindow6Hours, nil
	case "12h":
		return metrics.TimeWindow12Hours, nil
	case "3d":
		return metrics.TimeWindow3Days, nil
	case "1w":
		return metrics.TimeWindow1Week, nil
	default:
		return 0, fmt.Errorf("invalid time window, must be one of: 6h, 12h, 1d, 3d, 1w")
	}
}

func remoteDashboardAgentsForServer(serverID string) []agent.Config {
	all := profile.Default().Agents().All()
	result := make([]agent.Config, 0, len(all))
	for _, configured := range all {
		if configured.Source.Type == "agentteam" && strings.TrimSpace(configured.Source.ID) == strings.TrimSpace(serverID) {
			result = append(result, configured)
		}
	}
	return result
}

func uniqueTrimmedStrings(values []string) []string {
	if len(values) == 0 {
		return nil
	}
	result := make([]string, 0, len(values))
	seen := make(map[string]struct{}, len(values))
	for _, value := range values {
		value = strings.TrimSpace(value)
		if value == "" {
			continue
		}
		if _, exists := seen[value]; exists {
			continue
		}
		seen[value] = struct{}{}
		result = append(result, value)
	}
	return result
}

func emptyRemoteDashboardAggregate(window metrics.TimeWindow) metrics.AgentDashboardMetrics {
	return metrics.AgentDashboardMetrics{
		MessageTrend: metrics.ServerMessageDirectionalTimeSeriesResponse{
			Interval: int(metrics.MachineDashboardOverviewAggregationInterval(window).Seconds()),
			Data:     []metrics.ServerMessageDirectionalTimeSeriesPoint{},
		},
		TokenTrend: metrics.AgentTokenTimeSeriesResponse{
			Interval: int(metrics.MachineDashboardOverviewAggregationInterval(window).Seconds()),
			Data:     []metrics.TimeSeriesDataPoint{},
		},
		MessageBar:     metrics.BarSeriesResponse{Interval: int(metrics.MachineDashboardOverviewAggregationInterval(window).Seconds()), Series: []metrics.BarSeriesEntry{{Key: "inbound", Label: "Inbound"}, {Key: "outbound", Label: "Outbound"}}, Data: []metrics.BarSeriesPoint{}},
		InputTokenBar:  metrics.BarSeriesResponse{Interval: int(metrics.MachineDashboardOverviewAggregationInterval(window).Seconds()), Series: []metrics.BarSeriesEntry{}, Data: []metrics.BarSeriesPoint{}},
		CachedTokenBar: metrics.BarSeriesResponse{Interval: int(metrics.MachineDashboardOverviewAggregationInterval(window).Seconds()), Series: []metrics.BarSeriesEntry{}, Data: []metrics.BarSeriesPoint{}},
		OutputTokenBar: metrics.BarSeriesResponse{Interval: int(metrics.MachineDashboardOverviewAggregationInterval(window).Seconds()), Series: []metrics.BarSeriesEntry{}, Data: []metrics.BarSeriesPoint{}},
		Agents:         []metrics.AgentDashboardRow{},
	}
}

func handleRemoteRuntimeTrajectoryHistory(ctx context.Context, channel *Protocol, assignment remoteRequestAssignmentMsg) (json.RawMessage, string, error) {
	var payload remoteRuntimeTrajectoryHistoryPayload
	if err := json.Unmarshal(assignment.Payload, &payload); err != nil {
		return nil, "protocol_error", err
	}
	if strings.TrimSpace(payload.AgentID) == "" {
		return nil, "protocol_error", errors.New("runtime_access.trajectory_history requires agentId")
	}
	select {
	case <-ctx.Done():
		return nil, "", ctx.Err()
	default:
	}
	items := []types.TrajectoryLogItem{}
	if channel.disp == nil {
		return nil, "endpoint_error", errors.New("dispatcher unavailable")
	}
	items = channel.disp.TrajectoryHistory(payload.AgentID, payload.Limit, payload.Before)
	if items == nil {
		items = []types.TrajectoryLogItem{}
	}
	raw, err := json.Marshal(remoteRuntimeTrajectoryHistoryResponse{AgentID: payload.AgentID, Items: items})
	if err != nil {
		return nil, "endpoint_error", err
	}
	return raw, "ok", nil
}

func handleRemoteRuntimeRestoreRecoveryPoint(ctx context.Context, channel *Protocol, assignment remoteRequestAssignmentMsg) (json.RawMessage, string, error) {
	var payload remoteRuntimeRestoreRecoveryPointPayload
	if err := json.Unmarshal(assignment.Payload, &payload); err != nil {
		return nil, "protocol_error", err
	}
	if strings.TrimSpace(payload.AgentID) == "" || strings.TrimSpace(payload.Strategy) == "" || strings.TrimSpace(payload.RecoveryRef) == "" {
		return nil, "protocol_error", errors.New("runtime_access.restore_recovery_point requires agentId, strategy and recoveryRef")
	}
	select {
	case <-ctx.Done():
		return nil, "", ctx.Err()
	default:
	}
	if channel.disp == nil {
		return nil, "endpoint_error", errors.New("dispatcher unavailable")
	}
	if err := channel.disp.RestoreAgentRecoveryPoint(channel.id, payload.AgentID, remoteRequestID(assignment), payload.Runtime, payload.Model, payload.SessionID, payload.Strategy, payload.RecoveryRef); err != nil {
		slog.Warn("AgentTeam: remote runtime_access.restore_recovery_point handler error", "agent_id", payload.AgentID, "reason", "endpoint_error")
		return nil, "endpoint_error", err
	}
	raw, err := json.Marshal(remoteRuntimeRestoreRecoveryPointResponse{AgentID: payload.AgentID, Success: true})
	if err != nil {
		return nil, "endpoint_error", err
	}
	return raw, "ok", nil
}

// ---------------------------------------------------------------------------
// Machine Detail remote endpoints (settings / runtimes / mcp-servers)
//
// These handlers back the Machine Detail tabs exposed via the server-side
// Remote RPC bridge. They mirror the contract exposed by the existing gin
// handlers under /api/internal/* but are callable without an HTTP context,
// so the bridge can serve browser requests routed through the server.
// ---------------------------------------------------------------------------

// machineDetailInvalidOutcome mirrors the *_invalid outcome strings the
// Hub post-classifier interprets. Each handler below emits a
// machine_<resource>_invalid literal on validation/argument failures so
// the bridge can map them back to structured errors.
const (
	machineSettingsInvalidOutcome       = "machine_settings_invalid"
	machineRuntimesInvalidOutcome       = "machine_runtimes_invalid"
	machineMCPServersInvalidOutcome     = "machine_mcp_servers_invalid"
	machineMCPServersNotFoundOutcome    = "machine_mcp_servers_not_found"
	machineRuntimeConfigNotFoundOutcome = "machine_runtime_config_not_found"
)

// remoteMachineDetailRequest is the common envelope for all machine detail
// requests — workspaceId/machineId reconcile against the carrying
// connection (mirrors handleRemoteMachineDashboard L846-867). Per-endpoint
// payload structs embed the additional fields they need.
type remoteMachineDetailRequest struct {
	WorkspaceID string `json:"workspaceId,omitempty"`
	MachineID   string `json:"machineId,omitempty"`
}

type remoteMachineSettingsUpdatePayload struct {
	remoteMachineDetailRequest
	Desktop dto.SettingsRequest `json:"desktop"`
}

type remoteMachineRuntimeConfigPayload struct {
	remoteMachineDetailRequest
	Name   string                        `json:"name"`
	Config profile.RuntimeConfigEditable `json:"config"`
}

type remoteMachineRuntimeConfigReadPayload struct {
	remoteMachineDetailRequest
	Name string `json:"name"`
}

type remoteMachineMCPServerUpsertPayload struct {
	remoteMachineDetailRequest
	Server dto.MCPServerRequest `json:"server"`
}

type remoteMachineMCPServerNamePayload struct {
	remoteMachineDetailRequest
	Name string `json:"name"`
}

// reconcileMachineDetailIDs mirrors handleRemoteMachineDashboard's
// workspace/machine ID reconciliation (L846-867): payload fields override
// the connection-bound IDs when present, and mismatched non-empty values
// are rejected. Returns the resolved workspaceID, machineID, and a bool
// indicating whether reconciliation succeeded.
func reconcileMachineDetailIDs(c *Protocol, payloadWorkspaceID, payloadMachineID, endpointID string) (workspaceID, machineID string, ok bool) {
	workspaceID = ""
	machineID = ""
	if c != nil {
		workspaceID = strings.TrimSpace(c.workspaceID)
		machineID = strings.TrimSpace(c.machineID)
	}
	payloadWorkspaceID = strings.TrimSpace(payloadWorkspaceID)
	if payloadWorkspaceID != "" && workspaceID != "" && payloadWorkspaceID != workspaceID {
		return "", "", false
	}
	if payloadWorkspaceID != "" {
		workspaceID = payloadWorkspaceID
	}
	payloadMachineID = strings.TrimSpace(payloadMachineID)
	if payloadMachineID != "" && machineID != "" && payloadMachineID != machineID {
		return "", "", false
	}
	if payloadMachineID != "" {
		machineID = payloadMachineID
	}
	return workspaceID, machineID, true
}

// ---------------------------------------------------------------------------
// machine.settings (read)
// ---------------------------------------------------------------------------

func handleRemoteMachineSettings(ctx context.Context, c *Protocol, assignment remoteRequestAssignmentMsg) (json.RawMessage, string, error) {
	var payload remoteMachineDetailRequest
	if err := json.Unmarshal(assignment.Payload, &payload); err != nil {
		return nil, "protocol_error", err
	}
	select {
	case <-ctx.Done():
		return nil, "", ctx.Err()
	default:
	}
	if _, _, ok := reconcileMachineDetailIDs(c, payload.WorkspaceID, payload.MachineID, remoteEndpointMachineSettings); !ok {
		return nil, machineSettingsInvalidOutcome, errors.New("machine.settings workspaceId/machineId does not match this connection")
	}
	resp, err := dashboard.LoadSettingsBundle()
	if err != nil {
		return nil, "endpoint_error", err
	}
	raw, err := json.Marshal(resp)
	if err != nil {
		return nil, "endpoint_error", err
	}
	return raw, "ok", nil
}

// ---------------------------------------------------------------------------
// machine.settings.update (write)
// ---------------------------------------------------------------------------

func handleRemoteMachineSettingsUpdate(ctx context.Context, c *Protocol, assignment remoteRequestAssignmentMsg) (json.RawMessage, string, error) {
	var payload remoteMachineSettingsUpdatePayload
	if err := json.Unmarshal(assignment.Payload, &payload); err != nil {
		return nil, "protocol_error", err
	}
	select {
	case <-ctx.Done():
		return nil, "", ctx.Err()
	default:
	}
	if _, _, ok := reconcileMachineDetailIDs(c, payload.WorkspaceID, payload.MachineID, remoteEndpointMachineSettingsUpdate); !ok {
		return nil, machineSettingsInvalidOutcome, errors.New("machine.settings.update workspaceId/machineId does not match this connection")
	}
	resp, err := dashboard.UpdateSettingsBundle(payload.Desktop)
	if err != nil && errors.Is(err, dashboard.ErrInvalidSettingsRequest()) {
		return nil, machineSettingsInvalidOutcome, err
	}
	if err != nil {
		return nil, "endpoint_error", err
	}
	raw, err := json.Marshal(resp)
	if err != nil {
		return nil, "endpoint_error", err
	}
	return raw, "ok", nil
}

// ---------------------------------------------------------------------------
// machine.runtimes.bundle (read)
// ---------------------------------------------------------------------------

func handleRemoteMachineRuntimesBundle(ctx context.Context, c *Protocol, assignment remoteRequestAssignmentMsg) (json.RawMessage, string, error) {
	var payload remoteMachineDetailRequest
	if err := json.Unmarshal(assignment.Payload, &payload); err != nil {
		return nil, "protocol_error", err
	}
	select {
	case <-ctx.Done():
		return nil, "", ctx.Err()
	default:
	}
	if _, _, ok := reconcileMachineDetailIDs(c, payload.WorkspaceID, payload.MachineID, remoteEndpointMachineRuntimesBundle); !ok {
		return nil, machineRuntimesInvalidOutcome, errors.New("machine.runtimes.bundle workspaceId/machineId does not match this connection")
	}
	resp, err := dashboard.LoadRuntimesBundle()
	if err != nil {
		return nil, "endpoint_error", err
	}
	raw, err := json.Marshal(resp)
	if err != nil {
		return nil, "endpoint_error", err
	}
	return raw, "ok", nil
}

// ---------------------------------------------------------------------------
// machine.runtimes.config.read
// ---------------------------------------------------------------------------

func handleRemoteMachineRuntimeConfigRead(ctx context.Context, c *Protocol, assignment remoteRequestAssignmentMsg) (json.RawMessage, string, error) {
	var payload remoteMachineRuntimeConfigReadPayload
	if err := json.Unmarshal(assignment.Payload, &payload); err != nil {
		return nil, "protocol_error", err
	}
	select {
	case <-ctx.Done():
		return nil, "", ctx.Err()
	default:
	}
	if _, _, ok := reconcileMachineDetailIDs(c, payload.WorkspaceID, payload.MachineID, remoteEndpointMachineRuntimeConfigRead); !ok {
		return nil, machineRuntimesInvalidOutcome, errors.New("machine.runtimes.config.read workspaceId/machineId does not match this connection")
	}
	if strings.TrimSpace(payload.Name) == "" {
		return nil, machineRuntimesInvalidOutcome, errors.New("machine.runtimes.config.read requires name")
	}
	resp, err := dashboard.ReadRuntimeConfig(payload.Name)
	if err != nil && errors.Is(err, dashboard.ErrInvalidRuntimeRequest()) {
		return nil, machineRuntimesInvalidOutcome, err
	}
	if err != nil {
		return nil, "endpoint_error", err
	}
	raw, err := json.Marshal(resp)
	if err != nil {
		return nil, "endpoint_error", err
	}
	return raw, "ok", nil
}

// ---------------------------------------------------------------------------
// machine.runtimes.config.update
// ---------------------------------------------------------------------------

func handleRemoteMachineRuntimeConfigUpdate(ctx context.Context, c *Protocol, assignment remoteRequestAssignmentMsg) (json.RawMessage, string, error) {
	var payload remoteMachineRuntimeConfigPayload
	if err := json.Unmarshal(assignment.Payload, &payload); err != nil {
		return nil, "protocol_error", err
	}
	select {
	case <-ctx.Done():
		return nil, "", ctx.Err()
	default:
	}
	if _, _, ok := reconcileMachineDetailIDs(c, payload.WorkspaceID, payload.MachineID, remoteEndpointMachineRuntimeConfigUpdate); !ok {
		return nil, machineRuntimesInvalidOutcome, errors.New("machine.runtimes.config.update workspaceId/machineId does not match this connection")
	}
	if strings.TrimSpace(payload.Name) == "" {
		return nil, machineRuntimesInvalidOutcome, errors.New("machine.runtimes.config.update requires name")
	}
	resp, err := dashboard.UpdateRuntimeConfig(payload.Name, payload.Config)
	if err != nil && errors.Is(err, dashboard.ErrInvalidRuntimeRequest()) {
		return nil, machineRuntimesInvalidOutcome, err
	}
	if err != nil {
		return nil, "endpoint_error", err
	}
	raw, err := json.Marshal(resp)
	if err != nil {
		return nil, "endpoint_error", err
	}
	return raw, "ok", nil
}

// ---------------------------------------------------------------------------
// machine.runtimes.config.validate
// ---------------------------------------------------------------------------

func handleRemoteMachineRuntimeConfigValidate(ctx context.Context, c *Protocol, assignment remoteRequestAssignmentMsg) (json.RawMessage, string, error) {
	var payload remoteMachineRuntimeConfigPayload
	if err := json.Unmarshal(assignment.Payload, &payload); err != nil {
		return nil, "protocol_error", err
	}
	select {
	case <-ctx.Done():
		return nil, "", ctx.Err()
	default:
	}
	if _, _, ok := reconcileMachineDetailIDs(c, payload.WorkspaceID, payload.MachineID, remoteEndpointMachineRuntimeConfigValidate); !ok {
		return nil, machineRuntimesInvalidOutcome, errors.New("machine.runtimes.config.validate workspaceId/machineId does not match this connection")
	}
	if strings.TrimSpace(payload.Name) == "" {
		return nil, machineRuntimesInvalidOutcome, errors.New("machine.runtimes.config.validate requires name")
	}
	resp, err := dashboard.ValidateRuntimeConfig(payload.Name, payload.Config)
	if err != nil && errors.Is(err, dashboard.ErrInvalidRuntimeRequest()) {
		return nil, machineRuntimesInvalidOutcome, err
	}
	if err != nil {
		return nil, "endpoint_error", err
	}
	raw, err := json.Marshal(resp)
	if err != nil {
		return nil, "endpoint_error", err
	}
	return raw, "ok", nil
}

// ---------------------------------------------------------------------------
// machine.runtimes.detect
// ---------------------------------------------------------------------------

func handleRemoteMachineRuntimesDetect(ctx context.Context, c *Protocol, assignment remoteRequestAssignmentMsg) (json.RawMessage, string, error) {
	var payload remoteMachineDetailRequest
	if err := json.Unmarshal(assignment.Payload, &payload); err != nil {
		return nil, "protocol_error", err
	}
	select {
	case <-ctx.Done():
		return nil, "", ctx.Err()
	default:
	}
	if _, _, ok := reconcileMachineDetailIDs(c, payload.WorkspaceID, payload.MachineID, remoteEndpointMachineRuntimesDetect); !ok {
		return nil, machineRuntimesInvalidOutcome, errors.New("machine.runtimes.detect workspaceId/machineId does not match this connection")
	}
	resp, err := dashboard.LoadRuntimesBundle()
	if err != nil {
		return nil, "endpoint_error", err
	}
	raw, err := json.Marshal(resp)
	if err != nil {
		return nil, "endpoint_error", err
	}
	return raw, "ok", nil
}

// ---------------------------------------------------------------------------
// machine.mcp_servers.list
// ---------------------------------------------------------------------------

func handleRemoteMachineMCPServerList(ctx context.Context, c *Protocol, assignment remoteRequestAssignmentMsg) (json.RawMessage, string, error) {
	var payload remoteMachineDetailRequest
	if err := json.Unmarshal(assignment.Payload, &payload); err != nil {
		return nil, "protocol_error", err
	}
	select {
	case <-ctx.Done():
		return nil, "", ctx.Err()
	default:
	}
	if _, _, ok := reconcileMachineDetailIDs(c, payload.WorkspaceID, payload.MachineID, remoteEndpointMachineMCPServerList); !ok {
		return nil, machineMCPServersInvalidOutcome, errors.New("machine.mcp_servers.list workspaceId/machineId does not match this connection")
	}
	resp := dashboard.ListMCPServers()
	raw, err := json.Marshal(resp)
	if err != nil {
		return nil, "endpoint_error", err
	}
	return raw, "ok", nil
}

// ---------------------------------------------------------------------------
// machine.mcp_servers.upsert
// ---------------------------------------------------------------------------

func handleRemoteMachineMCPServerUpsert(ctx context.Context, c *Protocol, assignment remoteRequestAssignmentMsg) (json.RawMessage, string, error) {
	var payload remoteMachineMCPServerUpsertPayload
	if err := json.Unmarshal(assignment.Payload, &payload); err != nil {
		return nil, "protocol_error", err
	}
	select {
	case <-ctx.Done():
		return nil, "", ctx.Err()
	default:
	}
	if _, _, ok := reconcileMachineDetailIDs(c, payload.WorkspaceID, payload.MachineID, remoteEndpointMachineMCPServerUpsert); !ok {
		return nil, machineMCPServersInvalidOutcome, errors.New("machine.mcp_servers.upsert workspaceId/machineId does not match this connection")
	}
	if strings.TrimSpace(payload.Server.ID) == "" {
		return nil, machineMCPServersInvalidOutcome, errors.New("machine.mcp_servers.upsert requires server.id")
	}
	resp, err := dashboard.UpsertMCPServer(payload.Server)
	if err != nil {
		return nil, machineMCPServersInvalidOutcome, err
	}
	raw, err := json.Marshal(resp)
	if err != nil {
		return nil, "endpoint_error", err
	}
	return raw, "ok", nil
}

// ---------------------------------------------------------------------------
// machine.mcp_servers.delete
// ---------------------------------------------------------------------------

func handleRemoteMachineMCPServerDelete(ctx context.Context, c *Protocol, assignment remoteRequestAssignmentMsg) (json.RawMessage, string, error) {
	var payload remoteMachineMCPServerNamePayload
	if err := json.Unmarshal(assignment.Payload, &payload); err != nil {
		return nil, "protocol_error", err
	}
	select {
	case <-ctx.Done():
		return nil, "", ctx.Err()
	default:
	}
	if _, _, ok := reconcileMachineDetailIDs(c, payload.WorkspaceID, payload.MachineID, remoteEndpointMachineMCPServerDelete); !ok {
		return nil, machineMCPServersInvalidOutcome, errors.New("machine.mcp_servers.delete workspaceId/machineId does not match this connection")
	}
	if strings.TrimSpace(payload.Name) == "" {
		return nil, machineMCPServersInvalidOutcome, errors.New("machine.mcp_servers.delete requires name")
	}
	if err := dashboard.DeleteMCPServer(payload.Name); err != nil {
		if errors.Is(err, dashboard.ErrMCPServerNotFound) {
			return nil, machineMCPServersNotFoundOutcome, err
		}
		return nil, "endpoint_error", err
	}
	raw, err := json.Marshal(struct {
		Name    string `json:"name"`
		Deleted bool   `json:"deleted"`
	}{Name: payload.Name, Deleted: true})
	if err != nil {
		return nil, "endpoint_error", err
	}
	return raw, "ok", nil
}

// ---------------------------------------------------------------------------
// machine.mcp_servers.test
// ---------------------------------------------------------------------------

func handleRemoteMachineMCPServerTest(ctx context.Context, c *Protocol, assignment remoteRequestAssignmentMsg) (json.RawMessage, string, error) {
	var payload remoteMachineMCPServerNamePayload
	if err := json.Unmarshal(assignment.Payload, &payload); err != nil {
		return nil, "protocol_error", err
	}
	select {
	case <-ctx.Done():
		return nil, "", ctx.Err()
	default:
	}
	if _, _, ok := reconcileMachineDetailIDs(c, payload.WorkspaceID, payload.MachineID, remoteEndpointMachineMCPServerTest); !ok {
		return nil, machineMCPServersInvalidOutcome, errors.New("machine.mcp_servers.test workspaceId/machineId does not match this connection")
	}
	if strings.TrimSpace(payload.Name) == "" {
		return nil, machineMCPServersInvalidOutcome, errors.New("machine.mcp_servers.test requires name")
	}
	resp, err := dashboard.TestMCPServer(ctx, payload.Name)
	if err != nil {
		if errors.Is(err, dashboard.ErrMCPServerNotFound) {
			return nil, machineMCPServersNotFoundOutcome, err
		}
		return nil, "endpoint_error", err
	}
	raw, err := json.Marshal(resp)
	if err != nil {
		return nil, "endpoint_error", err
	}
	return raw, "ok", nil
}
