package dev.privacyllc.period.domain.cycle

import java.time.LocalDate

/** How a period record came to exist. Kept on the record: edits are recorded, never silent. */
enum class PeriodRecordSource { MANUAL, NOTIFICATION_CONFIRMATION, HISTORICAL_ENTRY, EDITED }

/**
 * A period the user confirmed.
 *
 * [endDate] is null while the period is still going or was never closed out —
 * those are different states to the user and the same to the cycle maths, which
 * only reads [startDate].
 */
data class PeriodRecord(
    val id: Long,
    val startDate: LocalDate,
    val endDate: LocalDate? = null,
    val source: PeriodRecordSource = PeriodRecordSource.MANUAL,
    val isConfirmed: Boolean = true,
) {
    /**
     * Deliberately dateless.
     *
     * This is the leak that needs no logging statement. `checkNoHealthLogging`
     * stops anyone writing `Log.d(...)`, but a data class renders its own
     * contents into any string that touches it — an exception message written as
     * "bad record: $record", a crash payload, a coroutine's own diagnostics. The
     * generated `toString()` would put a menstrual date in all three, and nobody
     * would have typed a date anywhere.
     *
     * The id is kept because it identifies the row without describing the
     * person, which is what a diagnostic actually needs.
     */
    override fun toString(): String =
        "PeriodRecord(id=$id, hasEnd=${endDate != null}, source=$source, confirmed=$isConfirmed)"
}

/**
 * Spotting. Tracked separately and **never** treated as a period start.
 *
 * Spotting resetting the cycle is the defect this separate type exists to make
 * impossible — see docs/planning/PRODUCT_PLAN.md §25.
 */
data class SpottingRecord(
    val id: Long,
    val date: LocalDate,
) {
    /** Dateless for the reason [PeriodRecord.toString] gives. */
    override fun toString(): String = "SpottingRecord(id=$id)"
}

/**
 * An interval between two confirmed period starts.
 *
 * Derived, never stored as truth: recomputed from the period records so an edit
 * to a record cannot leave a stale cycle behind it.
 */
data class CycleRecord(
    val previousPeriodStart: LocalDate,
    val currentPeriodStart: LocalDate,
    val cycleLengthDays: Int,
    val periodDurationDays: Int? = null,
) {
    /**
     * Lengths survive, dates do not.
     *
     * A cycle length on its own says nothing about when anything happened, and
     * it is the field worth seeing in a diagnostic about the prediction engine.
     */
    override fun toString(): String =
        "CycleRecord(cycleLengthDays=$cycleLengthDays, periodDurationDays=$periodDurationDays)"
}

/**
 * Derive the cycle intervals from confirmed period records.
 *
 * Unconfirmed records are ignored, records are ordered by start date, and two
 * records sharing a start date contribute one cycle rather than a zero-length
 * one — a duplicate entry is a data problem, not a zero-day cycle.
 */
fun List<PeriodRecord>.toCycles(): List<CycleRecord> =
    asSequence()
        .filter { it.isConfirmed }
        .sortedBy { it.startDate }
        .distinctBy { it.startDate }
        .zipWithNext { previous, current ->
            CycleRecord(
                previousPeriodStart = previous.startDate,
                currentPeriodStart = current.startDate,
                cycleLengthDays = (current.startDate.toEpochDay() - previous.startDate.toEpochDay()).toInt(),
                periodDurationDays = previous.endDate?.let {
                    (it.toEpochDay() - previous.startDate.toEpochDay()).toInt() + 1
                },
            )
        }
        .toList()
