package controller

import (
	"cmp"
	"fmt"
	"iter"
	"maps"
	"slices"

	"galaxy/util"

	e "galaxy/error"

	"galaxy/game/internal/model/game"

	"github.com/google/uuid"
)

// ShipGroup is a proxy func, nothing to cache
func (c *Cache) ShipGroup(groupIndex int) *game.ShipGroup {
	c.validateShipGroupIndex(groupIndex)
	return &c.g.ShipGroups[groupIndex]
}

func (c *Cache) internalShipGroupJoinFleet(groupIndex int, fID uuid.UUID) {
	c.validateShipGroupIndex(groupIndex)
	c.g.ShipGroups[groupIndex].FleetID = &fID
	c.invalidateFleetCache()
}

func (c *Cache) ShipGroupShipsNumber(groupIndex int, number uint) {
	c.validateShipGroupIndex(groupIndex)
	if c.g.ShipGroups[groupIndex].Number > 0 {
		c.g.ShipGroups[groupIndex].Load = game.F(c.g.ShipGroups[groupIndex].Load.F() / float64(c.g.ShipGroups[groupIndex].Number) * float64(number))
	}
	c.g.ShipGroups[groupIndex].Number = number
}

func (c *Cache) ShipGroupsIndex() iter.Seq[int] {
	return func(yield func(int) bool) {
		for i := range c.g.ShipGroups {
			if !yield(i) {
				return
			}
		}
	}
}

func (c *Cache) ShipGroupOwnerRaceIndex(groupIndex int) int {
	c.validateShipGroupIndex(groupIndex)
	if len(c.cacheRaceIndexByShipGroupIndex) == 0 {
		c.cacheShipsAndGroups()
	}
	if v, ok := c.cacheRaceIndexByShipGroupIndex[groupIndex]; ok {
		return v
	} else {
		panic(fmt.Sprintf("ShipGroupRace: group not found by index=%v", groupIndex))
	}
}

func (c *Cache) ShipGroupOwnerRace(groupIndex int) *game.Race {
	return &c.g.Race[c.ShipGroupOwnerRaceIndex(groupIndex)]
}

func (c *Cache) ShipGroupDestroyItem(i int) {
	c.validateShipGroupIndex(i)
	sg := &c.g.ShipGroups[i]
	if sg.Number == 0 {
		panic("group has no ships")
	}
	sg.Load = game.F(sg.Load.F() / float64(sg.Number) * float64(sg.Number-1))
	sg.Number -= 1
}

func (c *Cache) DeleteKilledShipGroups() {
	keepFleet := make(map[uuid.UUID]bool, len(c.g.Fleets))
	for sgi := len(c.g.ShipGroups) - 1; sgi >= 0; sgi-- {
		if c.g.ShipGroups[sgi].FleetID != nil {
			id := *c.g.ShipGroups[sgi].FleetID
			keepFleet[id] = keepFleet[id] || c.g.ShipGroups[sgi].Number > 0
		}
		if c.g.ShipGroups[sgi].Number == 0 {
			c.g.ShipGroups = append(c.g.ShipGroups[:sgi], c.g.ShipGroups[sgi+1:]...)
		}
	}
	c.invalidateShipGroupCache()
	for id, keep := range keepFleet {
		if keep {
			continue
		}
		c.unsafeDeleteFleet(c.MustFleetIndex(id))
	}
}

func (c *Cache) TurnMergeEqualShipGroups() {
	for i := range c.listRaceActingIdx() {
		c.transferPendingGroups(i)
		c.shipGroupMerge(i)
	}
}

func (c *Cache) transferPendingGroups(ri int) {
	c.validateRaceIndex(ri)
	for sg := range c.listShipGroups(ri) {
		if sg.State() == game.StateTransfer {
			sg.StateTransfer = false
		}
	}
}

