package service

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"os"
	"path"
	"sort"
	"strings"
	"sync"
	"time"

	"github.com/1Panel-dev/1Panel/agent/app/dto"
	"github.com/1Panel-dev/1Panel/agent/app/dto/request"
	"github.com/1Panel-dev/1Panel/agent/app/model"
	providercatalog "github.com/1Panel-dev/1Panel/agent/app/provider"
	"github.com/1Panel-dev/1Panel/agent/app/repo"
	"github.com/1Panel-dev/1Panel/agent/app/task"
	"github.com/1Panel-dev/1Panel/agent/buserr"
	"github.com/1Panel-dev/1Panel/agent/constant"
	"github.com/1Panel-dev/1Panel/agent/global"
	"github.com/1Panel-dev/1Panel/agent/utils/cmd"
	"github.com/1Panel-dev/1Panel/agent/utils/docker"
	terminalai "github.com/1Panel-dev/1Panel/agent/utils/terminal/ai"
	"github.com/1Panel-dev/1Panel/agent/utils/xpack"
	"github.com/docker/docker/api/types/container"
	"gopkg.in/yaml.v3"
	"gorm.io/gorm"
)

var agentAccountMutationMu sync.Mutex

type IAgentService interface {
	Create(req dto.AgentCreateReq) (*dto.AgentItem, error)
	BatchInstall(req dto.AgentBatchInstallReq) (*dto.AgentItem, error)
	BatchUpgrade(req dto.AgentBatchUpgradeReq) ([]dto.AgentBatchUpgradeResult, error)
	BatchInstallSkill(req dto.AgentBatchSkillInstallReq) ([]dto.AgentBatchSkillInstallResult, error)
	BatchOperate(req dto.AgentBatchOperateReq) ([]dto.AgentBatchOperateResult, error)
	Page(req dto.SearchWithPage) (int64, []dto.AgentItem, error)
	DeleteCheck(req dto.AgentIDReq) ([]dto.AppResource, error)
	Delete(req dto.AgentDeleteReq) error
	ResetToken(req dto.AgentTokenResetReq) error
	UpdateRemark(req dto.AgentRemarkUpdateReq) error
	BindWebsite(req dto.AgentWebsiteBindReq) error
	UnbindWebsite(req dto.AgentIDReq) error
	GetModelConfig(req dto.AgentIDReq) (*dto.AgentModelConfig, error)
	UpdateModelConfig(req dto.AgentModelConfigUpdateReq) error
	GetHermesChatSessions(req dto.AgentIDReq) ([]dto.AgentHermesChatSessionItem, error)
	RenameHermesChatSession(req dto.AgentHermesChatSessionRenameReq) error
	DeleteHermesChatSession(req dto.AgentHermesChatSessionDeleteReq) error
	GetOverview(req dto.AgentOverviewReq) (*dto.AgentOverview, error)
	GetProviders() ([]dto.ProviderInfo, error)
	GetSecurityConfig(req dto.AgentIDReq) (*dto.AgentSecurityConfig, error)
	UpdateSecurityConfig(req dto.AgentSecurityConfigUpdateReq) error
	GetOtherConfig(req dto.AgentIDReq) (*dto.AgentOtherConfig, error)
	UpdateOtherConfig(req dto.AgentOtherConfigUpdateReq) error
	GetConfigFile(req dto.AgentConfigFileReq) (*dto.AgentConfigFile, error)
	UpdateConfigFile(req dto.AgentConfigFileUpdateReq) error
	ListSkills(req dto.AgentIDReq) ([]dto.AgentSkillItem, error)
	SearchSkills(req dto.AgentSkillSearchReq) ([]dto.AgentSkillSearchItem, error)
	UpdateSkill(req dto.AgentSkillUpdateReq) error
	InstallSkill(req dto.AgentSkillInstallReq) error
	UninstallSkill(req dto.AgentSkillUninstallReq) error

	CreateRole(req dto.AgentRoleCreateReq) (*dto.AgentRoleCreateResp, error)
	DeleteRole(req dto.AgentRoleDeleteReq) error
	BindRole(req dto.AgentRoleBindReq) error
	UnbindRole(req dto.AgentRoleBindReq) error
	GetConfiguredAgents(req dto.AgentConfiguredAgentsReq) ([]dto.AgentConfiguredAgentItem, error)
	GetRoleChannels(req dto.AgentRoleChannelsReq) ([]dto.AgentRoleChannelItem, error)
	GetRoleMarkdownFiles(req dto.AgentRoleMarkdownFilesReq) ([]dto.AgentRoleMarkdownFileItem, error)
	UpdateRoleMarkdownFiles(req dto.AgentRoleMarkdownFilesUpdateReq) error

	CreateAccount(req dto.AgentAccountCreateReq) error
	UpdateAccount(req dto.AgentAccountUpdateReq) error
	SyncAgentsByAccount(account *model.AgentAccount) error
	PageAccounts(req dto.AgentAccountSearch) (int64, []dto.AgentAccountInfo, error)
	CountAccountsByProviders(req dto.AgentAccountProviderCountReq) (map[string]int64, error)
	GetAccountModels(req dto.AgentAccountModelReq) ([]dto.AgentAccountModel, error)
	DiscoverAccountModels(req dto.AgentAccountModelDiscoverReq) ([]dto.AgentAccountModel, error)
	CreateAccountModel(req dto.AgentAccountModelCreateReq) error
	UpdateAccountModel(req dto.AgentAccountModelUpdateReq) error
	DeleteAccountModel(req dto.AgentAccountModelDeleteReq) error
	VerifyAccount(req dto.AgentAccountVerifyReq) error
	DeleteAccount(req dto.AgentAccountDeleteReq) error

	GetFeishuConfig(req dto.AgentFeishuConfigReq) (*dto.AgentFeishuConfig, error)
	UpdateFeishuConfig(req dto.AgentFeishuConfigUpdateReq) error
	GetTelegramConfig(req dto.AgentTelegramConfigReq) (*dto.AgentTelegramConfig, error)
	UpdateTelegramConfig(req dto.AgentTelegramConfigUpdateReq) error
	GetDiscordConfig(req dto.AgentIDReq) (*dto.AgentDiscordConfig, error)
	UpdateDiscordConfig(req dto.AgentDiscordConfigUpdateReq) error
	GetWecomConfig(req dto.AgentIDReq) (*dto.AgentWecomConfig, error)
	UpdateWecomConfig(req dto.AgentWecomConfigUpdateReq) error
	GetDingTalkConfig(req dto.AgentIDReq) (*dto.AgentDingTalkConfig, error)
	UpdateDingTalkConfig(req dto.AgentDingTalkConfigUpdateReq) error
	GetWeixinConfig(req dto.AgentIDReq) (*dto.AgentWeixinConfig, error)
	LoginWeixinChannel(req dto.AgentWeixinLoginReq) error
	GetQQBotConfig(req dto.AgentIDReq) (*dto.AgentQQBotConfig, error)
	UpdateQQBotConfig(req dto.AgentQQBotConfigUpdateReq) error
	DeleteChannelConfig(req dto.AgentChannelDeleteReq) error
	InstallPlugin(req dto.AgentPluginInstallReq) error
	UpgradePlugin(req dto.AgentPluginUpgradeReq) error
	UninstallPlugin(req dto.AgentPluginUninstallReq) error
	CheckPlugin(req dto.AgentPluginCheckReq) (*dto.AgentPluginStatus, error)
	ListPlugins(req dto.AgentPluginsReq) ([]dto.AgentPluginItem, error)
	SearchPlugins(req dto.AgentPluginSearchReq) ([]dto.AgentPluginSearchItem, error)
	InstallMarketPlugin(req dto.AgentPluginMarketInstallReq) error
	OperatePlugin(req dto.AgentPluginOperateReq) error
	ApproveChannelPairing(req dto.AgentChannelPairingApproveReq) error
}

type batchUpgradePlan struct {
	ctx batchAgentInstallContext
	req request.AppInstallUpgrade
}

type batchAgentInstallContext struct {
	agent   model.Agent
	install model.AppInstall
}

