package controller

import (
	"cmp"
	"fmt"
	"maps"
	"math/big"
	"slices"

	"galaxy/game/internal/model/game"

	"github.com/google/uuid"
)

type VoteGroup struct {
	RaceIndex []int
	Sum       float64
}

type VoteNode struct {
	ID   int
	Ally bool
	Next *VoteNode
}

func (n VoteNode) String() string {
	lh, rh := " ", "."
	if n.Ally {
		lh, rh = "{", "}"
	}
	return fmt.Sprintf("%s%d%s", lh, n.ID, rh)
}

func (c *Cache) TurnAcceptWinners(v []int) {
	if c.g.Finished() {
		panic("game is already has its winner(s)")
	}
	if len(v) == 0 {
		return
	}
	for _, ri := range v {
		c.g.Winner = append(c.g.Winner, c.g.Race[ri].ID)
	}
}

func (c *Cache) TurnCalculateVotes() []int {
	raceVotes := c.votesByRace()
	calc := GroupVotes(raceVotes, VotingGraph(c.g.Race, c.RaceIndex))

	c.g.Votes = 0
	for ri, votes := range raceVotes {
		v := game.F(votes)
		c.g.Race[ri].Votes = v
		c.g.Votes += v
	}

	return votingWinners(calc, c.g.Votes.F())
}

func VotingGraph(races []game.Race, raceIndex func(uuid.UUID) int) map[int]*VoteNode {
	nodes := make(map[int]*VoteNode, len(races))
	for ri := range races {
		if races[ri].Extinct {
			continue
		}
		r := &races[ri]
		if _, ok := nodes[ri]; !ok {
			nodes[ri] = &VoteNode{
				ID: ri,
			}
		}
		if r.VoteFor != r.ID {
			vid := raceIndex(r.VoteFor)
			if !races[vid].Extinct {
				if _, ok := nodes[vid]; !ok {
					nodes[vid] = &VoteNode{
						ID: vid,
					}
				}
				nodes[ri].Next = nodes[vid]
			}
		}
	}
	return nodes
}

func (c *Cache) votesByRace() map[int]float64 {
	result := make(map[int]float64)
	for i := range c.g.Map.Planet {
		p := &c.g.Map.Planet[i]
		if !p.Owned() {
			continue
		}
		ri := c.RaceIndex(*p.Owner)
		planetVotes := p.Votes()
		result[ri] += planetVotes
	}
	return result
}

func GroupVotes(raceVotes map[int]float64, nodes map[int]*VoteNode) []*VoteGroup {
	votes := maps.Clone(raceVotes)
	result := make([]*VoteGroup, 0)
	chains := VotingChains(nodes)
	chainingRaces := make(map[int]bool)

	for i := range chains {
		chain := chains[i]
		if len(chain) == 0 {
			panic("voters chain is empty")
		}
		vg := &VoteGroup{}
		for j := range chain {
			node := &chain[j]
			if node.Ally || j == len(chain)-1 {
				vg.RaceIndex = append(vg.RaceIndex, node.ID)
			}
			vg.Sum += votes[node.ID]
			votes[node.ID] = 0
			chainingRaces[node.ID] = true
		}
		// find a non-ally group (single race) which already have its votes and merge with a new VoteGroup instead of adding to result
		if i := slices.IndexFunc(result, func(v *VoteGroup) bool { return len(v.RaceIndex) == 1 && v.RaceIndex[0] == vg.RaceIndex[0] }); i >= 0 && len(vg.RaceIndex) == 1 {
			result[i].Sum += vg.Sum
		} else {
			result = append(result, vg)
		}
	}

	for ri, votes := range votes {
		if _, ok := chainingRaces[ri]; !ok && votes > 0 {
			result = append(result, &VoteGroup{RaceIndex: []int{ri}, Sum: votes})
		}
	}

	return result
}

func VotingChains(nodes map[int]*VoteNode) [][]VoteNode {
	visited := make(map[int]bool)
	result := make([][]VoteNode, 0)
	raceIds := slices.Collect(maps.Keys(nodes))
	slices.Sort(raceIds)
	for _, rid := range raceIds {
		n := nodes[rid]
		if v, ok := visited[n.ID]; (ok && v) || n.Next == nil {
			continue
		}

		slow, fast := n, n
		cycled := false
		var cycleBound *VoteNode
		for slow != nil && fast != nil && fast.Next != nil {
			slow = slow.Next
			fast = fast.Next.Next

			if slow == fast {
				slow = n

				for slow != fast {
					slow = slow.Next
					fast = fast.Next
				}

				cycled = true
				cycleBound = slow

				break
			}
		}

		var current *VoteNode
		if cycled && !visited[slow.ID] {
			result = append(result, make([]VoteNode, 0))

			result[len(result)-1] = append(result[len(result)-1], VoteNode{ID: slow.ID, Ally: true})
			visited[slow.ID] = true

			current = slow.Next

			for current != slow {
				visited[current.ID] = true
				result[len(result)-1] = append(result[len(result)-1], VoteNode{ID: current.ID, Ally: true})
				current = current.Next
			}

			if n == slow {
				continue
			}
		}

		current = n
		var finish *VoteNode
		if cycleBound != nil {
			if cycleBound == current.Next {
				finish = current
			} else {
				finish = cycleBound
			}
		} else {
			finish = nil
		}

		if finish != current {
			result = append(result, make([]VoteNode, 0))
		}

		for current != finish {
			visited[current.ID] = current.ID != n.ID && current.Next != nil
			result[len(result)-1] = append(result[len(result)-1], VoteNode{ID: current.ID, Ally: false})
			current = current.Next
		}
	}

	return result
}

func votingWinners(calc []*VoteGroup, sumVotes float64) []int {
	slices.SortFunc(calc, func(a, b *VoteGroup) int { return cmp.Compare(b.Sum, a.Sum) })

	topVoter := calc[0]
	maxVotes := &big.Rat{}
	maxVotes.SetFloat64(topVoter.Sum)

	winVotes := &big.Rat{}
	winVotes.SetFloat64(sumVotes)
	winVotes = winVotes.Mul(winVotes, big.NewRat(2, 3))

	if maxVotes.Cmp(winVotes) >= 0 {
		return topVoter.RaceIndex
	}

	return nil
}
