package service

import (
	"crypto/rand"
	"encoding/json"
	"fmt"
	"math/big"
	"net/http"
	"os"
	"path"
	"strconv"
	"strings"

	"github.com/1Panel-dev/1Panel/core/app/dto"
	"github.com/1Panel-dev/1Panel/core/app/model"
	"github.com/1Panel-dev/1Panel/core/app/repo"
	"github.com/1Panel-dev/1Panel/core/app/task"
	"github.com/1Panel-dev/1Panel/core/buserr"
	"github.com/1Panel-dev/1Panel/core/constant"
	"github.com/1Panel-dev/1Panel/core/global"
	"github.com/1Panel-dev/1Panel/core/i18n"
	"github.com/1Panel-dev/1Panel/core/utils/common"
	"github.com/1Panel-dev/1Panel/core/utils/files"
	"github.com/1Panel-dev/1Panel/core/utils/req_helper"
	"github.com/1Panel-dev/1Panel/core/utils/xpack"
	"github.com/gin-gonic/gin"
	"github.com/jinzhu/copier"
	"gopkg.in/yaml.v2"
)

type ScriptService struct{}

type IScriptService interface {
	Run()
	Search(ctx *gin.Context, req dto.SearchPageWithGroup) (int64, interface{}, error)
	Create(req dto.ScriptOperate) error
	Update(req dto.ScriptOperate) error
	Delete(ids dto.OperateByIDs) error
	Sync(req dto.OperateByTaskID) error
}

func NewIScriptService() IScriptService {
	return &ScriptService{}
}

func (u *ScriptService) Search(ctx *gin.Context, req dto.SearchPageWithGroup) (int64, interface{}, error) {
	options := []global.DBOption{repo.WithOrderDesc("created_at")}
	if len(req.Info) != 0 {
		options = append(options, scriptRepo.WithByInfo(req.Info))
	}
	list, err := scriptRepo.GetList(options...)
	if err != nil {
		return 0, nil, err
	}
	groups, _ := groupRepo.GetList(repo.WithByType("script"))
	groupMap := make(map[uint]string)
	for _, item := range groups {
		groupMap[item.ID] = item.Name
	}
	var data []dto.ScriptInfo
	for _, itemData := range list {
		var item dto.ScriptInfo
		if err := copier.Copy(&item, &itemData); err != nil {
			global.LOG.Errorf("copy scripts to dto backup info failed, err: %v", err)
		}
		if item.IsSystem {
			lang := strings.ToLower(common.GetLang(ctx))
			var nameMap = make(map[string]string)
			_ = json.Unmarshal([]byte(item.Name), &nameMap)
			var descriptionMap = make(map[string]string)
			_ = json.Unmarshal([]byte(item.Description), &descriptionMap)
			if val, ok := nameMap[lang]; ok {
				item.Name = val
			}
			if val, ok := descriptionMap[lang]; ok {
				item.Description = val
			}
		}
		matchGroup := false
		groupIDs := strings.Split(itemData.Groups, ",")
		for _, idItem := range groupIDs {
			id, _ := strconv.Atoi(idItem)
			if id == 0 {
				continue
			}
			if uint(id) == req.GroupID {
				matchGroup = true
			}
			item.GroupList = append(item.GroupList, uint(id))
			item.GroupBelong = append(item.GroupBelong, groupMap[uint(id)])
		}
		if req.GroupID == 0 {
			data = append(data, item)
			continue
		}
		if matchGroup {
			data = append(data, item)
		}
	}
	var records []dto.ScriptInfo
	total, start, end := len(data), (req.Page-1)*req.PageSize, req.Page*req.PageSize
	if start > total {
		records = make([]dto.ScriptInfo, 0)
	} else {
		if end >= total {
			end = total
		}
		records = data[start:end]
	}
	return int64(total), records, nil
}

func (u *ScriptService) Create(req dto.ScriptOperate) error {
	itemData, _ := scriptRepo.Get(repo.WithByName(req.Name))
	if itemData.ID != 0 {
		return buserr.New("ErrRecordExist")
	}
	if err := copier.Copy(&itemData, &req); err != nil {
		return buserr.WithDetail("ErrStructTransform", err.Error(), nil)
	}
	if err := scriptRepo.Create(&itemData); err != nil {
		return err
	}
	if req.IsInteractive {
		return nil
	}
	if err := xpack.MultiNodeProvider.Sync(constant.SyncScripts); err != nil {
		global.LOG.Errorf("sync scripts to node failed, err: %v", err)
	}
	return nil
}

