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.DecisionContext
import com.jsjdesigns.poker.game.PlayerAgent
import com.jsjdesigns.poker.game.Street
import kotlin.math.roundToInt
import kotlin.random.Random

/**
 * The numbers behind one decision.
 *
 * Kept deliberately explicit because this is exactly what the coach hands to the
 * LLM. The model narrates these values; it never computes them.
 */
data class DecisionTrace(
    val equity: Double,
    val breakEvenEquity: Double,
    val potOdds: String,
    val chosen: Action,
    val reason: String,
)

/**
 * A bot that decides from equity and pot odds, then distorts that decision through
 * its [SkillLevel] and [PlayStyle].
 *
 * Everything here is deterministic given the seed, runs in well under a
 * millisecond, and needs no network — the LLM layer sits *on top* of this, never
 * inside it.
 */
class MathBot(
    val profile: BotProfile,
    private val random: Random = Random.Default,
) : PlayerAgent {

    val mood = BotMood(profile.style.tiltSusceptibility)
    val reads = OpponentModel()

    var lastTrace: DecisionTrace? = null
        private set

    override suspend fun act(ctx: DecisionContext): Action {
        val skill = profile.skill
        val style = profile.style
        val opponents = ctx.activeOpponents.coerceAtLeast(1)

        if (skill.readsOpponents) reads.observe(ctx.history, ctx.handNumber)
        if (ctx.street == Street.PREFLOP) return actPreflop(ctx)

        // Weaker players run fewer rollouts, so they genuinely misjudge their hand
        // rather than playing well and then blundering at random.
        val equity = Equity.estimate(
            hole = ctx.hole,
            board = ctx.board,
            opponents = opponents,
            iterations = skill.equityIterations,
            random = random,
        )

        val looseness = (style.looseness + mood.loosenessBonus()).coerceIn(0.0, 1.0)
        val aggression = (style.aggression + mood.aggressionBonus()).coerceIn(0.0, 1.0)

        val breakEven = Equity.potOdds(ctx.pot, ctx.toCall)

        // Discipline: experts use the true break-even point; weak players drift
        // toward calling regardless of price.
        val discipline = skill.potOddsRespect
        var bar = breakEven * discipline + breakEven * (1 - discipline) * 0.45
        bar *= (1.0 - looseness * 0.35)

        // Position is worth real equity, and better players know it.
        bar *= if (ctx.inPosition) 1.0 - 0.12 * skill.positionAwareness
        else 1.0 + 0.10 * skill.positionAwareness

        // Exploitation: a habitual bettor's bet means less, so call wider against
        // them; a passive player's bet means strength, so fold more.
        if (skill.readsOpponents && ctx.toCall > 0) {
            val bettor = ctx.history.lastOrNull {
                it.action.type == ActionType.BET || it.action.type == ActionType.RAISE
            }?.seat
            if (bettor != null && bettor != ctx.seat.index && reads.actionsObserved(bettor) >= 25) {
                bar *= (1.0 - (reads.aggressionRate(bettor) - 0.5) * 0.50).coerceIn(0.6, 1.4)
            }
        }

        val raiseBar = (0.62 - aggression * 0.22).coerceIn(0.30, 0.75)

        var decision = decide(ctx, equity, bar, raiseBar, aggression, style, opponents)

        // Outright mistakes, on top of misjudgement.
        if (random.nextDouble() < skill.errorRate) {
            decision = blunder(ctx, decision)
        }

        lastTrace = DecisionTrace(
            equity = equity,
            breakEvenEquity = breakEven,
            potOdds = if (ctx.toCall > 0) "${ctx.pot}:${ctx.toCall}" else "no bet to call",
            chosen = decision,
            reason = traceReason(equity, breakEven, ctx),
        )
        return decision
    }

    /**
     * Pre-flop is range-based rather than equity-based: real players think "I open
     * the top N%", so [PlayStyle.looseness] sets N directly and a Rock at 0.12
     * genuinely plays 12% of hands.
     */
    private fun actPreflop(ctx: DecisionContext): Action {
        val skill = profile.skill
        val style = profile.style
        val pct = PreflopChart.percentile(ctx.hole)

        val looseness = (style.looseness + mood.loosenessBonus()).coerceIn(0.02, 1.0)
        val aggression = (style.aggression + mood.aggressionBonus()).coerceIn(0.0, 1.0)

        // Undisciplined players simply play too many hands. This is the skill axis
        // acting on range width, kept separate from the style axis above.
        val sloppiness = 1.0 + (1.0 - skill.potOddsRespect) * 0.70
        val positional = if (ctx.inPosition) 1.0 + 0.45 * skill.positionAwareness
        else 1.0 - 0.25 * skill.positionAwareness

        val facingRaise = ctx.toCall > ctx.bigBlind
        var gate = looseness * sloppiness * positional
        if (facingRaise) gate *= 0.45
        gate = gate.coerceIn(0.01, 1.0)

        val raiseGate = gate * (0.30 + aggression * 0.45)

        val action = when {
            pct <= raiseGate && ctx.canRaise ->
                Action(ActionType.RAISE, preflopRaiseTo(ctx, facingRaise))
            pct <= gate ->
                if (ctx.canCheck) Action(ActionType.CHECK) else Action(ActionType.CALL, ctx.toCall)
            else ->
                if (ctx.canCheck) Action(ActionType.CHECK) else Action(ActionType.FOLD)
        }

        val final = if (random.nextDouble() < skill.errorRate) blunder(ctx, action) else action
        lastTrace = DecisionTrace(
            equity = 1.0 - pct,
            breakEvenEquity = Equity.potOdds(ctx.pot, ctx.toCall),
            potOdds = if (ctx.toCall > 0) "${ctx.pot}:${ctx.toCall}" else "no bet to call",
            chosen = final,
            reason = "Starting hand is in the top ${(pct * 100).roundToInt()}% " +
                "and this profile plays about the top ${(gate * 100).roundToInt()}%.",
        )
        return final
    }

    private fun preflopRaiseTo(ctx: DecisionContext, facingRaise: Boolean): Int {
        if (ctx.maxRaiseTo <= ctx.minRaiseTo) return ctx.maxRaiseTo
        val desired = if (facingRaise) ctx.minRaiseTo + (ctx.pot * 0.40).roundToInt()
        else ctx.bigBlind * 3
        return desired.coerceIn(ctx.minRaiseTo, ctx.maxRaiseTo)
    }

    private fun decide(
        ctx: DecisionContext,
        equity: Double,
        bar: Double,
        raiseBar: Double,
        aggression: Double,
        style: PlayStyle,
        opponents: Int,
    ): Action {
        val strong = equity >= raiseBar
        val monster = equity >= 0.82

        // Sandbagging: under-represent a monster to keep them in.
        if (monster && random.nextDouble() < style.slowplayFrequency && ctx.street != Street.RIVER) {
            return if (ctx.canCheck) Action(ActionType.CHECK) else Action(ActionType.CALL, ctx.toCall)
        }

        if (ctx.canCheck) {
            // Continuation bet: having taken the lead pre-flop, fire again on the
            // flop regardless of what it brought.
            val tookPreflopLead = ctx.history.lastOrNull {
                it.street == Street.PREFLOP &&
                    (it.action.type == ActionType.BET || it.action.type == ActionType.RAISE)
            }?.seat == ctx.seat.index
            if (tookPreflopLead && ctx.street == Street.FLOP &&
                random.nextDouble() < style.contBetFrequency
            ) {
                return Action(ActionType.BET, sizeBet(ctx, equity, aggression))
            }

            // No bet to face: either take the lead or check behind.
            val bluffing = equity < 0.35 &&
                random.nextDouble() < style.bluffFrequency / opponents.coerceAtLeast(1)
            if (strong || bluffing) {
                if (random.nextDouble() < aggression || bluffing) {
                    return Action(ActionType.BET, sizeBet(ctx, equity, aggression))
                }
            }
            return Action(ActionType.CHECK)
        }

        // Facing a bet.
        if (equity < bar) {
            // Bluff-raising with nothing, occasionally, when heads-up.
            if (opponents == 1 && equity > 0.18 &&
                random.nextDouble() < style.bluffFrequency * 0.5 && ctx.canRaise
            ) {
                return Action(ActionType.RAISE, sizeBet(ctx, equity, aggression))
            }
            return Action(ActionType.FOLD)
        }

        if (strong && ctx.canRaise && random.nextDouble() < aggression) {
            return Action(ActionType.RAISE, sizeBet(ctx, equity, aggression))
        }

        return Action(ActionType.CALL, ctx.toCall)
    }

    /** Pot-fraction sizing, widening with equity and aggression. */
    private fun sizeBet(ctx: DecisionContext, equity: Double, aggression: Double): Int {
        val fraction = when {
            equity > 0.85 -> 0.75 + aggression * 0.45
            equity > 0.65 -> 0.55 + aggression * 0.30
            equity > 0.45 -> 0.45 + aggression * 0.20
            else -> 0.40 + aggression * 0.25 // bluff sizing
        }
        val target = ctx.committedThisRoundPlus(((ctx.pot * fraction).roundToInt()))
        // A short stack that cannot afford a full min-raise may still shove; that
        // is legal, it simply does not reopen the betting.
        if (ctx.maxRaiseTo <= ctx.minRaiseTo) return ctx.maxRaiseTo
        return target.coerceIn(ctx.minRaiseTo, ctx.maxRaiseTo)
    }

    private fun blunder(ctx: DecisionContext, intended: Action): Action = when (intended.type) {
        ActionType.FOLD -> if (ctx.toCall > 0) Action(ActionType.CALL, ctx.toCall) else Action(ActionType.CHECK)
        ActionType.CHECK -> Action(ActionType.CHECK)
        ActionType.CALL -> if (random.nextBoolean() && ctx.toCall > 0) Action(ActionType.FOLD) else intended
        ActionType.BET, ActionType.RAISE -> if (ctx.canCheck) Action(ActionType.CHECK) else Action(ActionType.CALL, ctx.toCall)
    }

    private fun traceReason(equity: Double, breakEven: Double, ctx: DecisionContext): String {
        val pct = (equity * 100).roundToInt()
        return if (ctx.toCall > 0) {
            val need = (breakEven * 100).roundToInt()
            "About $pct% equity against ${ctx.activeOpponents} opponent(s); needed $need% to call profitably."
        } else {
            "About $pct% equity against ${ctx.activeOpponents} opponent(s), no bet to face."
        }
    }
}

/** Converts a pot-fraction bet into a raise-to figure. */
private fun DecisionContext.committedThisRoundPlus(extra: Int): Int =
    seat.committedThisRound + toCall + extra