// shipGroupMerge merges several equal ship groups into one
func (c *Cache) shipGroupMerge(ri int) {
	c.validateRaceIndex(ri)
	raceGroups := make([]game.ShipGroup, 0)
	for sg := range c.listShipGroups(ri) {
		raceGroups = append(raceGroups, *sg)
	}

	origin := len(raceGroups)
	if origin < 2 {
		return
	}
	for i := 0; i < len(raceGroups)-1; i++ {
		for j := len(raceGroups) - 1; j > i; j-- {
			if raceGroups[i].Equal(raceGroups[j]) {
				raceGroups[i].ID = raceGroups[j].ID // resulting group will have latest ID
				raceGroups[i].Number += raceGroups[j].Number
				raceGroups = append(raceGroups[:j], raceGroups[j+1:]...)
			}
		}
	}
	if len(raceGroups) == origin {
		return
	}

	toDelete := make([]int, 0)
	for i := range c.ShipGroupsIndex() {
		if c.ShipGroup(i).OwnerID == c.g.Race[ri].ID {
			toDelete = append(toDelete, i)
		}
	}

	slices.Sort(toDelete)
	slices.Reverse(toDelete)
	for _, sgi := range toDelete {
		c.unsafeDeleteShipGroup(sgi)
	}

	for i := range raceGroups {
		c.appendShipGroup(ri, &raceGroups[i])
	}
}

func (c *Cache) shipGroupDismantle(ri int, groupIndex uuid.UUID) error {
	c.validateRaceIndex(ri)
	sgi, ok := c.raceShipGroupIndex(ri, groupIndex)
	if !ok {
		return e.NewEntityNotExistsError("group #%d", groupIndex)
	}

	if state := c.ShipGroup(sgi).State(); state != game.StateInOrbit {
		return e.NewShipsBusyError("state: %s", state)
	}

	pl, ok := c.Planet(c.ShipGroup(sgi).Destination)
	if !ok {
		return e.NewGameStateError("planet #%d", c.ShipGroup(sgi).Destination)
	}
	p := *pl

	st := c.ShipGroupShipClass(sgi)

	if c.ShipGroup(sgi).CargoType != nil {
		ct := *c.ShipGroup(sgi).CargoType
		load := c.ShipGroup(sgi).Load.F()
		switch ct {
		case game.CargoColonist:
			if p.OwnedBy(c.g.Race[ri].ID) {
				p = game.UnloadColonists(p, load)
			}
		case game.CargoMaterial:
			p.Material = p.Material.Add(load)
		case game.CargoCapital:
			p.Capital = p.Capital.Add(load)
		}
	}

	p.Material = p.Material.Add(c.ShipGroup(sgi).EmptyMass(st))

	c.unsafeDeleteShipGroup(sgi)

	c.g.Map.Planet[c.MustPlanetIndex(p.Number)] = p

	return nil
}

// Корабль может нести только один тип груза одновременно.
// Возможные типы груза - это колонисты, сырье и промышленность.
// Груз может быть доставлен на борт корабля с Вашей или не занятой планеты, на которой он имеется.
// Указанное количество груза равномерно распределяется между всеми кораблями группы.
func (c *Cache) shipGroupLoad(ri int, groupID uuid.UUID, ct game.CargoType, quantity float64) error {
	c.validateRaceIndex(ri)
	sgi, ok := c.raceShipGroupIndex(ri, groupID)
	if !ok {
		return e.NewEntityNotExistsError("group %s", groupID)
	}
	if state := c.ShipGroup(sgi).State(); state != game.StateInOrbit {
		return e.NewShipsBusyError("state: %s", state)
	}
	p, ok := c.Planet(c.ShipGroup(sgi).Destination)
	if !ok {
		return e.NewGameStateError("planet #%d", c.ShipGroup(sgi).Destination)
	}

	if p.Owned() && !p.OwnedBy(c.g.Race[ri].ID) {
		return e.NewEntityNotOwnedError("planet #%d", p.Number)
	}
	st := c.ShipGroupShipClass(sgi)

	if st.Cargo < 1 {
		return e.NewNoCargoBayError("ship_type %q", st.Name)
	}
	if c.ShipGroup(sgi).CargoType != nil && *c.ShipGroup(sgi).CargoType != ct {
		return e.NewCargoLoadNotEqualError("cargo: %v", *c.ShipGroup(sgi).CargoType)
	}

	capacity := c.ShipGroup(sgi).CargoCapacity(st)
	freeShipGroupCargoLoad := capacity - float64(c.ShipGroup(sgi).Load)
	if freeShipGroupCargoLoad == 0 {
		return e.NewCargoLoadNoSpaceLeftError()
	}
	var availableOnPlanet *game.Float
	switch ct {
	case game.CargoMaterial:
		availableOnPlanet = &p.Material
	case game.CargoCapital:
		availableOnPlanet = &p.Capital
	case game.CargoColonist:
		availableOnPlanet = &p.Colonists
	default:
		return e.NewGameStateError("CargoType not accepted: %v", ct)
	}
	if quantity > float64(*availableOnPlanet) || *availableOnPlanet == 0 {
		return e.NewCargoLoadNotEnoughError("planet: #%d, %s=%.03f", p.Number, ct, *availableOnPlanet)
	}
	toBeLoaded := quantity
	if quantity == 0 {
		toBeLoaded = float64(*availableOnPlanet)
	}
	if toBeLoaded > freeShipGroupCargoLoad {
		toBeLoaded = freeShipGroupCargoLoad
	}
	*availableOnPlanet = (*availableOnPlanet).Add(-toBeLoaded)
	c.ShipGroup(sgi).Load = c.ShipGroup(sgi).Load.Add(toBeLoaded)
	if c.ShipGroup(sgi).Load > 0 {
		c.ShipGroup(sgi).CargoType = &ct
	}
	return nil
}

