// Copyright 2023 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package object

import (
	"crypto/md5"
	"crypto/tls"
	"encoding/base64"
	"errors"
	"fmt"
	"strings"

	"github.com/casdoor/casdoor/conf"
	"github.com/casdoor/casdoor/i18n"
	"github.com/casdoor/casdoor/util"
	goldap "github.com/go-ldap/ldap/v3"
	"github.com/nyaruka/phonenumbers"
	"github.com/thanhpk/randstr"
	"golang.org/x/text/encoding/unicode"
)

// formatUserPhone processes phone number for a user based on their CountryCode
func formatUserPhone(u *User) {
	if u.Phone == "" {
		return
	}

	// 1. Normalize hint (e.g., "China" -> "CN") for the parser
	countryHint := u.CountryCode
	if strings.EqualFold(countryHint, "China") {
		countryHint = "CN"
	}
	if len(countryHint) != 2 {
		countryHint = "" // Only 2-letter codes are valid hints
	}

	// 2. Try parsing (Strictly using countryHint from LDAP)
	num, err := phonenumbers.Parse(u.Phone, countryHint)

	if err == nil && num != nil && phonenumbers.IsValidNumber(num) {
		// Store a clean national number (digits only, without country prefix)
		u.Phone = fmt.Sprint(num.GetNationalNumber())
	}
}

type LdapConn struct {
	Conn *goldap.Conn
	IsAD bool
}

//type ldapGroup struct {
//	GidNumber string
//	Cn        string
//}

type LdapUser struct {
	UidNumber string `json:"uidNumber"`
	Uid       string `json:"uid"`
	Cn        string `json:"cn"`
	GidNumber string `json:"gidNumber"`
	// Gcn                   string
	Uuid                  string `json:"uuid"`
	UserPrincipalName     string `json:"userPrincipalName"`
	DisplayName           string `json:"displayName"`
	Mail                  string
	Email                 string `json:"email"`
	EmailAddress          string
	TelephoneNumber       string
	Mobile                string `json:"mobile"`
	MobileTelephoneNumber string
	RegisteredAddress     string
	PostalAddress         string
	Country               string `json:"country"`
	CountryName           string `json:"countryName"`

	GroupId    string            `json:"groupId"`
	Address    string            `json:"address"`
	MemberOf   []string          `json:"memberOf"`
	Attributes map[string]string `json:"attributes"`
}

type LdapGroup struct {
	Dn          string   `json:"dn"`
	Cn          string   `json:"cn"`
	Name        string   `json:"name"`
	Description string   `json:"description"`
	Member      []string `json:"member"`
	ParentDn    string   `json:"parentDn"`
}

func (ldap *Ldap) GetLdapConn() (c *LdapConn, err error) {
	var conn *goldap.Conn
	tlsConfig := tls.Config{
		InsecureSkipVerify: ldap.AllowSelfSignedCert,
	}
	if ldap.EnableSsl {
		conn, err = goldap.DialTLS("tcp", fmt.Sprintf("%s:%d", ldap.Host, ldap.Port), &tlsConfig)
	} else {
		conn, err = goldap.Dial("tcp", fmt.Sprintf("%s:%d", ldap.Host, ldap.Port))
	}

	if err != nil {
		return nil, err
	}

	err = conn.Bind(ldap.Username, ldap.Password)
	if err != nil {
		return nil, err
	}

	isAD, err := isMicrosoftAD(conn)
	if err != nil {
		return nil, err
	}
	return &LdapConn{Conn: conn, IsAD: isAD}, nil
}

func (l *LdapConn) Close() {
	if l.Conn == nil {
		return
	}

	err := l.Conn.Unbind()
	if err != nil {
		panic(err)
	}
}