const (
	defaultBrowserExecutablePath  = "/home/node/.cache/ms-playwright/openclaw-browser"
	defaultBrowserProfile         = "openclaw"
	defaultUserTimezone           = "Asia/Shanghai"
	defaultToolsProfile           = "full"
	defaultToolsSessionVisibility = "all"
	maxCommunityAIAgents          = int64(5)
	openclawPluginBaseDir         = "/home/node/.openclaw/extensions"
	openclawPluginPackageTmpDir   = "/tmp/openclaw-plugin"
	openclawManagedSkillsDir      = "/home/node/.openclaw/skills"
	openclawGatewayPort           = 18789
	openclawAllowedOriginHost     = "127.0.0.1"
	openclawHTTPSVersion          = "2026.3.13"
	openclawHTTPVersion           = "2026.3.23"
	openclawTrustedProxyLoopback  = "127.0.0.1/32"
	defaultOpenclawNPMRegistry    = "https://registry.npmjs.org/"
)

func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
	agentType := req.AgentType
	if err := checkPortExist(req.WebUIPort); err != nil {
		return nil, err
	}
	if exist, _ := agentRepo.GetFirst(repo.WithByLowerName(req.Name)); exist != nil && exist.ID > 0 {
		return nil, buserr.New("ErrNameIsExist")
	}
	if installs, _ := appInstallRepo.ListBy(context.Background(), repo.WithByLowerName(req.Name)); len(installs) > 0 {
		return nil, buserr.New("ErrNameIsExist")
	}
	if !global.CONF.Base.IsEnterprise && !xpack.MultiNodeProvider.IsXpack() {
		count, _, err := agentRepo.Page(1, 1)
		if err != nil {
			return nil, err
		}
		if count >= maxCommunityAIAgents {
			return nil, buserr.WithMap("ErrAgentLimitReached", map[string]interface{}{"max": maxCommunityAIAgents}, nil)
		}
	}
	app, err := appRepo.GetFirst(appRepo.WithKey(agentType))
	if err != nil || app.ID == 0 {
		return nil, buserr.New("ErrRecordNotFound")
	}
	detail, err := appDetailRepo.GetFirst(appDetailRepo.WithAppId(app.ID), appDetailRepo.WithVersion(req.AppVersion))
	if err != nil || detail.ID == 0 {
		return nil, buserr.New("ErrRecordNotFound")
	}

	provider := ""
	baseURL := ""
	apiType := ""
	apiKey := ""
	runtimeModel := ""
	accountID := uint(0)
	token := ""
	configPath := ""
	storedModel := ""
	var allowedOrigins []string
	var account *model.AgentAccount
	var installHooks *appInstallHooks
	var dashboardAuth agentDashboardAuth

	if agentType == constant.AppOpenclaw || agentType == constant.AppHermesAgent {
		if req.AccountID == 0 {
			return nil, buserr.New("ErrAgentAccountRequired")
		}
		account, err = agentAccountRepo.GetFirst(repo.WithByID(req.AccountID))
		if err != nil {
			return nil, err
		}
		if !account.Verified {
			return nil, buserr.New("ErrAgentAccountNotVerified")
		}
		provider = account.Provider
		baseURL = account.BaseURL
		resolvedRuntime, err := resolveOpenclawAccountModelRuntimeByID(account, req.Model)
		if err != nil {
			return nil, err
		}
		storedModel = resolvedRuntime.StoredModel
		apiType = resolvedRuntime.APIType
		runtimeModel = resolvedRuntime.PrimaryModel
		apiKey = account.APIKey
		accountID = account.ID
	}

	if agentType == constant.AppOpenclaw {
		var err error
		allowedOrigins, err = normalizeAllowedOrigins(req.AllowedOrigins)
		if err != nil {
			return nil, err
		}
		if len(allowedOrigins) == 0 {
			return nil, fmt.Errorf("allowed origins is required")
		}
		token = strings.TrimSpace(req.Token)
		if token == "" {
			token = generateToken()
		}
		installHooks = &appInstallHooks{
			AfterCopyData: func(appInstall *model.AppInstall) error {
				return prepareOpenclawInstallFiles(appInstall, account, storedModel, token, allowedOrigins)
			},
		}
	} else if agentType == constant.AppHermesAgent {
		dashboardAuth = normalizeAgentDashboardAuth(req.DashboardUsername, req.DashboardPassword)
		installHooks = &appInstallHooks{
			AfterCopyData: func(appInstall *model.AppInstall) error {
				if err := prepareHermesInstallFiles(appInstall, account, storedModel); err != nil {
					return err
				}
				return writeAgentDashboardAuthEnv(appInstall.GetEnvPath(), agentType, dashboardAuth, false)
			},
		}
	} else if agentType == constant.AppCopaw {
		dashboardAuth = normalizeAgentDashboardAuth(req.DashboardUsername, req.DashboardPassword)
	}

	params := map[string]interface{}{
		constant.CPUS:        "0",
		constant.MemoryLimit: "0",
		constant.HostIP:      "",
	}
	setAgentWebUIParams(params, agentType, detail.Version, req.WebUIPort)
	if agentType == constant.AppOpenclaw {
		if allowedOrigin := firstAllowedOrigin(allowedOrigins); allowedOrigin != "" {
			params["ALLOWED_ORIGIN"] = allowedOrigin
		}
		params["PROVIDER"] = provider
		params["MODEL"] = runtimeModel
		params["API_TYPE"] = apiType
		params["BASE_URL"] = baseURL
		params["API_KEY"] = apiKey
		params["OPENCLAW_GATEWAY_TOKEN"] = token
	}
	if usernameKey, passwordKey, ok := agentDashboardAuthEnvKeys(agentType); ok {
		params[usernameKey] = dashboardAuth.Username
		params[passwordKey] = dashboardAuth.Password
		if agentType == constant.AppCopaw {
			params[qwenPawAuthEnabledEnvKey] = "true"
		}
	}

	if req.EditCompose && strings.TrimSpace(req.DockerCompose) == "" {
		return nil, buserr.New("ErrAgentComposeRequired")
	}
	installReq := request.AppInstallCreate{
		AppDetailId: detail.ID,
		Name:        req.Name,
		Params:      params,
		TaskID:      req.TaskID,
		AppContainerConfig: request.AppContainerConfig{
			Advanced:      req.Advanced,
			ContainerName: req.ContainerName,
			AllowPort:     req.AllowPort,
			SpecifyIP:     req.SpecifyIP,
			RestartPolicy: req.RestartPolicy,
			CpuQuota:      req.CpuQuota,
			MemoryLimit:   req.MemoryLimit,
			MemoryUnit:    req.MemoryUnit,
			PullImage:     req.PullImage,
			EditCompose:   req.EditCompose,
			DockerCompose: req.DockerCompose,
		},
	}
	appInstall, err := AppService{}.installWithHooks(installReq, false, installHooks)
	if err != nil {
		return nil, err
	}
	if agentType == constant.AppOpenclaw {
		configPath = path.Join(appInstall.GetPath(), "data", "conf", "openclaw.json")
	}
	if agentType == constant.AppHermesAgent {
		configPath = path.Join(appInstall.GetPath(), "data", "config.yaml")
	}
	agent := &model.Agent{
		Name:         req.Name,
		Remark:       req.Remark,
		AgentType:    agentType,
		Provider:     provider,
		Model:        storedModel,
		APIType:      apiType,
		BaseURL:      baseURL,
		APIKey:       apiKey,
		Token:        token,
		Status:       appInstall.Status,
		Message:      appInstall.Message,
		AppInstallID: appInstall.ID,
		AccountID:    accountID,
		ConfigPath:   configPath,
	}
	if err := agentRepo.Create(agent); err != nil {
		return nil, err
	}

	item := buildAgentItem(agent, appInstall, nil)
	return &item, nil
}

func (a AgentService) BatchInstall(req dto.AgentBatchInstallReq) (*dto.AgentItem, error) {
	accountID, err := a.ensureBatchInstallAccount(req)
	if err != nil {
		return nil, err
	}
	createReq := buildCreateReqFromBatchInstallReq(req)
	createReq.AccountID = accountID
	if strings.TrimSpace(createReq.Name) == "" {
		name, err := buildBatchInstallAgentName(createReq.AgentType)
		if err != nil {
			return nil, err
		}
		createReq.Name = name
	}
	if createReq.AgentType == constant.AppOpenclaw && len(createReq.AllowedOrigins) == 0 {
		allowedOrigin, err := buildBatchInstallOpenclawAllowedOrigin(req)
		if err != nil {
			return nil, err
		}
		createReq.AllowedOrigins = []string{allowedOrigin}
	}
	return a.Create(createReq)
}

