package v1

import (
	"bytes"
	"context"
	"encoding/json"
	"math/rand"
	"sort"
	"strconv"
	"strings"
	"time"

	"github.com/spf13/viper"
	"gorm.io/driver/mysql"
	"gorm.io/gorm"

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

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

type downloadAlertService struct {
	store store.Factory
}

type DownloadAlertSrv interface {
	RetrieveEvent(ctx context.Context) error
	SaveMockData()
}

var _ DownloadAlertSrv = (*downloadAlertService)(nil)

func newDownloadAlert(srv *service) *downloadAlertService {
	return &downloadAlertService{store: srv.store}
}

func (d *downloadAlertService) RetrieveEvent(ctx context.Context) error {
	log.L(ctx).Info("start to retrieve event")

	events, err := d.exportDownloadEvent(ctx)
	if err != nil {
		log.L(ctx).Errorf("exportDownloadEvent error: %v", err)

		return err
	}

	for _, event := range events {
		bucket.AddItem(event)
	}
	yesterday := time.Now().AddDate(0, 0, -1)
	lastHour := time.Now().Add(-time.Hour)
	lastTen := time.Now().Add(-time.Minute * 10)
	bucket.RemoveExpired("", yesterday)

	over4, over10, over20 := d.GetAlertList(yesterday, lastHour, lastTen)
	if err != nil {
		return err
	}
	go d.SendAlert(ctx, over4, 0, lastTen)
	go d.SendAlert(ctx, over10, 1, lastHour)
	go d.SendAlert(ctx, over20, 2, yesterday)

	log.L(ctx).Info("retrieve event success")

	return nil
}

func (d *downloadAlertService) exportDownloadEvent(ctx context.Context) ([]Event, error) {
	log.L(ctx).Info("exportDownloadEvent Called.")

	now := time.Now()
	// 计算1小时之前的时间
	oneHourAgo := now.Add(-10 * time.Minute)
	// 获取1小时之前的时间戳（以秒为单位）
	timestamp := oneHourAgo.Unix()

	params := make(map[string]string)
	params["oldest"] = strconv.FormatInt(timestamp, 10)
	params["user_id_type"] = "user_id"
	params["event_name"] = "space_export_doc"
	params["page_size"] = "200"
	exportEvent, err := EventRequest(ctx, params)
	if err != nil {
		log.L(ctx).Errorf("EventRequest error: %v", err)

		return nil, err
	}

	params["event_name"] = "space_download_file"
	downloadEvent, err := EventRequest(ctx, params)
	if err != nil {
		log.L(ctx).Errorf("EventRequest error: %v", err)

		return nil, err
	}

	eventLogs := append(exportEvent, downloadEvent...)

	treeEvents := make([]Event, 0)

	for _, event := range eventLogs {

		treeEvent := Event{}

		sensitive := 2
		for _, cmInfo := range event.CommonDrawers.CommonDrawInfoList {
			if cmInfo.InfoKey == "ccm" { // document information
				var dInfoList []DrawerInfo
				err = json.Unmarshal([]byte(cmInfo.InfoVal), &dInfoList)
				if err != nil {
					log.L(ctx).Errorf("json.Unmarshal error: %v", err)

					return nil, err
				}

				for _, dInfo := range dInfoList {
					if dInfo.Key == "title" {
						treeEvent.DocTitle = dInfo.Value
					}
					if dInfo.Key == "sec_label" && (strings.Contains(dInfo.Value, "L3") || strings.Contains(dInfo.Value, "L4")) {
						if strings.Contains(dInfo.Value, "L3") {
							sensitive = 3
						} else {
							sensitive = 4
						}
					}
				}
			}
		}

		treeEvent.Actor = event.OperatorValue
		treeEvent.Time = time.Unix(event.EventTime, 0)
		treeEvent.EventID = event.UniqueID

		if sensitive > 2 { // process sensitive event
			go d.processSensitiveEvent(ctx, treeEvent, sensitive)

			continue // skip sensitive event
		}

		treeEvents = append(treeEvents, treeEvent)
	}

	log.L(ctx).Info("exportDownloadEvent end.")

	return treeEvents, nil
}

func (d *downloadAlertService) processSensitiveEvent(ctx context.Context, event Event, sensitiveLevel int) {
	log.L(ctx).Infof("processSensitiveEvent Called. event: %v level: %v", event, sensitiveLevel)

	rds, err := d.store.DownLoadAlert().GetByUserAndType(ctx, []string{event.Actor}, sensitiveLevel)
	if err != nil && !strings.Contains(err.Error(), "not found") {
		log.L(ctx).Errorf("store.DownLoadAlert().GetByUserAndType error: %v", err)

		return
	}

	if len(rds) > 0 {
		for _, rd := range rds {
			if rd.LastTime.After(time.Now().Add(-12 * time.Minute)) {
				return
			}
		}
	}

	userInfo, err := d.FetchUserInfo(ctx, event.Actor)
	if err != nil {
		log.L(ctx).Errorf("FetchUserInfo error: %v", err)

		return
	}
	responsible, collaborators := firstDownloadAlertRecipients(userInfo.UserID)
	apprMsg := SecApprMsg{
		SafeType:      v1.DataSafe,
		Assets:        v1.Document,
		Suggestion:    "用户下载的敏感数据，请确认是否符合数据安全规范",
		EmerLevel:     v1.Normal,
		RespPerson:    []string{responsible},
		Collaborators: collaborators,
		PromDesc:      "用户 " + userInfo.Name + "(" + userInfo.Email + ") 下载了L" + strconv.Itoa(sensitiveLevel) + "的文件《" + event.DocTitle + "》",
	}

	err = SendLarkAlert(ctx, apprMsg)
	if err != nil {
		log.L(ctx).Errorf("SendLarkAlert error: %v", err)
	}

	err = d.store.DownLoadAlert().Create(ctx, v1.DownLoadAlert{
		User:      event.Actor,
		AlertType: sensitiveLevel,
		CreatedAt: time.Now(),
		DocTitle:  event.DocTitle,
	})
	if err != nil {
		log.L(ctx).Errorf("store.DownLoadAlert().Create error: %v", err)

		return
	}
}

type UserInfo struct {
	Name   string `json:"name"`
	Email  string `json:"email"`
	HRBP   string `json:"hrbp"`
	Leader string `json:"leader"`
	UserID string `json:"user_id"`
}

func (d *downloadAlertService) FetchUserInfo(ctx context.Context, userid string) (*UserInfo, error) {
	log.L(ctx).Infof("FetchUserInfo Called. userid: %v", userid)

	comSrv := newCommon()
	tk, _ := comSrv.GetLarkToken(ctx, 1)

	params := make(map[string]string)
	params["user_id_type"] = "user_id"

	resp, err := comSrv.GetDataByReqLarkAPI(ctx, "GET", LarkOpenAPIBase+"/contact/v3/users/"+userid, nil, params, tk)
	if err != nil {
		log.L(ctx).Errorf("GetDataByReqLarkAPI error: %v", err)

		return nil, err
	}

	var userResp struct {
		User struct {
			LeaderUserID    string `json:"leader_user_id"`
			Name            string `json:"name"`
			EnterpriseEmail string `json:"enterprise_email"`
			CustomAttrs     []struct {
				ID    string      `json:"id"`
				Type  string      `json:"type"`
				Value interface{} `json:"value"`
			} `json:"custom_attrs"`
		} `json:"user"`
	}

	if err = json.Unmarshal(resp, &userResp); err != nil {
		log.L(ctx).Errorf("json.Unmarshal error: %v", err)

		return nil, err
	}

	userInfo := &UserInfo{}

	for _, attr := range userResp.User.CustomAttrs {
		if attr.ID == "C-7356519611520073732" {
			genericUser, _ := attr.Value.(map[string]interface{})
			if genericUser == nil {
				continue
			}
			HRBPID, _ := genericUser["generic_user"].(map[string]interface{})
			if HRBPID != nil {
				userInfo.HRBP = HRBPID["id"].(string)
			}
		}
	}

	userInfo.Leader = userResp.User.LeaderUserID
	userInfo.Name = userResp.User.Name
	userInfo.Email = userResp.User.EnterpriseEmail
	userInfo.UserID = userid

	return userInfo, nil
}

func (d *downloadAlertService) SendAlert(ctx context.Context, users []string, alertType int, timeLevel time.Time) {
	if len(users) == 0 {
		return
	}
	var lastAlertTime time.Time
	switch alertType {
	case 0:
		lastAlertTime = time.Now().Add(-time.Minute * 10)
	case 1:
		lastAlertTime = time.Now().Add(-time.Hour)
	case 2:
		lastAlertTime = time.Now().AddDate(0, 0, -1)
	}
	records, err := d.store.DownLoadAlert().GetByUserAndType(ctx, users, alertType)
	if err != nil {
		log.L(ctx).Errorf("store.Alert().Create error: %v", err)
	}

	notSend := make(map[string]int)

	for _, record := range records {
		if record.LastTime.After(lastAlertTime) {
			notSend[record.User] = 1
		}
	}

	sendChan := make(chan int, 10)
	for _, user := range users {
		// if the user has been alerted in the last x minutes, skip
		if _, ok := notSend[user]; ok {
			continue
		}
		sendChan <- 1
		go func(u string) {
			defer func() {
				<-sendChan
			}()

			docs := bucket.GetLatestDocs(u, timeLevel)

			// send lark alert
			err = d.SendLark(ctx, u, alertType, docs)
			if err != nil {
				return
			}

			// clear the record in 1 day
			if alertType == 2 {
				bucket.RemoveExpired(u, time.Now())
			}

			err = d.store.DownLoadAlert().Create(ctx, v1.DownLoadAlert{
				User:      u,
				AlertType: alertType,
				CreatedAt: time.Now(),
				DocTitle:  strings.Join(docs, ","),
			})
			if err != nil {
				log.L(ctx).Errorf("store.Alert().Create error: %v", err)

				return
			}
		}(user)
	}
}

func (d *downloadAlertService) SendLark(ctx context.Context, user string, alertType int, docs []string) error {
	if strings.TrimSpace(user) == "" {
		log.L(ctx).Warnf("user is empty")

		return errors.New("user is empty")
	}
	comSrv := newCommon()
	b, err := d.getApprovalReqBody(ctx, user, alertType, docs)
	if err != nil {
		log.L(ctx).Errorf("error when PhishingAlertSrv.getApprovalReqBody, err: %s", err.Error())

		return err
	}

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

		return err
	}

	newToken, err := comSrv.GetLarkToken(ctx, 1)
	if err != nil {
		log.L(ctx).Errorf("error when get token, err: %s", err.Error())

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

		return err
	}

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

		return err
	}

	log.L(ctx).Infof("send lark alert success, user: %s, alertType: %d", user, alertType)

	return nil
}

