package oneconsole

import (
	"context"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"sort"
	"strings"
	"time"

	"github.com/tidbcloud/account/internel/svc/token"

	"github.com/duke-git/lancet/v2/slice"
	"github.com/tidbcloud/account/commons/database"
	"github.com/tidbcloud/account/internel/dao"
	"github.com/tidbcloud/account/internel/dao/entity"
	"github.com/tidbcloud/account/internel/dao/history"
	daorole "github.com/tidbcloud/account/internel/dao/role"
	daouser "github.com/tidbcloud/account/internel/dao/user"
	historysvc "github.com/tidbcloud/account/internel/svc/history"
	"github.com/tidbcloud/account/internel/svc/user"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
)

type ListUsersSource int

const (
	ListUsersSourceConsole ListUsersSource = iota
	ListUsersSourceOpenApi
)

const (
	MaxPageSize   = 100
	AscOrder      = "asc"
	DescOrder     = "desc"
	LoginTimeSort = "login_time"
)

var (
	ListUsersSupportSortField = []string{LoginTimeSort, daouser.SortByEmail, daouser.SortByInviteTime}
)

type ListUsersParam struct {
	OrgId      uint64
	ScopeType  entity.ScopeType
	ScopeId    uint64
	OperatorId uint64
	Email      string
	Roles      []string
	Statuses   []string
	SortBy     string
	Order      string
	PageSize   int32
	PageNumber uint32
	PageToken  string
	Source     ListUsersSource
}

type ListUsersRet struct {
	Users         []ListUsersItem
	TotalSize     int32
	NextPageToken string
}

type ListUsersItem struct {
	UserId        uint64
	Email         string
	FirstName     string
	LastName      string
	Status        string
	OrgRole       *ListUsersRoleItem
	ProjectRoles  []ListUsersRoleItem
	ClusterRoles  []ListUsersRoleItem
	InviteTime    time.Time
	LastLoginTime *time.Time
}

type ListUsersRoleItem struct {
	ScopeType string
	ScopeId   uint64
	RbacRole  string
}

func (s *OneConsoleUserSvc) ListUsers(ctx context.Context, param *ListUsersParam) (*ListUsersRet, error) {
	tilog.Info(ctx, "OneConsole ListUsers", zap.Any("param", param))

	if !slice.Contain(ListUsersSupportSortField, param.SortBy) {
		param.SortBy = daouser.SortByEmail
	}
	lowerOrder := strings.ToLower(param.Order)
	if lowerOrder != AscOrder && lowerOrder != DescOrder {
		param.Order = AscOrder
	}

	f := daouser.RoleBindingUserFilter{
		RoleBindingStatus: []string{entity.RoleBindingStatusActive, entity.RoleBindingStatusInactive},
		UserStatus:        []string{string(entity.UserStatusActive), string(entity.UserStatusPending)},
		ScopeType:         param.ScopeType,
		ScopeId:           param.ScopeId,
		Order:             param.Order,
		SortBy:            param.SortBy,
		Roles:             param.Roles,
		EmailLike:         param.Email,
	}

	// console non org:owner/org:audit:admin user only can view himself
	if param.Source == ListUsersSourceConsole {
		operatorOrgRole, err := s.getOperatorOrgRole(ctx, param.OperatorId, param.OrgId)
		if err != nil {
			tilog.Info(ctx, "OneConsole ListUsers getOperatorOrgRole error", zap.Any("err", err))
			return nil, err
		}
		if operatorOrgRole != entity.RoleOrgOwner && operatorOrgRole != entity.RoleOrgAuditAdmin {
			f.UserId = param.OperatorId
		}
	}

	userSvc, _ := user.GetUserSvc()
	rbUsers, err := userSvc.ListRoleBindingUsers(ctx, f)
	if err != nil {
		tilog.Error(ctx, "OneConsole ListUsers ListRoleBindingUsers error", zap.Any("filter", f), zap.Any("err", err))
		return nil, err
	}

	rbUsers = filterSystemAndStatus(rbUsers, param.Statuses)

	loginHistoryMap, err := fetchLoginHistoryMap(ctx, rbUsers)
	if err != nil {
		tilog.Warn(ctx, "OneConsole ListUsers fetchLoginHistory error", zap.Any("err", err))
	}

	items := convertRbUsersToItems(ctx, rbUsers, param.ScopeType, param.ScopeId, loginHistoryMap)

	if param.SortBy == LoginTimeSort {
		sortByLoginTime(items, param.Order)
	}

	totalSize := int32(len(items))
	var pageItems []ListUsersItem
	var nextPageToken string

	if param.Source == ListUsersSourceOpenApi {
		pageItems, nextPageToken = paginateByToken(items, param.PageSize, param.PageToken)
	} else {
		pageItems = paginateByPage(items, param.PageNumber, param.PageSize)
	}

	if len(pageItems) > 0 {
		err = fillOtherScopeRoles(ctx, pageItems, param.ScopeType, param.OrgId)
		if err != nil {
			tilog.Error(ctx, "ListUsers fillOtherScopeRoles error", zap.Any("err", err))
			return nil, err
		}
	}

	if pageItems == nil {
		pageItems = []ListUsersItem{}
	}

	return &ListUsersRet{
		Users:         pageItems,
		TotalSize:     totalSize,
		NextPageToken: nextPageToken,
	}, nil
}

