package scim

import (
	"context"
	"encoding/json"
	"fmt"
	"strconv"
	"strings"

	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"

	"github.com/google/uuid"

	"github.com/pingcap/errors"
	"github.com/tidbcloud/account/internel/dao/entity"
)

var (
	scimUserIdFormat = "orguser-%d-%d"

	scimRole2RoleBindingRoleMap = map[string]string{
		"organization owner":               "org:owner",
		"organization billing admin":       "org:billing_admin",
		"organization console audit admin": "org:audit_admin",
		"organization member":              "org:member",
		"project owner":                    "project:owner",
		"project data access read-write":   "project:dev",
		"project data access read-only":    "project:readonly",
	}

	roleBindingRole2ScimRoleMap = map[string]string{
		"org:owner":         "Organization Owner",
		"org:billing_admin": "Organization Billing Admin",
		"org:audit_admin":   "Organization Console Audit Admin",
		"org:member":        "Organization Member",
		"project:owner":     "Project Owner",
		"project:dev":       "Project Data Access Read-Write",
		"project:readonly":  "Project Data Access Read-Only",
	}
)

func convRoleBindingUser2ScimUser(rbUser *roleBindingUser) ScimUserData {
	active := false
	if rbUser.UserStatus == string(entity.UserStatusActive) && rbUser.OrgBinding.RoleBindingStatus == entity.RoleBindingStatusActive {
		active = true
	}
	uniqueID := genScimUserUniqueID(rbUser.OrgBinding.ScopeID, rbUser.UserID)

	scimUser := ScimUserData{}
	scimUser.SetSchemas([]string{UserSchema, EnterpriseUserSchema})
	scimUser.SetID(uniqueID)
	scimUser.SetUserName(rbUser.Email)
	scimUser.SetActive(active)
	scimUser.SetName(ScimUserName{
		GivenName:  rbUser.FirstName,
		FamilyName: rbUser.LastName,
	})
	scimUser.SetEmails([]ScimUserEmail{
		{
			Value:   rbUser.Email,
			Type:    "work",
			Primary: true,
			Display: rbUser.Email,
		},
	})
	scimUser.SetMeta(ScimUserMeta{
		ResourceType: "User",
	})

	projectRoles := map[string]string{}
	if len(rbUser.ProjectBindings) > 0 {
		for _, pb := range rbUser.ProjectBindings {
			projectRoles[fmt.Sprintf("%d", pb.ScopeID)] = convRbRole2ScimRole(pb.Role)
		}
	}
	scimUser.SetEntInfo(ScimEntInfo{
		OrgRole:      convRbRole2ScimRole(rbUser.OrgBinding.Role),
		ProjectRoles: projectRoles,
	})

	return scimUser
}

func convScimUser2RoleBindingUser(scimUser ScimUserData, orgID uint64) *roleBindingUser {
	entInfo := scimUser.GetEntInfo()
	orgBinding := roleBinding{
		ScopeID:           orgID,
		Role:              convScimRole2RbRole(entInfo.OrgRole),
		RoleBindingStatus: entity.RoleBindingStatusActive,
	}
	var projectBindings []roleBinding
	if len(entInfo.ProjectRoles) > 0 {
		for pid, role := range entInfo.ProjectRoles {
			parsePid, err := strconv.ParseUint(pid, 10, 64)
			if err != nil {
				tilog.Error(context.Background(), "parse project id in scim user fail", zap.Any("projectId", pid), zap.Any("err", err))
				continue
			}
			pBinding := roleBinding{
				ScopeID:           parsePid,
				Role:              convScimRole2RbRole(role),
				RoleBindingStatus: entity.RoleBindingStatusActive,
			}
			projectBindings = append(projectBindings, pBinding)
		}
	}
	return &roleBindingUser{
		Email:           scimUser.GetUserName(),
		FirstName:       scimUser.GetName().GivenName,
		LastName:        scimUser.GetName().FamilyName,
		OrgID:           orgID,
		OrgBinding:      orgBinding,
		ProjectBindings: projectBindings,
	}
}

func convScimRole2RbRole(role string) string {
	if role == "" {
		return ""
	}
	rbRole, ok := scimRole2RoleBindingRoleMap[strings.ToLower(role)]
	if !ok {
		return ""
	}
	return rbRole
}

func convRbRole2ScimRole(role string) string {
	if role == "" {
		return ""
	}
	scimRole, ok := roleBindingRole2ScimRoleMap[role]
	if !ok {
		return ""
	}
	return scimRole
}

func genScimUserUniqueID(orgID, userID uint64) string {
	return fmt.Sprintf(scimUserIdFormat, orgID, userID)
}

func convScimUserUniqueID(uniqueID string) (uint64, uint64, error) {
	split := strings.Split(uniqueID, "-")
	if len(split) != 3 {
		return 0, 0, errors.New("id invalid")
	}
	orgID, err := strconv.ParseUint(split[1], 10, 64)
	if err != nil {
		return 0, 0, err
	}
	userID, err := strconv.ParseUint(split[2], 10, 64)
	if err != nil {
		return 0, 0, err
	}
	return orgID, userID, nil
}

func genScimGroupUniqueID(orgID uint64) string {
	return uuid.New().String()
}

func convDbGroup2ScimGroupData(group entity.ScimGroup) ScimGroupData {
	members, _ := convDbGroupMemberStr(group.Members)

	data := ScimGroupData{}
	data.SetSchemas([]string{GroupSchema})
	data.SetID(group.GroupID)
	data.SetDisplayName(group.DisplayName)
	data.SetMeta(ScimGroupMeta{
		ResourceType: "Group",
	})
	data.SetMembers(members)
	return data
}

func convDbGroupMemberStr(memberStr string) ([]ScimGroupMember, error) {
	var members []ScimGroupMember
	if memberStr != "" {
		memberMap := ScimGroupMemberMarshal{}
		err := json.Unmarshal([]byte(memberStr), &memberMap)
		if err != nil {
			tilog.Warn(context.Background(), "parse group members fail", zap.Any("memberStr", memberStr), zap.Any("err", err))
			return members, err
		}
		for _, member := range memberMap.MemberMap {
			members = append(members, member)
		}
	}
	return members, nil
}

func convDbGroupProjectRoleStr2RoleSlice(ctx context.Context, projectRoleStr string) ([]ScimGroupProjectRole, error) {
	var roles []ScimGroupProjectRole
	roleMap, err := convDbGroupProjectRoleStr2RoleMap(ctx, projectRoleStr)
	if err != nil {
		return roles, err
	}
	for _, role := range roleMap {
		roles = append(roles, role)
	}
	return roles, nil
}

func convDbGroupProjectRoleStr2RoleMap(ctx context.Context, projectRoleStr string) (map[uint64]ScimGroupProjectRole, error) {
	if projectRoleStr == "" {
		return map[uint64]ScimGroupProjectRole{}, nil
	}
	roleMap := ScimGroupProjectRoleMarshal{}
	err := json.Unmarshal([]byte(projectRoleStr), &roleMap)
	if err != nil {
		tilog.Warn(ctx, "parse group project role fail", zap.Any("projectRoleStr", projectRoleStr), zap.Any("err", err))
		return nil, err
	}
	if roleMap.ProjectRoleMap == nil {
		roleMap.ProjectRoleMap = map[uint64]ScimGroupProjectRole{}
	}
	return roleMap.ProjectRoleMap, nil

}
