package controller

import (
	"math"
	"slices"
	"strings"

	"galaxy/calc"
	e "galaxy/error"

	"galaxy/game/internal/model/game"

	"github.com/google/uuid"
)

func (c *Cache) shipGroupUpgrade(ri int, groupID uuid.UUID, techInput string, limitLevel float64) error {
	c.validateRaceIndex(ri)
	sgi, ok := c.raceShipGroupIndex(ri, groupID)
	if !ok {
		return e.NewEntityNotExistsError("group %s", groupID)
	}
	st := c.ShipGroupShipClass(sgi)
	sg := c.ShipGroup(sgi)

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

	p := c.MustPlanet(sg.Destination)
	if p.Owned() && !p.OwnedBy(c.g.Race[ri].ID) {
		return e.NewEntityNotOwnedError("planet #%d for upgrade group %s", p.Number, groupID)
	}

	upgradeValidTech := map[string]game.Tech{
		strings.ToLower(game.TechDrive.String()):   game.TechDrive,
		strings.ToLower(game.TechWeapons.String()): game.TechWeapons,
		strings.ToLower(game.TechShields.String()): game.TechShields,
		strings.ToLower(game.TechCargo.String()):   game.TechCargo,
		strings.ToLower(game.TechAll.String()):     game.TechAll,
	}

	techRequest, ok := upgradeValidTech[strings.ToLower(techInput)]
	if !ok {
		return e.NewTechUnknownError(techInput)
	}

	var blockMasses map[game.Tech]float64 = map[game.Tech]float64{
		game.TechDrive:   st.DriveBlockMass(),
		game.TechWeapons: st.WeaponsBlockMass(),
		game.TechShields: st.ShieldsBlockMass(),
		game.TechCargo:   st.CargoBlockMass(),
	}

	switch {
	case techRequest != game.TechAll && blockMasses[techRequest] == 0:
		return e.NewUpgradeShipTechNotUsedError()
	case techRequest == game.TechAll && limitLevel != 0:
		return e.NewUpgradeParameterNotAllowedError("tech=%s max_level=%f", techRequest.String(), limitLevel)
	}

	targetLevel := make(map[game.Tech]float64)
	var sumLevels float64
	for _, tech := range []game.Tech{game.TechDrive, game.TechWeapons, game.TechShields, game.TechCargo} {
		if techRequest == game.TechAll || tech == techRequest {
			if c.g.Race[ri].TechLevel(tech) < limitLevel {
				return e.NewUpgradeTechLevelInsufficientError("%s=%.03f < %.03f", tech.String(), c.g.Race[ri].TechLevel(tech), limitLevel)
			}
			targetLevel[tech] = FutureUpgradeLevel(c.g.Race[ri].TechLevel(tech), sg.TechLevel(tech).F(), limitLevel)
		} else {
			targetLevel[tech] = CurrentUpgradingLevel(sg, tech)
		}
		sumLevels += targetLevel[tech]
	}

	productionCapacity := c.PlanetProductionCapacity(p.Number)
	uc := GroupUpgradeCost(sg, *st, targetLevel[game.TechDrive], targetLevel[game.TechWeapons], targetLevel[game.TechShields], targetLevel[game.TechCargo])
	costForShip := uc.UpgradeCost(1)
	if costForShip == 0 {
		return e.NewUpgradeShipsAlreadyUpToDateError("%#v", targetLevel)
	}

	shipsToUpgrade := sg.Number
	maxUpgradableShips := uc.UpgradeMaxShips(productionCapacity)

	/*
		1. считаем стоимость модернизации одного корабля
		2. считаем сколько кораблей можно модернизировать
		3. если не хватает даже на 1 корабль, ограничиваемся одним кораблём и пересчитываем коэффициент пропорционально массе блоков
		4. иначе, считаем истинное количество кораблей с учётом ограничения maxShips
	*/
	blockMassSum := st.EmptyMass()

	coef := productionCapacity / costForShip
	if maxUpgradableShips == 0 {
		if limitLevel > 0 {
			return e.NewUpgradeInsufficientResourcesError("ship cost=%.03f L=%.03f", costForShip, productionCapacity)
		}
		sumLevels = sumLevels * coef
		for tech := range targetLevel {
			if blockMasses[tech] > 0 {
				proportional := sumLevels * (blockMasses[tech] / blockMassSum)
				targetLevel[tech] = proportional
			}
		}
		maxUpgradableShips = 1
	} else if maxUpgradableShips > shipsToUpgrade {
		maxUpgradableShips = shipsToUpgrade
	}

	// sanity check
	uc = GroupUpgradeCost(sg, *st, targetLevel[game.TechDrive], targetLevel[game.TechWeapons], targetLevel[game.TechShields], targetLevel[game.TechCargo])
	costForGroup := uc.UpgradeCost(maxUpgradableShips)
	if costForGroup > productionCapacity {
		e.NewGameStateError("cost recalculation: coef=%f cost(%d)=%f L=%f", coef, maxUpgradableShips, costForGroup, productionCapacity)
	}

	// break group if needed
	if maxUpgradableShips < sg.Number {
		nsgi, err := c.breakGroup(ri, groupID, maxUpgradableShips)
		if err != nil {
			return err
		}
		sgi = nsgi
	}

	// finally, fill group upgrade prefs
	for tech := range targetLevel {
		if targetLevel[tech] > 0 {
			c.UpgradeShipGroup(sgi, tech, targetLevel[tech])
		}
	}

	return nil
}

