package space.luminic.budgerapp.services import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.launch import kotlinx.coroutines.reactive.awaitFirst import kotlinx.coroutines.reactive.awaitFirstOrNull import kotlinx.coroutines.reactive.awaitLast import kotlinx.coroutines.reactive.awaitSingle import kotlinx.coroutines.reactor.awaitSingleOrNull import org.bson.Document import org.bson.types.ObjectId import org.springframework.data.mongodb.core.ReactiveMongoTemplate import org.springframework.data.mongodb.core.aggregation.Aggregation.* import org.springframework.data.mongodb.core.query.Criteria import org.springframework.data.mongodb.core.query.Query import org.springframework.security.core.context.ReactiveSecurityContextHolder import org.springframework.stereotype.Service import space.luminic.budgerapp.configs.AuthException import space.luminic.budgerapp.models.* import space.luminic.budgerapp.repos.* import java.time.LocalDateTime import java.time.ZoneId import java.util.UUID @Service class SpaceService( private val spaceRepo: SpaceRepo, private val userService: UserService, private val budgetRepo: BudgetRepo, private val reactiveMongoTemplate: ReactiveMongoTemplate, private val categoryRepo: CategoryRepo, private val recurrentRepo: RecurrentRepo, private val transactionRepo: TransactionRepo, private val financialService: FinancialService, private val categoryService: CategoryService, private val recurrentService: RecurrentService, private val tagRepo: TagRepo ) { suspend fun isValidRequest(spaceId: String, user: User): Space { val space = getSpace(spaceId) // Проверяем доступ пользователя к пространству return if (space.users.none { it.id.toString() == user.id }) { throw IllegalArgumentException("User does not have access to this Space") } else space } suspend fun getSpaces(): List { val securityContext = ReactiveSecurityContextHolder.getContext().awaitSingle() val authentication = securityContext.authentication val username = authentication.name val user = userService.getByUsername(username) val userId = ObjectId(user.id!!) // Агрегация для загрузки владельца и пользователей val lookupOwner = lookup("users", "owner.\$id", "_id", "ownerDetails") val unwindOwner = unwind("ownerDetails") val lookupUsers = lookup("users", "users.\$id", "_id", "usersDetails") // val unwindUsers = unwind("usersDetails") val matchStage = match(Criteria.where("usersDetails._id").`is`(userId)) val aggregation = newAggregation(lookupOwner, unwindOwner, lookupUsers, matchStage) return reactiveMongoTemplate.aggregate(aggregation, "spaces", Document::class.java) .collectList() .map { docs -> docs.map { doc -> val ownerDoc = doc.get("ownerDetails", Document::class.java) val usersDocList = doc.getList("usersDetails", Document::class.java) Space( id = doc.getObjectId("_id").toString(), name = doc.getString("name"), description = doc.getString("description"), owner = User( id = ownerDoc.getObjectId("_id").toString(), username = ownerDoc.getString("username"), firstName = ownerDoc.getString("firstName") ), users = usersDocList.map { userDoc -> User( id = userDoc.getObjectId("_id").toString(), username = userDoc.getString("username"), firstName = userDoc.getString("firstName") ) }.toMutableList(), createdAt = doc.getDate("createdAt").toInstant().atZone(ZoneId.systemDefault()).toLocalDate() ) } }.awaitFirst() } suspend fun getSpace(spaceId: String): Space { return spaceRepo.findById(spaceId).awaitSingleOrNull() ?: throw IllegalArgumentException("SpaceId not found for spaceId: $spaceId") } suspend fun createSpace(space: Space, createCategories: Boolean): Space { val securityContextHolder = ReactiveSecurityContextHolder.getContext().awaitSingleOrNull() ?: throw AuthException("Authentication failed") val authentication = securityContextHolder.authentication val username = authentication.name val user = userService.getByUsername(username) space.owner = user space.users.add(user) val savedSpace = spaceRepo.save(space).awaitSingle() return if (!createCategories) { savedSpace // Если не нужно создавать категории, просто возвращаем пространство } else { val categories = reactiveMongoTemplate.find(Query(), Category::class.java, "categories-etalon") .map { category -> category.copy(id = null, space = savedSpace) // Создаем новую копию } categoryRepo.saveAll(categories).awaitLast() savedSpace } } suspend fun deleteSpace(space: Space) { val objectId = ObjectId(space.id) coroutineScope { launch { val budgets = financialService.findProjectedBudgets(objectId).awaitFirstOrNull().orEmpty() budgetRepo.deleteAll(budgets).awaitFirstOrNull() } launch { val transactions = financialService.getTransactions(objectId.toString()).awaitFirstOrNull().orEmpty() transactionRepo.deleteAll(transactions).awaitFirstOrNull() } launch { val categories = categoryService.getCategories(objectId.toString(), null, "name", "ASC") categoryRepo.deleteAll(categories).awaitFirstOrNull() } launch { val recurrents = recurrentService.getRecurrents(objectId.toString()) recurrentRepo.deleteAll(recurrents).awaitFirstOrNull() } } spaceRepo.deleteById(space.id!!).awaitFirstOrNull() // Удаляем Space после всех операций } suspend fun createInviteSpace(spaceId: String): SpaceInvite { val securityContext = ReactiveSecurityContextHolder.getContext().awaitSingleOrNull() ?: throw AuthException("Authentication failed") val authentication = securityContext.authentication val user = userService.getByUsername(authentication.name) val space = getSpace(spaceId) if (space.owner?.id != user.id) { throw AuthException("Only owner could create invite into space") } val invite = SpaceInvite( UUID.randomUUID().toString().split("-")[0], user, LocalDateTime.now().plusHours(1), ) space.invites.add(invite) spaceRepo.save(space).awaitFirstOrNull() // Сохраняем изменения и возвращаем созданное приглашение return invite } suspend fun acceptInvite(code: String): Space { val securityContextHolder = ReactiveSecurityContextHolder.getContext().awaitSingleOrNull() ?: throw AuthException("Authentication failed") val user = userService.getByUsername(securityContextHolder.authentication.name) val space = spaceRepo.findSpaceByInvites(code).awaitFirstOrNull() ?: throw IllegalArgumentException("Space with invite code: $code not found") val invite = space.invites.find { it.code == code } // Проверяем, есть ли инвайт и не истек ли он if (invite == null || invite.activeTill.isBefore(LocalDateTime.now())) { throw IllegalArgumentException("Invite is invalid or expired") } // Проверяем, не является ли пользователь уже участником if (space.users.any { it.id == user.id }) { throw IllegalArgumentException("User is already a member of this Space") } // Добавляем пользователя и удаляем использованный инвайт space.users.add(user) space.invites.remove(invite) return spaceRepo.save(space).awaitFirst() } suspend fun leaveSpace(spaceId: String) { val securityContext = ReactiveSecurityContextHolder.getContext().awaitSingleOrNull() ?: throw AuthException("Authentication failed") val user = userService.getByUsername(securityContext.authentication.name) val space = getSpace(spaceId) // Удаляем пользователя из массива space.users.removeIf { it.id == user.id } // Сохраняем изменения spaceRepo.save(space).awaitFirst() } suspend fun kickMember(spaceId: String, kickedUsername: String) { val securityContext = ReactiveSecurityContextHolder.getContext().awaitSingleOrNull() ?: throw AuthException("Authentication failed") val currentUser = userService.getByUsername(securityContext.authentication.name) //проверяем что кикнутый пользователь сушествует userService.getByUsername(kickedUsername) val space = getSpace(spaceId) if (space.owner?.id != currentUser.id) { throw IllegalArgumentException("Only owners allowed for this action") } // Проверяем, что пользователь, которого нужно исключить, присутствует в списке пользователей val userToKick = space.users.find { it.username == kickedUsername } if (userToKick != null) { // Удаляем пользователя из пространства space.users.removeIf { it.username == kickedUsername } // Сохраняем изменения spaceRepo.save(space).awaitSingle() } else { throw IllegalArgumentException("User not found in this space") } } suspend fun findTag(space: Space, tagCode: String): Tag? { val lookupSpaces = lookup("spaces", "space.\$id", "_id", "spaceDetails") val unwindSpace = unwind("spaceDetails") val matchCriteria = mutableListOf() // Добавляем фильтры matchCriteria.add(Criteria.where("spaceDetails._id").`is`(ObjectId(space.id))) matchCriteria.add(Criteria.where("code").`is`(tagCode)) val match = match(Criteria().andOperator(*matchCriteria.toTypedArray())) val aggregationBuilder = mutableListOf( lookupSpaces, unwindSpace, match.takeIf { matchCriteria.isNotEmpty() }, ).filterNotNull() val aggregation = newAggregation(aggregationBuilder) return reactiveMongoTemplate.aggregate( aggregation, "tags", Document::class.java ).next() .map { doc -> Tag( id = doc.getObjectId("_id").toString(), space = Space(id = doc.get("spaceDetails", Document::class.java).getObjectId("_id").toString()), code = doc.getString("code"), name = doc.getString("name") ) }.awaitSingleOrNull() } suspend fun createTag(space: Space, tag: Tag): Tag { tag.space = space val existedTag = findTag(space, tag.code) return existedTag?.let { throw IllegalArgumentException("Tag with code ${tag.code} already exists") } ?: tagRepo.save(tag).awaitFirst() } suspend fun deleteTag(space: Space, tagCode: String) { val existedTag = findTag(space, tagCode) ?: throw NoSuchElementException("Tag with code $tagCode not found") val categoriesWithTag = categoryService.getCategories(space.id!!, sortBy = "name", direction = "ASC", tagCode = existedTag.code) categoriesWithTag.map { cat -> cat.tags.removeIf { it.code == tagCode } // Изменяем список тегов cat } categoryRepo.saveAll(categoriesWithTag).awaitFirst() // Сохраняем обновлённые категории tagRepo.deleteById(existedTag.id!!).awaitFirst() } suspend fun getTags(space: Space): List { val lookupSpaces = lookup("spaces", "space.\$id", "_id", "spaceDetails") val unwindSpace = unwind("spaceDetails") val matchCriteria = mutableListOf() // Добавляем фильтры matchCriteria.add(Criteria.where("spaceDetails._id").`is`(ObjectId(space.id))) val match = match(Criteria().andOperator(*matchCriteria.toTypedArray())) val aggregationBuilder = mutableListOf( lookupSpaces, unwindSpace, match.takeIf { matchCriteria.isNotEmpty() }, ).filterNotNull() val aggregation = newAggregation(aggregationBuilder) return reactiveMongoTemplate.aggregate( aggregation, "tags", Document::class.java ) .collectList() .map { docs -> docs.map { doc -> Tag( id = doc.getObjectId("_id").toString(), space = Space( id = doc.get("spaceDetails", Document::class.java).getObjectId("_id").toString() ), code = doc.getString("code"), name = doc.getString("name") ) } } .awaitSingleOrNull().orEmpty() } // fun regenSpaceCategory(): Mono { // return getSpace("67af3c0f652da946a7dd9931") // .flatMap { space -> // categoryService.findCategory(id = "677bc767c7857460a491bd4f") // .flatMap { category -> // заменил map на flatMap // category.space = space // category.name = "Сбережения" // category.description = "Отчисления в накопления или инвестиционные счета" // category.icon = "💰" // categoryRepo.save(category) // теперь возвращаем Mono // } // } // } // fun regenSpaces(): Mono> { // return spaceRepo.findAll() // .flatMap { space -> // userService.getUsers() // .flatMap { users -> // if (users.isEmpty()) { // return@flatMap Mono.error(IllegalStateException("No users found")) // } // val updatedSpace = space.copy(owner = users.first()) // Создаем копию (если `Space` data class) // spaceRepo.save(updatedSpace) // } // } // .collectList() // } }