package com.ponzischeme89.memby.ui.calendar

import com.ponzischeme89.memby.data.model.BaseItem
import com.ponzischeme89.memby.data.model.GatewayCalendar

/**
 * The month grid, derived from what the gateway sent and nothing else.
 *
 * The television does no calendar arithmetic of its own: the leading blanks and the number
 * of cells come from `firstWeekday`/`dayCount`, and which cell wears the ring comes from
 * `today`. A set working those out for itself would be a second calendar, kept in the
 * viewer's zone rather than the household's, free to disagree with the days the episodes
 * were grouped into — which on the wrong side of midnight it would.
 *
 * Everything here is pure so the awkward months (one starting on a Sunday, a February, a
 * month with nothing in it at all) are testable with no server and no Compose.
 */

/** One cell. A pad — the blanks before the 1st and after the last — has [day] of zero. */
data class CalendarCell(
    val date: String = "",
    val day: Int = 0,
    val items: List<BaseItem> = emptyList(),
    val today: Boolean = false,
) {
    val isPad: Boolean get() = day <= 0

    /** Pads are skipped by the D-pad: there is nothing there to be told about. */
    val isFocusable: Boolean get() = !isPad
}

const val CALENDAR_COLUMNS = 7

/** Sunday first, matching the `firstWeekday` the gateway sends. */
val CALENDAR_WEEKDAYS = listOf("Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat")

data class CalendarAgendaWeek(
    val cells: List<CalendarCell>,
    val label: String,
    val episodeCount: Int,
)

/** The month payload as television-sized weekly pages. No device date arithmetic. */
fun calendarAgendaWeeks(calendar: GatewayCalendar): List<CalendarAgendaWeek> {
    val month = calendar.label.substringBeforeLast(' ').trim().ifEmpty { "Month" }
    return calendarWeeks(calendar).map { cells ->
        val numbered = cells.filterNot(CalendarCell::isPad)
        val first = numbered.firstOrNull()?.day ?: 0
        val last = numbered.lastOrNull()?.day ?: first
        CalendarAgendaWeek(
            cells = cells,
            label = if (first == last) "$first $month" else "$first–$last $month",
            episodeCount = numbered.sumOf { it.items.size },
        )
    }
}

fun calendarAgendaWeekIndex(weeks: List<CalendarAgendaWeek>, date: String): Int =
    weeks.indexOfFirst { week -> week.cells.any { it.date == date } }.coerceAtLeast(0)

/** Today, then the first airing, then the first real day in that week. */
fun calendarAgendaWeekDate(week: CalendarAgendaWeek): String =
    week.cells.firstOrNull { it.today }?.date
        ?: week.cells.firstOrNull { !it.isPad && it.items.isNotEmpty() }?.date
        ?: week.cells.firstOrNull { !it.isPad }?.date
        ?: ""

/**
 * Lays the month out as whole weeks. The trailing pad is deliberate: a ragged last row
 * would leave the grid's cells different widths from one month to the next, and the eye
 * reads a calendar by its columns.
 */
fun calendarWeeks(calendar: GatewayCalendar): List<List<CalendarCell>> {
    val dayCount = calendar.dayCount.coerceIn(0, 31)
    if (dayCount == 0) return emptyList()
    val leading = calendar.firstWeekday.coerceIn(0, CALENDAR_COLUMNS - 1)
    val byDay = calendar.days.associateBy { it.day }

    val cells = ArrayList<CalendarCell>(leading + dayCount)
    repeat(leading) { cells += CalendarCell() }
    for (day in 1..dayCount) {
        val date = calendarDate(calendar.month, day)
        cells += CalendarCell(
            date = date,
            day = day,
            items = byDay[day]?.items.orEmpty(),
            today = calendar.today.isNotEmpty() && calendar.today == date,
        )
    }
    while (cells.size % CALENDAR_COLUMNS != 0) cells += CalendarCell()
    return cells.chunked(CALENDAR_COLUMNS)
}

/**
 * Which cell the page opens on.
 *
 * Today wins whenever the household is in this month — it is the day somebody opening a TV
 * guide is asking about, whether or not anything airs on it. Travelling to another month
 * lands on its first airing instead, because arriving on the 1st of a month whose news is
 * on the 14th means scrolling past a fortnight of nothing to find out there was any.
 */
fun calendarInitialDate(calendar: GatewayCalendar): String {
    if (calendar.today.isNotEmpty()) return calendar.today
    calendar.days.firstOrNull { it.items.isNotEmpty() }?.let { return calendarDate(calendar.month, it.day) }
    if (calendar.dayCount <= 0) return ""
    return calendarDate(calendar.month, 1)
}

/** How many episodes the month holds, for the line under the header. */
fun calendarEpisodeCount(calendar: GatewayCalendar): Int =
    calendar.days.sumOf { it.items.size }

fun calendarEpisodeCountLabel(count: Int): String = when (count) {
    0 -> "Nothing scheduled"
    1 -> "1 episode"
    else -> "$count episodes"
}

/** The heading over the day panel: "Tuesday 12 August", from the wire's own date string. */
fun calendarDayHeading(calendar: GatewayCalendar, date: String): String {
    val day = calendarDayOf(date) ?: return ""
    val weekday = calendarWeekdayName(calendar, day) ?: return ""
    val month = calendar.label.substringBefore(' ').trim()
    return if (month.isEmpty()) "$weekday $day" else "$weekday $day $month"
}

/**
 * The weekday a numbered day falls on, counted from the grid's own offset rather than from
 * a date library — the same reason the grid is built from `firstWeekday` at all.
 */
private fun calendarWeekdayName(calendar: GatewayCalendar, day: Int): String? {
    if (day < 1 || day > calendar.dayCount.coerceIn(0, 31)) return null
    val leading = calendar.firstWeekday.coerceIn(0, CALENDAR_COLUMNS - 1)
    val fullNames = listOf(
        "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday",
    )
    return fullNames[(leading + day - 1) % CALENDAR_COLUMNS]
}

/**
 * "2026-08" + 12 → "2026-08-12". An empty or malformed month yields no date at all.
 *
 * The shape is checked rather than only the length, because everything downstream compares
 * these strings for equality: a month the gateway sent as something unreadable would
 * otherwise produce thirty-one plausible-looking dates that agree with nothing, instead of
 * thirty-one blanks that visibly select nothing.
 */
internal fun calendarDate(month: String, day: Int): String {
    if (day <= 0 || day > 31 || !isCalendarMonth(month)) return ""
    return month + "-" + if (day < 10) "0$day" else "$day"
}

/** `YYYY-MM`, and nothing else. */
internal fun isCalendarMonth(month: String): Boolean =
    month.length == 7 &&
        month[4] == '-' &&
        (0..3).all { month[it].isDigit() } &&
        month[5].isDigit() &&
        month[6].isDigit()

internal fun calendarDayOf(date: String): Int? {
    if (date.length != 10) return null
    return date.substring(8).toIntOrNull()
}
