package webagent

import (
	"context"
	"errors"
	"fmt"
	"log/slog"
	"net/http"
	"strings"
	"time"

	"github.com/tidbcloud/AgentTeam/internal/api"
	"github.com/tidbcloud/AgentTeam/internal/api/dto"
	"github.com/tidbcloud/AgentTeam/internal/runtimecontrol"
	"github.com/tidbcloud/AgentTeam/internal/service/maas"
	"github.com/tidbcloud/AgentTeam/internal/store/agent"
	"github.com/tidbcloud/AgentTeam/internal/store/storeerr"
)

// AgentService is the shared, transport-free agent management domain service.
// Both Web handlers and the future approval worker executor call these methods.
type AgentService struct {
	agentStore            AgentStore
	rewindStore           RewindStore
	runtime               Runtime
	realtime              Realtime
	logger                *slog.Logger
	machineAccess         MachineAccessChecker
	orgs                  OrganizationProvider
	hostedAgents          HostedAgentValidator
	resourceCopy          AgentResourceCopier
	knowledgeBasesEnabled bool
	modelProfiles         ModelProfileStore
	memory                CloudMemoryProvisioner
}

func NewAgentService(agentStore AgentStore, rewindStore RewindStore, runtime Runtime, realtime Realtime, logger *slog.Logger, machineAccess MachineAccessChecker, orgs OrganizationProvider, hostedAgents HostedAgentValidator, resourceCopy AgentResourceCopier, knowledgeBasesEnabled bool, modelProfiles ModelProfileStore, memory CloudMemoryProvisioner) *AgentService {
	return &AgentService{
		agentStore: agentStore, rewindStore: rewindStore, runtime: runtime, realtime: realtime, logger: logger,
		machineAccess: machineAccess, orgs: orgs, hostedAgents: hostedAgents, resourceCopy: resourceCopy, knowledgeBasesEnabled: knowledgeBasesEnabled,
		modelProfiles: modelProfiles, memory: memory,
	}
}

// DeleteAgent deletes an agent: lists recovery points, deletes the agent row,
// stops its runtime, GCs recovery points, syncs the machine catalog, and emits
// a realtime event.
func (s *AgentService) DeleteAgent(ctx context.Context, userID, workspaceID, agentID string) error {
	if s.rewindStore == nil {
		return api.ErrNotFound
	}
	recoveryPoints, err := s.rewindStore.ListAllAgentRecoveryPointsForWorkspace(ctx, userID, workspaceID, agentID)
	if err != nil {
		return err
	}
	machineID, err := s.agentStore.DeleteAgentForWorkspace(ctx, userID, workspaceID, agentID)
	if err != nil {
		return err
	}
	if s.runtime != nil && machineID != "" {
		_ = s.runtime.StopAgent(machineID, agentID)
		if len(recoveryPoints) > 0 {
			if err := s.runtime.GCRecoveryPoints(machineID, agentID, recoveryPoints); err != nil && !errors.Is(err, runtimecontrol.ErrMachineOffline) {
				warn(s.logger, "gc deleted agent recovery points", slog.String("agent_id", agentID), slog.Any("error", err))
			}
		}
		if err := s.runtime.SyncMachineCatalog(ctx, machineID); err != nil && !errors.Is(err, runtimecontrol.ErrMachineOffline) {
			warn(s.logger, "sync machine catalog after agent delete", slog.String("agent_id", agentID), slog.Any("error", err))
		}
	}
	if s.realtime != nil {
		s.realtime.EmitAgentDeleted(workspaceID)
	}
	return nil
}

// DuplicateAgentInput is the transport-free input for single-copy duplication.
type DuplicateAgentInput struct {
	Mode        string
	Name        string
	DisplayName string
	MachineID   string
	Runtime     string
	ChannelIDs  []string
	Identity    AgentResourceCopyIdentity
}

type duplicateTarget struct {
	MachineID string
	Runtime   string
}

