package v1

import (
	"bufio"
	"bytes"
	"context"
	"crypto/tls"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"regexp"
	"strings"
	"sync"
	"time"

	"github.com/spf13/viper"

	"github.com/pingcap-inc/security/internal/alert-server/store"
	v1 "github.com/pingcap-inc/security/model/v1"

	"github.com/pingcap-inc/pchub/pkg/errors"
	"github.com/pingcap-inc/pchub/pkg/log"
)

type portScanService struct {
	store store.Factory
}

type PortScanSrv interface {
	ScanPorts(ctx context.Context)
	ReportNucleiAlerts(ctx context.Context, alerts []*PortAlert)
}

var _ PortScanSrv = (*portScanService)(nil)

func newPortScan(srv *service) *portScanService {
	return &portScanService{store: srv.store}
}

func (ps *portScanService) ScanPorts(ctx context.Context) {
	log.L(ctx).Info("PortsScanSrv.ScanPorts called.")

	// err := ps.LegacyScanPorts(ctx)
	// if err != nil {
	// 	log.L(ctx).Errorf("error when call legacy scan-ports api, err: %s", err.Error())
	// }

	err := ps.NucleiScanPorts(ctx)
	if err != nil {
		log.L(ctx).Errorf("error when call nuclei scan-ports api, err: %s", err.Error())
	}
}

type NucleiScanPortsItem struct {
	ID         string `json:"id"`
	Xray       string `json:"xray"`
	Nuclei     string `json:"nuclei"`
	CreateTime string `json:"create_time"`
}

func (ps *portScanService) NucleiScanPorts(ctx context.Context) error {
	pres, err := ps.nucleiScanPorts(ctx)
	if err != nil {
		log.L(ctx).Errorf("error when call nuclei scan-ports api, err: %s", err.Error())

		return err
	}
	if len(pres) == 0 {
		log.L(ctx).Info("No list for nuclei scan-ports api.")

		return nil
	}
	portsRes := make([]NucleiScanPortsItem, 0)
	if err = json.Unmarshal(pres, &portsRes); err != nil {
		log.L(ctx).Errorf("error when parse nuclei scan-ports api response, err: %s", err.Error())

		return err
	}
	latestItem := portsRes[len(portsRes)-1]

	nucleiRes, err := ps.nucleiScanPortsResult(ctx, latestItem.ID)
	if err != nil {
		log.L(ctx).Errorf("error when call nuclei scan-ports-result api, err: %s", err.Error())

		return err
	}
	if strings.TrimSpace(nucleiRes) == "" {
		log.L(ctx).Info("No latest result for nuclei scan-ports-result api.")

		return nil
	}
	nucleiResParsed := parseNucleiPortsRes(nucleiRes)
	nucleiAlerts := make([]*PortAlert, 0)
	particularNucleiAlerts := make([]*PortAlert, 0)
	particularReg := regexp.MustCompile(`(pingcap\.cn)`)
	spaceReg := regexp.MustCompile(`\s+`)

	for _, nit := range nucleiResParsed {
		alertID := fmt.Sprintf("nuclei-%s-%s", spaceReg.ReplaceAllString(nit.URL, "_"), spaceReg.ReplaceAllString(nit.Leak, "_"))
		alertMsg := fmt.Sprintf("%s\n检测到了安全漏洞，可能会被暴力破解存在安全隐患\n漏洞级别：%s\n漏洞信息：%s", nit.URL, nit.Level, nit.Leak)
		pa := &PortAlert{
			Type:     PATypeNuclei,
			AlertID:  alertID,
			Domain:   nit.URL,
			AlertMsg: alertMsg,
		}
		if particularReg.Match([]byte(nit.URL)) {
			particularNucleiAlerts = append(particularNucleiAlerts, pa)
		} else {
			nucleiAlerts = append(nucleiAlerts, pa)
		}
	}

	ps.ReportNucleiParticularDomainAlerts(ctx, particularNucleiAlerts)
	ps.ReportNucleiAlerts(ctx, nucleiAlerts)

	log.L(ctx).Info("PortsScanSrv.NucleiScanPorts done.")

	return nil
}

