package v1

import (
	"context"
	"encoding/json"
	"math/rand"
	"regexp"
	"strconv"
	"strings"

	"github.com/mitchellh/mapstructure"
	uuid "github.com/satori/go.uuid"

	"github.com/pingcap-inc/pchub/internal/pkg/code"

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

func (s *approvalService) parseApprovalForm(ctx context.Context, user *modelv1.User, approvalType int, formData interface{}) (string, MsgGenFn, error) {
	log.L(ctx).Info("parseApprovalForm called")

	var formStr string
	approvalFormField, err := getApprovalTypeDefinition(ctx, approvalType)
	if err != nil {
		return "", nil, err
	}

	widgets, err := s.store.LarkApprWidget().ListByApprCode(ctx, modelv1.ApprovalCodeMap[approvalType])
	if err != nil {
		return "", nil, err
	}

	var formDef []modelv1.LarkApprovalPart
	err = json.Unmarshal([]byte(approvalFormField), &formDef)
	if err != nil && !strings.Contains(err.Error(), "option") {
		return "", nil, err
	}

	var msg MsgGenFn

	switch approvalType {
	case modelv1.TypeApplyAccount:
		formStr, msg, err = s.parseApplyAccount(ctx, user, formDef, formData, widgets)
	case modelv1.TypeChangeBudget:
		formStr, msg, err = s.parseChangeBudget(ctx, user, formDef, formData, widgets)
	case modelv1.TypeRemoveUser:
		formStr, msg, err = s.parseRemoveUser(ctx, user, formDef, formData, widgets)
	// case modelv1.TypeChangePermission:
	//	formStr, err = parsePermission(ctx, formList, form)
	case modelv1.TypeCloseAccount:
		formStr, msg, err = s.parseCloseAccount(ctx, user, formDef, formData, widgets)
	case modelv1.TypeJoinAccount:
		formStr, msg, err = s.parseJoinAccount(ctx, user, formDef, formData, widgets)
	case modelv1.TypeInviteUser:
		formStr, msg, err = s.parseInviteUser(ctx, user, formDef, formData, widgets)
	default:
		return "", nil, errors.WithCode(code.ErrApprovalType, "approval type not supported")
	}
	if err != nil {
		return "", nil, err
	}

	return formStr, msg, nil
}

func getApprovalTypeDefinition(c context.Context, approvalCode int) (string, error) {
	log.L(c).Info("getApprovalTypeDefinition called, approvalCode: " + strconv.Itoa(approvalCode))

	db, _ := mysql.GetMySQLFactoryOr(nil)
	commonService := &commonService{store: db}
	approvalDefJson, err := commonService.GetDataByReqLarkAPI(c, "GET", LarkOpenAPIBase+"/approval/v4/approvals/"+modelv1.ApprovalCodeMap[approvalCode], nil, nil)
	if err != nil {
		return "", errors.Wrap(err, "failed to get approval definition, code: "+modelv1.ApprovalCodeMap[approvalCode])
	}
	apDef := modelv1.LarkApprovalDefinition{}

	if err = json.Unmarshal(approvalDefJson, &apDef); err != nil {
		log.L(c).Error(err.Error())

		return "", errors.WithCode(code.ErrDecodingJSON, "error when unmarshal lark approval data")
	}

	return apDef.Form, nil
}

func (s *approvalService) parseCloseAccount(ctx context.Context, user *modelv1.User, formDef []modelv1.LarkApprovalPart, formData interface{}, widgets *[]modelv1.LarkApprovalWidget) (string, MsgGenFn, error) {
	log.L(ctx).Info("fill approval form for CloseAccount")

	var cForm modelv1.CloseForm
	err := mapstructure.Decode(formData.(map[string]interface{}), &cForm)
	if err != nil {
		log.L(ctx).Error(err.Error())

		return "", nil, err
	}

	fwMap, fdMap := NormalizeFormWidgets(ctx, widgets, &formDef)

	r, err := s.store.CloudResource().Get(ctx, &modelv1.CloudResource{
		ManagementID: cForm.ManagementId,
	})
	if err != nil {
		log.L(ctx).Error("corresponding cloud resource not found" + err.Error())

		return "", nil, errors.WithCode(code.ErrResourceNotFound, "corresponding cloud resource not found")
	}

	if r.Owner.LarkUserID != user.LarkUserID {
		log.L(ctx).Error("only owner can close account, " + err.Error())

		return "", nil, errors.WithCode(code.ErrPermissionDenied, "only owner can close account")
	}

	var resForm []modelv1.FormItem

	// fill management id
	it, err := MatchLarkApprWidget(ctx, &fwMap, &fdMap, "managementID", cForm.ManagementId)
	if err != nil {
		return "", nil, err
	}
	resForm = append(resForm, *it)

	// fill provider
	it, err = MatchLarkApprWidget(ctx, &fwMap, &fdMap, "provider", r.Provider.Code)
	if err != nil {
		return "", nil, err
	}
	resForm = append(resForm, *it)

	// fill owner
	it, err = MatchLarkApprWidget(ctx, &fwMap, &fdMap, "owner", []string{r.Owner.LarkUserID})
	if err != nil {
		return "", nil, err
	}
	resForm = append(resForm, *it)

	// fill reason
	it, err = MatchLarkApprWidget(ctx, &fwMap, &fdMap, "reason", cForm.Description)
	if err != nil {
		return "", nil, err
	}
	resForm = append(resForm, *it)

	// fill close time
	it, err = MatchLarkApprWidget(ctx, &fwMap, &fdMap, "closeDate", cForm.CloseTime)
	if err != nil {
		return "", nil, err
	}
	resForm = append(resForm, *it)

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

		return "", nil, errors.WithCode(code.ErrDecodingJSON, "error when marshal lark approval form")
	}

	msg := genCloseAccountMsg(ctx, user, cForm.ManagementId)

	return string(resStr), msg, nil
}

