package socketclients

import (
	"encoding/base64"
	"jannex/admin-dashboard-backend/modules/cache"
	"jannex/admin-dashboard-backend/modules/database"
	"jannex/admin-dashboard-backend/modules/logger"
	"jannex/admin-dashboard-backend/modules/requestclient"
	"jannex/admin-dashboard-backend/modules/structs"
	"jannex/admin-dashboard-backend/modules/systempermissions"
	"jannex/admin-dashboard-backend/modules/utils"
	"os"
	"slices"
	"strconv"
	"strings"
	"time"

	"git.ex.umbach.dev/Alex/roese-utils/rslogger"
	"git.ex.umbach.dev/Alex/roese-utils/rsutils"
	"github.com/gofiber/websocket/v2"
	"github.com/google/uuid"
	"github.com/rs/zerolog/log"
	"golang.org/x/crypto/bcrypt"
	"gorm.io/gorm"
)

func BroadcastMessageToTopic(topic string, sendSocketMessage structs.SendSocketMessage) {
	for _, client := range cache.GetSocketClients() {
		if hasClientSubscribedToTopic(topic, client.SubscribedTopic) {
			client.SendMessage(sendSocketMessage)
		}
	}
}

func BroadcastMessageToTopicStartsWith(topic string, sendSocketMessage structs.SendSocketMessage) {
	for _, client := range cache.GetSocketClients() {
		if strings.HasPrefix(client.SubscribedTopic, topic) {
			client.SendMessage(sendSocketMessage)
		}
	}
}

func BroadcastMessageToTopics(topics []string, sendSocketMessage structs.SendSocketMessage) {
	for _, topic := range topics {
		BroadcastMessageToTopic(topic, sendSocketMessage)
	}
}

func BroadcastMessage(sendSocketMessage structs.SendSocketMessage) {
	for _, client := range cache.GetSocketClients() {
		client.SendMessage(sendSocketMessage)
	}
}

func BroadcastMessageExceptUserSessionId(ignoreUserSessionId string, sendSocketMessage structs.SendSocketMessage) {
	for _, client := range cache.GetSocketClients() {
		if client.SessionId != ignoreUserSessionId {
			client.SendMessage(sendSocketMessage)
		}
	}
}

func BroadcastMessageToTopicExceptUserSessionId(topic string, ignoreUserSessionId string, sendSocketMessage structs.SendSocketMessage) {
	for _, client := range cache.GetSocketClients() {
		if client.SessionId != ignoreUserSessionId && hasClientSubscribedToTopic(topic, client.SubscribedTopic) {
			client.SendMessage(sendSocketMessage)
		}
	}
}

func BroadcastMessageToTopicsExceptUserSessionId(topics []string, ignoreUserSessionId string, sendSocketMessage structs.SendSocketMessage) {
	for _, topic := range topics {
		BroadcastMessageToTopicExceptUserSessionId(topic, ignoreUserSessionId, sendSocketMessage)
	}
}

func hasClientSubscribedToTopic(topic string, clientTopic string) bool {
	return clientTopic == topic || strings.HasPrefix(clientTopic, topic)
}

func BroadcastMessageExceptUserId(ignoreUserId string, sendSocketMessage structs.SendSocketMessage) {
	for _, client := range cache.GetSocketClients() {
		if client.UserId != ignoreUserId {
			client.SendMessage(sendSocketMessage)
		}
	}
}

func BroadcastMessageToTopicExceptUserId(topic string, ignoreUserId string, sendSocketMessage structs.SendSocketMessage) {
	for _, client := range cache.GetSocketClients() {
		if client.UserId != ignoreUserId && hasClientSubscribedToTopic(topic, client.SubscribedTopic) {
			client.SendMessage(sendSocketMessage)
		}
	}
}

func BroadcastMessageToTopicsExceptUserId(topics []string, ignoreUserId string, sendSocketMessage structs.SendSocketMessage) {
	for _, topic := range topics {
		BroadcastMessageToTopicExceptUserId(topic, ignoreUserId, sendSocketMessage)
	}
}

func BroadcastMessageToUsersWithPermission(neededPermission string, sendSocketMessage structs.SendSocketMessage) {
	var rolePermissions []structs.RolePermission

	database.DB.Model(&structs.RolePermission{}).Where("permission_id = ?", neededPermission).Find(&rolePermissions)

	var foundUsers []structs.User

	for _, rolePermission := range rolePermissions {
		var users []structs.User

		database.DB.Where("role_id = ?", rolePermission.RoleId).Select("id").Find(&users)

		foundUsers = append(foundUsers, users...)
	}

	for _, foundUser := range foundUsers {
		SendMessageToUser(foundUser.Id, "", sendSocketMessage)
	}
}

