package service

import (
	"context"
	"encoding/json"
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
	"sort"
	"strconv"
	"strings"
	"time"

	"github.com/1Panel-dev/1Panel/agent/app/repo"

	"github.com/1Panel-dev/1Panel/agent/app/dto"
	"github.com/1Panel-dev/1Panel/agent/app/model"
	"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/common"
	"github.com/1Panel-dev/1Panel/agent/utils/compose"
	"github.com/1Panel-dev/1Panel/agent/utils/encrypt"
	"github.com/1Panel-dev/1Panel/agent/utils/mysql"
	"github.com/1Panel-dev/1Panel/agent/utils/mysql/client"
	"github.com/1Panel-dev/1Panel/agent/utils/re"
	_ "github.com/go-sql-driver/mysql"
	"github.com/jinzhu/copier"
	"github.com/pkg/errors"
	"gorm.io/gorm"
)

type MysqlService struct{}

type IMysqlService interface {
	SearchWithPage(search dto.MysqlDBSearch) (int64, interface{}, error)
	ListDBOption() ([]dto.MysqlOption, error)
	Create(ctx context.Context, req dto.MysqlDBCreate) (*model.DatabaseMysql, error)
	LoadFromRemote(req dto.MysqlLoadDB) error
	ChangeAccess(info dto.ChangeDBInfo) error
	ChangePassword(info dto.ChangeDBInfo) error
	UpdateVariables(req dto.MysqlVariablesUpdate) error
	UpdateDescription(req dto.UpdateDescription) error
	DeleteCheck(req dto.MysqlDBDeleteCheck) ([]dto.DBResource, error)
	Delete(ctx context.Context, req dto.MysqlDBDelete) error

	ListUsers(req dto.MysqlUserSearch) ([]dto.MysqlUser, error)
	ListGrants(req dto.MysqlUserSearch) ([]dto.MysqlGrant, error)
	ListGrantSummary(req dto.MysqlGrantSummarySearch) (map[string][]dto.MysqlUser, error)
	CreateUser(req dto.MysqlUserCreate) error
	UpdateUser(req dto.MysqlUserUpdate) error
	ChangeUserPassword(req dto.MysqlUserPassword) error
	SaveUserPassword(req dto.MysqlUserPassword) error
	DeleteUser(req dto.MysqlUserDelete) error
	GrantUser(req dto.MysqlGrantCreate) error
	RevokeGrant(req dto.MysqlGrantDelete) error

	LoadFormatOption(req dto.OperationWithName) []dto.MysqlFormatCollationOption
	LoadStatus(req dto.OperationWithNameAndType) (*dto.MysqlStatus, error)
	LoadVariables(req dto.OperationWithNameAndType) (*dto.MysqlVariables, error)
	LoadRemoteAccess(req dto.OperationWithNameAndType) (bool, error)
}

func NewIMysqlService() IMysqlService {
	return &MysqlService{}
}

func normalizeDatabaseUserType(dbType string) string {
	if len(dbType) == 0 {
		return "mysql"
	}
	return dbType
}

func resolveDatabaseUserType(database string) (string, error) {
	databaseItem, err := databaseRepo.Get(repo.WithByName(database))
	if err != nil {
		return "", err
	}
	return normalizeDatabaseUserType(databaseItem.Type), nil
}

func databaseUserKey(username, host string) string {
	return username + "@" + host
}

func splitMysqlHosts(permission string) []string {
	hosts := strings.Split(permission, ",")
	res := make([]string, 0, len(hosts))
	seen := make(map[string]struct{}, len(hosts))
	for _, host := range hosts {
		host = strings.TrimSpace(host)
		if len(host) == 0 {
			continue
		}
		if _, ok := seen[host]; ok {
			continue
		}
		seen[host] = struct{}{}
		res = append(res, host)
	}
	return res
}

func parseMysqlHosts(permission string, allowMultiple bool) ([]string, error) {
	hosts := splitMysqlHosts(permission)
	if len(hosts) == 0 {
		return nil, errors.New("mysql user host is required")
	}
	if !allowMultiple && len(hosts) > 1 {
		return nil, errors.New("multiple mysql user hosts are not supported for this operation")
	}
	return hosts, nil
}

func checkMysqlNormalUser(username string) error {
	if isMysqlSystemUser(username) {
		return errors.New("mysql system user does not support this operation")
	}
	return nil
}

// isMysqlSystemUser identifies internal accounts which must only be managed by
// MySQL or its container runtime. The root account is managed through the
// dedicated root API.
func isMysqlSystemUser(username string) bool {
	switch strings.ToLower(username) {
	case "root",
		"mysql.session", "mysql.sys", "mysql.infoschema", "mysqlxsys",
		"mariadb.sys", "mariadb-sys",
		"debian-sys-maint",
		"healthcheck":
		return true
	default:
		return false
	}
}

func saveDatabaseUserCredential(dbType, database, username, host, password, description string) error {
	dbType = normalizeDatabaseUserType(dbType)
	user, err := databaseUserRepo.Get(repo.WithByType(dbType), databaseUserRepo.WithByDatabase(database), databaseUserRepo.WithByUser(username, host))
	if err != nil {
		user = model.DatabaseUser{
			Type:     dbType,
			Database: database,
			Username: username,
			Host:     host,
		}
	}
	user.Password = password
	user.IsDelete = false
	if len(description) != 0 {
		user.Description = description
	}
	return databaseUserRepo.Save(&user)
}

func saveDatabaseUserCredentials(dbType, database, username, permission, password, description string) error {
	for _, host := range splitMysqlHosts(permission) {
		if err := saveDatabaseUserCredential(dbType, database, username, host, password, description); err != nil {
			return err
		}
	}
	return nil
}

type mysqlUserAppTarget struct {
	Key  string
	Name string
}