func (d *downloadAlertService) getApprovalReqBody(ctx context.Context, user string, alertType int, docs []string) (*LarkCreateApprovalBody, error) {
	res := LarkCreateApprovalBody{
		ApprovalCode: phishingApprovalCode,
	}

	reporterLarkID := primaryNotificationLarkUserID
	reporterOpLarkID := primaryNotificationLarkOpenID

	res.UserId = reporterLarkID
	res.OpenId = reporterOpLarkID

	var tim, fre string
	switch alertType {
	case 0:
		tim = "10m"
		fre = "4"
	case 1:
		tim = "60m"
		fre = "10"
	case 2:
		tim = "24h"
		fre = "20"
	}

	userInfo, err := d.FetchUserInfo(ctx, user)
	if err != nil {
		if strings.Contains("record not found", err.Error()) {
			log.L(ctx).Warnf("user %s not found", user)
		} else {
			return nil, err
		}
	}
	resForm := d.makeBody(tim, fre, user, alertType, docs, userInfo)

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

		return nil, err
	}

	res.Form = string(resStr)

	return &res, nil
}

func (d *downloadAlertService) makeBody(tim, fre, uMail string, alertType int, docs []string, user *UserInfo) []FormItem {
	var problemMsg, principal string
	var assistMembers []string
	if user != nil {
		switch alertType {
		case 0:
			principal, assistMembers = firstDownloadAlertRecipients(user.UserID)
			problemMsg = "【第一次告警】用户" + user.Email + "在10分钟内超过3次导出文件。这是自动触发的告警，如果您是正常工作需要进行的下载可以忽略。"
		case 1:
			principal, assistMembers = escalatedDownloadAlertRecipients(user.Leader)
			problemMsg = "【第二次告警】用户" + user.Email + "在60分钟超过10次导出文件。"
		case 2:
			principal, assistMembers = escalatedDownloadAlertRecipients(user.Leader)
			problemMsg = "【第三次告警】用户" + user.Email + "在1天内超过20次导出文件。"
		}
	}
	if user == nil {
		principal = primaryNotificationLarkUserID
		assistMembers = []string{}
		problemMsg = "非内部员工" + uMail + "在" + tim + "内下载了" + fre + "次文件，查看原因"
	}
	problemMsg += "\n"
	i := 1
	for _, doc := range docs {
		if strings.ReplaceAll(doc, " ", "") == "" {
			continue
		}
		problemMsg += "文件" + strconv.Itoa(i) + ":" + doc + "\n"
		i++
	}

	var resForm []FormItem
	resForm = append(resForm, FormItem{
		Id:    v1.SafeType,
		Type:  "radioV2",
		Value: v1.DataSafe,
	}, FormItem{
		Id:    v1.Assets,
		Type:  "radioV2",
		Value: v1.Document,
	}, FormItem{
		Id:    v1.ProblemDescription,
		Type:  "textarea",
		Value: problemMsg,
	}, FormItem{
		Id:    v1.ImprovementSuggestion,
		Type:  "textarea",
		Value: "用户频繁下载，请确认是否合理，并请用户在使用完成后进行删除销毁",
	}, FormItem{
		Id:    v1.EmergencyLevel,
		Type:  "radioV2",
		Value: v1.Normal,
	}, FormItem{
		Id:    v1.ResponsiblePerson,
		Type:  "contact",
		Value: []string{principal},
	}, FormItem{
		Id:    v1.Collaborators,
		Type:  "contact",
		Value: assistMembers,
	}, FormItem{
		Id:    v1.RectificationTime,
		Type:  "date",
		Value: time.Now().Add(time.Hour * 24 * 15).Format(time.RFC3339),
	})

	return resForm
}

