package com.jsjdesigns.poker

import com.jsjdesigns.poker.bot.CoachingReview
import com.jsjdesigns.poker.game.ActionType
import com.jsjdesigns.poker.game.Street
import kotlin.math.roundToInt

const val PLAYER_HISTORY_SCHEMA_VERSION = 1
const val MIN_DECISIONS_FOR_TREND = 10
const val MIN_OCCURRENCES_FOR_LEAK = 3

/**
 * Stable, aggregate categories for cross-session coaching.
 *
 * The identifiers are persisted and must not be renamed. We deliberately store
 * no cards, board, opponent data, or raw action history.
 */
enum class LeakKind(val id: String, val label: String) {
    PREFLOP_TOO_LOOSE("preflop_too_loose", "Pre-flop continues outside the baseline"),
    PREFLOP_TOO_TIGHT("preflop_too_tight", "Pre-flop folds inside the baseline"),
    PREFLOP_MISSED_RAISE("preflop_missed_raise", "Passed on baseline pre-flop raises"),
    PREFLOP_OVERAGGRESSION("preflop_overaggression", "Raised beyond the pre-flop baseline"),
    POSTFLOP_LOOSE_CONTINUE("postflop_loose_continue", "Post-flop continues below the baseline"),
    POSTFLOP_OVERFOLD("postflop_overfold", "Post-flop folds above the baseline"),
    POSTFLOP_MISSED_AGGRESSION("postflop_missed_aggression", "Passed on baseline bets and raises"),
    POSTFLOP_OVERAGGRESSION("postflop_overaggression", "Aggression beyond the post-flop baseline"),
}

data class PlayerHistory(
    val schemaVersion: Int = PLAYER_HISTORY_SCHEMA_VERSION,
    val playerName: String = "",
    val coachEnabled: Boolean = false,
    val sessionsStarted: Long = 0,
    val handsCompleted: Long = 0,
    val decisionsReviewed: Long = 0,
    val baselineAligned: Long = 0,
    val leakCounts: Map<LeakKind, Long> = emptyMap(),
) {
    init {
        require(schemaVersion == PLAYER_HISTORY_SCHEMA_VERSION)
        require(
            listOf(
                sessionsStarted,
                handsCompleted,
                decisionsReviewed,
                baselineAligned,
            ).all { it >= 0 },
        )
        require(baselineAligned <= decisionsReviewed)
        require(leakCounts.values.all { it >= 0 })
    }

    fun leakCount(kind: LeakKind): Long = leakCounts[kind] ?: 0
}

data class HistoryPresentation(
    val headline: String,
    val detail: String,
)

fun PlayerHistory.recordSession(playerName: String, coachEnabled: Boolean): PlayerHistory =
    copy(
        playerName = playerName,
        coachEnabled = coachEnabled,
        sessionsStarted = sessionsStarted + 1,
    )

fun PlayerHistory.recordCompletedHand(): PlayerHistory =
    copy(handsCompleted = handsCompleted + 1)

fun PlayerHistory.recordCoachReview(review: CoachingReview): PlayerHistory {
    val leak = classifyLeak(review)
    val updatedLeaks = if (leak == null) {
        leakCounts
    } else {
        leakCounts + (leak to leakCount(leak) + 1)
    }
    return copy(
        decisionsReviewed = decisionsReviewed + 1,
        baselineAligned = baselineAligned + if (review.alignedWithBaseline) 1 else 0,
        leakCounts = updatedLeaks,
    )
}

/**
 * Maps one baseline disagreement to at most one durable trend.
 *
 * This is a descriptive fundamentals comparison, not solver proof that the
 * player made a mistake. Bet and raise already count as the same action family
 * in [CoachingReview.alignedWithBaseline].
 */
fun classifyLeak(review: CoachingReview): LeakKind? {
    if (review.alignedWithBaseline) return null
    val intended = review.trace.intended.type
    val chosen = review.trace.chosen.type
    val intendedAggressive = intended == ActionType.BET || intended == ActionType.RAISE
    val chosenAggressive = chosen == ActionType.BET || chosen == ActionType.RAISE

    return if (review.street == Street.PREFLOP) {
        when {
            intended == ActionType.FOLD && chosen != ActionType.FOLD ->
                LeakKind.PREFLOP_TOO_LOOSE
            chosen == ActionType.FOLD && intended != ActionType.FOLD ->
                LeakKind.PREFLOP_TOO_TIGHT
            intendedAggressive && !chosenAggressive ->
                LeakKind.PREFLOP_MISSED_RAISE
            chosenAggressive && !intendedAggressive ->
                LeakKind.PREFLOP_OVERAGGRESSION
            else -> null
        }
    } else {
        when {
            intended == ActionType.FOLD && chosen != ActionType.FOLD ->
                LeakKind.POSTFLOP_LOOSE_CONTINUE
            chosen == ActionType.FOLD && intended != ActionType.FOLD ->
                LeakKind.POSTFLOP_OVERFOLD
            intendedAggressive && !chosenAggressive ->
                LeakKind.POSTFLOP_MISSED_AGGRESSION
            chosenAggressive && !intendedAggressive ->
                LeakKind.POSTFLOP_OVERAGGRESSION
            else -> null
        }
    }
}

fun historyPresentation(history: PlayerHistory): HistoryPresentation? {
    if (history.sessionsStarted == 0L && history.handsCompleted == 0L) return null

    val headline = "${history.handsCompleted} hands saved • " +
        "${history.decisionsReviewed} coached decisions"
    if (history.decisionsReviewed < MIN_DECISIONS_FOR_TREND) {
        val remaining = MIN_DECISIONS_FOR_TREND - history.decisionsReviewed
        return HistoryPresentation(
            headline,
            "$remaining more coached ${if (remaining == 1L) "decision" else "decisions"} " +
                "before calling anything a trend.",
        )
    }

    val agreement = (
        history.baselineAligned.toDouble() /
            history.decisionsReviewed.toDouble() *
            100.0
        ).roundToInt()
    val topLeak = LeakKind.entries
        .map { it to history.leakCount(it) }
        .filter { it.second >= MIN_OCCURRENCES_FOR_LEAK }
        .maxWithOrNull(compareBy<Pair<LeakKind, Long>> { it.second }.thenByDescending { it.first.ordinal })

    val detail = if (topLeak == null) {
        "Baseline agreement $agreement%. No repeated review pattern yet."
    } else {
        "Baseline agreement $agreement%. Most frequent review: " +
            "${topLeak.first.label} (${topLeak.second})."
    }
    return HistoryPresentation(headline, detail)
}
