package com.jsjdesigns.poker.core

import kotlin.random.Random

/**
 * Strength ranking of the 169 distinct starting hands.
 *
 * Two separate problems have to be solved here, and it is worth keeping them
 * distinct:
 *
 * 1. **The threshold.** Raw all-in equity must never be compared against pot odds
 *    pre-flop. 7-2o has ~35% equity against one random hand but is unplayable,
 *    because you never realise that equity across three streets. Solved by
 *    ranking hands and gating on percentile — "I open the top 15%".
 *
 * 2. **The ordering.** All-in equity is still a flawed way to *rank* hands: it
 *    undervalues suited connectors, whose worth is in implied odds, and
 *    overvalues weak aces and small pairs, which are dominated or hard to play.
 *    So the raw equity is adjusted by an explicit playability term below.
 *
 * The adjustment is a documented heuristic in the spirit of the Chen formula, not
 * solver output. It is a reasonable starting ordering to be tuned against the
 * simulator, not a claim of correctness.
 *
 * Computed once on first use (~50ms) and cached.
 */
object PreflopChart {

    /**
     * Indexed by [key]. Only 169 of the [TABLE_SIZE] slots are real hand classes;
     * the rest stay at 1.0 as an unreachable sentinel.
     */
    private val percentiles: DoubleArray by lazy { build() }

    /** Canonical slot for a starting hand: pairs, then suited, then offsuit. */
    fun key(hole: IntArray): Int {
        val r1 = 2 + hole[0] / 4
        val r2 = 2 + hole[1] / 4
        val suited = hole[0] % 4 == hole[1] % 4
        val hi = maxOf(r1, r2) - 2
        val lo = minOf(r1, r2) - 2
        return when {
            hi == lo -> hi                       // 0..12 pairs
            suited -> 13 + hi * 13 + lo          // suited
            else -> 13 + 169 + hi * 13 + lo      // offsuit
        }
    }

    private const val TABLE_SIZE = 13 + 169 + 169

    /**
     * The share of dealt hands ranked strictly stronger than this one, so a
     * player who "plays the top 20%" enters when this is `<= 0.20`.
     *
     * This is the **start** of the class's band, not its end: aces are 0.0, and
     * the worst class (72o) begins at `(1326 - 12) / 1326` ≈ 0.991 because its own
     * 12 combinations occupy the remainder. The value therefore never reaches 1.0
     * for a real hand — which is correct for gating, since a class is admitted
     * when its band starts inside the range.
     */
    fun percentile(hole: IntArray): Double = percentiles[key(hole)]

    /**
     * Playability adjustment applied on top of all-in equity, in equity points.
     *
     * Captures what raw equity cannot: implied odds for hands that make disguised
     * straights and flushes, and reverse implied odds for hands that are usually
     * dominated when they connect.
     */
    private fun playability(hi: Int, lo: Int, suited: Boolean, pair: Boolean): Double {
        var adj = 0.0

        if (pair) {
            // Small pairs have big all-in equity but need to flop a set to continue.
            if (hi <= 6) adj -= 0.030
            else if (hi <= 9) adj -= 0.012
            return adj
        }

        // Flush potential is worth real money postflop.
        if (suited) adj += 0.035

        // Connectedness: straight potential falls away fast as the gap widens.
        val gap = hi - lo - 1
        adj += when (gap) {
            0 -> 0.022
            1 -> 0.012
            2 -> 0.004
            else -> -0.004 * gap
        }

        // Two broadway cards dominate rather than being dominated.
        if (lo >= 10) adj += 0.020

        // Weak aces flop top pair with a hopeless kicker.
        if (hi == 14 && lo <= 9) adj -= if (suited) 0.018 else 0.034

        // Weak kings have the same problem, less severely.
        if (hi == 13 && lo <= 8) adj -= if (suited) 0.010 else 0.022

        return adj
    }

    private fun build(): DoubleArray {
        val random = Random(9_1_2026)
        val table = DoubleArray(TABLE_SIZE) { -1.0 }
        val entries = ArrayList<Pair<Int, Double>>(169)

        for (hi in 12 downTo 0) {
            for (lo in hi downTo 0) {
                if (hi == lo) {
                    val hole = intArrayOf(
                        Card.of(hi + 2, Suit.CLUBS).index,
                        Card.of(hi + 2, Suit.HEARTS).index,
                    )
                    val e = Equity.estimate(hole, IntArray(0), 1, 2500, random)
                    val k = key(hole)
                    val score = e + playability(hi + 2, lo + 2, suited = false, pair = true)
                    table[k] = score
                    entries.add(k to score)
                } else {
                    val suitedHole = intArrayOf(
                        Card.of(hi + 2, Suit.SPADES).index,
                        Card.of(lo + 2, Suit.SPADES).index,
                    )
                    val offHole = intArrayOf(
                        Card.of(hi + 2, Suit.SPADES).index,
                        Card.of(lo + 2, Suit.HEARTS).index,
                    )
                    for ((hole, suited) in listOf(suitedHole to true, offHole to false)) {
                        val e = Equity.estimate(hole, IntArray(0), 1, 2500, random)
                        val k = key(hole)
                        val score = e + playability(hi + 2, lo + 2, suited, pair = false)
                        table[k] = score
                        entries.add(k to score)
                    }
                }
            }
        }

        // Convert scores into a percentile weighted by how often each class is
        // actually dealt. The 169 classes are NOT equally likely — a pair is 6 of
        // the 1326 combinations, a suited hand 4, an offsuit hand 12 — so ranking
        // them evenly would make "top 12%" mean 12% of *classes* rather than 12%
        // of hands, which is what a player means by it.
        entries.sortByDescending { it.second }
        val out = DoubleArray(TABLE_SIZE) { 1.0 }
        var cumulative = 0
        for ((slot, _) in entries) {
            out[slot] = cumulative.toDouble() / TOTAL_COMBOS
            cumulative += combosForSlot(slot)
        }
        return out
    }

    /** Combinations dealt for a slot: 6 for a pair, 4 suited, 12 offsuit. */
    private fun combosForSlot(slot: Int): Int = when {
        slot < 13 -> 6
        slot < 13 + 169 -> 4
        else -> 12
    }

    private const val TOTAL_COMBOS = 1326  // C(52,2)
}