// DuplicateAgent copies one agent's configuration, role binding, and optionally
// its resources into a newly created agent. Returns the created agent + optional
// resource-copy summary.
func (s *AgentService) DuplicateAgent(ctx context.Context, userID, workspaceID, sourceAgentID string, input DuplicateAgentInput) (agent.AppAgent, *dto.WebAgentResourceCopyResponse, error) {
	copyResources, err := duplicateModeCopiesResources(input.Mode)
	if err != nil {
		return agent.AppAgent{}, nil, err
	}
	if copyResources {
		identityUserID := strings.TrimSpace(input.Identity.User.ID)
		ownerUserID := strings.TrimSpace(userID)
		if identityUserID == "" || ownerUserID == "" || identityUserID != ownerUserID {
			return agent.AppAgent{}, nil, storeerr.Forbidden("agent management resource copy identity does not match the authorized owner")
		}
	}

	sourceView, err := s.agentStore.GetAgentForUser(ctx, userID, workspaceID, sourceAgentID)
	if err != nil {
		return agent.AppAgent{}, nil, err
	}
	sourceAgent, err := s.agentStore.AgentForWorkspace(ctx, userID, workspaceID, sourceAgentID)
	if err != nil {
		return agent.AppAgent{}, nil, err
	}

	target, err := s.resolveAndValidateTarget(ctx, userID, workspaceID, sourceView, input)
	if err != nil {
		return agent.AppAgent{}, nil, err
	}

	displayName := strings.TrimSpace(input.DisplayName)
	if displayName == "" {
		displayName = strings.TrimSpace(input.Name)
	}
	if displayName == "" {
		displayName = duplicateFallbackDisplayName(sourceAgent)
	}
	if displayName == "" {
		return agent.AppAgent{}, nil, api.NewHTTPError(http.StatusBadRequest, "displayName is required")
	}

	requestedLLM := agentLLMFromFields(sourceView.LLMMode, sourceView.BaseURL, sourceView.APIKey, sourceView.Headers, sourceView.WireAPI)
	if err := s.validateHostedAgentBindingForUser(ctx, userID, workspaceID, target.MachineID, sourceView.Model, target.Runtime, requestedLLM); err != nil {
		return agent.AppAgent{}, nil, err
	}
	if err := validateWorkspaceAgentLLM(ctx, s.orgs, workspaceID, requestedLLM); err != nil {
		return agent.AppAgent{}, nil, err
	}
	storedLLM, err := buildStoredAgentLLM(ctx, s.orgs, workspaceID, requestedLLM)
	if err != nil {
		return agent.AppAgent{}, nil, err
	}
	llmMode, baseURL, apiKey, headers, wireAPI := agent.AgentLLMFieldsFromStored(storedLLM)

	created, err := s.agentStore.CreateAgentForUser(ctx, userID, workspaceID, target.MachineID, agent.CreateAgentParams{
		Name:            strings.TrimSpace(input.Name),
		DisplayName:     displayName,
		AvatarURL:       sourceAgent.AvatarURL,
		Description:     sourceView.Description,
		Runtime:         target.Runtime,
		Model:           sourceView.Model,
		ReasoningEffort: agent.NormalizeReasoningEffortForRuntime(target.Runtime, sourceView.ReasoningEffort),
		LLMMode:         llmMode,
		BaseURL:         baseURL,
		APIKey:          apiKey,
		Headers:         headers,
		WireAPI:         wireAPI,
		ModelProfileID:  sourceView.ModelProfileID,
		SourceKind:      sourceView.SourceKind,
		SourceRef:       sourceView.SourceRef,
		AutoWakeEnabled: &sourceView.AutoWakeEnabled,
		ChannelIDs:      input.ChannelIDs,
	})
	if err != nil {
		return agent.AppAgent{}, nil, err
	}

	if err := s.agentStore.CopyAgentRoleBindingForUser(ctx, userID, workspaceID, sourceAgentID, created.AgentID); err != nil {
		return agent.AppAgent{}, nil, s.cleanupCreatedAgent(ctx, userID, workspaceID, created.AgentID, err)
	}

	var resourceCopy *dto.WebAgentResourceCopyResponse
	if copyResources {
		if s.resourceCopy == nil {
			err := api.NewHTTPError(http.StatusServiceUnavailable, "agent resource copying is not configured")
			return agent.AppAgent{}, nil, s.cleanupCreatedAgent(ctx, userID, workspaceID, created.AgentID, err)
		}
		result, err := s.resourceCopy.CopyAgentResources(ctx, AgentResourceCopyRequest{
			WorkspaceID:        workspaceID,
			Identity:           input.Identity,
			Source:             agentResourceCopyAgentFromApp(sourceAgent),
			CopyKnowledgeBases: s.knowledgeBasesEnabled,
			Target: AgentResourceCopyAgent{
				ID:          created.AgentID,
				Name:        strings.TrimSpace(input.Name),
				DisplayName: displayName,
			},
		})
		if err != nil {
			return agent.AppAgent{}, nil, s.cleanupCreatedAgent(ctx, userID, workspaceID, created.AgentID, err)
		}
		resourceCopy = &dto.WebAgentResourceCopyResponse{
			Skills:         result.Skills,
			KnowledgeBases: result.KnowledgeBases,
			Memory:         result.Memory,
		}
	}

	createdAgent, err := s.agentStore.AgentForWorkspace(ctx, userID, workspaceID, created.AgentID)
	if err != nil {
		return agent.AppAgent{}, nil, s.cleanupCreatedAgent(ctx, userID, workspaceID, created.AgentID, err)
	}
	api.OverlayAppAgentRuntime(&createdAgent, s.runtime, time.Now().UTC())
	if s.runtime != nil {
		if err := s.runtime.UpdateAgentIdentity(createdAgent.MachineID, createdAgent.ID, createdAgent.Name, createdAgent.DisplayName); err != nil && !errors.Is(err, runtimecontrol.ErrMachineOffline) && s.logger != nil {
			s.logger.Warn("update duplicated agent runtime identity", slog.String("machine_id", createdAgent.MachineID), slog.String("agent_id", createdAgent.ID), slog.Any("error", err))
		}
	}
	s.syncCatalogAfterDuplicate(ctx, createdAgent.MachineID, createdAgent.ID)
	if s.realtime != nil {
		s.realtime.EmitAgentCreated(workspaceID)
	}
	return createdAgent, resourceCopy, nil
}

