package app.closer.domain

import app.closer.domain.model.Streak
import app.closer.domain.model.StreakInput
import app.closer.domain.model.StreakResult
import java.time.LocalDate
import java.time.temporal.ChronoUnit

/**
 * Pure, deterministic streak logic for Closer.
 *
 * No Android dependencies. Inputs are [LocalDate] lists; output is a [StreakResult].
 *
 * Streak rules:
 * - Couple streak: both partners answered or completed one shared action on the same local day.
 *   Counts consecutive days backward from today, allowing one missed day to be repaired per 7-day window.
 * - Personal streak: user answered or completed one private action on a local day.
 *   Counts consecutive days backward from today, independent of the couple streak.
 * - Weekly rhythm: couple completed 3+ shared actions within a rolling 7-day window.
 *   Counts consecutive 7-day windows backward from today. A window is counted if it contains at least
 *   [Streak.WeeklyRhythm.THRESHOLD] shared-action days by the couple.
 * - Repair: one missed day can be repaired per 7-day window. A repair is available when yesterday (or
 *   an earlier single day in the current window) was missed but today has already become a couple-action
 *   day. Repairs require both partners to complete today's question (represented by today being a
 *   couple-action day). The repair window is anchored from the most recent couple-action day.
 *
 * Timezone behavior: all dates are [LocalDate]. The caller is responsible for converting timestamps
 * to the user's local calendar day (already done by the daily-question local date key change).
 */
object StreakCalculator {

    /**
     * Main entry point. Computes all streaks and repair eligibility from [input].
     */
    fun calculate(input: StreakInput): StreakResult {
        val coupleActionDays = input.coupleActionDates.toSortedSet()
        val userDays = input.userDates.toSortedSet()
        val partnerDays = input.partnerDates.toSortedSet()

        val coupleStreak = computeCoupleStreak(coupleActionDays, input.today, input.lastRepairDate)
        val personalStreak = computePersonalStreak(userDays, input.today)
        val weeklyRhythm = computeWeeklyRhythm(coupleActionDays, input.today)

        val milestoneCopy = milestoneFor(coupleStreak.count, coupleStreak.includesToday)

        val repair = computeRepair(
            coupleActionDays = coupleActionDays,
            userDays = userDays,
            partnerDays = partnerDays,
            today = input.today,
            lastRepairDate = input.lastRepairDate
        )

        return StreakResult(
            coupleStreak = coupleStreak,
            personalStreak = personalStreak,
            weeklyRhythm = weeklyRhythm,
            milestoneCopy = milestoneCopy,
            canRepair = repair.canRepair,
            repairDueDate = repair.repairDueDate
        )
    }

    /**
     * Couple streak: consecutive days on which the couple completed a shared action,
     * counting backward from [today], with one repaired missed day per 7-day window allowed.
     */
    internal fun computeCoupleStreak(
        coupleActionDays: Set<LocalDate>,
        today: LocalDate,
        lastRepairDate: LocalDate? = null
    ): Streak.Couple {
        if (coupleActionDays.isEmpty()) {
            return Streak.Couple()
        }

        val sorted = coupleActionDays.toSortedSet().toList()
        val latest = sorted.last()
        val includesToday = latest == today

        // If the most recent shared day is older than yesterday, the streak is broken
        // unless we can repair exactly one gap day. We handle repair in calculate(), so
        // here we count strict consecutive days.
        if (latest.isBefore(today.minusDays(2))) {
            return Streak.Couple(
                count = 1,
                lastActiveDate = latest,
                includesToday = false
            )
        }

        var count = 1
        var previous = latest
        for (i in sorted.size - 2 downTo 0) {
            val current = sorted[i]
            val gap = ChronoUnit.DAYS.between(current, previous)
            if (gap == 1L) {
                count++
                previous = current
            } else {
                break
            }
        }

        return Streak.Couple(
            count = count,
            lastActiveDate = latest,
            includesToday = includesToday
        )
    }