func BroadcastMessageToUsersWithPermissionAndTopic(neededPermission string, topic string, sendSocketMessage structs.SendSocketMessage) {
	var rolePermissions []structs.RolePermission

	database.DB.Model(&structs.RolePermission{}).Where("permission_id = ?", neededPermission).Find(&rolePermissions)

	var foundUsers []structs.User

	for _, rolePermission := range rolePermissions {
		var users []structs.User

		database.DB.Where("role_id = ?", rolePermission.RoleId).Select("id").Find(&users)

		foundUsers = append(foundUsers, users...)
	}

	for _, foundUser := range foundUsers {
		SendMessageToUserWithTopic(foundUser.Id, topic, "", sendSocketMessage)
	}
}

func SendMessageToUser(userId string, ignoreUserSessionId string, sendSocketMessage structs.SendSocketMessage) {
	for _, client := range cache.GetSocketClients() {
		if client.UserId == userId && client.SessionId != ignoreUserSessionId {
			client.SendMessage(sendSocketMessage)
		}
	}
}

func SendMessageToUserWithTopic(userId string, topic string, ignoreUserSessionId string, sendSocketMessage structs.SendSocketMessage) {
	for _, client := range cache.GetSocketClients() {
		if client.UserId == userId && client.SessionId != ignoreUserSessionId && hasClientSubscribedToTopic(topic, client.SubscribedTopic) {
			client.SendMessage(sendSocketMessage)
		}
	}
}

func SendMessageOnlyToSessionId(sessionId string, sendSocketMessage structs.SendSocketMessage) {
	for _, client := range cache.GetSocketClients() {
		if client.SessionId == sessionId {
			client.SendMessage(sendSocketMessage)
		}
	}
}

func SendMessageToUsersByRoleId(roleId string, sendSocketMessage structs.SendSocketMessage) {
	for _, client := range cache.GetSocketClients() {
		var user structs.User

		database.DB.Where("id = ?", client.UserId).First(&user)

		if user.RoleId == roleId {
			client.SendMessage(sendSocketMessage)
		}
	}
}

func SendMessageToUserExceptBrowserTabSession(userId string, ignoreBrowserTabSession string, sendSocketMessage structs.SendSocketMessage) {
	for _, client := range cache.GetSocketClients() {
		if client.UserId == userId && client.BrowserTabSession != ignoreBrowserTabSession {
			client.SendMessage(sendSocketMessage)
		}
	}
}

// This close all connections that are connected with one session id.
// For example when a user has two browser tabs opened
func CloseAllUserSessionConnections(sessionId string) {
	for _, client := range cache.GetSocketClients() {
		if client.SessionId == sessionId {
			client.SendSessionClosedMessage()
		}
	}
}

// Used to close all user connections
// For example when a user changed his password
func CloseAndDeleteAllUserConnections(userId string) {
	for _, client := range cache.GetSocketClients() {
		if client.UserId == userId {
			client.SendSessionClosedMessage()
		}
	}

	database.DB.Where("user_id = ?", userId).Delete(&structs.UserSession{})
}

func UpdateConnectedUsers(userId string) {
	var user structs.User

	database.DB.First(&user, "id = ?", userId)

	BroadcastMessage(structs.SendSocketMessage{
		Cmd:  utils.SentCmdUpdateConnectedUsers,
		Body: len(cache.GetSocketClients()),
	})

	BroadcastMessageToTopic(utils.SubscribedTopicUsers, structs.SendSocketMessage{
		Cmd: utils.SentCmdUpdateUsers,
		Body: struct {
			UserId           string
			ConnectionStatus uint8
			LastOnline       time.Time
		}{
			UserId:           userId,
			ConnectionStatus: isUserGenerallyConnected(userId),
			LastOnline:       user.LastOnline,
		},
	})
}

func GetUserSessions(userId string) []structs.UserSessionSocket {
	var userSessions []structs.UserSession

	database.DB.Where("user_id = ?", userId).Find(&userSessions)

	var userSessionsSocket []structs.UserSessionSocket

	socketClients := cache.GetSocketClients()

	for _, userSession := range userSessions {
		userSessionsSocket = append(userSessionsSocket, structs.UserSessionSocket{
			IdForDeletion:    userSession.IdForDeletion,
			UserAgent:        userSession.UserAgent,
			ConnectionStatus: isUserSessionConnected(userSession.Id, socketClients),
			LastUsed:         userSession.LastUsed,
			ExpiresAt:        userSession.ExpiresAt,
		})
	}

	return userSessionsSocket
}