func (c *Cache) UpgradeShipGroup(sgi int, tech game.Tech, v float64) {
	sg := *(c.ShipGroup(sgi))
	st := c.ShipGroupShipClass(sgi)
	c.g.ShipGroups[sgi] = UpgradeGroupPreference(sg, *st, tech, v)
}

// helpers

type UpgradeCalc struct {
	Cost map[game.Tech]float64
}

func (uc UpgradeCalc) UpgradeCost(ships uint) float64 {
	var sum float64
	for _, v := range uc.Cost {
		sum += v
	}
	return sum * float64(ships)
}

func (uc UpgradeCalc) UpgradeMaxShips(resources float64) uint {
	return uint(math.Floor(resources / uc.UpgradeCost(1)))
}

func GroupUpgradeCost(sg *game.ShipGroup, st game.ShipType, drive, weapons, shields, cargo float64) UpgradeCalc {
	uc := &UpgradeCalc{Cost: make(map[game.Tech]float64)}
	if drive > 0 {
		uc.Cost[game.TechDrive] = calc.BlockUpgradeCost(st.DriveBlockMass(), sg.TechLevel(game.TechDrive).F(), drive)
	}
	if weapons > 0 {
		uc.Cost[game.TechWeapons] = calc.BlockUpgradeCost(st.WeaponsBlockMass(), sg.TechLevel(game.TechWeapons).F(), weapons)
	}
	if shields > 0 {
		uc.Cost[game.TechShields] = calc.BlockUpgradeCost(st.ShieldsBlockMass(), sg.TechLevel(game.TechShields).F(), shields)
	}
	if cargo > 0 {
		uc.Cost[game.TechCargo] = calc.BlockUpgradeCost(st.CargoBlockMass(), sg.TechLevel(game.TechCargo).F(), cargo)
	}
	return *uc
}

func CurrentUpgradingLevel(sg *game.ShipGroup, tech game.Tech) float64 {
	if sg.StateUpgrade == nil {
		return 0
	}
	ti := slices.IndexFunc(sg.StateUpgrade.UpgradeTech, func(pref game.UpgradePreference) bool { return pref.Tech == tech })
	if ti >= 0 {
		return sg.StateUpgrade.UpgradeTech[ti].Level.F()
	}
	return 0
}

func FutureUpgradeLevel(raceLevel, groupLevel, limit float64) float64 {
	target := limit
	if target == 0 || target > raceLevel {
		target = raceLevel
	}
	if groupLevel == target {
		return 0
	}
	return target
}

func UpgradeGroupPreference(sg game.ShipGroup, st game.ShipType, tech game.Tech, v float64) game.ShipGroup {
	if v <= 0 || st.BlockMass(tech) == 0 || sg.TechLevel(tech).F() >= v {
		return sg
	}
	var su game.InUpgrade
	if sg.StateUpgrade != nil {
		su = *sg.StateUpgrade
	} else {
		su = game.InUpgrade{UpgradeTech: []game.UpgradePreference{}}
	}
	ti := slices.IndexFunc(su.UpgradeTech, func(pref game.UpgradePreference) bool { return pref.Tech == tech })
	if ti < 0 {
		su.UpgradeTech = append(su.UpgradeTech, game.UpgradePreference{Tech: tech})
		ti = len(su.UpgradeTech) - 1
	}
	su.UpgradeTech[ti].Level = game.F(v)
	su.UpgradeTech[ti].Cost = game.F(calc.BlockUpgradeCost(st.BlockMass(tech), sg.TechLevel(tech).F(), v) * float64(sg.Number))

	sg.StateUpgrade = &su
	return sg
}