func (s *AgentService) resolveAndValidateTarget(ctx context.Context, userID, workspaceID string, source agent.AgentView, input DuplicateAgentInput) (duplicateTarget, error) {
	target := duplicateTarget{MachineID: strings.TrimSpace(input.MachineID), Runtime: strings.TrimSpace(input.Runtime)}
	if target.MachineID == "" {
		target.MachineID = strings.TrimSpace(source.MachineID)
	}
	if target.Runtime == "" {
		target.Runtime = strings.TrimSpace(source.Runtime)
	}
	if s.machineAccess == nil {
		return duplicateTarget{}, api.NewHTTPError(http.StatusServiceUnavailable, "machine access validation is not configured")
	}
	if err := s.machineAccess.RequireMachineManagementForUser(ctx, userID, workspaceID, target.MachineID); err != nil {
		return duplicateTarget{}, err
	}
	if s.runtime == nil {
		return duplicateTarget{}, api.NewHTTPError(http.StatusServiceUnavailable, "runtime catalog validation is not configured")
	}
	if !s.runtime.MachineConnected(target.MachineID) {
		return duplicateTarget{}, runtimecontrol.ErrMachineOffline
	}
	runtimes, _, known := s.runtime.MachineRuntimeCatalog(target.MachineID)
	if !known {
		return duplicateTarget{}, api.NewHTTPError(http.StatusServiceUnavailable, fmt.Sprintf("runtime catalog for machine %q is unavailable", target.MachineID))
	}
	for _, candidate := range runtimes {
		if strings.TrimSpace(candidate) == target.Runtime {
			return target, nil
		}
	}
	return duplicateTarget{}, storeerr.Conflict(fmt.Sprintf("runtime %q is not available on machine %q", target.Runtime, target.MachineID))
}