func UpdateUserSessionsForUser(userId string, ignoreUserSessionId string) {
	SendMessageToUserWithTopic(userId, utils.SubscribedTopicUserProfile, ignoreUserSessionId, structs.SendSocketMessage{
		Cmd:  utils.SentCmdUpdateUserSessions,
		Body: GetUserSessions(userId),
	})
}

func isUserSessionConnected(userSessionId string, socketClients []*structs.SocketClient) uint8 {
	for _, socketClient := range socketClients {
		if socketClient.SessionId == userSessionId {
			return 1
		}
	}

	return 0
}

// Used to determine if a user is connected regardless of the session used
func isUserGenerallyConnected(userId string) uint8 {
	for _, socketClient := range cache.GetSocketClients() {
		if socketClient.UserId == userId {
			return utils.ConnectionStateOnline
		}
	}

	return utils.ConnectionStateOffline
}

// Get all users from database.
// This is used in the UI to display all users.
func GetAllUsers() []structs.AllUsers {
	var users []structs.User
	var allUsers []structs.AllUsers

	database.DB.Find(&users)

	for _, user := range users {
		allUsers = append(allUsers, structs.AllUsers{
			Id:               user.Id,
			RoleId:           user.RoleId,
			Avatar:           user.Avatar,
			Username:         user.Username,
			ConnectionStatus: isUserGenerallyConnected(user.Id),
			Deactivated:      user.Deactivated,
			LastOnline:       user.LastOnline,
		})
	}

	return allUsers
}

func isUsernameAvailable(username string) bool {
	var user structs.User
	database.DB.Select("username").Where("username = ?", username).Find(&user)

	return user.Username == ""
}

func isEmailAvailable(email string) bool {
	var user structs.User
	database.DB.Select("email").Where("email = ?", email).Find(&user)

	return user.Email == ""
}

func UpdateUserProfile(conn *websocket.Conn, changes map[string]interface{}) {
	sessionId := conn.Locals("sessionId").(string)
	userId := conn.Locals("userId").(string)

	var user structs.User
	var updates = make(map[string]interface{})

	if changes["username"] != nil {
		username := changes["username"].(string)

		if isValueLenValid(username, utils.MinUsername, utils.MaxUsername) { // only affected if username was manipulated as min and max is provided in web ui
			if isUsernameAvailable(username) {
				user.Username = username
				updates["Username"] = username
			} else {
				SendMessageOnlyToSessionId(sessionId, structs.SendSocketMessage{
					Cmd: utils.SentCmdUserProfileUpdated,
					Body: struct {
						UserId string
						Result uint8
					}{
						UserId: userId,
						Result: 0,
					},
				})
				return
			}
		}
	}

	if changes["email"] != nil {
		email := changes["email"].(string)

		if isEmailAvailable(email) {
			user.Email = email
			updates["Email"] = email
		} else {
			SendMessageOnlyToSessionId(sessionId, structs.SendSocketMessage{
				Cmd: utils.SentCmdUserProfileUpdated,
				Body: struct {
					UserId string
					Result uint8
				}{
					UserId: userId,
					Result: 1,
				},
			})
			return
		}
	}

	if changes["oldPassword"] != nil && changes["newPassword"] != nil {
		oldPassword := changes["oldPassword"].(string)
		newPassword := changes["newPassword"].(string)

		decodedOldPassword, err := base64.StdEncoding.DecodeString(oldPassword)
		decodedNewPassword, err1 := base64.StdEncoding.DecodeString(newPassword)

		if err != nil || err1 != nil {
			log.Error().Msgf("Error decoding old or new password %s %s", err.Error(), err1.Error())

			SendMessageOnlyToSessionId(sessionId, structs.SendSocketMessage{
				Cmd: utils.SentCmdUserProfileUpdated,
				Body: struct {
					UserId string
					Result uint8
				}{
					UserId: userId,
					Result: 2,
				},
			})
			return
		}

		if utils.IsPasswordLengthValid(string(decodedOldPassword)) { // only affected if username was manipulated as min and max is provided in web ui
			database.DB.Select("password").First(&user, "id = ?", userId)

			if err := bcrypt.CompareHashAndPassword([]byte(user.Password), decodedOldPassword); err == nil {
				hashedPassword, err := bcrypt.GenerateFromPassword([]byte(decodedNewPassword), bcrypt.DefaultCost)

				if err == nil {
					user.Password = string(hashedPassword)
				} else {
					log.Error().Msgf("Failed to generate hash password %s", err.Error())
				}
			} else {
				log.Error().Msg("Incorrect password")

				SendMessageOnlyToSessionId(sessionId, structs.SendSocketMessage{
					Cmd: utils.SentCmdUserProfileUpdated,
					Body: struct {
						UserId string
						Result uint8
					}{
						UserId: userId,
						Result: 2,
					},
				})
				return
			}
		}
	}

	if len(changes) > 0 {
		user.UpdatedAt = time.Now()

		database.DB.Model(&structs.User{}).Where("id = ?", userId).Updates(user)

		if changes["username"] != nil || changes["email"] != nil || changes["oldPassword"] != nil && changes["newPassword"] != nil {
			if changes["oldPassword"] != nil && changes["newPassword"] != nil {
				// user has changed password - logout all his sessions
				CloseAndDeleteAllUserConnections(userId)

				logger.AddSystemLog(rslogger.LogTypeInfo, "%s has changed his password", userId)
				return
			}

			SendMessageOnlyToSessionId(sessionId, structs.SendSocketMessage{
				Cmd: utils.SentCmdUserProfileUpdated,
				Body: struct {
					UserId  string
					Changes map[string]interface{}
				}{
					UserId:  userId,
					Changes: updates,
				},
			})

			BroadcastMessageExceptUserSessionId(sessionId, structs.SendSocketMessage{
				Cmd: utils.SentCmdUserProfileUpdated,
				Body: struct {
					UserId  string
					Changes map[string]interface{}
				}{
					UserId:  userId,
					Changes: updates,
				},
			})

			logger.AddSystemLog(rslogger.LogTypeInfo, "User %s has updated his account with the following changes: %v", userId, rsutils.MarshalJson(updates))
		}
	}
}

