package controller

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

	"galaxy/calc"
	"galaxy/game/internal/model/game"

	"github.com/google/uuid"
)

type Battle struct {
	ID             uuid.UUID
	Planet         uint
	ObserverGroups map[int]bool // True = In_Battle, False = Out_Battle
	InitialNumbers map[int]uint // Initial number of ships in the group
	Protocol       []BattleAction

	shipAmmo map[int]uint
	attacker map[int]map[int]float64 // a group able to attack and destroy an opponent with some probability > 0
}

type BattleAction struct {
	Attacker  int
	Defender  int
	Destroyed bool
}

func CollectPlanetGroups(c *Cache) map[uint]map[int]bool {
	planetGroup := make(map[uint]map[int]bool)
	for groupIndex := range c.ShipGroupsIndex() {
		state := c.ShipGroup(groupIndex).State()
		if state == game.StateInOrbit || state == game.StateUpgrade {
			planetNumber := c.ShipGroup(groupIndex).Destination
			if _, ok := planetGroup[planetNumber]; !ok {
				planetGroup[planetNumber] = make(map[int]bool)
			}
			planetGroup[planetNumber][groupIndex] = false
		}
	}
	for pl := range planetGroup {
		if len(planetGroup[pl]) < 2 {
			delete(planetGroup, pl)
		}
	}
	return planetGroup
}

func FilterBattleGroups(c *Cache, groups map[int]bool) []int {
	return slices.DeleteFunc(slices.Collect(maps.Keys(groups)), func(groupIndex int) bool { return c.ShipGroup(groupIndex).State() != game.StateInOrbit })
}

func FilterBattleOpponents(c *Cache, attIdx, defIdx int, cacheProbability map[int]map[int]float64) bool {
	// Same Race's groups can't attack themselves
	if attIdx == defIdx || c.ShipGroupOwnerRaceIndex(attIdx) == c.ShipGroupOwnerRaceIndex(defIdx) {
		return true
	}

	// If any opponent has War relation to another, both will stay in battle
	if c.Relation(c.ShipGroupOwnerRaceIndex(attIdx), c.ShipGroupOwnerRaceIndex(defIdx)) == game.RelationPeace &&
		c.Relation(c.ShipGroupOwnerRaceIndex(defIdx), c.ShipGroupOwnerRaceIndex(attIdx)) == game.RelationPeace {
		return true
	}

	p := calc.DestructionProbability(
		c.ShipGroupShipClass(attIdx).Weapons.F(),
		c.ShipGroup(attIdx).TechLevel(game.TechWeapons).F(),
		c.ShipGroupShipClass(defIdx).Shields.F(),
		c.ShipGroup(defIdx).TechLevel(game.TechShields).F(),
		c.ShipGroup(defIdx).FullMass(c.ShipGroupShipClass(defIdx)),
	)
	// Exclude opponent's group which cannot be probably destroyed
	if p <= 0 {
		return true
	}

	if _, ok := cacheProbability[attIdx]; !ok {
		cacheProbability[attIdx] = make(map[int]float64)
	}
	cacheProbability[attIdx][defIdx] = p

	return false

}

func ProduceBattles(c *Cache) []*Battle {
	cacheProbability := make(map[int]map[int]float64)
	defer func() { clear(cacheProbability) }()

	planetGroups := CollectPlanetGroups(c)
	if len(planetGroups) == 0 {
		return nil
	}

	result := make([]*Battle, 0)

	// Multiple battles on single planet shoul be produced as single battle:
	// A <--> B
	// C <--> D
	// where: [A] and [B] are mutual enemies, as well [C] and [D]
	for pl, observerGroups := range planetGroups {
		battleGroups := FilterBattleGroups(c, observerGroups)
		b := &Battle{
			Planet:         pl,
			ObserverGroups: observerGroups,
			InitialNumbers: make(map[int]uint),
			attacker:       make(map[int]map[int]float64),
			shipAmmo:       make(map[int]uint),
		}
		for sgi := range observerGroups {
			b.InitialNumbers[sgi] = c.ShipGroup(sgi).Number
		}

		for i := range battleGroups {
			attIdx := battleGroups[i]

			// Ships with no Ammo will never attack somebody
			if c.ShipGroupShipClass(attIdx).Armament == 0 {
				continue
			}

			opponents := slices.DeleteFunc(slices.Clone(battleGroups), func(defIdx int) bool {
				return FilterBattleOpponents(c, attIdx, defIdx, cacheProbability)
			})
			if len(opponents) > 0 {
				b.shipAmmo[attIdx] = c.ShipGroupShipClass(attIdx).Armament
				b.ObserverGroups[attIdx] = true
				for _, defIdx := range opponents {
					if _, ok := b.attacker[attIdx][defIdx]; !ok {
						b.attacker[attIdx] = make(map[int]float64)
					}
					b.attacker[attIdx][defIdx] = cacheProbability[attIdx][defIdx]
					b.ObserverGroups[defIdx] = true
				}
			}
		}

		if len(b.attacker) > 0 {
			SingleBattle(c, b)
			b.ID = uuid.New()
			result = append(result, b)
		}

		clear(b.attacker)
		clear(b.shipAmmo)
	}

	return result
}

func SingleBattle(c *Cache, b *Battle) {
	roundShooters := make(map[int]bool)
	for len(b.attacker) > 0 {
		// список участников раунда
		clear(roundShooters)
		for sgi := range b.attacker {
			roundShooters[sgi] = true
		}

		for len(roundShooters) > 0 {
			// attacke group id among round participants
			attIdx := randomValue(maps.Keys(roundShooters))
			delete(roundShooters, attIdx)

			for range b.shipAmmo[attIdx] {
				// defender group id among all attacker's opponents
				defIdx := randomValue(maps.Keys(b.attacker[attIdx]))

				destroyed := false

				probability := b.attacker[attIdx][defIdx]
				switch {
				case probability >= 1:
					destroyed = true
				case probability > 0:
					destroyed = rand.Float64() >= probability
				default:
					panic("SingleBattle: probability unexpected: value <= 0")
				}

				b.Protocol = append(b.Protocol, BattleAction{
					Attacker:  attIdx,
					Defender:  defIdx,
					Destroyed: destroyed,
				})

				if destroyed {
					c.ShipGroupDestroyItem(defIdx)
				}
				if c.ShipGroup(defIdx).Number == 0 {
					// Eliminated group cant attack anyone
					delete(b.attacker, defIdx)
					delete(roundShooters, defIdx)

					for attIdx := range b.attacker {
						// Other attackers can't attack eliminated group anymore
						delete(b.attacker[attIdx], defIdx)

						if len(b.attacker[attIdx]) == 0 {
							// Remove attacker if he lost all opponents
							delete(b.attacker, attIdx)
							delete(roundShooters, attIdx)
						}
					}
				}

				// When attacker has no more targets to shoot - break its ammo cycle
				if len(b.attacker[attIdx]) == 0 {
					break
				}
			}
		}
	}
}

func randomValue(v iter.Seq[int]) int {
	ids := slices.Collect(v)
	return ids[rand.IntN(len(ids))]
}
