package client

import (
	"compress/gzip"
	"context"
	"errors"
	"fmt"
	"os"
	"os/exec"
	"path"
	"strings"
	"time"

	"github.com/1Panel-dev/1Panel/agent/app/dto"
	"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/files"
)

type Local struct {
	Type          string
	PrefixCommand []string
	Database      string
	Password      string
	ContainerName string
}

func NewLocal(command []string, dbType, containerName, password, database string) *Local {
	return &Local{Type: dbType, PrefixCommand: command, ContainerName: containerName, Password: password, Database: database}
}

func (r *Local) Create(info CreateInfo) error {
	if err := r.CreateDatabase(info); err != nil {
		return err
	}
	if len(info.Username) == 0 {
		return nil
	}
	if err := r.CreateUser(info, true); err != nil {
		_ = r.ExecSQL(fmt.Sprintf("drop database if exists `%s`", info.Name), info.Timeout)
		return err
	}

	return nil
}

func (r *Local) CreateDatabase(info CreateInfo) error {
	createSql := fmt.Sprintf("create database `%s` default character set %s collate %s", info.Name, info.Format, info.Collation)
	if len(info.Collation) == 0 {
		createSql = fmt.Sprintf("create database `%s` default character set %s", info.Name, info.Format)
	}
	if err := r.ExecSQL(createSql, info.Timeout); err != nil {
		if strings.Contains(strings.ToLower(err.Error()), "error 1007") {
			return buserr.New("ErrDatabaseIsExist")
		}
		return err
	}
	return nil
}

func (r *Local) CreateUser(info CreateInfo, withDeleteDB bool) error {
	for _, user := range userIdentities(info.Username, info.Permission) {
		if err := r.ExecSQL(createUserSQL(user, info.Password), info.Timeout); err != nil {
			if isUserExistsErr(err) {
				return buserr.New("ErrUserIsExist")
			}
			if withDeleteDB {
				_ = r.Delete(DeleteInfo{
					Name:        info.Name,
					Version:     info.Version,
					Username:    info.Username,
					Permission:  info.Permission,
					ForceDelete: true,
					Timeout:     300})
			}
			return err
		}
		if err := r.ExecSQL(createUserGrantSQL(info, user), info.Timeout); err != nil {
			if withDeleteDB {
				_ = r.Delete(DeleteInfo{
					Name:        info.Name,
					Version:     info.Version,
					Username:    info.Username,
					Permission:  info.Permission,
					ForceDelete: true,
					Timeout:     300})
			}
			return err
		}
	}
	return nil
}

func (r *Local) CreateUserOnly(info UserInfo, password string, timeout uint) error {
	if err := r.ExecSQL(createUserSQL(userIdentity(info.Username, info.Host), password), timeout); err != nil {
		if isUserExistsErr(err) {
			return buserr.New("ErrUserIsExist")
		}
		return err
	}
	return nil
}

func (r *Local) GrantUser(info GrantInfo, timeout uint) error {
	return r.ExecSQL(grantUserSQL(info), timeout)
}

func (r *Local) RevokeGrant(info GrantInfo, timeout uint) error {
	if err := r.ExecSQL(revokeGrantSQL(info), timeout); err != nil {
		return err
	}
	_ = r.ExecSQL(revokeGrantOptionSQL(info), timeout)
	return nil
}

func (r *Local) DeleteUser(info UserInfo, version string, timeout uint) error {
	return r.ExecSQL(dropUserSQL(info, version), timeout)
}

func (r *Local) UpdateUser(info UserUpdateInfo, timeout uint) error {
	if info.Host == info.NewHost {
		return nil
	}
	return r.ExecSQL(renameUserSQL(info), timeout)
}

func (r *Local) DeleteDatabase(info DeleteInfo) error {
	if len(info.Name) == 0 {
		return nil
	}
	if err := r.ExecSQL(dropDatabaseSQL(info.Name), info.Timeout); err != nil && !info.ForceDelete {
		return fmt.Errorf("drop database failed, err: %v", err)
	}
	return nil
}