func (ps *portScanService) ReportNucleiParticularDomainAlerts(ctx context.Context, alerts []*PortAlert) {
	log.L(ctx).Info("report nuclei particular domain alerts")

	alertIDs := make([]string, 0)
	for _, pa := range alerts {
		alertIDs = append(alertIDs, pa.AlertID)
	}
	alertsInDB, err := ps.store.Alert().MGet30DByAlertIDs(ctx, alertIDs)
	if err != nil {
		log.L(ctx).Errorf("error when call AlertStore.MGetByAlertIDs, err: %s", err.Error())

		return
	}
	alertsExistMap := make(map[string]*v1.Alert)
	for _, ad := range alertsInDB {
		cur := ad
		alertsExistMap[ad.AlertID] = cur
	}
	qb := strings.Builder{}
	newAlerts := make([]*v1.Alert, 0)
	for _, pa := range alerts {
		_, ok := alertsExistMap[pa.AlertID]
		if ok {
			continue
		}
		newAlerts = append(newAlerts, &v1.Alert{
			AlertID:   pa.AlertID,
			CreatedAt: time.Now(),
		})
		qb.WriteString(pa.AlertMsg)
		qb.WriteString("\n\n")
	}
	if qb.Len() > 0 {
		ps.SendLarkMsg(ctx, primaryNotificationLarkUserID, "新扫到以下漏洞(Nuclei):\n\n"+qb.String())
	}
	if len(newAlerts) > 0 {
		err = ps.store.Alert().MCreate(ctx, newAlerts)
		if err != nil {
			log.L(ctx).Errorf("error when insert alerts into db, err: %s", err.Error())

			return
		}
	}
}

