package dev.privacyllc.period.notifications

import dev.privacyllc.period.core.data.CycleRepository
import dev.privacyllc.period.core.datastore.UserPreferencesRepository
import dev.privacyllc.period.core.notifications.ReminderScheduler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import java.time.LocalDate
import java.time.LocalTime
import javax.inject.Inject
import javax.inject.Singleton

/**
 * Keeps the scheduled reminder pointed at the current forecast.
 *
 * §31 lists rescheduling after a confirmation or a forecast change explicitly,
 * and it is the requirement most likely to be missed — everything works, right
 * up until a "Not yet" moves the forecast and the queued reminder is aimed at a
 * day that no longer means anything.
 *
 * It lives in `app` rather than in the repository on purpose. `core:data` does
 * not depend on `core:datastore` or `core:notifications`, and it should not:
 * storing cycle history and deciding when to buzz a phone are different jobs,
 * and the module boundary guard would refuse the dependency anyway.
 */
@Singleton
class ReminderCoordinator @Inject constructor(
    private val repository: CycleRepository,
    private val preferences: UserPreferencesRepository,
    private val scheduler: ReminderScheduler,
) {

    /**
     * Watch for anything that changes what a reminder should say or when.
     *
     * `distinctUntilChanged` on a small key rather than on the whole state: the
     * forecast object is recreated on every recalculation even when it says the
     * same thing, and rescheduling on each of those would churn WorkManager for
     * no reason.
     */
    fun start(scope: CoroutineScope) {
        combine(
            repository.forecast.map { it?.mostLikelyStartDate },
            preferences.preferences.map { it.reminderTime to anyEnabled(it) },
        ) { forecastDate, (time, enabled) -> Triple(forecastDate, time, enabled) }
            .distinctUntilChanged()
            .onEach { (_, time, enabled) -> apply(time, enabled) }
            .launchIn(scope)

        // A confirmed period answers the question the check-ins were asking, so
        // the count resets and the app is willing to ask again next cycle.
        // Without this, somebody who ignored three check-ins once would never be
        // asked again — the stopping rule would become permanent.
        repository.confirmedPeriods
            .map { periods -> periods.maxOfOrNull { it.startDate } }
            .distinctUntilChanged()
            .onEach { latest -> if (latest != null) onPeriodConfirmed(latest) }
            .launchIn(scope)
    }

    private suspend fun apply(time: LocalTime, enabled: Boolean) {
        if (enabled) scheduler.schedule(time) else scheduler.cancel()
    }

    private suspend fun onPeriodConfirmed(@Suppress("UNUSED_PARAMETER") start: LocalDate) {
        preferences.resetCheckIns()
    }

    private fun anyEnabled(p: dev.privacyllc.period.core.datastore.UserPreferences) =
        p.periodApproachingEnabled || p.periodExpectedTodayEnabled || p.didItStartEnabled ||
            p.periodEndCheckInEnabled || p.fertileWindowReminderEnabled || p.ovulationReminderEnabled
}