func genCloseAccountMsg(ctx context.Context, user *modelv1.User, mid string) MsgGenFn {
	return genApprovalMsg(ctx, user, []string{"Close cloud account (project)", "关闭云账号（项目）"}, []string{"**ManagementID**: " + mid, "**ManagementID**: " + mid})
}

func (s *approvalService) parseInviteUser(ctx context.Context, user *modelv1.User, formDef []modelv1.LarkApprovalPart, formData interface{}, widgets *[]modelv1.LarkApprovalWidget) (string, MsgGenFn, error) {
	log.L(ctx).Info("fill approval form for TypeInviteUser")

	var iForm modelv1.InviteForm
	err := mapstructure.Decode(formData.(map[string]interface{}), &iForm)
	if err != nil {
		log.L(ctx).Error(err.Error())

		return "", nil, err
	}

	fwMap, fdMap := NormalizeFormWidgets(ctx, widgets, &formDef)

	r, err := s.store.CloudResource().Get(ctx, &modelv1.CloudResource{
		ManagementID: iForm.ManagementId,
	})
	if err != nil {
		log.L(ctx).Error("corresponding cloud resource not found" + err.Error())

		return "", nil, errors.WithCode(code.ErrResourceNotFound, "corresponding cloud resource not found")
	}
	if r.Owner.LarkUserID != user.LarkUserID {
		log.L(ctx).Error("only owner can invite member, " + err.Error())

		return "", nil, errors.WithCode(code.ErrPermissionDenied, "only owner can invite member")
	}

	var resForm []modelv1.FormItem

	// check and fill members
	memberVals := make([][]modelv1.FormItem, 0)
	for _, it := range iForm.InviteUsers {
		if it.Member == "" || it.RoleCode == "" {
			log.L(ctx).Error("invalid member or role" + err.Error())

			return "", nil, errors.WithCode(code.ErrValidation, "invalid member or role")
		}

		memberItem := make([]modelv1.FormItem, 0)
		// fill member lark id
		mem, err := MatchLarkApprWidget(ctx, &fwMap, &fdMap, "member", []string{it.Member})
		if err != nil {
			return "", nil, err
		}
		memberItem = append(memberItem, *mem)
		// fill role
		roleCode, err := MatchLarkApprWidget(ctx, &fwMap, &fdMap, "memberRole", []string{it.RoleCode})
		if err != nil {
			return "", nil, err
		}
		memberItem = append(memberItem, *roleCode)

		memberVals = append(memberVals, memberItem)
	}
	// fill members
	it, err := MatchLarkApprWidget(ctx, &fwMap, &fdMap, "members", memberVals)
	if err != nil {
		return "", nil, err
	}
	resForm = append(resForm, *it)

	// fill management id
	it, err = MatchLarkApprWidget(ctx, &fwMap, &fdMap, "managementID", iForm.ManagementId)
	if err != nil {
		return "", nil, err
	}
	resForm = append(resForm, *it)

	// fill provider
	it, err = MatchLarkApprWidget(ctx, &fwMap, &fdMap, "provider", r.Provider.Code)
	if err != nil {
		return "", nil, err
	}
	resForm = append(resForm, *it)

	// fill reason
	it, err = MatchLarkApprWidget(ctx, &fwMap, &fdMap, "reason", iForm.Description)
	if err != nil {
		return "", nil, err
	}
	resForm = append(resForm, *it)

	// fill cloudInsID
	it, err = MatchLarkApprWidget(ctx, &fwMap, &fdMap, "cloudInsID", r.CloudInsID)
	if err != nil {
		return "", nil, err
	}
	resForm = append(resForm, *it)

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

		return "", nil, errors.WithCode(code.ErrDecodingJSON, "error when marshal lark approval form")
	}

	msg := genInviteMemberMsg(ctx, user, iForm.ManagementId)

	return string(resStr), msg, nil
}

