package com.jsjdesigns.poker.bot

import com.jsjdesigns.poker.core.Equity
import com.jsjdesigns.poker.core.PreflopChart
import com.jsjdesigns.poker.game.Action
import com.jsjdesigns.poker.game.ActionType
import com.jsjdesigns.poker.game.DecisionOffer
import com.jsjdesigns.poker.game.Street
import kotlin.math.roundToInt
import kotlin.random.Random

/**
 * Offline review of one accepted human decision.
 *
 * This is a transparent fundamentals baseline, not a solver and not a disguised
 * bot persona. It receives only [DecisionOffer] — the immutable information the
 * human was allowed to know — so opponent hole cards cannot leak into advice.
 */
data class CoachingReview(
    val decisionToken: Long,
    val handNumber: Int,
    val street: Street,
    val trace: DecisionTrace,
    /** Same strategic action family; bet sizing differences are not condemned. */
    val alignedWithBaseline: Boolean,
)

class DecisionCoach(
    private val equityIterations: Int = 3_000,
) {
    init {
        require(equityIterations > 0) { "equity iterations must be positive" }
    }

    fun review(offer: DecisionOffer, chosen: Action): CoachingReview {
        val trace = if (offer.street == Street.PREFLOP) {
            reviewPreflop(offer, chosen)
        } else {
            reviewPostflop(offer, chosen)
        }
        return CoachingReview(
            decisionToken = offer.token,
            handNumber = offer.handNumber,
            street = offer.street,
            trace = trace,
            alignedWithBaseline = sameActionFamily(trace.intended, chosen),
        )
    }

    private fun reviewPreflop(offer: DecisionOffer, chosen: Action): DecisionTrace {
        val percentile = PreflopChart.percentile(offer.hole.toIntArray())
        val positionFactor = balancedPositionFactor(
            activeOpponents = offer.activeOpponents,
            inPosition = offer.inPosition,
            awareness = 1.0,
            inPositionDelta = 0.45,
        )
        // What this seat still owes is not enough to identify a raise: the big
        // blind can face a min-raise while owing only one big blind.
        val facingRaise = offer.currentBet > offer.bigBlind
        val facingRaiseFactor = if (facingRaise) 0.45 else 1.0
        val range = (BASELINE_PREFLOP_RANGE * positionFactor * facingRaiseFactor)
            .coerceIn(0.01, 1.0)
        val raiseRange = range * BASELINE_RAISE_SHARE

        val intended = when {
            percentile <= raiseRange && offer.canRaise ->
                aggressiveAction(offer, preflop = true, facingRaise = facingRaise)
            percentile <= range ->
                if (offer.canCheck) Action(ActionType.CHECK)
                else Action(ActionType.CALL, offer.callAmount)
            else ->
                if (offer.canCheck) Action(ActionType.CHECK) else Action(ActionType.FOLD)
        }

        return DecisionTrace(
            estimatedEquity = null,
            handStrengthPercentile = percentile,
            breakEvenEquity = null,
            decisionThreshold = null,
            preflopRangeThreshold = range,
            potOdds = null,
            intended = intended,
            chosen = chosen,
            // A human deviation is not a bot skill-error transform.
            mistakeApplied = false,
            adjustments = listOf(
                ThresholdAdjustment("position", positionFactor),
                ThresholdAdjustment("facing raise", facingRaiseFactor),
            ),
            reason = buildString {
                append("Starting hand is in the top ${(percentile * 100).roundToInt()}%; ")
                append("the fundamentals baseline range here is ${(range * 100).roundToInt()}%.")
            },
        )
    }

    private fun reviewPostflop(offer: DecisionOffer, chosen: Action): DecisionTrace {
        val equity = Equity.estimate(
            hole = offer.hole.toIntArray(),
            board = offer.board.toIntArray(),
            opponents = offer.activeOpponents.coerceAtLeast(1),
            iterations = equityIterations,
            random = Random(seedFor(offer)),
        )
        val breakEven = Equity.potOdds(offer.eligiblePot, offer.callAmount)
        val intended = when {
            offer.canCheck && equity >= VALUE_BET_EQUITY && offer.canRaise ->
                aggressiveAction(offer, preflop = false, facingRaise = false)
            offer.canCheck ->
                Action(ActionType.CHECK)
            equity < breakEven ->
                Action(ActionType.FOLD)
            equity >= VALUE_RAISE_EQUITY && offer.canRaise ->
                aggressiveAction(offer, preflop = false, facingRaise = true)
            else ->
                Action(ActionType.CALL, offer.callAmount)
        }

        return DecisionTrace(
            estimatedEquity = equity,
            handStrengthPercentile = null,
            breakEvenEquity = breakEven,
            decisionThreshold = breakEven,
            preflopRangeThreshold = null,
            potOdds = if (offer.toCall > 0) {
                "${offer.eligiblePot}:${offer.callAmount}"
            } else {
                "no bet to call"
            },
            intended = intended,
            chosen = chosen,
            mistakeApplied = false,
            adjustments = emptyList(),
            reason = buildString {
                append("About ${(equity * 100).roundToInt()}% equity against ")
                append("${offer.activeOpponents} unknown random hand(s)")
                if (offer.toCall > 0) {
                    append("; raw pot odds require ${(breakEven * 100).roundToInt()}%.")
                } else {
                    append(", with no bet to call.")
                }
            },
        )
    }

    private fun aggressiveAction(
        offer: DecisionOffer,
        preflop: Boolean,
        facingRaise: Boolean,
    ): Action {
        // A big blind may check pre-flop while still raising an existing blind.
        // Post-flop, a free aggressive action opens the betting.
        val type = if (preflop || !offer.canCheck) ActionType.RAISE else ActionType.BET
        if (offer.maxRaiseTo <= offer.minRaiseTo) {
            return Action(type, offer.maxRaiseTo)
        }
        val desired = when {
            preflop && !facingRaise -> offer.bigBlind * 3
            preflop -> offer.minRaiseTo + (offer.pot * 0.40).roundToInt()
            else -> offer.minRaiseTo + (offer.pot * 0.50).roundToInt()
        }
        return Action(type, desired.coerceIn(offer.minRaiseTo, offer.maxRaiseTo))
    }

    private fun seedFor(offer: DecisionOffer): Int {
        var seed = 17
        seed = seed * 31 + offer.handNumber
        seed = seed * 31 + offer.street.ordinal
        seed = seed * 31 + offer.activeOpponents
        for (card in offer.hole) seed = seed * 31 + card
        for (card in offer.board) seed = seed * 31 + card
        return seed
    }

    private fun sameActionFamily(recommended: Action, chosen: Action): Boolean {
        val recommendedAggressive =
            recommended.type == ActionType.BET || recommended.type == ActionType.RAISE
        val chosenAggressive =
            chosen.type == ActionType.BET || chosen.type == ActionType.RAISE
        return if (recommendedAggressive || chosenAggressive) {
            recommendedAggressive && chosenAggressive
        } else {
            recommended.type == chosen.type
        }
    }

    private companion object {
        const val BASELINE_PREFLOP_RANGE = 0.22
        const val BASELINE_RAISE_SHARE = 0.59
        const val VALUE_BET_EQUITY = 0.62
        const val VALUE_RAISE_EQUITY = 0.68
    }
}