func isValueLenValid(value string, min int, max int) bool {
	l := len(value)
	return l >= min && l <= max
}

func GetAllRoles() []structs.Role {
	var roles []structs.Role

	database.DB.Order("sorting_order").Find(&roles)

	return roles
}

func GetPermissionsByRoleId(roleId string) []string {
	var rolePermissions []structs.RolePermission

	database.DB.Where("role_id = ?", roleId).Find(&rolePermissions)

	var permissions []string

	for _, rolePermission := range rolePermissions {
		permissions = append(permissions, rolePermission.PermissionId)
	}

	return permissions
}

// Retrieve all roles with a list of all permissions for each role
func GetAdminAreaRolesPermissions() []structs.RolePermissions {
	roles := GetAllRoles()
	var rolesPermissions []structs.RolePermission

	database.DB.Find(&rolesPermissions)

	var rolePermissions []structs.RolePermissions

	for _, role := range roles {
		var permissions []string

		for _, rolePermission := range rolesPermissions {
			if rolePermission.RoleId == role.Id {
				permissions = append(permissions, rolePermission.PermissionId)
			}
		}

		rolePermissions = append(rolePermissions, structs.RolePermissions{
			RoleId:      role.Id,
			Permissions: permissions,
		})
	}

	return rolePermissions
}

func isRoleDisplayNameAvailable(roleDisplayName string) bool {
	var role structs.Role

	database.DB.Select("display_name").Where("display_name = ?", roleDisplayName).Find(&role)

	return role.DisplayName == ""
}

