package dev.privacyllc.period.domain.prediction

import java.time.LocalDate

enum class ConfidenceLabel { LOW, MEDIUM, HIGH }

/**
 * A forecast, taken before the outcome is known.
 *
 * The window is not decoration. docs/planning/PRODUCT_PLAN.md §8 and §15 require
 * a range and a confidence rather than a single date presented as fact, so this
 * type has no way to express a bare certain date.
 */
data class Prediction(
    val mostLikelyStartDate: LocalDate,
    val windowStart: LocalDate,
    val windowEnd: LocalDate,
    val confidenceScore: Double,
    val confidenceLabel: ConfidenceLabel,
    val modelVersion: String,
) {
    init {
        require(!windowStart.isAfter(windowEnd)) { "window start $windowStart is after window end $windowEnd" }
        require(mostLikelyStartDate in windowStart..windowEnd) {
            "most likely $mostLikelyStartDate falls outside the window $windowStart..$windowEnd"
        }
        require(confidenceScore in 0.0..1.0) { "confidence $confidenceScore is outside 0..1" }
    }
}

/**
 * The user told us the period had not started by [date].
 *
 * A censoring observation, not a nudge: the next forecast is re-conditioned on
 * it rather than shifted by a day. See PRODUCT_PLAN.md §13.
 */
data class NotYetObservation(
    val date: LocalDate,
    val predictionId: Long? = null,
)

/**
 * Everything an engine is allowed to know.
 *
 * A parameter object rather than a growing argument list, because §15 lists
 * eight things confidence should reflect and they will not all arrive at once.
 *
 * [recentAbsoluteErrors] is how the app learns from being wrong — §12 step 5
 * asks for the window to widen from *recent prediction error* as well as from
 * the user's variability, and every scored `PredictionRecord` already holds one.
 * Newest first. Passed in rather than fetched, because this module must not know
 * that storage exists.
 */
data class PredictionInput(
    val confirmedStarts: List<LocalDate>,
    val today: LocalDate,
    val notYet: List<NotYetObservation> = emptyList(),
    val recentAbsoluteErrors: List<Int> = emptyList(),
)

/**
 * The contract every version of the engine satisfies.
 *
 * Deterministic for the same inputs and model version — PRODUCT_PLAN.md §11 —
 * which is what makes the acceptance cases in §51 testable at all, and what
 * makes one engine comparable to the next.
 */
interface PredictionEngine {
    val modelVersion: String

    fun predict(input: PredictionInput): Prediction?
}

/** The common call, for callers with no scored history to offer. */
fun PredictionEngine.predict(
    confirmedStarts: List<LocalDate>,
    today: LocalDate,
    notYet: List<NotYetObservation> = emptyList(),
): Prediction? = predict(PredictionInput(confirmedStarts, today, notYet))