func loadMysqlUserAppTargets(dbType, database, username, host string, dbNames ...string) ([]mysqlUserAppTarget, error) {
	if host != "%" {
		return nil, nil
	}
	dbNameSet := make(map[string]struct{}, len(dbNames))
	for _, dbName := range dbNames {
		dbNameSet[dbName] = struct{}{}
	}
	dbItems, err := mysqlRepo.List(mysqlRepo.WithByMysqlName(database))
	if err != nil {
		return nil, err
	}
	localResourceIDs := make([]uint, 0, len(dbItems))
	remoteResourceIDs := make([]uint, 0, len(dbItems))
	for _, dbItem := range dbItems {
		if len(dbNameSet) != 0 {
			if _, ok := dbNameSet[dbItem.Name]; !ok {
				continue
			}
		}
		if dbItem.ID == 0 {
			continue
		}
		if dbItem.From == "local" {
			localResourceIDs = append(localResourceIDs, dbItem.ID)
		} else {
			remoteResourceIDs = append(remoteResourceIDs, dbItem.ID)
		}
	}

	appResources := make([]model.AppInstallResource, 0)
	if len(localResourceIDs) != 0 {
		app, err := appInstallRepo.LoadBaseInfo(dbType, database)
		if err != nil {
			return nil, err
		}
		resources, err := appInstallResourceRepo.GetBy(
			appInstallResourceRepo.WithLinkId(app.ID),
			appInstallResourceRepo.WithResourceIds(localResourceIDs),
		)
		if err != nil {
			return nil, err
		}
		appResources = append(appResources, resources...)
	}
	if len(remoteResourceIDs) != 0 {
		resources, err := appInstallResourceRepo.GetBy(
			appInstallResourceRepo.WithResourceIds(remoteResourceIDs),
			appRepo.WithKey(dbType),
		)
		if err != nil {
			return nil, err
		}
		appResources = append(appResources, resources...)
	}

	appInstallIDs := make([]uint, 0, len(appResources))
	appInstallIDSet := make(map[uint]struct{}, len(appResources))
	for _, appResource := range appResources {
		if appResource.AppInstallId == 0 {
			continue
		}
		if _, ok := appInstallIDSet[appResource.AppInstallId]; ok {
			continue
		}
		appInstallIDSet[appResource.AppInstallId] = struct{}{}
		appInstallIDs = append(appInstallIDs, appResource.AppInstallId)
	}
	if len(appInstallIDs) == 0 {
		return nil, nil
	}
	appInstalls, err := appInstallRepo.ListBy(context.Background(), repo.WithByIDs(appInstallIDs))
	if err != nil {
		return nil, err
	}
	targets := make([]mysqlUserAppTarget, 0, len(appInstalls))
	for _, appInstall := range appInstalls {
		var envMap map[string]interface{}
		if err := json.Unmarshal([]byte(appInstall.Env), &envMap); err != nil {
			return nil, err
		}
		appUsername, ok := envMap["PANEL_DB_USER"].(string)
		if !ok || appUsername != username {
			continue
		}
		targets = append(targets, mysqlUserAppTarget{Key: appInstall.App.Key, Name: appInstall.Name})
	}
	return targets, nil
}

func checkMysqlUserAppUsage(dbType, database, username, host string, dbNames ...string) error {
	targets, err := loadMysqlUserAppTargets(dbType, database, username, host, dbNames...)
	if err != nil {
		return err
	}
	if len(targets) == 0 {
		return nil
	}
	appNames := make([]string, 0, len(targets))
	for _, target := range targets {
		appNames = append(appNames, target.Name)
	}
	sort.Strings(appNames)
	return buserr.WithDetail("ErrMysqlUserUsedByApps", strings.Join(appNames, ", "), nil)
}

func updateMysqlPasswordAppTargets(targets []mysqlUserAppTarget, password string) error {
	for _, target := range targets {
		global.LOG.Infof("start to update mysql password used by app %s-%s", target.Key, target.Name)
		if err := updateInstallInfoInDB(target.Key, target.Name, "user-password", password); err != nil {
			return err
		}
	}
	return nil
}

func syncDatabaseUserMetadata(dbType, database string, users []client.UserInfo) error {
	dbType = normalizeDatabaseUserType(dbType)
	metas, err := databaseUserRepo.List(repo.WithByType(dbType), databaseUserRepo.WithByDatabase(database))
	if err != nil {
		return err
	}
	metaMap := make(map[string]model.DatabaseUser, len(metas))
	for _, item := range metas {
		metaMap[databaseUserKey(item.Username, item.Host)] = item
	}
	userMap := make(map[string]struct{}, len(users))
	for _, item := range users {
		if isMysqlSystemUser(item.Username) {
			continue
		}
		key := databaseUserKey(item.Username, item.Host)
		userMap[key] = struct{}{}
		if meta, ok := metaMap[key]; ok {
			if meta.IsDelete {
				if err := databaseUserRepo.Update(map[string]interface{}{"is_delete": false, "password": ""}, repo.WithByType(dbType), databaseUserRepo.WithByDatabase(database), databaseUserRepo.WithByUser(item.Username, item.Host)); err != nil {
					return err
				}
			}
			continue
		}
		if err := databaseUserRepo.Save(&model.DatabaseUser{
			Type:     dbType,
			Database: database,
			Username: item.Username,
			Host:     item.Host,
		}); err != nil {
			return err
		}
	}
	for _, item := range metas {
		if isMysqlSystemUser(item.Username) {
			continue
		}
		if _, ok := userMap[databaseUserKey(item.Username, item.Host)]; ok {
			continue
		}
		if item.IsDelete {
			continue
		}
		if err := databaseUserRepo.Update(map[string]interface{}{"is_delete": true, "password": ""}, repo.WithByType(dbType), databaseUserRepo.WithByDatabase(database), databaseUserRepo.WithByUser(item.Username, item.Host)); err != nil {
			return err
		}
	}
	return nil
}

func saveDatabaseUserGrant(dbType, database, dbName, username, host string) error {
	dbType = normalizeDatabaseUserType(dbType)
	grant, err := databaseUserGrantRepo.Get(
		repo.WithByType(dbType),
		databaseUserGrantRepo.WithByDatabase(database),
		databaseUserGrantRepo.WithByDBName(dbName),
		databaseUserGrantRepo.WithByUser(username, host),
	)
	if err == nil && grant.ID != 0 {
		return nil
	}
	if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
		return err
	}
	return databaseUserGrantRepo.Save(&model.DatabaseUserGrant{
		Type:     dbType,
		Database: database,
		DBName:   dbName,
		Username: username,
		Host:     host,
	})
}

func syncDatabaseUserGrants(dbType, database string, grants []client.GrantInfo) error {
	dbType = normalizeDatabaseUserType(dbType)
	items := make([]model.DatabaseUserGrant, 0, len(grants))
	grantMap := make(map[string]struct{}, len(grants))
	for _, item := range grants {
		if isMysqlSystemUser(item.Username) || item.Database == "*" {
			continue
		}
		key := item.Database + "\x00" + item.Username + "\x00" + item.Host
		if _, ok := grantMap[key]; ok {
			continue
		}
		grantMap[key] = struct{}{}
		items = append(items, model.DatabaseUserGrant{
			Type:     dbType,
			Database: database,
			DBName:   item.Database,
			Username: item.Username,
			Host:     item.Host,
		})
	}
	return databaseUserGrantRepo.Replace(dbType, database, items)
}

func (u *MysqlService) SearchWithPage(search dto.MysqlDBSearch) (int64, interface{}, error) {
	total, mysqls, err := mysqlRepo.Page(search.Page, search.PageSize,
		mysqlRepo.WithByMysqlName(search.Database),
		repo.WithByLikeName(search.Info),
		repo.WithOrderRuleBy(search.OrderBy, search.Order),
	)
	var dtoMysqls []dto.MysqlDBInfo
	for _, mysql := range mysqls {
		var item dto.MysqlDBInfo
		if err := copier.Copy(&item, &mysql); err != nil {
			return 0, nil, buserr.WithDetail("ErrStructTransform", err.Error(), nil)
		}
		item.Username = ""
		item.Password = ""
		item.Permission = ""
		dtoMysqls = append(dtoMysqls, item)
	}
	return total, dtoMysqls, err
}