func isMicrosoftAD(Conn *goldap.Conn) (bool, error) {
	SearchFilter := "(objectClass=*)"
	SearchAttributes := []string{"vendorname", "vendorversion", "isGlobalCatalogReady", "forestFunctionality"}

	searchReq := goldap.NewSearchRequest("",
		goldap.ScopeBaseObject, goldap.NeverDerefAliases, 0, 0, false,
		SearchFilter, SearchAttributes, nil)
	searchResult, err := Conn.Search(searchReq)
	if err != nil {
		return false, err
	}
	if len(searchResult.Entries) == 0 {
		return false, nil
	}
	isMicrosoft := false

	type ldapServerType struct {
		Vendorname           string
		Vendorversion        string
		IsGlobalCatalogReady string
		ForestFunctionality  string
	}
	var ldapServerTypes ldapServerType
	for _, entry := range searchResult.Entries {
		for _, attribute := range entry.Attributes {
			switch attribute.Name {
			case "vendorname":
				ldapServerTypes.Vendorname = attribute.Values[0]
			case "vendorversion":
				ldapServerTypes.Vendorversion = attribute.Values[0]
			case "isGlobalCatalogReady":
				ldapServerTypes.IsGlobalCatalogReady = attribute.Values[0]
			case "forestFunctionality":
				ldapServerTypes.ForestFunctionality = attribute.Values[0]
			}
		}
	}
	if ldapServerTypes.Vendorname == "" &&
		ldapServerTypes.Vendorversion == "" &&
		ldapServerTypes.IsGlobalCatalogReady == "TRUE" &&
		ldapServerTypes.ForestFunctionality != "" {
		isMicrosoft = true
	}
	return isMicrosoft, err
}

func (l *LdapConn) GetLdapUsers(ldapServer *Ldap) ([]LdapUser, error) {
	SearchAttributes := []string{
		"uidNumber", "cn", "sn", "gidNumber", "entryUUID", "displayName", "mail", "email",
		"emailAddress", "telephoneNumber", "mobile", "mobileTelephoneNumber", "registeredAddress", "postalAddress",
		"c", "co", "memberOf",
	}
	if l.IsAD {
		SearchAttributes = append(SearchAttributes, "sAMAccountName")
	} else {
		SearchAttributes = append(SearchAttributes, "uid")
	}

	for attribute := range ldapServer.CustomAttributes {
		SearchAttributes = append(SearchAttributes, attribute)
	}

	// Some LDAP servers/configs use "{}" as a placeholder (e.g. "(uid={})").
	// Casdoor doesn't interpolate it. For listing users, interpret it as a wildcard.
	filter := strings.TrimSpace(ldapServer.Filter)
	if filter == "" {
		filter = "(objectClass=*)"
	} else if strings.Contains(filter, "{}") {
		filter = strings.ReplaceAll(filter, "{}", "*")
	}

	searchReq := goldap.NewSearchRequest(ldapServer.BaseDn, goldap.ScopeWholeSubtree, goldap.NeverDerefAliases,
		0, 0, false,
		filter, SearchAttributes, nil)
	searchResult, err := l.Conn.SearchWithPaging(searchReq, 100)
	if err != nil {
		return nil, err
	}

	if len(searchResult.Entries) == 0 {
		return []LdapUser{}, nil
	}

	var ldapUsers []LdapUser
	for _, entry := range searchResult.Entries {
		var user LdapUser
		for _, attribute := range entry.Attributes {
			switch attribute.Name {
			case "uidNumber":
				user.UidNumber = attribute.Values[0]
			case "uid":
				user.Uid = attribute.Values[0]
			case "sAMAccountName":
				user.Uid = attribute.Values[0]
			case "cn":
				user.Cn = attribute.Values[0]
			case "gidNumber":
				user.GidNumber = attribute.Values[0]
			case "entryUUID":
				user.Uuid = attribute.Values[0]
			case "objectGUID":
				user.Uuid = attribute.Values[0]
			case "userPrincipalName":
				user.UserPrincipalName = attribute.Values[0]
			case "displayName":
				user.DisplayName = attribute.Values[0]
			case "mail":
				user.Mail = attribute.Values[0]
			case "email":
				user.Email = attribute.Values[0]
			case "emailAddress":
				user.EmailAddress = attribute.Values[0]
			case "telephoneNumber":
				user.TelephoneNumber = attribute.Values[0]
			case "mobile":
				user.Mobile = attribute.Values[0]
			case "mobileTelephoneNumber":
				user.MobileTelephoneNumber = attribute.Values[0]
			case "registeredAddress":
				user.RegisteredAddress = attribute.Values[0]
			case "postalAddress":
				user.PostalAddress = attribute.Values[0]
			case "c":
				user.Country = attribute.Values[0]
			case "co":
				user.CountryName = attribute.Values[0]
			case "memberOf":
				user.MemberOf = attribute.Values
			default:
				if propName, ok := ldapServer.CustomAttributes[attribute.Name]; ok {
					if user.Attributes == nil {
						user.Attributes = make(map[string]string)
					}
					user.Attributes[propName] = attribute.Values[0]
				}
			}
		}
		ldapUsers = append(ldapUsers, user)
	}

	return ldapUsers, nil
}

