package v1

import (
	"bytes"
	"context"
	"database/sql"
	"encoding/json"
	"sort"
	"sync"
	"time"

	"github.com/google/go-cmp/cmp"

	"github.com/pingcap-inc/pchub/internal/pkg/rstore"
	"github.com/pingcap-inc/pchub/pkg/rstorage"

	"github.com/pingcap-inc/pchub/internal/apiserver/store"
	"github.com/pingcap-inc/pchub/internal/pkg/code"
	modelv1 "github.com/pingcap-inc/pchub/model/v1"
	"github.com/pingcap-inc/pchub/pkg/errors"
	"github.com/pingcap-inc/pchub/pkg/log"
)

type userService struct {
	store store.Factory
}

type UserSrv interface {
	GetUserInfo(ctx context.Context, mparams *modelv1.GetUserInfoParams, authingID, email string) (*modelv1.GetUserInfoData, error)
	SyncUsersWithLark(ctx context.Context) error
	Search(ctx context.Context, kw string) ([]*modelv1.User, error)
	GetUserByID(ctx context.Context, id string) (*modelv1.User, error)
	JoinFeedbackGroup(ctx context.Context, user *modelv1.User) error
}

var _ UserSrv = (*userService)(nil)

func newUser(srv *service) *userService {
	return &userService{store: srv.store}
}

type JoinFeedbackGroupParams struct {
	IDList []string `json:"id_list"`
}

func (u *userService) JoinFeedbackGroup(ctx context.Context, user *modelv1.User) error {
	reqBody := JoinFeedbackGroupParams{
		IDList: []string{user.LarkUserID},
	}
	reqb, err := json.Marshal(&reqBody)
	if err != nil {
		log.L(ctx).Error(err.Error())

		return errors.Wrap(err, "Error when marshal lark join chat body")
	}

	commonService := &commonService{store: u.store}
	_, err = commonService.GetDataByReqLarkAPI(ctx, "POST", LarkOpenAPIBase+"/im/v1/chats/"+AskPCHChatID+"/members?member_id_type=user_id&succeed_type=1", bytes.NewReader(reqb), nil)
	if err != nil {
		return errors.WithCode(code.ErrHttpRequest, "Error when join users into feedback group")
	}

	return nil
}

func (u *userService) Search(ctx context.Context, kw string) ([]*modelv1.User, error) {
	us, err := u.store.User().Search(ctx, kw)
	if err != nil {
		coder := errors.ParseCoder(err)
		if coder.Code() != code.ErrRecordNotFound {
			log.L(ctx).Error(errors.Wrap(err, "Error when UserStore.Search from db").Error())

			return nil, errors.Wrap(err, "Error when search user from db.")
		}
	}

	return us, nil
}

type LarkUser struct {
	UserId        string             `json:"user_id"`
	Name          string             `json:"name"`
	EnName        string             `json:"en_name"`
	Email         string             `json:"email"`
	Avatar        modelv1.AvatarLark `json:"avatar"`
	DepartmentIds []string           `json:"department_ids"`
	OpenID        string             `json:"open_id"`
	Orders        []LarkUserOrder    `json:"orders"`
}

type LarkUserWithRawAvatars struct {
	Avatar json.RawMessage `json:"avatar"`
}

type ComparedUser struct {
	UserId           string
	Name             string
	EnName           string
	Email            string
	MainDepartmentID string
	Avatar           modelv1.AvatarLark
}