func (ps *portScanService) ReportNucleiAlerts(ctx context.Context, alerts []*PortAlert) {
	log.L(ctx).Info("report nuclei internal domain alerts")

	comSrv := newCommon()
	t, err := comSrv.GetLarkToken(ctx, 1)
	if err != nil {
		log.L(ctx).Errorf("error when call CommonSrv.GetLarkToken, err: %s", err.Error())

		return
	}
	alertIDs := make([]string, 0)
	for _, a := range alerts {
		alertIDs = append(alertIDs, a.AlertID)
	}
	alertsInDB, err := ps.store.Alert().MGet30DByAlertIDs(ctx, alertIDs)
	if err != nil {
		log.L(ctx).Errorf("error when call AlertStore.MGetByAlertIDs, err: %s", err.Error())

		return
	}
	alertsExistMap := make(map[string]*v1.Alert)
	for _, ad := range alertsInDB {
		cur := ad
		alertsExistMap[ad.AlertID] = cur
	}
	newAlerts := make([]*v1.Alert, 0)
	for _, alert := range alerts {
		cur := alert
		_, ok := alertsExistMap[cur.AlertID]
		if ok {
			continue
		}
		nAlert := &v1.Alert{
			AlertID:   cur.AlertID,
			CreatedAt: time.Now(),
		}
		res := LarkCreateApprovalBody{
			ApprovalCode: phishingApprovalCode,
			UserId:       primaryNotificationLarkUserID,
			OpenId:       primaryNotificationLarkOpenID,
		}
		var resForm []FormItem
		resForm = append(resForm, FormItem{
			Id:    v1.SafeType,
			Type:  "radioV2",
			Value: v1.AppSafe,
		}, FormItem{
			Id:    v1.Assets,
			Type:  "radioV2",
			Value: v1.Application,
		}, FormItem{
			Id:    v1.ProblemDescription,
			Type:  "textarea",
			Value: cur.AlertMsg,
		}, FormItem{
			Id:    v1.ImprovementSuggestion,
			Type:  "textarea",
			Value: "确认应用是否还在使用，请进行漏洞修复。",
		}, FormItem{
			Id:    v1.EmergencyLevel,
			Type:  "radioV2",
			Value: v1.Normal,
		}, FormItem{
			Id:    v1.ResponsiblePerson,
			Type:  "contact",
			Value: []string{primaryNotificationLarkUserID},
		}, FormItem{
			Id:    v1.Collaborators,
			Type:  "contact",
			Value: []string{},
		}, FormItem{
			Id:    v1.RectificationTime,
			Type:  "date",
			Value: time.Now().Add(time.Hour * 24 * 15).Format(time.RFC3339),
		})
		resStr, err := json.Marshal(&resForm)
		if err != nil {
			log.L(ctx).Errorf("can not marshal form: %s", err.Error())

			continue
		}
		res.Form = string(resStr)
		reqb, err := json.Marshal(&res)
		if err != nil {
			log.L(ctx).Errorf("error when marshal approval request body, err: %s", err.Error())

			continue
		}
		approvalR, err := comSrv.GetDataByReqLarkAPI(ctx, "POST", LarkOpenAPIBase+"/approval/v4/instances", bytes.NewReader(reqb), nil, t)
		if err != nil {
			log.L(ctx).Errorf("error when submit lark approval, err: %s", err.Error())

			continue
		}
		var apprData ApprovalLarkResData
		if err := json.Unmarshal(approvalR, &apprData); err != nil {
			log.L(ctx).Errorf("error when unmarshal approval res data, err: %s", err.Error())

			continue
		}
		nAlert.LarkInstanceCode = apprData.InstanceCode
		newAlerts = append(newAlerts, nAlert)
	}

	if len(newAlerts) > 0 {
		err = ps.store.Alert().MCreate(ctx, newAlerts)
		if err != nil {
			log.L(ctx).Errorf("error when insert alerts & approvals into db, err: %s", err.Error())

			return
		}
	}
}

func (ps *portScanService) LegacyScanPorts(ctx context.Context) error {
	pres, err := ps.scanPorts(ctx)
	if err != nil {
		log.L(ctx).Errorf("error when call scan-ports api, err: %s", err.Error())

		return err
	}
	portsRes := parsePortsRes(pres)
	domainAlerts := make(map[string][]*PortAlert)
	particularDomainAlerts := make(map[string][]*PortAlert)

	var wg sync.WaitGroup
	sem := make(chan struct{}, 10)
	rss := make(chan []*PortAlert)

	for domain, ports := range portsRes {
		wg.Add(1)
		go func(domain string, ports map[string]bool) {
			sem <- struct{}{}
			defer func() { <-sem }()
			defer wg.Done()

			rss <- ps.CheckPorts(ctx, domain, ports)
		}(domain, ports)
	}

	go func() {
		wg.Wait()
		close(rss)
	}()

	for rsItem := range rss {
		if len(rsItem) > 0 {
			domain := rsItem[0].Domain
			if strings.HasSuffix(domain, "pingcap.cn") {
				particularDomainAlerts[domain] = rsItem
			}
		}
	}

	ps.ReportParticularDomainAlerts(ctx, particularDomainAlerts)
	ps.ReportDomainAlerts(ctx, domainAlerts)

	log.L(ctx).Info("PortsScanSrv.LegacyScanPorts done.")

	return nil
}

type LarkMsg struct {
	ReceiveID string `json:"receive_id"`
	MsgType   string `json:"msg_type"`
	Content   string `json:"content"`
}