func genInviteMemberMsg(ctx context.Context, user *modelv1.User, mid string) MsgGenFn {
	return genApprovalMsg(ctx, user, []string{"Invite users to the cloud account (project)", "邀请加入云账号（项目）"}, []string{"**ManagementID**: " + mid, "**ManagementID**: " + mid})
}

//nolint:funlen,gocyclo,cyclop // complex
func (s *approvalService) parseApplyAccount(ctx context.Context, user *modelv1.User, formDef []modelv1.LarkApprovalPart, formData interface{}, widgets *[]modelv1.LarkApprovalWidget) (string, MsgGenFn, error) {
	log.L(ctx).Info("fill approval form for TypeApplyAccount")

	var aForm modelv1.ApplyForm
	err := mapstructure.Decode(formData.(map[string]interface{}), &aForm)
	if err != nil {
		log.L(ctx).Error(err.Error())

		return "", nil, err
	}

	// check name valid
	aForm.Name = strings.TrimSpace(aForm.Name)
	if aForm.Name == "" {
		log.L(ctx).Error("empty name")

		return "", nil, errors.WithCode(code.ErrValidation, "empty name")
	}
	if matched, err := regexp.Match("^[A-Za-z0-9-\\s]+$", []byte(aForm.Name)); err != nil || !matched {
		log.L(ctx).Error("invalid name")

		return "", nil, errors.WithCode(code.ErrValidation, "invalid name")
	}

	fwMap, fdMap := NormalizeFormWidgets(ctx, widgets, &formDef)

	var resForm []modelv1.FormItem

	// fill budget
	budgetVals := make([][]modelv1.FormItem, 0)
	var total float64
	for _, bit := range aForm.BudgetItems {
		budgetItem := make([]modelv1.FormItem, 0)
		// fill month
		mon, err := MatchLarkApprWidget(ctx, &fwMap, &fdMap, "budgetMonth", bit.Month)
		if err != nil {
			return "", nil, err
		}
		budgetItem = append(budgetItem, *mon)
		// fill amount
		amount, err := MatchLarkApprWidget(ctx, &fwMap, &fdMap, "budgetAmount", bit.Budget)
		if err != nil {
			return "", nil, err
		}
		total += bit.Budget
		budgetItem = append(budgetItem, *amount)

		budgetVals = append(budgetVals, budgetItem)
	}

	// fill budget items
	it, err := MatchLarkApprWidget(ctx, &fwMap, &fdMap, "budgets", budgetVals)
	if err != nil {
		return "", nil, err
	}
	resForm = append(resForm, *it)
	// fill total
	tot, err := MatchLarkApprWidget(ctx, &fwMap, &fdMap, "totalAmount", total)
	if err != nil {
		return "", nil, err
	}
	resForm = append(resForm, *tot)

	// fill name
	it, err = MatchLarkApprWidget(ctx, &fwMap, &fdMap, "name", aForm.Name)
	if err != nil {
		return "", nil, err
	}
	resForm = append(resForm, *it)

	// fill owner
	it, err = MatchLarkApprWidget(ctx, &fwMap, &fdMap, "owner", []string{user.LarkUserID})
	if err != nil {
		return "", nil, err
	}
	resForm = append(resForm, *it)

	// fill provider
	it, err = MatchLarkApprWidget(ctx, &fwMap, &fdMap, "provider", aForm.Provider)
	if err != nil {
		return "", nil, err
	}
	resForm = append(resForm, *it)

	// fill desc
	it, err = MatchLarkApprWidget(ctx, &fwMap, &fdMap, "desc", aForm.Description)
	if err != nil {
		return "", nil, err
	}
	resForm = append(resForm, *it)

	// fill departmentOpenID
	d, err := s.store.Department().GetByLarkID(ctx, aForm.Department)
	if err != nil {
		log.L(ctx).Error("corresponding department not found" + err.Error())

		return "", nil, errors.WithCode(code.ErrRecordNotFound, "corresponding department not found")
	}
	it, err = MatchLarkApprWidget(ctx, &fwMap, &fdMap, "department", []struct {
		OpenID string `json:"open_id"`
	}{{OpenID: d.OpenLarkID}})
	if err != nil {
		return "", nil, err
	}
	resForm = append(resForm, *it)

	// gen and fill managementID
	managementID, err := s.genManagementID(ctx, aForm.Provider, aForm.Name)
	if err != nil {
		return "", nil, err
	}
	it, err = MatchLarkApprWidget(ctx, &fwMap, &fdMap, "managementID", managementID)
	if err != nil {
		return "", nil, err
	}
	resForm = append(resForm, *it)

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

		return "", nil, errors.WithCode(code.ErrDecodingJSON, "error when marshal lark approval form")
	}

	msg := genApplyAccountMsg(ctx, user, aForm.Name)

	return string(resStr), msg, nil
}

