package controller

import (
	"fmt"
	"iter"
	"math"
	"slices"

	"galaxy/util"

	e "galaxy/error"

	"galaxy/game/internal/model/game"

	"github.com/google/uuid"
)

var fleetStateNil = game.ShipGroupState("-")

type FleetState struct {
	State       game.ShipGroupState
	Destination uint
	InSpace     func() (game.InSpace, bool)
	AtPlanet    func() (uint, bool)
}

func (fs *FleetState) inSpace() bool {
	_, ok := fs.InSpace()
	return ok
}

func (fs FleetState) AtSamePlanet(other FleetState) bool {
	pn1, ok := fs.AtPlanet()
	if !ok {
		return false
	}
	pn2, ok := other.AtPlanet()
	if !ok {
		return false
	}
	return pn1 == pn2
}

func (c *Cache) FleetState(fleetID uuid.UUID) FleetState {
	fi := c.MustFleetIndex(fleetID)
	ri := c.RaceIndex(c.g.Fleets[fi].OwnerID)
	fs := &FleetState{
		State:    fleetStateNil,
		InSpace:  func() (game.InSpace, bool) { return game.InSpace{}, false },
		AtPlanet: func() (uint, bool) { return 0, false },
	}
	for sgi := range c.FleetGroupIdx(ri, fi) {
		sg := c.ShipGroup(sgi)
		if fs.State == fleetStateNil {
			fs.State = sg.State()
			fs.Destination = sg.Destination
			if pn, ok := sg.AtPlanet(); ok {
				fs.AtPlanet = func() (uint, bool) { return pn, ok }
			} else if sg.StateInSpace != nil {
				fs.InSpace = func() (game.InSpace, bool) { return *sg.StateInSpace, true }
			}
			continue
		}
		if fs.State != sg.State() {
			panic(fmt.Sprintf("FleetState: one or more ships in race's %q fleet %q has different states", c.g.Race[ri].Name, c.g.Fleets[fi].Name))
		}
		if fs.Destination != sg.Destination {
			panic(fmt.Sprintf("FleetState: one or more ships in race's %q fleet %q has different destination", c.g.Race[ri].Name, c.g.Fleets[fi].Name))
		}
		if planet, ok := sg.AtPlanet(); ok {
			if onPlanet, ok := fs.AtPlanet(); ok && onPlanet != planet {
				panic(fmt.Sprintf("FleetState: one or more ships in race's %q fleet %q are on different planets: %d <> %d", c.g.Race[ri].Name, c.g.Fleets[fi].Name, onPlanet, planet))
			}
		}
		if (!fs.inSpace() && sg.StateInSpace != nil) || (fs.inSpace() && sg.StateInSpace == nil) {
			panic(fmt.Sprintf("FleetState: one or more ships in race's %q fleet %q on_planet and in_space at the same time", c.g.Race[ri].Name, c.g.Fleets[fi].Name))
		}
		if is, ok := fs.InSpace(); ok && sg.StateInSpace != nil && !is.Equal(*sg.StateInSpace) {
			panic(fmt.Sprintf("FleetState: one or more ships in race's %q fleet %q has different is_space states", c.g.Race[ri].Name, c.g.Fleets[fi].Name))
		}
	}
	if fs.State == fleetStateNil {
		panic(fmt.Sprintf("FleetState: race's %q fleet %q has no ships", c.g.Race[ri].Name, c.g.Fleets[fi].Name))
	}
	return *fs
}

func (c *Cache) FleetSpeedAndMass(fi int) (float64, float64) {
	c.validateFleetIndex(fi)
	speed := math.MaxFloat64
	mass := 0.
	for sgi := range c.ShipGroupsIndex() {
		if c.ShipGroup(sgi).FleetID == nil || *c.ShipGroup(sgi).FleetID != c.g.Fleets[fi].ID {
			continue
		}
		sg := c.ShipGroup(sgi)
		st := c.ShipGroupShipClass(sgi)
		typeSpeed := sg.Speed(st)
		if typeSpeed < speed {
			speed = typeSpeed
		}
		mass += sg.FullMass(st)
	}
	return speed, mass
}

func (c *Cache) ShipGroupJoinFleet(ri int, fleetName string, groupID uuid.UUID) (err error) {
	c.validateRaceIndex(ri)
	name, ok := util.ValidateTypeName(fleetName)
	if !ok {
		return e.NewEntityTypeNameValidationError("%q", name)
	}
	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)
	}

	var oldFleetID *uuid.UUID
	if c.ShipGroup(sgi).FleetID != nil {
		fID := *c.ShipGroup(sgi).FleetID
		oldFleetID = &fID
	}

	fi, ok := c.fleetIndex(ri, name)
	if !ok {
		fi, err = c.createFleet(ri, name)
		if err != nil {
			return err
		}
	} else {
		fleetState := c.FleetState(c.g.Fleets[fi].ID)
		if onPlanet, ok := fleetState.AtPlanet(); (ok && onPlanet != c.ShipGroup(sgi).Destination) || fleetState.State != game.StateInOrbit {
			return e.NewShipsNotOnSamePlanetError("fleet: %s", fleetName)
		}
	}

	c.internalShipGroupJoinFleet(sgi, c.g.Fleets[fi].ID)

	if oldFleetID != nil {
		keepOldFleet := false
		for sg := range c.listShipGroups(ri) {
			if sg.FleetID != nil && *sg.FleetID == *oldFleetID {
				keepOldFleet = true
				break
			}
		}
		if !keepOldFleet {
			oldFleetIndex, ok := c.FleetIndex(*oldFleetID)
			if !ok {
				return e.NewGameStateError("old fleet index not found by ID=%v", *oldFleetID)
			}
			if err := c.deleteFleet(ri, c.g.Fleets[oldFleetIndex].Name); err != nil {
				return err
			}
		}
	}

	return nil
}

