package ru.forbion.f7cloud.feature.calendar

import java.time.DayOfWeek
import java.time.LocalDate
import java.time.temporal.ChronoField
import java.time.temporal.ChronoUnit

/**
 * Локальный разворот повторяющихся событий (P1, mail/072/093).
 *
 * Генерирует даты-старты оккуренций правила RRULE в заданном диапазоне БЕЗ обращения к серверу
 * (`<c:expand>` не используем — это правка core, mail/072). Поддерживается то же подмножество,
 * что умеет создавать нативный редактор ([RecurrenceRule]): FREQ=DAILY|WEEKLY|MONTHLY|YEARLY,
 * INTERVAL, BYDAY (для WEEKLY), окончание UNTIL/COUNT/никогда.
 *
 * Развёртка делается на КАЖДЫЙ показ (не кэшируется — иначе окно диапазона «вмораживается»,
 * mail/072). Экземпляры различимы парой (uid, дата-старт оккуренции).
 */
object CalendarRecurrence {

    /** Жёсткий предел числа шагов генератора — страховка от бесконечных/патологических правил. */
    private const val MAX_STEPS = 3660

    /**
     * Покрывает ли событие [day] с учётом повторения.
     *
     * @param start дата-старт мастер-события (в нужной зоне).
     * @param spanDays длительность события в днях для all-day (>=1); для timed игнорируется.
     * @param allDay all-day событие занимает [occStart, occStart+spanDays); timed — только occStart.
     */
    fun occursOnDay(
        rule: RecurrenceRule,
        start: LocalDate,
        day: LocalDate,
        spanDays: Long,
        allDay: Boolean,
    ): Boolean {
        if (day.isBefore(start)) return false
        val span = if (allDay) spanDays.coerceAtLeast(1) else 1L
        // Оккуренция покрывает day, если её старт в диапазоне (day - (span-1) .. day).
        val earliestStart = day.minusDays(span - 1)
        var found = false
        forEachOccurrenceStart(rule, start, upTo = day) { occStart ->
            if (!occStart.isBefore(earliestStart) && !occStart.isAfter(day)) {
                found = true
                false // стоп
            } else {
                true // продолжаем
            }
        }
        return found
    }

    /** Список дат-стартов оккуренций в [rangeStart, rangeEnd] включительно (для списковых видов). */
    fun occurrenceStartsInRange(
        rule: RecurrenceRule,
        start: LocalDate,
        rangeStart: LocalDate,
        rangeEnd: LocalDate,
    ): List<LocalDate> {
        val out = mutableListOf<LocalDate>()
        forEachOccurrenceStart(rule, start, upTo = rangeEnd) { occStart ->
            if (!occStart.isBefore(rangeStart)) out.add(occStart)
            true
        }
        return out
    }

    /** Границы окончания правила: (UNTIL-дата или null, максимум оккуренций по COUNT). */
    private fun bounds(rule: RecurrenceRule): Pair<LocalDate?, Int> {
        val until = if (rule.endType == RecurrenceRule.EndType.UNTIL) rule.untilDate else null
        val maxCount = if (rule.endType == RecurrenceRule.EndType.COUNT) rule.count.coerceAtLeast(1) else Int.MAX_VALUE
        return until to maxCount
    }

    /**
     * Перебирает даты-старты оккуренций от [start], пока не превышена [upTo], не исчерпан
     * UNTIL/COUNT или лимит шагов. [onOccurrence] возвращает false, чтобы прервать перебор.
     */
    private inline fun forEachOccurrenceStart(
        rule: RecurrenceRule,
        start: LocalDate,
        upTo: LocalDate,
        onOccurrence: (LocalDate) -> Boolean,
    ) {
        when {
            rule.freq == "MONTHLY" && rule.monthlyOrdinal != null && rule.byDays.isNotEmpty() ->
                forEachMonthlyNthWeekday(rule, start, upTo, onOccurrence)
            rule.freq == "WEEKLY" && rule.byDays.isNotEmpty() ->
                forEachWeeklyByDay(rule, start, upTo, onOccurrence)
            else -> forEachSimpleStep(rule, start, upTo, onOccurrence)
        }
    }

