package dev.privacyllc.period.feature.calendar

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import dev.privacyllc.period.core.data.CycleRepository
import dev.privacyllc.period.domain.prediction.CalendarDay
import dev.privacyllc.period.domain.prediction.CalendarMarks
import dev.privacyllc.period.domain.prediction.ConfidenceLabel
import dev.privacyllc.period.domain.prediction.CycleProjection
import dev.privacyllc.period.domain.prediction.DayMark
import dev.privacyllc.period.domain.prediction.FertilityEstimate
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import java.time.Clock
import java.time.LocalDate
import java.time.YearMonth
import javax.inject.Inject

data class CalendarUiState(
    val month: YearMonth = YearMonth.of(2026, 1),
    val days: List<CalendarDay> = emptyList(),
    val today: LocalDate = LocalDate.ofEpochDay(0),
    val message: String? = null,
    val projection: CycleProjection = CycleProjection.Empty,
) {
    /** Blank leading cells so the 1st lands under its weekday. Monday-first. */
    val leadingBlanks: Int
        get() = days.firstOrNull()?.date?.dayOfWeek?.value?.minus(1) ?: 0

    /**
     * How far forward browsing goes.
     *
     * A year, since the calendar now has something to show out there. Logging
     * still stops at today — [CalendarViewModel.select] refuses a future day —
     * so this widens what can be *seen*, never what can be entered.
     */
    val canGoForward: Boolean get() = month < YearMonth.from(today).plusMonths(FORWARD_MONTHS)

    /** True when this month is showing projections rather than the next forecast. */
    val showsProjection: Boolean get() = days.any { it.mark == DayMark.PROJECTED_PERIOD }

    /**
     * The sentence a month of projections has to carry, or null when the month
     * needs no caveat.
     *
     * Two different empty months, deliberately distinguished: one where the app
     * cannot see this far, and one where it simply has no period to draw. Only
     * the first is worth saying anything about, and it is the one that tells her
     * logging more would help.
     */
    val projectionNote: String?
        get() = when {
            showsProjection -> CycleProjection.PROJECTION_ASSUMPTION
            beyondReach -> CycleProjection.BEYOND_HORIZON
            else -> null
        }

    /** This month is past everything the history can support. */
    private val beyondReach: Boolean
        get() = !projection.reachedHorizon &&
            projection.lastProjectedDate?.let { month > YearMonth.from(it) } ?: false

    /** Confidence in the projection covering this month, for the row beside the note. */
    val projectedConfidence: ConfidenceLabel?
        get() = days.firstOrNull { it.mark == DayMark.PROJECTED_PERIOD }
            ?.let { projection.cycleCovering(it.date)?.confidenceLabel }

    private companion object {
        /** A year, matching how far [CycleProjection] will ever reach. */
        const val FORWARD_MONTHS = CycleProjection.MAX_MONTHS_AHEAD
    }
}

@HiltViewModel
class CalendarViewModel @Inject constructor(
    private val repository: CycleRepository,
    private val clock: Clock,
) : ViewModel() {

    private val month = MutableStateFlow(YearMonth.from(LocalDate.now(clock)))
    private val message = MutableStateFlow<String?>(null)

    private val _selected = MutableStateFlow<CalendarDay?>(null)
    val selected: StateFlow<CalendarDay?> = _selected.asStateFlow()

    private val handler = CoroutineExceptionHandler { _, e ->
        message.value = "Could not save that (${e::class.simpleName}). Nothing was changed."
    }

    val state: StateFlow<CalendarUiState> =
        combine(
            month,
            repository.confirmedPeriods,
            repository.spotting,
            repository.forecast,
            message,
        ) { m, periods, spotting, forecast, msg ->
            val fertility = FertilityEstimate.from(forecast)
            val today = LocalDate.now(clock)

            // Her own typical cycle, never a population default: projecting a
            // year on 29 days for a 35-day user is the §3 defect repeated
            // twelve times over. With too little history to have a typical
            // length there is nothing to project, and CycleProjection returns
            // nothing rather than assuming one.
            val lengths = periods.map { it.startDate }.sorted()
                .zipWithNext { a, b -> b.toEpochDay() - a.toEpochDay() }
                .filter { it >= MINIMUM_PLAUSIBLE_CYCLE }
            val typical = if (lengths.isEmpty()) 0.0 else lengths.sorted()[lengths.size / 2].toDouble()

            val projection = CycleProjection.from(forecast, typical, today)

            CalendarUiState(
                month = m,
                days = CalendarMarks.forMonth(
                    month = m,
                    periods = periods,
                    spotting = spotting,
                    forecast = forecast,
                    today = today,
                    fertileWindow = fertility?.fertileWindow,
                    ovulation = fertility?.ovulationDate,
                    projection = projection,
                ),
                today = today,
                message = msg,
                projection = projection,
            )
        }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), CalendarUiState())

    private companion object {
        /** Below this a gap between starts is not a cycle. Mirrors IntervalAnalysis. */
        const val MINIMUM_PLAUSIBLE_CYCLE = 10L
    }

    fun previousMonth() { month.value = month.value.minusMonths(1) }
    fun nextMonth() { if (state.value.canGoForward) month.value = month.value.plusMonths(1) }

    fun select(day: CalendarDay) {
        // A future day cannot be logged — nothing has happened yet — so tapping
        // one does nothing rather than opening a sheet whose every action is
        // disabled.
        if (day.date.isAfter(LocalDate.now(clock))) return
        _selected.value = day
    }

    fun clearSelection() { _selected.value = null }
    fun messageShown() { message.value = null }

    fun logPeriodStart(date: LocalDate) = act {
        repository.confirmPeriodStart(date)
        message.value = "Logged ✓ Your predictions have been updated."
    }

    fun logSpotting(date: LocalDate) = act {
        repository.recordSpotting(date)
        message.value = "Spotting logged. Your cycle is unchanged."
    }

    fun removeEntry(day: CalendarDay) = act {
        when (day.mark) {
            DayMark.SPOTTING -> repository.removeSpotting(day.date)
            DayMark.CONFIRMED_PERIOD ->
                repository.confirmedPeriods.first()
                    .firstOrNull { it.startDate == day.date }
                    ?.let { repository.deletePeriod(it.id) }
                    // Only a period's START day can be removed here. Deleting the
                    // whole record because the user tapped its third day would
                    // remove more than they asked for.
                    ?: run { message.value = "Only the first day of a period can be removed here." }
            else -> Unit
        }
        if (message.value == null) message.value = "Removed."
    }

    private fun act(block: suspend () -> Unit) = viewModelScope.launch(handler) {
        block()
        _selected.value = null
    }
}
