package com.android.trisolarisserver.controller import com.android.trisolarisserver.component.PropertyAccess import com.android.trisolarisserver.component.RoomImageStorage import com.android.trisolarisserver.controller.dto.RoomImageResponse import com.android.trisolarisserver.models.room.RoomImage import com.android.trisolarisserver.models.property.Role import com.android.trisolarisserver.repo.RoomImageRepo import com.android.trisolarisserver.repo.RoomRepo import com.android.trisolarisserver.security.MyPrincipal import org.springframework.core.io.FileSystemResource import org.springframework.http.HttpHeaders import org.springframework.http.HttpStatus import org.springframework.http.MediaType import org.springframework.http.ResponseEntity import org.springframework.security.core.annotation.AuthenticationPrincipal import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.ResponseStatus import org.springframework.web.bind.annotation.RestController import org.springframework.web.multipart.MultipartFile import org.springframework.web.server.ResponseStatusException import java.nio.file.Files import java.nio.file.Paths import java.util.UUID @RestController @RequestMapping("/properties/{propertyId}/rooms/{roomId}/images") class RoomImages( private val propertyAccess: PropertyAccess, private val roomRepo: RoomRepo, private val roomImageRepo: RoomImageRepo, private val storage: RoomImageStorage, @org.springframework.beans.factory.annotation.Value("\${storage.rooms.publicBaseUrl}") private val publicBaseUrl: String ) { @GetMapping fun list( @PathVariable propertyId: UUID, @PathVariable roomId: UUID, @AuthenticationPrincipal principal: MyPrincipal? ): List { requirePrincipal(principal) propertyAccess.requireMember(propertyId, principal!!.userId) ensureRoom(propertyId, roomId) return roomImageRepo.findByRoomIdOrdered(roomId) .map { it.toResponse(publicBaseUrl) } } @PostMapping @ResponseStatus(HttpStatus.CREATED) fun upload( @PathVariable propertyId: UUID, @PathVariable roomId: UUID, @AuthenticationPrincipal principal: MyPrincipal?, @RequestParam("file") file: MultipartFile, @RequestParam(required = false) tags: List? ): RoomImageResponse { requirePrincipal(principal) propertyAccess.requireMember(propertyId, principal!!.userId) propertyAccess.requireAnyRole(propertyId, principal.userId, Role.ADMIN, Role.MANAGER) val room = ensureRoom(propertyId, roomId) if (file.isEmpty) { throw ResponseStatusException(HttpStatus.BAD_REQUEST, "File is empty") } val stored = try { storage.store(propertyId, roomId, file) } catch (ex: IllegalArgumentException) { throw ResponseStatusException(HttpStatus.BAD_REQUEST, ex.message ?: "Invalid image") } val nextRoomSortOrder = roomImageRepo.findMaxRoomSortOrder(roomId) + 1 val nextRoomTypeSortOrder = roomImageRepo.findMaxRoomTypeSortOrder(room.roomType.code) + 1 val image = RoomImage( property = room.property, room = room, originalPath = stored.originalPath, thumbnailPath = stored.thumbnailPath, contentType = stored.contentType, sizeBytes = stored.sizeBytes, roomTypeCode = room.roomType.code, tags = tags?.toMutableSet() ?: mutableSetOf(), roomSortOrder = nextRoomSortOrder, roomTypeSortOrder = nextRoomTypeSortOrder ) return roomImageRepo.save(image).toResponse(publicBaseUrl) } @GetMapping("/{imageId}/file") fun file( @PathVariable propertyId: UUID, @PathVariable roomId: UUID, @PathVariable imageId: UUID, @RequestParam(required = false, defaultValue = "full") size: String, @AuthenticationPrincipal principal: MyPrincipal? ): ResponseEntity { requirePrincipal(principal) propertyAccess.requireMember(propertyId, principal!!.userId) ensureRoom(propertyId, roomId) val image = roomImageRepo.findByIdAndRoomIdAndPropertyId(imageId, roomId, propertyId) ?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "Image not found") val path = if (size.equals("thumb", true)) image.thumbnailPath else image.originalPath val file = Paths.get(path) if (!Files.exists(file)) { throw ResponseStatusException(HttpStatus.NOT_FOUND, "File missing") } val resource = FileSystemResource(file) val type = image.contentType return ResponseEntity.ok() .contentType(MediaType.parseMediaType(type)) .header(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=\"room-${imageId}.${file.fileName}\"") .contentLength(resource.contentLength()) .body(resource) } private fun ensureRoom(propertyId: UUID, roomId: UUID): com.android.trisolarisserver.models.room.Room { return roomRepo.findByIdAndPropertyId(roomId, propertyId) ?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "Room not found") } private fun requirePrincipal(principal: MyPrincipal?) { if (principal == null) { throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "Missing principal") } } } private fun RoomImage.toResponse(baseUrl: String): RoomImageResponse { val id = id ?: throw ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Image id missing") return RoomImageResponse( id = id, propertyId = property.id!!, roomId = room.id!!, roomTypeCode = roomTypeCode, url = "$baseUrl/properties/${property.id}/rooms/${room.id}/images/$id/file", thumbnailUrl = "$baseUrl/properties/${property.id}/rooms/${room.id}/images/$id/file?size=thumb", contentType = contentType, sizeBytes = sizeBytes, tags = tags.toSet(), roomSortOrder = roomSortOrder, roomTypeSortOrder = roomTypeSortOrder, createdAt = createdAt.toString() ) }