func (a AgentService) BatchUpgrade(req dto.AgentBatchUpgradeReq) ([]dto.AgentBatchUpgradeResult, error) {
	plans, results, err := buildBatchUpgradePlans(req)
	if err != nil {
		return nil, err
	}
	for _, plan := range plans {
		result := dto.AgentBatchUpgradeResult{
			AgentID:      plan.ctx.agent.ID,
			AgentName:    plan.ctx.agent.Name,
			AppInstallID: plan.ctx.agent.AppInstallID,
		}
		if err := upgradeInstall(plan.req); err != nil {
			result.Message = err.Error()
		} else {
			result.Success = true
		}
		results = append(results, result)
	}
	return results, nil
}

func (a AgentService) BatchInstallSkill(req dto.AgentBatchSkillInstallReq) ([]dto.AgentBatchSkillInstallResult, error) {
	if req.AgentType != constant.AppOpenclaw && req.AgentType != constant.AppHermesAgent {
		return nil, fmt.Errorf("%s does not support skill batch install", req.AgentType)
	}
	packagePath, err := validateLocalSkillPackagePath(req.PackagePath)
	if err != nil {
		return nil, err
	}
	if format, err := localSkillPackageFormat(packagePath); err != nil {
		return nil, err
	} else if format != "zip" {
		return nil, fmt.Errorf("only .zip skill packages can be installed")
	}
	skillName := sanitizeLocalSkillDirName(req.SkillName, packagePath, req.SkillName)
	agents, err := listBatchAgents(req.AgentType)
	if err != nil {
		return nil, err
	}
	results := make([]dto.AgentBatchSkillInstallResult, 0, len(agents))
	for _, agent := range agents {
		result := dto.AgentBatchSkillInstallResult{
			AgentID:      agent.ID,
			AgentName:    agent.Name,
			AppInstallID: agent.AppInstallID,
		}
		ctx, message := loadBatchAgentInstall(agent, req.AgentType)
		if message != "" {
			result.Message = message
			results = append(results, result)
			continue
		}
		install := ctx.install
		result.AppInstallID = install.ID
		if install.Status == constant.StatusInstalling || install.Status == constant.StatusUpgrading {
			result.Message = fmt.Sprintf("agent status is %s", install.Status)
			results = append(results, result)
			continue
		}
		if err := ensureContainerRunning(install.ContainerName); err != nil {
			result.Message = err.Error()
			results = append(results, result)
			continue
		}
		installTask, err := task.NewTaskWithOps(skillName, task.TaskInstall, task.TaskScopeAI, buildBatchTaskID(req.TaskID, "batch-skill-install", agent.ID), agent.ID)
		if err != nil {
			result.Message = err.Error()
			results = append(results, result)
			continue
		}
		currentAgent := ctx.agent
		currentInstall := ctx.install
		installTask.AddSubTask("Install local skill", func(t *task.Task) error {
			mgr := cmd.NewCommandMgr(cmd.WithTask(*t), cmd.WithContext(t.TaskCtx), cmd.WithTimeout(20*time.Minute))
			return installLocalSkillPackage(mgr, currentInstall.ContainerName, currentAgent.AgentType, currentAgent.ConfigPath, packagePath, skillName)
		}, nil)
		go func() {
			if err := installTask.Execute(); err != nil {
				global.LOG.Errorf("batch install local skill failed: %v", err)
			}
		}()
		result.Success = true
		results = append(results, result)
	}
	return results, nil
}

func (a AgentService) BatchOperate(req dto.AgentBatchOperateReq) ([]dto.AgentBatchOperateResult, error) {
	operate := constant.AppOperate(strings.TrimSpace(req.Operate))
	if operate != constant.Start && operate != constant.Stop && operate != constant.Restart && operate != constant.Delete {
		return nil, fmt.Errorf("operate %s is not supported", req.Operate)
	}
	agents, err := listBatchAgents(req.AgentType)
	if err != nil {
		return nil, err
	}
	results := make([]dto.AgentBatchOperateResult, 0, len(agents))
	for _, agent := range agents {
		result := dto.AgentBatchOperateResult{
			AgentID:      agent.ID,
			AgentName:    agent.Name,
			AppInstallID: agent.AppInstallID,
		}
		if operate == constant.Delete {
			if err := a.Delete(dto.AgentDeleteReq{
				ID:          agent.ID,
				TaskID:      buildBatchTaskID(req.TaskID, "batch-operate", agent.ID),
				ForceDelete: req.ForceDelete,
			}); err != nil {
				result.Message = err.Error()
			} else {
				result.Success = true
			}
			results = append(results, result)
			continue
		}
		ctx, message := loadBatchAgentInstall(agent, req.AgentType)
		if message != "" {
			result.Message = message
			results = append(results, result)
			continue
		}
		install := ctx.install
		result.AppInstallID = install.ID
		if message := batchOperateSkipMessage(operate, install.Status); message != "" {
			result.Success = true
			result.Skipped = true
			result.Message = message
			results = append(results, result)
			continue
		}
		if err := NewIAppInstalledService().Operate(request.AppInstalledOperate{
			InstallId: install.ID,
			Operate:   operate,
			TaskID:    buildBatchTaskID(req.TaskID, "batch-operate", agent.ID),
		}); err != nil {
			result.Message = err.Error()
		} else {
			result.Success = true
		}
		results = append(results, result)
	}
	return results, nil
}

func listBatchAgents(agentType string) ([]model.Agent, error) {
	return agentRepo.List(func(db *gorm.DB) *gorm.DB {
		return db.Where("agent_type = ?", agentType).Order("id ASC")
	})
}

func loadBatchAgentInstall(agent model.Agent, agentType string) (batchAgentInstallContext, string) {
	ctx := batchAgentInstallContext{agent: agent}
	if agent.AppInstallID == 0 {
		return ctx, "agent app install id is empty"
	}
	install, err := appInstallRepo.GetFirst(repo.WithByID(agent.AppInstallID))
	if err != nil {
		return ctx, err.Error()
	}
	ctx.install = install
	if install.App.Key != agentType {
		return ctx, fmt.Sprintf("app key %s does not match agent type %s", install.App.Key, agentType)
	}
	return ctx, ""
}

func buildBatchUpgradePlans(req dto.AgentBatchUpgradeReq) ([]batchUpgradePlan, []dto.AgentBatchUpgradeResult, error) {
	agents, err := listBatchAgents(req.AgentType)
	if err != nil {
		return nil, nil, err
	}
	plans := make([]batchUpgradePlan, 0, len(agents))
	results := make([]dto.AgentBatchUpgradeResult, 0)
	for _, agent := range agents {
		result := dto.AgentBatchUpgradeResult{
			AgentID:      agent.ID,
			AgentName:    agent.Name,
			AppInstallID: agent.AppInstallID,
		}
		ctx, message := loadBatchAgentInstall(agent, req.AgentType)
		if message != "" {
			result.Message = message
			results = append(results, result)
			continue
		}
		install := ctx.install
		result.AppInstallID = install.ID
		if install.Status == constant.StatusInstalling || install.Status == constant.StatusUpgrading {
			result.Message = fmt.Sprintf("agent status is %s", install.Status)
			results = append(results, result)
			continue
		}
		if install.Version == req.TargetVersion {
			result.Success = true
			result.Skipped = true
			result.Message = "already target version"
			results = append(results, result)
			continue
		}
		detail, err := appDetailRepo.GetFirst(appDetailRepo.WithAppId(install.AppId), appDetailRepo.WithVersion(req.TargetVersion))
		if err != nil {
			result.Message = err.Error()
			results = append(results, result)
			continue
		}
		if detail.ID == 0 {
			result.Message = fmt.Sprintf("target version %s not found", req.TargetVersion)
			results = append(results, result)
			continue
		}
		plans = append(plans, batchUpgradePlan{
			ctx: ctx,
			req: request.AppInstallUpgrade{
				InstallID: install.ID,
				DetailID:  detail.ID,
				Backup:    req.Backup,
				PullImage: req.PullImage,
				TaskID:    buildBatchTaskID(req.TaskID, "batch-upgrade", install.ID),
			},
		})
	}
	return plans, results, nil
}