func (r *Local) ListUsers(timeout uint) ([]UserInfo, error) {
	lines, err := r.ExecSQLForRows("select user,host from mysql.user order by user,host;", timeout)
	if err != nil {
		return nil, err
	}
	users := make([]UserInfo, 0)
	for _, line := range lines {
		parts := strings.Fields(line)
		if len(parts) != 2 || strings.EqualFold(parts[0], "user") {
			continue
		}
		users = append(users, UserInfo{Username: parts[0], Host: parts[1]})
	}
	return users, nil
}

func (r *Local) ListGrants(timeout uint) ([]GrantInfo, error) {
	lines, err := r.ExecSQLForRows("select db,user,host from mysql.db where db not in ('information_schema','mysql','performance_schema','sys','__recycle_bin__','recycle_bin') order by db,user,host;", timeout)
	if err != nil {
		return nil, err
	}
	grants := make([]GrantInfo, 0)
	for _, line := range lines {
		parts := strings.Fields(line)
		if len(parts) != 3 || strings.EqualFold(parts[0], "db") {
			continue
		}
		if parts[1] == "root" {
			continue
		}
		grants = append(grants, GrantInfo{Database: parts[0], Username: parts[1], Host: parts[2]})
	}
	return grants, nil
}

func (r *Local) Delete(info DeleteInfo) error {
	for _, user := range userIdentities(info.Username, info.Permission) {
		if strings.HasPrefix(info.Version, "5.6") {
			if err := r.ExecSQL(fmt.Sprintf("drop user %s", user), info.Timeout); err != nil && !info.ForceDelete {
				return fmt.Errorf("drop user failed, err: %v", err)
			}
		} else {
			if err := r.ExecSQL(fmt.Sprintf("drop user if exists %s", user), info.Timeout); err != nil && !info.ForceDelete {
				return fmt.Errorf("drop user failed, err: %v", err)
			}
		}
	}
	if len(info.Name) != 0 {
		if err := r.ExecSQL(dropDatabaseSQL(info.Name), info.Timeout); err != nil && !info.ForceDelete {
			return fmt.Errorf("drop database failed, err: %v", err)
		}
	}
	if !info.ForceDelete {
		global.LOG.Info("execute delete database sql successful, now start to drop uploads and records")
	}

	return nil
}

func (r *Local) ChangePassword(info PasswordChangeInfo) error {
	if info.Username != "root" {
		for _, user := range userIdentities(info.Username, info.Permission) {
			if err := r.ExecSQL(changeUserPasswordSQL(user, info.Password, info.Version), info.Timeout); err != nil {
				return err
			}
		}
		return nil
	}

	hosts, err := r.ExecSQLForRows("select host from mysql.user where user='root';", info.Timeout)
	if err != nil {
		return err
	}
	for _, host := range hosts {
		if host == "%" || host == "localhost" {
			passwordRootChangeCMD := fmt.Sprintf("set password for 'root'@'%s' = password('%s')", host, info.Password)
			if !strings.HasPrefix(info.Version, "5.7") && !strings.HasPrefix(info.Version, "5.6") {
				passwordRootChangeCMD = fmt.Sprintf("alter user 'root'@'%s' identified by '%s';", host, info.Password)
			}
			if err := r.ExecSQL(passwordRootChangeCMD, info.Timeout); err != nil {
				return err
			}
		}
	}

	return nil
}

func (r *Local) ChangeAccess(info AccessChangeInfo) error {
	if info.Username == "root" {
		info.OldPermission = "%"
		info.Name = "*"
		info.Password = r.Password
	}
	if info.Permission != info.OldPermission {
		if err := r.Delete(DeleteInfo{
			Version:     info.Version,
			Username:    info.Username,
			Permission:  info.OldPermission,
			ForceDelete: true,
			Timeout:     300}); err != nil {
			return err
		}
		if info.Username == "root" {
			return nil
		}
	}
	if err := r.CreateUser(CreateInfo{
		Name:       info.Name,
		Version:    info.Version,
		Username:   info.Username,
		Password:   info.Password,
		Permission: info.Permission,
		Timeout:    info.Timeout,
	}, false); err != nil {
		return err
	}
	if err := r.ExecSQL("flush privileges", 300); err != nil {
		return err
	}
	return nil
}