func (s *approvalService) genManagementID(ctx context.Context, providerCode, name string) (string, error) {
	name = strings.ReplaceAll(name, " ", "-")
	name = strings.ToLower(name)
	tryName := providerCode + "-" + name
	for i := 0; i < 100; i++ {
		_, err := s.store.CloudResource().Get(ctx, &modelv1.CloudResource{
			ManagementID: tryName,
		})
		if err != nil {
			coder := errors.ParseCoder(err)
			if coder.Code() != code.ErrRecordNotFound {
				e := errors.Wrap(err, "Error when exec CloudResourceStore.Get")
				log.L(ctx).Error(e.Error())

				return "", e
			}

			return tryName, nil
		}

		postfix := strconv.Itoa(rand.Intn(1000))
		tryName = providerCode + "-" + name + "-" + postfix
	}

	return "", errors.WithCode(code.ErrUnknown, "too many same name")
}

func genApplyAccountMsg(ctx context.Context, user *modelv1.User, name string) MsgGenFn {
	return genApprovalMsg(ctx, user, []string{"Application for opening a cloud account (cloud project)", "开通云账号（项目）申请"}, []string{"**Name**: " + name, "**Name**: " + name})
}

//nolint:funlen,gocognit,gocyclo,cyclop // complex
func (s *approvalService) parseChangeBudget(ctx context.Context, user *modelv1.User, formDef []modelv1.LarkApprovalPart, formData interface{}, widgets *[]modelv1.LarkApprovalWidget) (string, MsgGenFn, error) {
	log.L(ctx).Info("fill approval form for TypeChangeBudget")

	var cbForm modelv1.BudgetChangeForm
	err := mapstructure.Decode(formData.(map[string]interface{}), &cbForm)
	if err != nil {
		log.L(ctx).Error(err.Error())

		return "", nil, err
	}

	fwMap, fdMap := NormalizeFormWidgets(ctx, widgets, &formDef)

	r, err := s.store.CloudResource().Get(ctx, &modelv1.CloudResource{
		ManagementID: cbForm.ManagementId,
	})
	if err != nil {
		log.L(ctx).Error("corresponding cloud resource not found" + err.Error())

		return "", nil, errors.WithCode(code.ErrResourceNotFound, "corresponding cloud resource not found")
	}

	cloudResourceSrv := &cloudResourceService{store: s.store}
	members, err := cloudResourceSrv.DetailMembers(ctx, r.ID, "")
	if err != nil {
		log.L(ctx).Error("corresponding members not found" + err.Error())

		return "", nil, errors.WithCode(code.ErrNotFound, "corresponding members not found")
	}
	isMem := false
	for _, m := range *members {
		if m.Member.LarkUserID == user.LarkUserID {
			isMem = true

			break
		}
	}

	if !isMem && r.Owner.LarkUserID != user.LarkUserID {
		log.L(ctx).Error("only member can change budget, " + err.Error())

		return "", nil, errors.WithCode(code.ErrPermissionDenied, "only member can change budget")
	}

	bs, err := s.store.CloudBudget().ListLatestFYResourceBudgets(ctx, r.ID, 2)
	if err != nil {
		log.L(ctx).Error("error when find corresponding budgets, " + err.Error())

		return "", nil, err
	}

	nbs := make(map[string]*modelv1.BudgetCalcItem)
	for _, b := range bs {
		nbs[b.Month] = b
	}

	var resForm []modelv1.FormItem

	// check and fill budget
	budgetVals := make([][]modelv1.FormItem, 0)
	var total float64
	for _, bit := range cbForm.BudgetItems {
		// check current month budget
		//if bit.Month == time.Now().UTC().Format("2006-01") {
		//	err := s.checkOverBudget(ctx, r, bit.After)
		//	if err != nil {
		//		return "", nil, err
		//	}
		//}

		budgetItem := make([]modelv1.FormItem, 0)

		if cbForm.BudgetType == modelv1.BudgetApprTypeNew { //nolint:nestif // complex
			// fill month
			mon, err := MatchLarkApprWidget(ctx, &fwMap, &fdMap, "newItemMonth", bit.Month)
			if err != nil {
				return "", nil, err
			}
			budgetItem = append(budgetItem, *mon)
			// fill amount
			amount, err := MatchLarkApprWidget(ctx, &fwMap, &fdMap, "newItemAmount", bit.After)
			if err != nil {
				return "", nil, err
			}
			total += bit.After
			budgetItem = append(budgetItem, *amount)
		} else if cbForm.BudgetType == modelv1.BudgetApprTypeChange {
			// fill month
			mon, err := MatchLarkApprWidget(ctx, &fwMap, &fdMap, "adjustItemMonth", bit.Month)
			if err != nil {
				return "", nil, err
			}
			budgetItem = append(budgetItem, *mon)
			// fill before
			bef, ok := nbs[bit.Month]
			if !ok {
				return "", nil, errors.WithCode(code.ErrBudgetNotFound, "budget not found for month "+bit.Month)
			}
			be, err := MatchLarkApprWidget(ctx, &fwMap, &fdMap, "adjustItemBefore", bef.Amount)
			if err != nil {
				return "", nil, err
			}
			budgetItem = append(budgetItem, *be)
			// fill after
			af, err := MatchLarkApprWidget(ctx, &fwMap, &fdMap, "adjustItemAfter", bit.After)
			if err != nil {
				return "", nil, err
			}
			total += bit.After - bef.Amount
			budgetItem = append(budgetItem, *af)
		} else {
			return "", nil, errors.WithCode(code.ErrValidation, "invalid budget type")
		}

		budgetVals = append(budgetVals, budgetItem)
	}
	// fill items
	typekey := "newItems"
	typeText := "New budget"
	if cbForm.BudgetType == modelv1.BudgetApprTypeChange {
		typekey = "adjustItems"
		typeText = "Adjust existing budget"
	}
	it, err := MatchLarkApprWidget(ctx, &fwMap, &fdMap, typekey, budgetVals)
	if err != nil {
		return "", nil, err
	}
	resForm = append(resForm, *it)
	// fill total
	tot, err := MatchLarkApprWidget(ctx, &fwMap, &fdMap, "totalAmount", total)
	if err != nil {
		return "", nil, err
	}
	resForm = append(resForm, *tot)

	// fill managementID
	it, err = MatchLarkApprWidget(ctx, &fwMap, &fdMap, "managementID", cbForm.ManagementId)
	if err != nil {
		return "", nil, err
	}
	resForm = append(resForm, *it)

	// fill budget type
	it, err = MatchLarkApprWidgetRadio(ctx, &fwMap, &fdMap, "type", typeText)
	if err != nil {
		return "", nil, err
	}
	resForm = append(resForm, *it)

	// fill reason
	it, err = MatchLarkApprWidget(ctx, &fwMap, &fdMap, "reason", cbForm.Description)
	if err != nil {
		return "", nil, err
	}
	resForm = append(resForm, *it)

	// fill owner
	it, err = MatchLarkApprWidget(ctx, &fwMap, &fdMap, "owner", []string{r.Owner.LarkUserID})
	if err != nil {
		return "", nil, err
	}
	resForm = append(resForm, *it)

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

		return "", nil, errors.WithCode(code.ErrDecodingJSON, "error when marshal lark approval form")
	}

	msg := genChangeBudgetMsg(ctx, user, cbForm.ManagementId)

	return string(resStr), msg, nil
}