func AdminAreaUpdateRole(conn *websocket.Conn, body map[string]interface{}) {
	if body["RoleId"] == nil {
		log.Error().Msgf("No role id specified in update role %v", body)
		return
	}

	roleId := body["RoleId"].(string)
	changes := body["Changes"].(map[string]interface{})

	// user has nothing changed
	if changes["DisplayName"] == nil &&
		changes["Description"] == nil &&
		changes["AddedPermissions"] == nil &&
		changes["RemovedPermissions"] == nil {
		log.Error().Msgf("User has not specified anything to update the role %v", body)
		return
	}

	updatedRole := structs.Role{
		Id: roleId,
	}

	var changesResult = make(map[string]uint8)
	var updates = make(map[string]interface{})

	if changes["DisplayName"] != nil && isValueLenValid(changes["DisplayName"].(string), utils.MinRoleDisplayName, utils.MaxRoleDisplayName) {
		if isRoleDisplayNameAvailable(changes["DisplayName"].(string)) {
			updatedRole.DisplayName = changes["DisplayName"].(string)
			updates["DisplayName"] = changes["DisplayName"].(string)
		} else {
			changesResult["DisplayName"] = 1
		}
	}

	if changes["Description"] != nil {
		updatedRole.Description = changes["Description"].(string)
		updates["Description"] = changes["Description"].(string)
	}

	if changes["AddedPermissions"] != nil {
		addedPermissions := changes["AddedPermissions"].([]interface{})

		updates["AddedPermissions"] = addedPermissions

		var dbAddedPermissions []structs.RolePermission

		for _, addedPermission := range addedPermissions {
			dbAddedPermissions = append(dbAddedPermissions, structs.RolePermission{
				RoleId:       roleId,
				PermissionId: addedPermission.(string),
			})
		}

		database.DB.Create(dbAddedPermissions)
	}

	if changes["RemovedPermissions"] != nil {
		removedPermissions := changes["RemovedPermissions"].([]interface{})

		updates["RemovedPermissions"] = removedPermissions

		for _, removedPermission := range removedPermissions {
			database.DB.Where("role_id = ?", roleId).Where("permission_id = ?", removedPermission.(string)).Delete(&structs.RolePermission{})
		}
	}

	database.DB.Model(&structs.Role{}).Where("id = ?", roleId).Updates(&updatedRole)

	sessionId := conn.Locals("sessionId").(string)
	availableCategories := GetAvailableCategories(conn.Locals("userId").(string))

	SendMessageOnlyToSessionId(sessionId, structs.SendSocketMessage{
		Cmd: utils.SentCmdAdminAreaRoleUpdated,
		Body: struct {
			RoleId              string
			Changes             map[string]interface{}
			Result              map[string]uint8
			AvailableCategories []string
		}{
			RoleId:              roleId,
			Changes:             updates,
			Result:              changesResult,
			AvailableCategories: availableCategories,
		},
	})

	BroadcastMessageToTopicsExceptUserSessionId(
		[]string{utils.SubscribedTopicAdminAreaRoles, utils.SubscribedTopicUsers},
		sessionId,
		structs.SendSocketMessage{
			Cmd: utils.SentCmdAdminAreaRoleUpdated,
			Body: struct {
				RoleId              string
				Changes             map[string]interface{}
				AvailableCategories []string
			}{
				RoleId:              roleId,
				Changes:             updates,
				AvailableCategories: availableCategories,
			},
		})

	SendMessageToUsersByRoleId(roleId, structs.SendSocketMessage{
		Cmd: utils.SentCmdRolePermissionsUpdated,
		Body: struct {
			Changes             map[string]interface{}
			AvailableCategories []string
		}{
			Changes:             updates,
			AvailableCategories: availableCategories,
		},
	})

	logger.AddSystemLog(rslogger.LogTypeInfo, "User %s has updated the role %s with the following changes: %v",
		conn.Locals("userId").(string), roleId, rsutils.MarshalJson(updates))
}

func AdminAreaMoveRoleToSortingOrder(conn *websocket.Conn, body map[string]interface{}) {
	roleId := body["RoleId"].(string)
	direction := int(body["Direction"].(float64))

	var role structs.Role

	if err := database.DB.First(&role, "id = ?", roleId).Error; err != nil {
		log.Error().Msgf("Failed to get role %s", err.Error())
		return
	}

	currentSortingOrder := role.SortingOrder
	var newSortingOrder int

	if direction == 1 {
		newSortingOrder = currentSortingOrder + 1

		if newSortingOrder > systempermissions.GetRoleSortingOrder()-1 {
			return
		}
	} else {
		newSortingOrder = currentSortingOrder - 1

		if newSortingOrder < 0 {
			return
		}
	}

	// If the new sorting order number is smaller than the current sorting order number,
	// increase the sorting order numbers of all roles between the new and current sorting order by 1
	if newSortingOrder < currentSortingOrder {
		if err := database.DB.Model(&structs.Role{}).Where("sorting_order >= ? AND sorting_order < ?", newSortingOrder, currentSortingOrder).Update("sorting_order", gorm.Expr("sorting_order + 1")).Error; err != nil {
			log.Error().Msgf("Failed to update sorting order upwards %s", err.Error())
			return
		}
	} else if newSortingOrder > currentSortingOrder {
		// If the new sorting order number is larger than the current sorting order number,
		// decrease the sorting order numbers of all roles between the current and new sorting order by 1.
		if err := database.DB.Model(&structs.Role{}).Where("sorting_order > ? AND sorting_order <= ?", currentSortingOrder, newSortingOrder).Update("sorting_order", gorm.Expr("sorting_order - 1")).Error; err != nil {
			log.Error().Msgf("Failed to update sorting order downwards %s", err.Error())
			return
		}
	}

	// Update the sorting order number of the moved role
	if err := database.DB.Model(&role).Update("sorting_order", newSortingOrder).Error; err != nil {
		log.Debug().Msgf("Failed to update sorting order %s", err.Error())
		return
	}

	BroadcastMessageToTopics(
		[]string{utils.SubscribedTopicAdminAreaRoles, utils.SubscribedTopicUsers},
		structs.SendSocketMessage{
			Cmd:  utils.SentCmdAdminAreaUpdateRoleSortingOrder,
			Body: body,
		})

	logger.AddSystemLog(rslogger.LogTypeInfo, "User %s has changed the sorting order of role %s to %s",
		conn.Locals("userId").(string), roleId, strconv.Itoa(newSortingOrder))
}