// GetLdapGroups fetches LDAP groups and organizational units
func (l *LdapConn) GetLdapGroups(ldapServer *Ldap) ([]LdapGroup, error) {
	var allGroups []LdapGroup

	// Search for LDAP groups (groupOfNames, groupOfUniqueNames, posixGroup)
	groupFilters := []string{
		"(objectClass=groupOfNames)",
		"(objectClass=groupOfUniqueNames)",
		"(objectClass=posixGroup)",
	}

	// Add Active Directory group filter
	if l.IsAD {
		groupFilters = append(groupFilters, "(objectClass=group)")
	}

	// Build combined filter
	var filterBuilder strings.Builder
	filterBuilder.WriteString("(|")
	for _, filter := range groupFilters {
		filterBuilder.WriteString(filter)
	}
	filterBuilder.WriteString(")")

	SearchAttributes := []string{"cn", "name", "description", "member", "uniqueMember", "memberUid"}
	searchReq := goldap.NewSearchRequest(ldapServer.BaseDn,
		goldap.ScopeWholeSubtree, goldap.NeverDerefAliases, 0, 0, false,
		filterBuilder.String(), SearchAttributes, nil)

	searchResult, err := l.Conn.SearchWithPaging(searchReq, 100)
	if err != nil {
		// Groups might not exist, which is okay
		return allGroups, nil
	}

	for _, entry := range searchResult.Entries {
		group := LdapGroup{
			Dn: entry.DN,
		}

		for _, attribute := range entry.Attributes {
			switch attribute.Name {
			case "cn":
				group.Cn = attribute.Values[0]
			case "name":
				group.Name = attribute.Values[0]
			case "description":
				if len(attribute.Values) > 0 {
					group.Description = attribute.Values[0]
				}
			case "member", "uniqueMember", "memberUid":
				group.Member = append(group.Member, attribute.Values...)
			}
		}

		// Use cn as name if name is not set
		if group.Name == "" {
			group.Name = group.Cn
		}

		// Parse parent DN from the entry DN
		group.ParentDn = getParentDn(entry.DN)

		allGroups = append(allGroups, group)
	}

	// Also fetch organizational units as groups
	ouFilter := "(objectClass=organizationalUnit)"
	ouSearchReq := goldap.NewSearchRequest(ldapServer.BaseDn,
		goldap.ScopeWholeSubtree, goldap.NeverDerefAliases, 0, 0, false,
		ouFilter, []string{"ou", "description"}, nil)

	ouSearchResult, err := l.Conn.SearchWithPaging(ouSearchReq, 100)
	if err == nil {
		for _, entry := range ouSearchResult.Entries {
			ou := LdapGroup{
				Dn: entry.DN,
			}

			for _, attribute := range entry.Attributes {
				switch attribute.Name {
				case "ou":
					ou.Name = attribute.Values[0]
					ou.Cn = attribute.Values[0]
				case "description":
					if len(attribute.Values) > 0 {
						ou.Description = attribute.Values[0]
					}
				}
			}

			// Parse parent DN from the entry DN
			ou.ParentDn = getParentDn(entry.DN)

			allGroups = append(allGroups, ou)
		}
	}

	return allGroups, nil
}

