package service

import (
	"crypto/sha1"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	uuid "github.com/satori/go.uuid"
	"os"
	"sec-cycle-scan/common"
	"sec-cycle-scan/model"
	"sec-cycle-scan/utils"
	"time"
	"unicode/utf8"
)

func GitleaksScan(dir string) ([]model.Secret, error) {

	var secret []model.Secret
	gitleaksConfigPath := common.CONFIG.Gitleaks.ConfigPath
	gitleaksPath := common.CONFIG.Gitleaks.GitleaksPath
	resPath := fmt.Sprintf("%s/%s.json", common.CONFIG.Gitleaks.DataDirectory, utils.RandStr(8))

	//defer func() {
	//	_ = os.Remove(resPath)
	//}()

	timeOut := 10 * 60 * 1000

	//--exit-code 来指定发现泄露时的退出码，否则默认发现泄露和错误是一个错误码
	args := []string{"detect", "-c", gitleaksConfigPath, "-s", dir, "-f", "json", "-r", resPath, "--exit-code", "3"}
	exitCode, _, stderr, killed := utils.RunCommandWithTimeout(timeOut, dir, gitleaksPath, nil, args...)
	if killed || (exitCode != 0 && exitCode != 3) {
		return secret, errors.New(fmt.Sprintf("Gitleaks Scan Failed.  err: %s exitCode: %d ", stderr, exitCode))
	}

	if exitCode == 3 {
		resBytes, err := os.ReadFile(resPath)
		if err != nil {
			return secret, errors.New(fmt.Sprintf("task result read failure. err: %s ", err))
		}

		if err = json.Unmarshal(resBytes, &secret); err != nil {
			return secret, err
		}
	}

	return secret, nil
}

func SecretScan(repo model.RepoResp, codeDir string, assetStr string) error {

	v4 := uuid.NewV4()
	taskID := v4.String()

	task := model.ScanInfo{
		TaskSource: "cycle",
		RepoLink:   repo.FullName,
		TaskId:     taskID,
		ScanType:   "secret",
		StartTime:  time.Now(),
	}

	if err := GitDownload(repo.CloneUrl, repo.DefaultBranch, codeDir); err != nil {
		return errors.New(fmt.Sprintf("Code download failed. err:%s", err))
	}

	task.CloneEndTime = time.Now()

	scanRes, err := GitleaksScan(codeDir)
	if err != nil {
		return errors.New(fmt.Sprintf("Gitleaks scan failed.  err:%s", err))
	}
	task.ScanEndTime = time.Now()

	if scanRes != nil && len(scanRes) != 0 {
		for i := range scanRes {
			v := &scanRes[i]
			uniqStr := fmt.Sprintf("%s-%d-%s-%s-%s", v.File, v.StartLine, v.Secret, v.RuleID, v.Commit)
			uniqHash := sha1.Sum([]byte(uniqStr))
			uniqId := hex.EncodeToString(uniqHash[:])
			v.UniqueID = uniqId
			tmpSecret := MaskSensitiveData(v.Secret, 7, 3, '*')
			v.Secret = TrimStringToMaxLen(tmpSecret, 255)
		}

		//if len(scanRes) > 0 {
		//	resStr, err := json.Marshal(scanRes)
		//	if err != nil {
		//		return err
		//	}
		//
		//	// 缓存报告
		//	if err := cache.SetString("secret:"+assetStr, string(resStr)); err != nil {
		//		return err
		//	}
		//}

		var secretRes []model.SecretResult

		for _, v := range scanRes {
			secret := model.SecretResult{
				AssetHash: assetStr,
				VulUnqID:  v.UniqueID,
				RepoLink:  repo.FullName,
				Author:    v.Author,
				Email:     v.Email,
				RuleID:    v.RuleID,
				StartLine: v.StartLine,
				Secret:    v.Secret,
				File:      TrimStringToMaxLen(v.File, 255),
				Commit:    v.Commit,
				PushDate:  v.Date,
				State:     1,
				CreatedAt: time.Now(),
				UpdatedAt: time.Now(),
			}

			rowsAffected, err := CreateSecretResult(secret)
			if err != nil {
				return errors.New(fmt.Sprintf("Secret result record failed. err: %s", err))
			} else {
				if rowsAffected != 0 {
					secretRes = append(secretRes, secret)
				}
			}
		}

		// 飞书通知
		SendSecretNotice(secretRes)
		task.RiskNum = len(secretRes)
	}
	if _, err := CreateScanInfo(task); err != nil {
		return errors.New(fmt.Sprintf("Failed to create the info scan.. err: %s", err))
	}

	return nil
}

func SendSecretNotice(secretRes []model.SecretResult) {

	if secretRes == nil || len(secretRes) == 0 {
		return
	}

	secretCount := len(secretRes)

	pageSize := 30
	total := secretCount/pageSize + 1

	for i := 1; i <= total; i++ {
		endIdx := i * pageSize
		if secretCount < endIdx {
			endIdx = secretCount
		}
		startIdx := (i - 1) * pageSize

		//构建 interactive 消息
		interactiveMsg, _ := GenerateSecretKeyReports(secretRes[startIdx:endIdx])
		if err := SendFeiShuMessage(interactiveMsg); err != nil {
			common.LOG.Error(fmt.Sprintf("FeiShu secret key no notification send failure. err: %s", err))
		}
	}

}

func MaskSensitiveData(data string, keepPrefix, keepSuffix int, mask rune) string {
	r := []rune(data)
	length := len(r)

	if keepPrefix >= length || keepSuffix >= length {
		return data
	}

	start := keepPrefix
	end := length - keepSuffix

	for i := start; i < end; i++ {
		r[i] = mask
	}

	return string(r)
}

func TrimStringToMaxLen(str string, maxLen int) string {
	if utf8.RuneCountInString(str) <= maxLen {
		return str
	}

	runes := []rune(str)
	if len(runes) > maxLen {
		runes = runes[:maxLen]
	}

	return string(runes)
}

func GenerateSecretKeyReports(s []model.SecretResult) (InteractiveMessage, error) {

	interactiveMsg := InteractiveMessage{
		MsgType: "interactive",
		Card: struct {
			Config   Config    `json:"config"`
			Header   Header    `json:"header"`
			Elements []Element `json:"elements"`
		}{
			Config: Config{
				WideScreenMode: true,
			},
			Header: Header{
				Template: "red",
				Title: Title{
					Tag:     "plain_text",
					Content: "[告警] 秘钥检测告警",
				},
			},
		},
	}

	if len(s) == 0 {
		return interactiveMsg, errors.New("empty key length")
	}

	elements := []Element{
		{
			Tag:     "markdown",
			Content: fmt.Sprintf("发现%s仓库存在秘钥泄露风险，新增秘钥%d个，请尽快确认处理", s[0].RepoLink, len(s)),
		},
		{
			Tag: "hr",
		},
	}

	for _, v := range s {
		content := fmt.Sprintf("**秘钥类型**: %s \n**秘钥内容**: %s \n**文件路径**: %s \n**文件行数**: %d行 \n"+
			"**commit**: %s \n**唯一ID**: %s \n**提交人**: %s \n**邮箱**: %s \n**提交时间**: %v \n---", v.RuleID, v.Secret, v.File, v.StartLine, v.Commit, v.VulUnqID, v.Author, v.Email, v.PushDate)
		e := Element{
			Content: content,
			Tag:     "markdown",
		}
		elements = append(elements, e)
	}

	interactiveMsg.Card.Elements = elements

	return interactiveMsg, nil
}