func (s *AgentService) cleanupCreatedAgent(ctx context.Context, userID, workspaceID, agentID string, cause error) error {
	if s.agentStore == nil {
		return api.NewHTTPErrorWithCause(http.StatusInternalServerError, "agent duplicate failed and rollback is not configured", cause)
	}
	cleanupCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), agentDuplicateCleanupTimeout)
	defer cancel()
	machineID, cleanupErr := s.agentStore.DeleteAgentForWorkspace(cleanupCtx, userID, workspaceID, agentID)
	if cleanupErr != nil {
		if s.logger != nil {
			s.logger.Error("cleanup duplicated agent after failure", slog.String("agent_id", agentID), slog.Any("cause", cause), slog.Any("error", cleanupErr))
		}
		return api.NewHTTPErrorWithCause(http.StatusInternalServerError, "agent duplicate failed and rollback failed", errors.Join(cause, cleanupErr))
	}
	if s.runtime != nil && strings.TrimSpace(machineID) != "" {
		var reconciliationErr error
		if stopErr := s.runtime.StopAgent(machineID, agentID); stopErr != nil && !errors.Is(stopErr, runtimecontrol.ErrMachineOffline) {
			reconciliationErr = errors.Join(reconciliationErr, fmt.Errorf("stop rolled back agent: %w", stopErr))
			if s.logger != nil {
				s.logger.Warn("stop rolled back duplicated agent", slog.String("machine_id", machineID), slog.String("agent_id", agentID), slog.Any("error", stopErr))
			}
		}
		if syncErr := s.runtime.SyncMachineCatalog(cleanupCtx, machineID); syncErr != nil && !errors.Is(syncErr, runtimecontrol.ErrMachineOffline) {
			reconciliationErr = errors.Join(reconciliationErr, fmt.Errorf("sync target catalog after rollback: %w", syncErr))
			if s.logger != nil {
				s.logger.Warn("sync target catalog after duplicate rollback", slog.String("machine_id", machineID), slog.String("agent_id", agentID), slog.Any("error", syncErr))
			}
		}
		if reconciliationErr != nil {
			return api.NewHTTPErrorWithCause(http.StatusInternalServerError, "agent duplicate failed and rollback reconciliation failed", errors.Join(cause, reconciliationErr))
		}
	}
	return cause
}

func (s *AgentService) syncCatalogAfterDuplicate(ctx context.Context, machineID, agentID string) {
	if s.runtime == nil {
		return
	}
	if err := s.runtime.SyncMachineCatalog(ctx, machineID); err != nil && !errors.Is(err, runtimecontrol.ErrMachineOffline) && s.logger != nil {
		s.logger.Warn("sync machine catalog after agent duplicate", slog.String("machine_id", machineID), slog.String("agent_id", agentID), slog.Any("error", err))
	}
}

func (s *AgentService) validateHostedAgentBindingForUser(ctx context.Context, userID, workspaceID, machineID, model, runtimeName string, llm *agent.AgentLLM) error {
	if !agentLLMUsesAgentTeamHosted(llm) {
		return nil
	}
	if s.hostedAgents == nil {
		return storeerr.Conflict(maas.ReasonRuntimeUnavailable)
	}
	var machineSupportsDirectMaaS *bool
	if s.runtime != nil {
		supported, known := s.runtime.AgentMaaSDirectSupport(machineID)
		if known {
			machineSupportsDirectMaaS = &supported
		}
	}
	return s.hostedAgents.ValidateAgentBindingForUser(ctx, maas.AgentBindingValidation{
		UserID: userID, WorkspaceID: workspaceID, ModelReference: model,
		LLMMode: agent.AgentLLMModeAgentTeamHosted, Runtime: runtimeName,
		MachineSupportsDirectMaaS: machineSupportsDirectMaaS,
	})
}

