package app.closer.domain

import app.closer.domain.model.ChallengeState
import app.closer.domain.model.ChallengeStateInput
import app.closer.domain.model.ChallengeStatus
import java.time.LocalDate
import java.time.temporal.ChronoUnit

/**
 * Pure, deterministic state machine for couple challenge progress.
 *
 * No Android dependencies. Inputs are local dates and completion lists; output is a [ChallengeState]
 * with user-facing copy and a clear next action.
 *
 * State lifecycle:
 * 1. Not started
 * 2. Started today (partner completed the current step, user has not)
 * 3. Waiting for partner (user completed the current step, partner has not)
 * 4. Both completed today
 * 5. Day missed (a past day has no progress from either partner)
 * 6. Challenge complete (all days jointly done or explicitly finished)
 *
 * Missed-day behavior is configurable via [ChallengeStateInput.breakOnMissedDay]:
 * - false (default): missed days are surfaced but do not block advancement.
 * - true: a missed day produces [ChallengeStatus.DAY_MISSED] and disables advancement until
 *   the caller handles recovery.
 */
object ChallengeStateMachine {

    /**
     * Computes the current [ChallengeState] from [input].
     */
    fun compute(input: ChallengeStateInput): ChallengeState {
        val challenge = input.challenge
        val progress = input.progress
        val today = input.today
        val totalDays = challenge.durationDays.coerceAtLeast(1)

        val myCompleted = progress.myCompletedDays.toSortedSet()
        val partnerCompleted = progress.partnerCompletedDays.toSortedSet()
        val jointCompleted = myCompleted.intersect(partnerCompleted).toSortedSet()

        val startedAtDate = if (progress.startedAt > 0L) {
            millisToLocalDate(progress.startedAt)
        } else {
            null
        }

        // The day the UI should display as "today's step" — the user's next incomplete day.
        val currentDay = ((myCompleted.maxOrNull() ?: 0) + 1).coerceAtMost(totalDays)

        // Challenge is complete when explicitly finished or all days jointly completed.
        val allJointlyComplete = jointCompleted.size >= totalDays
        val isExplicitlyComplete = progress.isComplete
        val isComplete = isExplicitlyComplete || allJointlyComplete

        if (isComplete) {
            return completeState(
                title = challenge.title,
                emoji = challenge.emoji,
                totalDays = totalDays,
                myCompletedDays = myCompleted.toList(),
                partnerCompletedDays = partnerCompleted.toList(),
                jointCompletedDays = jointCompleted.toList()
            )
        }

        // If the challenge has never started, return the not-started state.
        if (startedAtDate == null && myCompleted.isEmpty() && partnerCompleted.isEmpty()) {
            return notStartedState(
                title = challenge.title,
                emoji = challenge.emoji,
                totalDays = totalDays
            )
        }

        // The day used to evaluate today's status. When a start date is available we map the
        // calendar day to a challenge day; otherwise we evaluate the most recent day that has
        // any progress (or the user's next day if no progress exists yet).
        val statusDay = if (startedAtDate != null) {
            val daysSinceStart = ChronoUnit.DAYS.between(startedAtDate, today) + 1
            daysSinceStart.toInt().coerceAtLeast(1).coerceAtMost(totalDays)
        } else {
            val maxProgressDay = (myCompleted + partnerCompleted).maxOrNull() ?: currentDay
            maxProgressDay.coerceAtMost(totalDays)
        }

        // Detect missed day: the most recent day strictly before the status day that has no
        // progress from either partner. We never flag the current status day as missed because it
        // may still be in progress.
        val missedDate = findMissedDay(
            statusDay = statusDay,
            startDate = startedAtDate ?: today,
            today = today,
            totalDays = totalDays,
            myCompletedDays = myCompleted,
            partnerCompletedDays = partnerCompleted
        )

        val myDoneStatus = myCompleted.contains(statusDay)
        val partnerDoneStatus = partnerCompleted.contains(statusDay)

        val status = when {
            input.breakOnMissedDay && missedDate != null -> ChallengeStatus.DAY_MISSED
            myDoneStatus && partnerDoneStatus -> ChallengeStatus.BOTH_COMPLETED_TODAY
            myDoneStatus -> ChallengeStatus.WAITING_FOR_PARTNER
            partnerDoneStatus -> ChallengeStatus.STARTED_TODAY
            missedDate != null -> ChallengeStatus.DAY_MISSED
            else -> ChallengeStatus.NOT_STARTED
        }

        val copy = copyFor(status, challenge.title, totalDays)
        val cta = ctaFor(status, myDoneStatus, currentDay, totalDays)
        val canAdvance = when (status) {
            ChallengeStatus.CHALLENGE_COMPLETE -> false
            ChallengeStatus.WAITING_FOR_PARTNER -> false // user already did their part; wait for partner
            ChallengeStatus.BOTH_COMPLETED_TODAY -> false // today's shared step is done
            ChallengeStatus.DAY_MISSED -> !input.breakOnMissedDay // recovery allowed unless break-on-miss
            else -> true
        }

        return ChallengeState(
            state = status,
            title = challenge.title,
            emoji = challenge.emoji,
            totalDays = totalDays,
            currentDay = currentDay,
            myCompletedDays = myCompleted.toList(),
            partnerCompletedDays = partnerCompleted.toList(),
            jointCompletedDays = jointCompleted.toList(),
            isComplete = false,
            copy = copy,
            cta = cta,
            badge = null,
            canAdvance = canAdvance,
            missedDate = missedDate
        )
    }

