package com.jttools.datacontrol.guard import org.json.JSONArray import org.json.JSONObject import java.time.LocalDateTime /** Plage pendant laquelle l'appareil est utilisable. Minutes depuis minuit ; jours ISO (1 = lundi … 7 = dimanche). */ data class TimeWindow(val days: Set, val startMin: Int, val endMin: Int) data class AppLimit(val pkg: String, val minutes: Int) /** * Règles d'usage envoyées par FamilyControl (champ `rules` de la config distante). * Les temps s'entendent par jour ; le bonus n'est valable que le jour [bonusDate]. */ data class Rules( val scheduleEnabled: Boolean = false, val scheduleCutsMobile: Boolean = true, val windows: List = emptyList(), val dailyLimitMin: Int = 0, val appLimits: List = emptyList(), val blocked: Set = emptySet(), val bonusDate: String? = null, val bonusGlobalMin: Int = 0, val bonusApps: List = emptyList() ) { val isEmpty: Boolean get() = !scheduleEnabled && dailyLimitMin <= 0 && appLimits.isEmpty() && blocked.isEmpty() /** Vrai si [now] tombe dans une plage autorisée (une plage qui passe minuit déborde sur le lendemain). */ fun isOpenAt(now: LocalDateTime): Boolean { val dow = now.dayOfWeek.value val prev = if (dow == 1) 7 else dow - 1 val minute = now.hour * 60 + now.minute return windows.any { w -> if (w.startMin <= w.endMin) dow in w.days && minute >= w.startMin && minute < w.endMin else (dow in w.days && minute >= w.startMin) || (prev in w.days && minute < w.endMin) } } /** Heure de la prochaine ouverture, en texte (« Reprise à 08:00. »), ou "" s'il n'y en a pas. */ fun nextOpeningText(now: LocalDateTime): String { val minute = now.hour * 60 + now.minute for (offset in 0..7) { val dow = ((now.dayOfWeek.value - 1 + offset) % 7) + 1 val start = windows.filter { dow in it.days && (offset > 0 || it.startMin > minute) } .minOfOrNull { it.startMin } ?: continue val label = "%02d:%02d".format(start / 60, start % 60) return if (offset == 0) "Reprise à $label." else "Reprise à $label (dans $offset j)." } return "" } companion object { val NONE = Rules() fun parse(json: String?): Rules { if (json.isNullOrBlank()) return NONE return runCatching { val o = JSONObject(json) val sch = o.optJSONObject("schedule") val bonus = o.optJSONObject("bonus") Rules( scheduleEnabled = sch?.optBoolean("enabled", false) ?: false, scheduleCutsMobile = sch?.optBoolean("cutsMobile", true) ?: true, windows = sch?.optJSONArray("windows").objects().map { w -> TimeWindow( days = w.optJSONArray("days").ints().toSet(), startMin = w.optInt("start", 0), endMin = w.optInt("end", 0) ) }, dailyLimitMin = o.optInt("dailyLimitMin", 0), appLimits = o.optJSONArray("appLimits").objects().map { AppLimit(it.optString("pkg"), it.optInt("min")) }, blocked = o.optJSONArray("blocked").strings().toSet(), bonusDate = bonus?.optString("date")?.takeIf { it.isNotEmpty() }, bonusGlobalMin = bonus?.optInt("globalMin", 0) ?: 0, bonusApps = bonus?.optJSONArray("apps").objects().map { AppLimit(it.optString("pkg"), it.optInt("min")) } ) }.getOrDefault(NONE) } private fun JSONArray?.objects(): List = if (this == null) emptyList() else (0 until length()).mapNotNull { optJSONObject(it) } private fun JSONArray?.ints(): List = if (this == null) emptyList() else (0 until length()).map { optInt(it) } private fun JSONArray?.strings(): List = if (this == null) emptyList() else (0 until length()).map { optString(it) } } }