// CreateAgentInput is the transport-free input for creating a new agent.
type CreateAgentInput struct {
	Name            string
	MachineID       string
	Runtime         string
	Model           string
	ReasoningEffort string
	Description     string
	LLMMode         string
	BaseURL         string
	APIKey          string
	Headers         map[string]string
	WireAPI         string
	ModelProfileID  string
	ChannelIDs      []string
	TemplateLocale  string
}

// CreateAgent creates a new agent: validates name/machine, resolves the optional
// model profile, validates and stores the LLM binding, creates the agent row,
// provisions Cloud Memory (rolling back the create on provision failure),
// syncs the runtime catalog, and emits a realtime event.
func (s *AgentService) CreateAgent(ctx context.Context, userID, workspaceID string, input CreateAgentInput) (agent.AppAgent, error) {
	trimmedName := strings.TrimSpace(input.Name)
	if trimmedName == "" {
		return agent.AppAgent{}, api.NewHTTPError(http.StatusBadRequest, "name is required")
	}
	machineID := strings.TrimSpace(input.MachineID)
	if machineID == "" {
		return agent.AppAgent{}, api.NewHTTPError(http.StatusBadRequest, "machineId is required")
	}

	model := input.Model
	requestedLLM := agentLLMFromFields(input.LLMMode, input.BaseURL, input.APIKey, input.Headers, input.WireAPI)
	if strings.TrimSpace(input.ModelProfileID) != "" {
		profile, err := s.modelProfileLLM(ctx, userID, strings.TrimSpace(input.ModelProfileID))
		if err != nil {
			return agent.AppAgent{}, err
		}
		model = profile.Model
		requestedLLM = agentLLMFromFields(agent.AgentLLMModeLocal, profile.BaseURL, profile.APIKey, map[string]string{}, profile.WireAPI)
	}
	if err := s.validateHostedAgentBindingForUser(ctx, userID, workspaceID, machineID, model, input.Runtime, requestedLLM); err != nil {
		return agent.AppAgent{}, err
	}
	if err := validateWorkspaceAgentLLM(ctx, s.orgs, workspaceID, requestedLLM); err != nil {
		return agent.AppAgent{}, err
	}
	storedLLM, err := buildStoredAgentLLM(ctx, s.orgs, workspaceID, requestedLLM)
	if err != nil {
		return agent.AppAgent{}, err
	}
	llmMode, baseURL, apiKey, headers, wireAPI := agent.AgentLLMFieldsFromStored(storedLLM)

	created, err := s.agentStore.CreateAgentForUser(ctx, userID, workspaceID, machineID, agent.CreateAgentParams{
		DisplayName:     trimmedName,
		Description:     input.Description,
		Runtime:         input.Runtime,
		Model:           model,
		ReasoningEffort: input.ReasoningEffort,
		LLMMode:         llmMode,
		BaseURL:         baseURL,
		APIKey:          apiKey,
		Headers:         headers,
		WireAPI:         wireAPI,
		ModelProfileID:  input.ModelProfileID,
		ChannelIDs:      input.ChannelIDs,
	})
	if err != nil {
		return agent.AppAgent{}, err
	}

	if err := provisionCloudMemoryForCreatedAgent(ctx, createdAgentMemoryProvisionInput{
		store:          s.agentStore,
		memory:         s.memory,
		runtime:        s.runtime,
		logger:         s.logger,
		userID:         userID,
		workspaceID:    workspaceID,
		agentID:        created.AgentID,
		machineID:      machineID,
		templateLocale: input.TemplateLocale,
	}); err != nil {
		return agent.AppAgent{}, err
	}

	createdAgent, err := s.agentStore.AgentForWorkspace(ctx, userID, workspaceID, created.AgentID)
	if err != nil {
		return agent.AppAgent{}, err
	}
	api.OverlayAppAgentRuntime(&createdAgent, s.runtime, time.Now().UTC())
	if s.runtime != nil {
		if err := s.runtime.SyncMachineCatalog(ctx, machineID); err != nil && !errors.Is(err, runtimecontrol.ErrMachineOffline) {
			warn(s.logger, "sync machine catalog after agent create", slog.String("machine_id", machineID), slog.String("agent_id", created.AgentID), slog.Any("error", err))
		}
	}
	if s.realtime != nil {
		s.realtime.EmitAgentCreated(workspaceID)
	}
	return createdAgent, nil
}