func AdminAreaDeleteRole(conn *websocket.Conn, body map[string]interface{}) {
	roleId := body["RoleId"].(string)

	var role structs.Role

	database.DB.Where("id = ?", roleId).First(&role)

	if err := database.DB.Model(&structs.Role{}).Where("sorting_order > ?", role.SortingOrder).Update("sorting_order", gorm.Expr("sorting_order - 1")).Error; err != nil {
		log.Error().Msgf("Failed to update role sorting order after delete role %s", err.Error())
		return
	}

	database.DB.Where("id = ?", roleId).Delete(&structs.Role{})
	database.DB.Where("role_id = ?", roleId).Delete(&structs.RolePermission{})

	BroadcastMessageToTopic(utils.SubscribedTopicAdminAreaRoles,
		structs.SendSocketMessage{
			Cmd:  utils.SentCmdAdminAreaRoleDeleted,
			Body: body,
		})

	logger.AddSystemLog(rslogger.LogTypeInfo, "User %s has deleted the role %s", conn.Locals("userId").(string), role.Id)
}

func UpdateUserRole(conn *websocket.Conn, userId string, roleId string) {
	database.DB.Model(&structs.User{}).Where("id = ?", userId).Updates(structs.User{
		RoleId: roleId,
	})

	SendMessageToUser(userId, "", structs.SendSocketMessage{
		Cmd: utils.SentCmdAllUsersUserRoleUpdated,
		Body: struct {
			UserId      string
			RoleId      string
			Permissions []string
		}{
			UserId:      userId,
			RoleId:      roleId,
			Permissions: GetPermissionsByRoleId(roleId),
		},
	})

	BroadcastMessageToTopicsExceptUserId(
		[]string{utils.SubscribedTopicUsers, utils.SubscribedTopicAdminAreaRoles},
		userId, structs.SendSocketMessage{
			Cmd: utils.SentCmdAllUsersUserRoleUpdated,
			Body: struct {
				UserId string
				RoleId string
			}{
				UserId: userId,
				RoleId: roleId,
			},
		})

	logger.AddSystemLog(rslogger.LogTypeInfo, "User %s has assigned the role %s to %s",
		conn.Locals("userId").(string), roleId, userId)
}

func HasPermission(userId string, permission string) bool {
	var user structs.User

	database.DB.Where("id = ?", userId).First(&user)

	var rolePermission structs.RolePermission

	database.DB.Where("role_id = ?", user.RoleId).Where("permission_id = ?", permission).Find(&rolePermission)

	return rolePermission.PermissionId == permission
}

// This function is used to check if a user has one of the provided permissions
// For example if a user has one of the permissions "user.create" or "user.delete"
func HasOnePermission(userId string, permissions []string) bool {
	var user structs.User

	database.DB.Where("id = ?", userId).First(&user)

	var userPermissions []structs.RolePermission

	database.DB.Where("role_id = ?", user.RoleId).Find(&userPermissions)

	// loop through all provided permissions
	for _, permission := range permissions {
		// loop through all user permissions
		for _, userPermission := range userPermissions {
			// check if the user has the permission
			if userPermission.PermissionId == permission {
				return true
			}
		}
	}

	return false
}

func HasXYPermission(userId string, permission string, category string) bool {
	return HasPermission(userId, systempermissions.ConvertXYPermission(permission, category))
}

func SendErrorMessageNoPermissions(sessionId string) {
	SendMessageOnlyToSessionId(sessionId, structs.SendSocketMessage{
		Cmd: utils.SentCmdErrorNoPermissions,
	})
}