func genChangeBudgetMsg(ctx context.Context, user *modelv1.User, mid string) MsgGenFn {
	return genApprovalMsg(ctx, user, []string{"Cloud account (project) budget change application", "云账号（项目）预算变动"}, []string{"**ManagementID**: " + mid, "**ManagementID**: " + mid})
}

func (s *approvalService) parseJoinAccount(ctx context.Context, user *modelv1.User, formDef []modelv1.LarkApprovalPart, formData interface{}, widgets *[]modelv1.LarkApprovalWidget) (string, MsgGenFn, error) {
	log.L(ctx).Info("fill approval form for TypeJoinAccount")

	var joinForm modelv1.JoinForm
	err := mapstructure.Decode(formData.(map[string]interface{}), &joinForm)
	if err != nil {
		log.L(ctx).Error(err.Error())

		return "", nil, err
	}

	fwMap, fdMap := NormalizeFormWidgets(ctx, widgets, &formDef)

	r, err := s.store.CloudResource().Get(ctx, &modelv1.CloudResource{
		ManagementID: joinForm.ManagementId,
	})
	if err != nil {
		log.L(ctx).Error("corresponding cloud resource not found" + err.Error())

		return "", nil, errors.WithCode(code.ErrResourceNotFound, "corresponding cloud resource not found")
	}

	var resForm []modelv1.FormItem

	// fill managementID
	it, err := MatchLarkApprWidget(ctx, &fwMap, &fdMap, "managementID", joinForm.ManagementId)
	if err != nil {
		return "", nil, err
	}
	resForm = append(resForm, *it)

	// fill owner
	it, err = MatchLarkApprWidget(ctx, &fwMap, &fdMap, "owner", []string{r.Owner.LarkUserID})
	if err != nil {
		return "", nil, err
	}
	resForm = append(resForm, *it)

	// fill accProjID
	it, err = MatchLarkApprWidget(ctx, &fwMap, &fdMap, "cloudInsID", r.CloudInsID)
	if err != nil {
		return "", nil, err
	}
	resForm = append(resForm, *it)

	// fill provider
	it, err = MatchLarkApprWidget(ctx, &fwMap, &fdMap, "provider", r.Provider.Code)
	if err != nil {
		return "", nil, err
	}
	resForm = append(resForm, *it)

	// fill note
	it, err = MatchLarkApprWidget(ctx, &fwMap, &fdMap, "note", joinForm.Description)
	if err != nil {
		return "", nil, err
	}
	resForm = append(resForm, *it)

	// fill roles
	it, err = MatchLarkApprWidget(ctx, &fwMap, &fdMap, "roles", joinForm.RoleCodes)
	if err != nil {
		return "", nil, err
	}
	resForm = append(resForm, *it)

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

		return "", nil, errors.WithCode(code.ErrDecodingJSON, "error when marshal lark approval form")
	}

	msg := genJoinAccountMsg(ctx, user, joinForm.ManagementId)

	return string(resStr), msg, nil
}