// getParentDn extracts the parent DN from a full DN
func getParentDn(dn string) string {
	// Split DN by comma
	parts := strings.Split(dn, ",")
	if len(parts) <= 1 {
		return ""
	}

	// Remove the first component (the current node) and rejoin
	return strings.Join(parts[1:], ",")
}

// parseDnToGroupName converts a DN to a group name
func parseDnToGroupName(dn string) string {
	// Extract the CN or OU from the DN
	parts := strings.Split(dn, ",")
	if len(parts) == 0 {
		return ""
	}

	firstPart := parts[0]
	// Extract value after = sign
	if idx := strings.Index(firstPart, "="); idx != -1 {
		return firstPart[idx+1:]
	}

	return firstPart
}

func AutoAdjustLdapUser(users []LdapUser) []LdapUser {
	res := make([]LdapUser, len(users))
	for i, user := range users {
		res[i] = LdapUser{
			UidNumber:   user.UidNumber,
			Uid:         user.Uid,
			Cn:          user.Cn,
			GroupId:     user.GidNumber,
			Uuid:        user.GetLdapUuid(),
			DisplayName: user.DisplayName,
			Email:       util.ReturnAnyNotEmpty(user.Email, user.EmailAddress, user.Mail),
			Mobile:      util.ReturnAnyNotEmpty(user.Mobile, user.MobileTelephoneNumber, user.TelephoneNumber),
			Address:     util.ReturnAnyNotEmpty(user.Address, user.PostalAddress, user.RegisteredAddress),
			Country:     util.ReturnAnyNotEmpty(user.Country, user.CountryName),
			CountryName: user.CountryName,
			MemberOf:    user.MemberOf,
			Attributes:  user.Attributes,
		}
	}
	return res
}

func SyncLdapUsers(owner string, syncUsers []LdapUser, ldapId string) (existUsers []LdapUser, failedUsers []LdapUser, err error) {
	var uuids []string
	for _, user := range syncUsers {
		uuids = append(uuids, user.Uuid)
	}

	organization, err := getOrganization("admin", owner)
	if err != nil {
		panic(err)
	}

	ldap, err := GetLdap(ldapId)

	var dc []string
	for _, basedn := range strings.Split(ldap.BaseDn, ",") {
		if strings.Contains(basedn, "dc=") {
			dc = append(dc, basedn[3:])
		}
	}
	affiliation := strings.Join(dc, ".")

	var ou []string
	for _, admin := range strings.Split(ldap.Username, ",") {
		if strings.Contains(admin, "ou=") {
			ou = append(ou, admin[3:])
		}
	}
	tag := strings.Join(ou, ".")

	existUuids, err := GetExistUuids(owner, uuids)
	if err != nil {
		return nil, nil, err
	}

	existUuidSet := make(map[string]struct{}, len(existUuids))
	for _, uuid := range existUuids {
		existUuidSet[uuid] = struct{}{}
	}

	for _, syncUser := range syncUsers {
		_, found := existUuidSet[syncUser.Uuid]
		if found {
			existUsers = append(existUsers, syncUser)
		}

		if !found {
			score, err := organization.GetInitScore()
			if err != nil {
				return nil, nil, err
			}

			name, err := syncUser.buildLdapUserName(owner)
			if err != nil {
				return nil, nil, err
			}

			newUser := &User{
				Owner:             owner,
				Name:              name,
				CreatedTime:       util.GetCurrentTime(),
				DisplayName:       syncUser.buildLdapDisplayName(),
				SignupApplication: organization.DefaultApplication,
				Type:              "normal-user",
				Avatar:            organization.DefaultAvatar,
				Email:             syncUser.Email,
				Phone:             syncUser.Mobile,
				CountryCode:       syncUser.Country,
				Address:           []string{syncUser.Address},
				Region:            util.ReturnAnyNotEmpty(syncUser.Country, syncUser.CountryName),
				Affiliation:       affiliation,
				Tag:               tag,
				Score:             score,
				Ldap:              syncUser.Uuid,
				Properties:        syncUser.Attributes,
			}
			formatUserPhone(newUser)

			// Assign user to groups based on memberOf attribute
			userGroups := []string{}
			if len(ldap.DefaultGroups) > 0 {
				userGroups = append(userGroups, ldap.DefaultGroups...)
			} else if ldap.DefaultGroup != "" {
				userGroups = append(userGroups, ldap.DefaultGroup)
			}

			// Extract group names from memberOf DNs
			for _, memberDn := range syncUser.MemberOf {
				groupName := dnToGroupName(owner, memberDn)
				if groupName != "" {
					userGroups = append(userGroups, groupName)
				}
			}

			if len(userGroups) > 0 {
				newUser.Groups = userGroups
			}

			affected, err := AddUser(newUser, "en")
			if err != nil {
				return nil, nil, err
			}

			if !affected {
				failedUsers = append(failedUsers, syncUser)
				continue
			}

			// Trigger webhook for LDAP user sync
			TriggerWebhookForUser("new-user-ldap", newUser)
		}
	}

	return existUsers, failedUsers, err
}