func buildBatchTaskID(taskID, prefix string, id uint) string {
	taskID = strings.TrimSpace(taskID)
	if taskID == "" {
		taskID = fmt.Sprintf("%s-%d-%d", prefix, id, time.Now().UnixNano())
	}
	return fmt.Sprintf("%s-%d", taskID, id)
}

func batchOperateSkipMessage(operate constant.AppOperate, status string) string {
	switch status {
	case constant.StatusInstalling, constant.StatusUpgrading, constant.StatusUninstalling, constant.StatusRebuilding:
		return fmt.Sprintf("agent status is %s", status)
	}
	switch operate {
	case constant.Start:
		if status == constant.StatusRunning || status == constant.StatusStarting || status == constant.StatusRestarting {
			return fmt.Sprintf("agent status is %s", status)
		}
	case constant.Stop:
		if status != constant.StatusRunning {
			return fmt.Sprintf("agent status is %s", status)
		}
	case constant.Restart:
		if status == constant.StatusStarting {
			return fmt.Sprintf("agent status is %s", status)
		}
	}
	return ""
}

func buildCreateReqFromBatchInstallReq(req dto.AgentBatchInstallReq) dto.AgentCreateReq {
	return dto.AgentCreateReq{
		Name:              req.Name,
		Remark:            req.Remark,
		AppVersion:        req.AppVersion,
		WebUIPort:         req.WebUIPort,
		BridgePort:        req.BridgePort,
		AllowedOrigins:    req.AllowedOrigins,
		AgentType:         req.AgentType,
		Model:             req.Model,
		AccountID:         req.AccountID,
		Token:             req.Token,
		DashboardUsername: req.DashboardUsername,
		DashboardPassword: req.DashboardPassword,
		TaskID:            req.TaskID,
		Advanced:          req.Advanced,
		ContainerName:     req.ContainerName,
		AllowPort:         req.AllowPort,
		SpecifyIP:         req.SpecifyIP,
		RestartPolicy:     req.RestartPolicy,
		CpuQuota:          req.CpuQuota,
		MemoryLimit:       req.MemoryLimit,
		MemoryUnit:        req.MemoryUnit,
		PullImage:         req.PullImage,
		EditCompose:       req.EditCompose,
		DockerCompose:     req.DockerCompose,
	}
}

func buildBatchInstallAgentName(agentType string) (string, error) {
	prefix := strings.ToLower(strings.TrimSpace(agentType))
	if prefix == "" {
		prefix = "agent"
	}
	for i := 1; i <= 10000; i++ {
		name := fmt.Sprintf("%s-%d", prefix, i)
		exists, err := agentNameExists(name)
		if err != nil {
			return "", err
		}
		if !exists {
			return name, nil
		}
	}
	return "", fmt.Errorf("no available agent name")
}

func agentNameExists(name string) (bool, error) {
	if exist, err := agentRepo.GetFirst(repo.WithByLowerName(name)); err == nil && exist != nil && exist.ID > 0 {
		return true, nil
	} else if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
		return false, err
	}
	installs, err := appInstallRepo.ListBy(context.Background(), repo.WithByLowerName(name))
	if err != nil {
		return false, err
	}
	return len(installs) > 0, nil
}

func buildBatchInstallOpenclawAllowedOrigin(req dto.AgentBatchInstallReq) (string, error) {
	host := ""
	if value, err := settingRepo.GetValueByKey("SystemIP"); err == nil {
		host = strings.TrimSpace(value)
	}
	if host == "" {
		host = strings.TrimSpace(req.FallbackAccessHost)
	}
	if host == "" {
		host = "127.0.0.1"
	}
	return buildOpenclawAllowedOrigin(openclawAllowedOriginScheme(req.AppVersion), host, req.WebUIPort)
}

func (a AgentService) ensureBatchInstallAccount(req dto.AgentBatchInstallReq) (uint, error) {
	if !requiresAgentAccount(req.AgentType) {
		return 0, nil
	}
	if req.MasterAccountID == 0 {
		return 0, buserr.New("ErrInvalidParams")
	}
	snapshot := req.AccountSnapshot
	account, err := agentAccountRepo.GetFirst(agentAccountRepo.WithByMasterAccountID(req.MasterAccountID))
	if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
		return 0, err
	}
	if account == nil && global.IsMaster {
		account, err = agentAccountRepo.GetFirst(repo.WithByID(req.MasterAccountID))
		if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
			return 0, err
		}
	}
	if account == nil {
		account = &model.AgentAccount{
			MasterAccountID: req.MasterAccountID,
		}
	}
	account.MasterAccountID = req.MasterAccountID
	account.Provider = snapshot.Provider
	account.Name = snapshot.Name
	account.APIKey = snapshot.APIKey
	account.RememberAPIKey = snapshot.RememberAPIKey
	account.BaseURL = snapshot.BaseURL
	account.APIType = snapshot.APIType
	account.AuthMode = snapshot.AuthMode
	account.Remark = snapshot.Remark
	account.Verified = true

	initialModels, err := buildInitialAgentAccountModels(account, req.AccountModels)
	if err != nil {
		return 0, err
	}
	verifyModel, err := resolveAgentAccountVerifyModel(account.Provider, snapshot.VerifyModel, initialModels)
	if err != nil {
		return 0, err
	}
	account.VerifyModel = verifyModel
	if err := global.DB.Transaction(func(tx *gorm.DB) error {
		if account.ID == 0 {
			if err := tx.Create(account).Error; err != nil {
				return err
			}
		} else {
			if err := tx.Save(account).Error; err != nil {
				return err
			}
		}
		return replacePersistedAgentAccountModelsWithTx(tx, account.ID, initialModels)
	}); err != nil {
		return 0, err
	}
	return account.ID, nil
}

func requiresAgentAccount(agentType string) bool {
	return agentType != constant.AppCopaw
}

func setAgentWebUIParams(params map[string]interface{}, agentType, appVersion string, webUIPort int) {
	if agentType == constant.AppOpenclaw && isOpenclawHTTPSWindowVersion(appVersion) {
		params["PANEL_APP_PORT_HTTPS"] = webUIPort
		return
	}
	params["PANEL_APP_PORT_HTTP"] = webUIPort
}

func (a AgentService) Page(req dto.SearchWithPage) (int64, []dto.AgentItem, error) {
	var opts []repo.DBOption
	if strings.TrimSpace(req.Info) != "" {
		opts = append(opts, repo.WithByLikeName(req.Info))
	}
	count, list, err := agentRepo.Page(req.Page, req.PageSize, opts...)
	if err != nil {
		return 0, nil, err
	}
	items := make([]dto.AgentItem, 0, len(list))
	appInstalls := make([]model.AppInstall, 0, len(list))
	for _, item := range list {
		appInstall, _ := appInstallRepo.GetFirst(repo.WithByID(item.AppInstallID))
		appInstalls = append(appInstalls, appInstall)
	}
	syncAgentAppInstalls(appInstalls)
	for index, item := range list {
		appInstall := appInstalls[index]
		envMap := readInstallEnv(appInstall.Env)
		agentItem := buildAgentItem(&item, &appInstall, envMap)
		agentItem.Upgradable = checkAgentUpgradable(appInstall)
		items = append(items, agentItem)
	}
	if err := hydrateAgentWebsiteItems(items); err != nil {
		return 0, nil, err
	}
	return count, items, nil
}

func (a AgentService) Delete(req dto.AgentDeleteReq) error {
	agent, err := agentRepo.GetFirst(repo.WithByID(req.ID))
	if err != nil {
		return err
	}
	resources, err := a.deleteCheckByAgent(agent)
	if err != nil {
		return err
	}
	if len(resources) > 0 {
		return buserr.New("ErrAgentWebsiteBound")
	}
	if agent.AppInstallID == 0 {
		return agentRepo.DeleteByID(agent.ID)
	}
	operate := request.AppInstalledOperate{
		InstallId:   agent.AppInstallID,
		Operate:     constant.Delete,
		TaskID:      req.TaskID,
		ForceDelete: req.ForceDelete,
	}
	if err := NewIAppInstalledService().Operate(operate); err != nil {
		return err
	}
	return nil
}

