package com.android.trisolarisserver.component import com.android.trisolarisserver.controller.GuestDocumentResponse import com.android.trisolarisserver.db.repo.GuestDocumentRepo import com.android.trisolarisserver.models.booking.GuestDocument import com.fasterxml.jackson.databind.ObjectMapper import org.springframework.scheduling.annotation.Scheduled import org.springframework.stereotype.Component import org.springframework.web.servlet.mvc.method.annotation.SseEmitter import java.io.IOException import java.util.UUID import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.CopyOnWriteArrayList @Component class GuestDocumentEvents( private val guestDocumentRepo: GuestDocumentRepo, private val objectMapper: ObjectMapper ) { private val emitters: MutableMap> = ConcurrentHashMap() fun subscribe(propertyId: UUID, guestId: UUID): SseEmitter { val key = GuestDocKey(propertyId, guestId) val emitter = SseEmitter(0L) emitters.computeIfAbsent(key) { CopyOnWriteArrayList() }.add(emitter) emitter.onCompletion { emitters[key]?.remove(emitter) } emitter.onTimeout { emitters[key]?.remove(emitter) } emitter.onError { emitters[key]?.remove(emitter) } try { emitter.send(SseEmitter.event().name("guest-documents").data(buildSnapshot(propertyId, guestId))) } catch (_: IOException) { emitters[key]?.remove(emitter) } return emitter } fun emit(propertyId: UUID, guestId: UUID) { val key = GuestDocKey(propertyId, guestId) val list = emitters[key] ?: return val data = buildSnapshot(propertyId, guestId) val dead = mutableListOf() for (emitter in list) { try { emitter.send(SseEmitter.event().name("guest-documents").data(data)) } catch (_: IOException) { dead.add(emitter) } } if (dead.isNotEmpty()) { list.removeAll(dead.toSet()) } } @Scheduled(fixedDelayString = "25000") fun heartbeat() { emitters.forEach { (_, list) -> val dead = mutableListOf() for (emitter in list) { try { emitter.send(SseEmitter.event().name("ping").data("ok")) } catch (_: IOException) { dead.add(emitter) } } if (dead.isNotEmpty()) { list.removeAll(dead.toSet()) } } } private fun buildSnapshot(propertyId: UUID, guestId: UUID): List { return guestDocumentRepo .findByPropertyIdAndGuestIdOrderByUploadedAtDesc(propertyId, guestId) .map { it.toResponse(objectMapper) } } } private data class GuestDocKey( val propertyId: UUID, val guestId: UUID ) private fun GuestDocument.toResponse(objectMapper: ObjectMapper): GuestDocumentResponse { val id = id ?: throw IllegalStateException("Document id missing") val extracted: Map? = extractedData?.let { try { val raw = objectMapper.readValue(it, Map::class.java) raw.entries.associate { entry -> entry.key.toString() to (entry.value?.toString() ?: "") } } catch (_: Exception) { null } } return GuestDocumentResponse( id = id, propertyId = property.id!!, guestId = guest.id!!, bookingId = booking.id!!, uploadedByUserId = uploadedBy.id!!, uploadedAt = uploadedAt.toString(), originalFilename = originalFilename, contentType = contentType, sizeBytes = sizeBytes, extractedData = extracted, extractedAt = extractedAt?.toString() ) }