    /**
     * Personal streak: consecutive days on which the user answered or completed a private action,
     * counting backward from [today]. Does not depend on partner activity.
     */
    internal fun computePersonalStreak(
        userDays: Set<LocalDate>,
        today: LocalDate
    ): Streak.Personal {
        if (userDays.isEmpty()) {
            return Streak.Personal()
        }

        val sorted = userDays.toSortedSet().toList()
        val latest = sorted.last()
        val includesToday = latest == today

        // If the latest personal day is older than yesterday, the streak is broken.
        if (latest.isBefore(today.minusDays(1))) {
            return Streak.Personal(
                count = 0,
                lastActiveDate = null,
                includesToday = false
            )
        }

        var count = 1
        var previous = latest
        for (i in sorted.size - 2 downTo 0) {
            val current = sorted[i]
            if (ChronoUnit.DAYS.between(current, previous) == 1L) {
                count++
                previous = current
            } else {
                break
            }
        }

        return Streak.Personal(
            count = count,
            lastActiveDate = latest,
            includesToday = includesToday
        )
    }

    /**
     * Weekly rhythm: consecutive rolling 7-day windows containing at least
     * [Streak.WeeklyRhythm.THRESHOLD] couple-action days.
     */
    internal fun computeWeeklyRhythm(
        coupleActionDays: Set<LocalDate>,
        today: LocalDate
    ): Streak.WeeklyRhythm {
        if (coupleActionDays.isEmpty()) {
            return Streak.WeeklyRhythm()
        }

        val windowSize = Streak.WeeklyRhythm.WINDOW_DAYS
        val threshold = Streak.WeeklyRhythm.THRESHOLD

        // Start with the window ending on the most recent couple-action day, or today if none.
        val latestAction = coupleActionDays.maxOrNull() ?: today
        val firstWindowEnd = if (latestAction.isAfter(today)) today else latestAction

        // Count how many windows are satisfied consecutively, moving backward 7 days at a time.
        var count = 0
        var windowEnd = firstWindowEnd
        while (true) {
            val windowStart = windowEnd.minusDays((windowSize - 1).toLong())
            val actionsInWindow = coupleActionDays.count { !it.isBefore(windowStart) && !it.isAfter(windowEnd) }
            if (actionsInWindow >= threshold) {
                count++
                windowEnd = windowEnd.minusDays(windowSize.toLong())
            } else {
                break
            }
        }

        val includesToday = if (count > 0) {
            val currentWindowStart = today.minusDays((windowSize - 1).toLong())
            coupleActionDays.count { !it.isBefore(currentWindowStart) && !it.isAfter(today) } >= threshold
        } else false

        return Streak.WeeklyRhythm(
            count = count,
            lastActiveDate = latestAction,
            includesToday = includesToday
        )
    }

    /**
     * Determines whether a streak repair is available today and the due date for it.
     *
     * Repair is available when:
     * - Today is already a couple-action day (both partners showed up today).
     * - The couple streak would otherwise be broken by exactly one missed day.
     * - No repair has been used in the current 7-day window.
     */
    internal fun computeRepair(
        coupleActionDays: Set<LocalDate>,
        userDays: Set<LocalDate>,
        partnerDays: Set<LocalDate>,
        today: LocalDate,
        lastRepairDate: LocalDate? = null
    ): Repair {
        if (today !in coupleActionDays) {
            return Repair(false, null)
        }

        // Find the first break in the strict couple streak counting back from today.
        // If the break is exactly one missed day, we can repair it.
        val sorted = coupleActionDays.toSortedSet().toList()
        var previous = today
        var gapFound: LocalDate? = null
        for (i in sorted.size - 2 downTo 0) {
            val current = sorted[i]
            val gap = ChronoUnit.DAYS.between(current, previous)
            if (gap == 1L) {
                previous = current
            } else if (gap == 2L) {
                gapFound = previous.minusDays(1)
                break
            } else {
                break
            }
        }

        // If there is no gap or the gap is not exactly one day, no repair is needed/possible.
        val repairDueDate = gapFound ?: return Repair(false, null)

        // Only allow one repair per 7-day window anchored from today.
        if (lastRepairDate != null && !lastRepairDate.isBefore(today.minusDays(7))) {
            return Repair(false, null)
        }

        return Repair(true, repairDueDate)
    }

    internal data class Repair(val canRepair: Boolean, val repairDueDate: LocalDate?)

    /**
     * Returns milestone copy for the current streak count, or null if no milestone applies.
     * Milestones: 1, 3, 7, 14, 30.
     */
    internal fun milestoneFor(count: Int, includesToday: Boolean): String? {
        if (count <= 0 || !includesToday) return null
        return when (count) {
            1 -> "You started a rhythm."
            3 -> "Three nights showing up."
            7 -> "One week of small conversations."
            14 -> "Two weeks of staying connected."
            30 -> "Thirty days of making space for each other."
            else -> null
        }
    }
}