func (a AgentService) DeleteCheck(req dto.AgentIDReq) ([]dto.AppResource, error) {
	agent, err := agentRepo.GetFirst(repo.WithByID(req.AgentID))
	if err != nil {
		return nil, err
	}
	return a.deleteCheckByAgent(agent)
}

func (a AgentService) deleteCheckByAgent(agent *model.Agent) ([]dto.AppResource, error) {
	if agent == nil || agent.WebsiteID == 0 {
		return nil, nil
	}
	website, err := websiteRepo.GetFirst(repo.WithByID(agent.WebsiteID))
	if err != nil {
		if err == gorm.ErrRecordNotFound {
			return nil, nil
		}
		return nil, err
	}
	websiteName, err := loadAgentWebsiteResourceName(website)
	if err != nil {
		return nil, err
	}
	return []dto.AppResource{{Type: "website", Name: websiteName}}, nil
}

func syncAgentAppInstalls(appInstalls []model.AppInstall) {
	if len(appInstalls) == 0 {
		return
	}

	var containersMap map[string]container.Summary
	cli, err := docker.NewClient()
	if err == nil {
		defer cli.Close()
		containers, err := cli.ListAllContainers()
		if err == nil {
			containersMap = make(map[string]container.Summary, len(containers))
			for _, contain := range containers {
				containersMap[contain.Names[0]] = contain
			}
		}
	}

	for index := range appInstalls {
		if appInstalls[index].ID == 0 || doNotNeedSync(appInstalls[index]) {
			continue
		}
		synAppInstall(containersMap, &appInstalls[index], false)
	}
}

func (a AgentService) ResetToken(req dto.AgentTokenResetReq) error {
	agent, err := loadOpenclawAgentByID(req.ID)
	if err != nil {
		return err
	}
	conf, err := readOpenclawConfig(agent.ConfigPath)
	if err != nil {
		return err
	}
	newToken := generateToken()
	if newToken == "" {
		return fmt.Errorf("generate token failed")
	}
	gatewayMap := ensureChildMap(conf, "gateway")
	authMap := ensureChildMap(gatewayMap, "auth")
	if _, ok := authMap["mode"]; !ok {
		authMap["mode"] = "token"
	}
	authMap["token"] = newToken
	if err := writeOpenclawConfigRaw(agent.ConfigPath, conf); err != nil {
		return err
	}
	agent.Token = newToken
	return agentRepo.Save(agent)
}

func (a AgentService) UpdateRemark(req dto.AgentRemarkUpdateReq) error {
	agent, err := agentRepo.GetFirst(repo.WithByID(req.ID))
	if err != nil {
		return err
	}
	agent.Remark = req.Remark
	return agentRepo.Save(agent)
}

func (a AgentService) GetModelConfig(req dto.AgentIDReq) (*dto.AgentModelConfig, error) {
	agent, err := agentRepo.GetFirst(repo.WithByID(req.AgentID))
	if err != nil {
		return nil, err
	}
	if agent.AgentType == constant.AppHermesAgent {
		cfg, err := readHermesConfig(agent.ConfigPath)
		if err != nil {
			return nil, err
		}
		account, err := agentAccountRepo.GetFirst(repo.WithByID(agent.AccountID))
		if err != nil {
			return nil, err
		}
		accountModels, err := loadAgentAccountModels(account)
		if err != nil {
			return nil, err
		}
		model, err := resolveHermesConfiguredModelID(account, accountModels, cfg.Model.Default)
		if err != nil {
			return nil, err
		}
		return &dto.AgentModelConfig{
			AccountID: agent.AccountID,
			Model:     model,
			Fallbacks: []string{},
		}, nil
	}
	agent, _, conf, err := a.loadOpenclawAgentConfig(req.AgentID)
	if err != nil {
		return nil, err
	}
	account, err := agentAccountRepo.GetFirst(repo.WithByID(agent.AccountID))
	if err != nil {
		return nil, err
	}
	models, err := loadAgentAccountModels(account)
	if err != nil {
		return nil, err
	}
	model := extractOpenclawPrimaryModelID(conf, account, models)
	if model == "" {
		model = agent.Model
	}
	return &dto.AgentModelConfig{
		AccountID: agent.AccountID,
		Model:     model,
		Fallbacks: extractOpenclawFallbackModelIDs(conf, account, models, model),
	}, nil
}

func (a AgentService) UpdateModelConfig(req dto.AgentModelConfigUpdateReq) error {
	agent, err := agentRepo.GetFirst(repo.WithByID(req.AgentID))
	if err != nil {
		return err
	}
	account, err := agentAccountRepo.GetFirst(repo.WithByID(req.AccountID))
	if err != nil {
		return err
	}
	resolvedRuntime, err := resolveOpenclawAccountModelRuntimeByID(account, req.Model)
	if err != nil {
		return err
	}
	modelName := resolvedRuntime.StoredModel
	apiType := resolvedRuntime.APIType
	confDir := path.Dir(agent.ConfigPath)
	if agent.AgentType == constant.AppHermesAgent {
		cfg, err := readHermesConfig(agent.ConfigPath)
		if err != nil {
			return err
		}
		if err := writeHermesConfig(confDir, account, modelName, cfg.Timezone); err != nil {
			return err
		}
	} else {
		if agent.AgentType != constant.AppOpenclaw {
			return fmt.Errorf("%s does not support", agent.AgentType)
		}
		if err := writeOpenclawConfig(confDir, account, modelName, agent.Token, nil, req.Fallbacks); err != nil {
			return err
		}
	}
	agent.Provider = account.Provider
	agent.Model = modelName
	agent.APIType = apiType
	agent.BaseURL = account.BaseURL
	agent.APIKey = account.APIKey
	agent.AccountID = account.ID
	return agentRepo.Save(agent)
}

func (a AgentService) GetProviders() ([]dto.ProviderInfo, error) {
	definitions := providercatalog.All()
	providers := make([]dto.ProviderInfo, 0, len(definitions))
	for key, def := range definitions {
		models := make([]dto.ProviderModelInfo, 0, len(def.Models))
		for _, item := range def.Models {
			models = append(models, dto.ProviderModelInfo{
				ID:   item.ID,
				Name: item.Name,
			})
		}
		apiTypes := make([]dto.ProviderAPIInfo, 0, len(def.APIConfigs))
		for _, item := range def.APIConfigs {
			apiModels := make([]dto.ProviderModelInfo, 0, len(item.Models))
			for _, model := range item.Models {
				apiModels = append(apiModels, dto.ProviderModelInfo{
					ID:   model.ID,
					Name: model.Name,
				})
			}
			apiTypes = append(apiTypes, dto.ProviderAPIInfo{
				APIType:                item.APIType,
				BaseURL:                item.BaseURL,
				EditableBaseURL:        item.EditableBaseURL,
				SupportsModelDiscovery: item.DiscoverModels,
				DefaultAuthMode:        item.DefaultAuthMode,
				AuthModes:              item.AuthModes,
				Models:                 apiModels,
			})
		}
		baseURL, _ := providercatalog.DefaultBaseURL(key)
		providers = append(providers, dto.ProviderInfo{
			Sort:           def.Sort,
			Provider:       key,
			DisplayName:    localizedAgentProviderName(key),
			BaseURL:        baseURL,
			DefaultAPIType: def.DefaultAPIType,
			APITypes:       apiTypes,
			Models:         models,
		})
	}
	sort.Slice(providers, func(i, j int) bool {
		return providers[i].Sort < providers[j].Sort
	})
	return providers, nil
}