// GetUserInfo will request for lark user info and return it.
// Then it will sync the info into the tabel 'user' in db.
func (u *userService) GetUserInfo(ctx context.Context, mparams *modelv1.GetUserInfoParams, authingID, email string) (*modelv1.GetUserInfoData, error) {
	commonService := &commonService{store: u.store}
	userJsonMsg, err := commonService.GetDataByReqLarkAPI(ctx, "GET", LarkOpenAPIBase+"/contact/v3/users/"+mparams.
		Luid+"?user_id_type=user_id&department_id_type=department_id", nil, nil)
	if err != nil {
		return nil, errors.Wrap(err, "Error when GetUserInfo")
	}
	var lu struct {
		User LarkUser `json:"user"`
	}
	if err = json.Unmarshal(userJsonMsg, &lu); err != nil {
		log.L(ctx).Error(err.Error())

		return nil, errors.Wrap(err, "Error when unmarshal lark userInfo data")
	}
	var uAvatar struct {
		User LarkUserWithRawAvatars `json:"user"`
	}
	if err = json.Unmarshal(userJsonMsg, &uAvatar); err != nil {
		log.L(ctx).Error(err.Error())

		return nil, errors.Wrap(err, "Error when unmarshal lark userInfo data for creating user.")
	}

	user := getUserFromDB(ctx, u, mparams.Luid, email)

	isInAskPCH := true
	rStore := &rstorage.RedisCluster{
		KeyPrefix: rstore.AskPCHChat,
	}
	_, err = rStore.GetKey(lu.User.UserId)
	if err != nil {
		if !errors.As(err, &rstorage.ErrKeyNotFound) {
			log.L(ctx).Error(errors.Wrap(err, "error when retrieve key in redis").Error())
		}

		isInAskPCH = false
	}

	res := &modelv1.GetUserInfoData{
		LarkUserId: lu.User.UserId,
		Name:       lu.User.Name,
		EnName:     lu.User.EnName,
		Email:      lu.User.Email,
		Avatar:     &lu.User.Avatar,
		Marks: &[]modelv1.UserMark{
			{
				Key:     "isInAskPCH",
				BoolVal: isInAskPCH,
			},
		},
	}

	err = syncUserIntoDB(ctx, u, user, &lu.User, &uAvatar.User, authingID)
	if err != nil {
		return nil, errors.Wrap(err, "Error when sync user into db")
	}

	dpts, err := u.store.Department().GetChainByLarkID(ctx, lu.User.DepartmentIds[0])
	if err != nil {
		return nil, errors.Wrap(err, "Error when get department chain by lark department id")
	}

	res.Departments = dpts

	perms, err := u.store.PCHPermission().ListByLarkUID(ctx, lu.User.UserId)
	if err != nil {
		return nil, errors.Wrap(err, "Error when get permissions by lark user id")
	}

	res.Permissions = &perms

	return res, nil
}

func getUserFromDB(ctx context.Context, u *userService, luid, email string) *modelv1.User {
	// get lark user info by id from db
	user, err := u.store.User().GetByAnyID(ctx, luid)
	if err != nil {
		coder := errors.ParseCoder(err)
		if coder.Code() != code.ErrUserNotFound {
			log.L(ctx).Error(errors.Wrap(err, "Error when GetByAnyID from db").Error())
		}
	}

	// try to get lark user info by email from db
	if user == nil {
		user, err = u.store.User().GetByEmail(ctx, email)
		if err != nil {
			coder := errors.ParseCoder(err)
			if coder.Code() != code.ErrUserNotFound {
				log.L(ctx).Error(errors.Wrap(err, "Error when GetByEmail from db").Error())
			}
		}
	}

	return user
}