// SyncLdapGroups syncs LDAP groups/OUs to Casdoor groups with hierarchy
func SyncLdapGroups(owner string, ldapGroups []LdapGroup, ldapId string) (newGroups int, updatedGroups int, err error) {
	if len(ldapGroups) == 0 {
		return 0, 0, nil
	}

	// Create a map of DN to group for quick lookup
	dnToGroup := make(map[string]*LdapGroup)
	for i := range ldapGroups {
		dnToGroup[ldapGroups[i].Dn] = &ldapGroups[i]
	}

	// Get existing groups for this organization
	existingGroups, err := GetGroups(owner)
	if err != nil {
		return 0, 0, err
	}

	existingGroupMap := make(map[string]*Group)
	for _, group := range existingGroups {
		existingGroupMap[group.Name] = group
	}

	ldap, err := GetLdap(ldapId)
	if err != nil {
		return 0, 0, err
	}

	// Process groups in hierarchical order (parents before children)
	processedGroups := make(map[string]bool)
	var processGroup func(ldapGroup *LdapGroup) error

	processGroup = func(ldapGroup *LdapGroup) error {
		if processedGroups[ldapGroup.Dn] {
			return nil
		}

		// Generate group name from DN
		groupName := dnToGroupName(owner, ldapGroup.Dn)
		if groupName == "" {
			return nil
		}

		// Determine parent
		var parentId string
		var isTopGroup bool

		if ldapGroup.ParentDn == "" || ldapGroup.ParentDn == ldap.BaseDn {
			isTopGroup = true
			parentId = ""
		} else {
			// Process parent first
			if parentGroup, exists := dnToGroup[ldapGroup.ParentDn]; exists {
				err := processGroup(parentGroup)
				if err != nil {
					return err
				}
				parentId = dnToGroupName(owner, ldapGroup.ParentDn)
			} else {
				isTopGroup = true
			}
		}

		// Check if group already exists
		if existingGroup, exists := existingGroupMap[groupName]; exists {
			// Update existing group
			existingGroup.DisplayName = ldapGroup.Name
			existingGroup.ParentId = parentId
			existingGroup.IsTopGroup = isTopGroup
			existingGroup.Type = "ldap-synced"
			existingGroup.UpdatedTime = util.GetCurrentTime()

			_, err := UpdateGroup(existingGroup.GetId(), existingGroup, true, "")
			if err == nil {
				updatedGroups++
			}
		} else {
			// Create new group
			newGroup := &Group{
				Owner:       owner,
				Name:        groupName,
				CreatedTime: util.GetCurrentTime(),
				UpdatedTime: util.GetCurrentTime(),
				DisplayName: ldapGroup.Name,
				ParentId:    parentId,
				IsTopGroup:  isTopGroup,
				Type:        "ldap-synced",
				IsEnabled:   true,
			}

			_, err := AddGroup(newGroup)
			if err == nil {
				newGroups++
				existingGroupMap[groupName] = newGroup
			}
		}

		processedGroups[ldapGroup.Dn] = true
		return nil
	}

	// Process all groups
	for i := range ldapGroups {
		err := processGroup(&ldapGroups[i])
		if err != nil {
			// Log error but continue processing other groups
			continue
		}
	}

	return newGroups, updatedGroups, nil
}

