package controller

import (
	"cmp"
	"iter"
	"maps"
	"math"
	"math/rand/v2"
	"slices"

	"galaxy/calc"

	e "galaxy/error"

	"galaxy/game/internal/model/game"
)

func (c *Cache) PlanetRouteSet(ri int, rt game.RouteType, origin, destination uint) error {
	c.validateRaceIndex(ri)
	p1, ok := c.Planet(origin)
	if !ok {
		return e.NewEntityNotExistsError("origin planet #%d", origin)
	}
	if !p1.OwnedBy(c.g.Race[ri].ID) {
		return e.NewEntityNotOwnedError("planet #%d", origin)
	}
	p2, ok := c.Planet(destination)
	if !ok {
		return e.NewEntityNotExistsError("destination planet #%d", destination)
	}
	rangeToDestination := calc.ShortDistance(c.g.Map.Width, c.g.Map.Height, p1.X.F(), p1.Y.F(), p2.X.F(), p2.Y.F())
	if rangeToDestination > c.g.Race[ri].FlightDistance() {
		return e.NewSendUnreachableDestinationError("range=%.03f max=%.03f", rangeToDestination, c.g.Race[ri].FlightDistance())
	}

	c.SetPlanetRoute(rt, origin, destination)

	return nil
}

func (c *Cache) PlanetRouteRemove(ri int, rt game.RouteType, origin uint) error {
	c.validateRaceIndex(ri)
	p1, ok := c.Planet(origin)
	if !ok {
		return e.NewEntityNotExistsError("origin planet #%d", origin)
	}
	if !p1.OwnedBy(c.g.Race[ri].ID) {
		return e.NewEntityNotOwnedError("planet #%d", origin)
	}

	c.RemovePlanetRoute(rt, origin)

	return nil
}

func (c *Cache) SetPlanetRoute(rt game.RouteType, origin, destination uint) {
	pi := c.MustPlanetIndex(origin)
	if c.g.Map.Planet[pi].Route == nil {
		c.g.Map.Planet[pi].Route = make(map[game.RouteType]uint)
	}
	c.g.Map.Planet[pi].Route[rt] = destination
}

func (c *Cache) RemovePlanetRoute(rt game.RouteType, origin uint) {
	pi := c.MustPlanetIndex(origin)
	if c.g.Map.Planet[pi].Route != nil {
		delete(c.g.Map.Planet[pi].Route, rt)
	}
}

func (c *Cache) SendRoutedGroups() {
	for pi := range c.g.Map.Planet {
		if len(c.g.Map.Planet[pi].Route) == 0 {
			continue
		}
		groups := slices.Collect(c.listRoutedSendGroupIds(c.g.Map.Planet[pi].Number))
		if len(groups) == 0 {
			continue
		}

		sortGroups := func(g []int) {
			// sort groups by largest CargoCapacity
			slices.SortFunc(g, func(l, r int) int {
				return cmp.Or(
					cmp.Compare(c.ShipGroup(r).CargoCapacity(c.ShipGroupShipClass(r)), c.ShipGroup(l).CargoCapacity(c.ShipGroupShipClass(l))),
					cmp.Compare(l, r))
			})

		}
		reorderGroups := func(g []int) []int {
			g = slices.DeleteFunc(g, func(i int) bool { return c.ShipGroup(i).State() != game.StateInOrbit })
			sortGroups(g)
			return g
		}

		sortGroups(groups)

		p := c.MustPlanet(c.g.Map.Planet[pi].Number)

		// COL -> CAP -> MAT -> EMPTY
		for _, rt := range []game.RouteType{game.RouteColonist, game.RouteCapital, game.RouteMaterial, game.RouteEmpty} {
			dest, ok := c.g.Map.Planet[pi].Route[rt]
			if !ok {
				continue
			}
			var res *game.Float
			var ct game.CargoType
			switch rt {
			case game.RouteColonist:
				res = &p.Colonists
				ct = game.CargoColonist
			case game.RouteCapital:
				res = &p.Capital
				ct = game.CargoCapital
			case game.RouteMaterial:
				res = &p.Material
				ct = game.CargoMaterial
			case game.RouteEmpty:
				// empty routes launched immediately so the're not required to be loaded
				for _, sgi := range groups {
					c.LaunchShips(sgi, dest)
				}
				groups = reorderGroups(groups)
				continue
			default:
				continue
			}
			for res != nil && *res > 0 && len(groups) > 0 {
				sgi := groups[0]
				sg := c.ShipGroup(sgi)
				st := c.ShipGroupShipClass(sgi)
				ships := sg.Number
				sgCapacity := sg.CargoCapacity(st)
				toLoad := (*res).F()
				if toLoad > sgCapacity {
					toLoad = sgCapacity
				} else if maxShips := uint(math.Ceil(toLoad / (sgCapacity / float64(ships)))); maxShips < ships {
					newGroupIdx := c.unsafeBreakGroup(c.RaceIndex(sg.OwnerID), sgi, maxShips)
					sgi = newGroupIdx
					sg = c.ShipGroup(newGroupIdx)
				}
				// decrease planet resource
				*res = (*res).Add(-toLoad)
				// load group
				sg.Load = sg.Load.Add(toLoad)
				sg.CargoType = &ct
				c.LaunchShips(sgi, dest)
				groups = reorderGroups(groups)
			}
		}
	}
}