// Промышленность и Сырье могут быть выгружены на любой планете.
// Колонисты могут быть высажены только на планеты, принадлежащие Вам или на необитаемые планеты.
func (c *Cache) shipGroupUnload(ri int, groupID uuid.UUID, quantity float64) error {
	c.validateRaceIndex(ri)
	sgi, ok := c.raceShipGroupIndex(ri, groupID)
	if !ok {
		return e.NewEntityNotExistsError("group %s", groupID)
	}
	if state := c.ShipGroup(sgi).State(); state != game.StateInOrbit {
		return e.NewShipsBusyError("state: %s", state)
	}
	st := c.ShipGroupShipClass(sgi)
	if st.Cargo < 1 {
		return e.NewNoCargoBayError("ship_type %q", st.Name)
	}
	if c.ShipGroup(sgi).CargoType == nil || c.ShipGroup(sgi).Load == 0 {
		return e.NewCargoUnloadEmptyError()
	}

	ct := *c.ShipGroup(sgi).CargoType
	p := c.MustPlanet(c.ShipGroup(sgi).Destination)

	if ct == game.CargoColonist && p.Owned() && !p.OwnedBy(c.g.Race[ri].ID) {
		return e.NewEntityNotOwnedError("planet #%d unload %v", p.Number, ct)
	}

	c.unsafeUnloadCargo(sgi, UnloadCargoRequest(float64(c.ShipGroup(sgi).Load), quantity))
	return nil
}

func UnloadCargoRequest(load, quantity float64) float64 {
	result := quantity
	if result == 0 || result > load {
		result = load
	}
	return result
}

func (c *Cache) shipGroupIndexByID(id uuid.UUID) (int, bool) {
	for sgi := range c.g.ShipGroups {
		if c.g.ShipGroups[sgi].ID == id {
			return sgi, true
		}
	}
	return -1, false
}

func (c *Cache) unsafeUnloadCargo(sgi int, q float64) {
	if q <= 0 {
		return
	}
	if st := c.ShipGroup(sgi).State(); st != game.StateInOrbit {
		panic(fmt.Sprintf("invalid group state: %v", st))
	}
	c.validateShipGroupIndex(sgi)
	p := c.MustPlanet(c.ShipGroup(sgi).Destination)
	ct := *c.ShipGroup(sgi).CargoType

	var availableOnPlanet *game.Float
	switch ct {
	case game.CargoColonist:
		availableOnPlanet = &p.Colonists
		if !p.Owned() {
			p.Own(c.ShipGroup(sgi).OwnerID)
			p.Production = game.ProductionCapital.AsType(uuid.Nil)
		}
	case game.CargoMaterial:
		availableOnPlanet = &p.Material
	case game.CargoCapital:
		availableOnPlanet = &p.Capital
	}
	*availableOnPlanet = (*availableOnPlanet).Add(q)

	c.ShipGroup(sgi).Load = c.ShipGroup(sgi).Load.Add(-q)
	if c.ShipGroup(sgi).Load == 0 {
		c.ShipGroup(sgi).CargoType = nil
	}

	p.UnpackColonists()
	p.UnpackCapital()
}

