package service

import (
	"context"
	"encoding/json"
	"fmt"
	"github.com/1Panel-dev/1Panel/backend/app/dto"
	"github.com/1Panel-dev/1Panel/backend/app/dto/request"
	"github.com/1Panel-dev/1Panel/backend/app/dto/response"
	"github.com/1Panel-dev/1Panel/backend/app/model"
	"github.com/1Panel-dev/1Panel/backend/app/repo"
	"github.com/1Panel-dev/1Panel/backend/buserr"
	"github.com/1Panel-dev/1Panel/backend/constant"
	"github.com/1Panel-dev/1Panel/backend/global"
	cmd2 "github.com/1Panel-dev/1Panel/backend/utils/cmd"
	"github.com/1Panel-dev/1Panel/backend/utils/compose"
	"github.com/1Panel-dev/1Panel/backend/utils/docker"
	"github.com/1Panel-dev/1Panel/backend/utils/env"
	"github.com/1Panel-dev/1Panel/backend/utils/files"
	"github.com/pkg/errors"
	"github.com/subosito/gotenv"
	"os"
	"path"
	"path/filepath"
	"regexp"
	"strconv"
	"strings"
	"time"
)

type RuntimeService struct {
}

type IRuntimeService interface {
	Page(req request.RuntimeSearch) (int64, []response.RuntimeDTO, error)
	Create(create request.RuntimeCreate) (*model.Runtime, error)
	Delete(delete request.RuntimeDelete) error
	Update(req request.RuntimeUpdate) error
	Get(id uint) (res *response.RuntimeDTO, err error)
	GetNodePackageRunScript(req request.NodePackageReq) ([]response.PackageScripts, error)
	OperateRuntime(req request.RuntimeOperate) error
	GetNodeModules(req request.NodeModuleReq) ([]response.NodeModule, error)
	OperateNodeModules(req request.NodeModuleOperateReq) error
	SyncForRestart() error
}

func NewRuntimeService() IRuntimeService {
	return &RuntimeService{}
}

func (r *RuntimeService) Create(create request.RuntimeCreate) (*model.Runtime, error) {
	var (
		opts []repo.DBOption
	)
	if create.Name != "" {
		opts = append(opts, commonRepo.WithLikeName(create.Name))
	}
	if create.Type != "" {
		opts = append(opts, commonRepo.WithByType(create.Type))
	}
	exist, _ := runtimeRepo.GetFirst(opts...)
	if exist != nil {
		return nil, buserr.New(constant.ErrNameIsExist)
	}
	fileOp := files.NewFileOp()

	switch create.Type {
	case constant.RuntimePHP:
		if create.Resource == constant.ResourceLocal {
			runtime := &model.Runtime{
				Name:     create.Name,
				Resource: create.Resource,
				Type:     create.Type,
				Version:  create.Version,
				Status:   constant.RuntimeNormal,
			}
			return nil, runtimeRepo.Create(context.Background(), runtime)
		}
		exist, _ = runtimeRepo.GetFirst(runtimeRepo.WithImage(create.Image))
		if exist != nil {
			return nil, buserr.New(constant.ErrImageExist)
		}
	case constant.RuntimeNode:
		if !fileOp.Stat(create.CodeDir) {
			return nil, buserr.New(constant.ErrPathNotFound)
		}
		create.Install = true
		if err := checkPortExist(create.Port); err != nil {
			return nil, err
		}
		for _, export := range create.ExposedPorts {
			if err := checkPortExist(export.HostPort); err != nil {
				return nil, err
			}
		}
		if containerName, ok := create.Params["CONTAINER_NAME"]; ok {
			if err := checkContainerName(containerName.(string)); err != nil {
				return nil, err
			}
		}
	}

	appDetail, err := appDetailRepo.GetFirst(commonRepo.WithByID(create.AppDetailID))
	if err != nil {
		return nil, err
	}
	app, err := appRepo.GetFirst(commonRepo.WithByID(appDetail.AppId))
	if err != nil {
		return nil, err
	}

	appVersionDir := filepath.Join(app.GetAppResourcePath(), appDetail.Version)
	if !fileOp.Stat(appVersionDir) || appDetail.Update {
		if err = downloadApp(app, appDetail, nil); err != nil {
			return nil, err
		}
	}

	runtime := &model.Runtime{
		Name:        create.Name,
		AppDetailID: create.AppDetailID,
		Type:        create.Type,
		Image:       create.Image,
		Resource:    create.Resource,
		Version:     create.Version,
	}

	switch create.Type {
	case constant.RuntimePHP:
		if err = handlePHP(create, runtime, fileOp, appVersionDir); err != nil {
			return nil, err
		}
	case constant.RuntimeNode:
		runtime.Port = create.Port
		if err = handleNode(create, runtime, fileOp, appVersionDir); err != nil {
			return nil, err
		}
	}
	if err := runtimeRepo.Create(context.Background(), runtime); err != nil {
		return nil, err
	}
	return runtime, nil
}