func (s *AgentService) modelProfileLLM(ctx context.Context, userID, profileID string) (agent.ModelProfile, error) {
	if s.modelProfiles == nil {
		return agent.ModelProfile{}, api.ErrNotFound
	}
	return s.modelProfiles.GetModelProfileForUser(ctx, userID, profileID)
}

// UpdateAgentInput is the transport-free input for updating an existing agent.
// Field types mirror the Web update request so the existing optional/null PATCH
// semantics are preserved without new behavior.
type UpdateAgentInput struct {
	AgentID         string
	Name            *string
	DisplayName     *string
	DeriveName      bool
	Description     dto.OptionalString
	AvatarURL       dto.OptionalString
	Runtime         *string
	PermissionMode  dto.OptionalString
	Model           dto.OptionalString
	ReasoningEffort dto.OptionalString
	LLMMode         dto.OptionalString
	BaseURL         dto.OptionalString
	APIKey          dto.OptionalString
	Headers         dto.OptionalStringMap
	WireAPI         dto.OptionalString
	ModelProfileID  dto.OptionalString
	// ResolveTrustedLLM asks the approval executor path to derive the binding
	// from server-trusted profile/model state. Raw LLM fields remain unset.
	ResolveTrustedLLM bool
}

// UpdateAgent updates an existing agent's configuration and restarts its runtime
// when runtime-critical fields change. It preserves the existing Web PATCH
// semantics verbatim — current-agent load, model-profile + LLM merge/storage,
// permission-mode tightening gate with rollback, running-state detection +
// runtime restart, identity update, and catalog sync. It intentionally does
// not reject empty patches or emit realtime events (matching current Web
// behavior).
type agentUpdatePlan struct {
	input              UpdateAgentInput
	current            agent.AgentView
	nextLLM            *agent.AgentLLM
	nextLLMSet         bool
	nextPermissionMode string
	restartRequired    bool
	tightensPermission bool
}

func (s *AgentService) UpdateAgent(ctx context.Context, userID, workspaceID string, input UpdateAgentInput) (agent.AppAgent, error) {
	current, err := s.agentStore.GetAgentForUser(ctx, userID, workspaceID, input.AgentID)
	if err != nil {
		return agent.AppAgent{}, err
	}
	plan, err := s.prepareAgentUpdate(ctx, userID, workspaceID, current, input)
	if err != nil {
		return agent.AppAgent{}, err
	}
	if _, err := s.agentStore.UpdateAgentForUser(
		ctx, userID, workspaceID, input.AgentID,
		updateAgentParamsFromRequest(plan.input, plan.nextLLMSet),
	); err != nil {
		return agent.AppAgent{}, err
	}
	if err := s.restartUpdatedAgent(ctx, userID, workspaceID, plan); err != nil {
		return agent.AppAgent{}, err
	}
	updated, err := s.agentStore.AgentForWorkspace(ctx, userID, workspaceID, input.AgentID)
	if err != nil {
		return agent.AppAgent{}, err
	}
	api.OverlayAppAgentRuntime(&updated, s.runtime, time.Now().UTC())
	if s.runtime != nil {
		if !plan.restartRequired && (input.Name != nil || input.DisplayName != nil) {
			if err := s.runtime.UpdateAgentIdentity(updated.MachineID, updated.ID, updated.Name, updated.DisplayName); err != nil && !errors.Is(err, runtimecontrol.ErrMachineOffline) {
				warn(s.logger, "update running agent identity", slog.String("agent_id", updated.ID), slog.Any("error", err))
			}
		}
		if err := s.runtime.SyncMachineCatalog(ctx, updated.MachineID); err != nil && !errors.Is(err, runtimecontrol.ErrMachineOffline) {
			warn(s.logger, "sync machine catalog after agent update", slog.String("agent_id", updated.ID), slog.Any("error", err))
		}
	}
	return updated, nil
}

