package service

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"os"
	"os/exec"
	"strings"

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

	"github.com/1Panel-dev/1Panel/agent/app/dto"
	"github.com/1Panel-dev/1Panel/agent/constant"
	"github.com/1Panel-dev/1Panel/agent/utils/compose"
	"github.com/1Panel-dev/1Panel/agent/utils/docker"
	"github.com/1Panel-dev/1Panel/agent/utils/encrypt"
	"github.com/docker/docker/api/types/container"
	_ "github.com/go-sql-driver/mysql"
)

type RedisService struct{}

type IRedisService interface {
	UpdateConf(req dto.RedisConfUpdate) error
	UpdatePersistenceConf(req dto.RedisConfPersistenceUpdate) error
	ChangePassword(info dto.ChangeRedisPass) error

	LoadStatus(req dto.LoadRedisStatus) (*dto.RedisStatus, error)
	LoadConf(req dto.LoadRedisStatus) (*dto.RedisConf, error)
	LoadPersistenceConf(req dto.LoadRedisStatus) (*dto.RedisPersistence, error)

	CheckHasCli() bool
	InstallCli() error
}

func NewIRedisService() IRedisService {
	return &RedisService{}
}

func (u *RedisService) UpdateConf(req dto.RedisConfUpdate) error {
	redisInfo, err := appInstallRepo.LoadBaseInfo(req.DBType, req.Database)
	if err != nil {
		return err
	}

	var confs []redisConfig
	confs = append(confs, redisConfig{key: "timeout", value: req.Timeout})
	confs = append(confs, redisConfig{key: "maxclients", value: req.Maxclients})
	confs = append(confs, redisConfig{key: "maxmemory", value: req.Maxmemory})
	if err := confSet(redisInfo.Name, req.DBType, "", confs); err != nil {
		return err
	}
	if _, err := compose.Restart(fmt.Sprintf("%s/%s/%s/docker-compose.yml", global.Dir.AppInstallDir, req.DBType, redisInfo.Name)); err != nil {
		return err
	}

	return nil
}

func (u *RedisService) CheckHasCli() bool {
	client, err := docker.NewDockerClient()
	if err != nil {
		return false
	}
	defer client.Close()
	containerLists, err := client.ContainerList(context.Background(), container.ListOptions{})
	if err != nil {
		return false
	}
	for _, item := range containerLists {
		if strings.ReplaceAll(item.Names[0], "/", "") == "1Panel-redis-cli-tools" {
			return true
		}
	}
	return false
}

func (u *RedisService) InstallCli() error {
	item := dto.ContainerOperate{
		Name:    "1Panel-redis-cli-tools",
		Image:   "redis:7.4.4",
		Network: "1panel-network",
	}
	return NewIContainerService().ContainerCreate(item, false)
}