func (ps *portScanService) ReportParticularDomainAlerts(ctx context.Context, alerts map[string][]*PortAlert) {
	log.L(ctx).Info("report particular domain alerts")

	alertIDs := make([]string, 0)
	for _, pas := range alerts {
		for _, pa := range pas {
			alertIDs = append(alertIDs, pa.AlertID)
		}
	}
	alertsInDB, err := ps.store.Alert().MGet30DByAlertIDs(ctx, alertIDs)
	if err != nil {
		log.L(ctx).Errorf("error when call AlertStore.MGetByAlertIDs, err: %s", err.Error())

		return
	}
	alertsExistMap := make(map[string]*v1.Alert)
	for _, ad := range alertsInDB {
		cur := ad
		alertsExistMap[ad.AlertID] = cur
	}

	qb := strings.Builder{}
	qb3389 := strings.Builder{}
	qb80443 := strings.Builder{}
	qbcert := strings.Builder{}

	newAlerts := make([]*v1.Alert, 0)
	for _, pas := range alerts {
		for _, pa := range pas {
			cur := pa
			_, ok := alertsExistMap[pa.AlertID]
			if ok {
				continue
			}
			newAlerts = append(newAlerts, &v1.Alert{
				AlertID:   pa.AlertID,
				CreatedAt: time.Now(),
			})

			switch cur.Type {
			case PAType3389:
				qb3389.WriteString(pa.Domain)
				qb3389.WriteString("\n")
			case PAType80443:
				qb80443.WriteString(pa.Domain)
				qb80443.WriteString("\n")
			case PATypeCert:
				qbcert.WriteString(pa.Domain)
				qbcert.WriteString("\n")
			}
		}
	}

	if qb3389.Len() > 0 {
		qb.WriteString("以下域名开放了 3389 端口: \n")
		qb.WriteString(qb3389.String())
		qb.WriteString("\n")
	}

	if qb80443.Len() > 0 {
		qb.WriteString("以下域名开放了 80 端口但未开放 443 端口: \n")
		qb.WriteString(qb80443.String())
		qb.WriteString("\n")
	}

	if qbcert.Len() > 0 {
		qb.WriteString("以下域名的 SSL 证书已过期或将在 20 天内过期: \n")
		qb.WriteString(qbcert.String())
		qb.WriteString("\n")
	}

	if qb.Len() > 0 {
		ps.SendLarkMsg(ctx, primaryNotificationLarkUserID, qb.String())
	}

	if len(newAlerts) > 0 {
		err = ps.store.Alert().MCreate(ctx, newAlerts)
		if err != nil {
			log.L(ctx).Errorf("error when insert alerts into db, err: %s", err.Error())

			return
		}
	}
}

func (ps *portScanService) SendLarkMsg(ctx context.Context, receiveID, msg string) {
	cs := newCommon()

	cont, err := json.Marshal(struct {
		Text string `json:"text"`
	}{
		Text: msg,
	})
	if err != nil {
		log.L(ctx).Errorf("error when marshal lark msg, err: %s", err.Error())

		return
	}

	reqb, err := json.Marshal(&LarkMsg{
		ReceiveID: receiveID,
		MsgType:   "text",
		Content:   string(cont),
	})

	if err != nil {
		log.L(ctx).Errorf("error when marshal approval request body, err: %s", err.Error())

		return
	}
	_, err = cs.GetDataByReqLarkAPI(ctx, "POST", LarkOpenAPIBase+"/im/v1/messages?receive_id_type=user_id", bytes.NewReader(reqb), nil, "")
	if err != nil {
		log.L(ctx).Errorf("error when call lark api, err: %s", err.Error())

		return
	}
}