func (u *MysqlService) ListDBOption() ([]dto.MysqlOption, error) {
	mysqls, err := mysqlRepo.List()
	if err != nil {
		return nil, err
	}

	databases, err := databaseRepo.GetList(databaseRepo.WithTypeList("mysql,mariadb"))
	if err != nil {
		return nil, err
	}
	var dbs []dto.MysqlOption
	for _, mysql := range mysqls {
		var item dto.MysqlOption
		if err := copier.Copy(&item, &mysql); err != nil {
			return nil, buserr.WithDetail("ErrStructTransform", err.Error(), nil)
		}
		item.Database = mysql.MysqlName
		for _, database := range databases {
			if database.Name == item.Database {
				item.Type = database.Type
			}
		}
		dbs = append(dbs, item)
	}
	return dbs, err
}

func (u *MysqlService) Create(ctx context.Context, req dto.MysqlDBCreate) (*model.DatabaseMysql, error) {
	if cmd.CheckIllegal(req.Name, req.Username, req.Password, req.Format, req.Collation, req.Permission) {
		return nil, buserr.New("ErrCmdIllegal")
	}
	if len(req.Username) != 0 && len(req.Password) == 0 {
		return nil, errors.New("password is required when creating mysql user")
	}
	permissionHosts := make([]string, 0)
	if len(req.Username) != 0 {
		var err error
		permissionHosts, err = parseMysqlHosts(req.Permission, true)
		if err != nil {
			return nil, err
		}
		req.Permission = strings.Join(permissionHosts, ",")
	}
	dbType, err := resolveDatabaseUserType(req.Database)
	if err != nil {
		return nil, err
	}

	mysql, _ := mysqlRepo.Get(repo.WithByName(req.Name), mysqlRepo.WithByMysqlName(req.Database), repo.WithByFrom(req.From))
	if mysql.ID != 0 {
		return nil, buserr.New("ErrRecordExist")
	}

	var createItem model.DatabaseMysql
	if err := copier.Copy(&createItem, &req); err != nil {
		return nil, buserr.WithDetail("ErrStructTransform", err.Error(), nil)
	}
	createItem.Username = ""
	createItem.Password = ""
	createItem.Permission = ""

	if req.From == "local" && req.Username == "root" {
		return nil, errors.New("cannot set root as user name")
	}

	cli, version, err := LoadMysqlClientByFrom(req.Database)
	if err != nil {
		return nil, err
	}
	createItem.MysqlName = req.Database
	defer cli.Close()
	if err := cli.Create(client.CreateInfo{
		Name:       req.Name,
		Format:     req.Format,
		Collation:  req.Collation,
		Username:   req.Username,
		Password:   req.Password,
		Permission: req.Permission,
		Version:    version,
		Timeout:    300,
	}); err != nil {
		return nil, err
	}
	if len(req.Username) != 0 {
		if err := saveDatabaseUserCredentials(dbType, req.Database, req.Username, req.Permission, req.Password, req.Description); err != nil {
			return nil, err
		}
		for _, host := range permissionHosts {
			if err := saveDatabaseUserGrant(dbType, req.Database, req.Name, req.Username, host); err != nil {
				return nil, err
			}
		}
	}

	global.LOG.Infof("create database %s successful!", req.Name)
	if err := mysqlRepo.Create(ctx, &createItem); err != nil {
		return nil, err
	}
	return &createItem, nil
}

func (u *MysqlService) ListUsers(req dto.MysqlUserSearch) ([]dto.MysqlUser, error) {
	dbType, err := resolveDatabaseUserType(req.Database)
	if err != nil {
		return nil, err
	}
	users, err := databaseUserRepo.List(repo.WithByType(dbType), databaseUserRepo.WithByDatabase(req.Database))
	if err != nil {
		return nil, err
	}
	res := make([]dto.MysqlUser, 0, len(users))
	for _, user := range users {
		if isMysqlSystemUser(user.Username) {
			continue
		}
		res = append(res, dto.MysqlUser{
			Username:    user.Username,
			Host:        user.Host,
			Password:    user.Password,
			Description: user.Description,
			IsDelete:    user.IsDelete,
		})
	}
	return res, nil
}

func (u *MysqlService) ListGrants(req dto.MysqlUserSearch) ([]dto.MysqlGrant, error) {
	dbType, err := resolveDatabaseUserType(req.Database)
	if err != nil {
		return nil, err
	}
	grants, err := databaseUserGrantRepo.List(repo.WithByType(dbType), databaseUserGrantRepo.WithByDatabase(req.Database))
	if err != nil {
		return nil, err
	}
	res := make([]dto.MysqlGrant, 0, len(grants))
	for _, grant := range grants {
		res = append(res, dto.MysqlGrant{Database: grant.DBName, Username: grant.Username, Host: grant.Host})
	}
	return res, nil
}

func (u *MysqlService) ListGrantSummary(req dto.MysqlGrantSummarySearch) (map[string][]dto.MysqlUser, error) {
	res := make(map[string][]dto.MysqlUser, len(req.DBs))
	dbMap := make(map[string]struct{}, len(req.DBs))
	dbNames := make([]string, 0, len(req.DBs))
	for _, item := range req.DBs {
		if item == "" {
			continue
		}
		if _, ok := dbMap[item]; ok {
			continue
		}
		dbMap[item] = struct{}{}
		dbNames = append(dbNames, item)
		res[item] = []dto.MysqlUser{}
	}
	if len(dbMap) == 0 {
		return res, nil
	}
	dbType, err := resolveDatabaseUserType(req.Database)
	if err != nil {
		return nil, err
	}
	grants, err := databaseUserGrantRepo.List(
		repo.WithByType(dbType),
		databaseUserGrantRepo.WithByDatabase(req.Database),
		databaseUserGrantRepo.WithByDBNames(dbNames),
	)
	if err != nil {
		return nil, err
	}
	userList := make([][2]string, 0, len(grants))
	userSet := make(map[string]struct{}, len(grants))
	for _, grant := range grants {
		key := databaseUserKey(grant.Username, grant.Host)
		if _, ok := userSet[key]; ok {
			continue
		}
		userSet[key] = struct{}{}
		userList = append(userList, [2]string{grant.Username, grant.Host})
	}
	userMetas, err := databaseUserRepo.List(
		repo.WithByType(dbType),
		databaseUserRepo.WithByDatabase(req.Database),
		databaseUserRepo.WithByUserList(userList),
	)
	if err != nil {
		return nil, err
	}
	metaMap := make(map[string]model.DatabaseUser, len(userMetas))
	for _, item := range userMetas {
		metaMap[databaseUserKey(item.Username, item.Host)] = item
	}

	for _, grant := range grants {
		if _, ok := dbMap[grant.DBName]; !ok {
			continue
		}
		meta, ok := metaMap[databaseUserKey(grant.Username, grant.Host)]
		if !ok || meta.IsDelete {
			continue
		}
		item := dto.MysqlUser{Username: grant.Username, Host: grant.Host, Description: meta.Description}
		res[grant.DBName] = append(res[grant.DBName], item)
	}
	return res, nil
}

