package com.android.trisolarisserver.controller import com.android.trisolarisserver.component.PropertyAccess import com.android.trisolarisserver.controller.dto.CardPrepareResponse import com.android.trisolarisserver.controller.dto.IssueCardRequest import com.android.trisolarisserver.controller.dto.IssuedCardResponse import com.android.trisolarisserver.models.property.Role import com.android.trisolarisserver.models.room.IssuedCard import com.android.trisolarisserver.repo.AppUserRepo import com.android.trisolarisserver.repo.IssuedCardRepo import com.android.trisolarisserver.repo.PropertyCardCounterRepo import com.android.trisolarisserver.repo.PropertyRepo import com.android.trisolarisserver.repo.RoomRepo import com.android.trisolarisserver.security.MyPrincipal import org.springframework.http.HttpStatus import org.springframework.security.core.annotation.AuthenticationPrincipal import org.springframework.transaction.annotation.Transactional import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.ResponseStatus import org.springframework.web.bind.annotation.RestController import org.springframework.web.server.ResponseStatusException import java.time.OffsetDateTime import java.util.UUID @RestController @RequestMapping("/properties/{propertyId}/rooms/{roomId}/cards") class TemporaryRoomCards( private val propertyAccess: PropertyAccess, private val roomRepo: RoomRepo, private val issuedCardRepo: IssuedCardRepo, private val appUserRepo: AppUserRepo, private val counterRepo: PropertyCardCounterRepo, private val propertyRepo: PropertyRepo ) { private val temporaryCardDurationMinutes = 7L @PostMapping("/prepare-temp") @ResponseStatus(HttpStatus.CREATED) @Transactional fun prepareTemporary( @PathVariable propertyId: UUID, @PathVariable roomId: UUID, @AuthenticationPrincipal principal: MyPrincipal? ): CardPrepareResponse { requireIssueActor(propertyId, principal) val room = roomRepo.findByIdAndPropertyId(roomId, propertyId) ?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "Room not found") val issuedAt = OffsetDateTime.now() val expiresAt = issuedAt.plusMinutes(temporaryCardDurationMinutes) val cardIndex = nextCardIndex(propertyId) val payload = buildSector0Payload(room.roomNumber, cardIndex, issuedAt, expiresAt) return CardPrepareResponse( cardIndex = cardIndex, key = payload.key, timeData = payload.timeData, issuedAt = issuedAt.toString(), expiresAt = expiresAt.toString() ) } @PostMapping("/temp") @ResponseStatus(HttpStatus.CREATED) @Transactional fun issueTemporary( @PathVariable propertyId: UUID, @PathVariable roomId: UUID, @AuthenticationPrincipal principal: MyPrincipal?, @RequestBody request: IssueCardRequest ): IssuedCardResponse { val actor = requireIssueActor(propertyId, principal) if (request.cardId.isBlank()) { throw ResponseStatusException(HttpStatus.BAD_REQUEST, "cardId required") } if (request.cardIndex <= 0) { throw ResponseStatusException(HttpStatus.BAD_REQUEST, "cardIndex required") } val room = roomRepo.findByIdAndPropertyId(roomId, propertyId) ?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "Room not found") val issuedAt = parseOffset(request.issuedAt) ?: OffsetDateTime.now() val expiresAt = issuedAt.plusMinutes(temporaryCardDurationMinutes) val now = OffsetDateTime.now() if (issuedCardRepo.existsActiveForRoom(propertyId, room.id!!, now)) { throw ResponseStatusException(HttpStatus.CONFLICT, "Active card already exists for room") } val card = IssuedCard( property = room.property, room = room, roomStay = null, cardId = request.cardId.trim(), cardIndex = request.cardIndex, issuedAt = issuedAt, expiresAt = expiresAt, issuedBy = actor ) return issuedCardRepo.save(card).toResponse() } private fun parseOffset(value: String?): OffsetDateTime? { if (value.isNullOrBlank()) return null return try { OffsetDateTime.parse(value.trim()) } catch (_: Exception) { throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid timestamp") } } private fun requireIssueActor(propertyId: UUID, principal: MyPrincipal?): com.android.trisolarisserver.models.property.AppUser { if (principal == null) { throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "Missing principal") } propertyAccess.requireMember(propertyId, principal.userId) propertyAccess.requireAnyRole(propertyId, principal.userId, Role.ADMIN, Role.MANAGER) return appUserRepo.findById(principal.userId).orElseThrow { ResponseStatusException(HttpStatus.UNAUTHORIZED, "User not found") } } private fun nextCardIndex(propertyId: UUID): Int { var counter = counterRepo.findByPropertyIdForUpdate(propertyId) if (counter == null) { val property = propertyRepo.findById(propertyId).orElseThrow { ResponseStatusException(HttpStatus.NOT_FOUND, "Property not found") } counterRepo.save(com.android.trisolarisserver.models.room.PropertyCardCounter( property = property, nextIndex = 10001 )) counter = counterRepo.findByPropertyIdForUpdate(propertyId) } val current = counter!!.nextIndex counter.nextIndex = current + 1 counter.updatedAt = OffsetDateTime.now() counterRepo.save(counter) return current } private fun buildSector0Payload( roomNumber: Int, cardIndex: Int, issuedAt: OffsetDateTime, expiresAt: OffsetDateTime ): TempSector0Payload { val key = buildSector0Block2(roomNumber, cardIndex) val newData = "0000000000" + formatDateComponents(issuedAt) + formatDateComponents(expiresAt) val checkSum = calculateChecksum(key + newData) val finalData = newData + checkSum return TempSector0Payload(key, finalData) } private fun buildSector0Block2(roomNumber: Int, cardID: Int): String { val guestID = cardID + 1 val key = "${cardID}2F${guestID}" val finalRoom = if (roomNumber < 10) "0$roomNumber" else roomNumber.toString() return "472F${key}00010000${finalRoom}0000" } private fun formatDateComponents(time: OffsetDateTime): String { val minute = time.minute.toString().padStart(2, '0') val hour = time.hour.toString().padStart(2, '0') val day = time.dayOfMonth.toString().padStart(2, '0') val month = time.monthValue.toString().padStart(2, '0') val year = time.year.toString().takeLast(2) return "${minute}${hour}${day}${month}${year}" } private fun calculateChecksum(dataHex: String): String { val data = hexStringToByteArray(dataHex) var checksum = 0 for (byte in data) { checksum = calculateByteChecksum(byte, checksum) } return String.format("%02X", checksum) } private fun calculateByteChecksum(byte: Byte, currentChecksum: Int): Int { var checksum = currentChecksum var b = byte.toInt() for (i in 0 until 8) { checksum = if ((checksum xor b) and 1 != 0) { (checksum xor 0x18) shr 1 or 0x80 } else { checksum shr 1 } b = b shr 1 } return checksum } private fun hexStringToByteArray(hexString: String): ByteArray { val len = hexString.length val data = ByteArray(len / 2) for (i in 0 until len step 2) { data[i / 2] = ((Character.digit(hexString[i], 16) shl 4) + Character.digit(hexString[i + 1], 16)).toByte() } return data } } private data class TempSector0Payload( val key: String, val timeData: String ) private fun IssuedCard.toResponse(): IssuedCardResponse { return IssuedCardResponse( id = id!!, propertyId = property.id!!, roomId = room.id!!, roomStayId = roomStay?.id, cardId = cardId, cardIndex = cardIndex, issuedAt = issuedAt.toString(), expiresAt = expiresAt.toString(), issuedByUserId = issuedBy?.id, revokedAt = revokedAt?.toString() ) }