package client

import (
	"compress/gzip"
	"context"
	"database/sql"
	"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"
	"github.com/docker/docker/api/types/image"
	"github.com/docker/docker/client"
)

type Remote struct {
	Type     string
	Client   *sql.DB
	Database string
	User     string
	Password string
	Address  string
	Port     uint

	SSL        bool
	RootCert   string
	ClientKey  string
	ClientCert string
	SkipVerify bool
}

func NewRemote(db Remote) *Remote {
	return &db
}

func (r *Remote) 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 *Remote) 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 *Remote) 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 *Remote) 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 *Remote) GrantUser(info GrantInfo, timeout uint) error {
	return r.ExecSQL(grantUserSQL(info), timeout)
}

func (r *Remote) 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 *Remote) DeleteUser(info UserInfo, version string, timeout uint) error {
	return r.ExecSQL(dropUserSQL(info, version), timeout)
}

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

func (r *Remote) 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 *Remote) ListUsers(timeout uint) ([]UserInfo, error) {
	ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second)
	defer cancel()
	rows, err := r.Client.QueryContext(ctx, "select user,host from mysql.user order by user,host")
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	users := make([]UserInfo, 0)
	for rows.Next() {
		var user, host string
		if err := rows.Scan(&user, &host); err != nil {
			return nil, err
		}
		users = append(users, UserInfo{Username: user, Host: host})
	}
	return users, rows.Err()
}

func (r *Remote) ListGrants(timeout uint) ([]GrantInfo, error) {
	ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second)
	defer cancel()
	rows, err := r.Client.QueryContext(ctx, "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")
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	grants := make([]GrantInfo, 0)
	for rows.Next() {
		var db, user, host string
		if err := rows.Scan(&db, &user, &host); err != nil {
			return nil, err
		}
		if user == "root" {
			continue
		}
		grants = append(grants, GrantInfo{Database: db, Username: user, Host: host})
	}
	return grants, rows.Err()
}

func (r *Remote) 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 *Remote) 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.ExecSQLForHosts(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 *Remote) 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 *Remote) Backup(info BackupInfo) error {
	if cmd.CheckIllegal(r.Password, r.Address, r.User, info.Name, info.Format) {
		return buserr.New("ErrCmdIllegal")
	}
	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", dumpCmd, info.TargetDir+"/"+info.FileName)
	image, err := loadImage(info.Type, info.Version)
	if err != nil {
		return err
	}
	info.Args = append(info.Args, "--routines")
	itemArgs := common.RemoveRepeatStr(info.Args)
	var args []string
	for _, arg := range itemArgs {
		if len(arg) == 0 {
			continue
		}
		args = append(args, arg)
	}

	backupArgs := []string{"run", "--rm", "--net=host", "-i", image, dumpCmd}
	backupArgs = append(backupArgs, args...)
	backupArgs = append(
		backupArgs,
		"-h", r.Address,
		"-P", fmt.Sprintf("%d", r.Port),
		"-u"+r.User,
		"-p"+r.Password,
		sslSkip(info.Version, r.Type),
		"--default-character-set="+info.Format,
		info.Name,
	)
	debugArgs := append([]string{}, backupArgs...)
	for i, arg := range debugArgs {
		if strings.Contains(arg, r.Password) {
			debugArgs[i] = strings.ReplaceAll(arg, r.Password, "******")
		}
	}
	global.LOG.Debug("docker " + strings.Join(debugArgs, " "))
	cmdMgr := cmd.NewCommandMgr()
	if _, err := cmdMgr.RunPipeToFile(path.Join(info.TargetDir, info.FileName),
		cmd.PipeCommand{Name: "docker", Args: backupArgs},
		cmd.PipeCommand{Name: "gzip", Args: []string{"-cf"}},
	); err != nil {
		return fmt.Errorf("handle backup database failed, err: %v", err)
	}
	return nil
}

func (r *Remote) Recover(info RecoverInfo) error {
	if cmd.CheckIllegal(r.Password, r.Address, r.User, info.Name, info.Format) {
		return buserr.New("ErrCmdIllegal")
	}
	fi, _ := os.Open(info.SourceFile)
	defer func() { _ = fi.Close() }()

	image, err := loadImage(info.Type, info.Version)
	if err != nil {
		return err
	}

	recoverArgs := []string{
		"run", "--rm", "--net=host", "-i", image, r.Type,
		"-h", r.Address,
		"-P", fmt.Sprintf("%d", r.Port),
		"-u" + r.User,
		"-p" + r.Password,
		sslSkip(info.Version, r.Type),
		"--default-character-set=" + info.Format,
		info.Name,
	}
	debugArgs := append([]string{}, recoverArgs...)
	for i, arg := range debugArgs {
		if strings.Contains(arg, r.Password) {
			debugArgs[i] = strings.ReplaceAll(arg, r.Password, "******")
		}
	}
	global.LOG.Debug("docker " + strings.Join(debugArgs, " "))
	cmd := exec.Command("docker", recoverArgs...)

	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(string(stdStr), "ERROR ") {
		return errors.New(stdStr)
	}

	return nil
}

