package dev.privacyllc.period.core.notifications

import dev.privacyllc.period.domain.prediction.CycleStatus
import dev.privacyllc.period.domain.prediction.FertilityEstimate
import java.time.LocalDate

/** The six toggles from §29, as the rules see them. */
data class ReminderPreferences(
    val periodApproaching: Boolean = true,
    val periodExpectedToday: Boolean = true,
    val didItStart: Boolean = true,
    val periodEndCheckIn: Boolean = true,
    val fertileWindowApproaching: Boolean = false,
    val ovulationEstimated: Boolean = false,
)

/** What a wake-up should do. */
sealed interface ReminderDecision {
    data class Send(val kind: ReminderKind, val daysUntil: Int? = null) : ReminderDecision

    /**
     * §30's last word: *"We'll stop checking for now. Log your period whenever
     * it begins."*
     *
     * Sent once, and then nothing until something changes. The sentence after it
     * in §30 is the design — *the internal prediction may continue updating* —
     * so this stops the app **asking**, never the engine learning.
     */
    data object StopAsking : ReminderDecision

    data object Nothing : ReminderDecision
}

/**
 * When a reminder is due, decided from state the rest of the app already
 * computed.
 *
 * Pure, and deliberately does no cycle arithmetic of its own. `CycleStatusRules`
 * already decides which of §22's states the user is in, and a second copy of
 * that logic living inside a background worker is the classic way for a
 * notification to contradict the screen it deep-links to.
 */
object ReminderRules {

    /** How many days before the forecast the heads-up goes out. §30's example uses three. */
    const val APPROACHING_LEAD_DAYS = 3

    /** Days into an unclosed period before asking whether it is over. */
    const val END_CHECK_IN_AFTER_DAYS = 5

    /**
     * How many times the app asks before it stops.
     *
     * §30: *do not nag forever*. Three is a judgement, not a measurement — the
     * predicted day, and two follow-ups. What matters is that a number exists
     * and the app says something kind when it reaches it, rather than asking a
     * woman every morning whether her period has started.
     */
    const val MAX_CHECK_INS = 3

    /** How close to the fertile window the heads-up fires. */
    const val FERTILE_LEAD_DAYS = 2

    fun decide(
        status: CycleStatus,
        preferences: ReminderPreferences,
        checkInsSoFar: Int,
        fertility: FertilityEstimate?,
        today: LocalDate,
    ): ReminderDecision {
        // Period first, always. Fertility is an estimate about something that
        // might happen; a period arriving is the thing the user opened the app
        // for, and two notifications in one morning is how both get muted.
        periodDecision(status, preferences, checkInsSoFar)?.let { return it }
        fertilityDecision(preferences, fertility, today)?.let { return it }
        return ReminderDecision.Nothing
    }

    private fun periodDecision(
        status: CycleStatus,
        preferences: ReminderPreferences,
        checkInsSoFar: Int,
    ): ReminderDecision? = when (status) {
        is CycleStatus.DuringPeriod ->
            if (preferences.periodEndCheckIn &&
                status.endedOn == null &&
                status.dayOfPeriod >= END_CHECK_IN_AFTER_DAYS
            ) {
                ReminderDecision.Send(ReminderKind.PERIOD_END_CHECK_IN)
            } else {
                null
            }

        is CycleStatus.PeriodApproaching ->
            if (preferences.periodApproaching && status.daysUntil == APPROACHING_LEAD_DAYS) {
                ReminderDecision.Send(ReminderKind.PERIOD_APPROACHING, status.daysUntil)
            } else {
                null
            }

        is CycleStatus.PredictedDay ->
            if (preferences.periodExpectedToday) {
                ReminderDecision.Send(ReminderKind.PERIOD_EXPECTED_TODAY, 0)
            } else {
                null
            }

        is CycleStatus.BeyondForecast -> when {
            !preferences.didItStart -> null
            // The stopping message goes out exactly once, on the wake-up that
            // crosses the limit. After that the count keeps rising and this
            // returns null, so nothing more is sent.
            checkInsSoFar == MAX_CHECK_INS -> ReminderDecision.StopAsking
            checkInsSoFar > MAX_CHECK_INS -> null
            else -> ReminderDecision.Send(ReminderKind.DID_IT_START)
        }

        else -> null
    }

    private fun fertilityDecision(
        preferences: ReminderPreferences,
        fertility: FertilityEstimate?,
        today: LocalDate,
    ): ReminderDecision? {
        fertility ?: return null

        if (preferences.ovulationEstimated && today == fertility.ovulationDate) {
            return ReminderDecision.Send(ReminderKind.OVULATION_ESTIMATED)
        }

        if (preferences.fertileWindowApproaching) {
            val daysToWindow = (fertility.fertileWindowStart.toEpochDay() - today.toEpochDay()).toInt()
            if (daysToWindow in 1..FERTILE_LEAD_DAYS) {
                return ReminderDecision.Send(ReminderKind.FERTILE_WINDOW_APPROACHING, daysToWindow)
            }
        }

        return null
    }
}