func (a AgentService) CreateAccount(req dto.AgentAccountCreateReq) error {
	agentAccountMutationMu.Lock()
	defer agentAccountMutationMu.Unlock()
	provider := req.Provider
	if err := ensureAgentAccountNameAvailable(provider, req.Name, 0); err != nil {
		return err
	}
	initialModels, err := buildInitialAgentAccountModels(&model.AgentAccount{Provider: provider, APIType: req.APIType}, req.Models)
	if err != nil {
		return err
	}
	verifyModel, err := resolveAgentAccountVerifyModel(provider, req.VerifyModel, initialModels)
	if err != nil {
		return err
	}
	validateAvailability := req.ValidateAvailability == nil || *req.ValidateAvailability
	resolvedInput, err := resolveAgentAccountInput(provider, req.APIType, req.AuthMode, req.APIKey, req.BaseURL, verifyModel, validateAvailability)
	if err != nil {
		return err
	}
	account := &model.AgentAccount{
		Provider:       resolvedInput.Provider,
		Name:           req.Name,
		APIKey:         resolvedInput.APIKey,
		RememberAPIKey: req.RememberAPIKey,
		BaseURL:        resolvedInput.BaseURL,
		APIType:        resolvedInput.APIType,
		AuthMode:       resolvedInput.AuthMode,
		VerifyModel:    verifyModel,
		Verified:       true,
		Remark:         req.Remark,
	}
	if err := global.DB.Transaction(func(tx *gorm.DB) error {
		if err := tx.Create(account).Error; err != nil {
			return err
		}
		if len(initialModels) == 0 {
			return nil
		}
		return replacePersistedAgentAccountModelsWithTx(tx, account.ID, initialModels)
	}); err != nil {
		return err
	}
	asyncReportAIProviderInstall(provider)
	return nil
}

func (a AgentService) UpdateAccount(req dto.AgentAccountUpdateReq) error {
	agentAccountMutationMu.Lock()
	defer agentAccountMutationMu.Unlock()
	account, err := agentAccountRepo.GetFirst(repo.WithByID(req.ID))
	if err != nil {
		return err
	}
	if req.APIType != account.APIType {
		return buserr.WithDetail("ErrInvalidParams", "API type cannot be changed", nil)
	}
	provider := account.Provider
	if err := ensureAgentAccountNameAvailable(provider, req.Name, account.ID); err != nil {
		return err
	}
	models, err := loadAgentAccountModels(account)
	if err != nil {
		return err
	}
	requestedVerifyModel := req.VerifyModel
	if strings.TrimSpace(requestedVerifyModel) == "" {
		requestedVerifyModel = account.VerifyModel
	}
	verifyModel, err := resolveAgentAccountVerifyModel(provider, requestedVerifyModel, models)
	if err != nil {
		return err
	}
	validateAvailability := req.ValidateAvailability == nil || *req.ValidateAvailability
	resolvedInput, err := resolveAgentAccountInput(provider, req.APIType, req.AuthMode, req.APIKey, req.BaseURL, verifyModel, validateAvailability)
	if err != nil {
		return err
	}
	account.Name = req.Name
	account.APIKey = resolvedInput.APIKey
	account.RememberAPIKey = req.RememberAPIKey
	account.BaseURL = resolvedInput.BaseURL
	account.APIType = resolvedInput.APIType
	account.AuthMode = resolvedInput.AuthMode
	account.VerifyModel = verifyModel
	account.Remark = req.Remark
	account.Verified = true

	if err := global.DB.Save(account).Error; err != nil {
		return err
	}
	terminalai.InvalidateTerminalRuntimeCache()
	terminalai.InvalidateFileAIRuntimeCache()
	if req.SyncAgents {
		if err := a.syncAgentsByAccount(account); err != nil {
			return err
		}
	}
	return nil
}

func (a AgentService) PageAccounts(req dto.AgentAccountSearch) (int64, []dto.AgentAccountInfo, error) {
	var opts []repo.DBOption
	if strings.TrimSpace(req.Provider) != "" {
		opts = append(opts, repo.WithByProvider(req.Provider))
	}
	if apiType := strings.TrimSpace(req.APIType); apiType != "" {
		opts = append(opts, repo.WithByAPIType(apiType))
	}
	if req.TextOnly {
		opts = append(opts, repo.WithTextAPIType())
	}
	if strings.TrimSpace(req.Name) != "" {
		opts = append(opts, repo.WithByLikeName(req.Name))
	}
	count, list, err := agentAccountRepo.Page(req.Page, req.PageSize, opts...)
	if err != nil {
		return 0, nil, err
	}
	items := make([]dto.AgentAccountInfo, 0, len(list))
	for _, item := range list {
		apiKey := ""
		if item.RememberAPIKey {
			apiKey = item.APIKey
		}
		items = append(items, dto.AgentAccountInfo{
			ID:              item.ID,
			MasterAccountID: item.MasterAccountID,
			Provider:        item.Provider,
			ProviderName:    localizedAgentProviderName(item.Provider),
			Name:            item.Name,
			APIKey:          apiKey,
			RememberAPIKey:  item.RememberAPIKey,
			BaseURL:         item.BaseURL,
			Models:          nil,
			APIType:         item.APIType,
			AuthMode:        item.AuthMode,
			VerifyModel:     item.VerifyModel,
			Verified:        item.Verified,
			Remark:          item.Remark,
			CreatedAt:       item.CreatedAt,
		})
	}
	if len(list) > 0 {
		accountIDs := make([]uint, 0, len(list))
		for _, account := range list {
			accountIDs = append(accountIDs, account.ID)
		}
		var rows []model.AgentAccountModel
		if err := global.DB.Where("account_id IN ?", accountIDs).Order("account_id ASC, sort_order ASC, id ASC").Find(&rows).Error; err != nil {
			return 0, nil, err
		}
		modelsByAccount := make(map[uint][]dto.AgentAccountModel, len(list))
		for _, row := range rows {
			modelsByAccount[row.AccountID] = append(modelsByAccount[row.AccountID], dto.AgentAccountModel{
				RecordID: row.ID,
				ID:       strings.TrimSpace(row.Model),
				Name:     strings.TrimSpace(row.Name),
			})
		}
		for index, account := range list {
			items[index].Models = modelsByAccount[account.ID]
		}
	}
	return count, items, nil
}

func (a AgentService) CountAccountsByProviders(req dto.AgentAccountProviderCountReq) (map[string]int64, error) {
	return agentAccountRepo.CountTextByProviders(req.Providers)
}

func (a AgentService) GetAccountModels(req dto.AgentAccountModelReq) ([]dto.AgentAccountModel, error) {
	account, err := agentAccountRepo.GetFirst(repo.WithByID(req.AccountID))
	if err != nil {
		return nil, err
	}
	return loadAgentAccountModels(account)
}

func (a AgentService) DiscoverAccountModels(req dto.AgentAccountModelDiscoverReq) ([]dto.AgentAccountModel, error) {
	config, ok := providercatalog.FindAPIConfig(req.Provider, req.APIType)
	if !ok || !config.DiscoverModels {
		return nil, buserr.New("ErrAgentAccountModelsRequired")
	}
	baseURL, err := providercatalog.ResolveBaseURL(req.Provider, req.APIType, req.BaseURL)
	if err != nil {
		return nil, buserr.WithErr("ErrAgentAccountUnavailable", err)
	}
	models, err := providercatalog.DiscoverModels(baseURL, req.APIKey)
	if err != nil {
		return nil, buserr.WithErr("ErrAgentAccountUnavailable", err)
	}
	return buildDiscoveredAgentAccountModels(models), nil
}

func (a AgentService) CreateAccountModel(req dto.AgentAccountModelCreateReq) error {
	account, err := agentAccountRepo.GetFirst(repo.WithByID(req.AccountID))
	if err != nil {
		return err
	}
	models, err := loadAgentAccountModels(account)
	if err != nil {
		return err
	}
	nextModel, err := normalizeAgentAccountModel(account, req.Model)
	if err != nil {
		return err
	}
	if _, ok := findAgentAccountModelForProvider(account.Provider, models, nextModel.ID); ok {
		return buserr.New("ErrRecordExist")
	}
	sortOrder := len(models) + 1
	record := &model.AgentAccountModel{
		AccountID: account.ID,
		Model:     nextModel.ID,
		Name:      nextModel.Name,
		SortOrder: sortOrder,
	}
	if err := agentAccountModelRepo.Create(record); err != nil {
		return err
	}
	return a.syncAgentsByAccount(account)
}