func (u *MysqlService) CreateUser(req dto.MysqlUserCreate) error {
	if cmd.CheckIllegal(req.Username, req.Password, req.Host) {
		return buserr.New("ErrCmdIllegal")
	}
	dbs := make([]string, 0, len(req.DBs))
	dbSet := make(map[string]struct{}, len(req.DBs))
	for _, db := range req.DBs {
		if db == "" {
			continue
		}
		if cmd.CheckIllegal(db) {
			return buserr.New("ErrCmdIllegal")
		}
		if db == "*" {
			return errors.New("global mysql privileges must be managed outside 1Panel")
		}
		if _, ok := dbSet[db]; ok {
			continue
		}
		dbSet[db] = struct{}{}
		dbs = append(dbs, db)
	}
	if err := checkMysqlNormalUser(req.Username); err != nil {
		return err
	}
	hosts, err := parseMysqlHosts(req.Host, true)
	if err != nil {
		return err
	}
	dbType, err := resolveDatabaseUserType(req.Database)
	if err != nil {
		return err
	}
	cli, version, err := LoadMysqlClientByFrom(req.Database)
	if err != nil {
		return err
	}
	defer cli.Close()
	createdHosts := make([]string, 0, len(hosts))
	rollbackCreatedHosts := func() {
		for i := len(createdHosts) - 1; i >= 0; i-- {
			host := createdHosts[i]
			if rollbackErr := cli.DeleteUser(client.UserInfo{Username: req.Username, Host: host}, version, 300); rollbackErr != nil {
				global.LOG.Errorf("rollback mysql user %s@%s failed, err: %v", req.Username, host, rollbackErr)
			}
		}
	}
	savedHosts := make([]string, 0, len(hosts))
	rollbackSavedHosts := func() {
		for i := len(savedHosts) - 1; i >= 0; i-- {
			host := savedHosts[i]
			if rollbackErr := databaseUserRepo.Delete(
				repo.WithByType(dbType),
				databaseUserRepo.WithByDatabase(req.Database),
				databaseUserRepo.WithByUser(req.Username, host),
			); rollbackErr != nil {
				global.LOG.Errorf("rollback mysql user record %s@%s failed, err: %v", req.Username, host, rollbackErr)
			}
		}
	}
	grantedItems := make([]client.GrantInfo, 0, len(dbs)*len(hosts))
	rollbackGrantedItems := func() {
		for i := len(grantedItems) - 1; i >= 0; i-- {
			item := grantedItems[i]
			if rollbackErr := cli.RevokeGrant(item, 300); rollbackErr != nil {
				global.LOG.Errorf(
					"rollback mysql grant %s to %s@%s failed, err: %v",
					item.Database,
					item.Username,
					item.Host,
					rollbackErr,
				)
			}
			if rollbackErr := databaseUserGrantRepo.Delete(
				repo.WithByType(dbType),
				databaseUserGrantRepo.WithByDatabase(req.Database),
				databaseUserGrantRepo.WithByDBName(item.Database),
				databaseUserGrantRepo.WithByUser(item.Username, item.Host),
			); rollbackErr != nil {
				global.LOG.Errorf(
					"rollback mysql grant record %s to %s@%s failed, err: %v",
					item.Database,
					item.Username,
					item.Host,
					rollbackErr,
				)
			}
		}
	}
	rollbackAll := func() {
		rollbackGrantedItems()
		rollbackSavedHosts()
		rollbackCreatedHosts()
	}
	for _, host := range hosts {
		if err := cli.CreateUserOnly(client.UserInfo{Username: req.Username, Host: host}, req.Password, 300); err != nil {
			rollbackCreatedHosts()
			return err
		}
		createdHosts = append(createdHosts, host)
	}
	for _, host := range hosts {
		if err := saveDatabaseUserCredential(dbType, req.Database, req.Username, host, req.Password, req.Description); err != nil {
			rollbackAll()
			return err
		}
		savedHosts = append(savedHosts, host)
	}
	for _, db := range dbs {
		for _, host := range hosts {
			item := client.GrantInfo{Database: db, Username: req.Username, Host: host}
			if err := cli.GrantUser(item, 300); err != nil {
				rollbackAll()
				return err
			}
			grantedItems = append(grantedItems, item)
			if err := saveDatabaseUserGrant(dbType, req.Database, db, req.Username, host); err != nil {
				rollbackAll()
				return err
			}
		}
	}
	return nil
}

func (u *MysqlService) UpdateUser(req dto.MysqlUserUpdate) error {
	if cmd.CheckIllegal(req.Username, req.Host, req.NewHost) {
		return buserr.New("ErrCmdIllegal")
	}
	if err := checkMysqlNormalUser(req.Username); err != nil {
		return err
	}
	oldHosts, err := parseMysqlHosts(req.Host, false)
	if err != nil {
		return err
	}
	newHosts, err := parseMysqlHosts(req.NewHost, false)
	if err != nil {
		return err
	}
	dbType, err := resolveDatabaseUserType(req.Database)
	if err != nil {
		return err
	}
	req.Host = oldHosts[0]
	req.NewHost = newHosts[0]
	if req.Host != req.NewHost {
		targetUser, _ := databaseUserRepo.Get(repo.WithByType(dbType), databaseUserRepo.WithByDatabase(req.Database), databaseUserRepo.WithByUser(req.Username, req.NewHost))
		if targetUser.ID != 0 {
			return buserr.New("ErrRecordExist")
		}
		if err := checkMysqlUserAppUsage(dbType, req.Database, req.Username, req.Host); err != nil {
			return err
		}
		cli, _, err := LoadMysqlClientByFrom(req.Database)
		if err != nil {
			return err
		}
		defer cli.Close()
		if err := cli.UpdateUser(client.UserUpdateInfo{Username: req.Username, Host: req.Host, NewHost: req.NewHost}, 300); err != nil {
			return err
		}
		user, err := databaseUserRepo.Get(repo.WithByType(dbType), databaseUserRepo.WithByDatabase(req.Database), databaseUserRepo.WithByUser(req.Username, req.Host))
		if err != nil {
			user = model.DatabaseUser{
				Type:     dbType,
				Database: req.Database,
				Username: req.Username,
			}
		}
		user.Host = req.NewHost
		user.IsDelete = false
		user.Description = req.Description
		if err := databaseUserRepo.Save(&user); err != nil {
			if rollbackErr := cli.UpdateUser(client.UserUpdateInfo{Username: req.Username, Host: req.NewHost, NewHost: req.Host}, 300); rollbackErr != nil {
				global.LOG.Errorf("rollback mysql user %s host from %s to %s failed, err: %v", req.Username, req.NewHost, req.Host, rollbackErr)
			}
			return err
		}
		if err := databaseUserGrantRepo.Update(
			map[string]interface{}{"host": req.NewHost},
			repo.WithByType(dbType),
			databaseUserGrantRepo.WithByDatabase(req.Database),
			databaseUserGrantRepo.WithByUser(req.Username, req.Host),
		); err != nil {
			return err
		}
		return nil
	}
	user, err := databaseUserRepo.Get(repo.WithByType(dbType), databaseUserRepo.WithByDatabase(req.Database), databaseUserRepo.WithByUser(req.Username, req.Host))
	if err != nil {
		user = model.DatabaseUser{
			Type:     dbType,
			Database: req.Database,
			Username: req.Username,
			Host:     req.Host,
		}
	}
	user.IsDelete = false
	user.Description = req.Description
	return databaseUserRepo.Save(&user)
}