func AllUsersCreateNewUser(conn *websocket.Conn, body map[string]interface{}) {
	if body["Username"] == nil ||
		body["Email"] == nil ||
		body["Password"] == nil ||
		body["RoleId"] == nil {
		log.Error().Msgf("Invalid body provided for user creation: %v", body)
		return
	}

	username := body["Username"].(string)
	email := body["Email"].(string)
	password := body["Password"].(string)
	roleId := body["RoleId"].(string)

	if !isValueLenValid(username, utils.MinUsername, utils.MaxUsername) {
		log.Error().Msgf("Invalid username length: %s", username)
		return
	}

	sessionId := conn.Locals("sessionId").(string)

	if !isUsernameAvailable(username) {
		SendMessageOnlyToSessionId(sessionId, structs.SendSocketMessage{
			Cmd: utils.SentCmdAllUsersNewUserCreated,
			Body: struct {
				Result uint8
			}{
				Result: 0,
			},
		})
		return
	}

	if !isEmailAvailable(email) {
		SendMessageOnlyToSessionId(sessionId, structs.SendSocketMessage{
			Cmd: utils.SentCmdAllUsersNewUserCreated,
			Body: struct {
				Result uint8
			}{
				Result: 1,
			},
		})
		return
	}

	decodedPassword, err := base64.StdEncoding.DecodeString(password)

	if err != nil {
		log.Error().Msgf("Failed to decode base64 password, err: %s", err.Error())
		return
	}

	hashedPassword, err := bcrypt.GenerateFromPassword(decodedPassword, bcrypt.DefaultCost)

	if err != nil {
		log.Error().Msgf("Failed to generate password: %s", err.Error())
		return
	}

	newUser := structs.User{
		Id:        uuid.New().String(),
		RoleId:    roleId,
		Username:  username,
		Email:     email,
		Password:  string(hashedPassword),
		CreatedAt: time.Now(),
	}

	database.DB.Create(&newUser)

	BroadcastMessageToTopics(
		[]string{utils.SubscribedTopicUsers, utils.SubscribedTopicAdminAreaRoles},
		structs.SendSocketMessage{
			Cmd: utils.SentCmdAllUsersNewUserCreated,
			Body: struct {
				Id               string
				Username         string
				RoleId           string
				ConnectionStatus uint8
				Deactivated      bool
			}{
				Id:               newUser.Id,
				Username:         username,
				RoleId:           roleId,
				ConnectionStatus: utils.ConnectionStateOffline,
				Deactivated:      false,
			},
		})

	logger.AddSystemLog(rslogger.LogTypeInfo, "User %s has created the user %s with the assigned role %s",
		conn.Locals("userId").(string), newUser.Id, roleId)
}

func AllUsersDeleteUser(conn *websocket.Conn, userId string) {
	var user structs.User

	database.DB.Select("avatar").Where("id = ?", userId).Find(&user)

	if user.Avatar != "" {
		os.Remove("./public/avatars/" + user.Avatar)
	}

	database.DB.Where("id = ?", userId).Delete(&structs.User{})
	database.DB.Where("user_id = ?", userId).Delete(&structs.UserSession{})

	CloseAndDeleteAllUserConnections(userId)

	BroadcastMessageToTopics(
		[]string{utils.SubscribedTopicUsers, utils.SubscribedTopicAdminAreaRoles},
		structs.SendSocketMessage{
			Cmd: utils.SentCmdAllUsersUserDeleted,
			Body: struct {
				UserId string
			}{
				UserId: userId,
			},
		})

	logger.AddSystemLog(rslogger.LogTypeInfo, "User %s has deleted the user %s",
		conn.Locals("userId").(string), userId)

	requestclient.TelegramBotManagerDeleteUserRequestClient(userId)
}

func AllUsersUserDeactivation(conn *websocket.Conn, userId string, deactivate bool) {
	database.DB.Model(&structs.User{}).Select("deactivated").Where("id = ?", userId).Updates(structs.User{
		Deactivated: deactivate,
	})

	if deactivate {
		CloseAndDeleteAllUserConnections(userId)
	}

	BroadcastMessageToTopic(utils.SubscribedTopicUsers, structs.SendSocketMessage{
		Cmd: utils.SentCmdAllUsersUserDeactivation,
		Body: struct {
			UserId      string
			Deactivated bool
		}{
			UserId:      userId,
			Deactivated: deactivate,
		},
	})

	logger.AddSystemLog(rslogger.LogTypeInfo, "User %s has changed the deactivation status of user %s to %v",
		conn.Locals("userId").(string), userId, strconv.FormatBool(deactivate))
}

func GetUserApiKeys(userId string) []structs.UserApiKey {
	var apiKeys []structs.UserApiKey

	database.DB.Where("user_id = ?", userId).Find(&apiKeys)

	return apiKeys
}

func CreateNewUserApiKey(userId string, apiName string) {
	if len(apiName) < utils.MinUserApiKeyNameLength || len(apiName) > utils.MaxUserApiKeyNameLength {
		return
	}

	token, err := rsutils.GenerateSession()

	if err != nil {
		return
	}

	newApiKey := structs.UserApiKey{
		Id:         uuid.New().String(),
		Token:      token,
		UserId:     userId,
		Name:       apiName,
		UsageCount: 0,
		CreatedAt:  time.Now(),
	}

	database.DB.Create(&newApiKey)

	SendMessageToUserWithTopic(userId, utils.SubscribedTopicUserProfile, "", structs.SendSocketMessage{
		Cmd:  utils.SentCmdNewUserApiKeyCreated,
		Body: newApiKey,
	})

	logger.AddSystemLog(rslogger.LogTypeInfo, "User %s has created an API key", userId)
}