// dnToGroupName converts an LDAP DN to a Casdoor group name
func dnToGroupName(owner, dn string) string {
	if dn == "" {
		return ""
	}

	// Parse DN to extract meaningful components
	parts := strings.Split(dn, ",")

	// Build a hierarchical name from DN components (excluding DC parts)
	var nameComponents []string
	for _, part := range parts {
		part = strings.TrimSpace(part)
		lowerPart := strings.ToLower(part)

		// Skip DC (domain component) parts
		if strings.HasPrefix(lowerPart, "dc=") {
			continue
		}

		// Extract value after = sign
		if idx := strings.Index(part, "="); idx != -1 {
			value := part[idx+1:]
			nameComponents = append(nameComponents, value)
		}
	}

	if len(nameComponents) == 0 {
		return ""
	}

	// Reverse to get top-down hierarchy
	for i, j := 0, len(nameComponents)-1; i < j; i, j = i+1, j-1 {
		nameComponents[i], nameComponents[j] = nameComponents[j], nameComponents[i]
	}

	// Join with underscore to create a unique group name
	groupName := strings.Join(nameComponents, "_")

	// Sanitize group name - replace invalid characters with underscores
	// Keep only alphanumeric characters, underscores, and hyphens
	var sanitized strings.Builder
	for _, r := range groupName {
		if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '_' || r == '-' {
			sanitized.WriteRune(r)
		} else {
			sanitized.WriteRune('_')
		}
	}
	groupName = sanitized.String()

	// Remove consecutive underscores and trim
	for strings.Contains(groupName, "__") {
		groupName = strings.ReplaceAll(groupName, "__", "_")
	}
	groupName = strings.Trim(groupName, "_")

	return groupName
}

func GetExistUuids(owner string, uuids []string) ([]string, error) {
	var existUuids []string

	// PostgreSQL only supports up to 65535 parameters per query, so we batch the uuids
	const batchSize = 100
	tableNamePrefix := conf.GetConfigString("tableNamePrefix")
	for i := 0; i < len(uuids); i += batchSize {
		end := i + batchSize
		if end > len(uuids) {
			end = len(uuids)
		}
		batch := uuids[i:end]

		var batchUuids []string
		err := ormer.Engine.Table(tableNamePrefix+"user").Where("owner = ?", owner).Cols("ldap").
			In("ldap", batch).Select("DISTINCT ldap").Find(&batchUuids)
		if err != nil {
			return existUuids, err
		}
		existUuids = append(existUuids, batchUuids...)
	}

	return existUuids, nil
}