func (u *MysqlService) ChangeUserPassword(req dto.MysqlUserPassword) error {
	if cmd.CheckIllegal(req.Username, req.Host, req.Password) {
		return buserr.New("ErrCmdIllegal")
	}
	if err := checkMysqlNormalUser(req.Username); err != nil {
		return err
	}
	hosts, err := parseMysqlHosts(req.Host, false)
	if err != nil {
		return err
	}
	req.Host = hosts[0]
	dbType, err := resolveDatabaseUserType(req.Database)
	if err != nil {
		return err
	}
	cli, version, err := LoadMysqlClientByFrom(req.Database)
	if err != nil {
		return err
	}
	defer cli.Close()
	grants, err := cli.ListGrants(300)
	if err != nil {
		return err
	}
	grantDBs := make([]string, 0)
	for _, grant := range grants {
		if grant.Username == req.Username && grant.Host == req.Host {
			grantDBs = append(grantDBs, grant.Database)
		}
	}
	var appTargets []mysqlUserAppTarget
	if len(grantDBs) != 0 {
		appTargets, err = loadMysqlUserAppTargets(dbType, req.Database, req.Username, req.Host, grantDBs...)
		if err != nil {
			return err
		}
	}
	if err := cli.ChangePassword(client.PasswordChangeInfo{
		Username:   req.Username,
		Permission: req.Host,
		Password:   req.Password,
		Version:    version,
		Timeout:    300,
	}); err != nil {
		return err
	}
	if err := saveDatabaseUserCredential(dbType, req.Database, req.Username, req.Host, req.Password, ""); err != nil {
		return err
	}
	return updateMysqlPasswordAppTargets(appTargets, req.Password)
}

func (u *MysqlService) SaveUserPassword(req dto.MysqlUserPassword) error {
	if cmd.CheckIllegal(req.Username, req.Host, req.Password) {
		return buserr.New("ErrCmdIllegal")
	}
	if err := checkMysqlNormalUser(req.Username); err != nil {
		return err
	}
	hosts, err := parseMysqlHosts(req.Host, false)
	if err != nil {
		return err
	}
	req.Host = hosts[0]
	dbType, err := resolveDatabaseUserType(req.Database)
	if err != nil {
		return err
	}
	user, err := databaseUserRepo.Get(repo.WithByType(dbType), databaseUserRepo.WithByDatabase(req.Database), databaseUserRepo.WithByUser(req.Username, req.Host))
	if err != nil {
		return err
	}
	if user.IsDelete {
		return errors.New("cannot save password for a deleted mysql user")
	}
	user.Password = req.Password
	return databaseUserRepo.Save(&user)
}

func (u *MysqlService) DeleteUser(req dto.MysqlUserDelete) error {
	if cmd.CheckIllegal(req.Username, req.Host) {
		return buserr.New("ErrCmdIllegal")
	}
	if err := checkMysqlNormalUser(req.Username); err != nil {
		return err
	}
	hosts, err := parseMysqlHosts(req.Host, false)
	if err != nil {
		return err
	}
	req.Host = hosts[0]
	dbType, err := resolveDatabaseUserType(req.Database)
	if err != nil {
		return err
	}
	if err := checkMysqlUserAppUsage(dbType, req.Database, req.Username, req.Host); err != nil {
		return err
	}
	user, err := databaseUserRepo.Get(repo.WithByType(dbType), databaseUserRepo.WithByDatabase(req.Database), databaseUserRepo.WithByUser(req.Username, req.Host))
	if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
		return err
	}
	if err != nil || !user.IsDelete {
		cli, version, err := LoadMysqlClientByFrom(req.Database)
		if err != nil {
			return err
		}
		defer cli.Close()
		if err := cli.DeleteUser(client.UserInfo{Username: req.Username, Host: req.Host}, version, 300); err != nil {
			return err
		}
	}
	if err := databaseUserGrantRepo.Delete(repo.WithByType(dbType), databaseUserGrantRepo.WithByDatabase(req.Database), databaseUserGrantRepo.WithByUser(req.Username, req.Host)); err != nil {
		return err
	}
	return databaseUserRepo.Delete(repo.WithByType(dbType), databaseUserRepo.WithByDatabase(req.Database), databaseUserRepo.WithByUser(req.Username, req.Host))
}

func (u *MysqlService) GrantUser(req dto.MysqlGrantCreate) error {
	if cmd.CheckIllegal(req.DB, req.Username, req.Host) {
		return buserr.New("ErrCmdIllegal")
	}
	if req.DB == "*" {
		return errors.New("global mysql privileges must be managed outside 1Panel")
	}
	if err := checkMysqlNormalUser(req.Username); err != nil {
		return err
	}
	hosts, err := parseMysqlHosts(req.Host, true)
	if err != nil {
		return err
	}
	dbType, err := resolveDatabaseUserType(req.Database)
	if err != nil {
		return err
	}
	cli, _, err := LoadMysqlClientByFrom(req.Database)
	if err != nil {
		return err
	}
	defer cli.Close()
	for _, host := range hosts {
		if err := cli.GrantUser(client.GrantInfo{Database: req.DB, Username: req.Username, Host: host}, 300); err != nil {
			return err
		}
		if err := saveDatabaseUserGrant(dbType, req.Database, req.DB, req.Username, host); err != nil {
			return err
		}
	}
	return nil
}