func syncUserIntoDB(ctx context.Context, u *userService, user *modelv1.User, lu *LarkUser, lua *LarkUserWithRawAvatars, authingID string) error {
	sort.Slice(lu.Orders, func(i, j int) bool {
		return lu.Orders[i].DepartmentOrder > lu.Orders[j].DepartmentOrder
	})
	if user == nil { //nolint:nestif // accepetable
		// if no record in db, create it sync
		log.L(ctx).Info("User info not found in db, try to create.")

		newUser := modelv1.User{
			AuthingID:        authingID,
			LarkUserID:       lu.UserId,
			CreatedAt:        time.Now(),
			SyncedAt:         time.Now(),
			Name:             lu.Name,
			EnName:           lu.EnName,
			Email:            lu.Email,
			Avatars:          lua.Avatar,
			MainDepartmentID: lu.Orders[0].DepartmentID,
			LarkOpenID:       lu.OpenID,
		}
		_, err := u.store.User().Create(ctx, &newUser)
		if err != nil {
			log.L(ctx).Error(err.Error())

			return errors.Wrap(err, "Error when create user.")
		}
	} else {
		// if any diff between, sync it async
		go func() {
			var avatarInDB modelv1.AvatarLark
			if len(user.Avatars) > 0 {
				if err := json.Unmarshal(user.Avatars, &avatarInDB); err != nil {
					log.L(ctx).Error(err.Error())
					avatarInDB = modelv1.AvatarLark{}
				}
			}
			var avatarInAPI modelv1.AvatarLark
			if len(lua.Avatar) > 0 {
				if err := json.Unmarshal(lua.Avatar, &avatarInAPI); err != nil {
					log.L(ctx).Error(err.Error())
					avatarInAPI = modelv1.AvatarLark{}
				}
			}
			uInDB := &ComparedUser{
				UserId:           user.LarkUserID,
				Name:             user.Name,
				EnName:           user.EnName,
				Email:            user.Email,
				MainDepartmentID: user.MainDepartmentID,
				Avatar:           avatarInDB,
			}
			uInAPI := &ComparedUser{
				UserId:           lu.UserId,
				Name:             lu.Name,
				EnName:           lu.EnName,
				Email:            lu.Email,
				MainDepartmentID: lu.Orders[0].DepartmentID,
				Avatar:           avatarInAPI,
			}
			if !cmp.Equal(uInDB, uInAPI) || user.AuthingID != authingID {
				log.L(ctx).Info("User info changed, try to update.")

				newUser := modelv1.User{
					ID:         user.ID,
					AuthingID:  authingID,
					LarkUserID: lu.UserId,
					CreatedAt:  user.CreatedAt,
					UpdatedAt: sql.NullTime{
						Time:  time.Now(),
						Valid: true,
					},
					SyncedAt:         time.Now(),
					Name:             lu.Name,
					EnName:           lu.EnName,
					Avatars:          lua.Avatar,
					Email:            lu.Email,
					MainDepartmentID: lu.Orders[0].DepartmentID,
					LarkOpenID:       lu.OpenID,
				}

				er := u.store.User().Update(ctx, &newUser)
				if er != nil {
					log.L(ctx).Error(errors.Wrap(er, "Error when update user in db").Error())
				}
			}
		}()
	}

	return nil
}

type HasSeenMap struct {
	sync.RWMutex
	Map map[string]bool
}

func (l *HasSeenMap) readMap(key string) (bool, bool) {
	l.RLock()
	value, ok := l.Map[key]
	l.RUnlock()

	return value, ok
}

func (l *HasSeenMap) writeMap(key string, value bool) {
	l.Lock()
	l.Map[key] = value
	l.Unlock()
}

func (u *userService) SyncUsersWithLark(ctx context.Context) error {
	log.L(ctx).Info("InitUsersWithLark called.")

	departmentService := &departmentService{store: u.store}
	cs := &commonService{store: u.store}
	cs.GetAndCachePchubLarkToken(ctx)

	dpts, err := departmentService.ListAll(ctx)
	if err != nil {
		log.L(ctx).Error("Error when list all department from db, err: " + err.Error())

		return errors.Wrap(err, "Error when list all departments")
	}

	var wg sync.WaitGroup
	sem := make(chan struct{}, 10)

	hasHandledUserLarkIDs := &HasSeenMap{
		Map: make(map[string]bool),
	}
	hasHandledUserDepartmentIDs := &HasSeenMap{
		Map: make(map[string]bool),
	}

	for _, dpt := range dpts.Items {
		wg.Add(1)
		go func(dpt *modelv1.Department) {
			sem <- struct{}{}
			defer func() { <-sem }()
			defer wg.Done()

			u.SyncLarkUsersByDepartment(ctx, dpt.LarkID, "", hasHandledUserLarkIDs, hasHandledUserDepartmentIDs)
		}(dpt)
	}

	wg.Wait()

	// handle delete
	err = u.deleteUsersIfNotIn(ctx, hasHandledUserLarkIDs)
	if err != nil {
		log.L(ctx).Error("Error when delete users if not in, err: " + err.Error())

		return errors.Wrap(err, "Error when delete users if not in")
	}
	// handle delete
	err = u.deleteUserDepartmentsIfNotIn(ctx, hasHandledUserDepartmentIDs)
	if err != nil {
		log.L(ctx).Error("Error when delete user departments if not in, err: " + err.Error())

		return errors.Wrap(err, "Error when delete user departments if not in")
	}

	log.L(ctx).Info("Success to sync lark contact users.")

	return nil
}