func (r *Remote) SyncDB(version string) ([]SyncDBInfo, error) {
	var datas []SyncDBInfo
	rows, err := r.Client.Query("select schema_name, default_character_set_name, default_collation_name from information_schema.SCHEMATA")
	if err != nil {
		return datas, err
	}
	defer func() { _ = rows.Close() }()

	for rows.Next() {
		var dbName, charsetName, collation string
		if err = rows.Scan(&dbName, &charsetName, &collation); err != nil {
			return datas, err
		}
		if dbName == "information_schema" || dbName == "mysql" || dbName == "performance_schema" || dbName == "sys" || dbName == "__recycle_bin__" || dbName == "recycle_bin" {
			continue
		}
		dataItem := SyncDBInfo{
			Name:      dbName,
			From:      "remote",
			MysqlName: r.Database,
			Format:    charsetName,
			Collation: collation,
		}
		userRows, err := r.Client.Query("select user,host from mysql.db where db = ?", dbName)
		if err != nil {
			global.LOG.Debugf("sync user of db %s failed, err: %v", dbName, err)
			dataItem.Permission = "%"
			datas = append(datas, dataItem)
			continue
		}

		var permissionItem []string
		isLocal := true
		i := 0
		for userRows.Next() {
			var user, host string
			if err = userRows.Scan(&user, &host); err != nil {
				return datas, err
			}
			if user == "root" {
				continue
			}
			if i == 0 {
				dataItem.Username = user
			}
			if dataItem.Username == user && host == "%" {
				isLocal = false
				dataItem.Permission = "%"
			} else if dataItem.Username == user && host != "localhost" {
				isLocal = false
				permissionItem = append(permissionItem, host)
			}
			i++
		}
		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)
	}
	if err = rows.Err(); err != nil {
		return datas, err
	}
	return datas, nil
}

func (r *Remote) Close() {
	_ = r.Client.Close()
}

func (r *Remote) ExecSQL(command string, timeout uint) error {
	ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second)
	defer cancel()

	if _, err := r.Client.ExecContext(ctx, command); err != nil {
		return err
	}
	if errors.Is(ctx.Err(), context.DeadlineExceeded) {
		return buserr.New("ErrExecTimeOut")
	}

	return nil
}

func (r *Remote) LoadFormatCollation(timeout uint) ([]dto.MysqlFormatCollationOption, error) {
	ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second)
	defer cancel()

	rows, err := r.Client.QueryContext(ctx, "SELECT CHARACTER_SET_NAME, COLLATION_NAME FROM INFORMATION_SCHEMA.COLLATIONS ORDER BY CHARACTER_SET_NAME, COLLATION_NAME;")
	if errors.Is(ctx.Err(), context.DeadlineExceeded) {
		return nil, buserr.New("ErrExecTimeOut")
	}
	if err != nil {
		return nil, err
	}
	defer func() { _ = rows.Close() }()

	formatMap := make(map[string][]string)
	for rows.Next() {
		var item FormatCollation
		if err := rows.Scan(&item.Format, &item.Collation); err != nil {
			return nil, err
		}
		if !item.Format.Valid {
			continue
		}
		if _, ok := formatMap[item.Format.String]; !ok {
			formatMap[item.Format.String] = []string{item.Collation.String}
		} else {
			formatMap[item.Format.String] = append(formatMap[item.Format.String], item.Collation.String)
		}
	}
	options := []dto.MysqlFormatCollationOption{}
	for key, val := range formatMap {
		options = append(options, dto.MysqlFormatCollationOption{
			Format:     key,
			Collations: val,
		})
	}

	return options, nil
}

func (r *Remote) ExecSQLForHosts(timeout uint) ([]string, error) {
	ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second)
	defer cancel()

	results, err := r.Client.QueryContext(ctx, "select host from mysql.user where user='root';")
	if err != nil {
		return nil, err
	}
	if errors.Is(ctx.Err(), context.DeadlineExceeded) {
		return nil, buserr.New("ErrExecTimeOut")
	}
	var rows []string
	defer func() { _ = results.Close() }()
	for results.Next() {
		var host string
		if err := results.Scan(&host); err != nil {
			continue
		}
		rows = append(rows, host)
	}
	return rows, nil
}

func loadImage(dbType, version string) (string, error) {
	cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
	if err != nil {
		return "", err
	}
	defer cli.Close()
	images, err := cli.ImageList(context.Background(), image.ListOptions{})
	if err != nil {
		return "", err
	}

	for _, image := range images {
		for _, tag := range image.RepoTags {
			if !strings.HasPrefix(tag, dbType+":") {
				continue
			}
			if dbType == "mariadb" && strings.HasPrefix(tag, "mariadb:") {
				return tag, nil
			}
			if strings.HasPrefix(version, "5.6") && strings.HasPrefix(tag, "mysql:5.6") {
				return tag, nil
			}
			if strings.HasPrefix(version, "5.7") && strings.HasPrefix(tag, "mysql:5.7") {
				return tag, nil
			}
			if strings.HasPrefix(version, "8.") && strings.HasPrefix(tag, "mysql:8.") {
				return tag, nil
			}
		}
	}
	return loadVersion(dbType, version), nil
}

func loadVersion(dbType string, version string) string {
	if dbType == "mariadb" {
		return "mariadb:11.3.2"
	}
	if strings.HasPrefix(version, "5.6") {
		return "mysql:5.6.51"
	}
	if strings.HasPrefix(version, "5.7") {
		return "mysql:5.7.44"
	}
	return "mysql:8.2.0"
}

func sslSkip(version, dbType string) string {
	if dbType == constant.AppMariaDB || strings.HasPrefix(version, "5.6") || strings.HasPrefix(version, "5.7") {
		return "--skip-ssl"
	}
	return "--ssl-mode=DISABLED"
}
