package holdem

import "sort"

// The betting rules. These are the fiddly ones — min-raise, short all-ins that
// don't reopen the action, side pots — and they came over from gogobee, where
// they had no tests at all. They have some now.

// blinds posts the small and the big. Heads-up is the exception every poker
// implementation gets wrong once: with two players the button *is* the small
// blind and acts first before the flop, and last after it.
func (s *State) blinds(evs *[]Event) (bb int) {
	var sb int
	if s.dealt() == 2 {
		sb, bb = s.Button, s.nextIn(s.Button)
	} else {
		sb = s.nextIn(s.Button)
		bb = s.nextIn(sb)
	}

	s.post(sb, s.Tier.SB, "small", evs)
	s.post(bb, s.Tier.BB, "big", evs)

	s.Bet = s.Tier.BB
	s.MinRaise = s.Tier.BB
	s.Aggressor = bb // the big blind has the option to raise their own blind
	return bb
}

// post puts a blind up. A player too short to cover it is all-in for what they
// have, which is legal and is why the amount is clamped rather than refused.
func (s *State) post(seat int, amount int64, which string, evs *[]Event) {
	p := &s.Seats[seat]
	if amount > p.Stack {
		amount = p.Stack
	}
	p.Stack -= amount
	p.Bet = amount
	p.Total = amount
	if p.Stack == 0 {
		p.State = AllIn
	}
	*evs = append(*evs, Event{Kind: "blind", Seat: seat, Amount: amount, Text: which})
}

// firstPreFlop is under the gun: the seat after the big blind, or the button
// itself when the table is heads-up.
//
// The button only gets it if the button can still act. A short stack can be
// all-in on its own blind — post a small blind of 1 with 1 chip left and you are
// in the hand with no chips and no say — and handing the action to a seat that
// cannot act wedges the table.
func (s *State) firstPreFlop(bb int) int {
	if s.dealt() != 2 {
		return s.nextCanAct(bb)
	}
	if s.Seats[s.Button].State == Active {
		return s.Button
	}
	return s.nextCanAct(s.Button)
}

// firstPostFlop is the first seat left of the button, on every street after the
// flop. The button acts last from here on, which is the whole point of it.
func (s *State) firstPostFlop() int { return s.nextCanAct(s.Button) }

// ---- the five things a seat can do ----------------------------------------

func (s *State) fold(seat int, evs *[]Event) {
	p := &s.Seats[seat]
	p.State = Folded
	p.Acted = true
	s.History += "f"
	*evs = append(*evs, Event{Kind: "action", Seat: seat, Text: "fold"})
}

func (s *State) check(seat int, evs *[]Event) error {
	p := &s.Seats[seat]
	if p.Bet < s.Bet {
		return ErrCantCheck
	}
	p.Acted = true
	s.History += "c"
	*evs = append(*evs, Event{Kind: "action", Seat: seat, Text: "check"})
	return nil
}

func (s *State) call(seat int, evs *[]Event) error {
	p := &s.Seats[seat]
	owed := s.Bet - p.Bet
	if owed <= 0 {
		return ErrNothingToCall
	}
	if owed > p.Stack {
		owed = p.Stack // a call for less than the bet is a call all-in
	}

	p.Stack -= owed
	p.Bet += owed
	p.Total += owed
	p.Acted = true

	text := "call"
	if p.Stack == 0 {
		p.State = AllIn
		text = "allin"
		s.History += "a"
	} else {
		s.History += "c"
	}
	*evs = append(*evs, Event{Kind: "action", Seat: seat, Text: text, Amount: owed, Total: p.Bet})
	return nil
}