func (r *RuntimeService) Page(req request.RuntimeSearch) (int64, []response.RuntimeDTO, error) {
	var (
		opts []repo.DBOption
		res  []response.RuntimeDTO
	)
	if req.Name != "" {
		opts = append(opts, commonRepo.WithLikeName(req.Name))
	}
	if req.Status != "" {
		opts = append(opts, runtimeRepo.WithStatus(req.Status))
	}
	if req.Type != "" {
		opts = append(opts, commonRepo.WithByType(req.Type))
	}
	total, runtimes, err := runtimeRepo.Page(req.Page, req.PageSize, opts...)
	if err != nil {
		return 0, nil, err
	}
	for _, runtime := range runtimes {
		runtimeDTO := response.NewRuntimeDTO(runtime)
		runtimeDTO.Params = make(map[string]interface{})
		envs, err := gotenv.Unmarshal(runtime.Env)
		if err != nil {
			return 0, nil, err
		}
		for k, v := range envs {
			runtimeDTO.Params[k] = v
		}
		res = append(res, runtimeDTO)
	}
	return total, res, nil
}

func (r *RuntimeService) Delete(runtimeDelete request.RuntimeDelete) error {
	runtime, err := runtimeRepo.GetFirst(commonRepo.WithByID(runtimeDelete.ID))
	if err != nil {
		return err
	}
	website, _ := websiteRepo.GetFirst(websiteRepo.WithRuntimeID(runtimeDelete.ID))
	if website.ID > 0 {
		return buserr.New(constant.ErrDelWithWebsite)
	}
	if runtime.Resource == constant.ResourceAppstore {
		projectDir := runtime.GetPath()
		switch runtime.Type {
		case constant.RuntimePHP:
			client, err := docker.NewClient()
			if err != nil {
				return err
			}
			imageID, err := client.GetImageIDByName(runtime.Image)
			if err != nil {
				return err
			}
			if imageID != "" {
				if err := client.DeleteImage(imageID); err != nil {
					global.LOG.Errorf("delete image id [%s] error %v", imageID, err)
				}
			}
		case constant.RuntimeNode:
			if out, err := compose.Down(runtime.GetComposePath()); err != nil && !runtimeDelete.ForceDelete {
				if out != "" {
					return errors.New(out)
				}
				return err
			}
		}
		if err := files.NewFileOp().DeleteDir(projectDir); err != nil && !runtimeDelete.ForceDelete {
			return err
		}
	}
	return runtimeRepo.DeleteBy(commonRepo.WithByID(runtimeDelete.ID))
}