func ResetLdapPassword(user *User, oldPassword string, newPassword string, lang string) error {
	ldaps, err := GetLdaps(user.Owner)
	if err != nil {
		return err
	}

	for _, ldapServer := range ldaps {
		conn, err := ldapServer.GetLdapConn()
		if err != nil {
			continue
		}

		searchReq := goldap.NewSearchRequest(ldapServer.BaseDn, goldap.ScopeWholeSubtree, goldap.NeverDerefAliases,
			0, 0, false, ldapServer.buildAuthFilterString(user), []string{}, nil)

		searchResult, err := conn.Conn.Search(searchReq)
		if err != nil {
			conn.Close()
			return err
		}

		if len(searchResult.Entries) == 0 {
			conn.Close()
			continue
		}
		if len(searchResult.Entries) > 1 {
			conn.Close()
			return errors.New(i18n.Translate(lang, "check:Multiple accounts with same uid, please check your ldap server"))
		}

		userDn := searchResult.Entries[0].DN

		var pwdEncoded string
		modifyPasswordRequest := goldap.NewModifyRequest(userDn, nil)
		if conn.IsAD {
			utf16 := unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM)
			pwdEncoded, err := utf16.NewEncoder().String("\"" + newPassword + "\"")
			if err != nil {
				conn.Close()
				return err
			}
			modifyPasswordRequest.Replace("unicodePwd", []string{pwdEncoded})
			modifyPasswordRequest.Replace("userAccountControl", []string{"512"})
		} else if oldPassword != "" {
			modifyPasswordRequestWithOldPassword := goldap.NewPasswordModifyRequest(userDn, oldPassword, newPassword)
			_, err = conn.Conn.PasswordModify(modifyPasswordRequestWithOldPassword)
			if err != nil {
				conn.Close()
				return err
			}
			conn.Close()
			return nil
		} else {
			switch ldapServer.PasswordType {
			case "SSHA":
				pwdEncoded, err = generateSSHA(newPassword)
				break
			case "MD5":
				md5Byte := md5.Sum([]byte(newPassword))
				md5Password := base64.StdEncoding.EncodeToString(md5Byte[:])
				pwdEncoded = "{MD5}" + md5Password
				break
			case "Plain":
				pwdEncoded = newPassword
				break
			default:
				pwdEncoded = newPassword
				break
			}
			modifyPasswordRequest.Replace("userPassword", []string{pwdEncoded})
		}

		err = conn.Conn.Modify(modifyPasswordRequest)
		if err != nil {
			conn.Close()
			return err
		}
		conn.Close()
	}
	return nil
}

func (ldapUser *LdapUser) buildLdapUserName(owner string) (string, error) {
	user := User{}
	uidWithNumber := fmt.Sprintf("%s_%s", ldapUser.Uid, ldapUser.UidNumber)
	has, err := ormer.Engine.Where("owner = ? and (name = ? or name = ?)", owner, ldapUser.Uid, uidWithNumber).Get(&user)
	if err != nil {
		return "", err
	}

	if has {
		if user.Name == ldapUser.Uid {
			return uidWithNumber, nil
		}
		return fmt.Sprintf("%s_%s", uidWithNumber, randstr.Hex(6)), nil
	}

	if ldapUser.Uid != "" {
		return ldapUser.Uid, nil
	}

	return ldapUser.Cn, nil
}

func (ldapUser *LdapUser) buildLdapDisplayName() string {
	if ldapUser.DisplayName != "" {
		return ldapUser.DisplayName
	}

	return ldapUser.Cn
}

func (ldapUser *LdapUser) GetLdapUuid() string {
	if ldapUser.Uuid != "" {
		return ldapUser.Uuid
	}
	if ldapUser.Uid != "" {
		return ldapUser.Uid
	}

	return ldapUser.Cn
}

func (ldap *Ldap) buildAuthFilterString(user *User) string {
	// Tolerate configs that use "{}" as a placeholder, e.g. "(uid={})".
	// Casdoor doesn't interpolate it; treat it as wildcard so the base filter remains valid.
	baseFilter := strings.TrimSpace(ldap.Filter)
	if baseFilter == "" {
		baseFilter = "(objectClass=*)"
	} else if strings.Contains(baseFilter, "{}") {
		baseFilter = strings.ReplaceAll(baseFilter, "{}", "*")
	}

	if len(ldap.FilterFields) == 0 {
		return fmt.Sprintf("(&%s(uid=%s))", baseFilter, goldap.EscapeFilter(user.Name))
	}

	filter := fmt.Sprintf("(&%s(|", baseFilter)
	for _, field := range ldap.FilterFields {
		filter = fmt.Sprintf("%s(%s=%s)", filter, field, goldap.EscapeFilter(user.getFieldFromLdapAttribute(field)))
	}
	filter = fmt.Sprintf("%s))", filter)

	return filter
}

func (user *User) getFieldFromLdapAttribute(attribute string) string {
	switch attribute {
	case "uid":
		return user.Name
	case "sAMAccountName":
		return user.Name
	case "mail":
		return user.Email
	case "mobile":
		return user.Phone
	case "c", "co":
		return user.Region
	default:
		return ""
	}
}
