package dev.privacyllc.period.domain.prediction

import dev.privacyllc.period.domain.cycle.PeriodRecord
import dev.privacyllc.period.domain.cycle.SpottingRecord
import java.time.LocalDate

/**
 * What a calendar day is, at most one thing.
 *
 * Ordered by precedence, highest first. A day can qualify for several — the
 * predicted window can overlap a fertile window on a short cycle — and showing
 * two markers on one square makes both unreadable at the size a month grid
 * gives you.
 *
 * The rule for the ordering: **a fact outranks an estimate, and the more
 * specific estimate outranks the vaguer one.**
 */
enum class DayMark {
    /** Recorded by the user. The only certain one. */
    CONFIRMED_PERIOD,

    /** Recorded by the user, and deliberately not a period. */
    SPOTTING,

    /** Inside the forecast window. */
    PREDICTED_PERIOD,

    /** The single estimated ovulation day. Batch 04. */
    OVULATION,

    /** Inside the estimated fertile window. Batch 04. */
    FERTILE_WINDOW,

    NONE,
}

data class CalendarDay(
    val date: LocalDate,
    val mark: DayMark,
    val isToday: Boolean,
) {
    /**
     * What a screen reader says.
     *
     * §43: a calendar that reads as a grid of bare numbers carries none of its
     * information. The marker shape is what a sighted user reads; this is the
     * same fact for everybody else, and it is not optional.
     */
    val accessibilityLabel: String = buildString {
        append(date.dayOfMonth)
        when (mark) {
            DayMark.CONFIRMED_PERIOD -> append(", period")
            DayMark.SPOTTING -> append(", spotting")
            DayMark.PREDICTED_PERIOD -> append(", period predicted")
            DayMark.OVULATION -> append(", estimated ovulation")
            DayMark.FERTILE_WINDOW -> append(", estimated fertile window")
            DayMark.NONE -> Unit
        }
        if (isToday) append(", today")
    }
}

object CalendarMarks {

    /**
     * Mark every day in [month].
     *
     * Everything is derived per call rather than stored. A `calendar_days` table
     * would be a second copy of facts the period records and the forecast
     * already hold, and it would be stale the moment either changed.
     */
    fun forMonth(
        month: java.time.YearMonth,
        periods: List<PeriodRecord>,
        spotting: List<SpottingRecord>,
        forecast: Prediction?,
        today: LocalDate,
        fertileWindow: ClosedRange<LocalDate>? = null,
        ovulation: LocalDate? = null,
    ): List<CalendarDay> {
        val confirmed = buildSet {
            periods.filter { it.isConfirmed }.forEach { p ->
                // An unclosed period marks its start day only. Filling forward
                // to today would draw a period the user never said was still
                // running — the calendar would be asserting it.
                val end = p.endDate ?: p.startDate
                var d = p.startDate
                while (!d.isAfter(end)) { add(d); d = d.plusDays(1) }
            }
        }
        val spotted = spotting.map { it.date }.toSet()

        return (1..month.lengthOfMonth()).map { day ->
            val date = month.atDay(day)
            CalendarDay(
                date = date,
                mark = markFor(date, confirmed, spotted, forecast, fertileWindow, ovulation),
                isToday = date == today,
            )
        }
    }

    private fun markFor(
        date: LocalDate,
        confirmed: Set<LocalDate>,
        spotted: Set<LocalDate>,
        forecast: Prediction?,
        fertileWindow: ClosedRange<LocalDate>?,
        ovulation: LocalDate?,
    ): DayMark = when {
        date in confirmed -> DayMark.CONFIRMED_PERIOD
        date in spotted -> DayMark.SPOTTING
        forecast != null && date >= forecast.windowStart && date <= forecast.windowEnd ->
            DayMark.PREDICTED_PERIOD
        ovulation != null && date == ovulation -> DayMark.OVULATION
        fertileWindow != null && date in fertileWindow -> DayMark.FERTILE_WINDOW
        else -> DayMark.NONE
    }
}