func (c *Cache) shipGroupTransfer(ri, riAccept int, groupID uuid.UUID) (err error) {
	c.validateRaceIndex(ri)
	if ri == riAccept {
		return e.NewSameRaceError(c.g.Race[riAccept].Name)
	}
	sgi, ok := c.raceShipGroupIndex(ri, groupID)
	if !ok {
		return e.NewEntityNotExistsError("group %s", groupID)
	}
	sg := c.ShipGroup(sgi)
	state := sg.State()
	if state == game.StateTransfer {
		return e.NewShipsBusyError("state: %s", state)
	}

	st := c.ShipGroupShipClass(sgi)

	var stAcc int
	var name = st.Name
	if stAcc = slices.IndexFunc(c.g.Race[riAccept].ShipTypes, func(v game.ShipType) bool { return v.Name == st.Name }); stAcc >= 0 &&
		!st.Equal(c.g.Race[riAccept].ShipTypes[stAcc]) {
		name = util.AppendRandomSuffix(name)
	}
	if stAcc < 0 || name != st.Name {
		err = c.ShipClassCreate(riAccept,
			name,
			st.Drive.F(),
			int(st.Armament),
			st.Weapons.F(),
			st.Shields.F(),
			st.Cargo.F())
		if err != nil {
			return err
		}
		stAcc = len(c.g.Race[riAccept].ShipTypes) - 1
	}

	newGroup := *(sg)
	newGroup.ID = uuid.New()
	newGroup.TypeID = c.g.Race[riAccept].ShipTypes[stAcc].ID
	newGroup.Tech = maps.Clone(sg.Tech)
	if state == game.StateLaunched {
		newGroup.StateTransfer = true
	}

	c.appendShipGroup(riAccept, &newGroup)
	c.unsafeDeleteShipGroup(sgi)

	return nil
}

func (c *Cache) ShipGroupBreak(ri int, groupID, newID uuid.UUID, quantity uint) (err error) {
	c.validateRaceIndex(ri)
	sgi, ok := c.raceShipGroupIndex(ri, groupID)
	if !ok {
		return e.NewEntityNotExistsError("group %s", groupID)
	}
	for sgi := range c.g.ShipGroups {
		if c.g.ShipGroups[sgi].ID == newID {
			return e.NewEntityDuplicateIdentifierError("group %s", newID)
		}
	}

	if state := c.ShipGroup(sgi).State(); state != game.StateInOrbit {
		return e.NewShipsBusyError()
	}

	if c.ShipGroup(sgi).Number < quantity {
		return e.NewBeakGroupNumberNotEnoughError("%d<%d", c.ShipGroup(sgi).Number, quantity)
	}

	if quantity > 0 && quantity < c.ShipGroup(sgi).Number {
		if sgi, err = c.breakGroup(ri, groupID, quantity); err != nil {
			return
		}
	}

	c.ShipGroup(sgi).FleetID = nil

	return nil
}

func (c *Cache) breakGroup(ri int, groupID uuid.UUID, newGroupShips uint) (int, error) {
	c.validateRaceIndex(ri)
	sgi, ok := c.raceShipGroupIndex(ri, groupID)
	if !ok {
		return -1, e.NewEntityNotExistsError("group %s", groupID)
	}
	if c.ShipGroup(sgi).Number < newGroupShips {
		return -1, e.NewBreakGroupIllegalNumberError("group=%s ships: %d -> %d", c.ShipGroup(sgi).ID, c.ShipGroup(sgi).Number, newGroupShips)
	}
	return c.unsafeBreakGroup(ri, sgi, newGroupShips), nil
}

func (c *Cache) unsafeBreakGroup(ri, sgi int, newGroupShips uint) int {
	newGroup := *c.ShipGroup(sgi)
	if c.ShipGroup(sgi).CargoType != nil {
		newGroup.Load = game.F(float64(c.ShipGroup(sgi).Load) / float64(c.ShipGroup(sgi).Number) * float64(newGroupShips))
	}
	newGroup.Number = newGroupShips
	c.ShipGroupShipsNumber(sgi, c.ShipGroup(sgi).Number-newGroup.Number)
	newGroup.FleetID = nil
	return c.appendShipGroup(ri, &newGroup)
}

// Internal funcs

func (c *Cache) raceShipGroupIndex(ri int, id uuid.UUID) (int, bool) {
	c.validateRaceIndex(ri)
	for i := range c.ShipGroupsIndex() {
		if c.ShipGroupOwnerRaceIndex(i) == ri && c.ShipGroup(i).ID == id {
			return i, true
		}
	}
	return -1, false
}