func (r *Local) Backup(info BackupInfo) error {
	fileOp := files.NewFileOp()
	if !fileOp.Stat(info.TargetDir) {
		if err := os.MkdirAll(info.TargetDir, os.ModePerm); err != nil {
			return fmt.Errorf("mkdir %s failed, err: %v", info.TargetDir, err)
		}
	}
	dumpCmd := "mysqldump"
	if r.Type == constant.AppMariaDB {
		dumpCmd = "mariadb-dump"
	}
	global.LOG.Infof("start to %s | gzip > %s.gzip, args: %v", dumpCmd, info.TargetDir+"/"+info.FileName, info.Args)

	info.Args = append(info.Args, "--routines")
	itemArgs := common.RemoveRepeatStr(info.Args)
	args := []string{"exec", r.ContainerName, dumpCmd, "-uroot", "-p" + r.Password, "--default-character-set=" + info.Format}
	for _, arg := range itemArgs {
		if len(arg) == 0 {
			continue
		}
		args = append(args, arg)
	}
	args = append(args, info.Name)
	cmdMgr := cmd.NewCommandMgr()
	if _, err := cmdMgr.RunPipeToFile(path.Join(info.TargetDir, info.FileName),
		cmd.PipeCommand{Name: "docker", Args: args},
		cmd.PipeCommand{Name: "gzip", Args: []string{"-cf"}},
	); err != nil {
		return fmt.Errorf("handle backup database failed, err: %v", err)
	}
	return nil
}

func (r *Local) Recover(info RecoverInfo) error {
	fi, _ := os.Open(info.SourceFile)
	defer func() { _ = fi.Close() }()
	mysqlCli := r.Type
	if mysqlCli == "mysql-cluster" {
		mysqlCli = "mysql"
	}

	cmd := exec.Command("docker", "exec", "-i", r.ContainerName, mysqlCli, "-uroot", "-p"+r.Password, "--default-character-set="+info.Format, info.Name)
	if strings.HasSuffix(info.SourceFile, ".gz") {
		gzipFile, err := os.Open(info.SourceFile)
		if err != nil {
			return err
		}
		defer func() { _ = gzipFile.Close() }()
		gzipReader, err := gzip.NewReader(gzipFile)
		if err != nil {
			return err
		}
		defer func() { _ = gzipReader.Close() }()
		cmd.Stdin = gzipReader
	} else {
		cmd.Stdin = fi
	}
	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 errors.New(stdStr)
	}

	return nil
}