func (c *Cache) listRoutedSendGroupIds(pn uint) iter.Seq[int] {
	return func(yield func(int) bool) {
		p := c.MustPlanet(pn)
		for i := range c.ShipGroupsIndex() {
			sg := c.ShipGroup(i)
			st := c.ShipGroupShipClass(i)
			if !p.OwnedBy(sg.OwnerID) || // Planet must be owned by ships owner
				sg.FleetID != nil || // Ships must not be part of a Fleet
				sg.State() != game.StateInOrbit || // Ships must be only In_Orbit state
				st.CargoBlockMass() == 0 || // Ship Class must have Cargo bays
				sg.Load != 0 || // Ships must not be loaded for enrouting
				sg.Destination != p.Number {
				continue
			}
			if !yield(i) {
				return
			}
		}
	}
}

// Невозможно лишь выгрузить колонистов на чужой планете.
func (c *Cache) TurnUnloadEnroutedGroups() {
	for i := range c.g.Map.Planet {
		p := &c.g.Map.Planet[i]
		c.doUnload(c.unloadRoutedColonists(p.Number, c.listRoutedUnloadShipGroupIds(p.Number, game.RouteColonist)))
		for _, rt := range []game.RouteType{game.RouteMaterial, game.RouteCapital} {
			c.doUnload(c.listRoutedUnloadShipGroupIds(p.Number, rt))
		}
		p.UnpackColonists()
		p.UnpackCapital()
	}
}

func (c *Cache) RemoveUnreachableRoutes() {
	for i := range c.g.Map.Planet {
		p1 := &c.g.Map.Planet[i]
		if !p1.Owned() {
			continue
		}
		ri := c.RaceIndex(*p1.Owner)
		for rt, destination := range p1.Route {
			p2 := c.MustPlanet(destination)
			rangeToDestination := calc.ShortDistance(c.g.Map.Width, c.g.Map.Height, p1.X.F(), p1.Y.F(), p2.X.F(), p2.Y.F())
			if rangeToDestination > c.g.Race[ri].FlightDistance() {
				delete(p1.Route, rt)
			}
		}
	}
}

func (c *Cache) doUnload(groups iter.Seq[int]) {
	for sgi := range groups {
		c.unsafeUnloadCargo(sgi, c.ShipGroup(sgi).Load.F())
	}
}

func (c *Cache) unloadRoutedColonists(pn uint, groups iter.Seq[int]) iter.Seq[int] {
	p := c.MustPlanet(pn)
	gr := slices.Collect(groups)
	if !p.Owned() {
		return c.selectColUnloadGroup(gr)
	}
	return func(yield func(int) bool) {
		for _, sgi := range gr {
			sg := c.ShipGroup(sgi)
			if !p.OwnedBy(sg.OwnerID) {
				continue
			}
			if !yield(sgi) {
				return
			}
		}
	}

}

func (c *Cache) selectColUnloadGroup(groups []int) (result iter.Seq[int]) {
	groupByRace := make(map[int][]int)
	loadByRace := make(map[int]float64)
	for _, i := range groups {
		sg := c.ShipGroup(i)
		ri := c.RaceIndex(sg.OwnerID)
		groupByRace[ri] = append(groupByRace[ri], i)
		loadByRace[ri] += sg.Load.F()
	}
	if len(loadByRace) < 2 {
		// only one race has to unload cargo
		result = slices.Values(groups)
		return
	}

	// select winner to unload cargo
	id := MaxOrRandomLoadId(loadByRace, func(ri int) float64 { return float64(c.g.Race[ri].Votes) })
	result = slices.Values(groupByRace[id])

	return
}

func (c *Cache) listRoutedUnloadShipGroupIds(pn uint, routeType game.RouteType) iter.Seq[int] {
	return func(yield func(int) bool) {
		yielded := make(map[int]bool)
		for i := range c.g.Map.Planet {
			for rt, dest := range c.g.Map.Planet[i].Route {
				if dest != pn || rt != routeType {
					continue
				}
				for i := range c.ShipGroupsIndex() {
					sg := c.ShipGroup(i)
					if _, ok := yielded[i]; ok || sg.FleetID != nil || sg.CargoType == nil || sg.Load == 0. || sg.State() != game.StateInOrbit || sg.Destination != dest {
						continue
					}
					if v, ok := game.RouteToCargo[rt]; !ok || v != *sg.CargoType {
						continue
					}
					if !yield(i) {
						return
					}
					yielded[i] = true
				}
			}
		}
	}
}

func MaxOrRandomLoadId(raceLoad map[int]float64, pop func(int) float64) int {
	if len(raceLoad) < 2 {
		panic("loadByRace must contain at least 2 keys")
	}
	raceIndex := slices.Collect(maps.Keys(raceLoad))
	slices.SortFunc(raceIndex, func(ria, rib int) int {
		return cmp.Or(
			// maximum quantity of unloading colonists
			cmp.Compare(raceLoad[rib], raceLoad[ria]),

			// maximum population of the race
			cmp.Compare(pop(rib), pop(ria)),

			// Random winner
			cmp.Compare(rand.Float64(), rand.Float64()),

			// in theoty, unreacheable option, but let's randomize again
			cmp.Compare(rand.Float64(), rand.Float64()),
		)
	})
	return raceIndex[0]
}