func genJoinAccountMsg(ctx context.Context, user *modelv1.User, mid string) MsgGenFn {
	return genApprovalMsg(ctx, user, []string{"Application for access to cloud account (project)", "加入云账号（项目）申请"}, []string{"**ManagementID**: " + mid, "**ManagementID**: " + mid})
}

func (s *approvalService) parseRemoveUser(ctx context.Context, user *modelv1.User, formDef []modelv1.LarkApprovalPart, formData interface{}, widgets *[]modelv1.LarkApprovalWidget) (string, MsgGenFn, error) {
	log.L(ctx).Info("fill approval form for TypeRemoveUser")

	var iForm modelv1.RemoveForm
	err := mapstructure.Decode(formData.(map[string]interface{}), &iForm)
	if err != nil {
		log.L(ctx).Error(err.Error())

		return "", nil, err
	}

	fwMap, fdMap := NormalizeFormWidgets(ctx, widgets, &formDef)

	r, err := s.store.CloudResource().Get(ctx, &modelv1.CloudResource{
		ManagementID: iForm.ManagementId,
	})
	if err != nil {
		log.L(ctx).Error("corresponding cloud resource not found" + err.Error())

		return "", nil, errors.WithCode(code.ErrResourceNotFound, "corresponding cloud resource not found")
	}

	if r.Owner.LarkUserID != user.LarkUserID {
		log.L(ctx).Error("only owner can remove member, " + err.Error())

		return "", nil, errors.WithCode(code.ErrPermissionDenied, "only owner can invite member")
	}

	var resForm []modelv1.FormItem

	// check and fill members
	var members []string
	members = append(members, iForm.Members...)

	// fill members
	it, err := MatchLarkApprWidget(ctx, &fwMap, &fdMap, "members", members)
	if err != nil {
		return "", nil, err
	}
	resForm = append(resForm, *it)

	// fill management id
	it, err = MatchLarkApprWidget(ctx, &fwMap, &fdMap, "managementID", iForm.ManagementId)
	if err != nil {
		return "", nil, err
	}
	resForm = append(resForm, *it)

	// fill provider
	it, err = MatchLarkApprWidget(ctx, &fwMap, &fdMap, "provider", r.Provider.Code)
	if err != nil {
		return "", nil, err
	}
	resForm = append(resForm, *it)

	// fill owner
	it, err = MatchLarkApprWidget(ctx, &fwMap, &fdMap, "owner", []string{user.LarkUserID})
	if err != nil {
		return "", nil, err
	}
	resForm = append(resForm, *it)

	// fill reason
	it, err = MatchLarkApprWidget(ctx, &fwMap, &fdMap, "reason", iForm.Description)
	if err != nil {
		return "", nil, err
	}
	resForm = append(resForm, *it)

	// fill cloudInsID
	it, err = MatchLarkApprWidget(ctx, &fwMap, &fdMap, "cloudInsID", r.CloudInsID)
	if err != nil {
		return "", nil, err
	}
	resForm = append(resForm, *it)

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

		return "", nil, errors.WithCode(code.ErrDecodingJSON, "error when marshal lark approval form")
	}

	msg := genRemoveUserMsg(ctx, user, iForm.ManagementId)

	return string(resStr), msg, nil
}

