package com.jsjdesigns.poker.sim

import com.jsjdesigns.poker.bot.BotProfile
import com.jsjdesigns.poker.bot.MathBot
import com.jsjdesigns.poker.bot.PlayStyle
import com.jsjdesigns.poker.bot.SkillLevel
import com.jsjdesigns.poker.core.Card
import com.jsjdesigns.poker.core.PreflopChart
import com.jsjdesigns.poker.core.Suit
import com.jsjdesigns.poker.game.ActionType
import com.jsjdesigns.poker.game.Seat
import com.jsjdesigns.poker.game.Street
import com.jsjdesigns.poker.game.Table
import kotlin.math.abs
import kotlinx.coroutines.runBlocking
import kotlin.random.Random

private const val SMALL_BLIND = 1
private const val BIG_BLIND = 2
private const val STARTING_STACK = 200 // 100 big blinds

private class Stats(val name: String, val profile: BotProfile) {
    var net = 0L
    var hands = 0
    var vpip = 0
    var pfr = 0
    var wins = 0
    var postflopBets = 0
    var postflopCalls = 0

    fun bbPer100(): Double = if (hands == 0) 0.0 else (net.toDouble() / BIG_BLIND) / hands * 100.0
    fun pct(n: Int): Double = if (hands == 0) 0.0 else n * 100.0 / hands
    fun aggressionFactor(): Double =
        if (postflopCalls == 0) postflopBets.toDouble() else postflopBets.toDouble() / postflopCalls
}

private fun runTable(label: String, roster: List<BotProfile>, hands: Int, seed: Long): List<Stats> = runBlocking {
    // The deck gets its own RNG. If bots drew from the same stream, the number of
    // Monte Carlo rollouts a bot performs — which varies by skill level — would
    // shift every subsequent deal, so changing a profile would silently change the
    // cards and no two runs would be comparable.
    val deckRandom = Random(seed)
    val bots = roster.mapIndexed { i, p -> MathBot(p, Random(seed * 31 + i)) }
    val stats = roster.map { Stats(it.name, it) }
    val seats = roster.mapIndexed { i, p -> Seat(i, p.name, STARTING_STACK, bots[i]) }
    val table = Table(seats, SMALL_BLIND, BIG_BLIND, deckRandom)

    println("\n=== $label ===")
    println("$hands hands, ${roster.size}-handed, ${STARTING_STACK / BIG_BLIND}bb stacks, seed=$seed")
    val started = System.nanoTime()

    // VPIP/PFR are per-hand booleans, not action counts: a player who calls and
    // then calls a re-raise has still only entered the pot once.
    val enteredPot = BooleanArray(seats.size)
    val raisedPre = BooleanArray(seats.size)

    repeat(hands) {
        for (s in seats) s.stack = STARTING_STACK
        java.util.Arrays.fill(enteredPot, false)
        java.util.Arrays.fill(raisedPre, false)
        table.advanceButton()

        val result = table.playHand()

        for (e in result.events) {
            val st = stats[e.seat]
            if (e.street == Street.PREFLOP) {
                when (e.action.type) {
                    ActionType.CALL -> enteredPot[e.seat] = true
                    ActionType.BET, ActionType.RAISE -> {
                        enteredPot[e.seat] = true; raisedPre[e.seat] = true
                    }
                    else -> Unit
                }
            } else {
                when (e.action.type) {
                    ActionType.BET, ActionType.RAISE -> st.postflopBets++
                    ActionType.CALL -> st.postflopCalls++
                    else -> Unit
                }
            }
        }

        for (i in seats.indices) {
            val st = stats[i]
            st.hands++
            st.net += result.net[i]
            if (enteredPot[i]) st.vpip++
            if (raisedPre[i]) st.pfr++
            if (i in result.winners) st.wins++

            val bb = abs(result.net[i]).toDouble() / BIG_BLIND
            if (result.net[i] > 0) bots[i].mood.recordWin(bb)
            else if (result.net[i] < 0) bots[i].mood.recordLoss(bb, wasBadBeat = result.wentToShowdown && bb > 25)
            bots[i].mood.decay()
        }
    }

    val elapsed = (System.nanoTime() - started) / 1_000_000.0
    println("%.1f ms  (%.0f hands/sec)\n".format(elapsed, hands / (elapsed / 1000.0)))

    println("%-7s %-28s %9s %7s %7s %6s %7s".format("Player", "Profile", "bb/100", "VPIP%", "PFR%", "AF", "Won%"))
    println("-".repeat(78))
    for (s in stats.sortedByDescending { it.bbPer100() }) {
        println(
            "%-7s %-28s %9.2f %7.1f %7.1f %6.2f %7.1f".format(
                s.name, s.profile.description, s.bbPer100(),
                s.pct(s.vpip), s.pct(s.pfr), s.aggressionFactor(), s.pct(s.wins),
            )
        )
    }
    println("chip conservation: %d (must be 0)".format(stats.sumOf { it.net }))
    stats
}

