package v1

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

	"github.com/pingcap-inc/pchub/pkg/errors"
	"github.com/pingcap-inc/pchub/pkg/log"
	"github.com/spf13/viper"
	report "google.golang.org/api/admin/reports/v1"
	"gorm.io/driver/mysql"
	"gorm.io/gorm"

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

const (
	DownloadAlertApproval = "44F79E0D-5869-4F33-8070-DED6B1131AA5"
)

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

	scope := []string{
		report.AdminReportsAuditReadonlyScope,
	}

	reportSrv, err := NewGoogleService().WsSrv(ctx, "report", scope...)
	if err != nil {
		log.L(ctx).Errorf("new google service error: %s", err.Error())

		return err
	}

	// before 1hour
	startTime := time.Now().Add(-time.Hour * 12).Format(time.RFC3339Nano)
	var events []Event
	pageToken := ""
	for {
		call := reportSrv.(*report.Service).Activities.List("all", "drive").
			EventName("download").
			StartTime(startTime)
		if pageToken != "" {
			call.PageToken(pageToken)
		}
		resp, err := call.Do()
		// 预防空指针
		if resp == nil {
			log.L(ctx).Errorf("reportSrv.Activities.ListCompany resp is nil")

			return nil
		}
		for _, activity := range resp.Items {
			if activity.Events[0].Name == "download" {
				parse, err := time.Parse(time.RFC3339Nano, activity.Id.Time)
				if err != nil {
					log.L(ctx).Errorf("time.Parse error: %v", err)

					continue
				}
				event := Event{
					Actor:    activity.Actor.Email,
					EventID:  strconv.FormatInt(activity.Id.UniqueQualifier, 10),
					DocTitle: activity.Events[0].Parameters[7].Value,
					Time:     parse,
				}
				events = append(events, event)
			}
		}
		if err != nil {
			log.L(ctx).Errorf("reportSrv.Activities.ListCompany error: %v", err)

			return err
		}
		if resp.NextPageToken == "" {
			break
		}
		pageToken = resp.NextPageToken
	}
	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) 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 := &commonService{store: d.store}
	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 := d.getToken(ctx)
	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: DownloadAlertApproval,
	}

	reporterLarkID := defaultUserLarkID
	reporterOpLarkID := defaultUserOpLarkID

	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.GetUserInfo(ctx, strings.ToLower(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 && user.IsChina == "true" {
		switch alertType {
		case 0:
			principal = user.Leader
			problemMsg = user.Email + " have downloaded files " + fre + " times within " + tim + ". Please ensure the safety of company assets and avoid leaks.\nThis is an automatically triggered alert. If this download is part of your normal work, you can ignore it."
			assistMembers = []string{
				"e359137a",
			}
		case 1:
			principal = user.Leader
			problemMsg = user.Email + " has exported " + fre + " files within " + tim + ". Please review and confirm if there are any security risks."
			assistMembers = []string{
				user.HRBP,
				user.UserID,
			}
		case 2:
			principal = "e359137a"
			problemMsg = user.Email + " has exported " + fre + " files within " + tim + ". Please review and confirm if there are any security risks."
			assistMembers = []string{
				"966933ae",
				user.Leader,
				user.HRBP,
			}
		}
	} else {
		principal = "e359137a"
		assistMembers = []string{
			"966933ae",
		}
		problemMsg = "Foreign employees have downloaded files " + fre + " times within " + tim + ". Please check the reason."
	}
	if user == nil {
		principal = "e359137a"
		assistMembers = []string{
			"966933ae",
		}
		problemMsg = "Non-internal employee " + uMail + " has downloaded files " + fre + " times within " + tim + ". Please check the reason."
	}
	problemMsg += "\n"

	i := 1
	for _, doc := range docs {
		if strings.ReplaceAll(doc, " ", "") == "" {
			continue
		}
		problemMsg += "doc_" + strconv.Itoa(i) + ":" + doc + "\n"
		i++
	}

	var resForm []FormItem
	resForm = append(resForm, FormItem{
		Id:    "widget16614938322000001",
		Type:  "radioV2",
		Value: "l7a2jgxo-jqbkukuvsva-9",
	}, FormItem{
		Id:    "widget16614939701650001",
		Type:  "radioV2",
		Value: "l7a2lbta-bxh87q5cbaq-13",
	}, FormItem{
		Id:    "widget16487195633330001",
		Type:  "textarea",
		Value: problemMsg,
	}, FormItem{
		Id:    "widget16614941455820001",
		Type:  "textarea",
		Value: uMail + " has frequently downloaded files. Please review and confirm if there are any security risks.",
	}, FormItem{
		Id:    "widget16487195406530001",
		Type:  "radioV2",
		Value: "l1et11i4-qql1hs1hjtj-5",
	}, FormItem{
		Id:    "widget16614941687660001",
		Type:  "contact",
		Value: []string{principal},
	}, FormItem{
		Id:    "widget16614942163000001",
		Type:  "contact",
		Value: assistMembers,
	}, FormItem{
		Id:    "widget16614942960990001",
		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 := commonService{store: d.store}
	newToken, err := d.getToken(ctx)
	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) getToken(ctx context.Context) (string, error) {
	if token != "" && time.Now().Before(expiry) {
		return token, nil
	}

	commonSrv := commonService{
		d.store,
	}
	larkToken, err := commonSrv.GetLarkToken(ctx, pchub)
	if err != nil {
		return "", err
	}

	token = larkToken
	expiry = time.Now().Add(time.Hour)

	return token, nil
}

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

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

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

		return nil, err
	}

	comSrv := &commonService{store: d.store}
	tk, _ := comSrv.GetLarkToken(ctx, pchub)

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

	resp, err := comSrv.GetDataByReqLarkAPI(ctx, "GET", LarkOpenAPIBase+"/contact/v3/users/"+us.LarkUserId, 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-7143156434032312324" {
			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 = user
	userInfo.UserID = us.LarkUserId

	if us != nil {
		userInfo.IsChina = us.IsChina
	} else {
		userInfo.IsChina = "false"
	}

	return userInfo, nil
}

func (d *downloadAlertService) IsChina(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 := "it-internal-app"
	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:    "xiaopeng.zhu@gmail.com",
			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
}