func genRemoveUserMsg(ctx context.Context, user *modelv1.User, mid string) MsgGenFn {
	return genApprovalMsg(ctx, user, []string{"Remove users from the cloud account (project)", "移除云账号（项目）Member"}, []string{"**ManagementID**: " + mid, "**ManagementID**: " + mid})
}

func MatchLarkApprWidget(ctx context.Context, fwMap *map[string]modelv1.LarkApprovalWidget, fdMap *map[string]modelv1.LarkApprovalPartChild, k string, v interface{}) (*modelv1.FormItem, error) {
	widgetID := (*fwMap)[k].LarkWidgetID
	widgetType := (*fdMap)[widgetID].Type
	if widgetID == "" || widgetType == "" {
		err := errors.WithCode(code.ErrValidation, k+"-larkWidgetID or corresponding widget not found")
		log.L(ctx).Error(err.Error())

		return nil, err
	}

	return &modelv1.FormItem{
		Id:    widgetID,
		Type:  widgetType,
		Value: v,
	}, nil
}

func MatchLarkApprWidgetRadio(ctx context.Context, fwMap *map[string]modelv1.LarkApprovalWidget, fdMap *map[string]modelv1.LarkApprovalPartChild, k, vtext string) (*modelv1.FormItem, error) {
	widgetID := (*fwMap)[k].LarkWidgetID
	widgetType := (*fdMap)[widgetID].Type
	if widgetID == "" || widgetType == "" {
		err := errors.WithCode(code.ErrValidation, k+"-larkWidgetID or corresponding widget not found")
		log.L(ctx).Error(err.Error())

		return nil, err
	}

	if widgetType != "radioV2" {
		err := errors.WithCode(code.ErrValidation, "MatchLarkApprWidgetRadio only support radioV2")
		log.L(ctx).Error(err.Error())

		return nil, err
	}

	var v interface{}
	for _, opt := range (*fdMap)[widgetID].Option {
		if opt.Text == vtext {
			v = opt.Value

			break
		}
	}

	return &modelv1.FormItem{
		Id:    widgetID,
		Type:  widgetType,
		Value: v,
	}, nil
}

