package plugin

import (
	"fmt"
	"sort"

	"maunium.net/go/mautrix/id"
)

// postBlinds posts SB and BB, handling heads-up edge case.
func (g *HoldemGame) postBlinds() (sbIdx, bbIdx int) {
	inHand := g.inHandPlayers()
	n := len(inHand)

	if n == 2 {
		// Heads-up: dealer posts SB, other posts BB.
		sbIdx = g.DealerIdx
		bbIdx = g.nextActiveIdx(g.DealerIdx)
	} else {
		sbIdx = g.nextActiveIdx(g.DealerIdx)
		bbIdx = g.nextActiveIdx(sbIdx)
	}

	// Post small blind.
	sb := g.Players[sbIdx]
	sbAmount := g.SmallBlind
	if sbAmount > sb.Stack {
		sbAmount = sb.Stack
	}
	sb.Stack -= sbAmount
	sb.Bet = sbAmount
	sb.TotalBet = sbAmount
	if sb.Stack == 0 {
		sb.State = PlayerAllIn
	}

	// Post big blind.
	bb := g.Players[bbIdx]
	bbAmount := g.BigBlind
	if bbAmount > bb.Stack {
		bbAmount = bb.Stack
	}
	bb.Stack -= bbAmount
	bb.Bet = bbAmount
	bb.TotalBet = bbAmount
	if bb.Stack == 0 {
		bb.State = PlayerAllIn
	}

	g.CurrentBet = g.BigBlind
	g.MinRaise = g.BigBlind
	g.LastAggressorIdx = bbIdx // BB has option

	return sbIdx, bbIdx
}

// firstToActPreflop returns the seat index of the first player to act preflop.
func (g *HoldemGame) firstToActPreflop(bbIdx int) int {
	inHand := len(g.inHandPlayers())
	if inHand == 2 {
		// Heads-up: dealer/SB acts first preflop.
		return g.DealerIdx
	}
	// UTG = next active after BB.
	return g.nextCanActIdx(bbIdx)
}

// firstToActPostflop returns the first player to act on post-flop streets.
func (g *HoldemGame) firstToActPostflop() int {
	// First active player after dealer.
	return g.nextCanActIdx(g.DealerIdx)
}

// ActionResult describes what happened after an action.
type ActionResult struct {
	Announcement string
	HandOver     bool // only 1 player remains
	StreetOver   bool // street betting is complete
	AllAllIn     bool // all remaining players are all-in
}

// doFold processes a fold action.
func (g *HoldemGame) doFold(seatIdx int) ActionResult {
	p := g.Players[seatIdx]
	p.State = PlayerFolded
	p.HasActed = true
	g.StreetHistory += "f"

	ann := renderActionAnnouncement(p.DisplayName, "fold", 0)

	if g.activeCount() == 1 {
		return ActionResult{Announcement: ann, HandOver: true}
	}

	return ActionResult{
		Announcement: ann,
		StreetOver:   g.isStreetComplete(g.nextCanActIdx(seatIdx)),
		AllAllIn:     g.canActCount() == 0,
	}
}

// doCheck processes a check action. Returns error string if invalid.
func (g *HoldemGame) doCheck(seatIdx int) (ActionResult, string) {
	p := g.Players[seatIdx]
	if p.Bet < g.CurrentBet {
		return ActionResult{}, "You must call, raise, or fold — there's a bet to you."
	}

	p.HasActed = true
	g.StreetHistory += "c"
	ann := renderActionAnnouncement(p.DisplayName, "check", 0)
	return ActionResult{
		Announcement: ann,
		StreetOver:   g.isStreetComplete(g.nextCanActIdx(seatIdx)),
	}, ""
}

// doCall processes a call action. Returns error string if nothing to call.
func (g *HoldemGame) doCall(seatIdx int) (ActionResult, string) {
	p := g.Players[seatIdx]
	toCall := g.CurrentBet - p.Bet
	if toCall <= 0 {
		return ActionResult{}, "Nothing to call. Use `!holdem check` instead."
	}
	if toCall > p.Stack {
		toCall = p.Stack
	}

	p.Stack -= toCall
	p.Bet += toCall
	p.TotalBet += toCall
	p.HasActed = true

	action := "call"
	if p.Stack == 0 {
		p.State = PlayerAllIn
		action = "allin"
		g.StreetHistory += "a"
	} else {
		g.StreetHistory += "c"
	}

	ann := renderActionAnnouncement(p.DisplayName, action, toCall)

	if g.activeCount() == 1 {
		return ActionResult{Announcement: ann, HandOver: true}, ""
	}

	return ActionResult{
		Announcement: ann,
		StreetOver:   g.isStreetComplete(g.nextCanActIdx(seatIdx)),
		AllAllIn:     g.canActCount() == 0,
	}, ""
}

// doRaise processes a raise action. raiseTo is the total bet amount.
func (g *HoldemGame) doRaise(seatIdx int, raiseTo int64) (ActionResult, string) {
	p := g.Players[seatIdx]

	minRaiseTo := g.CurrentBet + g.MinRaise
	maxRaiseTo := p.Bet + p.Stack

	if raiseTo < minRaiseTo && raiseTo < maxRaiseTo {
		return ActionResult{}, fmt.Sprintf("Minimum raise is to €%d.", minRaiseTo)
	}

	if raiseTo > maxRaiseTo {
		return ActionResult{}, fmt.Sprintf("You can raise to at most €%d (your stack).", maxRaiseTo)
	}

	raiseAmount := raiseTo - p.Bet
	actualRaise := raiseTo - g.CurrentBet

	p.Stack -= raiseAmount
	p.Bet = raiseTo
	p.TotalBet += raiseAmount
	p.HasActed = true

	if actualRaise > 0 {
		g.MinRaise = actualRaise
	}
	g.CurrentBet = raiseTo
	g.LastAggressorIdx = seatIdx

	action := "raise"
	if p.Stack == 0 {
		p.State = PlayerAllIn
		action = "allin"
		g.StreetHistory += "a"
	} else {
		// Approximate: >=75% of pot is a pot-size raise ('R'), otherwise half-pot ('r').
		totalPot := g.Pot
		for _, pp := range g.Players {
			totalPot += pp.Bet
		}
		if totalPot > 0 && float64(actualRaise) >= float64(totalPot)*0.75 {
			g.StreetHistory += "R"
		} else {
			g.StreetHistory += "r"
		}
	}

	ann := renderActionAnnouncement(p.DisplayName, action, raiseTo)

	return ActionResult{
		Announcement: ann,
		AllAllIn:     g.canActCount() == 0,
	}, ""
}

