package com.jsjdesigns.poker.bot

/**
 * How *correct* a player's decisions are. Independent of [PlayStyle].
 *
 * The interesting lever here is [equityIterations]. Rather than making weak bots
 * flip coins, we give them a noisier estimate of their own hand strength — so a
 * beginner genuinely *misjudges* a hand the way a human does, instead of playing
 * well and then randomly blundering. [errorRate] is a smaller, separate effect for
 * outright mistakes.
 */
enum class SkillLevel(
    val label: String,
    val equityIterations: Int,
    val errorRate: Double,
    val potOddsRespect: Double,
    val positionAwareness: Double,
    val readsOpponents: Boolean,
) {
    // Levels must be far enough apart to be *felt*. An earlier spacing put
    // Advanced and Expert within seed-to-seed noise of each other over 120k
    // hands, which is not two difficulty levels — it is one, labelled twice.
    BEGINNER("Beginner", equityIterations = 120, errorRate = 0.30, potOddsRespect = 0.20, positionAwareness = 0.10, readsOpponents = false),
    INTERMEDIATE("Intermediate", equityIterations = 500, errorRate = 0.16, potOddsRespect = 0.52, positionAwareness = 0.40, readsOpponents = false),
    ADVANCED("Advanced", equityIterations = 1200, errorRate = 0.07, potOddsRespect = 0.76, positionAwareness = 0.70, readsOpponents = false),
    EXPERT("Expert", equityIterations = 3000, errorRate = 0.010, potOddsRespect = 1.00, positionAwareness = 1.00, readsOpponents = true),
}

/**
 * *How* a player plays, independent of how well. A beginner and an expert can both
 * be maniacs; they will be wildly different opponents.
 *
 * All values are 0..1 frequencies or weights.
 */
data class PlayStyle(
    val label: String,
    /** Preference for betting/raising over calling. */
    val aggression: Double,
    /** How wide a range they enter pots with. */
    val looseness: Double,
    /** How often they fire with little or no equity. */
    val bluffFrequency: Double,
    /** Sandbagging: how often they under-represent a strong hand to induce. */
    val slowplayFrequency: Double,
    /** How often they continuation-bet after taking the lead pre-flop. */
    val contBetFrequency: Double,
    /** How much a bad beat destabilises them, feeding the tilt model. */
    val tiltSusceptibility: Double,
) {
    companion object {
        val ROCK = PlayStyle("Rock", aggression = 0.30, looseness = 0.12, bluffFrequency = 0.04, slowplayFrequency = 0.15, contBetFrequency = 0.40, tiltSusceptibility = 0.15)
        val TIGHT_AGGRESSIVE = PlayStyle("Tight-Aggressive", aggression = 0.72, looseness = 0.22, bluffFrequency = 0.18, slowplayFrequency = 0.12, contBetFrequency = 0.70, tiltSusceptibility = 0.30)
        val LOOSE_AGGRESSIVE = PlayStyle("Loose-Aggressive", aggression = 0.82, looseness = 0.45, bluffFrequency = 0.32, slowplayFrequency = 0.10, contBetFrequency = 0.78, tiltSusceptibility = 0.45)
        val CALLING_STATION = PlayStyle("Calling Station", aggression = 0.12, looseness = 0.62, bluffFrequency = 0.03, slowplayFrequency = 0.30, contBetFrequency = 0.20, tiltSusceptibility = 0.25)
        val MANIAC = PlayStyle("Maniac", aggression = 0.95, looseness = 0.75, bluffFrequency = 0.50, slowplayFrequency = 0.05, contBetFrequency = 0.88, tiltSusceptibility = 0.70)
        val TRAPPER = PlayStyle("Trapper", aggression = 0.40, looseness = 0.28, bluffFrequency = 0.10, slowplayFrequency = 0.55, contBetFrequency = 0.35, tiltSusceptibility = 0.20)

        val ALL = listOf(ROCK, TIGHT_AGGRESSIVE, LOOSE_AGGRESSIVE, CALLING_STATION, MANIAC, TRAPPER)
    }
}

/**
 * A named opponent: the crossing of a skill level with a style, plus the mutable
 * emotional state that makes them feel like a person across a session.
 */
data class BotProfile(
    val name: String,
    val skill: SkillLevel,
    val style: PlayStyle,
    /** Short character note; later fed to the LLM for voice and table talk. */
    val persona: String = "",
) {
    val description: String get() = "${skill.label} ${style.label}"
}

/**
 * Emotional state that persists between hands and decays back toward baseline.
 * Tilt widens a player's range and inflates their aggression — the same way it
 * does in a real game.
 */
class BotMood(private val susceptibility: Double) {

    /** -1 (rattled/tilted) .. +1 (running over the table). */
    var tilt: Double = 0.0
        private set

    fun recordLoss(potBigBlinds: Double, wasBadBeat: Boolean) {
        val sting = (potBigBlinds / 40.0).coerceAtMost(1.0) * susceptibility
        tilt -= if (wasBadBeat) sting * 1.8 else sting
        tilt = tilt.coerceIn(-1.0, 1.0)
    }

    fun recordWin(potBigBlinds: Double) {
        tilt += (potBigBlinds / 60.0).coerceAtMost(1.0) * susceptibility * 0.6
        tilt = tilt.coerceIn(-1.0, 1.0)
    }

    /** Called once per hand; mood fades rather than lasting forever. */
    fun decay() {
        tilt *= 0.90
        if (tilt in -0.01..0.01) tilt = 0.0
    }

    /** Tilted players play looser and more aggressively, in both directions. */
    fun loosenessBonus(): Double = if (tilt < 0) -tilt * 0.35 else tilt * 0.12
    fun aggressionBonus(): Double = if (tilt < 0) -tilt * 0.30 else tilt * 0.18
}