func (ps *portScanService) ReportDomainAlerts(ctx context.Context, alerts map[string][]*PortAlert) {
	log.L(ctx).Info("report internal domain alerts")

	comSrv := newCommon()
	t, err := comSrv.GetLarkToken(ctx, 1)
	if err != nil {
		log.L(ctx).Errorf("error when call CommonSrv.GetLarkToken, err: %s", err.Error())

		return
	}

	alertIDs := make([]string, 0)
	for _, pas := range alerts {
		for _, pa := range pas {
			alertIDs = append(alertIDs, pa.AlertID)
		}
	}
	alertsInDB, err := ps.store.Alert().MGet30DByAlertIDs(ctx, alertIDs)
	if err != nil {
		log.L(ctx).Errorf("error when call AlertStore.MGetByAlertIDs, err: %s", err.Error())

		return
	}
	alertsExistMap := make(map[string]*v1.Alert)
	for _, ad := range alertsInDB {
		cur := ad
		alertsExistMap[ad.AlertID] = cur
	}

	newAlerts := make([]*v1.Alert, 0)
	for _, pas := range alerts {
		for _, pa := range pas {
			cur := pa
			_, ok := alertsExistMap[pa.AlertID]
			if ok {
				continue
			}
			nAlert := &v1.Alert{
				AlertID:   pa.AlertID,
				CreatedAt: time.Now(),
			}

			res := LarkCreateApprovalBody{
				ApprovalCode: phishingApprovalCode,
				UserId:       primaryNotificationLarkUserID,
				OpenId:       primaryNotificationLarkOpenID,
			}

			var resForm []FormItem
			switch cur.Type {
			case PAType3389:
				resForm = append(resForm, FormItem{
					Id:    v1.SafeType,
					Type:  "radioV2",
					Value: v1.HostSafe,
				}, FormItem{
					Id:    v1.Assets,
					Type:  "radioV2",
					Value: v1.Port,
				}, FormItem{
					Id:    v1.ProblemDescription,
					Type:  "textarea",
					Value: cur.AlertMsg,
				}, FormItem{
					Id:    v1.ImprovementSuggestion,
					Type:  "textarea",
					Value: "确认域名是否还在使用，域名对应的机器是否还在使用 如果使用的话3389开放是否是合理的？ 不合理的话进行关闭。合理的话需要记录清楚并且告知风险，最好还能换个端口来规避入侵。",
				}, FormItem{
					Id:    v1.EmergencyLevel,
					Type:  "radioV2",
					Value: v1.Normal,
				}, FormItem{
					Id:    v1.ResponsiblePerson,
					Type:  "contact",
					Value: []string{primaryNotificationLarkUserID},
				}, FormItem{
					Id:    v1.Collaborators,
					Type:  "contact",
					Value: []string{},
				}, FormItem{
					Id:    v1.RectificationTime,
					Type:  "date",
					Value: time.Now().Add(time.Hour * 24 * 15).Format(time.RFC3339),
				})
			case PAType80443:
				resForm = append(resForm, FormItem{
					Id:    v1.SafeType,
					Type:  "radioV2",
					Value: v1.AppSafe,
				}, FormItem{
					Id:    v1.Assets,
					Type:  "radioV2",
					Value: v1.Domain,
				}, FormItem{
					Id:    v1.ProblemDescription,
					Type:  "textarea",
					Value: cur.AlertMsg,
				}, FormItem{
					Id:    v1.ImprovementSuggestion,
					Type:  "textarea",
					Value: "建议添加https证书",
				}, FormItem{
					Id:    v1.EmergencyLevel,
					Type:  "radioV2",
					Value: v1.Normal,
				}, FormItem{
					Id:    v1.ResponsiblePerson,
					Type:  "contact",
					Value: []string{primaryNotificationLarkUserID},
				}, FormItem{
					Id:    v1.Collaborators,
					Type:  "contact",
					Value: []string{},
				}, FormItem{
					Id:    v1.RectificationTime,
					Type:  "date",
					Value: time.Now().Add(time.Hour * 24 * 15).Format(time.RFC3339),
				})
			case PATypeCert:
				resForm = append(resForm, FormItem{
					Id:    v1.SafeType,
					Type:  "radioV2",
					Value: v1.AppSafe,
				}, FormItem{
					Id:    v1.Assets,
					Type:  "radioV2",
					Value: v1.Domain,
				}, FormItem{
					Id:    v1.ProblemDescription,
					Type:  "textarea",
					Value: cur.AlertMsg,
				}, FormItem{
					Id:    v1.ImprovementSuggestion,
					Type:  "textarea",
					Value: "更新证书，防止过期",
				}, FormItem{
					Id:    v1.EmergencyLevel,
					Type:  "radioV2",
					Value: v1.Normal,
				}, FormItem{
					Id:    v1.ResponsiblePerson,
					Type:  "contact",
					Value: []string{primaryNotificationLarkUserID},
				}, FormItem{
					Id:    v1.Collaborators,
					Type:  "contact",
					Value: []string{},
				}, FormItem{
					Id:    v1.RectificationTime,
					Type:  "date",
					Value: time.Now().Add(time.Hour * 24 * 15).Format(time.RFC3339),
				})
			}

			resStr, err := json.Marshal(&resForm)
			if err != nil {
				log.L(ctx).Errorf("can not marshal form: %s", err.Error())

				continue
			}
			res.Form = string(resStr)
			reqb, err := json.Marshal(&res)
			if err != nil {
				log.L(ctx).Errorf("error when marshal approval request body, err: %s", err.Error())

				continue
			}

			approvalR, err := comSrv.GetDataByReqLarkAPI(ctx, "POST", LarkOpenAPIBase+"/approval/v4/instances", bytes.NewReader(reqb), nil, t)
			if err != nil {
				log.L(ctx).Errorf("error when submit lark approval, err: %s", err.Error())

				continue
			}

			var apprData ApprovalLarkResData
			if err := json.Unmarshal(approvalR, &apprData); err != nil {
				log.L(ctx).Errorf("error when unmarshal approval res data, err: %s", err.Error())

				continue
			}
			nAlert.LarkInstanceCode = apprData.InstanceCode
			newAlerts = append(newAlerts, nAlert)
		}
	}

	if len(newAlerts) > 0 {
		err = ps.store.Alert().MCreate(ctx, newAlerts)
		if err != nil {
			log.L(ctx).Errorf("error when insert alerts & approvals into db, err: %s", err.Error())

			return
		}
	}
}

