package holdem

import (
	"math"
	"sort"
	"strings"

	"github.com/chehsunliu/poker"

	"pete/internal/games/cards"
)

// The bridge to the evaluator.
//
// The engine deals Pete's own cards.Card — the same one blackjack, solitaire and
// the felt already speak — and converts at the door. Hand strength is the one
// thing in this package that is genuinely hard to get right (7-card best-of-5,
// 7,462 distinct hands), so it is not homegrown: github.com/chehsunliu/poker is
// a lookup table and it is correct.
//
// The conversion is a table built once. Doing it per evaluation would matter:
// a bot's equity estimate is a thousand seven-card evaluations, and it makes
// several of those per hand.

var (
	pokerRanks = [14]string{"", "A", "2", "3", "4", "5", "6", "7", "8", "9", "T", "J", "Q", "K"}
	pokerSuits = [4]string{"s", "h", "d", "c"} // cards.Spades, Hearts, Diamonds, Clubs

	// A var initializer, not an init(). Go builds package-level variables before
	// it runs init functions, so anything else in this package that is itself a
	// var built out of this table — equity.go's deck52 is — would otherwise be
	// built out of an empty one. It was, briefly: every card came out identical,
	// every showdown tied, and every bot believed it held exactly 50% equity.
	pokerOf = func() (t [4][14]poker.Card) {
		for s := cards.Spades; s <= cards.Clubs; s++ {
			for r := cards.Ace; r <= cards.King; r++ {
				t[s][r] = poker.NewCard(pokerRanks[r] + pokerSuits[s])
			}
		}
		return t
	}()
)

// toPoker converts one card for the evaluator.
func toPoker(c cards.Card) poker.Card { return pokerOf[c.Suit][c.Rank] }

func toPokerAll(cs []cards.Card) []poker.Card {
	out := make([]poker.Card, len(cs))
	for i, c := range cs {
		out[i] = toPoker(c)
	}
	return out
}

// rankOf evaluates a seat's best five from its hole cards and the board. Lower
// is better — 1 is a royal flush — which is the evaluator's convention and not
// worth inverting, since nothing outside this file ever sees the number.
func rankOf(hole [2]cards.Card, board []cards.Card) (int32, string) {
	seven := make([]poker.Card, 0, 7)
	seven = append(seven, toPoker(hole[0]), toPoker(hole[1]))
	seven = append(seven, toPokerAll(board)...)
	r := poker.Evaluate(seven)
	return r, strings.ToLower(poker.RankString(r))
}

// ---- showdown -------------------------------------------------------------

type ranked struct {
	seat int
	rank int32
	desc string
}

// showdown turns the cards over, splits the pots, and pays. Every player still
// in the hand shows, in the order the felt should turn them over: best hand
// first, so the winner is the first card face the player sees.
func (s *State) showdown(evs *[]Event) {
	s.collect()
	s.Street = Showdown

	// Cut the side pots, if nobody has cut them yet.
	//
	// runout() does this, but runout only happens when the betting stops because
	// there is nobody left able to bet. A hand can reach a showdown with an all-in
	// player in it and the betting having finished perfectly normally: a short stack
	// shoves, and two players who both have chips behind keep betting past them,
	// street after street, all the way to the river. Nothing has been cut, and the
	// short stack is sitting in a single pot marked eligible for all of it.
	//
	// Which means they can win every chip the deep players put in *after* they were
	// already all-in — money they could never have lost. All-in for 100 against two
	// players who each put in 500, and the best hand takes 1,100 instead of the 300
	// they were playing for. The chips still balance, so conservation says nothing;
	// they just go to the wrong seat.
	if len(s.Side) == 0 && s.anyAllIn() {
		s.sidePots()
	}

	// Say so. The last street's bets are still sitting in front of the seats that
	// made them, as far as the felt knows, and nothing else in the script is going
	// to tell it they have been swept in.
	*evs = append(*evs, Event{Kind: "pot", Seat: -1, Amount: s.Total()})

	var live []ranked
	for i := range s.Seats {
		p := &s.Seats[i]
		if p.State == Folded || p.State == Out {
			continue
		}
		r, desc := rankOf(p.Hole, s.Community)
		live = append(live, ranked{seat: i, rank: r, desc: desc})
	}
	sort.Slice(live, func(i, j int) bool { return live[i].rank < live[j].rank })

	for _, e := range live {
		*evs = append(*evs, Event{
			Kind: "show", Seat: e.seat,
			Cards: []cards.Card{s.Seats[e.seat].Hole[0], s.Seats[e.seat].Hole[1]},
			Text:  e.desc,
		})
	}

	pots := s.Side
	if len(pots) == 0 {
		all := make([]int, 0, len(live))
		for _, e := range live {
			all = append(all, e.seat)
		}
		pots = []Pot{{Amount: s.Pot, Eligible: all}}
		s.Pot = 0
	}
	s.Side = nil

	for _, pot := range pots {
		s.payPot(pot, live, evs)
	}
	s.endHand(evs)
}

