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"
	"github.com/1Panel-dev/1Panel/backend/utils/docker"
	"github.com/1Panel-dev/1Panel/backend/utils/files"
	"github.com/subosito/gotenv"
	"path"
	"path/filepath"
	"strings"
	"time"
)

type RuntimeService struct {
}

type IRuntimeService interface {
	Page(req request.RuntimeSearch) (int64, []response.RuntimeRes, error)
	Create(create request.RuntimeCreate) error
	Delete(id uint) error
	Update(req request.RuntimeUpdate) error
	Get(id uint) (res *response.RuntimeRes, err error)
}

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

func (r *RuntimeService) Create(create request.RuntimeCreate) (err error) {
	exist, _ := runtimeRepo.GetFirst(runtimeRepo.WithName(create.Name))
	if exist != nil {
		return buserr.New(constant.ErrNameIsExist)
	}
	if create.Resource == constant.ResourceLocal {
		runtime := &model.Runtime{
			Name:     create.Name,
			Resource: create.Resource,
			Type:     create.Type,
			Version:  create.Version,
			Status:   constant.RuntimeNormal,
		}
		return runtimeRepo.Create(context.Background(), runtime)
	}
	exist, _ = runtimeRepo.GetFirst(runtimeRepo.WithImage(create.Image))
	if exist != nil {
		return buserr.New(constant.ErrImageExist)
	}
	appDetail, err := appDetailRepo.GetFirst(commonRepo.WithByID(create.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
		}
	}
	buildDir := path.Join(appVersionDir, "build")
	if !fileOp.Stat(buildDir) {
		return buserr.New(constant.ErrDirNotFound)
	}
	runtimeDir := path.Join(constant.RuntimeDir, create.Type)
	tempDir := filepath.Join(runtimeDir, fmt.Sprintf("%d", time.Now().UnixNano()))
	if err = fileOp.CopyDir(buildDir, tempDir); err != nil {
		return
	}
	oldDir := path.Join(tempDir, "build")
	newNameDir := path.Join(runtimeDir, create.Name)
	defer func() {
		if err != nil {
			_ = fileOp.DeleteDir(newNameDir)
		}
	}()
	if oldDir != newNameDir {
		if err = fileOp.Rename(oldDir, newNameDir); err != nil {
			return
		}
		if err = fileOp.DeleteDir(tempDir); err != nil {
			return
		}
	}
	composeContent, envContent, forms, err := handleParams(create.Image, create.Type, newNameDir, create.Params)
	if err != nil {
		return
	}
	runtime := &model.Runtime{
		Name:          create.Name,
		DockerCompose: string(composeContent),
		Env:           string(envContent),
		AppDetailID:   create.AppDetailID,
		Type:          create.Type,
		Image:         create.Image,
		Resource:      create.Resource,
		Status:        constant.RuntimeBuildIng,
		Version:       create.Version,
		Params:        string(forms),
	}
	if err = runtimeRepo.Create(context.Background(), runtime); err != nil {
		return
	}
	go buildRuntime(runtime, "")
	return
}

func (r *RuntimeService) Page(req request.RuntimeSearch) (int64, []response.RuntimeRes, error) {
	var (
		opts []repo.DBOption
		res  []response.RuntimeRes
	)
	if req.Name != "" {
		opts = append(opts, commonRepo.WithLikeName(req.Name))
	}
	if req.Status != "" {
		opts = append(opts, runtimeRepo.WithStatus(req.Status))
	}
	total, runtimes, err := runtimeRepo.Page(req.Page, req.PageSize, opts...)
	if err != nil {
		return 0, nil, err
	}
	for _, runtime := range runtimes {
		res = append(res, response.RuntimeRes{
			Runtime: runtime,
		})
	}
	return total, res, nil
}

func (r *RuntimeService) Delete(id uint) error {
	runtime, err := runtimeRepo.GetFirst(commonRepo.WithByID(id))
	if err != nil {
		return err
	}
	website, _ := websiteRepo.GetFirst(websiteRepo.WithRuntimeID(id))
	if website.ID > 0 {
		return buserr.New(constant.ErrDelWithWebsite)
	}
	if runtime.Resource == constant.ResourceAppstore {
		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)
			}
		}
		runtimeDir := path.Join(constant.RuntimeDir, runtime.Type, runtime.Name)
		if err := files.NewFileOp().DeleteDir(runtimeDir); err != nil {
			return err
		}
	}
	return runtimeRepo.DeleteBy(commonRepo.WithByID(id))
}

func (r *RuntimeService) Get(id uint) (*response.RuntimeRes, error) {
	runtime, err := runtimeRepo.GetFirst(commonRepo.WithByID(id))
	if err != nil {
		return nil, err
	}
	res := &response.RuntimeRes{}
	res.Runtime = *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
	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
	}
	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
	return res, nil
}

func (r *RuntimeService) Update(req request.RuntimeUpdate) error {
	runtime, err := runtimeRepo.GetFirst(commonRepo.WithByID(req.ID))
	if err != nil {
		return err
	}
	oldImage := runtime.Image
	if runtime.Resource == constant.ResourceLocal {
		runtime.Version = req.Version
		return runtimeRepo.Save(runtime)
	}
	exist, _ := runtimeRepo.GetFirst(runtimeRepo.WithImage(req.Name), runtimeRepo.WithNotId(req.ID))
	if exist != nil {
		return buserr.New(constant.ErrImageExist)
	}
	runtimeDir := path.Join(constant.RuntimeDir, runtime.Type, runtime.Name)
	composeContent, envContent, _, err := handleParams(req.Image, runtime.Type, runtimeDir, req.Params)
	if err != nil {
		return err
	}
	if err != nil {
		return err
	}
	runtime.Image = req.Image
	runtime.Env = string(envContent)
	runtime.DockerCompose = string(composeContent)
	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)
	return nil
}