    // -----------------------------------------------------------------
    // State constructors
    // -----------------------------------------------------------------

    private fun notStartedState(
        title: String,
        emoji: String,
        totalDays: Int
    ): ChallengeState = ChallengeState(
        state = ChallengeStatus.NOT_STARTED,
        title = title,
        emoji = emoji,
        totalDays = totalDays,
        currentDay = 1,
        copy = "Your $totalDays-day challenge is ready — do today's step and mark it done.",
        cta = "I did it today",
        canAdvance = true
    )

    private fun completeState(
        title: String,
        emoji: String,
        totalDays: Int,
        myCompletedDays: List<Int>,
        partnerCompletedDays: List<Int>,
        jointCompletedDays: List<Int>
    ): ChallengeState = ChallengeState(
        state = ChallengeStatus.CHALLENGE_COMPLETE,
        title = title,
        emoji = emoji,
        totalDays = totalDays,
        currentDay = totalDays,
        myCompletedDays = myCompletedDays,
        partnerCompletedDays = partnerCompletedDays,
        jointCompletedDays = jointCompletedDays,
        isComplete = true,
        copy = "You finished \"$title\" together. That's $totalDays days of showing up.",
        cta = null,
        badge = "🏅",
        canAdvance = false
    )

    // -----------------------------------------------------------------
    // Copy
    // -----------------------------------------------------------------

    internal fun copyFor(status: ChallengeStatus, title: String, durationDays: Int): String = when (status) {
        ChallengeStatus.NOT_STARTED -> "Do today's step together — $durationDays days total."
        ChallengeStatus.STARTED_TODAY -> "Your partner completed today — now it's your turn."
        ChallengeStatus.WAITING_FOR_PARTNER -> "You did your part today. Waiting for your partner."
        ChallengeStatus.BOTH_COMPLETED_TODAY -> "Both of you showed up today."
        ChallengeStatus.DAY_MISSED -> "No shame. Pick it back up tonight."
        ChallengeStatus.CHALLENGE_COMPLETE -> "You finished \"$title\" together."
    }

    internal fun ctaFor(
        status: ChallengeStatus,
        myDoneToday: Boolean,
        currentDay: Int,
        totalDays: Int
    ): String? = when (status) {
        ChallengeStatus.NOT_STARTED -> "I did it today"
        ChallengeStatus.STARTED_TODAY ->
            if (myDoneToday) null else "I did it today"
        ChallengeStatus.WAITING_FOR_PARTNER -> null  // already done; no action available
        ChallengeStatus.BOTH_COMPLETED_TODAY ->
            if (currentDay < totalDays) "See tomorrow's step" else "Challenge complete"
        ChallengeStatus.DAY_MISSED -> "Pick it back up"
        ChallengeStatus.CHALLENGE_COMPLETE -> null
    }

    // -----------------------------------------------------------------
    // Missed-day detection
    // -----------------------------------------------------------------

    /**
     * Returns the most recent missed local day, if any.
     *
     * A day is considered missed when:
     * - It is strictly before the current [statusDay].
     * - It is at or before [today] (we do not flag future days).
     * - Neither partner has marked that day complete.
     *
     * Only the latest missed day is returned to keep the UI focused on recovery.
     */
    internal fun findMissedDay(
        statusDay: Int,
        startDate: LocalDate,
        today: LocalDate,
        totalDays: Int,
        myCompletedDays: Set<Int>,
        partnerCompletedDays: Set<Int>
    ): LocalDate? {
        // Iterate backward from the day just before the status day.
        for (day in (statusDay - 1) downTo 1) {
            val dayDate = startDate.plusDays((day - 1).toLong())
            if (dayDate.isAfter(today)) continue
            val anyoneDidIt = day in myCompletedDays || day in partnerCompletedDays
            if (!anyoneDidIt) return dayDate
        }
        return null
    }

    // -----------------------------------------------------------------
    // Helpers
    // -----------------------------------------------------------------

    /**
     * Best-effort conversion of a UTC timestamp into a [LocalDate].
     * The state machine treats dates as local calendar days; this helper is a convenience
     * for callers that only have epoch millis.
     */
    internal fun millisToLocalDate(millis: Long): LocalDate =
        java.time.Instant.ofEpochMilli(millis)
            .atZone(java.time.ZoneId.systemDefault())
            .toLocalDate()
}
