package v1

import (
	"context"
	"database/sql"
	"encoding/json"
	"github.com/pingcap-inc/pinsights/internal/pkg/authz"
	"strconv"
	"time"

	"github.com/google/go-cmp/cmp"
	"github.com/marmotedu/errors"
	"github.com/pingcap-inc/pinsights/internal/apiserver/store"
	"github.com/pingcap-inc/pinsights/internal/pkg/code"
	"github.com/pingcap-inc/pinsights/model"
	v1 "github.com/pingcap-inc/pinsights/model/v1"
	"github.com/pingcap-inc/pinsights/pkg/log"
	"github.com/pingcap-inc/pinsights/pkg/util/jsonc"
)

type LarkUser struct {
	UserId        string           `json:"userId"`
	Name          string           `json:"name"`
	EnName        string           `json:"enName"`
	Email         string           `json:"email"`
	Avatar        model.AvatarLark `json:"avatar"`
	DepartmentIds []string         `json:"departmentIds"`
}

type ComparedAvatar struct {
	Avatar72     string `json:"avatar72,omitempty"`
	Avatar240    string `json:"avatar240,omitempty"`
	Avatar640    string `json:"avatar640,omitempty"`
	AvatarOrigin string `json:"avatarOrigin,omitempty"`
}

type ComparedUser struct {
	UserId string
	Name   string
	Email  string
	Avatar ComparedAvatar
}

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

type LarkUserSrv interface {
	GetUserInfo(ctx context.Context, mparams *model.GetLarkUserParams, authingID string, email string) (*model.GetLarkUserData, error)
}

type larkUserService struct {
	store store.Factory
}

var _ LarkUserSrv = (*larkUserService)(nil)

func newLarkUser(srv *service) *larkUserService {
	return &larkUserService{store: srv.store}
}

