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)
}

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\.com|tidb\.com|tidbcloud\.com|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, "26ba4f64", "新扫到以下漏洞(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 := &commonService{store: ps.store}
	t, err := comSrv.GetLarkToken(ctx, pchub)
	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:       "e359137a",
			OpenId:       "ou_560c70c17d7173f10747f40539eccc4e",
		}
		var resForm []FormItem
		resForm = append(resForm, FormItem{
			Id:    "widget16614938322000001",
			Type:  "radioV2",
			Value: "$i18n-l7a2g2r6-klab9dmr3kp-4",
		}, FormItem{
			Id:    "widget16614939701650001",
			Type:  "radioV2",
			Value: "l7a2lbta-cd8pmuoswpk-3",
		}, FormItem{
			Id:    "widget16487195633330001",
			Type:  "textarea",
			Value: cur.AlertMsg,
		}, FormItem{
			Id:    "widget16614941455820001",
			Type:  "textarea",
			Value: "Confirm whether the application is still in use and perform vulnerability fixes.",
		}, FormItem{
			Id:    "widget16487195406530001",
			Type:  "radioV2",
			Value: "l1et1ne6-wlhbvc5yoj-0",
		}, FormItem{
			Id:    "widget16614941687660001",
			Type:  "contact",
			Value: []string{"e359137a"},
		}, FormItem{
			Id:    "widget16614942163000001",
			Type:  "contact",
			Value: []string{"38gc8f4e", "966933ae"},
		}, FormItem{
			Id:    "widget16614942960990001",
			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.com") || strings.HasSuffix(domain, "tidb.com") || strings.HasSuffix(domain, "tidbcloud.com") || strings.HasSuffix(domain, "pingcap.cn") {
				particularDomainAlerts[domain] = rsItem
			} else {
				domainAlerts[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("The following domain names have opened port 3389: \n")
		qb.WriteString(qb3389.String())
		qb.WriteString("\n")
	}

	if qb80443.Len() > 0 {
		qb.WriteString("The following domain names have opened port 80 but have not opened port 443: \n")
		qb.WriteString(qb80443.String())
		qb.WriteString("\n")
	}

	if qbcert.Len() > 0 {
		qb.WriteString("The SSL certificates for the following domain names have expired or will expire within 20 days: \n")
		qb.WriteString(qbcert.String())
		qb.WriteString("\n")
	}

	if qb.Len() > 0 {
		ps.SendLarkMsg(ctx, "26ba4f64", 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 := &commonService{store: ps.store}

	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 := &commonService{store: ps.store}
	t, err := comSrv.GetLarkToken(ctx, pchub)
	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:       "e359137a",
				OpenId:       "ou_560c70c17d7173f10747f40539eccc4e",
			}

			var resForm []FormItem
			switch cur.Type {
			case PAType3389:
				resForm = append(resForm, FormItem{
					Id:    "widget16614938322000001",
					Type:  "radioV2",
					Value: "lbowqi7a-y72wsnjh59l-1",
				}, FormItem{
					Id:    "widget16614939701650001",
					Type:  "radioV2",
					Value: "lb0msap1-j5vnxm291kb-3",
				}, FormItem{
					Id:    "widget16487195633330001",
					Type:  "textarea",
					Value: cur.AlertMsg,
				}, FormItem{
					Id:    "widget16614941455820001",
					Type:  "textarea",
					Value: "Confirm whether the domain is still in use and whether the corresponding machine is still in use. If it is in use, is the opening of port 3389 reasonable? If it is unreasonable, close it. If it is reasonable, it needs to be documented clearly and inform about the risks. It is best to change to a different port to mitigate potential intrusions."}, FormItem{
					Id:    "widget16487195406530001",
					Type:  "radioV2",
					Value: "l1et1ne6-wlhbvc5yoj-0",
				}, FormItem{
					Id:    "widget16614941687660001",
					Type:  "contact",
					Value: []string{"e359137a"},
				}, FormItem{
					Id:    "widget16614942163000001",
					Type:  "contact",
					Value: []string{"38gc8f4e", "966933ae"},
				}, FormItem{
					Id:    "widget16614942960990001",
					Type:  "date",
					Value: time.Now().Add(time.Hour * 24 * 15).Format(time.RFC3339),
				})
			case PAType80443:
				resForm = append(resForm, FormItem{
					Id:    "widget16614938322000001",
					Type:  "radioV2",
					Value: "$i18n-l7a2g2r6-klab9dmr3kp-4",
				}, FormItem{
					Id:    "widget16614939701650001",
					Type:  "radioV2",
					Value: "lb0msap1-v4k71b1lffg-1",
				}, FormItem{
					Id:    "widget16487195633330001",
					Type:  "textarea",
					Value: cur.AlertMsg,
				}, FormItem{
					Id:    "widget16614941455820001",
					Type:  "textarea",
					Value: "It is recommended to add an HTTPS certificate.",
				}, FormItem{
					Id:    "widget16487195406530001",
					Type:  "radioV2",
					Value: "l1et1ne6-wlhbvc5yoj-0",
				}, FormItem{
					Id:    "widget16614941687660001",
					Type:  "contact",
					Value: []string{"e359137a"},
				}, FormItem{
					Id:    "widget16614942163000001",
					Type:  "contact",
					Value: []string{"38gc8f4e", "966933ae", "9c7ca79f"},
				}, FormItem{
					Id:    "widget16614942960990001",
					Type:  "date",
					Value: time.Now().Add(time.Hour * 24 * 15).Format(time.RFC3339),
				})
			case PATypeCert:
				resForm = append(resForm, FormItem{
					Id:    "widget16614938322000001",
					Type:  "radioV2",
					Value: "$i18n-l7a2g2r6-klab9dmr3kp-4",
				}, FormItem{
					Id:    "widget16614939701650001",
					Type:  "radioV2",
					Value: "lb0msap1-v4k71b1lffg-1",
				}, FormItem{
					Id:    "widget16487195633330001",
					Type:  "textarea",
					Value: cur.AlertMsg,
				}, FormItem{
					Id:    "widget16614941455820001",
					Type:  "textarea",
					Value: "Update the certificate to prevent expiration.",
				}, FormItem{
					Id:    "widget16487195406530001",
					Type:  "radioV2",
					Value: "l1et1ne6-wlhbvc5yoj-0",
				}, FormItem{
					Id:    "widget16614941687660001",
					Type:  "contact",
					Value: []string{"e359137a"},
				}, FormItem{
					Id:    "widget16614942163000001",
					Type:  "contact",
					Value: []string{"38gc8f4e", "966933ae", "9c7ca79f"},
				}, FormItem{
					Id:    "widget16614942960990001",
					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\nThe server has port 3389 open, which may be vulnerable to brute-force login attempts or other security risks.",
	}

	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 + "\nHas login services, but does not use HTTPS protocol, which may leak usernames and passwords during login.",
	}

	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 + "\nThe certificate is about to expire (<20 days), expiration date is: " + 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, "e359137a", "Failed to request security service token, please check the service status.")

		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, "e359137a", "The request for the security service token is empty, please check the service status.")

		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
}