func (s *AgentService) prepareAgentUpdate(
	ctx context.Context, userID, workspaceID string, current agent.AgentView, input UpdateAgentInput,
) (agentUpdatePlan, error) {
	plan := agentUpdatePlan{
		input: input, current: current,
		nextPermissionMode: agent.DefaultAgentPermissionMode(current.PermissionMode),
	}
	nextRuntime := current.Runtime
	runtimeChanged := false
	if input.Runtime != nil && strings.TrimSpace(*input.Runtime) != current.Runtime {
		nextRuntime, runtimeChanged, plan.restartRequired = strings.TrimSpace(*input.Runtime), true, true
	}
	nextModel := current.Model
	modelChanged := false
	if input.Model.Set {
		nextModel = ""
		if input.Model.Value != nil {
			nextModel = strings.TrimSpace(*input.Model.Value)
		}
		modelChanged = nextModel != current.Model
		plan.restartRequired = plan.restartRequired || modelChanged
	}
	if input.PermissionMode.Set {
		plan.nextPermissionMode = ""
		if input.PermissionMode.Value != nil {
			plan.nextPermissionMode = agent.DefaultAgentPermissionMode(*input.PermissionMode.Value)
		}
		plan.restartRequired = plan.restartRequired || plan.nextPermissionMode != agent.DefaultAgentPermissionMode(current.PermissionMode)
	}
	if input.ReasoningEffort.Set {
		nextReasoningEffort := ""
		if input.ReasoningEffort.Value != nil {
			nextReasoningEffort = strings.TrimSpace(*input.ReasoningEffort.Value)
		}
		plan.restartRequired = plan.restartRequired || nextReasoningEffort != current.ReasoningEffort
	}
	currentLLM := agent.NewAgentLLM(current.LLMMode, current.BaseURL, current.APIKey, current.Headers, current.WireAPI)
	plan.nextLLM = currentLLM
	if input.ModelProfileID.Set && input.ModelProfileID.Value != nil && strings.TrimSpace(*input.ModelProfileID.Value) != "" {
		profile, err := s.modelProfileLLM(ctx, userID, strings.TrimSpace(*input.ModelProfileID.Value))
		if err != nil {
			return agentUpdatePlan{}, err
		}
		if profile.Model != current.Model {
			nextModel, modelChanged, plan.restartRequired = profile.Model, true, true
		}
		plan.input.Model = dto.OptionalString{Set: true, Value: stringPointer(profile.Model)}
		plan.nextLLM = agentLLMFromFields(agent.AgentLLMModeLocal, profile.BaseURL, profile.APIKey, map[string]string{}, profile.WireAPI)
		plan.nextLLMSet = true
	} else if input.ResolveTrustedLLM {
		requested := agentLLMFromFields(trustedLLMMode(nextModel, ""), "", "", nil, "")
		if err := validateWorkspaceAgentLLM(ctx, s.orgs, workspaceID, requested); err != nil {
			return agentUpdatePlan{}, err
		}
		resolved, err := buildStoredAgentLLM(ctx, s.orgs, workspaceID, requested)
		if err != nil {
			return agentUpdatePlan{}, err
		}
		plan.nextLLM, plan.nextLLMSet = resolved, true
	} else if input.LLMMode.Set || input.BaseURL.Set || input.APIKey.Set || input.Headers.Set || input.WireAPI.Set {
		requested := agentLLMFromFields(
			stringValueFromOptionalString(input.LLMMode, current.LLMMode),
			stringValueFromOptionalString(input.BaseURL, current.BaseURL),
			stringValueFromOptionalString(input.APIKey, current.APIKey),
			mapValueFromOptionalStringMap(input.Headers, current.Headers),
			stringValueFromOptionalString(input.WireAPI, current.WireAPI),
		)
		if err := validateWorkspaceAgentLLM(ctx, s.orgs, workspaceID, requested); err != nil {
			return agentUpdatePlan{}, err
		}
		resolved, err := buildStoredAgentLLM(ctx, s.orgs, workspaceID, requested)
		if err != nil {
			return agentUpdatePlan{}, err
		}
		plan.nextLLM, plan.nextLLMSet = resolved, true
	}
	if agentLLMUsesAgentTeamHosted(plan.nextLLM) && (!agentLLMUsesAgentTeamHosted(currentLLM) || plan.nextLLMSet || modelChanged || runtimeChanged) {
		if err := s.validateHostedAgentBindingForUser(ctx, userID, workspaceID, current.MachineID, nextModel, nextRuntime, plan.nextLLM); err != nil {
			return agentUpdatePlan{}, err
		}
	}
	if !plan.nextLLMSet && agentLLMUsesAgentTeamProxy(plan.nextLLM) && nextRuntime != current.Runtime {
		resolved, err := buildStoredAgentLLM(ctx, s.orgs, workspaceID, plan.nextLLM)
		if err != nil {
			return agentUpdatePlan{}, err
		}
		plan.nextLLM, plan.nextLLMSet = resolved, true
	}
	plan.restartRequired = plan.restartRequired || plan.nextLLMSet && !storedAgentLLMEqual(plan.nextLLM, currentLLM)
	plan.tightensPermission = permissionModeTightensToWorkspaceWrite(current.PermissionMode, plan.nextPermissionMode)
	if plan.restartRequired && plan.nextPermissionMode == agent.AgentPermissionModeWorkspaceWrite && s.runtime != nil &&
		agentRuntimeShouldRestart(s.runtime, current) && !s.runtime.AgentPermissionModeSupported(current.MachineID) {
		return agentUpdatePlan{}, runtimecontrol.ErrDaemonPermissionModeUpgradeRequired
	}
	if plan.nextLLMSet {
		llmMode, baseURL, apiKey, headers, wireAPI := agent.AgentLLMFieldsFromStored(plan.nextLLM)
		plan.input.LLMMode = dto.OptionalString{Set: true, Value: stringPointer(llmMode)}
		plan.input.BaseURL = dto.OptionalString{Set: true, Value: stringPointer(baseURL)}
		plan.input.APIKey = dto.OptionalString{Set: true, Value: stringPointer(apiKey)}
		plan.input.Headers = dto.OptionalStringMap{Set: true, Value: headers}
		plan.input.WireAPI = dto.OptionalString{Set: true, Value: stringPointer(wireAPI)}
	}
	return plan, nil
}