func (r *Local) SyncDB(version string) ([]SyncDBInfo, error) {
	var datas []SyncDBInfo
	lines, err := r.ExecSQLForRows("SELECT SCHEMA_NAME, DEFAULT_CHARACTER_SET_NAME, DEFAULT_COLLATION_NAME FROM information_schema.SCHEMATA", 300)
	if err != nil {
		return datas, err
	}
	for _, line := range lines {
		parts := strings.Fields(line)
		if len(parts) != 3 {
			continue
		}
		if parts[0] == "SCHEMA_NAME" || parts[0] == "information_schema" || parts[0] == "mysql" || parts[0] == "performance_schema" || parts[0] == "sys" || parts[0] == "__recycle_bin__" || parts[0] == "recycle_bin" {
			continue
		}
		dataItem := SyncDBInfo{
			Name:      parts[0],
			From:      "local",
			MysqlName: r.Database,
			Format:    parts[1],
			Collation: parts[2],
		}
		userLines, err := r.ExecSQLForRows(fmt.Sprintf("select user,host from mysql.db where db = '%s'", parts[0]), 300)
		if err != nil {
			global.LOG.Debugf("sync user of db %s failed, err: %v", parts[0], err)
			dataItem.Permission = "%"
			datas = append(datas, dataItem)
			continue
		}

		var permissionItem []string
		isLocal := true
		i := 0
		for _, userline := range userLines {
			userparts := strings.Fields(userline)
			if len(userparts) != 2 {
				continue
			}
			if userparts[0] == "root" {
				continue
			}
			if i == 0 {
				dataItem.Username = userparts[0]
			}
			dataItem.Username = userparts[0]
			if dataItem.Username == userparts[0] && userparts[1] == "%" {
				isLocal = false
				dataItem.Permission = "%"
			} else if dataItem.Username == userparts[0] && userparts[1] != "localhost" {
				isLocal = false
				permissionItem = append(permissionItem, userparts[1])
			}
		}
		if len(dataItem.Username) == 0 {
			dataItem.Permission = "%"
		} else {
			if isLocal {
				dataItem.Permission = "localhost"
			}
			if len(dataItem.Permission) == 0 {
				dataItem.Permission = strings.Join(permissionItem, ",")
			}
		}
		datas = append(datas, dataItem)
	}
	return datas, nil
}

func (r *Local) Close() {}

func (r *Local) ExecSQL(command string, timeout uint) error {
	itemCommand := r.PrefixCommand[:]
	itemCommand = append(itemCommand, command)
	ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second)
	defer cancel()
	cmd := exec.CommandContext(ctx, "docker", itemCommand...)
	stdout, err := cmd.CombinedOutput()
	if errors.Is(ctx.Err(), context.DeadlineExceeded) {
		return buserr.New("ErrExecTimeOut")
	}
	stdStr := strings.ReplaceAll(string(stdout), "mysql: [Warning] Using a password on the command line interface can be insecure.\n", "")
	if err != nil || strings.HasPrefix(string(stdStr), "ERROR ") {
		return errors.New(stdStr)
	}
	return nil
}

func (r *Local) ExecSQLForRows(command string, timeout uint) ([]string, error) {
	itemCommand := r.PrefixCommand[:]
	itemCommand = append(itemCommand, command)
	ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second)
	defer cancel()
	cmd := exec.CommandContext(ctx, "docker", itemCommand...)
	stdout, err := cmd.CombinedOutput()
	if errors.Is(ctx.Err(), context.DeadlineExceeded) {
		return nil, buserr.New("ErrExecTimeOut")
	}
	stdStr := strings.ReplaceAll(string(stdout), "mysql: [Warning] Using a password on the command line interface can be insecure.\n", "")
	if err != nil || strings.HasPrefix(string(stdStr), "ERROR ") {
		return nil, errors.New(stdStr)
	}
	return strings.Split(stdStr, "\n"), nil
}

func (r *Local) LoadFormatCollation(timeout uint) ([]dto.MysqlFormatCollationOption, error) {
	std, err := r.ExecSQLForRows("SELECT CHARACTER_SET_NAME, COLLATION_NAME FROM INFORMATION_SCHEMA.COLLATIONS ORDER BY CHARACTER_SET_NAME, COLLATION_NAME;", timeout)
	if err != nil {
		return nil, err
	}
	formatMap := make(map[string][]string)
	for _, item := range std {
		if strings.ToLower(item) == "character_set_name\tcollation_name" {
			continue
		}
		parts := strings.Split(item, "\t")
		if len(parts) != 2 {
			continue
		}
		if parts[0] == "NULL" {
			continue
		}
		if _, ok := formatMap[parts[0]]; !ok {
			formatMap[parts[0]] = []string{parts[1]}
		} else {
			formatMap[parts[0]] = append(formatMap[parts[0]], parts[1])
		}
	}
	options := []dto.MysqlFormatCollationOption{}
	for key, val := range formatMap {
		options = append(options, dto.MysqlFormatCollationOption{
			Format:     key,
			Collations: val,
		})
	}
	return options, nil
}