// doAllIn processes an all-in action.
func (g *HoldemGame) doAllIn(seatIdx int) ActionResult {
	p := g.Players[seatIdx]
	allInAmount := p.Stack
	totalBet := p.Bet + allInAmount

	p.Stack = 0
	p.Bet = totalBet
	p.TotalBet += allInAmount
	p.State = PlayerAllIn
	p.HasActed = true

	if totalBet > g.CurrentBet {
		actualRaise := totalBet - g.CurrentBet
		// Only reopen action if the raise meets the minimum.
		// A short all-in (under-raise) does NOT reopen betting.
		if actualRaise >= g.MinRaise {
			g.MinRaise = actualRaise
			g.LastAggressorIdx = seatIdx
		}
		g.CurrentBet = totalBet
	}

	g.StreetHistory += "a"
	ann := renderActionAnnouncement(p.DisplayName, "allin", totalBet)

	if g.activeCount() == 1 {
		return ActionResult{Announcement: ann, HandOver: true}
	}

	return ActionResult{
		Announcement: ann,
		StreetOver:   g.isStreetComplete(g.nextCanActIdx(seatIdx)),
		AllAllIn:     g.canActCount() == 0,
	}
}

// isStreetComplete checks if the betting round is done.
func (g *HoldemGame) isStreetComplete(nextIdx int) bool {
	// All active players have matched the current bet and action has returned to the last aggressor.
	if g.canActCount() == 0 {
		return true
	}

	// Check if all Active players have matched the bet AND have acted at least once.
	// The acted check is critical for preflop: BB posts a blind but hasn't voluntarily
	// acted yet, so the street can't end before BB gets their option.
	for _, p := range g.Players {
		if p.State == PlayerActive && p.Bet != g.CurrentBet {
			return false
		}
		if p.State == PlayerActive && !p.HasActed {
			return false
		}
	}

	// If the last aggressor is all-in (can't act), the street is done when
	// all active players have matched the bet (already checked above).
	if g.Players[g.LastAggressorIdx].State == PlayerAllIn {
		return true
	}

	// Action must have gone around to the last aggressor.
	return nextIdx == g.LastAggressorIdx
}

// buildSidePots creates side pots when all-ins are present.
func (g *HoldemGame) buildSidePots() {
	// Collect all bets.
	g.collectPot()

	type betEntry struct {
		uid id.UserID
		bet int64
	}

	var entries []betEntry
	for _, p := range g.Players {
		if p.State != PlayerFolded && p.State != PlayerSatOut {
			entries = append(entries, betEntry{p.UserID, p.TotalBet})
		}
	}

	if len(entries) == 0 {
		return
	}

	sort.Slice(entries, func(i, j int) bool {
		return entries[i].bet < entries[j].bet
	})

	var sidePots []SidePot
	prevLevel := int64(0)

	for _, e := range entries {
		if e.bet <= prevLevel {
			continue
		}

		level := e.bet
		potSlice := int64(0)

		// All players who bet >= level contribute (level - prevLevel) each.
		var eligible []id.UserID
		for _, p := range g.Players {
			if p.State == PlayerFolded || p.State == PlayerSatOut {
				// Folded players still contributed up to their TotalBet.
				contrib := p.TotalBet - prevLevel
				if contrib > level-prevLevel {
					contrib = level - prevLevel
				}
				if contrib > 0 {
					potSlice += contrib
				}
				continue
			}

			contrib := p.TotalBet - prevLevel
			if contrib > level-prevLevel {
				contrib = level - prevLevel
			}
			if contrib > 0 {
				potSlice += contrib
			}
			if p.TotalBet >= level {
				eligible = append(eligible, p.UserID)
			}
		}

		if potSlice > 0 {
			sidePots = append(sidePots, SidePot{Amount: potSlice, Eligible: eligible})
		}

		prevLevel = level
	}

	if len(sidePots) > 0 {
		g.SidePots = sidePots
		g.Pot = 0
	}
}

// returnUncalledBet returns any unmatched portion of a bet to the player.
func (g *HoldemGame) returnUncalledBet() (name string, amount int64) {
	// Find the highest and second-highest bets among non-folded players.
	var highest, secondHighest int64
	var highestIdx int

	for i, p := range g.Players {
		if p.State == PlayerFolded || p.State == PlayerSatOut {
			continue
		}
		if p.TotalBet > highest {
			secondHighest = highest
			highest = p.TotalBet
			highestIdx = i
		} else if p.TotalBet > secondHighest {
			secondHighest = p.TotalBet
		}
	}

	excess := highest - secondHighest
	if excess > 0 && secondHighest > 0 {
		p := g.Players[highestIdx]
		p.Stack += excess
		p.TotalBet -= excess
		// Don't adjust per-street Bet — it may be smaller than the hand-total
		// excess, and it's irrelevant since the hand is ending.
		return p.DisplayName, excess
	}

	return "", 0
}
