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.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,
) {
    /** Blank leading cells so the 1st lands under its weekday. Monday-first. */
    val leadingBlanks: Int
        get() = days.firstOrNull()?.date?.dayOfWeek?.value?.minus(1) ?: 0

    /** Nothing ahead of today can be logged, so next-month browsing stops there. */
    val canGoForward: Boolean get() = month < YearMonth.from(today).plusMonths(FORWARD_MONTHS)

    private companion object {
        /** One month ahead, so a predicted period near a month boundary is visible. */
        const val FORWARD_MONTHS = 1L
    }
}

@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)
            CalendarUiState(
                month = m,
                days = CalendarMarks.forMonth(
                    month = m,
                    periods = periods,
                    spotting = spotting,
                    forecast = forecast,
                    today = LocalDate.now(clock),
                    fertileWindow = fertility?.fertileWindow,
                    ovulation = fertility?.ovulationDate,
                ),
                today = LocalDate.now(clock),
                message = msg,
            )
        }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), CalendarUiState())

    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
    }
}