/** Dumps the starting-hand ranking so the ordering can be eyeballed against a real chart. */
private fun printChart() {
    val rows = ArrayList<Pair<String, Double>>()
    for (hi in 14 downTo 2) for (lo in hi downTo 2) {
        if (hi == lo) {
            val h = intArrayOf(Card.of(hi, Suit.CLUBS).index, Card.of(hi, Suit.HEARTS).index)
            rows += "${Card.rankSymbol(hi)}${Card.rankSymbol(lo)}  " to PreflopChart.percentile(h)
        } else {
            val s = intArrayOf(Card.of(hi, Suit.SPADES).index, Card.of(lo, Suit.SPADES).index)
            val o = intArrayOf(Card.of(hi, Suit.SPADES).index, Card.of(lo, Suit.HEARTS).index)
            rows += "${Card.rankSymbol(hi)}${Card.rankSymbol(lo)}s " to PreflopChart.percentile(s)
            rows += "${Card.rankSymbol(hi)}${Card.rankSymbol(lo)}o " to PreflopChart.percentile(o)
        }
    }
    rows.sortBy { it.second }
    println("Top 30 starting hands:")
    rows.take(30).forEachIndexed { i, (h, v) -> print("%2d.%s%.3f   ".format(i + 1, h, v)); if ((i + 1) % 5 == 0) println() }
    println("\nBottom 10:")
    rows.takeLast(10).forEach { (h, v) -> print("$h%.3f   ".format(v)) }
    println()
}

fun main(args: Array<String>) {
    if (args.firstOrNull() == "chart") { printChart(); return }
    val hands = args.getOrNull(0)?.toIntOrNull() ?: 50_000
    val seed = args.getOrNull(1)?.toLongOrNull() ?: 20_260_724L

    // A mixed table: what a real game looks like.
    runTable(
        "Mixed table", listOf(
            BotProfile("Ada", SkillLevel.EXPERT, PlayStyle.TIGHT_AGGRESSIVE, "Ice-cold. Punishes mistakes."),
            BotProfile("Bruno", SkillLevel.ADVANCED, PlayStyle.LOOSE_AGGRESSIVE, "Relentless pressure."),
            BotProfile("Cleo", SkillLevel.INTERMEDIATE, PlayStyle.TRAPPER, "Quiet until she has you."),
            BotProfile("Dex", SkillLevel.INTERMEDIATE, PlayStyle.CALLING_STATION, "Pays to see it."),
            BotProfile("Enzo", SkillLevel.BEGINNER, PlayStyle.MANIAC, "Chaos, and certain he's winning."),
            BotProfile("Fay", SkillLevel.BEGINNER, PlayStyle.ROCK, "Waits for aces."),
        ), hands, seed
    )

    // Controlled: identical style, only skill varies. This is the experiment that
    // actually tests whether the difficulty axis produces a real skill gradient.
    val ladder = runTable(
        "Skill ladder (style held constant at Tight-Aggressive)", listOf(
            BotProfile("Expert", SkillLevel.EXPERT, PlayStyle.TIGHT_AGGRESSIVE),
            BotProfile("Advncd", SkillLevel.ADVANCED, PlayStyle.TIGHT_AGGRESSIVE),
            BotProfile("Interm", SkillLevel.INTERMEDIATE, PlayStyle.TIGHT_AGGRESSIVE),
            BotProfile("Begin", SkillLevel.BEGINNER, PlayStyle.TIGHT_AGGRESSIVE),
        ), hands, seed + 1
    )

    println("\nDifficulty gradient (must decrease monotonically):")
    var monotonic = true
    var previous = Double.MAX_VALUE
    for (level in SkillLevel.entries.reversed()) {   // strongest first
        val s = ladder.firstOrNull { it.profile.skill == level } ?: continue
        val v = s.bbPer100()
        println("  %-14s %9.2f bb/100".format(level.label, v))
        if (v > previous) monotonic = false
        previous = v
    }
    println(if (monotonic) "  -> PASS: stronger skill earns more." else "  -> FAIL: gradient inverted somewhere.")
}