func (u *userService) deleteUsersIfNotIn(ctx context.Context, useen *HasSeenMap) error {
	larkIDs := make([]string, 0)
	for k := range useen.Map {
		larkIDs = append(larkIDs, k)
	}

	return u.store.User().MDelByLarkIDsIfNotIn(ctx, larkIDs)
}

func (u *userService) deleteUserDepartmentsIfNotIn(ctx context.Context, udseen *HasSeenMap) error {
	larkIDs := make([]string, 0)
	for k := range udseen.Map {
		larkIDs = append(larkIDs, k)
	}

	return u.store.UserDepartment().MDelByLarkIDsIfNotIn(ctx, larkIDs)
}

type LarkUserStatus struct {
	IsFrozen   bool `json:"is_frozen"`
	IsResigned bool `json:"is_resigned"`
	IsActived  bool `json:"is_activated"`
	IsExited   bool `json:"is_exited"`
	IsUnjoin   bool `json:"is_unjoin"`
}

type LarkUserOrder struct {
	DepartmentID string `json:"department_id"`
	UserOrder    int    `json:"user_order"`
	// the higher the value, the higher the ranking
	DepartmentOrder int `json:"department_order"`
}

type LarkUserItem struct {
	UserID        string             `json:"user_id"`
	Name          string             `json:"name"`
	EnName        string             `json:"en_name"`
	Email         string             `json:"email"`
	Avatar        modelv1.AvatarLark `json:"avatar"`
	Status        LarkUserStatus     `json:"status"`
	DepartmentIds []string           `json:"department_ids"`
	Orders        []LarkUserOrder    `json:"orders"`
	OpenID        string             `json:"open_id"`
}

type LarkUsersData struct {
	Items     []LarkUserItem `json:"items"`
	PageToken string         `json:"page_token"`
	HasMore   bool           `json:"has_more"`
}

func (u *userService) SyncLarkUsersByDepartment(ctx context.Context, departLarkID, pageToken string, useen, udseen *HasSeenMap) {
	log.L(ctx).Info("Start to sync department (" + departLarkID + ") users.")

	cs := &commonService{store: u.store}
	var pageTokenQ string
	if pageToken != "" {
		pageTokenQ = "&page_token=" + pageToken
	}

	usersJsonRaw, err := cs.GetDataByReqLarkAPI(ctx, "GET", LarkOpenAPIBase+"/contact/v3/users/find_by_department?user_id_type=user_id&page_size=50&department_id_type=department_id&department_id="+departLarkID+pageTokenQ, nil, nil)
	if err != nil {
		log.Error(err.Error())

		return
	}

	var d LarkUsersData
	if err = json.Unmarshal(usersJsonRaw, &d); err != nil {
		log.L(ctx).Error(err.Error())

		return
	}

	validItemsMap := make(map[string]*modelv1.User)
	validUDItemsMap := make(map[string]*modelv1.UserDepartment)
	var userLarkIDs []string
	var userDptIDs []string
	for _, it := range d.Items {
		if !it.Status.IsResigned && !it.Status.IsExited && !it.Status.IsUnjoin {
			if _, ok := useen.readMap(it.UserID); !ok {
				useen.writeMap(it.UserID, true)
				avatars, err := json.Marshal(it.Avatar)
				if err != nil {
					log.L(ctx).Error("Marshal lark avatar error: " + err.Error())
				}
				orders := it.Orders
				sort.Slice(orders, func(i, j int) bool {
					return orders[i].DepartmentOrder > orders[j].DepartmentOrder
				})
				item := &modelv1.User{
					LarkUserID:       it.UserID,
					CreatedAt:        time.Now(),
					Name:             it.Name,
					EnName:           it.EnName,
					Avatars:          avatars,
					Email:            it.Email,
					SyncedAt:         time.Now(),
					MainDepartmentID: orders[0].DepartmentID,
					LarkOpenID:       it.OpenID,
				}
				validItemsMap[it.UserID] = item
				userLarkIDs = append(userLarkIDs, it.UserID)
			}

			if _, ok := udseen.readMap(it.UserID + "," + departLarkID); !ok {
				udseen.writeMap(it.UserID+","+departLarkID, true)
				item := &modelv1.UserDepartment{
					CreatedAt:        time.Now(),
					UserLarkID:       it.UserID,
					DepartmentLarkID: departLarkID,
				}
				validUDItemsMap[it.UserID+","+departLarkID] = item
				userDptIDs = append(userDptIDs, it.UserID+","+departLarkID)
			}
		}
	}

	err = u.batchSyncUsers(ctx, userLarkIDs, validItemsMap)
	if err != nil {
		return
	}

	err = u.batchSyncUserDepartments(ctx, userDptIDs, validUDItemsMap)
	if err != nil {
		return
	}

	// handle next page
	if d.HasMore && d.PageToken != "" {
		u.SyncLarkUsersByDepartment(ctx, departLarkID, d.PageToken, useen, udseen)
	}

	log.L(ctx).Info("Success to sync department (" + departLarkID + ") users.")
}

