package com.jsjdesigns.poker

import com.jsjdesigns.poker.game.DecisionOffer

/**
 * What the action bar should show for a decision.
 *
 * Kept as a pure function of the offer so it can be unit-tested without a Compose
 * runtime. The UI only renders this; it makes no decisions of its own.
 */
data class ActionButtons(
    val showFold: Boolean,
    val checkOrCallLabel: String,
    /** What calling actually costs: the engine clamps a call to the stack. */
    val effectiveCall: Int,
    val showRaise: Boolean,
    /** False when there is exactly one legal raise size, so a slider is pointless. */
    val showSlider: Boolean,
    val sliderMin: Int,
    val sliderMax: Int,
    /** The amount to submit when the slider is hidden. */
    val fixedRaiseTo: Int,
)

data class RaisePreset(
    val label: String,
    val amount: Int,
)

fun buttonsFor(offer: DecisionOffer): ActionButtons {
    // When a stack cannot cover a full min-raise it may still shove; the engine
    // clamps such a raise to maxRaiseTo. Requiring maxRaiseTo > minRaiseTo to show
    // the button hid both that short all-in AND an exact-minimum raise, even
    // though the engine accepts both.
    val shoveOnly = offer.maxRaiseTo <= offer.minRaiseTo
    val low = if (shoveOnly) offer.maxRaiseTo else offer.minRaiseTo
    val high = offer.maxRaiseTo

    // Sourced from the offer, not recomputed: the label, the submitted action and
    // the engine must all agree on one number.
    val effectiveCall = offer.callAmount

    return ActionButtons(
        // Folding a free hand is never correct, so don't invite an accidental muck.
        showFold = !offer.canCheck,
        checkOrCallLabel = when {
            offer.canCheck -> "Check"
            offer.callIsAllIn -> "All in $effectiveCall"
            else -> "Call $effectiveCall"
        },
        effectiveCall = effectiveCall,
        showRaise = offer.canRaise,
        showSlider = high > low,
        sliderMin = low,
        sliderMax = high,
        fixedRaiseTo = low,
    )
}

/** Label for the raise button at [amount]. */
fun raiseLabel(buttons: ActionButtons, amount: Int): String =
    if (amount >= buttons.sliderMax) "All in" else "Raise $amount"

/** One-chip fine tuning for the sizing rail, clamped to the engine's legal range. */
fun adjustRaiseAmount(
    current: Int,
    change: Int,
    buttons: ActionButtons,
): Int = (current + change).coerceIn(buttons.sliderMin, buttons.sliderMax)

/** The exact amount submitted by the confirmation screen. */
fun raiseConfirmationAmount(
    buttons: ActionButtons,
    selected: Int,
): Int = if (buttons.showSlider) {
    selected.coerceIn(buttons.sliderMin, buttons.sliderMax)
} else {
    buttons.fixedRaiseTo
}

/**
 * Honest, legal raise-to shortcuts for the raise-sizing screen.
 *
 * Facing a bet, a fraction describes the extra raise after calling: the pot
 * after a call is [DecisionOffer.pot] plus the affordable call, and the final
 * round commitment is the current bet plus that fraction. Every result is
 * clamped to the exact engine-provided range; duplicates disappear rather than
 * showing several labels that submit the same action.
 */
fun raisePresets(
    offer: DecisionOffer,
    buttons: ActionButtons = buttonsFor(offer),
): List<RaisePreset> {
    if (!buttons.showRaise) return emptyList()
    val low = buttons.sliderMin
    val high = buttons.sliderMax
    if (low >= high) return listOf(RaisePreset("All in", high))

    val potAfterCall = offer.pot + offer.callAmount
    val halfPot = (offer.currentBet + potAfterCall / 2).coerceIn(low, high)
    val fullPot = (offer.currentBet + potAfterCall).coerceIn(low, high)

    return buildList {
        add(RaisePreset("Min", low))
        if (halfPot in (low + 1) until high) add(RaisePreset("½ pot", halfPot))
        if (fullPot in (low + 1) until high && fullPot != halfPot) {
            add(RaisePreset("Pot", fullPot))
        }
        add(RaisePreset("All in", high))
    }
}
