package service

import (
	"context"
	"encoding/json"
	"fmt"
	"io/ioutil"
	"math"
	"net/http"
	"os"
	"path"
	"reflect"
	"strconv"
	"strings"
	"time"

	"github.com/1Panel-dev/1Panel/backend/app/dto"
	"github.com/1Panel-dev/1Panel/backend/app/model"
	"github.com/1Panel-dev/1Panel/backend/constant"
	"github.com/1Panel-dev/1Panel/backend/global"
	"github.com/1Panel-dev/1Panel/backend/utils/cmd"
	"github.com/1Panel-dev/1Panel/backend/utils/common"
	"github.com/1Panel-dev/1Panel/backend/utils/compose"
	"github.com/1Panel-dev/1Panel/backend/utils/files"
	"github.com/joho/godotenv"
	"github.com/pkg/errors"
	"gopkg.in/yaml.v3"
)

type DatabaseOp string

var (
	Add    DatabaseOp = "add"
	Delete DatabaseOp = "delete"
)

func execDockerCommand(database model.AppDatabase, dbInstall model.AppInstall, op DatabaseOp) error {
	var auth dto.AuthParam
	var dbConfig dto.AppDatabase
	dbConfig.Password = database.Password
	dbConfig.DbUser = database.Username
	dbConfig.DbName = database.Dbname
	_ = json.Unmarshal([]byte(dbInstall.Param), &auth)
	execConfig := dto.ContainerExec{
		ContainerName: dbInstall.ContainerName,
		Auth:          auth,
		DbParam:       dbConfig,
	}
	_, err := cmd.Exec(getSqlStr(database.Key, dbInstall.Version, op, execConfig))
	if err != nil {
		return err
	}
	return nil
}

func getSqlStr(key, version string, operate DatabaseOp, exec dto.ContainerExec) string {
	var str string
	param := exec.DbParam
	switch key {
	case "mysql":
		if operate == Add {
			if common.CompareVersion(version, "8.0") {
				str = fmt.Sprintf("docker exec -i  %s  mysql -uroot -p%s  -e \"CREATE USER '%s'@'%%' IDENTIFIED BY '%s';\" -e \"create database %s;\" -e \"GRANT ALL ON %s.* TO '%s'@'%%';\" -e \"FLUSH PRIVILEGES;\"",
					exec.ContainerName, exec.Auth.RootPassword, param.DbUser, param.Password, param.DbName, param.DbName, param.DbUser)
			} else {
				str = fmt.Sprintf("docker exec -i  %s  mysql -uroot -p%s  -e \"CREATE USER '%s'@'%%' IDENTIFIED BY '%s';\" -e \"create database %s;\" -e \"GRANT ALL ON %s.* TO '%s'@'%%' IDENTIFIED BY '%s';\" -e \"FLUSH PRIVILEGES;\"",
					exec.ContainerName, exec.Auth.RootPassword, param.DbUser, param.Password, param.DbName, param.DbName, param.DbUser, param.Password)
			}
		}
		if operate == Delete {
			str = fmt.Sprintf("docker exec -i  %s  mysql -uroot -p%s   -e \"drop database %s;\"  -e \"drop user %s;\" ",
				exec.ContainerName, exec.Auth.RootPassword, param.DbName, param.DbUser)
		}
	}
	return str
}

func checkPort(key string, params map[string]interface{}) (int, error) {

	port, ok := params[key]
	if ok {
		portN := int(math.Ceil(port.(float64)))
		if common.ScanPort(portN) {
			return portN, errors.New("port is in used")
		} else {
			return portN, nil
		}
	}
	return 0, nil
}