func (s *OneConsoleUserSvc) getOperatorOrgRole(ctx context.Context, operatorId, orgId uint64) (string, error) {
	oBindings, err := dao.GetDaoHub().Role.ListRolesByTargetAndScope(ctx, database.DBConn.DB,
		entity.RoleBindingTargetUser, operatorId, entity.RoleBindingScopeOrg, orgId, []string{entity.RoleBindingStatusActive})
	if err != nil {
		tilog.Error(ctx, "ListUsers list org role binding error", zap.Any("err", err))
		return "", err
	}
	if len(oBindings) == 0 {
		return "", nil
	}
	return oBindings[0].Role, nil
}

func filterSystemAndStatus(rbUsers []daouser.RoleBindingUser, statuses []string) []daouser.RoleBindingUser {
	var lowerStatus []string
	if len(statuses) > 0 {
		for _, s := range statuses {
			ls := strings.ToLower(s)
			if ls == entity.RoleBindingStatusPending {
				ls = strings.ToLower(entity.RoleBindingStatusInactive)
			}
			lowerStatus = append(lowerStatus, ls)
		}
	}
	var result []daouser.RoleBindingUser
	for _, rbu := range rbUsers {
		if rbu.FirstName == "system" && rbu.LastName == "user" {
			continue
		}
		if len(lowerStatus) > 0 && !slice.Contain(lowerStatus, strings.ToLower(rbu.Status)) {
			continue
		}
		result = append(result, rbu)
	}
	return result
}

func fetchLoginHistoryMap(ctx context.Context, rbUsers []daouser.RoleBindingUser) (map[uint64]*entity.EventHistoryLast, error) {
	var uids []entity.ID
	for _, rbu := range rbUsers {
		uids = append(uids, entity.ID(rbu.UserId))
	}
	historyMap := make(map[uint64]*entity.EventHistoryLast)
	if len(uids) == 0 {
		return historyMap, nil
	}
	historySvc, _ := historysvc.GetHistorySvc()
	histories, err := historySvc.ListEventHistoryLast(ctx, history.EventHistoryFilter{
		IdentityIDs:   uids,
		IdentityTypes: []entity.HistoryIdentityType{entity.IdentityTypeUser},
		Events:        []entity.HistoryEvent{entity.HistoryEventLogin},
	})
	if err != nil {
		return nil, err
	}
	for i, h := range histories {
		historyMap[h.IdentityID.Uint64()] = &histories[i]
	}
	return historyMap, nil
}

func convertRbUsersToItems(ctx context.Context, rbUsers []daouser.RoleBindingUser, scopeType entity.ScopeType, scopeId uint64, historyMap map[uint64]*entity.EventHistoryLast) []ListUsersItem {
	var items []ListUsersItem
	for _, rbu := range rbUsers {
		status := strings.ToUpper(rbu.Status)
		if status == strings.ToUpper(entity.RoleBindingStatusInactive) {
			status = strings.ToUpper(entity.RoleBindingStatusPending)
		}

		var loginTime *time.Time
		h := historyMap[rbu.UserId]
		if h != nil && status == strings.ToUpper(entity.RoleBindingStatusActive) && h.UpdatedAt != nil {
			t := *h.UpdatedAt
			loginTime = &t
		}

		item := ListUsersItem{
			UserId:        rbu.UserId,
			Email:         rbu.Email,
			FirstName:     rbu.FirstName,
			LastName:      rbu.LastName,
			Status:        status,
			InviteTime:    rbu.InviteTime,
			LastLoginTime: loginTime,
		}

		if rbu.Role != "" {
			roleItem := ListUsersRoleItem{
				ScopeType: scopeType.String(),
				ScopeId:   scopeId,
				RbacRole:  rbu.Role,
			}
			switch scopeType {
			case entity.RoleBindingScopeOrg:
				item.OrgRole = &roleItem
			case entity.RoleBindingScopeProject:
				item.ProjectRoles = []ListUsersRoleItem{roleItem}
			case entity.RoleBindingScopeTiDB:
				item.ClusterRoles = []ListUsersRoleItem{roleItem}
			}
		}

		items = append(items, item)
	}
	return items
}