func (u *ScriptService) Delete(req dto.OperateByIDs) error {
	for _, item := range req.IDs {
		scriptItem, _ := scriptRepo.Get(repo.WithByID(item))
		if scriptItem.ID == 0 || scriptItem.IsSystem {
			continue
		}
		if err := scriptRepo.Delete(repo.WithByID(item)); err != nil {
			return err
		}
	}
	if err := xpack.MultiNodeProvider.Sync(constant.SyncScripts); err != nil {
		global.LOG.Errorf("sync scripts to node failed, err: %v", err)
	}
	return nil
}

func (u *ScriptService) Update(req dto.ScriptOperate) error {
	itemData, _ := scriptRepo.Get(repo.WithByID(req.ID))
	if itemData.ID == 0 {
		return buserr.New("ErrRecordNotFound")
	}
	updateMap := make(map[string]interface{})
	updateMap["name"] = req.Name
	updateMap["script"] = req.Script
	updateMap["groups"] = req.Groups
	updateMap["is_interactive"] = req.IsInteractive
	updateMap["description"] = req.Description
	if err := scriptRepo.Update(req.ID, updateMap); err != nil {
		return err
	}
	if err := xpack.MultiNodeProvider.Sync(constant.SyncScripts); err != nil {
		global.LOG.Errorf("sync scripts to node failed, err: %v", err)
	}
	return nil
}

func StartSync() {
	if global.ScriptSyncJobID != 0 {
		global.Cron.Remove(global.ScriptSyncJobID)
	}
	service := NewIScriptService()
	scriptSync, _ := repo.NewISettingRepo().GetValueByKey("ScriptSync")
	if !global.CONF.Base.IsOffline && scriptSync == constant.StatusEnable {
		minuteRand, err := rand.Int(rand.Reader, big.NewInt(60))
		if err != nil {
			global.LOG.Errorf("generate random minute failed: %v", err)
			minuteRand = big.NewInt(0)
		}
		hourRand, err := rand.Int(rand.Reader, big.NewInt(3))
		if err != nil {
			global.LOG.Errorf("generate random hour failed: %v", err)
			hourRand = big.NewInt(0)
		}
		id, err := global.Cron.AddJob(fmt.Sprintf("%v %v * * *", minuteRand.Int64(), hourRand.Int64()), service)
		if err != nil {
			global.LOG.Errorf("[core] can not add script sync corn job: %s", err.Error())
		}
		global.LOG.Info("add job for script library sync successful")
		global.ScriptSyncJobID = id
	}
}

func SyncScriptLibraryOnStartup() {
	if global.CONF.Base.IsOffline {
		return
	}
	scriptSync, err := repo.NewISettingRepo().GetValueByKey("ScriptSync")
	if err != nil {
		global.LOG.Errorf("load script library sync setting failed, err: %v", err)
		return
	}
	if scriptSync != constant.StatusEnable {
		return
	}
	go NewIScriptService().Run()
}

func (u *ScriptService) Run() {
	if global.CONF.Base.IsOffline {
		return
	}
	versionRes, isLatest, err := loadRemoteScriptVersion()
	if err != nil {
		global.LOG.Errorf("sync scripts from remote failed, err: %v", err)
		return
	}
	if isLatest {
		return
	}
	if err := u.sync(dto.OperateByTaskID{}, versionRes); err != nil {
		global.LOG.Errorf("sync scripts from remote failed, err: %v", err)
	}
}

func LoadScriptInfo(id uint) (model.ScriptLibrary, error) {
	return scriptRepo.Get(repo.WithByID(id))
}

func (u *ScriptService) Sync(req dto.OperateByTaskID) error {
	if global.CONF.Base.IsOffline {
		return nil
	}
	return u.sync(req, nil)
}