const (
	PAType3389   = "3389"
	PAType80443  = "80443"
	PATypeCert   = "cert"
	PATypeNuclei = "nuclei"
)

type PortAlert struct {
	Type     string `json:"type"`
	AlertID  string `json:"alertID"`
	Domain   string `json:"domain"`
	AlertMsg string `json:"alertMsg"`
}

func (ps *portScanService) CheckPorts(ctx context.Context, domain string, ports map[string]bool) []*PortAlert {
	res := make([]*PortAlert, 0)

	if _, ok := ports["3389"]; ok {
		pa := ps.HandlePort3389(ctx, domain)
		res = append(res, pa)
	}
	_, has80 := ports["80"]
	_, has443 := ports["443"]
	if has80 && !has443 {
		pa := ps.HandlePortNoSSL(ctx, domain)
		res = append(res, pa)
	}

	if has443 {
		pa := ps.CheckSSLCert(ctx, domain)
		if pa != nil {
			res = append(res, pa)
		}
	}

	return res
}

func (ps *portScanService) HandlePort3389(ctx context.Context, domain string) *PortAlert {
	log.L(ctx).Infof("domain %s open the port 3389", domain)

	res := &PortAlert{
		Type:     PAType3389,
		AlertID:  "port-" + domain + "-3389",
		Domain:   domain,
		AlertMsg: domain + ":3389\n服务器开启了3389端口，可能会被暴力破解登陆或存在安全隐患",
	}

	return res
}

func (ps *portScanService) HandlePortNoSSL(ctx context.Context, domain string) *PortAlert {
	log.L(ctx).Infof("domain %s open the port 80 but not open 443", domain)

	res := &PortAlert{
		Type:     PAType80443,
		AlertID:  "port-" + domain + "-80443",
		Domain:   domain,
		AlertMsg: domain + "\n具有登陆服务，但未使用https协议，会泄漏登陆时的用户名和密码",
	}

	return res
}