func createLink(ctx context.Context, app model.App, appInstall *model.AppInstall, params map[string]interface{}) error {
	var dbConfig dto.AppDatabase
	if app.Type == "runtime" {
		var authParam dto.AuthParam
		paramByte, err := json.Marshal(params)
		if err != nil {
			return err
		}
		if err := json.Unmarshal(paramByte, &authParam); err != nil {
			return err
		}
		authByte, err := json.Marshal(authParam)
		if err != nil {
			return err
		}
		appInstall.Param = string(authByte)
	}
	if app.Type == "website" {
		paramByte, err := json.Marshal(params)
		if err != nil {
			return err
		}
		if err := json.Unmarshal(paramByte, &dbConfig); err != nil {
			return err
		}
	}

	if !reflect.DeepEqual(dbConfig, dto.AppDatabase{}) {
		dbInstall, err := appInstallRepo.GetFirst(appInstallRepo.WithServiceName(dbConfig.ServiceName))
		if err != nil {
			return err
		}
		var database model.AppDatabase
		database.Dbname = dbConfig.DbName
		database.Username = dbConfig.DbUser
		database.Password = dbConfig.Password
		database.AppInstallId = dbInstall.ID
		database.Key = dbInstall.App.Key
		if err := dataBaseRepo.Create(ctx, &database); err != nil {
			return err
		}
		var installResource model.AppInstallResource
		installResource.ResourceId = database.ID
		installResource.AppInstallId = appInstall.ID
		installResource.LinkId = dbInstall.ID
		installResource.Key = dbInstall.App.Key
		if err := appInstallResourceRepo.Create(ctx, &installResource); err != nil {
			return err
		}
		if err := execDockerCommand(database, dbInstall, Add); err != nil {
			return err
		}
	}

	return nil
}

func deleteAppInstall(install model.AppInstall) error {
	op := files.NewFileOp()
	appDir := install.GetPath()
	dir, _ := os.Stat(appDir)
	if dir != nil {
		out, err := compose.Down(install.GetComposePath())
		if err != nil {
			return handleErr(install, err, out)
		}
		if err := op.DeleteDir(appDir); err != nil {
			return err
		}
	}

	tx, ctx := getTxAndContext()
	if err := appInstallRepo.Delete(ctx, install); err != nil {
		tx.Rollback()
		return err
	}
	if err := deleteLink(ctx, &install); err != nil {
		tx.Rollback()
		return err
	}
	if err := appInstallBackupRepo.Delete(ctx, appInstallBackupRepo.WithAppInstallID(install.ID)); err != nil {
		tx.Rollback()
		return err
	}
	tx.Commit()
	return nil
}

func deleteLink(ctx context.Context, install *model.AppInstall) error {
	resources, _ := appInstallResourceRepo.GetBy(appInstallResourceRepo.WithAppInstallId(install.ID))
	if len(resources) == 0 {
		return nil
	}
	for _, re := range resources {
		if re.Key == "mysql" {
			database, _ := dataBaseRepo.GetFirst(commonRepo.WithByID(re.ResourceId))
			if reflect.DeepEqual(database, model.AppDatabase{}) {
				continue
			}
			appInstall, err := appInstallRepo.GetFirst(commonRepo.WithByID(database.AppInstallId))
			if err != nil {
				return nil
			}
			if err := execDockerCommand(database, appInstall, Delete); err != nil {
				return err
			}
			if err := dataBaseRepo.DeleteBy(ctx, commonRepo.WithByID(database.ID)); err != nil {
				return err
			}
		}
	}
	return appInstallResourceRepo.DeleteBy(ctx, appInstallResourceRepo.WithAppInstallId(install.ID))
}

func updateInstall(installId uint, detailId uint) error {
	install, err := appInstallRepo.GetFirst(commonRepo.WithByID(installId))
	if err != nil {
		return err
	}
	oldDetail, err := appDetailRepo.GetFirst(commonRepo.WithByID(install.AppDetailId))
	if err != nil {
		return err
	}

	detail, err := appDetailRepo.GetFirst(commonRepo.WithByID(detailId))
	if err != nil {
		return err
	}
	if oldDetail.LastVersion == detail.Version {
		install.CanUpdate = false
	}
	if install.Version == detail.Version {
		return errors.New("two version is same")
	}
	tx, ctx := getTxAndContext()
	if err := backupInstall(ctx, install); err != nil {
		return err
	}
	tx.Commit()
	if _, err = compose.Down(install.GetComposePath()); err != nil {
		return err
	}
	install.DockerCompose = detail.DockerCompose
	install.Version = detail.Version

	fileOp := files.NewFileOp()
	if err := fileOp.WriteFile(install.GetComposePath(), strings.NewReader(install.DockerCompose), 0775); err != nil {
		return err
	}
	if _, err = compose.Up(install.GetComposePath()); err != nil {
		return err
	}
	return appInstallRepo.Save(&install)
}

func backupInstall(ctx context.Context, install model.AppInstall) error {
	var backup model.AppInstallBackup
	appPath := install.GetPath()
	backupDir := path.Join(constant.BackupDir, install.App.Key, install.Name)
	fileOp := files.NewFileOp()
	now := time.Now()
	day := now.Format("2006-01-02-15-04")
	fileName := fmt.Sprintf("%s-%s-%s%s", install.Name, install.Version, day, ".tar.gz")
	if err := fileOp.Compress([]string{appPath}, backupDir, fileName, files.TarGz); err != nil {
		return err
	}
	backup.Name = fileName
	backup.Path = backupDir
	backup.AppInstallId = install.ID
	backup.AppDetailId = install.AppDetailId
	backup.Param = install.Param

	if err := appInstallBackupRepo.Create(ctx, backup); err != nil {
		return err
	}
	return nil
}