func (d *downloadAlertService) GetAlertList(_, lastHour, lastTen time.Time) ([]string, []string, []string) {
	var over3, over10, over20 []string

	for _, queue := range bucket.queueMap {
		if len(queue) > 20 {
			over20 = append(over20, queue[0].Actor)

			continue
		}
		if len(queue) > 10 && queue[len(queue)-11].Time.After(lastHour) {
			over10 = append(over10, queue[0].Actor)

			continue
		}
		if len(queue) > 3 && queue[len(queue)-4].Time.After(lastTen) {
			over3 = append(over3, queue[0].Actor)
		}
	}

	return over3, over10, over20
}

type LarkUser struct {
	UserId       string                   `json:"user_id"`
	Name         string                   `json:"name"`
	Email        string                   `json:"email"`
	OpenID       string                   `json:"open_id"`
	CustomAttrs  []map[string]interface{} `json:"custom_attrs"`
	LeaderUserId string                   `json:"leader_user_id"`
}

func (d *downloadAlertService) GetHRBPAndLeader(ctx context.Context, user *v1.User) (string, string, error) {
	if user.HRBP != nil && user.Leader != nil && *user.HRBP != "" && *user.Leader != "" {
		return *user.HRBP, *user.Leader, nil
	}
	commonSrv := newCommon()
	newToken, err := commonSrv.GetLarkToken(ctx, 1)
	if err != nil {
		return "", "", err
	}
	userJsonMsg, err := commonSrv.GetDataByReqLarkAPI(ctx, "GET", LarkOpenAPIBase+"/contact/v3/users/"+user.LarkUserID+"?user_id_type=user_id&department_id_type=department_id", nil, nil, newToken)
	if err != nil {
		log.L(ctx).Errorf("commonSrv.GetDataByReqLarkAPI error: %v", err)

		return "", "", err
	}

	var userInfo struct {
		User LarkUser `json:"user"`
	}
	if err = json.Unmarshal(userJsonMsg, &userInfo); err != nil {
		log.L(ctx).Errorf("json.Unmarshal error: %v", err)

		return "", "", err
	}

	var hrbp, leader string
	if user.HRBP == nil {
		var hrInfo interface{}
		for _, attr := range userInfo.User.CustomAttrs {
			info, ok := attr["type"].(string)
			if !ok {
				log.L(ctx).Errorf("attr type is not string")

				return "", "", errors.New("attr type is not string")
			}
			if info == "GENERIC_USER" {
				hrInfo = attr["value"]
			}
		}
		if hrInfo == nil {
			log.L(ctx).Warnf("user %s don't hava hr info", user.Email)
		} else {
			hrMap, ok := hrInfo.(map[string]interface{})
			if !ok {
				log.L(ctx).Errorf("hrInfo.(map[string]interface{}) error")

				return "", "", errors.New("hrInfo.(map[string]interface{}) error")
			}
			guser, ok := hrMap["generic_user"].(map[string]interface{})
			if !ok {
				log.L(ctx).Errorf("hrMap[\"generic_user\"].(map[string]interface{}) error")

				return "", "", errors.New("hrMap[\"generic_user\"].(map[string]interface{}) error")
			}
			hrbp, ok = guser["id"].(string)
			if !ok {
				log.L(ctx).Warnf("user %s don't have hrbp", user.LarkUserID)
			}
		}
	}
	user.HRBP = &hrbp
	if user.Leader == nil {
		leader = userInfo.User.LeaderUserId
	}
	user.Leader = &leader

	err = d.store.User().SaveUser(ctx, *user)
	if err != nil {
		log.L(ctx).Errorf("d.store.User().SaveUser error: %v", err)

		return "", "", err
	}

	return hrbp, leader, nil
}