func (ps *portScanService) CheckSSLCert(ctx context.Context, domain string) *PortAlert {
	client := &http.Client{
		Transport: &http.Transport{
			TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
		},
		Timeout: 10 * time.Second,
	}
	resp, err := client.Get("https://" + domain)
	if err != nil {
		return nil
	}
	defer func() { _ = resp.Body.Close() }()

	for _, cert := range resp.TLS.PeerCertificates {
		if cert.NotAfter.Before(time.Now().Add(20 * 24 * time.Hour)) {
			log.L(ctx).Infof("domain %s ssl cert will expire in 20 days or has expired", domain)

			res := &PortAlert{
				Type:     PATypeCert,
				AlertID:  "port-" + domain + "-cert-expire",
				Domain:   domain,
				AlertMsg: domain + "\n证书快要过期了(<20day)，过期时间为：" + cert.NotAfter.Format("2006-01-02"),
			}

			return res
		}
	}

	return nil
}

func parsePortsRes(pres string) map[string]map[string]bool {
	rd := bufio.NewReader(bytes.NewBufferString(pres))
	portsRes := make(map[string]map[string]bool)
	for {
		line, err := rd.ReadString('\n')

		if err != nil || io.EOF == err {
			if line == "" {
				break
			}
		}

		dp := strings.Split(line, ":")
		if len(dp) != 2 {
			continue
		}
		domain := dp[0]
		port := strings.ReplaceAll(dp[1], "\n", "")

		if _, ok := portsRes[domain]; !ok {
			portsRes[domain] = make(map[string]bool)
		}
		portsRes[domain][port] = true
	}

	return portsRes
}

func (ps *portScanService) scanPorts(ctx context.Context) (string, error) {
	tk, err := ps.GetToken(ctx)
	if err != nil {
		return "", err
	}

	bd, _ := json.Marshal(map[string]string{ //nolint:errchkjson // must Marshal
		"type": "ports",
		"name": "PingCAP",
	})
	reqBody := bytes.NewBuffer(bd)
	BaseURL := viper.GetString("portscan.host")
	req, _ := http.NewRequestWithContext(ctx, http.MethodPost, BaseURL+"/v1/scan/result", reqBody)
	req.Header.Set("token", tk)
	resp, err := (&http.Client{}).Do(req)
	if err != nil {
		err := errors.Wrap(err, "error when scan ports")
		log.L(ctx).Error(err.Error())

		return "", err
	}
	defer func() {
		_ = resp.Body.Close()
	}()
	pres, _ := io.ReadAll(resp.Body)

	return string(pres), nil
}

func (ps *portScanService) GetToken(ctx context.Context) (string, error) {
	UName := viper.GetString("portscan.user")
	Pwd := viper.GetString("portscan.passwd")
	BaseURL := viper.GetString("portscan.host")
	bd, _ := json.Marshal(map[string]string{ //nolint:errchkjson // must Marshal
		"username": UName,
		"password": Pwd,
	})
	tokenReqBody := bytes.NewBuffer(bd)
	tokenReq, _ := http.NewRequestWithContext(ctx, http.MethodPost, BaseURL+"/v1/user/token", tokenReqBody)
	tokenResp, err := (&http.Client{}).Do(tokenReq)
	if err != nil {
		err := errors.Wrap(err, "error when request token")
		log.L(ctx).Error(err.Error())
		ps.SendLarkMsg(ctx, primaryNotificationLarkUserID, "请求安全服务token失败, 请检查服务状态")

		return "", err
	}
	defer func() {
		_ = tokenResp.Body.Close()
	}()
	tokenRespBody, _ := io.ReadAll(tokenResp.Body)
	var tokenRes struct {
		Token string `json:"token"`
	}
	if len(tokenRespBody) == 0 {
		err := errors.New("token response body is empty")
		log.L(ctx).Error(err.Error())
		ps.SendLarkMsg(ctx, primaryNotificationLarkUserID, "请求安全服务token为空, 请检查服务状态")

		return "", err
	}
	if err := json.Unmarshal(tokenRespBody, &tokenRes); err != nil {
		log.L(ctx).Errorf("Error when unmarshal token res: %s", err.Error())

		return "", errors.Wrap(err, "error when unmarshal token res")
	}
	if tokenRes.Token == "" {
		err := errors.New("token is empty")
		log.L(ctx).Error(err.Error())

		return "", err
	}

	return tokenRes.Token, nil
}