func DeleteUserApiKey(userId string, apiKey string) {
	database.DB.Where("id = ?", apiKey).Where("user_id = ?", userId).Delete(&structs.UserApiKey{})
	/*
		SendMessageToUser(userId, "", structs.SendSocketMessage{
			Cmd:  utils.SentCmdDeletedUserApiKey,
			Body: apiKey,
		}) */

	SendMessageToUserWithTopic(userId, utils.SubscribedTopicUserProfile, "", structs.SendSocketMessage{
		Cmd:  utils.SentCmdDeletedUserApiKey,
		Body: apiKey,
	})

	logger.AddSystemLog(rslogger.LogTypeInfo, "User %s has deleted one of its api keys", userId)
}

func GetAvailableCategories(userId string) []string {
	var categories []string

	for _, categoryGroup := range cache.GetCategoryGroups() {
		categories = append(categories, categoryGroup.Category)
	}

	// sort categories alphabetically
	slices.Sort(categories)

	return categories
}

func HandleCheckWhichCategoriesAreAvailable() {
	// this will add/remove permissions for the master and returns the role id of the master role
	masterRoleId := systempermissions.HandleMasterRolePermissions()
	masterRolePermissions := GetPermissionsByRoleId(masterRoleId)

	// we need to send the master role permissions to the user who is
	// member of the master role as the master role has access to all
	// categories but without this message the member of the master role
	// wouldn't see new categories
	// web ui will set the permissions to appContext userPermissions
	SendMessageToUsersByRoleId(masterRoleId, structs.SendSocketMessage{
		Cmd: utils.SentCmdAdminAreaManageCheckedForAvailableCategories,
		Body: struct {
			UserPermissions []string
		}{
			UserPermissions: masterRolePermissions,
		},
	})

	// send response the admin area manage topic that the check is finished
	BroadcastMessageToTopic(
		utils.SubscribedTopicAdminAreaManage,
		structs.SendSocketMessage{
			Cmd: utils.SentCmdAdminAreaManageCheckedForAvailableCategories,
		},
	)

	// send the current available categories to all users - so they can remove the no longer existing categories from the sidebar
	BroadcastMessage(
		structs.SendSocketMessage{
			Cmd: utils.SentCmdAdminAreaManageCheckedForAvailableCategories,
			Body: struct {
				AvailableCategories []string
			}{
				AvailableCategories: GetAvailableCategories(""),
			},
		},
	)

	// send message to admin area roles topic to update master role permissions
	// web ui will save the master permissions into adminAreaRoles context
	BroadcastMessageToTopic(
		utils.SubscribedTopicAdminAreaRoles,
		structs.SendSocketMessage{
			Cmd: utils.SentCmdAdminAreaManageCheckedForAvailableCategories,
			Body: struct {
				MasterRoleId          string
				MasterRolePermissions []string
			}{
				MasterRoleId:          masterRoleId,
				MasterRolePermissions: masterRolePermissions,
			},
		},
	)
}

func AddLogManagerServerConnection(userId string, displayName string, address string) {
	newConnection := structs.LogManagerServerConnection{
		Id:          uuid.New().String(),
		DisplayName: displayName,
		Address:     address,
		CreatedAt:   time.Now(),
	}

	database.DB.Create(&newConnection)

	BroadcastMessageToTopics(
		[]string{utils.SubscribedTopicAdminAreaManage, utils.SubscribedTopicConsoles},
		structs.SendSocketMessage{
			Cmd:  utils.SentCmdAdminAreaManageLogManagerServerConnectionAdded,
			Body: newConnection,
		})

	logger.AddSystemLog(rslogger.LogTypeInfo, "User %s has added log manager server connection with id %s, display name %s and address %s", userId, newConnection.Id, displayName, address)
}

func DeleteLogManagerServerConnection(userId string, id string) {
	database.DB.Where("id = ?", id).Delete(&structs.LogManagerServerConnection{})

	BroadcastMessageToTopics(
		[]string{utils.SubscribedTopicAdminAreaManage, utils.SubscribedTopicConsoles},
		structs.SendSocketMessage{
			Cmd:  utils.SentCmdAdminAreaManageLogManagerServerConnectionRemoved,
			Body: id,
		})

	logger.AddSystemLog(rslogger.LogTypeInfo, "User %s has removed log manager server connection with id %s", userId, id)
}