func restoreInstall(install model.AppInstall, backupId uint) error {
	backup, err := appInstallBackupRepo.GetFirst(commonRepo.WithByID(backupId))
	if err != nil {
		return err
	}
	if _, err := compose.Down(install.GetComposePath()); err != nil {
		return err
	}
	installKeyDir := path.Join(constant.AppInstallDir, install.App.Key)
	installDir := path.Join(installKeyDir, install.Name)
	backupFile := path.Join(backup.Path, backup.Name)
	fileOp := files.NewFileOp()
	if !fileOp.Stat(backupFile) {
		return errors.New(fmt.Sprintf("%s file is not exist", backup.Name))
	}

	backupDir, err := fileOp.Backup(installDir)
	if err != nil {
		return err
	}
	if err := fileOp.Decompress(backupFile, installKeyDir, files.TarGz); err != nil {
		return err
	}
	composeContent, err := os.ReadFile(install.GetComposePath())
	if err != nil {
		return err
	}
	install.DockerCompose = string(composeContent)
	envContent, err := os.ReadFile(path.Join(installDir, ".env"))
	if err != nil {
		return err
	}
	install.Env = string(envContent)
	envMaps, err := godotenv.Unmarshal(string(envContent))
	if err != nil {
		return err
	}
	install.HttpPort = 0
	httpPort, ok := envMaps["PANEL_APP_PORT_HTTP"]
	if ok {
		httpPortN, _ := strconv.Atoi(httpPort)
		install.HttpPort = httpPortN
	}
	install.HttpsPort = 0
	httpsPort, ok := envMaps["PANEL_APP_PORT_HTTPS"]
	if ok {
		httpsPortN, _ := strconv.Atoi(httpsPort)
		install.HttpsPort = httpsPortN
	}

	composeMap := make(map[string]interface{})
	if err := yaml.Unmarshal(composeContent, &composeMap); err != nil {
		return err
	}
	servicesMap := composeMap["services"].(map[string]interface{})
	for k, v := range servicesMap {
		install.ServiceName = k
		value := v.(map[string]interface{})
		install.ContainerName = value["container_name"].(string)
	}

	install.Param = backup.Param
	_ = fileOp.DeleteDir(backupDir)
	if out, err := compose.Up(install.GetComposePath()); err != nil {
		return handleErr(install, err, out)
	}
	install.AppDetailId = backup.AppDetailId
	install.Version = backup.AppDetail.Version
	install.Status = constant.Running
	return appInstallRepo.Save(&install)
}

func getContainerNames(install model.AppInstall) ([]string, error) {
	composeMap := install.DockerCompose
	envMap := make(map[string]string)
	_ = json.Unmarshal([]byte(install.Env), &envMap)
	project, err := compose.GetComposeProject([]byte(composeMap), envMap)
	if err != nil {
		return nil, err
	}
	var containerNames []string
	for _, service := range project.AllServices() {
		containerNames = append(containerNames, service.ContainerName)
	}
	return containerNames, nil
}

func checkRequiredAndLimit(app model.App) error {

	if app.Limit > 0 {
		installs, err := appInstallRepo.GetBy(appInstallRepo.WithAppId(app.ID))
		if err != nil {
			return err
		}
		if len(installs) >= app.Limit {
			return errors.New(fmt.Sprintf("app install limit %d", app.Limit))
		}
	}

	if app.Required != "" {
		var requiredArray []string
		if err := json.Unmarshal([]byte(app.Required), &requiredArray); err != nil {
			return err
		}
		for _, key := range requiredArray {
			if key == "" {
				continue
			}
			requireApp, err := appRepo.GetFirst(appRepo.WithKey(key))
			if err != nil {
				return err
			}
			details, err := appDetailRepo.GetBy(appDetailRepo.WithAppId(requireApp.ID))
			if err != nil {
				return err
			}
			var detailIds []uint
			for _, d := range details {
				detailIds = append(detailIds, d.ID)
			}

			_, err = appInstallRepo.GetFirst(appInstallRepo.WithDetailIdsIn(detailIds))
			if err != nil {
				return errors.New(fmt.Sprintf("%s is required", requireApp.Key))
			}
		}
	}

	return nil
}