func MatchLarkApprWidgetCheckbox(ctx context.Context, fwMap *map[string]modelv1.LarkApprovalWidget, fdMap *map[string]modelv1.LarkApprovalPart, k string, vtexts []string) (*modelv1.FormItem, error) {
	widgetID := (*fwMap)[k].LarkWidgetID
	widgetType := (*fdMap)[widgetID].Type
	if widgetID == "" || widgetType == "" {
		err := errors.WithCode(code.ErrValidation, k+"-larkWidgetID or corresponding widget not found")
		log.L(ctx).Error(err.Error())

		return nil, err
	}

	if widgetType != "checkboxV2" {
		err := errors.WithCode(code.ErrValidation, "MatchLarkApprWidgetCheckbox only support checkboxV2")
		log.L(ctx).Error(err.Error())

		return nil, err
	}

	var v []string
	nameValMap := make(map[string]string)
	for _, opt := range (*fdMap)[widgetID].Option {
		nameValMap[opt.Text] = opt.Value
	}
	for _, vtext := range vtexts {
		if val, ok := nameValMap[vtext]; ok {
			v = append(v, val)
		}
	}

	return &modelv1.FormItem{
		Id:    widgetID,
		Type:  widgetType,
		Value: v,
	}, nil
}

func NormalizeFormWidgets(_ context.Context, widgets *[]modelv1.LarkApprovalWidget, formDef *[]modelv1.LarkApprovalPart) (map[string]modelv1.LarkApprovalWidget, map[string]modelv1.LarkApprovalPartChild) {
	fwMap := make(map[string]modelv1.LarkApprovalWidget)
	for _, widget := range *widgets {
		fwMap[widget.FieldCode] = widget
	}
	fdMap := make(map[string]modelv1.LarkApprovalPartChild)
	for _, f := range *formDef {
		fdMap[f.Id] = modelv1.LarkApprovalPartChild{
			Id:      f.Id,
			Name:    f.Name,
			Type:    f.Type,
			Value:   f.Value,
			OpenIds: f.OpenIds,
			Ext:     f.Ext,
			Option:  f.Option,
		}

		if f.Children != nil && len(f.Children) > 0 {
			for _, c := range f.Children {
				fdMap[c.Id] = modelv1.LarkApprovalPartChild{
					Id:      c.Id,
					Name:    c.Name,
					Type:    c.Type,
					Value:   c.Value,
					OpenIds: c.OpenIds,
					Ext:     c.Ext,
					Option:  c.Option,
				}
			}
		}
	}

	return fwMap, fdMap
}

type MsgGenFn func(string) *ApprovalSuccMsg

func genApprovalMsg(_ context.Context, user *modelv1.User, titles, summaries []string) MsgGenFn {
	return func(approvalInsID string) *ApprovalSuccMsg {
		return &ApprovalSuccMsg{
			UUID:            uuid.Must(uuid.NewV4(), nil).String(),
			TemplateID:      "1005",
			UserID:          user.LarkUserID,
			TitleUserIDType: "user_id",
			OpenID:          user.LarkOpenID,
			ApprovalName:    "@i18n@1",
			Note:            "@i18n@2",
			Actions: []ApprovalAction{
				{
					ActionName: "DETAIL",
					// these urls are from: https://bytedance.feishu.cn/wiki/wikcnCyfarmbKDsnWbrqI2Vyxvc
					URL:        "https://applink.feishu.cn/client/mini_program/open?appId=cli_9cb844403dbb9108&path=pages%2Fdetail%2Findex%3FinstanceId%3D" + approvalInsID,
					AndroidURL: "https://applink.feishu.cn/client/mini_program/open?appId=cli_9cb844403dbb9108&path=pages%2Fdetail%2Findex%3FinstanceId%3D" + approvalInsID,
					IOSURL:     "https://applink.feishu.cn/client/mini_program/open?appId=cli_9cb844403dbb9108&path=pages%2Fdetail%2Findex%3FinstanceId%3D" + approvalInsID,
					PCURL:      "https://applink.feishu.cn/client/mini_program/open?mode=sidebar-semi&appId=cli_9cb844403dbb9108&path=pages%2Fdetail%2Findex%3FinstanceId%3D" + approvalInsID,
				},
			},
			I18nResources: []I18nResource{
				{
					Locale:    "en_us",
					IsDefault: false,
					Texts: map[string]string{
						"@i18n@1": titles[0],
						"@i18n@2": "via PCHub (pch.pingcap.net)",
						"@i18n@3": summaries[0],
					},
				},
				{
					Locale:    "zh_cn",
					IsDefault: true,
					Texts: map[string]string{
						"@i18n@1": titles[1],
						"@i18n@2": "发起于 PCHub (pch.pingcap.net)",
						"@i18n@3": summaries[1],
					},
				},
			},
			Content: ApprMsgContent{
				Summaries: []SummaryIt{
					{
						Summary: "@i18n@3",
					},
				},
			},
		}
	}
}