func (u *ScriptService) sync(req dto.OperateByTaskID, versionRes []byte) error {
	syncTask, err := task.NewTaskWithOps(i18n.GetMsgByKey("RemoteScriptLibrary"), task.TaskSync, task.TaskScopeScript, req.TaskID, 0)
	if err != nil {
		global.LOG.Errorf("create sync task failed %v", err)
		return err
	}

	syncTask.AddSubTask(task.GetTaskName(i18n.GetMsgByKey("RemoteScriptLibrary"), task.TaskSync, task.TaskScopeScript), func(t *task.Task) (err error) {
		if versionRes == nil {
			var isLatest bool
			versionRes, isLatest, err = loadRemoteScriptVersion()
			if err != nil {
				return err
			}
			if isLatest {
				syncTask.Log(i18n.GetMsgByKey("ScriptSyncSkip"))
				return nil
			}
		}

		dataUrl := fmt.Sprintf("%s/scripts/data.yaml", global.ResourceURL())
		_, dataRes, err := req_helper.HandleRequestWithProxy(dataUrl, http.MethodGet, constant.TimeOut20s)
		syncTask.LogWithStatus(i18n.GetMsgByKey("DownloadData"), err)
		if err != nil {
			return fmt.Errorf("load scripts data.yaml from remote failed, err: %v", err)
		}

		var scripts Scripts
		if err = yaml.Unmarshal(dataRes, &scripts); err != nil {
			return fmt.Errorf("the format of data.yaml is err: %v", err)
		}

		tmpDir := path.Join(global.CONF.Base.InstallDir, "1panel/tmp/script")
		if _, err := os.Stat(tmpDir); err != nil {
			_ = os.MkdirAll(tmpDir, 0755)
		}
		scriptsUrl := fmt.Sprintf("%s/scripts/scripts.tar.gz", global.ResourceURL())
		err = files.DownloadFileWithProxyStream(scriptsUrl, tmpDir+"/scripts.tar.gz")
		syncTask.LogWithStatus(i18n.GetMsgByKey("DownloadPackage"), err)
		if err != nil {
			return fmt.Errorf("download scripts.tar.gz failed, err: %v", err)
		}

		if err := files.HandleUnTar(tmpDir+"/scripts.tar.gz", tmpDir, ""); err != nil {
			return fmt.Errorf("handle decompress scripts.tar.gz failed, err: %v", err)
		}
		var scriptsForDB []model.ScriptLibrary
		for _, item := range scripts.Scripts.Sh {
			itemName, _ := json.Marshal(item.Name)
			itemDescription, _ := json.Marshal(item.Description)
			shell, _ := os.ReadFile(fmt.Sprintf("%s/scripts/sh/%s.sh", tmpDir, item.Key))
			scriptItem := model.ScriptLibrary{
				Name:          string(itemName),
				IsInteractive: item.Interactive,
				IsSystem:      true,
				Script:        string(shell),
				Description:   string(itemDescription),
			}
			scriptsForDB = append(scriptsForDB, scriptItem)
		}

		syncTask.Log(i18n.GetMsgByKey("AnalyticCompletion"))
		if err := scriptRepo.SyncAll(scriptsForDB); err != nil {
			return fmt.Errorf("sync script with db failed, err: %v", err)
		}
		_ = os.RemoveAll(tmpDir)
		if err := settingRepo.Update("ScriptVersion", string(versionRes)); err != nil {
			return fmt.Errorf("update script version in db failed, err: %v", err)
		}
		if err := xpack.MultiNodeProvider.Sync(constant.SyncScripts); err != nil {
			global.LOG.Errorf("sync scripts to node failed, err: %v", err)
		}
		return nil
	}, nil)

	if err := syncTask.Execute(); err != nil {
		return fmt.Errorf("sync scripts from remote failed, err: %v", err)
	}
	return nil
}

func loadRemoteScriptVersion() ([]byte, bool, error) {
	versionURL := fmt.Sprintf("%s/scripts/version.txt", global.ResourceURL())
	_, remoteVersion, err := req_helper.HandleRequestWithProxy(versionURL, http.MethodGet, constant.TimeOut20s)
	if err != nil {
		return nil, false, fmt.Errorf("load scripts version from remote failed, err: %v", err)
	}
	localVersion, err := settingRepo.GetValueByKey("ScriptVersion")
	if err != nil {
		return nil, false, fmt.Errorf("load local scripts version failed, err: %v", err)
	}
	isLatest := strings.TrimSpace(localVersion) == strings.TrimSpace(string(remoteVersion))
	return remoteVersion, isLatest, nil
}

type Scripts struct {
	Scripts ScriptDetail `json:"scripts"`
}

type ScriptDetail struct {
	Sh []ScriptHelper `json:"sh"`
}

type ScriptHelper struct {
	Key         string            `json:"key"`
	Sort        uint              `json:"sort"`
	Groups      string            `json:"groups"`
	Name        map[string]string `json:"name"`
	Interactive bool              `json:"interactive"`
	Description map[string]string `json:"description"`
}