func handleMap(params map[string]interface{}, envParams map[string]string) {
	for k, v := range params {
		switch t := v.(type) {
		case string:
			envParams[k] = t
		case float64:
			envParams[k] = strconv.FormatFloat(t, 'f', -1, 32)
		default:
			envParams[k] = t.(string)
		}
	}
}

func copyAppData(key, version, installName string, params map[string]interface{}) (err error) {
	resourceDir := path.Join(constant.AppResourceDir, key, version)
	installDir := path.Join(constant.AppInstallDir, key)
	installVersionDir := path.Join(installDir, version)
	fileOp := files.NewFileOp()
	if err = fileOp.Copy(resourceDir, installVersionDir); err != nil {
		return
	}
	appDir := path.Join(installDir, installName)
	if err = fileOp.Rename(installVersionDir, appDir); err != nil {
		return
	}
	envPath := path.Join(appDir, ".env")

	envParams := make(map[string]string, len(params))
	handleMap(params, envParams)
	if err = godotenv.Write(envParams, envPath); err != nil {
		return
	}
	return
}

func upApp(composeFilePath string, appInstall model.AppInstall) {
	out, err := compose.Up(composeFilePath)
	if err != nil {
		if out != "" {
			appInstall.Message = out
		} else {
			appInstall.Message = err.Error()
		}
		appInstall.Status = constant.Error
		_ = appInstallRepo.Save(&appInstall)
	} else {
		appInstall.Status = constant.Running
		_ = appInstallRepo.Save(&appInstall)
	}
}

func getAppDetails(details []model.AppDetail, versions []string) map[string]model.AppDetail {
	appDetails := make(map[string]model.AppDetail, len(details))
	for _, old := range details {
		old.Status = constant.AppTakeDown
		appDetails[old.Version] = old
	}

	for _, v := range versions {
		detail, ok := appDetails[v]
		if ok {
			detail.Status = constant.AppNormal
			appDetails[v] = detail
		} else {
			appDetails[v] = model.AppDetail{
				Version: v,
				Status:  constant.AppNormal,
			}
		}
	}
	return appDetails
}

func getApps(oldApps []model.App, items []dto.AppDefine) map[string]model.App {
	apps := make(map[string]model.App, len(oldApps))
	for _, old := range oldApps {
		old.Status = constant.AppTakeDown
		apps[old.Key] = old
	}
	for _, item := range items {
		app, ok := apps[item.Key]
		if !ok {
			app = model.App{}
		}
		app.Name = item.Name
		app.Key = item.Key
		app.ShortDesc = item.ShortDesc
		app.Author = item.Author
		app.Source = item.Source
		app.Type = item.Type
		app.CrossVersionUpdate = item.CrossVersionUpdate
		app.Required = item.GetRequired()
		app.Status = constant.AppNormal
		apps[item.Key] = app
	}
	return apps
}

func handleErr(install model.AppInstall, err error, out string) error {
	reErr := err
	install.Message = err.Error()
	if out != "" {
		install.Message = out
		reErr = errors.New(out)
	}
	_ = appInstallRepo.Save(&install)
	return reErr
}

func getAppFromOss() error {
	res, err := http.Get(global.CONF.System.AppOss)
	if err != nil {
		return err
	}
	appByte, err := ioutil.ReadAll(res.Body)
	if err != nil {
		return err
	}
	var ossConfig dto.AppOssConfig
	if err := json.Unmarshal(appByte, &ossConfig); err != nil {
		return err
	}
	appDir := constant.AppResourceDir
	oldListFile := path.Join(appDir, "list.json")
	content, err := os.ReadFile(oldListFile)
	if err != nil {
		return err
	}
	list := &dto.AppList{}
	if err := json.Unmarshal(content, list); err != nil {
		return err
	}
	if list.Version == ossConfig.Version {
		return nil
	}

	fileOp := files.NewFileOp()
	if _, err := fileOp.Backup(appDir); err != nil {
		return err
	}

	packageName := path.Base(ossConfig.Package)
	packagePath := path.Join(constant.ResourceDir, packageName)
	if err := fileOp.DownloadFile(ossConfig.Package, packagePath); err != nil {
		return err
	}
	if err := fileOp.Decompress(packagePath, constant.ResourceDir, files.Zip); err != nil {
		return err
	}

	defer func() {
		_ = fileOp.DeleteFile(packagePath)
	}()
	return nil
}