func (r *RuntimeService) Get(id uint) (*response.RuntimeDTO, error) {
	runtime, err := runtimeRepo.GetFirst(commonRepo.WithByID(id))
	if err != nil {
		return nil, err
	}

	res := response.NewRuntimeDTO(*runtime)
	if runtime.Resource == constant.ResourceLocal {
		return &res, nil
	}
	appDetail, err := appDetailRepo.GetFirst(commonRepo.WithByID(runtime.AppDetailID))
	if err != nil {
		return nil, err
	}
	res.AppID = appDetail.AppId
	switch runtime.Type {
	case constant.RuntimePHP:
		var (
			appForm   dto.AppForm
			appParams []response.AppParam
		)
		if err := json.Unmarshal([]byte(runtime.Params), &appForm); err != nil {
			return nil, err
		}
		envs, err := gotenv.Unmarshal(runtime.Env)
		if err != nil {
			return nil, err
		}
		if v, ok := envs["CONTAINER_PACKAGE_URL"]; ok {
			res.Source = v
		}
		for _, form := range appForm.FormFields {
			if v, ok := envs[form.EnvKey]; ok {
				appParam := response.AppParam{
					Edit:     false,
					Key:      form.EnvKey,
					Rule:     form.Rule,
					Type:     form.Type,
					Required: form.Required,
				}
				if form.Edit {
					appParam.Edit = true
				}
				appParam.LabelZh = form.LabelZh
				appParam.LabelEn = form.LabelEn
				appParam.Multiple = form.Multiple
				appParam.Value = v
				if form.Type == "select" {
					if form.Multiple {
						if v == "" {
							appParam.Value = []string{}
						} else {
							appParam.Value = strings.Split(v, ",")
						}
					} else {
						for _, fv := range form.Values {
							if fv.Value == v {
								appParam.ShowValue = fv.Label
								break
							}
						}
					}
					appParam.Values = form.Values
				}
				appParams = append(appParams, appParam)
			}
		}
		res.AppParams = appParams
	case constant.RuntimeNode:
		res.Params = make(map[string]interface{})
		envs, err := gotenv.Unmarshal(runtime.Env)
		if err != nil {
			return nil, err
		}
		for k, v := range envs {
			switch k {
			case "NODE_APP_PORT", "PANEL_APP_PORT_HTTP":
				port, err := strconv.Atoi(v)
				if err != nil {
					return nil, err
				}
				res.Params[k] = port
			default:
				if strings.Contains(k, "CONTAINER_PORT") || strings.Contains(k, "HOST_PORT") {
					if strings.Contains(k, "CONTAINER_PORT") {
						r := regexp.MustCompile(`_(\d+)$`)
						matches := r.FindStringSubmatch(k)
						containerPort, err := strconv.Atoi(v)
						if err != nil {
							return nil, err
						}
						hostPort, err := strconv.Atoi(envs[fmt.Sprintf("HOST_PORT_%s", matches[1])])
						if err != nil {
							return nil, err
						}
						res.ExposedPorts = append(res.ExposedPorts, request.ExposedPort{
							ContainerPort: containerPort,
							HostPort:      hostPort,
						})
					}
				} else {
					res.Params[k] = v
				}
			}
		}
		if v, ok := envs["CONTAINER_PACKAGE_URL"]; ok {
			res.Source = v
		}
	}

	return &res, nil
}

