package com.estateunified.panel import com.google.gson.Gson import com.google.gson.reflect.TypeToken import java.nio.charset.StandardCharsets import java.nio.file.Files import java.nio.file.Path import java.nio.file.StandardCopyOption import java.util.UUID internal data class RentalRangeDto( val id: String = UUID.randomUUID().toString().take(10), val fromIso: String, val toIso: String, val status: String = "BOOKED", val note: String = "", val createdAtIso: String = java.time.format.DateTimeFormatter.ISO_LOCAL_DATE.format(java.time.LocalDate.now()), ) internal data class RentalCalendarDto( val listingId: String, val ownerUserId: String = "", val ranges: List = emptyList(), val updatedAtIso: String = java.time.format.DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(java.time.LocalDateTime.now()), ) internal class RentalCalendarStore( private val gson: Gson, private val file: Path, ) { private val lock = Any() fun absolutePath(): String = file.toAbsolutePath().normalize().toString() fun get(listingId: String): RentalCalendarDto = synchronized(lock) { load().firstOrNull { it.listingId == listingId } ?: RentalCalendarDto(listingId = listingId) } fun replace( listingId: String, ownerUserId: String, ranges: List, ): RentalCalendarDto { val normalized = ranges.mapNotNull { normalize(it) }.sortedBy { it.fromIso } val dto = RentalCalendarDto( listingId = listingId, ownerUserId = ownerUserId, ranges = normalized, updatedAtIso = java.time.format.DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(java.time.LocalDateTime.now()), ) synchronized(lock) { val list = load().toMutableList() list.removeAll { it.listingId == listingId } list.add(0, dto) persist(list) } return dto } fun addRange( listingId: String, ownerUserId: String, range: RentalRangeDto, ): RentalCalendarDto { val cur = get(listingId) val normalized = normalize(range) ?: return cur return replace( listingId, ownerUserId.ifBlank { cur.ownerUserId }, cur.ranges + normalized, ) } fun removeRange( listingId: String, rangeId: String, ): RentalCalendarDto { val cur = get(listingId) return replace( listingId, cur.ownerUserId, cur.ranges.filterNot { it.id == rangeId }, ) } fun statsForListing(listingId: String): Triple { val cur = get(listingId) val today = java.time.LocalDate.now() var pastCount = 0 var totalDays = 0 var lastEndIso = "" cur.ranges.forEach { r -> val from = runCatching { java.time.LocalDate.parse(r.fromIso) }.getOrNull() ?: return@forEach val to = runCatching { java.time.LocalDate.parse(r.toIso) }.getOrNull() ?: return@forEach val daysExclusive = java.time.temporal.ChronoUnit.DAYS.between(from, to).toInt().coerceAtLeast(0) + 1 totalDays += daysExclusive if (!to.isAfter(today)) { pastCount += 1 if (lastEndIso.isEmpty() || r.toIso > lastEndIso) lastEndIso = r.toIso } } return Triple(pastCount, totalDays, lastEndIso) } private fun normalize(r: RentalRangeDto): RentalRangeDto? { val from = runCatching { java.time.LocalDate.parse(r.fromIso) }.getOrNull() ?: return null val to = runCatching { java.time.LocalDate.parse(r.toIso) }.getOrNull() ?: return null if (to.isBefore(from)) return null return r.copy( fromIso = from.toString(), toIso = to.toString(), status = r.status.ifBlank { "BOOKED" }.uppercase(), ) } private fun load(): MutableList { if (!Files.isRegularFile(file)) return mutableListOf() val text = Files.readString(file, StandardCharsets.UTF_8) if (text.isBlank()) return mutableListOf() return runCatching { gson.fromJson>(text, listType) ?: mutableListOf() }.getOrElse { mutableListOf() } } private fun persist(list: List) { Files.createDirectories(file.toAbsolutePath().parent) val tmp = file.resolveSibling(file.fileName.toString() + ".tmp") Files.writeString(tmp, gson.toJson(list), StandardCharsets.UTF_8) Files.move(tmp, file, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE) } private companion object { val listType = object : TypeToken>() {}.type } }