package lark

import (
	"context"
	"errors"
	"sync"
	"time"

	"github.com/Authing/authing-go-sdk/lib/enum"
	"github.com/Authing/authing-go-sdk/lib/management"
	authingmodel "github.com/Authing/authing-go-sdk/lib/model"
	"github.com/spf13/viper"
	"github.com/xyctruth/stream"
	"gorm.io/gorm"

	"github.com/pingcap-inc/it-automation/internal/sync-google-email/model"
	"github.com/pingcap-inc/it-automation/internal/sync-google-email/store/mysql"
	"github.com/pingcap-inc/it-automation/pkg/log"
)

const (
	HrbpID      = "C-7143156434032312324"
	ExtraMailID = "C-7137192459914477572"
)

type visitMap struct {
	visited map[string]bool
	lock    sync.Mutex
}

func (v *visitMap) Get(key string) bool {
	v.lock.Lock()
	defer v.lock.Unlock()

	tag, _ := v.visited[key]
	if !tag {
		v.visited[key] = true
	}
	return tag
}

func (v *visitMap) GetKeys() []string {

	keys := make([]string, 0)
	for k := range v.visited {
		keys = append(keys, k)
	}
	return keys
}

func (v *visitMap) Set(key string) bool {
	v.lock.Lock()
	defer v.lock.Unlock()

	v.visited[key] = true
	return true
}

func SyncUser(ctx context.Context) error {
	log.L(ctx).Info("sync lark user start")

	db, _ := mysql.GetMySQLFactoryOr(nil)
	departments, err := db.Department().ListAll(ctx)
	if err != nil {
		log.L(ctx).Errorf("Error when get department from db:%v", err)

		return err
	}

	group := sync.WaitGroup{}
	visMap := &visitMap{
		visited: make(map[string]bool),
	}
	groupChan := make(chan int, 10)
	for _, department := range departments {
		group.Add(1)
		go func(departmentId string) {
			groupChan <- 1
			defer group.Done()
			defer func() {
				<-groupChan
			}()
			syncToDb(ctx, departmentId, visMap)
		}(department.DepartmentID)
	}
	group.Wait()

	// delete user not in lark
	err = deleteUserNotInLark(ctx, visMap)
	if err != nil {
		log.L(ctx).Errorf("Error when delete user not in lark:%v", err)

		return err
	}

	authingCli, err := management.NewClientWithError(viper.GetString("authing.id"), viper.GetString("authing.secret"), "https://api.authing.pingcap.net")
	if err != nil {
		log.L(ctx).Errorf("Error when init authing client:%v", err)

		return err
	}

	users := make([]authingmodel.User, 0)
	page := 1
	for {
		ok := true
		paginatedUsers, err := authingCli.GetUserList(authingmodel.QueryListRequest{
			Page:           page,
			Limit:          200,
			SortBy:         enum.SortByCreatedAtAsc,
			WithCustomData: &ok,
		})
		if paginatedUsers.TotalCount == 0 || len(paginatedUsers.List) == 0 {
			break
		}
		if err != nil {
			log.Errorf("Error when calling `GetUserList`: %s", err.Error())

			return err
		}
		users = append(users, paginatedUsers.List...)
		page++
	}

	usersInDb, err := db.User().ListUsers(ctx)
	if err != nil {
		return err
	}

	usersMap := make(map[string]*model.LarkUser)
	for _, user := range usersInDb {
		usersMap[user.Email] = user
	}
	var updates []*model.LarkUser
	for _, u := range users {
		var isChaina string
		for _, customData := range u.CustomData {
			if customData.Key == "extsfwzgqyg_112020_549457112" {
				isChaina = *customData.Value
			}
		}
		if uDb, ok := usersMap[*u.Email]; ok {
			if uDb.IsChina != isChaina {
				uDb.IsChina = isChaina
				updates = append(updates, uDb)
			}
		}
	}
	err = db.User().BatchSave(ctx, updates)
	if err != nil {
		log.L(ctx).Errorf("Error when update user:%v", err)

		return err
	}

	log.L(ctx).Info("sync user end")

	return nil
}