func (r *RuntimeService) Update(req request.RuntimeUpdate) error {
	runtime, err := runtimeRepo.GetFirst(commonRepo.WithByID(req.ID))
	if err != nil {
		return err
	}
	if runtime.Resource == constant.ResourceLocal {
		runtime.Version = req.Version
		return runtimeRepo.Save(runtime)
	}
	oldImage := runtime.Image
	switch runtime.Type {
	case constant.RuntimePHP:
		exist, _ := runtimeRepo.GetFirst(runtimeRepo.WithImage(req.Name), runtimeRepo.WithNotId(req.ID))
		if exist != nil {
			return buserr.New(constant.ErrImageExist)
		}
	case constant.RuntimeNode:
		if runtime.Port != req.Port {
			if err = checkPortExist(req.Port); err != nil {
				return err
			}
			runtime.Port = req.Port
		}
		for _, export := range req.ExposedPorts {
			if err = checkPortExist(export.HostPort); err != nil {
				return err
			}
		}
		if containerName, ok := req.Params["CONTAINER_NAME"]; ok {
			envs, err := gotenv.Unmarshal(runtime.Env)
			if err != nil {
				return err
			}
			oldContainerName := envs["CONTAINER_NAME"]
			if containerName != oldContainerName {
				if err := checkContainerName(containerName.(string)); err != nil {
					return err
				}
			}
		}

		appDetail, err := appDetailRepo.GetFirst(commonRepo.WithByID(runtime.AppDetailID))
		if err != nil {
			return err
		}
		app, err := appRepo.GetFirst(commonRepo.WithByID(appDetail.AppId))
		if err != nil {
			return err
		}
		fileOp := files.NewFileOp()
		appVersionDir := path.Join(constant.AppResourceDir, app.Resource, app.Key, appDetail.Version)
		if !fileOp.Stat(appVersionDir) || appDetail.Update {
			if err := downloadApp(app, appDetail, nil); err != nil {
				return err
			}
			_ = fileOp.Rename(path.Join(runtime.GetPath(), "run.sh"), path.Join(runtime.GetPath(), "run.sh.bak"))
			_ = fileOp.CopyFile(path.Join(appVersionDir, "run.sh"), runtime.GetPath())
		}
	}

	projectDir := path.Join(constant.RuntimeDir, runtime.Type, runtime.Name)
	create := request.RuntimeCreate{
		Image:   req.Image,
		Type:    runtime.Type,
		Source:  req.Source,
		Params:  req.Params,
		CodeDir: req.CodeDir,
		Version: req.Version,
		NodeConfig: request.NodeConfig{
			Port:         req.Port,
			Install:      true,
			ExposedPorts: req.ExposedPorts,
		},
	}
	composeContent, envContent, _, err := handleParams(create, projectDir)
	if err != nil {
		return err
	}
	runtime.Env = string(envContent)
	runtime.DockerCompose = string(composeContent)

	switch runtime.Type {
	case constant.RuntimePHP:
		runtime.Image = req.Image
		runtime.Status = constant.RuntimeBuildIng
		_ = runtimeRepo.Save(runtime)
		client, err := docker.NewClient()
		if err != nil {
			return err
		}
		imageID, err := client.GetImageIDByName(oldImage)
		if err != nil {
			return err
		}
		go buildRuntime(runtime, imageID, req.Rebuild)
	case constant.RuntimeNode:
		runtime.Version = req.Version
		runtime.CodeDir = req.CodeDir
		runtime.Port = req.Port
		runtime.Status = constant.RuntimeReCreating
		_ = runtimeRepo.Save(runtime)
		go reCreateRuntime(runtime)
	}
	return nil
}

func (r *RuntimeService) GetNodePackageRunScript(req request.NodePackageReq) ([]response.PackageScripts, error) {
	fileOp := files.NewFileOp()
	if !fileOp.Stat(req.CodeDir) {
		return nil, buserr.New(constant.ErrPathNotFound)
	}
	if !fileOp.Stat(path.Join(req.CodeDir, "package.json")) {
		return nil, buserr.New(constant.ErrPackageJsonNotFound)
	}
	content, err := fileOp.GetContent(path.Join(req.CodeDir, "package.json"))
	if err != nil {
		return nil, err
	}
	var packageMap map[string]interface{}
	err = json.Unmarshal(content, &packageMap)
	if err != nil {
		return nil, err
	}
	scripts, ok := packageMap["scripts"]
	if !ok {
		return nil, buserr.New(constant.ErrScriptsNotFound)
	}
	var packageScripts []response.PackageScripts
	for k, v := range scripts.(map[string]interface{}) {
		packageScripts = append(packageScripts, response.PackageScripts{
			Name:   k,
			Script: v.(string),
		})
	}
	return packageScripts, nil
}

