package dev.privacyllc.period.domain.cycle

import kotlin.math.roundToInt

/**
 * How much the app can honestly claim to know.
 *
 * PRODUCT_PLAN.md §27's copy — "Learning your cycle", "Getting to know your
 * pattern", "Personalized to your cycle" — is a claim about the model, so which
 * one is shown has to be a fact about the data rather than a mood.
 */
enum class LearningStage {
    /** Nothing to learn from yet. */
    NO_DATA,

    /** One or two intervals. A forecast exists; a *pattern* does not. */
    LEARNING,

    /** Enough to see a shape, not enough to be sure of it. */
    GETTING_TO_KNOW,

    /** Enough history that the forecast is genuinely hers rather than a default. */
    PERSONALIZED,
}

/**
 * What Insights shows — §27, whose whole purpose is one sentence: **show the
 * user what the app has learned.**
 *
 * Every field is nullable and every one means "not enough data to say" when it
 * is null. That is deliberate: §27 ends with *do not overstate accuracy*, and
 * the easiest way to overstate is to compute a mean over two numbers and print
 * it with a decimal place.
 */
data class CycleInsights(
    val cycleCount: Int,
    val averageCycleDays: Double?,
    val typicalCycleRange: IntRange?,
    val averagePeriodDays: Double?,
    /** Newest first, for the "recent cycles" list. */
    val recentCycleLengths: List<Int>,
    val stage: LearningStage,
) {
    companion object {
        /** Below this there is no average worth printing — one interval is an anecdote. */
        const val MINIMUM_FOR_AVERAGE = 2

        /** Below this, §27's copy says "getting to know" rather than "personalized". */
        const val PERSONALIZED_FROM = 5

        const val RECENT_COUNT = 6

        fun from(periods: List<PeriodRecord>): CycleInsights {
            val cycles = periods.toCycles()
            val lengths = cycles.map { it.cycleLengthDays }.filter { it in 10..90 }

            val durations = periods.mapNotNull { p ->
                p.endDate?.let { (it.toEpochDay() - p.startDate.toEpochDay()).toInt() + 1 }
            }.filter { it in 1..14 }

            val stage = when {
                lengths.isEmpty() -> LearningStage.NO_DATA
                lengths.size < MINIMUM_FOR_AVERAGE -> LearningStage.LEARNING
                lengths.size < PERSONALIZED_FROM -> LearningStage.GETTING_TO_KNOW
                else -> LearningStage.PERSONALIZED
            }

            val enough = lengths.size >= MINIMUM_FOR_AVERAGE

            return CycleInsights(
                cycleCount = lengths.size,
                averageCycleDays = if (enough) lengths.average() else null,
                // The middle of the distribution rather than min..max: one long
                // cycle should not be reported as this user's "typical range",
                // which would make the app describe her as more erratic than she
                // is on the screen whose job is to say what it has learned.
                typicalCycleRange = if (enough) typicalRange(lengths) else null,
                averagePeriodDays = if (durations.size >= MINIMUM_FOR_AVERAGE) durations.average() else null,
                recentCycleLengths = lengths.reversed().take(RECENT_COUNT),
                stage = stage,
            )
        }

        /**
         * The interquartile span, indexed off `size - 1` rather than `size`.
         *
         * Using `size` puts the upper index one place too high, which on an
         * even-length list lands on the largest value — so a history of
         * 29, 28, 30, 29, 61, 29 reported a "typical range" of 29–61 and
         * described a regular cycle as wildly erratic. Caught by the test that
         * says one long cycle must not become the typical range.
         */
        private fun typicalRange(lengths: List<Int>): IntRange {
            val sorted = lengths.sorted()
            val last = sorted.lastIndex
            val low = sorted[(last * 0.25).roundToInt().coerceIn(0, last)]
            val high = sorted[(last * 0.75).roundToInt().coerceIn(0, last)]
            return low..maxOf(low, high)
        }
    }
}