func (c *Cache) fleetMerge(ri int, fleetSourceName, fleetTargetName string) (err error) {
	fiSource, ok := c.fleetIndex(ri, fleetSourceName)
	if !ok {
		return e.NewEntityNotExistsError("source fleet %s", fleetSourceName)
	}
	fiTarget, ok := c.fleetIndex(ri, fleetTargetName)
	if !ok {
		return e.NewEntityNotExistsError("target fleet %s", fleetTargetName)
	}
	stateSrc := c.FleetState(c.g.Fleets[fiSource].ID)
	stateDst := c.FleetState(c.g.Fleets[fiTarget].ID)
	if !stateSrc.AtSamePlanet(stateDst) {
		return e.NewShipsNotOnSamePlanetError()
	}

	for sg := range c.listShipGroups(ri) {
		if sg.FleetID != nil && *sg.FleetID == c.g.Fleets[fiSource].ID {
			sg.FleetID = &c.g.Fleets[fiTarget].ID
		}
	}
	return c.deleteFleet(ri, fleetSourceName)
}

func (c *Cache) createFleet(ri int, name string) (int, error) {
	c.validateRaceIndex(ri)
	n, ok := util.ValidateTypeName(name)
	if !ok {
		return 0, e.NewEntityTypeNameValidationError("%q", n)
	}
	if _, ok := c.fleetIndex(ri, n); ok {
		return 0, e.NewEntityDuplicateIdentifierError("fleet %q", n)
	}
	fleets := slices.Clone(c.g.Fleets)
	fleets = append(fleets, game.Fleet{
		ID:      uuid.New(),
		OwnerID: c.g.Race[ri].ID,
		Name:    n,
	})
	c.g.Fleets = fleets
	i := len(c.g.Fleets) - 1
	if c.cacheFleetIndexByID != nil {
		c.cacheFleetIndexByID[c.g.Fleets[i].ID] = i
	}
	return i, nil
}

func (c *Cache) deleteFleet(ri int, name string) error {
	fi, ok := c.fleetIndex(ri, name)
	if !ok {
		return e.NewEntityNotExistsError("fleet %s", name)
	}
	for sg := range c.listShipGroups(ri) {
		if sg.FleetID != nil && *(sg.FleetID) == c.g.Fleets[fi].ID {
			return e.NewEntityInUseError("fleet %s: race %s, group #%d", name, c.g.Race[ri].Name, sg.Number)
		}
	}
	c.unsafeDeleteFleet(fi)
	return nil
}

func (c *Cache) unsafeDeleteFleet(fi int) {
	c.validateFleetIndex(fi)
	c.g.Fleets = append(c.g.Fleets[:fi], c.g.Fleets[fi+1:]...)
	c.invalidateFleetCache()
}

// Internal funcs

func (c *Cache) FleetIndex(ID uuid.UUID) (int, bool) {
	if len(c.cacheFleetIndexByID) == 0 {
		c.cacheFleetIndex()
	}
	if v, ok := c.cacheFleetIndexByID[ID]; ok {
		return v, true
	} else {
		return -1, false
	}
}

func (c *Cache) cacheFleetIndex() {
	if c.cacheFleetIndexByID != nil {
		clear(c.cacheFleetIndexByID)
	} else {
		c.cacheFleetIndexByID = make(map[uuid.UUID]int)
	}
	for i := range c.g.Fleets {
		c.cacheFleetIndexByID[c.g.Fleets[i].ID] = i
	}
}

func (c *Cache) MustFleetIndex(ID uuid.UUID) int {
	if v, ok := c.FleetIndex(ID); ok {
		return v
	} else {
		panic(fmt.Sprintf("fleet not found by ID=%v", ID))
	}
}

func (c *Cache) FleetGroupIdx(ri, fi int) iter.Seq[int] {
	c.validateRaceIndex(ri)
	c.validateFleetIndex(fi)
	return func(yield func(int) bool) {
		for sgi := range c.listShipGroupIdx(ri) {
			sg := c.ShipGroup(sgi)
			if sg.FleetID != nil && *sg.FleetID == c.g.Fleets[fi].ID {
				if !yield(sgi) {
					break
				}
			}
		}
	}
}

func (c *Cache) fleetGroupIds(ri, fi int) iter.Seq[int] {
	c.validateRaceIndex(ri)
	c.validateFleetIndex(fi)
	return func(yield func(int) bool) {
		for i := range c.ShipGroupsIndex() {
			sg := c.ShipGroup(i)
			if c.g.Race[ri].ID != sg.OwnerID {
				continue
			}
			if sg.FleetID == nil || c.MustFleetIndex(*sg.FleetID) != fi {
				continue
			}
			if !yield(i) {
				return
			}
		}
	}
}

func (c *Cache) listFleets(ri int) iter.Seq[*game.Fleet] {
	c.validateRaceIndex(ri)
	return func(yield func(*game.Fleet) bool) {
		for i := range c.g.Fleets {
			if c.g.Fleets[i].OwnerID == c.g.Race[ri].ID {
				if !yield(&c.g.Fleets[i]) {
					return
				}
			}
		}
	}
}

func (c *Cache) fleetIndex(ri int, name string) (int, bool) {
	c.validateRaceIndex(ri)
	if i := slices.IndexFunc(c.g.Fleets, func(f game.Fleet) bool { return f.OwnerID == c.g.Race[ri].ID && f.Name == name }); i < 0 {
		return -1, false
	} else {
		return i, true
	}
}

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