func (c *Cache) listShipGroupIdx(ri int) iter.Seq[int] {
	c.validateRaceIndex(ri)
	return func(yield func(int) bool) {
		for i := range c.g.ShipGroups {
			if ri == c.ShipGroupOwnerRaceIndex(i) {
				if !yield(i) {
					return
				}
			}
		}
	}
}

func (c *Cache) listShipGroups(ri int) iter.Seq[*game.ShipGroup] {
	c.validateRaceIndex(ri)
	return func(yield func(*game.ShipGroup) bool) {
		for sgi := range c.listShipGroupIdx(ri) {
			if !yield(c.ShipGroup(sgi)) {
				return
			}
		}
	}
}

func (c *Cache) shipGroupsInUpgrade(planetNumber uint) iter.Seq[*game.ShipGroup] {
	return func(yield func(*game.ShipGroup) bool) {
		result := make([]int, 0)
		for sg := range c.g.ShipGroups {
			// number checked for further sanity after battles
			if c.g.ShipGroups[sg].Number > 0 && c.g.ShipGroups[sg].Destination == planetNumber && c.g.ShipGroups[sg].State() == game.StateUpgrade {
				result = append(result, sg)
			}
		}
		slices.SortFunc(result, func(a, b int) int {
			return cmp.Compare(c.g.ShipGroups[b].StateUpgrade.Cost(), c.g.ShipGroups[a].StateUpgrade.Cost())
		})
		for i := range result {
			if !yield(&c.g.ShipGroups[result[i]]) {
				return
			}
		}
	}
}

func (c *Cache) unsafeDeleteShipGroup(sgi int) {
	c.validateShipGroupIndex(sgi)

	sg := c.ShipGroup(sgi)
	if sg.FleetID != nil {
		fi := c.MustFleetIndex(*sg.FleetID)
		fleetGroups := slices.Collect(c.fleetGroupIds(c.RaceIndex(sg.OwnerID), fi))
		if len(fleetGroups) == 1 {
			// remove fleet when deleting last group in the fleet
			c.unsafeDeleteFleet(fi)
		}
	}

	c.g.ShipGroups = append(c.g.ShipGroups[:sgi], c.g.ShipGroups[sgi+1:]...)
	c.invalidateShipGroupCache()
}

func (c *Cache) validateShipGroupIndex(i int) {
	if i >= len(c.g.ShipGroups) {
		panic(fmt.Sprintf("group index out of range: %d >= %d", i, len(c.g.ShipGroups)))
	}
}

func (c *Cache) unsafeCreateShips(ri int, classID uuid.UUID, planet uint, quantity uint) int {
	st := c.MustShipType(ri, classID)
	level := func(t game.Tech) float64 {
		if t == game.TechDrive && st.DriveBlockMass() > 0 {
			return util.Fixed3(c.g.Race[ri].TechLevel(game.TechDrive))
		}
		if t == game.TechWeapons && st.WeaponsBlockMass() > 0 {
			return util.Fixed3(c.g.Race[ri].TechLevel(game.TechWeapons))
		}
		if t == game.TechShields && st.ShieldsBlockMass() > 0 {
			return util.Fixed3(c.g.Race[ri].TechLevel(game.TechShields))
		}
		if t == game.TechCargo && st.CargoBlockMass() > 0 {
			return util.Fixed3(c.g.Race[ri].TechLevel(game.TechCargo))
		}
		return 0
	}
	return c.appendShipGroup(ri, &game.ShipGroup{
		OwnerID:     c.g.Race[ri].ID,
		TypeID:      classID,
		Destination: planet,
		Number:      uint(quantity),
		Tech: map[game.Tech]game.Float{
			game.TechDrive:   game.F(level(game.TechDrive)),
			game.TechWeapons: game.F(level(game.TechWeapons)),
			game.TechShields: game.F(level(game.TechShields)),
			game.TechCargo:   game.F(level(game.TechCargo)),
		},
	})
}

func (c *Cache) appendShipGroup(ri int, sg *game.ShipGroup) int {
	c.validateRaceIndex(ri)
	sg.ID = uuid.New()
	sg.OwnerID = c.g.Race[ri].ID
	sg.FleetID = nil
	c.g.ShipGroups = append(c.g.ShipGroups, *sg)
	i := len(c.g.ShipGroups) - 1
	c.invalidateShipGroupCache()
	return i
}