func sortByLoginTime(items []ListUsersItem, order string) {
	orderStr := AscOrder
	if order != "" {
		orderStr = order
	}
	sort.SliceStable(items, func(i, j int) bool {
		a := items[i].LastLoginTime
		b := items[j].LastLoginTime
		if orderStr == AscOrder {
			if a == nil {
				return true
			}
			if b == nil {
				return false
			}
			return a.Before(*b)
		}
		if b == nil {
			return true
		}
		if a == nil {
			return false
		}
		return b.Before(*a)
	})
}

type pageTokenData struct {
	Offset    int32     `json:"o"`
	CreatedAt time.Time `json:"t"`
}

func encodePageToken(offset int32) (string, error) {
	session, err := token.GetSession()
	if err != nil {
		return "", err
	}
	data := pageTokenData{
		Offset:    offset,
		CreatedAt: time.Now().Truncate(time.Second),
	}
	payload, err := json.Marshal(data)
	if err != nil {
		return "", err
	}
	encrypted := session.AESCrypt.Encrypt(payload)
	return base64.URLEncoding.EncodeToString(encrypted), nil
}

func decodePageToken(pageToken string) (int32, error) {
	session, err := token.GetSession()
	if err != nil {
		return 0, err
	}
	ciphertext, err := base64.URLEncoding.DecodeString(pageToken)
	if err != nil {
		return 0, fmt.Errorf("invalid page token: %w", err)
	}
	decrypted := session.AESCrypt.Decrypt(ciphertext)
	var data pageTokenData
	if err := json.Unmarshal(decrypted, &data); err != nil {
		return 0, fmt.Errorf("invalid page token: %w", err)
	}
	return data.Offset, nil
}

func paginateByToken(items []ListUsersItem, pageSize int32, pageToken string) ([]ListUsersItem, string) {
	ps := int32(MaxPageSize)
	if pageSize > 0 {
		ps = pageSize
	}
	if ps > MaxPageSize {
		ps = MaxPageSize
	}
	var startIdx int32 = 0
	if pageToken != "" {
		offset, err := decodePageToken(pageToken)
		if err != nil {
			tilog.Info(context.Background(), "paginateByToken decodePageToken error", zap.Error(err))
		} else {
			startIdx = offset
		}
	}
	total := int32(len(items))
	if startIdx >= total {
		return []ListUsersItem{}, ""
	}
	endIdx := startIdx + ps
	if endIdx > total {
		endIdx = total
	}
	nextToken := ""
	if endIdx < total {
		var err error
		nextToken, err = encodePageToken(endIdx)
		if err != nil {
			tilog.Error(context.Background(), "paginateByToken encodePageToken error", zap.Error(err))
		}
	}
	return items[startIdx:endIdx], nextToken
}

func paginateByPage(items []ListUsersItem, page uint32, pageSize int32) []ListUsersItem {
	if page < 1 {
		page = 1
	}
	ps := int32(MaxPageSize)
	if pageSize > 0 {
		ps = pageSize
	}
	if ps > MaxPageSize {
		ps = MaxPageSize
	}
	startIdx := int((page - 1) * uint32(ps))
	total := len(items)
	if startIdx >= total {
		return []ListUsersItem{}
	}
	endIdx := startIdx + int(ps)
	if endIdx > total {
		endIdx = total
	}
	return items[startIdx:endIdx]
}

func fillOtherScopeRoles(ctx context.Context, items []ListUsersItem, currentScopeType entity.ScopeType, orgId uint64) error {
	if len(items) == 0 {
		return nil
	}
	var uids []entity.ID
	for _, u := range items {
		uids = append(uids, entity.ID(u.UserId))
	}

	allScopeTypes := []entity.ScopeType{entity.RoleBindingScopeOrg, entity.RoleBindingScopeProject, entity.RoleBindingScopeTiDB}
	for _, st := range allScopeTypes {
		if st == currentScopeType {
			continue
		}
		switch st {
		case entity.RoleBindingScopeOrg:
			fillOrgRoles(ctx, items, uids, orgId)
		case entity.RoleBindingScopeProject:
			if err := fillProjectRoles(ctx, items, uids, orgId); err != nil {
				return err
			}
		case entity.RoleBindingScopeTiDB:
			if err := fillClusterRoles(ctx, items, uids, orgId); err != nil {
				return err
			}
		}
	}
	return nil
}

