Pith - kiln
package com.vgmlr.kiln

import android.content.Context
import androidx.work.CoroutineWorker
import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
import androidx.work.WorkerParameters
import java.time.Duration
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.LocalTime
import java.util.concurrent.TimeUnit

class KilnAlertWorker(context: Context, params: WorkerParameters) :
    CoroutineWorker(context, params) {

    override suspend fun doWork(): Result {
        val ctx = applicationContext
        runAlerts(ctx)
        schedule(ctx)
        return Result.success()
    }

    private suspend fun runAlerts(ctx: Context) {
        if (!KilnNotifications.allowed(ctx)) return

        val db = KilnDb.get(ctx)
        val prefs = KilnPrefs.get(ctx)
        val today = LocalDate.now()

        for (account in db.accountDao().getAll()) {
            if (!account.pmsAlert && !account.menstruationAlert) continue
            val records = db.dayRecordDao().getAll(account.id)
            val buttons = KilnButtonSet(db.buttonDao().getAll(account.id))
            val starts = KilnCyclePredictor.periodStarts(records, buttons)
            if (starts.isEmpty()) continue
            val cycle = KilnCyclePredictor.avgCycleLength(starts)

            if (account.pmsAlert) {
                val hueOf = KilnGradientPalette.lookup(records, buttons, starts, cycle)
                pms(ctx, prefs, account, today, starts, cycle, hueOf)
            }
            if (account.menstruationAlert) {
                menstruation(ctx, prefs, account, today, starts, cycle)
            }
        }
    }
    
    private fun pms(
        ctx: Context,
        prefs: KilnPrefs,
        account: KilnAccount,
        today: LocalDate,
        starts: List<LocalDate>,
        cycle: Int,
        hueOf: (LocalDate) -> Float
    ) {
        if (KilnDayStats.preMenstrualOnset(starts, cycle, hueOf) != today) return
        val day = today.toEpochDay()
        if (prefs.alertDay(KilnPrefs.KEY_PMS_DAY, account.id) == day) return
        prefs.setAlertDay(KilnPrefs.KEY_PMS_DAY, account.id, day)
        KilnNotifications.pms(ctx, account)
    }

    private fun menstruation(
        ctx: Context,
        prefs: KilnPrefs,
        account: KilnAccount,
        today: LocalDate,
        starts: List<LocalDate>,
        cycle: Int
    ) {
        val id = account.id
        val day = today.toEpochDay()
        val due = starts.last().plusDays(cycle.toLong())
        if (due.isAfter(today)) return
        val anchor = due.toEpochDay()

        if (day > anchor) {
            if (!account.menstruationReminder) return
            if (day >= anchor + RUN_DAYS) return
            if (prefs.alertDay(KilnPrefs.KEY_PERIOD_DISMISSED, id) == anchor) return
        }
        if (prefs.alertDay(KilnPrefs.KEY_PERIOD_DAY, id) == day) return
        prefs.setAlertDay(KilnPrefs.KEY_PERIOD_ANCHOR, id, anchor)
        prefs.setAlertDay(KilnPrefs.KEY_PERIOD_DAY, id, day)
        KilnNotifications.menstruation(ctx, account)
    }

    companion object {
        private const val WORK_NAME = "alert_sync"
        private const val ONCE_NAME = "alert_sync_once"
        private const val RUN_DAYS = 5
        private val ALERT_TIME: LocalTime = LocalTime.of(8, 0)

        fun schedule(context: Context) {
            WorkManager.getInstance(context).enqueueUniqueWork(
                WORK_NAME,
                ExistingWorkPolicy.REPLACE,
                OneTimeWorkRequestBuilder<KilnAlertWorker>()
                    .setInitialDelay(minutesUntilAlert(), TimeUnit.MINUTES)
                    .build()
            )
        }

        fun runOnce(context: Context) {
            WorkManager.getInstance(context).enqueueUniqueWork(
                ONCE_NAME,
                ExistingWorkPolicy.REPLACE,
                OneTimeWorkRequestBuilder<KilnAlertWorker>().build()
            )
        }

        private fun minutesUntilAlert(): Long {
            val now = LocalDateTime.now()
            var next = now.toLocalDate().atTime(ALERT_TIME)
            if (!next.isAfter(now)) next = next.plusDays(1)
            return Duration.between(now, next).toMinutes()
        }
    }
}