func (s *AgentService) restartUpdatedAgent(
	ctx context.Context, userID, workspaceID string, plan agentUpdatePlan,
) error {
	if s.runtime == nil || !plan.restartRequired || !agentRuntimeShouldRestart(s.runtime, plan.current) {
		return nil
	}
	rollback := func() error {
		return s.agentStore.RestoreAgentPermissionModeForUser(
			ctx, userID, workspaceID, plan.input.AgentID,
			plan.current.PermissionMode, plan.nextPermissionMode,
		)
	}
	if plan.nextPermissionMode == agent.AgentPermissionModeWorkspaceWrite && !s.runtime.AgentPermissionModeSupported(plan.current.MachineID) {
		if plan.tightensPermission {
			if err := rollback(); err != nil {
				return errors.Join(runtimecontrol.ErrDaemonPermissionModeUpgradeRequired, err)
			}
		}
		return runtimecontrol.ErrDaemonPermissionModeUpgradeRequired
	}
	if err := s.runtime.RestartAgent(ctx, plan.current.MachineID, plan.current.AgentID); err != nil {
		if plan.tightensPermission {
			if revertErr := rollback(); revertErr != nil {
				return errors.Join(err, revertErr)
			}
			return err
		}
		if !errors.Is(err, runtimecontrol.ErrMachineOffline) {
			warn(s.logger, "restart running agent after config update", slog.String("agent_id", plan.current.AgentID), slog.Any("error", err))
		}
	}
	return nil
}