func (ps *portScanService) nucleiScanPorts(ctx context.Context) ([]byte, error) {
	tk, err := ps.GetToken(ctx)
	if err != nil {
		return nil, err
	}
	bd, _ := json.Marshal(map[string]string{ //nolint:errchkjson // must Marshal
		"type": "vul",
		"name": "PingCAP",
		"tool": "nuclei",
		"dump": "list",
	})
	reqBody := bytes.NewBuffer(bd)
	BaseURL := viper.GetString("portscan.host")
	req, _ := http.NewRequestWithContext(ctx, http.MethodPost, BaseURL+"/v1/scan/result", reqBody)
	req.Header.Set("token", tk)
	resp, err := (&http.Client{}).Do(req)
	if err != nil {
		err := errors.Wrap(err, "error when scan ports")
		log.L(ctx).Error(err.Error())

		return nil, err
	}
	defer func() {
		_ = resp.Body.Close()
	}()
	pres, _ := io.ReadAll(resp.Body)

	return pres, nil
}

func (ps *portScanService) nucleiScanPortsResult(ctx context.Context, nid string) (string, error) {
	tk, err := ps.GetToken(ctx)
	if err != nil {
		return "", err
	}
	bd, _ := json.Marshal(map[string]string{ //nolint:errchkjson // must Marshal
		"type": "vul",
		"name": "PingCAP",
		"tool": "nuclei",
		"dump": nid,
	})
	reqBody := bytes.NewBuffer(bd)
	BaseURL := viper.GetString("portscan.host")
	req, _ := http.NewRequestWithContext(ctx, http.MethodPost, BaseURL+"/v1/scan/result", reqBody)
	req.Header.Set("token", tk)
	resp, err := (&http.Client{}).Do(req)
	if err != nil {
		err := errors.Wrap(err, "error when get result for nuclei scan ports, id: "+nid)
		log.L(ctx).Error(err.Error())

		return "", err
	}
	defer func() {
		_ = resp.Body.Close()
	}()
	pres, _ := io.ReadAll(resp.Body)

	return string(pres), nil
}

type NucleiResItem struct {
	URL   string `json:"url"`
	Type  string `json:"type"`
	Leak  string `json:"leak"`
	Level string `json:"level"`
}

func parseNucleiPortsRes(pres string) []NucleiResItem {
	rd := bufio.NewReader(bytes.NewBufferString(pres))
	res := make([]NucleiResItem, 0)
	for {
		line, err := rd.ReadString('\n')

		if err != nil || io.EOF == err {
			if line == "" {
				break
			}
		}
		line = strings.ReplaceAll(line, "\n", "")
		parseReg := regexp.MustCompile(`^\[([^]])]\s+\[([^]])]\s+\[([^]])]\s+\[([^]])]\s+(.*)$`)
		matches := parseReg.FindStringSubmatch(line)

		if len(matches) != 6 {
			continue
		}

		nit := NucleiResItem{
			URL:   matches[5],
			Leak:  matches[2],
			Level: matches[4],
			Type:  matches[3],
		}

		res = append(res, nit)
	}

	return res
}