func (u *userService) batchSyncUsers(ctx context.Context, userLarkIDs []string, validItemsMap map[string]*modelv1.User) error {
	// retrieve user using userLarkIDs
	users, err := u.store.User().MGetByLarkIDs(ctx, userLarkIDs)
	if err != nil {
		coder := errors.ParseCoder(err)
		if coder.Code() != code.ErrRecordNotFound {
			log.L(ctx).Error(errors.Wrap(err, "Error when User.MGetByLarkIDs from db").Error())

			return err
		}
	}
	nus := make(map[string]*modelv1.User)
	for _, u := range users {
		nus[u.LarkUserID] = u
	}
	nu, uu := diffUsers(validItemsMap, nus)
	err = u.store.User().BatchSave(ctx, nu, uu)
	if err != nil {
		log.L(ctx).Error("Error when batch save users into db, err: " + err.Error())

		return err
	}

	return nil
}

func diffUsers(larkUsersMap, dbUsers map[string]*modelv1.User) (nu, uu []*modelv1.User) {
	for larkID, it := range larkUsersMap {
		if _, ok := dbUsers[larkID]; !ok {
			nu = append(nu, it)
		} else {
			it.ID = dbUsers[larkID].ID
			it.AuthingID = dbUsers[larkID].AuthingID
			uu = append(uu, it)
		}
	}

	return nu, uu
}

func (u *userService) batchSyncUserDepartments(ctx context.Context, userDptIDs []string, validUDItemsMap map[string]*modelv1.UserDepartment) error {
	// retrieve user department using userDptIDs
	uds, err := u.store.UserDepartment().MGetByLarkIDs(ctx, userDptIDs)
	if err != nil {
		coder := errors.ParseCoder(err)
		if coder.Code() != code.ErrRecordNotFound {
			log.L(ctx).Error(errors.Wrap(err, "Error when UserDepartment.MGetByLarkIDs from db").Error())

			return err
		}
	}
	nus := make(map[string]*modelv1.UserDepartment)
	for _, u := range uds {
		nus[u.UserLarkID+","+u.DepartmentLarkID] = u
	}
	nu := diffUserDepartments(validUDItemsMap, nus)
	err = u.store.UserDepartment().BatchSave(ctx, nu)
	if err != nil {
		log.L(ctx).Error("Error when batch save user departments into db, err: " + err.Error())

		return err
	}

	return nil
}

func diffUserDepartments(larkUserDptsMap, dbUserDpts map[string]*modelv1.UserDepartment) (nu []*modelv1.UserDepartment) {
	for larkID, it := range larkUserDptsMap {
		if _, ok := dbUserDpts[larkID]; !ok {
			nu = append(nu, it)
		}
	}

	return nu
}

func (u *userService) GetUserByID(ctx context.Context, id string) (*modelv1.User, error) {
	user, err := u.store.User().GetByAnyID(ctx, id)
	if err != nil {
		log.L(ctx).Error(errors.Wrap(err, "Error when get user from db").Error())

		return nil, err
	}

	return user, nil
}