func (u *RedisService) ChangePassword(req dto.ChangeRedisPass) error {
	if err := updateInstallInfoInDB("redis", req.Database, "password", req.Value); err != nil {
		return err
	}
	remote, err := databaseRepo.Get(repo.WithByName(req.Database))
	if err != nil {
		return err
	}
	if remote.From == "local" {
		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 *RedisService) UpdatePersistenceConf(req dto.RedisConfPersistenceUpdate) error {
	redisInfo, err := appInstallRepo.LoadBaseInfo(req.DBType, req.Database)
	if err != nil {
		return err
	}

	var confs []redisConfig
	if req.Type == "rbd" {
		confs = append(confs, redisConfig{key: "save", value: req.Save})
	} else {
		confs = append(confs, redisConfig{key: "appendonly", value: req.Appendonly})
		confs = append(confs, redisConfig{key: "appendfsync", value: req.Appendfsync})
	}
	if err := confSet(redisInfo.Name, req.DBType, req.Type, confs); err != nil {
		return err
	}
	if _, err := compose.Restart(fmt.Sprintf("%s/%s/%s/docker-compose.yml", global.Dir.AppInstallDir, req.DBType, redisInfo.Name)); err != nil {
		return err
	}

	return nil
}

func (u *RedisService) LoadStatus(req dto.LoadRedisStatus) (*dto.RedisStatus, error) {
	redisInfo, err := appInstallRepo.LoadBaseInfo(req.Type, req.Name)
	if err != nil {
		return nil, err
	}
	commands := append(redisExec(redisInfo.ContainerName, redisInfo.Password), "info")
	cmd := exec.Command("docker", commands...)
	stdout, err := cmd.CombinedOutput()
	if err != nil {
		return nil, errors.New(string(stdout))
	}
	rows := strings.Split(string(stdout), "\r\n")
	rowMap := make(map[string]string)
	for _, v := range rows {
		itemRow := strings.Split(v, ":")
		if len(itemRow) == 2 {
			rowMap[itemRow[0]] = itemRow[1]
		}
	}
	var info dto.RedisStatus
	arr, err := json.Marshal(rowMap)
	if err != nil {
		return nil, err
	}
	_ = json.Unmarshal(arr, &info)
	return &info, nil
}

func (u *RedisService) LoadConf(req dto.LoadRedisStatus) (*dto.RedisConf, error) {
	redisInfo, err := appInstallRepo.LoadBaseInfo(req.Type, req.Name)
	if err != nil {
		return nil, err
	}

	var item dto.RedisConf
	item.ContainerName = redisInfo.ContainerName
	item.Name = redisInfo.Name
	item.Port = redisInfo.Port
	item.Requirepass = redisInfo.Password
	item.Timeout, _ = configGetStr(redisInfo.ContainerName, redisInfo.Password, "timeout")
	item.Maxclients, _ = configGetStr(redisInfo.ContainerName, redisInfo.Password, "maxclients")
	item.Maxmemory, _ = configGetStr(redisInfo.ContainerName, redisInfo.Password, "maxmemory")
	return &item, nil
}

func (u *RedisService) LoadPersistenceConf(req dto.LoadRedisStatus) (*dto.RedisPersistence, error) {
	redisInfo, err := appInstallRepo.LoadBaseInfo(req.Type, req.Name)
	if err != nil {
		return nil, err
	}
	var item dto.RedisPersistence
	if item.Appendonly, err = configGetStr(redisInfo.ContainerName, redisInfo.Password, "appendonly"); err != nil {
		return nil, err
	}
	if item.Appendfsync, err = configGetStr(redisInfo.ContainerName, redisInfo.Password, "appendfsync"); err != nil {
		return nil, err
	}
	if item.Save, err = configGetStr(redisInfo.ContainerName, redisInfo.Password, "save"); err != nil {
		return nil, err
	}
	return &item, nil
}

func configGetStr(containerName, password, param string) (string, error) {
	commands := append(redisExec(containerName, password), []string{"config", "get", param}...)
	cmd := exec.Command("docker", commands...)
	stdout, err := cmd.CombinedOutput()
	if err != nil {
		return "", errors.New(string(stdout))
	}
	rows := strings.Split(string(stdout), "\r\n")
	for _, v := range rows {
		itemRow := strings.Split(v, "\n")
		if len(itemRow) == 3 {
			return itemRow[1], nil
		}
	}
	return "", nil
}

type redisConfig struct {
	key   string
	value string
}

func confSet(redisName string, redisType string, updateType string, changeConf []redisConfig) error {
	path := fmt.Sprintf("%s/%s/%s/conf/redis.conf", global.Dir.AppInstallDir, redisType, redisName)
	lineBytes, err := os.ReadFile(path)
	if err != nil {
		return err
	}
	files := strings.Split(string(lineBytes), "\n")

	startIndex, endIndex, emptyLine := 0, 0, 0
	var newFiles []string
	for i := 0; i < len(files); i++ {
		if files[i] == "# Redis configuration rewrite by 1Panel" {
			startIndex = i
			newFiles = append(newFiles, files[i])
			continue
		}
		if files[i] == "# End Redis configuration rewrite by 1Panel" {
			endIndex = i
			break
		}
		if startIndex == 0 && strings.HasPrefix(files[i], "save ") {
			newFiles = append(newFiles, "#   "+files[i])
			continue
		}
		if startIndex != 0 && endIndex == 0 && (len(files[i]) == 0 || (updateType == "rbd" && strings.HasPrefix(files[i], "save "))) {
			emptyLine++
			continue
		}
		newFiles = append(newFiles, files[i])
	}
	if startIndex == 0 {
		newFiles = append(newFiles, "# Redis configuration rewrite by 1Panel")
		startIndex = len(newFiles) - 1
	}
	endIndex = endIndex - emptyLine
	for _, item := range changeConf {
		if item.key == "save" {
			saveVal := strings.Split(item.value, ",")
			for i := 0; i < len(saveVal); i++ {
				newFiles = append(newFiles, "save "+saveVal[i])
			}
			continue
		}

		isExist := false
		for i := startIndex; i < endIndex; i++ {
			if strings.HasPrefix(newFiles[i], item.key) || strings.HasPrefix(newFiles[i], "# "+item.key) {
				if item.value == "0" || len(item.value) == 0 {
					newFiles[i] = fmt.Sprintf("# %s %s", item.key, item.value)
				} else {
					newFiles[i] = fmt.Sprintf("%s %s", item.key, item.value)
				}
				isExist = true
				break
			}
		}
		if !isExist {
			if item.value == "0" || len(item.value) == 0 {
				newFiles = append(newFiles, fmt.Sprintf("# %s %s", item.key, item.value))
			} else {
				newFiles = append(newFiles, fmt.Sprintf("%s %s", item.key, item.value))
			}
		}
	}
	newFiles = append(newFiles, "# End Redis configuration rewrite by 1Panel")

	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(newFiles, "\n"))
	if err != nil {
		return err
	}
	return nil
}

func redisExec(containerName, password string) []string {
	cmds := []string{"exec", containerName, "redis-cli", "-a", password, "--no-auth-warning"}
	if len(password) == 0 {
		cmds = []string{"exec", containerName, "redis-cli"}
	}
	return cmds
}