func (u *MysqlService) RevokeGrant(req dto.MysqlGrantDelete) error {
	if cmd.CheckIllegal(req.DB, req.Username, req.Host) {
		return buserr.New("ErrCmdIllegal")
	}
	if req.DB == "*" {
		return errors.New("global mysql privileges must be managed outside 1Panel")
	}
	if err := checkMysqlNormalUser(req.Username); err != nil {
		return err
	}
	hosts, err := parseMysqlHosts(req.Host, false)
	if err != nil {
		return err
	}
	req.Host = hosts[0]
	dbType, err := resolveDatabaseUserType(req.Database)
	if err != nil {
		return err
	}
	if err := checkMysqlUserAppUsage(dbType, req.Database, req.Username, req.Host, req.DB); err != nil {
		return err
	}
	cli, _, err := LoadMysqlClientByFrom(req.Database)
	if err != nil {
		return err
	}
	defer cli.Close()
	if err := cli.RevokeGrant(client.GrantInfo{Database: req.DB, Username: req.Username, Host: req.Host}, 300); err != nil {
		return err
	}
	return databaseUserGrantRepo.Delete(
		repo.WithByType(dbType),
		databaseUserGrantRepo.WithByDatabase(req.Database),
		databaseUserGrantRepo.WithByDBName(req.DB),
		databaseUserGrantRepo.WithByUser(req.Username, req.Host),
	)
}

func (u *MysqlService) LoadFromRemote(req dto.MysqlLoadDB) error {
	dbType, err := resolveDatabaseUserType(req.Database)
	if err != nil {
		return err
	}
	client, version, err := LoadMysqlClientByFrom(req.Database)
	if err != nil {
		return err
	}
	defer client.Close()

	databases, err := mysqlRepo.List(mysqlRepo.WithByMysqlName(req.Database))
	if err != nil {
		return err
	}
	datas, err := client.SyncDB(version)
	if err != nil {
		return err
	}
	users, err := client.ListUsers(300)
	if err != nil {
		return err
	}
	grants, err := client.ListGrants(300)
	if err != nil {
		return err
	}
	if err := syncDatabaseUserMetadata(dbType, req.Database, users); err != nil {
		return err
	}
	if err := syncDatabaseUserGrants(dbType, req.Database, grants); err != nil {
		return err
	}
	deleteList := databases
	for _, data := range datas {
		hasOld := false
		for i := 0; i < len(databases); i++ {
			if strings.EqualFold(databases[i].Name, data.Name) && strings.EqualFold(databases[i].MysqlName, data.MysqlName) {
				hasOld = true
				if databases[i].IsDelete {
					_ = mysqlRepo.Update(databases[i].ID, map[string]interface{}{"is_delete": false})
				}
				deleteList = append(deleteList[:i], deleteList[i+1:]...)
				break
			}
		}
		if !hasOld {
			var createItem model.DatabaseMysql
			if err := copier.Copy(&createItem, &data); err != nil {
				return buserr.WithDetail("ErrStructTransform", err.Error(), nil)
			}
			createItem.Username = ""
			createItem.Password = ""
			createItem.Permission = ""
			if err := mysqlRepo.Create(context.Background(), &createItem); err != nil {
				return err
			}
		}
	}
	for _, delItem := range deleteList {
		_ = mysqlRepo.Update(delItem.ID, map[string]interface{}{"is_delete": true})
	}
	return nil
}

func (u *MysqlService) UpdateDescription(req dto.UpdateDescription) error {
	return mysqlRepo.Update(req.ID, map[string]interface{}{"description": req.Description})
}

func loadMysqlDeleteTarget(id uint) (model.DatabaseMysql, string, error) {
	db, err := mysqlRepo.Get(repo.WithByID(id))
	if err != nil {
		return db, "", err
	}
	dbType, err := resolveDatabaseUserType(db.MysqlName)
	if err != nil {
		return db, "", err
	}
	return db, dbType, nil
}

func (u *MysqlService) deleteCheck(db model.DatabaseMysql, dbType string) ([]dto.DBResource, error) {
	var res []dto.DBResource
	websites, err := websiteRepo.GetBy(
		websiteRepo.WithDBTypes([]string{constant.AppMysql, constant.AppMariaDB, constant.AppMysqlCluster}),
		websiteRepo.WithDBID(db.ID),
	)
	if err != nil {
		return res, err
	}
	for _, website := range websites {
		res = append(res, dto.DBResource{
			Type: constant.TypeWebsite,
			Name: website.PrimaryDomain,
		})
	}

	if db.From == "local" {
		app, err := appInstallRepo.LoadBaseInfo(dbType, db.MysqlName)
		if err != nil {
			return res, err
		}
		apps, err := appInstallResourceRepo.GetBy(appInstallResourceRepo.WithLinkId(app.ID), appInstallResourceRepo.WithResourceId(db.ID))
		if err != nil {
			return res, err
		}
		for _, app := range apps {
			appInstall, err := appInstallRepo.GetFirst(repo.WithByID(app.AppInstallId))
			if err != nil {
				if errors.Is(err, gorm.ErrRecordNotFound) {
					continue
				}
				return res, err
			}
			if appInstall.ID != 0 {
				res = append(res, dto.DBResource{
					Type: constant.TypeApp,
					Name: appInstall.Name,
				})
			}
		}
	} else {
		apps, err := appInstallResourceRepo.GetBy(appInstallResourceRepo.WithResourceId(db.ID), appRepo.WithKey(dbType))
		if err != nil {
			return res, err
		}
		for _, app := range apps {
			appInstall, err := appInstallRepo.GetFirst(repo.WithByID(app.AppInstallId))
			if err != nil {
				if errors.Is(err, gorm.ErrRecordNotFound) {
					continue
				}
				return res, err
			}
			if appInstall.ID != 0 {
				res = append(res, dto.DBResource{
					Type: constant.TypeApp,
					Name: appInstall.Name,
				})
			}
		}
	}

	return res, nil
}

func (u *MysqlService) DeleteCheck(req dto.MysqlDBDeleteCheck) ([]dto.DBResource, error) {
	db, dbType, err := loadMysqlDeleteTarget(req.ID)
	if err != nil {
		return nil, err
	}
	return u.deleteCheck(db, dbType)
}

func (u *MysqlService) Delete(ctx context.Context, req dto.MysqlDBDelete) error {
	return u.delete(ctx, req, nil)
}