var (
	token  string
	expiry time.Time
)

func (d *downloadAlertService) GetUserInfo(ctx context.Context, user string) (*v1.UserIT, error) {
	host := viper.GetString("mysql.host")
	username := viper.GetString("mysql.username-sub")
	password := viper.GetString("mysql.password-sub")
	dbname := viper.GetString("mysql.database-sub")
	dsn := username + ":" + password + "@tcp(" + host + ")/" + dbname + "?charset=utf8mb4&parseTime=True&loc=Local"
	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
	if err != nil {
		log.L(ctx).Errorf("gorm.Open error: %v", err)

		return nil, err
	}

	us := &v1.UserIT{}
	err = db.Where("extraMail = ? or email = ?", user, user).First(&us).Error
	if err != nil {
		log.L(ctx).Warnf("db.Where error: %v", err)

		return nil, err
	}

	return us, nil
}

// SaveMockData mock a user.
func (d *downloadAlertService) SaveMockData() {
	rand.Seed(time.Now().UnixNano())
	for i := 0; i < 5; i++ {
		bucket.AddItem(Event{
			Actor:    "liuy@pingcap.cn",
			Time:     time.Now(),
			EventID:  strconv.Itoa(rand.Intn(100000)),
			DocTitle: "testfile" + strconv.Itoa(i) + ".txt",
		})
	}
}