func syncToDb(ctx context.Context, departmentId string, visMap *visitMap) {

	larkSrv := NewLarkService()
	pageToken := ""

	larkUsers := make([]UserByDptItem, 0)
	for {
		subUserData, err := larkSrv.GetUsersByDepartment(ctx, departmentId, pageToken)
		if err != nil {
			log.L(ctx).Errorf("Error when get larkUsers by department, err: %v", err)

			return
		}
		larkUsers = append(larkUsers, subUserData.Items...)

		if !subUserData.HasMore || subUserData.PageToken == "" {
			break
		}
		pageToken = subUserData.PageToken
	}
	larkIds := stream.NewSliceByMapping[UserByDptItem, string, int](larkUsers).Parallel(10).Map(
		func(user UserByDptItem) string {
			return user.UserId
		},
	).ToSlice()

	db, _ := mysql.GetMySQLFactoryOr(nil)
	usersInDb, err := db.User().GetUsersByLarkIds(ctx, larkIds)

	if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
		log.L(ctx).Errorf("Error when get larkUsers from db:%v", err)

		return
	}

	userCreate, userUpdate := diffUsers(ctx, usersInDb, larkUsers, visMap)
	err = db.User().SyncUser(ctx, userCreate, userUpdate)
	if err != nil {
		log.L(ctx).Errorf("Error when sync user to db:%v", err)

		return
	}
}

// diffLarkUser return the diff between larkUsers and usersInDb
func diffUsers(ctx context.Context, uInDb []model.LarkUser, uInLark []UserByDptItem, visMap *visitMap) ([]model.LarkUser, []model.LarkUser) {
	userCrate := make([]model.LarkUser, 0)
	userUpdate := make([]model.LarkUser, 0)

	uInDbMap := make(map[string]model.LarkUser)
	for _, user := range uInDb {
		uInDbMap[user.LarkUserID] = user
	}
	for _, u := range uInLark {
		if u.Status.IsExited || u.Status.IsFrozen || u.Status.IsResigned || u.Status.IsUnjoin {
			continue
		}
		if visMap.Get(u.UserId) {
			continue
		}
		visMap.Set(u.UserId)
		var hrbp, extraMail, leader string
		leader = u.LeaderUserId
		for _, attr := range u.CustomAttrs {
			if attr.Id == HrbpID {
				hrbp = attr.Value.GenericUser.Id
			}
			if attr.Id == ExtraMailID {
				extraMail = attr.Value.Text
			}
		}
		if user, ok := uInDbMap[u.UserId]; ok {
			if user.Name != u.Name || user.Email != u.Email ||
				user.Hrbp != hrbp || user.ExtraMail != extraMail ||
				user.Leader != leader {
				user.Name = u.Name
				user.Email = u.Email
				user.Hrbp = hrbp
				user.Leader = leader
				user.ExtraMail = extraMail
				userUpdate = append(userUpdate, user)
			}
		} else {
			userCrate = append(userCrate, model.LarkUser{
				LarkUserID: u.UserId,
				Name:       u.Name,
				Email:      u.Email,
				LarkOpenID: u.OpenId,
				Hrbp:       hrbp,
				ExtraMail:  extraMail,
				IsChina:    "false",
				Leader:     leader,
				CreatedAt:  time.Now(),
			})
		}
	}
	return userCrate, userUpdate
}

func deleteUserNotInLark(ctx context.Context, visMap *visitMap) error {
	keys := visMap.GetKeys()
	db, _ := mysql.GetMySQLFactoryOr(nil)
	err := db.User().BatchDeleteNotIn(ctx, keys)
	if err != nil {
		log.L(ctx).Errorf("Error when delete user not in lark:%v", err)

		return err
	}

	return nil
}