func (u *MysqlService) delete(ctx context.Context, req dto.MysqlDBDelete, exclusions []dto.DBResource) error {
	db, dbType, err := loadMysqlDeleteTarget(req.ID)
	if err != nil {
		return err
	}
	resources, err := u.deleteCheck(db, dbType)
	if err != nil {
		return err
	}
	if len(exclusions) != 0 {
		exclusionMap := make(map[string]struct{}, len(exclusions))
		for _, exclusion := range exclusions {
			exclusionMap[exclusion.Type+"\x00"+exclusion.Name] = struct{}{}
		}
		filtered := resources[:0]
		for _, resource := range resources {
			if _, ok := exclusionMap[resource.Type+"\x00"+resource.Name]; ok {
				continue
			}
			filtered = append(filtered, resource)
		}
		resources = filtered
	}
	if len(resources) != 0 {
		names := make([]string, 0, len(resources))
		for _, resource := range resources {
			names = append(names, resource.Name)
		}
		sort.Strings(names)
		return buserr.WithDetail("ErrInUsed", strings.Join(names, ", "), nil)
	}
	cli, version, err := LoadMysqlClientByFrom(db.MysqlName)
	if err != nil {
		return err
	}
	defer cli.Close()
	if err := cli.DeleteDatabase(client.DeleteInfo{
		Name:    db.Name,
		Version: version,
		Timeout: 300,
	}); err != nil && !req.ForceDelete {
		return err
	}

	if req.DeleteBackup {
		uploadDir := filepath.Join(global.Dir.DataDir, fmt.Sprintf("uploads/database/%s/%s/%s", dbType, db.MysqlName, db.Name))
		if _, err := os.Stat(uploadDir); err == nil {
			_ = os.RemoveAll(uploadDir)
		}
		backupDir := filepath.Join(global.Dir.LocalBackupDir, fmt.Sprintf("database/%s/%s/%s", dbType, db.MysqlName, db.Name))
		if _, err := os.Stat(backupDir); err == nil {
			_ = os.RemoveAll(backupDir)
		}
		_ = backupRepo.DeleteRecord(ctx, repo.WithByType(dbType), repo.WithByName(db.MysqlName), repo.WithByDetailName(db.Name))
		global.LOG.Infof("delete database %s-%s backups successful", db.MysqlName, db.Name)
	}

	_ = mysqlRepo.Delete(ctx, repo.WithByID(db.ID))
	return nil
}

func deleteMysqlDatabaseForResourceOwner(ctx context.Context, req dto.MysqlDBDelete, owner dto.DBResource) error {
	return (&MysqlService{}).delete(ctx, req, []dto.DBResource{owner})
}

func isMysqlDatabaseResourceInUseError(err error) bool {
	businessErr, ok := err.(buserr.BusinessError)
	return ok && businessErr.Msg == "ErrInUsed"
}

func (u *MysqlService) ChangePassword(req dto.ChangeDBInfo) error {
	if cmd.CheckIllegal(req.Value) {
		return buserr.New("ErrCmdIllegal")
	}
	cli, version, err := LoadMysqlClientByFrom(req.Database)
	if err != nil {
		return err
	}
	defer cli.Close()
	if req.ID != 0 {
		return errors.New("mysql user password should be changed by user api")
	}
	passwordInfo := client.PasswordChangeInfo{
		Username: "root",
		Password: req.Value,
		Timeout:  300,
		Version:  version,
	}
	if err := cli.ChangePassword(passwordInfo); err != nil {
		return err
	}

	if err := updateInstallInfoInDB(req.Type, req.Database, "password", req.Value); err != nil {
		return err
	}
	if req.From == "local" {
		remote, err := databaseRepo.Get(repo.WithByName(req.Database))
		if err != nil {
			return err
		}
		pass, err := encrypt.StringEncrypt(req.Value)
		if err != nil {
			return fmt.Errorf("decrypt database password failed, err: %v", err)
		}
		_ = databaseRepo.Update(remote.ID, map[string]interface{}{"password": pass})
	}
	return nil
}

func (u *MysqlService) ChangeAccess(req dto.ChangeDBInfo) error {
	if cmd.CheckIllegal(req.Value) {
		return buserr.New("ErrCmdIllegal")
	}
	cli, version, err := LoadMysqlClientByFrom(req.Database)
	if err != nil {
		return err
	}
	defer cli.Close()
	if req.ID != 0 {
		return errors.New("mysql user access should be changed by user api")
	}
	accessInfo := client.AccessChangeInfo{
		Username:   "root",
		Permission: req.Value,
		Timeout:    300,
		Version:    version,
	}
	if err := cli.ChangeAccess(accessInfo); err != nil {
		return err
	}

	return nil
}