func (a AgentService) UpdateAccountModel(req dto.AgentAccountModelUpdateReq) error {
	account, err := agentAccountRepo.GetFirst(repo.WithByID(req.AccountID))
	if err != nil {
		return err
	}
	record, err := agentAccountModelRepo.GetFirst(repo.WithByID(req.Model.RecordID), repo.WithByAccountID(req.AccountID))
	if err != nil {
		return err
	}
	models, err := loadAgentAccountModels(account)
	if err != nil {
		return err
	}
	nextModel, err := normalizeAgentAccountModel(account, req.Model)
	if err != nil {
		return err
	}
	nextModel.RecordID = req.Model.RecordID
	for _, item := range models {
		if item.RecordID == req.Model.RecordID {
			continue
		}
		if sameProviderModelID(account.Provider, item.ID, nextModel.ID) {
			return buserr.New("ErrRecordExist")
		}
	}
	nextModels := make([]dto.AgentAccountModel, 0, len(models))
	for _, item := range models {
		if item.RecordID == req.Model.RecordID {
			nextModels = append(nextModels, nextModel)
			continue
		}
		nextModels = append(nextModels, item)
	}
	if err := ensureAccountModelsNotBound(account, nextModels); err != nil {
		return err
	}
	previousModelID := record.Model
	record.Model = nextModel.ID
	record.Name = nextModel.Name
	if err := agentAccountModelRepo.Save(record); err != nil {
		return err
	}
	if sameProviderModelID(account.Provider, account.VerifyModel, previousModelID) {
		account.VerifyModel = nextModel.ID
		if err := agentAccountRepo.Save(account); err != nil {
			return err
		}
	}
	terminalai.InvalidateTerminalRuntimeCache()
	terminalai.InvalidateFileAIRuntimeCache()
	return a.syncAgentsByAccount(account)
}

func (a AgentService) DeleteAccountModel(req dto.AgentAccountModelDeleteReq) error {
	account, err := agentAccountRepo.GetFirst(repo.WithByID(req.AccountID))
	if err != nil {
		return err
	}
	record, err := agentAccountModelRepo.GetFirst(repo.WithByID(req.RecordID), repo.WithByAccountID(req.AccountID))
	if err != nil {
		return err
	}
	if sameProviderModelID(account.Provider, account.VerifyModel, record.Model) {
		return buserr.New("ErrAgentVerifyModelInUse")
	}
	models, err := loadAgentAccountModels(account)
	if err != nil {
		return err
	}
	nextModels := make([]dto.AgentAccountModel, 0, len(models))
	for _, item := range models {
		if item.RecordID == req.RecordID {
			continue
		}
		nextModels = append(nextModels, item)
	}
	if err := ensureAccountModelsNotBound(account, nextModels); err != nil {
		return err
	}
	if err := agentAccountModelRepo.DeleteByID(req.RecordID); err != nil {
		return err
	}
	if err := compactPersistedAgentAccountModelSortOrder(req.AccountID); err != nil {
		return err
	}
	terminalai.InvalidateTerminalRuntimeCache()
	terminalai.InvalidateFileAIRuntimeCache()
	return a.syncAgentsByAccount(account)
}

func (a AgentService) SyncAgentsByAccount(account *model.AgentAccount) error {
	if account == nil || account.ID == 0 {
		return nil
	}
	return a.syncAgentsByAccount(account)
}

func (a AgentService) VerifyAccount(req dto.AgentAccountVerifyReq) error {
	_, err := resolveAgentAccountInput(req.Provider, req.APIType, req.AuthMode, req.APIKey, req.BaseURL, req.Model, true)
	return err
}

func (a AgentService) DeleteAccount(req dto.AgentAccountDeleteReq) error {
	agentAccountMutationMu.Lock()
	defer agentAccountMutationMu.Unlock()
	if err := global.DB.Transaction(func(tx *gorm.DB) error {
		var agentCount int64
		if err := tx.Model(&model.Agent{}).Where("account_id = ?", req.ID).Count(&agentCount).Error; err != nil {
			return err
		}
		if agentCount > 0 {
			return buserr.New("ErrAgentAccountBound")
		}
		used, err := agentAccountUsedBySetting(tx, req.ID, "AIStatus", "AIAccountID")
		if err != nil {
			return err
		}
		if used {
			return buserr.New("ErrTerminalAIAccountInUse")
		}
		used, err = agentAccountUsedBySetting(tx, req.ID, "FileAIStatus", "FileAIAccountID")
		if err != nil {
			return err
		}
		if used {
			return buserr.New("ErrFileAIAccountInUse")
		}
		if err := tx.Where("account_id = ?", req.ID).Delete(&model.AgentAccountModel{}).Error; err != nil {
			return err
		}
		return tx.Delete(&model.AgentAccount{}, req.ID).Error
	}); err != nil {
		return err
	}
	terminalai.InvalidateTerminalRuntimeCache()
	terminalai.InvalidateFileAIRuntimeCache()
	return nil
}

func (a AgentService) GetSecurityConfig(req dto.AgentIDReq) (*dto.AgentSecurityConfig, error) {
	agent, _, err := a.loadOpenclawAgentAndInstall(req.AgentID)
	if err != nil {
		return nil, err
	}
	conf, err := readOpenclawConfig(agent.ConfigPath)
	if err != nil {
		return nil, err
	}
	result := extractSecurityConfig(conf)
	return &result, nil
}

func (a AgentService) UpdateSecurityConfig(req dto.AgentSecurityConfigUpdateReq) error {
	agent, install, err := a.loadOpenclawAgentAndInstall(req.AgentID)
	if err != nil {
		return err
	}
	allowedOrigins, err := normalizeAllowedOrigins(req.AllowedOrigins)
	if err != nil {
		return err
	}
	if len(allowedOrigins) == 0 {
		return fmt.Errorf("allowed origins is required")
	}
	conf, err := readOpenclawConfig(agent.ConfigPath)
	if err != nil {
		return err
	}
	setSecurityConfig(conf, dto.AgentSecurityConfig{AllowedOrigins: allowedOrigins})
	if err := writeOpenclawConfigRaw(agent.ConfigPath, conf); err != nil {
		return err
	}
	if err := syncOpenclawAllowedOriginEnv(install, allowedOrigins); err != nil {
		return err
	}
	return appInstallRepo.Save(context.Background(), install)
}

func (a AgentService) GetOtherConfig(req dto.AgentIDReq) (*dto.AgentOtherConfig, error) {
	agent, install, err := a.loadAgentAndInstall(req.AgentID)
	if err != nil {
		return nil, err
	}
	if agent.AgentType == constant.AppHermesAgent {
		cfg, err := readHermesConfig(agent.ConfigPath)
		if err != nil {
			return nil, err
		}
		auth := readAgentDashboardAuthFromInstall(install, agent.AgentType)
		return &dto.AgentOtherConfig{
			UserTimezone:      cfg.Timezone,
			BrowserEnabled:    true,
			NPMRegistry:       "https://registry.npmjs.org/",
			DashboardUsername: auth.Username,
			DashboardPassword: auth.Password,
		}, nil
	}
	if agent.AgentType == constant.AppCopaw {
		auth := readAgentDashboardAuthFromInstall(install, agent.AgentType)
		return &dto.AgentOtherConfig{
			DashboardUsername: auth.Username,
			DashboardPassword: auth.Password,
		}, nil
	}
	conf, err := readOpenclawConfig(agent.ConfigPath)
	if err != nil {
		return nil, err
	}
	result := extractOtherConfig(conf)
	npmRegistry, err := getOpenclawNPMRegistry(install.ContainerName)
	if err == nil {
		result.NPMRegistry = npmRegistry
	}
	return &result, nil
}