// raise raises *to* a total, not *by* an amount. Every poker interface in the
// world means the total, and a browser that means the other thing bets wrong.
func (s *State) raise(seat int, to int64, evs *[]Event) error {
	p := &s.Seats[seat]
	most := p.Bet + p.Stack

	if to > most {
		return ErrTooBig
	}
	if to < s.Bet+s.MinRaise && to < most {
		return ErrTooSmall // only a shove may be smaller than a legal raise
	}

	added := to - p.Bet
	over := to - s.Bet

	p.Stack -= added
	p.Bet = to
	p.Total += added
	p.Acted = true

	if over > 0 {
		s.MinRaise = over
	}
	s.Bet = to
	s.Aggressor = seat

	text := "raise"
	if p.Stack == 0 {
		p.State = AllIn
		text = "allin"
		s.History += "a"
	} else {
		// The policy was trained against a tree with two raise sizes in it, so the
		// history it reads has to say which one this was: R for a pot-sized raise
		// or bigger, r for anything smaller.
		if pot := s.inPlay(); pot > 0 && float64(over) >= float64(pot)*0.75 {
			s.History += "R"
		} else {
			s.History += "r"
		}
	}
	*evs = append(*evs, Event{Kind: "action", Seat: seat, Text: text, Amount: added, Total: to})
	return nil
}

// allin pushes the lot.
//
// A short all-in does not reopen the betting. If a player shoves for less than
// a full raise over the current bet, players who have already acted may call it
// but may not raise again — otherwise a tiny stack could be used to reopen the
// action for a partner, which is the oldest collusion trick there is.
func (s *State) allin(seat int, evs *[]Event) error {
	p := &s.Seats[seat]
	if p.Stack <= 0 {
		return ErrNoChips
	}
	added := p.Stack
	to := p.Bet + added

	p.Stack = 0
	p.Bet = to
	p.Total += added
	p.State = AllIn
	p.Acted = true

	if to > s.Bet {
		if over := to - s.Bet; over >= s.MinRaise {
			s.MinRaise = over
			s.Aggressor = seat
		}
		s.Bet = to
	}

	s.History += "a"
	*evs = append(*evs, Event{Kind: "action", Seat: seat, Text: "allin", Amount: added, Total: to})
	return nil
}

// ---- when is a street over ------------------------------------------------

// streetDone reports whether the betting round is finished, given the seat the
// action would pass to next.
//
// The "has acted" check is the load-bearing half. The big blind has money in
// front of them without having chosen to put it there, so a round where
// everybody merely limps in has all bets matched while the blind has never had
// a say. Without this, they never get their option.
func (s *State) streetDone(next int) bool {
	if s.canActCount() == 0 {
		return true
	}
	for i := range s.Seats {
		p := &s.Seats[i]
		if p.State != Active {
			continue
		}
		if p.Bet != s.Bet || !p.Acted {
			return false
		}
	}
	// The last aggressor being all-in means the action can't get back to them:
	// everyone left has matched the bet above, so there is nothing more to do.
	if s.Seats[s.Aggressor].State == AllIn {
		return true
	}
	return next == s.Aggressor
}

// ---- side pots -------------------------------------------------------------

// sidePots slices the pot into layers, one per distinct all-in level. A player
// can only win the part of the pot they could have lost, so each layer is
// contested by exactly the players who paid into it.
//
// Folded players' chips stay in the pot — they paid for the right to fold — but
// they are eligible for nothing.
func (s *State) sidePots() {
	s.collect()

	var levels []int64
	for i := range s.Seats {
		p := &s.Seats[i]
		if p.State == Folded || p.State == Out || p.Total == 0 {
			continue
		}
		levels = append(levels, p.Total)
	}
	if len(levels) == 0 {
		return
	}
	sort.Slice(levels, func(i, j int) bool { return levels[i] < levels[j] })

	var pots []Pot
	var prev int64
	for _, level := range levels {
		if level <= prev {
			continue
		}
		var amount int64
		var eligible []int
		for i := range s.Seats {
			p := &s.Seats[i]
			paid := p.Total - prev
			if paid > level-prev {
				paid = level - prev
			}
			if paid > 0 {
				amount += paid // folded money counts toward the pot...
			}
			if p.State != Folded && p.State != Out && p.Total >= level {
				eligible = append(eligible, i) // ...but wins no part of it
			}
		}
		if amount > 0 {
			pots = append(pots, Pot{Amount: amount, Eligible: eligible})
		}
		prev = level
	}

	if len(pots) > 0 {
		s.Side = pots
		s.Pot = 0
	}
}
