package app.closer.domain

import app.closer.domain.model.QuestionPack
import app.closer.domain.model.WeeklyRecap
import app.closer.domain.model.WeeklyRecapAnswer
import app.closer.domain.model.WeeklyRecapCapsule
import app.closer.domain.model.WeeklyRecapInput
import java.time.LocalDate
import java.time.LocalTime

/**
 * Pure, deterministic logic for building the weekly couple recap.
 *
 * No Android dependencies. The generator receives a neutral summary of events for
 * the week and produces a [WeeklyRecap] with counts, a favorite category, a
 * suggested next pack, and formatted copy.
 *
 * Privacy rules enforced here:
 * - Answer text is never read; only answer metadata (category, reveal state, timestamp) is used.
 * - Revealed answers do not expose their text in the recap output.
 * - Category IDs are converted to safe display titles before surfacing to the user.
 */
object WeeklyRecapGenerator {

    internal const val DEFAULT_CTA = "Choose next week's pack"

    /**
     * Main entry point. Builds a [WeeklyRecap] from the provided [input].
     *
     * The generator handles partial data gracefully: any missing event type simply
     * contributes zero to the corresponding count.
     */
    fun generate(input: WeeklyRecapInput): WeeklyRecap {
        val answersInWeek = input.answers.filter { it.localDate inWeek input.weekStart..input.weekEnd }
        val revealsInWeek = input.reveals.filter { it.localDate inWeek input.weekStart..input.weekEnd }
        val gamesInWeek = input.gamesCompleted.filter { it.localDate inWeek input.weekStart..input.weekEnd }
        val challengeDaysInWeek = input.challengeProgress
            .filter { it.localDate inWeek input.weekStart..input.weekEnd }
        val datesInWeek = input.dateIdeasSaved.filter { it.localDate inWeek input.weekStart..input.weekEnd }
        val capsulesInWeek = input.capsules.filter { it.localDate inWeek input.weekStart..input.weekEnd }

        val questionsAnswered = answersInWeek.count { !it.isRevealedOnly || it.isRevealed }
        val revealsOpened = revealsInWeek.size
        val gamesCompleted = gamesInWeek.size
        val challengeProgressDays = challengeDaysInWeek
            .map { it.challengeId to it.day }
            .distinct()
            .size
        val dateIdeaSaved = datesInWeek.isNotEmpty()
        val memoryCapsuleCreated = capsulesInWeek.any { !it.isUnlock }
        val memoryCapsuleUnlocked = capsulesInWeek.any { it.isUnlock }

        val favoriteCategory = computeFavoriteCategory(answersInWeek)
        val favoriteCategoryDisplay = favoriteCategory?.let { input.categoryTitles[it] ?: it }
        val suggestedNextPack = pickSuggestedPack(
            favoriteCategory = favoriteCategory,
            answers = answersInWeek,
            availablePacks = input.availablePacks
        )
        val strongestRhythmCopy = computeStrongestRhythm(answersInWeek)
        val bodyCopy = formatBodyCopy(
            questionsAnswered = questionsAnswered,
            revealsOpened = revealsOpened,
            gamesCompleted = gamesCompleted,
            favoriteCategoryDisplay = favoriteCategoryDisplay,
            strongestRhythmCopy = strongestRhythmCopy
        )

        return WeeklyRecap(
            weekStart = input.weekStart,
            weekEnd = input.weekEnd,
            questionsAnswered = questionsAnswered,
            revealsOpened = revealsOpened,
            gamesCompleted = gamesCompleted,
            challengeProgressDays = challengeProgressDays,
            favoriteCategory = favoriteCategory,
            favoriteCategoryDisplay = favoriteCategoryDisplay,
            dateIdeaSaved = dateIdeaSaved,
            memoryCapsuleCreated = memoryCapsuleCreated,
            memoryCapsuleUnlocked = memoryCapsuleUnlocked,
            suggestedNextPack = suggestedNextPack,
            strongestRhythmCopy = strongestRhythmCopy,
            bodyCopy = bodyCopy,
            ctaCopy = DEFAULT_CTA
        )
    }