    /** MONTHLY с BYDAY=<n><день>: n-й (или последний при -1) день недели каждого интервал-месяца. */
    private inline fun forEachMonthlyNthWeekday(
        rule: RecurrenceRule,
        start: LocalDate,
        upTo: LocalDate,
        onOccurrence: (LocalDate) -> Boolean,
    ) {
        val interval = rule.interval.coerceAtLeast(1)
        val (until, maxCount) = bounds(rule)
        val ordinal = rule.monthlyOrdinal ?: return
        val dow = RecurrenceRule.ICS_DAYS.firstOrNull { it in rule.byDays }?.let { icsDayToDow(it) } ?: return
        val baseMonth = java.time.YearMonth.from(start)
        var emitted = 0
        var n = 0L
        while (n < MAX_STEPS) {
            val month = baseMonth.plusMonths(n * interval)
            val occ = nthWeekdayOfMonth(month, dow, ordinal)
            n++
            if (occ == null || occ.isBefore(start)) {
                if (month.atEndOfMonth().isAfter(upTo)) return
                continue
            }
            val pastUntil = until != null && occ.isAfter(until)
            if (occ.isAfter(upTo) || pastUntil || emitted >= maxCount) return
            emitted++
            if (!onOccurrence(occ)) return
        }
    }

    /** n-й день недели [dow] месяца [month]; ordinal>0 — с начала, -1 — последний; null если нет. */
    private fun nthWeekdayOfMonth(month: java.time.YearMonth, dow: DayOfWeek, ordinal: Int): LocalDate? {
        if (ordinal < 0) {
            val last = month.atEndOfMonth()
            val shift = (last.dayOfWeek.value - dow.value + 7) % 7
            val candidate = last.minusDays(shift.toLong())
            return if (java.time.YearMonth.from(candidate) == month) candidate else null
        }
        val first = month.atDay(1)
        val shift = (dow.value - first.dayOfWeek.value + 7) % 7
        val candidate = first.plusDays(shift.toLong() + (ordinal - 1) * 7L)
        return if (java.time.YearMonth.from(candidate) == month) candidate else null
    }

    /** WEEKLY с BYDAY: раскрываем выбранные дни недели в каждом интервал-блоке недель. */
    private inline fun forEachWeeklyByDay(
        rule: RecurrenceRule,
        start: LocalDate,
        upTo: LocalDate,
        onOccurrence: (LocalDate) -> Boolean,
    ) {
        val interval = rule.interval.coerceAtLeast(1)
        val (until, maxCount) = bounds(rule)
        val weeklyDows = RecurrenceRule.ICS_DAYS.filter { it in rule.byDays }.map { icsDayToDow(it).value.toLong() }
        val weekBase = mondayOf(start)
        var emitted = 0
        var block = 0L
        while (block < MAX_STEPS) {
            val weekStart = weekBase.plusWeeks(block * interval)
            if (weekStart.isAfter(upTo)) return // все дни этой и следующих недель уже за upTo
            for (dowValue in weeklyDows) {
                val occ = weekStart.with(ChronoField.DAY_OF_WEEK, dowValue)
                if (occ.isBefore(start) || occ.isAfter(upTo)) continue
                if (until != null && occ.isAfter(until)) return
                if (emitted >= maxCount) return
                emitted++
                if (!onOccurrence(occ)) return
            }
            block++
        }
    }

    /** DAILY / WEEKLY(без BYDAY) / MONTHLY / YEARLY: простой шаг по мастер-старту. */
    private inline fun forEachSimpleStep(
        rule: RecurrenceRule,
        start: LocalDate,
        upTo: LocalDate,
        onOccurrence: (LocalDate) -> Boolean,
    ) {
        val interval = rule.interval.coerceAtLeast(1)
        val (until, maxCount) = bounds(rule)
        var emitted = 0
        var n = 0L
        while (n < MAX_STEPS) {
            val occ = when (rule.freq) {
                "DAILY" -> start.plusDays(n * interval)
                "WEEKLY" -> start.plusWeeks(n * interval)
                "MONTHLY" -> start.plusMonths(n * interval)
                "YEARLY" -> start.plusYears(n * interval)
                else -> return
            }
            val pastUntil = until != null && occ.isAfter(until)
            if (occ.isAfter(upTo) || pastUntil || emitted >= maxCount) return
            emitted++
            if (!onOccurrence(occ)) return
            n++
        }
    }

    private fun mondayOf(date: LocalDate): LocalDate = date.with(ChronoField.DAY_OF_WEEK, 1L)

    private fun icsDayToDow(ics: String): DayOfWeek = when (ics) {
        "MO" -> DayOfWeek.MONDAY
        "TU" -> DayOfWeek.TUESDAY
        "WE" -> DayOfWeek.WEDNESDAY
        "TH" -> DayOfWeek.THURSDAY
        "FR" -> DayOfWeek.FRIDAY
        "SA" -> DayOfWeek.SATURDAY
        else -> DayOfWeek.SUNDAY
    }

    /** Число дней-длительности для all-day по границам [start,end) (в датах). */
    fun allDaySpanDays(start: LocalDate, end: LocalDate): Long =
        ChronoUnit.DAYS.between(start, end).coerceAtLeast(1)
}