// payPot rakes a pot and splits it between the best eligible hands.
//
// The rake comes out of the pot before it is split, which is what a cardroom
// does and is also the only thing consistent with the rest of this casino: a
// player pays it out of a pot they *win*, never out of a bet they lose. A hand
// that dies before the flop is not raked at all — no flop, no drop — so folding
// your blind round after round costs you exactly the blinds and no fee.
func (s *State) payPot(pot Pot, live []ranked, evs *[]Event) {
	if pot.Amount <= 0 {
		return
	}

	eligible := make(map[int]bool, len(pot.Eligible))
	for _, seat := range pot.Eligible {
		eligible[seat] = true
	}

	var winners []ranked
	best := int32(0)
	for _, e := range live {
		if !eligible[e.seat] {
			continue
		}
		if len(winners) == 0 || e.rank < best {
			best, winners = e.rank, []ranked{e}
		} else if e.rank == best {
			winners = append(winners, e)
		}
	}
	if len(winners) == 0 {
		return
	}

	amount := pot.Amount
	if s.Flopped {
		rake := int64(math.Floor(float64(amount) * s.Tier.RakePct))
		if cap := s.Tier.BB * rakeCapBB; rake > cap {
			rake = cap
		}
		if rake > 0 {
			amount -= rake
			s.Rake += rake // every chip of it, so the table still balances

			// But only the part that came out of a *human's* winnings is money the
			// house actually made, and it is the only part worth quoting. The bots'
			// chips are not real — the only real money at the table is the players' —
			// so raking a pot a bot won costs nobody anything, and a counter that
			// climbed while every human folded would be telling them it had.
			for _, w := range winners {
				if !s.Seats[w.seat].Bot {
					// The table total (for the audit's delta) and the winner's own
					// running tally (for the ledger line the felt shows them). At a
					// table with two humans these are different numbers: each player is
					// only ever quoted the rake that came out of a pot they won.
					s.Paid += rake / int64(len(winners))
					s.Seats[w.seat].Paid += rake / int64(len(winners))
				}
			}
			*evs = append(*evs, Event{Kind: "rake", Seat: -1, Amount: rake})
		}
	}

	share := amount / int64(len(winners))
	odd := amount % int64(len(winners)) // the odd chip goes to the first seat left of the button
	for i, w := range winners {
		won := share
		if i == 0 {
			won += odd
		}
		s.Seats[w.seat].Stack += won
		s.Seats[w.seat].Won += won
		*evs = append(*evs, Event{Kind: "win", Seat: w.seat, Amount: won, Text: w.desc})
	}
}

// takeit ends a hand nobody contested: everyone else folded, so the last player
// standing takes the pot without showing. Their own uncalled bet comes back
// first — it was never called, so it was never really in the pot.
func (s *State) takeit(evs *[]Event) {
	s.uncalled(evs)
	s.collect()
	*evs = append(*evs, Event{Kind: "pot", Seat: -1, Amount: s.Total()})

	winner := -1
	for i := range s.Seats {
		if s.Seats[i].State != Folded && s.Seats[i].State != Out {
			winner = i
			break
		}
	}
	if winner < 0 {
		s.endHand(evs)
		return
	}

	// There are never side pots here: they are only cut once the betting is over
	// because everybody is all-in, and a table where everybody is all-in is a table
	// where nobody is left to fold.
	pot := Pot{Amount: s.Pot, Eligible: []int{winner}}
	s.Pot = 0
	s.payPot(pot, []ranked{{seat: winner, rank: 0}}, evs)
	s.endHand(evs)
}

// uncalled returns the unmatched top of a bet. If you shove 500 into a player
// with 200 behind, 300 of that was never contested and comes straight back.
//
// It must run *before* the bets are swept into the pot, and the matched level it
// measures against counts the players who folded. Their chips are in the pot —
// they paid to see the bet and then gave up — so the money they put in is money
// that called. Miss that and a bet folded to on the river comes back whole,
// including the part that was called on the flop, which mints chips out of air.
//
// The rake is the other reason this matters at all. When everybody folds, the
// winner takes the pot back either way and the arithmetic looks the same — but a
// pot with an uncalled bet still in it is a pot the house rakes, and it would be
// raking the player on their own money that nobody ever contested.
func (s *State) uncalled(evs *[]Event) {
	top, topSeat := int64(-1), -1
	for i := range s.Seats {
		p := &s.Seats[i]
		if p.State == Folded || p.State == Out {
			continue
		}
		if p.Total > top {
			top, topSeat = p.Total, i
		}
	}
	if topSeat < 0 {
		return
	}

	var matched int64 // the most anybody else put in, whether or not they're still in
	for i := range s.Seats {
		if i == topSeat || s.Seats[i].State == Out {
			continue
		}
		if s.Seats[i].Total > matched {
			matched = s.Seats[i].Total
		}
	}

	excess := top - matched
	p := &s.Seats[topSeat]
	if excess <= 0 || excess > p.Bet {
		// An uncalled bet is always part of the street it was made on, so it cannot
		// be bigger than what that seat has in front of them right now.
		return
	}

	p.Stack += excess
	p.Total -= excess
	p.Bet -= excess
	if p.State == AllIn && p.Stack > 0 {
		p.State = Active // they were never really all-in against anybody
	}
	*evs = append(*evs, Event{Kind: "uncalled", Seat: topSeat, Amount: excess})
}