func (u *MysqlService) UpdateVariables(req dto.MysqlVariablesUpdate) error {
	app, err := appInstallRepo.LoadBaseInfo(req.Type, req.Database)
	if err != nil {
		return err
	}
	var files []string

	path := fmt.Sprintf("%s/%s/%s/conf/my.cnf", global.Dir.AppInstallDir, req.Type, app.Name)
	lineBytes, err := os.ReadFile(path)
	if err != nil {
		return err
	}
	files = strings.Split(string(lineBytes), "\n")

	group := "[mysqld]"
	for _, info := range req.Variables {
		if info.Param == "slow_query_log" && info.Value == "ON" {
			logFilePath := filepath.Join(global.Dir.DataDir, fmt.Sprintf("apps/%s/%s/data/1Panel-slow.log", app.Key, app.Name))
			if req.Type == "mariadb" {
				logFilePath = filepath.Join(global.Dir.DataDir, fmt.Sprintf("apps/%s/%s/db/data/1Panel-slow.log", app.Key, app.Name))
			}
			file, err := os.OpenFile(logFilePath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
			if err != nil {
				return err
			}
			defer file.Close()
		}
		if !strings.HasPrefix(app.Version, "5.7") && !strings.HasPrefix(app.Version, "5.6") {
			if info.Param == "query_cache_size" {
				continue
			}
		}

		if _, ok := info.Value.(float64); ok {
			files = updateMyCnf(files, group, info.Param, common.LoadSizeUnit(info.Value.(float64)))
		} else {
			files = updateMyCnf(files, group, info.Param, info.Value)
		}
	}
	file, err := os.OpenFile(path, os.O_WRONLY|os.O_TRUNC, constant.FilePerm)
	if err != nil {
		return err
	}
	defer file.Close()
	_, err = file.WriteString(strings.Join(files, "\n"))
	if err != nil {
		return err
	}

	if _, err := compose.Restart(fmt.Sprintf("%s/%s/%s/docker-compose.yml", global.Dir.AppInstallDir, req.Type, app.Name)); err != nil {
		return err
	}

	return nil
}

func (u *MysqlService) LoadRemoteAccess(req dto.OperationWithNameAndType) (bool, error) {
	app, err := appInstallRepo.LoadBaseInfo(req.Type, req.Name)
	if err != nil {
		return false, err
	}
	hosts, err := executeSqlForRows(app.ContainerName, app.Key, app.Password, "select host from mysql.user where user='root';")
	if err != nil {
		return false, err
	}
	for _, host := range hosts {
		if host == "%" {
			return true, nil
		}
	}

	return false, nil
}

func (u *MysqlService) LoadVariables(req dto.OperationWithNameAndType) (*dto.MysqlVariables, error) {
	app, err := appInstallRepo.LoadBaseInfo(req.Type, req.Name)
	if err != nil {
		return nil, err
	}
	variableMap, err := executeSqlForMaps(app.ContainerName, app.Key, app.Password, "show global variables;")
	if err != nil {
		return nil, err
	}
	var info dto.MysqlVariables
	arr, err := json.Marshal(variableMap)
	if err != nil {
		return nil, err
	}
	_ = json.Unmarshal(arr, &info)
	return &info, nil
}

func (u *MysqlService) LoadStatus(req dto.OperationWithNameAndType) (*dto.MysqlStatus, error) {
	app, err := appInstallRepo.LoadBaseInfo(req.Type, req.Name)
	if err != nil {
		return nil, err
	}

	statusMap, err := executeSqlForMaps(app.ContainerName, app.Key, app.Password, "show global status;")
	if err != nil {
		return nil, err
	}

	var info dto.MysqlStatus
	arr, err := json.Marshal(statusMap)
	if err != nil {
		return nil, err
	}
	_ = json.Unmarshal(arr, &info)

	if value, ok := statusMap["Run"]; ok {
		uptime, _ := strconv.Atoi(value)
		info.Run = time.Unix(time.Now().Unix()-int64(uptime), 0).Format(constant.DateTimeLayout)
	} else {
		if value, ok := statusMap["Uptime"]; ok {
			uptime, _ := strconv.Atoi(value)
			info.Run = time.Unix(time.Now().Unix()-int64(uptime), 0).Format(constant.DateTimeLayout)
		}
	}

	info.File = "OFF"
	info.Position = "OFF"
	masterStatus := "show master status;"
	if common.CompareAppVersion(app.Version, "8.4.0") && (req.Type == constant.AppMysql || req.Type == constant.AppMysqlCluster) {
		masterStatus = "show binary log status;"
	}
	rows, err := executeSqlForRows(app.ContainerName, app.Key, app.Password, masterStatus)
	if err != nil {
		return nil, err
	}
	if len(rows) > 2 {
		itemValue := strings.Split(rows[1], "\t")
		if len(itemValue) > 2 {
			info.File = itemValue[0]
			info.Position = itemValue[1]
		}
	}

	return &info, nil
}

func (u *MysqlService) LoadFormatOption(req dto.OperationWithName) []dto.MysqlFormatCollationOption {
	defaultList := []dto.MysqlFormatCollationOption{{Format: "utf8mb4"}, {Format: "utf8mb3"}, {Format: "gbk"}, {Format: "big5"}}
	client, _, err := LoadMysqlClientByFrom(req.Name)
	if err != nil {
		return defaultList
	}
	options, err := client.LoadFormatCollation(3)
	if err != nil {
		return defaultList
	}
	return options
}

func executeSqlForMaps(containerName, dbType, password, command string) (map[string]string, error) {
	if dbType == "mysql-cluster" {
		dbType = "mysql"
	}
	cmd := exec.Command("docker", "exec", containerName, dbType, "-uroot", "-p"+password, "-e", command)
	stdout, err := cmd.CombinedOutput()
	stdStr := strings.ReplaceAll(string(stdout), "mysql: [Warning] Using a password on the command line interface can be insecure.\n", "")
	if err != nil || strings.HasPrefix(stdStr, "ERROR ") {
		return nil, errors.New(stdStr)
	}

	rows := strings.Split(stdStr, "\n")
	rowMap := make(map[string]string)
	for _, v := range rows {
		itemRow := strings.Split(v, "\t")
		if len(itemRow) == 2 {
			rowMap[itemRow[0]] = itemRow[1]
		}
	}
	return rowMap, nil
}

func executeSqlForRows(containerName, dbType, password, command string) ([]string, error) {
	if dbType == "mysql-cluster" {
		dbType = "mysql"
	}
	cmd := exec.Command("docker", "exec", containerName, dbType, "-uroot", "-p"+password, "-e", command)
	stdout, err := cmd.CombinedOutput()
	stdStr := strings.ReplaceAll(string(stdout), "mysql: [Warning] Using a password on the command line interface can be insecure.\n", "")
	if err != nil || strings.HasPrefix(stdStr, "ERROR ") {
		return nil, errors.New(stdStr)
	}
	return strings.Split(stdStr, "\n"), nil
}

func updateMyCnf(oldFiles []string, group string, param string, value interface{}) []string {
	isOn := false
	hasGroup := false
	hasKey := false
	regItem := re.GetRegex(re.MysqlGroupPattern)
	var newFiles []string
	i := 0
	for _, line := range oldFiles {
		i++
		if strings.HasPrefix(line, group) {
			isOn = true
			hasGroup = true
			newFiles = append(newFiles, line)
			continue
		}
		if !isOn {
			newFiles = append(newFiles, line)
			continue
		}
		if strings.HasPrefix(line, param+"=") || strings.HasPrefix(line, "# "+param+"=") {
			newFiles = append(newFiles, fmt.Sprintf("%s=%v", param, value))
			hasKey = true
			continue
		}
		if regItem.Match([]byte(line)) || i == len(oldFiles) {
			isOn = false
			if !hasKey {
				newFiles = append(newFiles, fmt.Sprintf("%s=%v", param, value))
			}
			newFiles = append(newFiles, line)
			continue
		}
		newFiles = append(newFiles, line)
	}
	if !hasGroup {
		newFiles = append(newFiles, group+"\n")
		newFiles = append(newFiles, fmt.Sprintf("%s=%v\n", param, value))
	}
	return newFiles
}

func LoadMysqlClientByFrom(database string) (mysql.MysqlClient, string, error) {
	var (
		dbInfo  client.DBInfo
		version string
		err     error
	)

	dbInfo.Timeout = 300
	databaseItem, err := databaseRepo.Get(repo.WithByName(database))
	if err != nil {
		return nil, "", err
	}
	dbInfo.Type = databaseItem.Type
	dbInfo.From = databaseItem.From
	dbInfo.Database = database
	if dbInfo.From != "local" {
		dbInfo.Address = databaseItem.Address
		dbInfo.Port = databaseItem.Port
		dbInfo.Username = databaseItem.Username
		dbInfo.Password = databaseItem.Password
		dbInfo.SSL = databaseItem.SSL
		dbInfo.ClientKey = databaseItem.ClientKey
		dbInfo.ClientCert = databaseItem.ClientCert
		dbInfo.RootCert = databaseItem.RootCert
		dbInfo.SkipVerify = databaseItem.SkipVerify
		version = databaseItem.Version

	} else {
		app, err := appInstallRepo.LoadBaseInfo(databaseItem.Type, database)
		if err != nil {
			return nil, "", err
		}
		dbInfo.Address = app.ContainerName
		dbInfo.Username = "root"
		dbInfo.Password = app.Password
		version = app.Version
	}

	cli, err := mysql.NewMysqlClient(dbInfo)
	if err != nil {
		return nil, "", err
	}
	return cli, version, nil
}