func (r *RuntimeService) OperateRuntime(req request.RuntimeOperate) error {
	runtime, err := runtimeRepo.GetFirst(commonRepo.WithByID(req.ID))
	if err != nil {
		return err
	}
	defer func() {
		if err != nil {
			runtime.Status = constant.RuntimeError
			runtime.Message = err.Error()
			_ = runtimeRepo.Save(runtime)
		}
	}()
	switch req.Operate {
	case constant.RuntimeUp:
		if err = runComposeCmdWithLog(req.Operate, runtime.GetComposePath(), runtime.GetLogPath()); err != nil {
			return err
		}
		if err = SyncRuntimeContainerStatus(runtime); err != nil {
			return err
		}
	case constant.RuntimeDown:
		if err = runComposeCmdWithLog(req.Operate, runtime.GetComposePath(), runtime.GetLogPath()); err != nil {
			return err
		}
		runtime.Status = constant.RuntimeStopped
	case constant.RuntimeRestart:
		if err = runComposeCmdWithLog(constant.RuntimeDown, runtime.GetComposePath(), runtime.GetLogPath()); err != nil {
			return err
		}
		if err = runComposeCmdWithLog(constant.RuntimeUp, runtime.GetComposePath(), runtime.GetLogPath()); err != nil {
			return err
		}
		if err = SyncRuntimeContainerStatus(runtime); err != nil {
			return err
		}
	}
	return runtimeRepo.Save(runtime)
}

func (r *RuntimeService) GetNodeModules(req request.NodeModuleReq) ([]response.NodeModule, error) {
	runtime, err := runtimeRepo.GetFirst(commonRepo.WithByID(req.ID))
	if err != nil {
		return nil, err
	}
	var res []response.NodeModule
	nodeModulesPath := path.Join(runtime.CodeDir, "node_modules")
	fileOp := files.NewFileOp()
	if !fileOp.Stat(nodeModulesPath) {
		return nil, buserr.New("ErrNodeModulesNotFound")
	}
	moduleDirs, err := os.ReadDir(nodeModulesPath)
	if err != nil {
		return nil, err
	}
	for _, moduleDir := range moduleDirs {
		packagePath := path.Join(nodeModulesPath, moduleDir.Name(), "package.json")
		if !fileOp.Stat(packagePath) {
			continue
		}
		content, err := fileOp.GetContent(packagePath)
		if err != nil {
			continue
		}
		module := response.NodeModule{}
		if err := json.Unmarshal(content, &module); err != nil {
			continue
		}
		res = append(res, module)
	}
	return res, nil
}

func (r *RuntimeService) OperateNodeModules(req request.NodeModuleOperateReq) error {
	runtime, err := runtimeRepo.GetFirst(commonRepo.WithByID(req.ID))
	if err != nil {
		return err
	}
	containerName, err := env.GetEnvValueByKey(runtime.GetEnvPath(), "CONTAINER_NAME")
	if err != nil {
		return err
	}
	cmd := req.PkgManager
	switch req.Operate {
	case constant.RuntimeInstall:
		if req.PkgManager == constant.RuntimeNpm {
			cmd += " install"
		} else {
			cmd += " add"
		}
	case constant.RuntimeUninstall:
		if req.PkgManager == constant.RuntimeNpm {
			cmd += " uninstall"
		} else {
			cmd += " remove"
		}
	case constant.RuntimeUpdate:
		if req.PkgManager == constant.RuntimeNpm {
			cmd += " update"
		} else {
			cmd += " upgrade"
		}
	}
	cmd += " " + req.Module
	return cmd2.ExecContainerScript(containerName, cmd, 5*time.Minute)
}

func (r *RuntimeService) SyncForRestart() error {
	runtimes, err := runtimeRepo.List()
	if err != nil {
		return err
	}
	for _, runtime := range runtimes {
		if runtime.Status == constant.RuntimeBuildIng || runtime.Status == constant.RuntimeReCreating || runtime.Status == constant.RuntimeStarting || runtime.Status == constant.RuntimeCreating {
			runtime.Status = constant.SystemRestart
			runtime.Message = "System restart causing interrupt"
			_ = runtimeRepo.Save(&runtime)
		}
	}
	return nil
}