func fillOrgRoles(ctx context.Context, items []ListUsersItem, uids []entity.ID, orgId uint64) {
	bs, err := dao.GetDaoHub().Role.ListRoleBindings(ctx, database.DBConn.DB, daorole.RoleBindingFilter{
		TargetType: []entity.TargetType{entity.RoleBindingTargetUser},
		TargetIDs:  uids,
		ScopeTypes: []entity.ScopeType{entity.RoleBindingScopeOrg},
		ScopeIDs:   []entity.ID{entity.ID(orgId)},
		Status:     []string{entity.RoleBindingStatusActive, entity.RoleBindingStatusInactive},
	})
	if err != nil {
		tilog.Warn(ctx, "fillOrgRoles ListRoleBindings error", zap.Any("err", err))
		return
	}
	uid2bs := map[uint64][]entity.RoleBinding{}
	for _, b := range bs {
		uid2bs[b.TargetID.Uint64()] = append(uid2bs[b.TargetID.Uint64()], b)
	}
	for i := range items {
		if obs, ok := uid2bs[items[i].UserId]; ok {
			for _, b := range obs {
				items[i].OrgRole = &ListUsersRoleItem{
					ScopeType: b.ScopeType.String(),
					ScopeId:   b.ScopeID.Uint64(),
					RbacRole:  b.Role,
				}
			}
		}
	}
}

func fillProjectRoles(ctx context.Context, items []ListUsersItem, uids []entity.ID, orgId uint64) error {
	ps, err := dao.GetDaoHub().Project.ListActiveProjectsByOrgID(ctx, database.DBConn.DB, orgId)
	if err != nil {
		return err
	}
	var pids []entity.ID
	for _, p := range ps {
		pids = append(pids, p.ID)
	}
	bs, err := dao.GetDaoHub().Role.ListRoleBindings(ctx, database.DBConn.DB, daorole.RoleBindingFilter{
		TargetType: []entity.TargetType{entity.RoleBindingTargetUser},
		TargetIDs:  uids,
		ScopeTypes: []entity.ScopeType{entity.RoleBindingScopeProject},
		ScopeIDs:   pids,
		Status:     []string{entity.RoleBindingStatusActive, entity.RoleBindingStatusInactive},
	})
	if err != nil {
		return err
	}
	uid2bs := map[uint64][]entity.RoleBinding{}
	for _, b := range bs {
		uid2bs[b.TargetID.Uint64()] = append(uid2bs[b.TargetID.Uint64()], b)
	}
	for i := range items {
		if pbs, ok := uid2bs[items[i].UserId]; ok {
			for _, b := range pbs {
				items[i].ProjectRoles = append(items[i].ProjectRoles, ListUsersRoleItem{
					ScopeType: b.ScopeType.String(),
					ScopeId:   b.ScopeID.Uint64(),
					RbacRole:  b.Role,
				})
			}
		}
	}
	return nil
}

func fillClusterRoles(ctx context.Context, items []ListUsersItem, uids []entity.ID, orgId uint64) error {
	allClusterBindings, err := dao.GetDaoHub().Role.ListRoleBindings(ctx, database.DBConn.DB, daorole.RoleBindingFilter{
		TargetType: []entity.TargetType{entity.RoleBindingTargetUser},
		TargetIDs:  uids,
		ScopeTypes: []entity.ScopeType{entity.RoleBindingScopeTiDB},
		Status:     []string{entity.RoleBindingStatusActive, entity.RoleBindingStatusInactive},
	})
	if err != nil {
		return err
	}

	uid2cbs := map[uint64][]entity.RoleBinding{}
	if len(allClusterBindings) > 0 {
		hasRoleAllClusterIDs := make([]uint64, 0, len(allClusterBindings))
		seen := make(map[uint64]struct{}, len(allClusterBindings))
		for _, rb := range allClusterBindings {
			scopeID := rb.ScopeID.Uint64()
			if _, ok := seen[scopeID]; ok {
				continue
			}
			seen[scopeID] = struct{}{}
			hasRoleAllClusterIDs = append(hasRoleAllClusterIDs, scopeID)
		}
		hasRoleInOClusterIds, err := dao.GetDaoHub().ResourceRelation.GetResourceIDsByParentAndResourceIDs(ctx, database.DBConn.DB, entity.RoleBindingScopeOrg.String(), orgId, entity.RoleBindingScopeTiDB.String(), hasRoleAllClusterIDs)
		if err != nil {
			return err
		}
		var orgClusterBindings []entity.RoleBinding
		for _, cb := range allClusterBindings {
			if slice.Contain(hasRoleInOClusterIds, cb.ScopeID.Uint64()) {
				orgClusterBindings = append(orgClusterBindings, cb)
			}
		}
		for _, cb := range orgClusterBindings {
			uid2cbs[cb.TargetID.Uint64()] = append(uid2cbs[cb.TargetID.Uint64()], cb)
		}
	}

	for i := range items {
		if cbs, ok := uid2cbs[items[i].UserId]; ok {
			for _, cb := range cbs {
				items[i].ClusterRoles = append(items[i].ClusterRoles, ListUsersRoleItem{
					ScopeType: cb.ScopeType.String(),
					ScopeId:   cb.ScopeID.Uint64(),
					RbacRole:  cb.Role,
				})
			}
		}
	}
	return nil
}