    /**
     * Returns the category ID with the most answer events this week, or null when
     * no answers have category information.
     */
    internal fun computeFavoriteCategory(answers: List<WeeklyRecapAnswer>): String? {
        if (answers.isEmpty()) return null
        return answers
            .mapNotNull { it.categoryId }
            .groupingBy { it }
            .eachCount()
            .maxByOrNull { it.value }
            ?.key
    }

    /**
     * Picks a suggested next pack.
     *
     * Strategy:
     * 1. If the favorite category appears in a pack the couple has not used this week,
     *    suggest that pack.
     * 2. Otherwise, suggest a pack covering the couple's least-used category.
     * 3. If no packs are available, return null.
     */
    internal fun pickSuggestedPack(
        favoriteCategory: String?,
        answers: List<WeeklyRecapAnswer>,
        availablePacks: List<QuestionPack>
    ): QuestionPack? {
        if (availablePacks.isEmpty()) return null

        val usedCategoryIds = answers.mapNotNull { it.categoryId }.toSet()

        val byFavorite = favoriteCategory?.let { cat ->
            availablePacks
                .filter { cat in it.categoryIds && it.categoryIds.any { id -> id !in usedCategoryIds } }
                .maxByOrNull { it.categoryIds.count { id -> id == cat } }
        }

        if (byFavorite != null) return byFavorite

        val leastUsedCategory = availablePacks
            .flatMap { it.categoryIds }
            .groupingBy { it }
            .eachCount()
            .minByOrNull { it.value }
            ?.key

        return leastUsedCategory?.let { cat ->
            availablePacks
                .filter { cat in it.categoryIds }
                .maxByOrNull { it.categoryIds.count { id -> id == cat } }
        } ?: availablePacks.firstOrNull()
    }

    /**
     * Computes a friendly description of the couple's most active time pattern.
     *
     * Uses the answer timestamps' hour-of-day. The input currently supplies only a
     * [LocalDate]; to keep the object pure and avoid Java 8 date/time ambiguity,
     * we bucket by the stored local date's day-of-week as a fallback when no
     * hour metadata is present.
     */
    internal fun computeStrongestRhythm(answers: List<WeeklyRecapAnswer>): String? {
        if (answers.size < 2) return null

        val dayCounts = answers
            .groupingBy { it.localDate.dayOfWeek }
            .eachCount()

        val topDay = dayCounts.maxByOrNull { it.value }?.key ?: return null
        val dayName = topDay.name.lowercase().replaceFirstChar { it.uppercase() }
        return "Your strongest rhythm this week was $dayName check-ins."
    }

    /**
     * Formats the body copy. Matches the warm, private tone from the build plan.
     */
    internal fun formatBodyCopy(
        questionsAnswered: Int,
        revealsOpened: Int,
        gamesCompleted: Int,
        favoriteCategoryDisplay: String?,
        strongestRhythmCopy: String?
    ): String {
        val parts = mutableListOf<String>()

        val answerPhrase = when (questionsAnswered) {
            0 -> "You answered no questions"
            1 -> "You answered 1 question"
            else -> "You answered $questionsAnswered questions"
        }

        val revealPhrase = when (revealsOpened) {
            0 -> "opened no reveals"
            1 -> "opened 1 reveal"
            else -> "opened $revealsOpened reveals"
        }

        val gamePhrase = when (gamesCompleted) {
            0 -> "completed no games"
            1 -> "completed 1 game"
            else -> "completed $gamesCompleted games"
        }

        parts.add("$answerPhrase, $revealPhrase, and $gamePhrase.")

        if (favoriteCategoryDisplay != null) {
            parts.add("You spent the most time in $favoriteCategoryDisplay.")
        }

        if (strongestRhythmCopy != null) {
            parts.add(strongestRhythmCopy)
        }

        return parts.joinToString(" ")
    }

    /**
     * Convenience infix operator for readable inclusive-date range checks.
     */
    private infix fun LocalDate.inWeek(range: ClosedRange<LocalDate>): Boolean {
        return !this.isBefore(range.start) && !this.isAfter(range.endInclusive)
    }

    /**
     * An answer contributes to the answered count if it is not a text-only hidden answer,
     * or if it has already been revealed. We still count non-text answers (e.g., scale,
     * multiple choice) because they carry no private content.
     */
    private val WeeklyRecapAnswer.isRevealedOnly: Boolean
        get() = hasText && !isRevealed
}