var bucket = NewBucket()

type Bucket struct {
	visMap   map[string]int
	docMap   map[string]int
	queueMap map[string][]*Event
}

func NewBucket() *Bucket {
	return &Bucket{
		visMap:   make(map[string]int),
		docMap:   make(map[string]int),
		queueMap: make(map[string][]*Event),
	}
}

func (b *Bucket) AddItem(event Event) {
	queue := b.queueMap[event.Actor]
	if queue == nil {
		queue = []*Event{}
	}

	b.enqueue(&event)
}

func (b *Bucket) RemoveExpired(actor string, now time.Time) {
	if actor != "" {
		queue := b.queueMap[actor]
		if queue == nil {
			return
		}
		newQueue := []*Event{}
		for _, e := range queue {
			if e.Time.After(now) {
				newQueue = append(newQueue, e)
			} else {
				delete(b.visMap, e.Actor+e.EventID)
				delete(b.docMap, e.Actor+e.DocTitle)
			}
		}
		if len(newQueue) == 0 {
			delete(b.queueMap, actor)
		} else {
			b.queueMap[actor] = newQueue
		}
		return
	}

	for k, v := range b.queueMap {
		newQueue := []*Event{}
		for _, e := range v {
			if e.Time.After(now) {
				newQueue = append(newQueue, e)
			} else {
				delete(b.visMap, e.Actor+e.EventID)
				delete(b.docMap, e.Actor+e.DocTitle)
			}
		}
		if len(newQueue) == 0 {
			delete(b.queueMap, k)
		} else {
			b.queueMap[k] = newQueue
		}
	}
}

type Event struct {
	Index    int
	Actor    string
	EventID  string
	DocTitle string
	Time     time.Time
}

func (b *Bucket) enqueue(event *Event) {
	if _, ok := b.visMap[event.Actor+event.EventID]; ok {
		return
	}
	if _, ok := b.docMap[event.Actor+event.DocTitle]; ok {
		return
	}

	b.visMap[event.Actor+event.EventID] = 1
	b.docMap[event.Actor+event.DocTitle] = 1

	queue := b.queueMap[event.Actor]
	queue = append(queue, event)
	sort.Slice(queue, func(i, j int) bool {
		return queue[i].Time.Before(queue[j].Time)
	})
	b.queueMap[event.Actor] = queue
}

func (b *Bucket) GetLatestDocs(actor string, date time.Time) []string {
	vmp := make(map[string]int)
	queue := b.queueMap[actor]
	for _, v := range queue {
		if len(vmp) >= 5 {
			break
		}
		if _, ok := vmp[v.DocTitle]; !ok && v.Time.After(date) {
			if strings.ReplaceAll(v.DocTitle, " ", "") == "" {
				continue
			}
			vmp[v.DocTitle] = 1
		}
	}
	var docs []string
	for k := range vmp {
		docs = append(docs, k)
	}
	return docs
}