func (a AgentService) UpdateOtherConfig(req dto.AgentOtherConfigUpdateReq) error {
	agent, install, err := a.loadAgentAndInstall(req.AgentID)
	if err != nil {
		return err
	}
	if agent.AgentType == constant.AppHermesAgent {
		if strings.TrimSpace(req.UserTimezone) == "" {
			return buserr.New("ErrInvalidParams")
		}
		account, err := agentAccountRepo.GetFirst(repo.WithByID(agent.AccountID))
		if err != nil {
			return err
		}
		previousAuth := readAgentDashboardAuthFromInstall(install, agent.AgentType)
		nextAuth := normalizeAgentDashboardAuth(req.DashboardUsername, req.DashboardPassword)
		if err := writeHermesConfig(path.Dir(agent.ConfigPath), account, agent.Model, strings.TrimSpace(req.UserTimezone)); err != nil {
			return err
		}
		if err := writeAgentDashboardAuthEnv(install.GetEnvPath(), agent.AgentType, nextAuth, true); err != nil {
			return err
		}
		operate := constant.Restart
		if previousAuth.Username != nextAuth.Username || previousAuth.Password != nextAuth.Password {
			operate = constant.Rebuild
		}
		return NewIAppInstalledService().Operate(request.AppInstalledOperate{
			InstallId: install.ID,
			Operate:   operate,
		})
	}
	if agent.AgentType == constant.AppCopaw {
		return updateQwenPawDashboardAuth(install, normalizeAgentDashboardAuth(req.DashboardUsername, req.DashboardPassword))
	}
	if strings.TrimSpace(req.UserTimezone) == "" || strings.TrimSpace(req.NPMRegistry) == "" {
		return buserr.New("ErrInvalidParams")
	}
	if err := ensureContainerRunning(install.ContainerName); err != nil {
		return err
	}
	conf, err := readOpenclawConfig(agent.ConfigPath)
	if err != nil {
		return err
	}
	setOtherConfig(conf, dto.AgentOtherConfig{
		UserTimezone:   strings.TrimSpace(req.UserTimezone),
		BrowserEnabled: req.BrowserEnabled,
	})
	if err := writeOpenclawConfigRaw(agent.ConfigPath, conf); err != nil {
		return err
	}
	if err := setOpenclawNPMRegistry(install.ContainerName, req.NPMRegistry); err != nil {
		return err
	}
	return nil
}

func (a AgentService) GetConfigFile(req dto.AgentConfigFileReq) (*dto.AgentConfigFile, error) {
	agent, _, err := a.loadAgentAndInstall(req.AgentID)
	if err != nil {
		return nil, err
	}
	content, err := os.ReadFile(agent.ConfigPath)
	if err != nil {
		return nil, err
	}
	return &dto.AgentConfigFile{Content: string(content)}, nil
}

func (a AgentService) UpdateConfigFile(req dto.AgentConfigFileUpdateReq) error {
	agent, install, err := a.loadAgentAndInstall(req.AgentID)
	if err != nil {
		return err
	}
	if err := validateAgentConfigFileContent(agent.AgentType, req.Content); err != nil {
		return err
	}
	info, err := os.Stat(agent.ConfigPath)
	if err != nil {
		return err
	}
	if err := os.WriteFile(agent.ConfigPath, []byte(req.Content), info.Mode()); err != nil {
		return err
	}
	if agent.AgentType == constant.AppHermesAgent {
		cfg, err := readHermesConfig(agent.ConfigPath)
		if err != nil {
			return err
		}
		if cfg.Model.Default != "" {
			agent.Model = cfg.Model.Default
			if err := agentRepo.Save(agent); err != nil {
				return err
			}
		}
	}
	return NewIAppInstalledService().Operate(request.AppInstalledOperate{
		InstallId: install.ID,
		Operate:   constant.Restart,
	})
}

func validateAgentConfigFileContent(agentType, content string) error {
	var payload interface{}
	if agentType == constant.AppHermesAgent {
		return yaml.Unmarshal([]byte(content), &payload)
	}
	return json.Unmarshal([]byte(content), &payload)
}

func getOpenclawNPMRegistry(containerName string) (string, error) {
	registry, err := cmd.RunDockerExecWithStdout(20*time.Second, containerName, "npm", "get", "registry")
	if err != nil {
		return "", err
	}
	registry = strings.TrimSpace(registry)
	if registry == "" {
		return defaultOpenclawNPMRegistry, nil
	}
	return registry, nil
}

func setOpenclawNPMRegistry(containerName, registry string) error {
	return cmd.NewCommandMgr().Run("docker", "exec", containerName, "npm", "set", "registry", registry)
}

func (a AgentService) loadAgentAndInstall(agentID uint) (*model.Agent, *model.AppInstall, error) {
	agent, err := agentRepo.GetFirst(repo.WithByID(agentID))
	if err != nil {
		return nil, nil, err
	}
	if agent.AppInstallID == 0 {
		return nil, nil, buserr.New("ErrRecordNotFound")
	}
	install, err := appInstallRepo.GetFirst(repo.WithByID(agent.AppInstallID))
	if err != nil {
		return nil, nil, err
	}
	return agent, &install, nil
}

func (a AgentService) loadOpenclawAgentAndInstall(agentID uint) (*model.Agent, *model.AppInstall, error) {
	agent, install, err := a.loadAgentAndInstall(agentID)
	if err != nil {
		return nil, nil, err
	}
	if agent.AgentType != constant.AppOpenclaw {
		return nil, nil, fmt.Errorf("%s does not support", agent.AgentType)
	}
	return agent, install, nil
}

func (a AgentService) loadAgentConfig(agentID uint) (*model.Agent, *model.AppInstall, map[string]interface{}, error) {
	agent, install, err := a.loadAgentAndInstall(agentID)
	if err != nil {
		return nil, nil, nil, err
	}
	conf, err := readOpenclawConfig(agent.ConfigPath)
	if err != nil {
		return nil, nil, nil, err
	}
	return agent, install, conf, nil
}

func (a AgentService) loadOpenclawAgentConfig(agentID uint) (*model.Agent, *model.AppInstall, map[string]interface{}, error) {
	agent, install, conf, err := a.loadAgentConfig(agentID)
	if err != nil {
		return nil, nil, nil, err
	}
	if agent.AgentType != constant.AppOpenclaw {
		return nil, nil, nil, fmt.Errorf("%s does not support", agent.AgentType)
	}
	return agent, install, conf, nil
}

func (a AgentService) mutateAgentConfig(agentID uint, fn func(agent *model.Agent, install *model.AppInstall, conf map[string]interface{}) error) error {
	agent, install, conf, err := a.loadAgentConfig(agentID)
	if err != nil {
		return err
	}
	if err := fn(agent, install, conf); err != nil {
		return err
	}
	return writeOpenclawConfigRaw(agent.ConfigPath, conf)
}

func (a AgentService) syncAgentsByAccount(account *model.AgentAccount) error {
	agents, err := agentRepo.List(repo.WithByAccountID(account.ID))
	if err != nil {
		return err
	}
	accountModels, err := loadAgentAccountModels(account)
	if err != nil {
		return err
	}
	if len(accountModels) == 0 {
		return nil
	}
	for _, agent := range agents {
		selectedAccountModel, err := requireAgentAccountModelForProvider(account.Provider, accountModels, agent.Model)
		if err != nil {
			return buserr.WithName("ErrAgentModelInUse", agent.Name)
		}
		resolvedRuntime, err := buildOpenclawAccountModelRuntime(account, selectedAccountModel)
		if err != nil {
			return err
		}
		modelName := resolvedRuntime.StoredModel
		apiType := resolvedRuntime.APIType
		confDir := path.Dir(agent.ConfigPath)
		switch agent.AgentType {
		case constant.AppOpenclaw:
			conf, err := readOpenclawConfig(agent.ConfigPath)
			if err != nil {
				return err
			}
			fallbacks := extractOpenclawFallbackModelIDs(conf, account, accountModels, selectedAccountModel.ID)
			if err := writeOpenclawConfig(confDir, account, modelName, agent.Token, nil, fallbacks); err != nil {
				return err
			}
		case constant.AppHermesAgent:
			cfg, err := readHermesConfig(agent.ConfigPath)
			if err != nil {
				return err
			}
			if err := writeHermesConfig(confDir, account, modelName, cfg.Timezone); err != nil {
				return err
			}
		default:
			continue
		}
		agent.BaseURL = account.BaseURL
		agent.APIKey = account.APIKey
		agent.Provider = account.Provider
		agent.Model = modelName
		agent.APIType = apiType
		_ = agentRepo.Save(&agent)
	}
	return nil
}