func (u *larkUserService) GetUserInfo(ctx context.Context, mparams *model.GetLarkUserParams, authingID string, email string) (*model.GetLarkUserData, error) {
	// get lark info from lark api
	larkCommonService := newLarkCommon()
	userJsonMsg, err := larkCommonService.GetDataByReqLarkAPI("GET", LARK_OPEN_API_BASE+"/contact/v3/users/"+mparams.
		Uid+"?user_id_type=user_id&department_id_type=department_id", nil)
	if err != nil {
		return nil, errors.Wrap(err, "Error when GetUserInfo")
	}
	var lu struct {
		User LarkUser `json:"user"`
	}
	if err := json.Unmarshal(jsonc.ConvertJSON2Camel(userJsonMsg), &lu); err != nil {
		log.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(jsonc.ConvertJSON2Camel(userJsonMsg), &uAvatar); err != nil {
		log.Error(err.Error())
		return nil, errors.Wrap(err, "Error when unmarshal lark userInfo data for creating user.")
	}

	// get lark user info from db
	user, err := u.store.Users().GetByAnyID(ctx, mparams.Uid)
	if err != nil {
		coder := errors.ParseCoder(err)
		if coder.Code() != code.ErrUserNotFound {
			log.Error(errors.Wrap(err, "Error when GetByAnyID from db").Error())
		}
	}

	if user == nil {
		user, err = u.store.Users().GetByEmail(ctx, email)
		if err != nil {
			coder := errors.ParseCoder(err)
			if coder.Code() != code.ErrUserNotFound {
				log.Error(errors.Wrap(err, "Error when GetByEmail from db").Error())
			}
		}
	}

	res := &model.GetLarkUserData{
		UserId: lu.User.UserId,
		Name:   lu.User.Name,
		EnName: lu.User.EnName,
		Email:  lu.User.Email,
		Avatar: &lu.User.Avatar,
	}

	// get casbin enforcer
	e, err := authz.GetCasbinEnforcer(nil)
	if err != nil {
		log.Error(err.Error())
		return nil, errors.Wrap(err, "Error when GetCasbinEnforcer.")
	}

	// if no record in db, create it
	if user == nil {
		// created user in db
		log.Info("User info not found in db, try to create.")
		newUser := v1.User{
			AuthingID:  authingID,
			LarkUserID: lu.User.UserId,
			CreatedAt:  time.Now(),
			SyncedAt:   time.Now(),
			Name:       lu.User.Name,
			Email:      lu.User.Email,
			Avatars:    uAvatar.User.Avatar,
		}

		userAdded, err := u.store.Users().Create(ctx, &newUser)
		if err != nil {
			log.Error(err.Error())
			return nil, errors.Wrap(err, "Error when create user.")
		}

		var addRules [][]string
		for _, dptId := range lu.User.DepartmentIds {
			addRules = append(addRules, []string{"u_" + strconv.FormatUint(userAdded.ID, 10), "g_" + dptId})
		}

		if len(addRules) > 0 {
			_, err := e.AddNamedGroupingPolicies("g", addRules)
			if err != nil {
				log.Error(err.Error())
				return nil, errors.Wrap(err, "Error when add group policies for new user.")
			}
		}
	} else {
		go func() {
			var avatarInDB ComparedAvatar
			if len(user.Avatars) > 0 {
				if err := json.Unmarshal(jsonc.ConvertJSON2Camel(user.Avatars), &avatarInDB); err != nil {
					log.Error(err.Error())
					avatarInDB = ComparedAvatar{}
				}
			}
			var avatarInAPI ComparedAvatar
			if len(uAvatar.User.Avatar) > 0 {
				if err := json.Unmarshal(jsonc.ConvertJSON2Camel(uAvatar.User.Avatar), &avatarInAPI); err != nil {
					log.Error(err.Error())
					avatarInAPI = ComparedAvatar{}
				}
			}
			uInDB := &ComparedUser{
				UserId: user.LarkUserID,
				Name:   user.Name,
				Email:  user.Email,
				Avatar: avatarInDB,
			}
			uInAPI := &ComparedUser{
				UserId: lu.User.UserId,
				Name:   lu.User.Name,
				Email:  lu.User.Email,
				Avatar: avatarInAPI,
			}
			if !cmp.Equal(uInDB, uInAPI) {
				log.Info("User info changed, try to update.")

				newUser := v1.User{
					ID:         user.ID,
					AuthingID:  authingID,
					LarkUserID: lu.User.UserId,
					CreatedAt:  user.CreatedAt,
					UpdatedAt: sql.NullTime{
						Time:  time.Now(),
						Valid: true,
					},
					SyncedAt: time.Now(),
					Name:     lu.User.Name,
					Avatars:  uAvatar.User.Avatar,
					Email:    lu.User.Email,
				}

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

		log.Info("Try to sync user lark group.")

		dptIdsMap := make(map[string]bool, len(lu.User.DepartmentIds))
		for _, dptId := range lu.User.DepartmentIds {
			dptIdsMap["g_"+dptId] = true
		}
		gPolicies := e.GetFilteredNamedGroupingPolicy("g", 0, "u_"+strconv.FormatUint(user.ID, 10))
		var gp2del [][]string
		gIdsMap := make(map[string]bool, len(gPolicies))
		for _, gp := range gPolicies {
			v, ok := dptIdsMap[gp[1]]
			gIdsMap[gp[1]] = true
			if v && ok {
				continue
			} else {
				gp2del = append(gp2del, gp)
			}
		}
		var gp2add [][]string
		for _, d := range lu.User.DepartmentIds {
			v, ok := gIdsMap["g_"+d]
			if v && ok {
				continue
			} else {
				gp2add = append(gp2add, []string{"u_" + strconv.FormatUint(user.ID, 10), "g_" + d})
			}
		}

		if len(gp2del) > 0 {
			log.Info("")
			_, err := e.RemoveNamedGroupingPolicies("g", gp2del)
			if err != nil {
				log.Error(err.Error())
				return nil, errors.Wrap(err, "Error when RemoveNamedGroupingPolicies.")
			}
		}
		if len(gp2add) > 0 {
			_, err = e.AddNamedGroupingPolicies("g", gp2add)
			if err != nil {
				log.Error(err.Error())
				return nil, errors.Wrap(err, "Error when AddNamedGroupingPolicies.")
			}
		}

		log.Info("Success to sync user lark group.")
	}

	return res, nil
}
