package transcoder

import (
	"errors"
	"fmt"
	"sort"

	model "galaxy/model/report"
	commonfbs "galaxy/schema/fbs/common"
	fbs "galaxy/schema/fbs/report"

	flatbuffers "github.com/google/flatbuffers/go"
)

// ReportToPayload converts model.Report from the internal representation to
// FlatBuffers bytes that can be sent over network transports.
//
// The function returns an error when the input is nil.
func ReportToPayload(report *model.Report) ([]byte, error) {
	if report == nil {
		return nil, errors.New("encode report payload: report is nil")
	}

	builder := flatbuffers.NewBuilder(4096)

	race := builder.CreateString(report.Race)
	voteFor := builder.CreateString(report.VoteFor)

	playerOffsets := make([]flatbuffers.UOffsetT, len(report.Player))
	for i := range report.Player {
		playerOffsets[i] = encodeReportPlayer(builder, &report.Player[i])
	}

	localScienceOffsets := make([]flatbuffers.UOffsetT, len(report.LocalScience))
	for i := range report.LocalScience {
		localScienceOffsets[i] = encodeReportScience(builder, &report.LocalScience[i])
	}

	otherScienceOffsets := make([]flatbuffers.UOffsetT, len(report.OtherScience))
	for i := range report.OtherScience {
		otherScienceOffsets[i] = encodeReportOtherScience(builder, &report.OtherScience[i])
	}

	localShipClassOffsets := make([]flatbuffers.UOffsetT, len(report.LocalShipClass))
	for i := range report.LocalShipClass {
		localShipClassOffsets[i] = encodeReportShipClass(builder, &report.LocalShipClass[i])
	}

	otherShipClassOffsets := make([]flatbuffers.UOffsetT, len(report.OtherShipClass))
	for i := range report.OtherShipClass {
		otherShipClassOffsets[i] = encodeReportOthersShipClass(builder, &report.OtherShipClass[i])
	}

	bombingOffsets := make([]flatbuffers.UOffsetT, len(report.Bombing))
	for i := range report.Bombing {
		if report.Bombing[i] == nil {
			return nil, fmt.Errorf("encode report bombing %d: bombing is nil", i)
		}
		bombingOffsets[i] = encodeReportBombing(builder, report.Bombing[i])
	}

	incomingGroupOffsets := make([]flatbuffers.UOffsetT, len(report.IncomingGroup))
	for i := range report.IncomingGroup {
		incomingGroupOffsets[i] = encodeReportIncomingGroup(builder, &report.IncomingGroup[i])
	}

	localPlanetOffsets := make([]flatbuffers.UOffsetT, len(report.LocalPlanet))
	for i := range report.LocalPlanet {
		localPlanetOffsets[i] = encodeReportLocalPlanet(builder, &report.LocalPlanet[i])
	}

	shipProductionOffsets := make([]flatbuffers.UOffsetT, len(report.ShipProduction))
	for i := range report.ShipProduction {
		shipProductionOffsets[i] = encodeReportShipProduction(builder, &report.ShipProduction[i])
	}

	routeOffsets := make([]flatbuffers.UOffsetT, len(report.Route))
	for i := range report.Route {
		routeOffsets[i] = encodeReportRoute(builder, &report.Route[i])
	}

	otherPlanetOffsets := make([]flatbuffers.UOffsetT, len(report.OtherPlanet))
	for i := range report.OtherPlanet {
		otherPlanetOffsets[i] = encodeReportOtherPlanet(builder, &report.OtherPlanet[i])
	}

	uninhabitedPlanetOffsets := make([]flatbuffers.UOffsetT, len(report.UninhabitedPlanet))
	for i := range report.UninhabitedPlanet {
		uninhabitedPlanetOffsets[i] = encodeReportUninhabitedPlanet(builder, &report.UninhabitedPlanet[i])
	}

	unidentifiedPlanetOffsets := make([]flatbuffers.UOffsetT, len(report.UnidentifiedPlanet))
	for i := range report.UnidentifiedPlanet {
		unidentifiedPlanetOffsets[i] = encodeReportUnidentifiedPlanet(builder, &report.UnidentifiedPlanet[i])
	}

	localFleetOffsets := make([]flatbuffers.UOffsetT, len(report.LocalFleet))
	for i := range report.LocalFleet {
		localFleetOffsets[i] = encodeReportLocalFleet(builder, &report.LocalFleet[i])
	}

	localGroupOffsets := make([]flatbuffers.UOffsetT, len(report.LocalGroup))
	for i := range report.LocalGroup {
		localGroupOffsets[i] = encodeReportLocalGroup(builder, &report.LocalGroup[i])
	}

	otherGroupOffsets := make([]flatbuffers.UOffsetT, len(report.OtherGroup))
	for i := range report.OtherGroup {
		otherGroupOffsets[i] = encodeReportOtherGroup(builder, &report.OtherGroup[i])
	}

	unidentifiedGroupOffsets := make([]flatbuffers.UOffsetT, len(report.UnidentifiedGroup))
	for i := range report.UnidentifiedGroup {
		unidentifiedGroupOffsets[i] = encodeReportUnidentifiedGroup(builder, &report.UnidentifiedGroup[i])
	}

	playerVector := encodeReportOffsetVector(builder, len(playerOffsets), fbs.ReportStartPlayerVector, playerOffsets)
	localScienceVector := encodeReportOffsetVector(builder, len(localScienceOffsets), fbs.ReportStartLocalScienceVector, localScienceOffsets)
	otherScienceVector := encodeReportOffsetVector(builder, len(otherScienceOffsets), fbs.ReportStartOtherScienceVector, otherScienceOffsets)
	localShipClassVector := encodeReportOffsetVector(builder, len(localShipClassOffsets), fbs.ReportStartLocalShipClassVector, localShipClassOffsets)
	otherShipClassVector := encodeReportOffsetVector(builder, len(otherShipClassOffsets), fbs.ReportStartOtherShipClassVector, otherShipClassOffsets)
	battleVector := encodeReportBattleSummaries(builder, report.Battle)
	bombingVector := encodeReportOffsetVector(builder, len(bombingOffsets), fbs.ReportStartBombingVector, bombingOffsets)
	incomingGroupVector := encodeReportOffsetVector(builder, len(incomingGroupOffsets), fbs.ReportStartIncomingGroupVector, incomingGroupOffsets)
	localPlanetVector := encodeReportOffsetVector(builder, len(localPlanetOffsets), fbs.ReportStartLocalPlanetVector, localPlanetOffsets)
	shipProductionVector := encodeReportOffsetVector(builder, len(shipProductionOffsets), fbs.ReportStartShipProductionVector, shipProductionOffsets)
	routeVector := encodeReportOffsetVector(builder, len(routeOffsets), fbs.ReportStartRouteVector, routeOffsets)
	otherPlanetVector := encodeReportOffsetVector(builder, len(otherPlanetOffsets), fbs.ReportStartOtherPlanetVector, otherPlanetOffsets)
	uninhabitedPlanetVector := encodeReportOffsetVector(builder, len(uninhabitedPlanetOffsets), fbs.ReportStartUninhabitedPlanetVector, uninhabitedPlanetOffsets)
	unidentifiedPlanetVector := encodeReportOffsetVector(builder, len(unidentifiedPlanetOffsets), fbs.ReportStartUnidentifiedPlanetVector, unidentifiedPlanetOffsets)
	localFleetVector := encodeReportOffsetVector(builder, len(localFleetOffsets), fbs.ReportStartLocalFleetVector, localFleetOffsets)
	localGroupVector := encodeReportOffsetVector(builder, len(localGroupOffsets), fbs.ReportStartLocalGroupVector, localGroupOffsets)
	otherGroupVector := encodeReportOffsetVector(builder, len(otherGroupOffsets), fbs.ReportStartOtherGroupVector, otherGroupOffsets)
	unidentifiedGroupVector := encodeReportOffsetVector(builder, len(unidentifiedGroupOffsets), fbs.ReportStartUnidentifiedGroupVector, unidentifiedGroupOffsets)

	fbs.ReportStart(builder)
	fbs.ReportAddVersion(builder, uint64(report.Version))
	fbs.ReportAddTurn(builder, uint64(report.Turn))
	fbs.ReportAddWidth(builder, report.Width)
	fbs.ReportAddHeight(builder, report.Height)
	fbs.ReportAddPlanetCount(builder, report.PlanetCount)
	fbs.ReportAddRace(builder, race)
	fbs.ReportAddVotes(builder, reportFloatToFBS(report.Votes))
	fbs.ReportAddVoteFor(builder, voteFor)
	if len(playerOffsets) > 0 {
		fbs.ReportAddPlayer(builder, playerVector)
	}
	if len(localScienceOffsets) > 0 {
		fbs.ReportAddLocalScience(builder, localScienceVector)
	}
	if len(otherScienceOffsets) > 0 {
		fbs.ReportAddOtherScience(builder, otherScienceVector)
	}
	if len(localShipClassOffsets) > 0 {
		fbs.ReportAddLocalShipClass(builder, localShipClassVector)
	}
	if len(otherShipClassOffsets) > 0 {
		fbs.ReportAddOtherShipClass(builder, otherShipClassVector)
	}
	if len(report.Battle) > 0 {
		fbs.ReportAddBattle(builder, battleVector)
	}
	if len(bombingOffsets) > 0 {
		fbs.ReportAddBombing(builder, bombingVector)
	}
	if len(incomingGroupOffsets) > 0 {
		fbs.ReportAddIncomingGroup(builder, incomingGroupVector)
	}
	if len(localPlanetOffsets) > 0 {
		fbs.ReportAddLocalPlanet(builder, localPlanetVector)
	}
	if len(shipProductionOffsets) > 0 {
		fbs.ReportAddShipProduction(builder, shipProductionVector)
	}
	if len(routeOffsets) > 0 {
		fbs.ReportAddRoute(builder, routeVector)
	}
	if len(otherPlanetOffsets) > 0 {
		fbs.ReportAddOtherPlanet(builder, otherPlanetVector)
	}
	if len(uninhabitedPlanetOffsets) > 0 {
		fbs.ReportAddUninhabitedPlanet(builder, uninhabitedPlanetVector)
	}
	if len(unidentifiedPlanetOffsets) > 0 {
		fbs.ReportAddUnidentifiedPlanet(builder, unidentifiedPlanetVector)
	}
	if len(localFleetOffsets) > 0 {
		fbs.ReportAddLocalFleet(builder, localFleetVector)
	}
	if len(localGroupOffsets) > 0 {
		fbs.ReportAddLocalGroup(builder, localGroupVector)
	}
	if len(otherGroupOffsets) > 0 {
		fbs.ReportAddOtherGroup(builder, otherGroupVector)
	}
	if len(unidentifiedGroupOffsets) > 0 {
		fbs.ReportAddUnidentifiedGroup(builder, unidentifiedGroupVector)
	}

	reportOffset := fbs.ReportEnd(builder)
	fbs.FinishReportBuffer(builder, reportOffset)

	return builder.FinishedBytes(), nil
}

// PayloadToReport converts FlatBuffers payload bytes into model.Report.
//
// The function validates payload structure and integer conversions.
// Malformed payloads are returned as errors.
func PayloadToReport(data []byte) (result *model.Report, err error) {
	if len(data) == 0 {
		return nil, errors.New("decode report payload: data is empty")
	}

	defer func() {
		if recovered := recover(); recovered != nil {
			result = nil
			err = fmt.Errorf("decode report payload: panic recovered: %v", recovered)
		}
	}()

	flatReport := fbs.GetRootAsReport(data, 0)
	version, err := uint64ToUint(flatReport.Version(), "version")
	if err != nil {
		return nil, fmt.Errorf("decode report payload: %w", err)
	}
	turn, err := uint64ToUint(flatReport.Turn(), "turn")
	if err != nil {
		return nil, fmt.Errorf("decode report payload: %w", err)
	}

	result = &model.Report{
		Version:     version,
		Turn:        turn,
		Width:       flatReport.Width(),
		Height:      flatReport.Height(),
		PlanetCount: flatReport.PlanetCount(),
		Race:        string(flatReport.Race()),
		Votes:       reportFloatFromFBS(flatReport.Votes()),
		VoteFor:     string(flatReport.VoteFor()),
	}

	if err := decodeReportPlayerVector(flatReport, result); err != nil {
		return nil, err
	}
	if err := decodeReportLocalScienceVector(flatReport, result); err != nil {
		return nil, err
	}
	if err := decodeReportOtherScienceVector(flatReport, result); err != nil {
		return nil, err
	}
	if err := decodeReportLocalShipClassVector(flatReport, result); err != nil {
		return nil, err
	}
	if err := decodeReportOtherShipClassVector(flatReport, result); err != nil {
		return nil, err
	}
	if err := decodeReportBattleVector(flatReport, result); err != nil {
		return nil, err
	}
	if err := decodeReportBombingVector(flatReport, result); err != nil {
		return nil, err
	}
	if err := decodeReportIncomingGroupVector(flatReport, result); err != nil {
		return nil, err
	}
	if err := decodeReportLocalPlanetVector(flatReport, result); err != nil {
		return nil, err
	}
	if err := decodeReportShipProductionVector(flatReport, result); err != nil {
		return nil, err
	}
	if err := decodeReportRouteVector(flatReport, result); err != nil {
		return nil, err
	}
	if err := decodeReportOtherPlanetVector(flatReport, result); err != nil {
		return nil, err
	}
	if err := decodeReportUninhabitedPlanetVector(flatReport, result); err != nil {
		return nil, err
	}
	if err := decodeReportUnidentifiedPlanetVector(flatReport, result); err != nil {
		return nil, err
	}
	if err := decodeReportLocalFleetVector(flatReport, result); err != nil {
		return nil, err
	}
	if err := decodeReportLocalGroupVector(flatReport, result); err != nil {
		return nil, err
	}
	if err := decodeReportOtherGroupVector(flatReport, result); err != nil {
		return nil, err
	}
	if err := decodeReportUnidentifiedGroupVector(flatReport, result); err != nil {
		return nil, err
	}

	return result, nil
}

func encodeReportPlayer(builder *flatbuffers.Builder, player *model.Player) flatbuffers.UOffsetT {
	name := builder.CreateString(player.Name)
	relation := builder.CreateString(player.Relation)

	fbs.PlayerStart(builder)
	fbs.PlayerAddName(builder, name)
	fbs.PlayerAddDrive(builder, reportFloatToFBS(player.Drive))
	fbs.PlayerAddWeapons(builder, reportFloatToFBS(player.Weapons))
	fbs.PlayerAddShields(builder, reportFloatToFBS(player.Shields))
	fbs.PlayerAddCargo(builder, reportFloatToFBS(player.Cargo))
	fbs.PlayerAddPopulation(builder, reportFloatToFBS(player.Population))
	fbs.PlayerAddIndustry(builder, reportFloatToFBS(player.Industry))
	fbs.PlayerAddPlanets(builder, player.Planets)
	fbs.PlayerAddRelation(builder, relation)
	fbs.PlayerAddVotes(builder, reportFloatToFBS(player.Votes))
	fbs.PlayerAddExtinct(builder, player.Extinct)
	return fbs.PlayerEnd(builder)
}

func encodeReportScience(builder *flatbuffers.Builder, science *model.Science) flatbuffers.UOffsetT {
	name := builder.CreateString(science.Name)
	fbs.ScienceStart(builder)
	fbs.ScienceAddName(builder, name)
	fbs.ScienceAddDrive(builder, reportFloatToFBS(science.Drive))
	fbs.ScienceAddWeapons(builder, reportFloatToFBS(science.Weapons))
	fbs.ScienceAddShields(builder, reportFloatToFBS(science.Shields))
	fbs.ScienceAddCargo(builder, reportFloatToFBS(science.Cargo))
	return fbs.ScienceEnd(builder)
}

func encodeReportOtherScience(builder *flatbuffers.Builder, science *model.OtherScience) flatbuffers.UOffsetT {
	race := builder.CreateString(science.Race)
	name := builder.CreateString(science.Name)
	fbs.OtherScienceStart(builder)
	fbs.OtherScienceAddRace(builder, race)
	fbs.OtherScienceAddName(builder, name)
	fbs.OtherScienceAddDrive(builder, reportFloatToFBS(science.Drive))
	fbs.OtherScienceAddWeapons(builder, reportFloatToFBS(science.Weapons))
	fbs.OtherScienceAddShields(builder, reportFloatToFBS(science.Shields))
	fbs.OtherScienceAddCargo(builder, reportFloatToFBS(science.Cargo))
	return fbs.OtherScienceEnd(builder)
}

func encodeReportShipClass(builder *flatbuffers.Builder, shipClass *model.ShipClass) flatbuffers.UOffsetT {
	name := builder.CreateString(shipClass.Name)
	fbs.ShipClassStart(builder)
	fbs.ShipClassAddName(builder, name)
	fbs.ShipClassAddDrive(builder, reportFloatToFBS(shipClass.Drive))
	fbs.ShipClassAddArmament(builder, uint64(shipClass.Armament))
	fbs.ShipClassAddWeapons(builder, reportFloatToFBS(shipClass.Weapons))
	fbs.ShipClassAddShields(builder, reportFloatToFBS(shipClass.Shields))
	fbs.ShipClassAddCargo(builder, reportFloatToFBS(shipClass.Cargo))
	fbs.ShipClassAddMass(builder, reportFloatToFBS(shipClass.Mass))
	return fbs.ShipClassEnd(builder)
}

func encodeReportOthersShipClass(builder *flatbuffers.Builder, shipClass *model.OthersShipClass) flatbuffers.UOffsetT {
	race := builder.CreateString(shipClass.Race)
	name := builder.CreateString(shipClass.Name)
	fbs.OthersShipClassStart(builder)
	fbs.OthersShipClassAddRace(builder, race)
	fbs.OthersShipClassAddName(builder, name)
	fbs.OthersShipClassAddDrive(builder, reportFloatToFBS(shipClass.Drive))
	fbs.OthersShipClassAddArmament(builder, uint64(shipClass.Armament))
	fbs.OthersShipClassAddWeapons(builder, reportFloatToFBS(shipClass.Weapons))
	fbs.OthersShipClassAddShields(builder, reportFloatToFBS(shipClass.Shields))
	fbs.OthersShipClassAddCargo(builder, reportFloatToFBS(shipClass.Cargo))
	fbs.OthersShipClassAddMass(builder, reportFloatToFBS(shipClass.Mass))
	return fbs.OthersShipClassEnd(builder)
}

func encodeReportBombing(builder *flatbuffers.Builder, bombing *model.Bombing) flatbuffers.UOffsetT {
	planet := builder.CreateString(bombing.Planet)
	owner := builder.CreateString(bombing.Owner)
	attacker := builder.CreateString(bombing.Attacker)
	production := builder.CreateString(bombing.Production)

	fbs.BombingStart(builder)
	fbs.BombingAddNumber(builder, uint64(bombing.Number))
	fbs.BombingAddPlanet(builder, planet)
	fbs.BombingAddOwner(builder, owner)
	fbs.BombingAddAttacker(builder, attacker)
	fbs.BombingAddProduction(builder, production)
	fbs.BombingAddIndustry(builder, reportFloatToFBS(bombing.Industry))
	fbs.BombingAddPopulation(builder, reportFloatToFBS(bombing.Population))
	fbs.BombingAddColonists(builder, reportFloatToFBS(bombing.Colonists))
	fbs.BombingAddCapital(builder, reportFloatToFBS(bombing.Capital))
	fbs.BombingAddMaterial(builder, reportFloatToFBS(bombing.Material))
	fbs.BombingAddAttackPower(builder, reportFloatToFBS(bombing.AttackPower))
	fbs.BombingAddWiped(builder, bombing.Wiped)
	return fbs.BombingEnd(builder)
}

func encodeReportIncomingGroup(builder *flatbuffers.Builder, group *model.IncomingGroup) flatbuffers.UOffsetT {
	fbs.IncomingGroupStart(builder)
	fbs.IncomingGroupAddOrigin(builder, uint64(group.Origin))
	fbs.IncomingGroupAddDestination(builder, uint64(group.Destination))
	fbs.IncomingGroupAddDistance(builder, reportFloatToFBS(group.Distance))
	fbs.IncomingGroupAddSpeed(builder, reportFloatToFBS(group.Speed))
	fbs.IncomingGroupAddMass(builder, reportFloatToFBS(group.Mass))
	return fbs.IncomingGroupEnd(builder)
}

func encodeReportLocalPlanet(builder *flatbuffers.Builder, planet *model.LocalPlanet) flatbuffers.UOffsetT {
	name := builder.CreateString(planet.Name)
	production := builder.CreateString(planet.Production)

	fbs.LocalPlanetStart(builder)
	fbs.LocalPlanetAddX(builder, reportFloatToFBS(planet.X))
	fbs.LocalPlanetAddY(builder, reportFloatToFBS(planet.Y))
	fbs.LocalPlanetAddNumber(builder, uint64(planet.Number))
	fbs.LocalPlanetAddSize(builder, reportFloatToFBS(planet.Size))
	fbs.LocalPlanetAddName(builder, name)
	fbs.LocalPlanetAddResources(builder, reportFloatToFBS(planet.Resources))
	fbs.LocalPlanetAddCapital(builder, reportFloatToFBS(planet.Capital))
	fbs.LocalPlanetAddMaterial(builder, reportFloatToFBS(planet.Material))
	fbs.LocalPlanetAddIndustry(builder, reportFloatToFBS(planet.Industry))
	fbs.LocalPlanetAddPopulation(builder, reportFloatToFBS(planet.Population))
	fbs.LocalPlanetAddColonists(builder, reportFloatToFBS(planet.Colonists))
	fbs.LocalPlanetAddProduction(builder, production)
	fbs.LocalPlanetAddFreeIndustry(builder, reportFloatToFBS(planet.FreeIndustry))
	return fbs.LocalPlanetEnd(builder)
}

func encodeReportShipProduction(builder *flatbuffers.Builder, production *model.ShipProduction) flatbuffers.UOffsetT {
	class := builder.CreateString(production.Class)
	fbs.ShipProductionStart(builder)
	fbs.ShipProductionAddPlanet(builder, uint64(production.Planet))
	fbs.ShipProductionAddClass(builder, class)
	fbs.ShipProductionAddCost(builder, reportFloatToFBS(production.Cost))
	fbs.ShipProductionAddProdUsed(builder, reportFloatToFBS(production.ProdUsed))
	fbs.ShipProductionAddPercent(builder, reportFloatToFBS(production.Percent))
	fbs.ShipProductionAddFree(builder, reportFloatToFBS(production.Free))
	return fbs.ShipProductionEnd(builder)
}

func encodeReportRoute(builder *flatbuffers.Builder, route *model.Route) flatbuffers.UOffsetT {
	routeEntries := encodeReportRouteEntryVector(builder, route.Route)

	fbs.RouteStart(builder)
	fbs.RouteAddPlanet(builder, uint64(route.Planet))
	if routeEntries != 0 {
		fbs.RouteAddRoute(builder, routeEntries)
	}
	return fbs.RouteEnd(builder)
}

func encodeReportOtherPlanet(builder *flatbuffers.Builder, planet *model.OtherPlanet) flatbuffers.UOffsetT {
	owner := builder.CreateString(planet.Owner)
	name := builder.CreateString(planet.Name)
	production := builder.CreateString(planet.Production)

	fbs.OtherPlanetStart(builder)
	fbs.OtherPlanetAddOwner(builder, owner)
	fbs.OtherPlanetAddX(builder, reportFloatToFBS(planet.X))
	fbs.OtherPlanetAddY(builder, reportFloatToFBS(planet.Y))
	fbs.OtherPlanetAddNumber(builder, uint64(planet.Number))
	fbs.OtherPlanetAddSize(builder, reportFloatToFBS(planet.Size))
	fbs.OtherPlanetAddName(builder, name)
	fbs.OtherPlanetAddResources(builder, reportFloatToFBS(planet.Resources))
	fbs.OtherPlanetAddCapital(builder, reportFloatToFBS(planet.Capital))
	fbs.OtherPlanetAddMaterial(builder, reportFloatToFBS(planet.Material))
	fbs.OtherPlanetAddIndustry(builder, reportFloatToFBS(planet.Industry))
	fbs.OtherPlanetAddPopulation(builder, reportFloatToFBS(planet.Population))
	fbs.OtherPlanetAddColonists(builder, reportFloatToFBS(planet.Colonists))
	fbs.OtherPlanetAddProduction(builder, production)
	fbs.OtherPlanetAddFreeIndustry(builder, reportFloatToFBS(planet.FreeIndustry))
	return fbs.OtherPlanetEnd(builder)
}

func encodeReportUninhabitedPlanet(builder *flatbuffers.Builder, planet *model.UninhabitedPlanet) flatbuffers.UOffsetT {
	name := builder.CreateString(planet.Name)
	fbs.UninhabitedPlanetStart(builder)
	fbs.UninhabitedPlanetAddX(builder, reportFloatToFBS(planet.X))
	fbs.UninhabitedPlanetAddY(builder, reportFloatToFBS(planet.Y))
	fbs.UninhabitedPlanetAddNumber(builder, uint64(planet.Number))
	fbs.UninhabitedPlanetAddSize(builder, reportFloatToFBS(planet.Size))
	fbs.UninhabitedPlanetAddName(builder, name)
	fbs.UninhabitedPlanetAddResources(builder, reportFloatToFBS(planet.Resources))
	fbs.UninhabitedPlanetAddCapital(builder, reportFloatToFBS(planet.Capital))
	fbs.UninhabitedPlanetAddMaterial(builder, reportFloatToFBS(planet.Material))
	return fbs.UninhabitedPlanetEnd(builder)
}

func encodeReportUnidentifiedPlanet(builder *flatbuffers.Builder, planet *model.UnidentifiedPlanet) flatbuffers.UOffsetT {
	fbs.UnidentifiedPlanetStart(builder)
	fbs.UnidentifiedPlanetAddX(builder, reportFloatToFBS(planet.X))
	fbs.UnidentifiedPlanetAddY(builder, reportFloatToFBS(planet.Y))
	fbs.UnidentifiedPlanetAddNumber(builder, uint64(planet.Number))
	return fbs.UnidentifiedPlanetEnd(builder)
}

func encodeReportLocalFleet(builder *flatbuffers.Builder, fleet *model.LocalFleet) flatbuffers.UOffsetT {
	name := builder.CreateString(fleet.Name)
	state := builder.CreateString(fleet.State)

	fbs.LocalFleetStart(builder)
	fbs.LocalFleetAddName(builder, name)
	fbs.LocalFleetAddGroups(builder, uint64(fleet.Groups))
	fbs.LocalFleetAddDestination(builder, uint64(fleet.Destination))
	if fleet.Origin != nil {
		fbs.LocalFleetAddOrigin(builder, uint64(*fleet.Origin))
	}
	if fleet.Range != nil {
		fbs.LocalFleetAddRange(builder, reportFloatToFBS(*fleet.Range))
	}
	fbs.LocalFleetAddSpeed(builder, reportFloatToFBS(fleet.Speed))
	fbs.LocalFleetAddState(builder, state)
	return fbs.LocalFleetEnd(builder)
}

func encodeReportLocalGroup(builder *flatbuffers.Builder, group *model.LocalGroup) flatbuffers.UOffsetT {
	class := builder.CreateString(group.Class)
	cargo := builder.CreateString(group.Cargo)
	state := builder.CreateString(group.State)

	tech := encodeReportTechEntryVector(builder, group.Tech)
	var fleet flatbuffers.UOffsetT
	if group.Fleet != nil {
		fleet = builder.CreateString(*group.Fleet)
	}

	idHi, idLo := uuidToHiLo(group.ID)

	fbs.LocalGroupStart(builder)
	fbs.LocalGroupAddNumber(builder, uint64(group.Number))
	fbs.LocalGroupAddClass(builder, class)
	if tech != 0 {
		fbs.LocalGroupAddTech(builder, tech)
	}
	fbs.LocalGroupAddCargo(builder, cargo)
	fbs.LocalGroupAddLoad(builder, reportFloatToFBS(group.Load))
	fbs.LocalGroupAddDestination(builder, uint64(group.Destination))
	if group.Origin != nil {
		fbs.LocalGroupAddOrigin(builder, uint64(*group.Origin))
	}
	if group.Range != nil {
		fbs.LocalGroupAddRange(builder, reportFloatToFBS(*group.Range))
	}
	fbs.LocalGroupAddSpeed(builder, reportFloatToFBS(group.Speed))
	fbs.LocalGroupAddMass(builder, reportFloatToFBS(group.Mass))
	fbs.LocalGroupAddId(builder, commonfbs.CreateUUID(builder, idHi, idLo))
	fbs.LocalGroupAddState(builder, state)
	if group.Fleet != nil {
		fbs.LocalGroupAddFleet(builder, fleet)
	}
	return fbs.LocalGroupEnd(builder)
}

func encodeReportOtherGroup(builder *flatbuffers.Builder, group *model.OtherGroup) flatbuffers.UOffsetT {
	class := builder.CreateString(group.Class)
	cargo := builder.CreateString(group.Cargo)
	tech := encodeReportTechEntryVector(builder, group.Tech)

	fbs.OtherGroupStart(builder)
	fbs.OtherGroupAddNumber(builder, uint64(group.Number))
	fbs.OtherGroupAddClass(builder, class)
	if tech != 0 {
		fbs.OtherGroupAddTech(builder, tech)
	}
	fbs.OtherGroupAddCargo(builder, cargo)
	fbs.OtherGroupAddLoad(builder, reportFloatToFBS(group.Load))
	fbs.OtherGroupAddDestination(builder, uint64(group.Destination))
	if group.Origin != nil {
		fbs.OtherGroupAddOrigin(builder, uint64(*group.Origin))
	}
	if group.Range != nil {
		fbs.OtherGroupAddRange(builder, reportFloatToFBS(*group.Range))
	}
	fbs.OtherGroupAddSpeed(builder, reportFloatToFBS(group.Speed))
	fbs.OtherGroupAddMass(builder, reportFloatToFBS(group.Mass))
	return fbs.OtherGroupEnd(builder)
}

func encodeReportUnidentifiedGroup(builder *flatbuffers.Builder, group *model.UnidentifiedGroup) flatbuffers.UOffsetT {
	fbs.UnidentifiedGroupStart(builder)
	fbs.UnidentifiedGroupAddX(builder, reportFloatToFBS(group.X))
	fbs.UnidentifiedGroupAddY(builder, reportFloatToFBS(group.Y))
	return fbs.UnidentifiedGroupEnd(builder)
}

func decodeReportPlayerVector(flatReport *fbs.Report, result *model.Report) error {
	length := flatReport.PlayerLength()
	if length == 0 {
		return nil
	}

	result.Player = make([]model.Player, length)
	item := new(fbs.Player)
	for i := 0; i < length; i++ {
		if !flatReport.Player(item, i) {
			return fmt.Errorf("decode report player %d: player is missing", i)
		}
		result.Player[i] = model.Player{
			Name:       string(item.Name()),
			Drive:      reportFloatFromFBS(item.Drive()),
			Weapons:    reportFloatFromFBS(item.Weapons()),
			Shields:    reportFloatFromFBS(item.Shields()),
			Cargo:      reportFloatFromFBS(item.Cargo()),
			Population: reportFloatFromFBS(item.Population()),
			Industry:   reportFloatFromFBS(item.Industry()),
			Planets:    item.Planets(),
			Relation:   string(item.Relation()),
			Votes:      reportFloatFromFBS(item.Votes()),
			Extinct:    item.Extinct(),
		}
	}

	return nil
}

func decodeReportLocalScienceVector(flatReport *fbs.Report, result *model.Report) error {
	length := flatReport.LocalScienceLength()
	if length == 0 {
		return nil
	}

	result.LocalScience = make([]model.Science, length)
	item := new(fbs.Science)
	for i := 0; i < length; i++ {
		if !flatReport.LocalScience(item, i) {
			return fmt.Errorf("decode report local science %d: science is missing", i)
		}
		result.LocalScience[i] = model.Science{
			Name:    string(item.Name()),
			Drive:   reportFloatFromFBS(item.Drive()),
			Weapons: reportFloatFromFBS(item.Weapons()),
			Shields: reportFloatFromFBS(item.Shields()),
			Cargo:   reportFloatFromFBS(item.Cargo()),
		}
	}

	return nil
}

func decodeReportOtherScienceVector(flatReport *fbs.Report, result *model.Report) error {
	length := flatReport.OtherScienceLength()
	if length == 0 {
		return nil
	}

	result.OtherScience = make([]model.OtherScience, length)
	item := new(fbs.OtherScience)
	for i := 0; i < length; i++ {
		if !flatReport.OtherScience(item, i) {
			return fmt.Errorf("decode report other science %d: science is missing", i)
		}
		result.OtherScience[i] = model.OtherScience{
			Race: string(item.Race()),
			Science: model.Science{
				Name:    string(item.Name()),
				Drive:   reportFloatFromFBS(item.Drive()),
				Weapons: reportFloatFromFBS(item.Weapons()),
				Shields: reportFloatFromFBS(item.Shields()),
				Cargo:   reportFloatFromFBS(item.Cargo()),
			},
		}
	}

	return nil
}

func decodeReportLocalShipClassVector(flatReport *fbs.Report, result *model.Report) error {
	length := flatReport.LocalShipClassLength()
	if length == 0 {
		return nil
	}

	result.LocalShipClass = make([]model.ShipClass, length)
	item := new(fbs.ShipClass)
	for i := 0; i < length; i++ {
		if !flatReport.LocalShipClass(item, i) {
			return fmt.Errorf("decode report local ship class %d: ship class is missing", i)
		}

		armament, err := uint64ToUint(item.Armament(), "armament")
		if err != nil {
			return fmt.Errorf("decode report local ship class %d: %w", i, err)
		}

		result.LocalShipClass[i] = model.ShipClass{
			Name:     string(item.Name()),
			Drive:    reportFloatFromFBS(item.Drive()),
			Armament: armament,
			Weapons:  reportFloatFromFBS(item.Weapons()),
			Shields:  reportFloatFromFBS(item.Shields()),
			Cargo:    reportFloatFromFBS(item.Cargo()),
			Mass:     reportFloatFromFBS(item.Mass()),
		}
	}

	return nil
}

func decodeReportOtherShipClassVector(flatReport *fbs.Report, result *model.Report) error {
	length := flatReport.OtherShipClassLength()
	if length == 0 {
		return nil
	}

	result.OtherShipClass = make([]model.OthersShipClass, length)
	item := new(fbs.OthersShipClass)
	for i := 0; i < length; i++ {
		if !flatReport.OtherShipClass(item, i) {
			return fmt.Errorf("decode report other ship class %d: ship class is missing", i)
		}

		armament, err := uint64ToUint(item.Armament(), "armament")
		if err != nil {
			return fmt.Errorf("decode report other ship class %d: %w", i, err)
		}

		result.OtherShipClass[i] = model.OthersShipClass{
			Race: string(item.Race()),
			ShipClass: model.ShipClass{
				Name:     string(item.Name()),
				Drive:    reportFloatFromFBS(item.Drive()),
				Armament: armament,
				Weapons:  reportFloatFromFBS(item.Weapons()),
				Shields:  reportFloatFromFBS(item.Shields()),
				Cargo:    reportFloatFromFBS(item.Cargo()),
				Mass:     reportFloatFromFBS(item.Mass()),
			},
		}
	}

	return nil
}

func decodeReportBattleVector(flatReport *fbs.Report, result *model.Report) error {
	length := flatReport.BattleLength()
	if length == 0 {
		return nil
	}

	result.Battle = make([]model.BattleSummary, length)
	item := new(fbs.BattleSummary)
	idHolder := new(commonfbs.UUID)
	for i := 0; i < length; i++ {
		if !flatReport.Battle(item, i) {
			return fmt.Errorf("decode report battle %d: battle is missing", i)
		}
		if item.Id(idHolder) == nil {
			return fmt.Errorf("decode report battle %d: battle id is missing", i)
		}
		planet, err := uint64ToUint(item.Planet(), "planet")
		if err != nil {
			return fmt.Errorf("decode report battle %d: %w", i, err)
		}
		shots, err := uint64ToUint(item.Shots(), "shots")
		if err != nil {
			return fmt.Errorf("decode report battle %d: %w", i, err)
		}
		result.Battle[i] = model.BattleSummary{
			ID:     uuidFromHiLo(idHolder.Hi(), idHolder.Lo()),
			Planet: planet,
			Shots:  shots,
		}
	}

	return nil
}

func decodeReportBombingVector(flatReport *fbs.Report, result *model.Report) error {
	length := flatReport.BombingLength()
	if length == 0 {
		return nil
	}

	result.Bombing = make([]*model.Bombing, length)
	item := new(fbs.Bombing)
	for i := 0; i < length; i++ {
		if !flatReport.Bombing(item, i) {
			return fmt.Errorf("decode report bombing %d: bombing is missing", i)
		}

		number, err := uint64ToUint(item.Number(), "number")
		if err != nil {
			return fmt.Errorf("decode report bombing %d: %w", i, err)
		}

		result.Bombing[i] = &model.Bombing{
			Number:      number,
			Planet:      string(item.Planet()),
			Owner:       string(item.Owner()),
			Attacker:    string(item.Attacker()),
			Production:  string(item.Production()),
			Industry:    reportFloatFromFBS(item.Industry()),
			Population:  reportFloatFromFBS(item.Population()),
			Colonists:   reportFloatFromFBS(item.Colonists()),
			Capital:     reportFloatFromFBS(item.Capital()),
			Material:    reportFloatFromFBS(item.Material()),
			AttackPower: reportFloatFromFBS(item.AttackPower()),
			Wiped:       item.Wiped(),
		}
	}

	return nil
}

func decodeReportIncomingGroupVector(flatReport *fbs.Report, result *model.Report) error {
	length := flatReport.IncomingGroupLength()
	if length == 0 {
		return nil
	}

	result.IncomingGroup = make([]model.IncomingGroup, length)
	item := new(fbs.IncomingGroup)
	for i := 0; i < length; i++ {
		if !flatReport.IncomingGroup(item, i) {
			return fmt.Errorf("decode report incoming group %d: group is missing", i)
		}

		origin, err := uint64ToUint(item.Origin(), "origin")
		if err != nil {
			return fmt.Errorf("decode report incoming group %d: %w", i, err)
		}
		destination, err := uint64ToUint(item.Destination(), "destination")
		if err != nil {
			return fmt.Errorf("decode report incoming group %d: %w", i, err)
		}

		result.IncomingGroup[i] = model.IncomingGroup{
			Origin:      origin,
			Destination: destination,
			Distance:    reportFloatFromFBS(item.Distance()),
			Speed:       reportFloatFromFBS(item.Speed()),
			Mass:        reportFloatFromFBS(item.Mass()),
		}
	}

	return nil
}

func decodeReportLocalPlanetVector(flatReport *fbs.Report, result *model.Report) error {
	length := flatReport.LocalPlanetLength()
	if length == 0 {
		return nil
	}

	result.LocalPlanet = make([]model.LocalPlanet, length)
	item := new(fbs.LocalPlanet)
	for i := 0; i < length; i++ {
		if !flatReport.LocalPlanet(item, i) {
			return fmt.Errorf("decode report local planet %d: planet is missing", i)
		}

		number, err := uint64ToUint(item.Number(), "number")
		if err != nil {
			return fmt.Errorf("decode report local planet %d: %w", i, err)
		}

		result.LocalPlanet[i] = model.LocalPlanet{
			UninhabitedPlanet: model.UninhabitedPlanet{
				UnidentifiedPlanet: model.UnidentifiedPlanet{
					X:      reportFloatFromFBS(item.X()),
					Y:      reportFloatFromFBS(item.Y()),
					Number: number,
				},
				Size:      reportFloatFromFBS(item.Size()),
				Name:      string(item.Name()),
				Resources: reportFloatFromFBS(item.Resources()),
				Capital:   reportFloatFromFBS(item.Capital()),
				Material:  reportFloatFromFBS(item.Material()),
			},
			Industry:     reportFloatFromFBS(item.Industry()),
			Population:   reportFloatFromFBS(item.Population()),
			Colonists:    reportFloatFromFBS(item.Colonists()),
			Production:   string(item.Production()),
			FreeIndustry: reportFloatFromFBS(item.FreeIndustry()),
		}
	}

	return nil
}

func decodeReportShipProductionVector(flatReport *fbs.Report, result *model.Report) error {
	length := flatReport.ShipProductionLength()
	if length == 0 {
		return nil
	}

	result.ShipProduction = make([]model.ShipProduction, length)
	item := new(fbs.ShipProduction)
	for i := 0; i < length; i++ {
		if !flatReport.ShipProduction(item, i) {
			return fmt.Errorf("decode report ship production %d: production is missing", i)
		}

		planet, err := uint64ToUint(item.Planet(), "planet")
		if err != nil {
			return fmt.Errorf("decode report ship production %d: %w", i, err)
		}

		result.ShipProduction[i] = model.ShipProduction{
			Planet:   planet,
			Class:    string(item.Class()),
			Cost:     reportFloatFromFBS(item.Cost()),
			ProdUsed: reportFloatFromFBS(item.ProdUsed()),
			Percent:  reportFloatFromFBS(item.Percent()),
			Free:     reportFloatFromFBS(item.Free()),
		}
	}

	return nil
}

func decodeReportRouteVector(flatReport *fbs.Report, result *model.Report) error {
	length := flatReport.RouteLength()
	if length == 0 {
		return nil
	}

	result.Route = make([]model.Route, length)
	item := new(fbs.Route)
	for i := 0; i < length; i++ {
		if !flatReport.Route(item, i) {
			return fmt.Errorf("decode report route %d: route is missing", i)
		}

		planet, err := uint64ToUint(item.Planet(), "planet")
		if err != nil {
			return fmt.Errorf("decode report route %d: %w", i, err)
		}

		routeMap, err := decodeReportRouteMap(item, i)
		if err != nil {
			return err
		}

		result.Route[i] = model.Route{
			Planet: planet,
			Route:  routeMap,
		}
	}

	return nil
}

func decodeReportOtherPlanetVector(flatReport *fbs.Report, result *model.Report) error {
	length := flatReport.OtherPlanetLength()
	if length == 0 {
		return nil
	}

	result.OtherPlanet = make([]model.OtherPlanet, length)
	item := new(fbs.OtherPlanet)
	for i := 0; i < length; i++ {
		if !flatReport.OtherPlanet(item, i) {
			return fmt.Errorf("decode report other planet %d: planet is missing", i)
		}

		number, err := uint64ToUint(item.Number(), "number")
		if err != nil {
			return fmt.Errorf("decode report other planet %d: %w", i, err)
		}

		result.OtherPlanet[i] = model.OtherPlanet{
			Owner: string(item.Owner()),
			LocalPlanet: model.LocalPlanet{
				UninhabitedPlanet: model.UninhabitedPlanet{
					UnidentifiedPlanet: model.UnidentifiedPlanet{
						X:      reportFloatFromFBS(item.X()),
						Y:      reportFloatFromFBS(item.Y()),
						Number: number,
					},
					Size:      reportFloatFromFBS(item.Size()),
					Name:      string(item.Name()),
					Resources: reportFloatFromFBS(item.Resources()),
					Capital:   reportFloatFromFBS(item.Capital()),
					Material:  reportFloatFromFBS(item.Material()),
				},
				Industry:     reportFloatFromFBS(item.Industry()),
				Population:   reportFloatFromFBS(item.Population()),
				Colonists:    reportFloatFromFBS(item.Colonists()),
				Production:   string(item.Production()),
				FreeIndustry: reportFloatFromFBS(item.FreeIndustry()),
			},
		}
	}

	return nil
}

func decodeReportUninhabitedPlanetVector(flatReport *fbs.Report, result *model.Report) error {
	length := flatReport.UninhabitedPlanetLength()
	if length == 0 {
		return nil
	}

	result.UninhabitedPlanet = make([]model.UninhabitedPlanet, length)
	item := new(fbs.UninhabitedPlanet)
	for i := 0; i < length; i++ {
		if !flatReport.UninhabitedPlanet(item, i) {
			return fmt.Errorf("decode report uninhabited planet %d: planet is missing", i)
		}

		number, err := uint64ToUint(item.Number(), "number")
		if err != nil {
			return fmt.Errorf("decode report uninhabited planet %d: %w", i, err)
		}

		result.UninhabitedPlanet[i] = model.UninhabitedPlanet{
			UnidentifiedPlanet: model.UnidentifiedPlanet{
				X:      reportFloatFromFBS(item.X()),
				Y:      reportFloatFromFBS(item.Y()),
				Number: number,
			},
			Size:      reportFloatFromFBS(item.Size()),
			Name:      string(item.Name()),
			Resources: reportFloatFromFBS(item.Resources()),
			Capital:   reportFloatFromFBS(item.Capital()),
			Material:  reportFloatFromFBS(item.Material()),
		}
	}

	return nil
}

func decodeReportUnidentifiedPlanetVector(flatReport *fbs.Report, result *model.Report) error {
	length := flatReport.UnidentifiedPlanetLength()
	if length == 0 {
		return nil
	}

	result.UnidentifiedPlanet = make([]model.UnidentifiedPlanet, length)
	item := new(fbs.UnidentifiedPlanet)
	for i := 0; i < length; i++ {
		if !flatReport.UnidentifiedPlanet(item, i) {
			return fmt.Errorf("decode report unidentified planet %d: planet is missing", i)
		}

		number, err := uint64ToUint(item.Number(), "number")
		if err != nil {
			return fmt.Errorf("decode report unidentified planet %d: %w", i, err)
		}

		result.UnidentifiedPlanet[i] = model.UnidentifiedPlanet{
			X:      reportFloatFromFBS(item.X()),
			Y:      reportFloatFromFBS(item.Y()),
			Number: number,
		}
	}

	return nil
}

func decodeReportLocalFleetVector(flatReport *fbs.Report, result *model.Report) error {
	length := flatReport.LocalFleetLength()
	if length == 0 {
		return nil
	}

	result.LocalFleet = make([]model.LocalFleet, length)
	item := new(fbs.LocalFleet)
	for i := 0; i < length; i++ {
		if !flatReport.LocalFleet(item, i) {
			return fmt.Errorf("decode report local fleet %d: fleet is missing", i)
		}

		groups, err := uint64ToUint(item.Groups(), "groups")
		if err != nil {
			return fmt.Errorf("decode report local fleet %d: %w", i, err)
		}
		destination, err := uint64ToUint(item.Destination(), "destination")
		if err != nil {
			return fmt.Errorf("decode report local fleet %d: %w", i, err)
		}

		decoded := model.LocalFleet{
			Name:        string(item.Name()),
			Groups:      groups,
			Destination: destination,
			Speed:       reportFloatFromFBS(item.Speed()),
			State:       string(item.State()),
		}

		if origin := item.Origin(); origin != nil {
			decodedOrigin, err := uint64ToUint(*origin, "origin")
			if err != nil {
				return fmt.Errorf("decode report local fleet %d: %w", i, err)
			}
			decoded.Origin = &decodedOrigin
		}
		if range_ := item.Range(); range_ != nil {
			decodedRange := reportFloatFromFBS(*range_)
			decoded.Range = &decodedRange
		}

		result.LocalFleet[i] = decoded
	}

	return nil
}

func decodeReportLocalGroupVector(flatReport *fbs.Report, result *model.Report) error {
	length := flatReport.LocalGroupLength()
	if length == 0 {
		return nil
	}

	result.LocalGroup = make([]model.LocalGroup, length)
	item := new(fbs.LocalGroup)
	for i := 0; i < length; i++ {
		if !flatReport.LocalGroup(item, i) {
			return fmt.Errorf("decode report local group %d: group is missing", i)
		}

		number, err := uint64ToUint(item.Number(), "number")
		if err != nil {
			return fmt.Errorf("decode report local group %d: %w", i, err)
		}
		destination, err := uint64ToUint(item.Destination(), "destination")
		if err != nil {
			return fmt.Errorf("decode report local group %d: %w", i, err)
		}
		tech, err := decodeReportTechMapFromLocalGroup(item, i)
		if err != nil {
			return err
		}

		id := item.Id(nil)
		if id == nil {
			return fmt.Errorf("decode report local group %d: id is missing", i)
		}

		decoded := model.LocalGroup{
			OtherGroup: model.OtherGroup{
				Number:      number,
				Class:       string(item.Class()),
				Tech:        tech,
				Cargo:       string(item.Cargo()),
				Load:        reportFloatFromFBS(item.Load()),
				Destination: destination,
				Speed:       reportFloatFromFBS(item.Speed()),
				Mass:        reportFloatFromFBS(item.Mass()),
			},
			ID:    uuidFromHiLo(id.Hi(), id.Lo()),
			State: string(item.State()),
		}

		if origin := item.Origin(); origin != nil {
			decodedOrigin, err := uint64ToUint(*origin, "origin")
			if err != nil {
				return fmt.Errorf("decode report local group %d: %w", i, err)
			}
			decoded.Origin = &decodedOrigin
		}
		if range_ := item.Range(); range_ != nil {
			decodedRange := reportFloatFromFBS(*range_)
			decoded.Range = &decodedRange
		}
		if fleet := item.Fleet(); fleet != nil {
			decodedFleet := string(fleet)
			decoded.Fleet = &decodedFleet
		}

		result.LocalGroup[i] = decoded
	}

	return nil
}

func decodeReportOtherGroupVector(flatReport *fbs.Report, result *model.Report) error {
	length := flatReport.OtherGroupLength()
	if length == 0 {
		return nil
	}

	result.OtherGroup = make([]model.OtherGroup, length)
	item := new(fbs.OtherGroup)
	for i := 0; i < length; i++ {
		if !flatReport.OtherGroup(item, i) {
			return fmt.Errorf("decode report other group %d: group is missing", i)
		}

		number, err := uint64ToUint(item.Number(), "number")
		if err != nil {
			return fmt.Errorf("decode report other group %d: %w", i, err)
		}
		destination, err := uint64ToUint(item.Destination(), "destination")
		if err != nil {
			return fmt.Errorf("decode report other group %d: %w", i, err)
		}
		tech, err := decodeReportTechMapFromOtherGroup(item, i)
		if err != nil {
			return err
		}

		decoded := model.OtherGroup{
			Number:      number,
			Class:       string(item.Class()),
			Tech:        tech,
			Cargo:       string(item.Cargo()),
			Load:        reportFloatFromFBS(item.Load()),
			Destination: destination,
			Speed:       reportFloatFromFBS(item.Speed()),
			Mass:        reportFloatFromFBS(item.Mass()),
		}

		if origin := item.Origin(); origin != nil {
			decodedOrigin, err := uint64ToUint(*origin, "origin")
			if err != nil {
				return fmt.Errorf("decode report other group %d: %w", i, err)
			}
			decoded.Origin = &decodedOrigin
		}
		if range_ := item.Range(); range_ != nil {
			decodedRange := reportFloatFromFBS(*range_)
			decoded.Range = &decodedRange
		}

		result.OtherGroup[i] = decoded
	}

	return nil
}

func decodeReportUnidentifiedGroupVector(flatReport *fbs.Report, result *model.Report) error {
	length := flatReport.UnidentifiedGroupLength()
	if length == 0 {
		return nil
	}

	result.UnidentifiedGroup = make([]model.UnidentifiedGroup, length)
	item := new(fbs.UnidentifiedGroup)
	for i := 0; i < length; i++ {
		if !flatReport.UnidentifiedGroup(item, i) {
			return fmt.Errorf("decode report unidentified group %d: group is missing", i)
		}

		result.UnidentifiedGroup[i] = model.UnidentifiedGroup{
			X: reportFloatFromFBS(item.X()),
			Y: reportFloatFromFBS(item.Y()),
		}
	}

	return nil
}

func decodeReportRouteMap(flatRoute *fbs.Route, routeIndex int) (map[uint]string, error) {
	length := flatRoute.RouteLength()
	if length == 0 {
		return nil, nil
	}

	result := make(map[uint]string, length)
	item := new(fbs.RouteEntry)
	for i := 0; i < length; i++ {
		if !flatRoute.Route(item, i) {
			return nil, fmt.Errorf("decode report route %d entry %d: route entry is missing", routeIndex, i)
		}

		key, err := uint64ToUint(item.Key(), "route key")
		if err != nil {
			return nil, fmt.Errorf("decode report route %d entry %d: %w", routeIndex, i, err)
		}
		result[key] = string(item.Value())
	}

	return result, nil
}

func decodeReportTechMapFromOtherGroup(group *fbs.OtherGroup, groupIndex int) (map[string]model.Float, error) {
	length := group.TechLength()
	if length == 0 {
		return nil, nil
	}

	result := make(map[string]model.Float, length)
	item := new(fbs.TechEntry)
	for i := 0; i < length; i++ {
		if !group.Tech(item, i) {
			return nil, fmt.Errorf("decode report other group %d tech entry %d: tech entry is missing", groupIndex, i)
		}
		result[string(item.Key())] = reportFloatFromFBS(item.Value())
	}

	return result, nil
}

func decodeReportTechMapFromLocalGroup(group *fbs.LocalGroup, groupIndex int) (map[string]model.Float, error) {
	length := group.TechLength()
	if length == 0 {
		return nil, nil
	}

	result := make(map[string]model.Float, length)
	item := new(fbs.TechEntry)
	for i := 0; i < length; i++ {
		if !group.Tech(item, i) {
			return nil, fmt.Errorf("decode report local group %d tech entry %d: tech entry is missing", groupIndex, i)
		}
		result[string(item.Key())] = reportFloatFromFBS(item.Value())
	}

	return result, nil
}

func encodeReportOffsetVector(
	builder *flatbuffers.Builder,
	length int,
	startVector func(*flatbuffers.Builder, int) flatbuffers.UOffsetT,
	offsets []flatbuffers.UOffsetT,
) flatbuffers.UOffsetT {
	if length == 0 {
		return 0
	}

	startVector(builder, length)
	for i := length - 1; i >= 0; i-- {
		builder.PrependUOffsetT(offsets[i])
	}
	return builder.EndVector(length)
}

func encodeReportBattleSummaries(builder *flatbuffers.Builder, summaries []model.BattleSummary) flatbuffers.UOffsetT {
	if len(summaries) == 0 {
		return 0
	}

	offsets := make([]flatbuffers.UOffsetT, len(summaries))
	for i := range summaries {
		hi, lo := uuidToHiLo(summaries[i].ID)
		fbs.BattleSummaryStart(builder)
		fbs.BattleSummaryAddId(builder, commonfbs.CreateUUID(builder, hi, lo))
		fbs.BattleSummaryAddPlanet(builder, uint64(summaries[i].Planet))
		fbs.BattleSummaryAddShots(builder, uint64(summaries[i].Shots))
		offsets[i] = fbs.BattleSummaryEnd(builder)
	}

	fbs.ReportStartBattleVector(builder, len(offsets))
	for i := len(offsets) - 1; i >= 0; i-- {
		builder.PrependUOffsetT(offsets[i])
	}
	return builder.EndVector(len(offsets))
}

func encodeReportRouteEntryVector(builder *flatbuffers.Builder, route map[uint]string) flatbuffers.UOffsetT {
	if len(route) == 0 {
		return 0
	}

	keys := make([]uint, 0, len(route))
	for key := range route {
		keys = append(keys, key)
	}
	sort.Slice(keys, func(i, j int) bool { return keys[i] < keys[j] })

	offsets := make([]flatbuffers.UOffsetT, len(keys))
	for i, key := range keys {
		value := builder.CreateString(route[key])
		fbs.RouteEntryStart(builder)
		fbs.RouteEntryAddKey(builder, uint64(key))
		fbs.RouteEntryAddValue(builder, value)
		offsets[i] = fbs.RouteEntryEnd(builder)
	}

	fbs.RouteStartRouteVector(builder, len(offsets))
	for i := len(offsets) - 1; i >= 0; i-- {
		builder.PrependUOffsetT(offsets[i])
	}
	return builder.EndVector(len(offsets))
}

func encodeReportTechEntryVector(builder *flatbuffers.Builder, tech map[string]model.Float) flatbuffers.UOffsetT {
	if len(tech) == 0 {
		return 0
	}

	keys := make([]string, 0, len(tech))
	for key := range tech {
		keys = append(keys, key)
	}
	sort.Strings(keys)

	offsets := make([]flatbuffers.UOffsetT, len(keys))
	for i, key := range keys {
		encodedKey := builder.CreateString(key)
		fbs.TechEntryStart(builder)
		fbs.TechEntryAddKey(builder, encodedKey)
		fbs.TechEntryAddValue(builder, reportFloatToFBS(tech[key]))
		offsets[i] = fbs.TechEntryEnd(builder)
	}

	fbs.OtherGroupStartTechVector(builder, len(offsets))
	for i := len(offsets) - 1; i >= 0; i-- {
		builder.PrependUOffsetT(offsets[i])
	}
	return builder.EndVector(len(offsets))
}

func reportFloatToFBS(value model.Float) float32 {
	return float32(value.F())
}

func reportFloatFromFBS(value float32) model.Float {
	return model.Float(float64(value))
}

func uint64ToUint(value uint64, field string) (uint, error) {
	maxUint := uint64(^uint(0))
	if value > maxUint {
		return 0, fmt.Errorf("%s value %d overflows uint", field, value)
	}
	return uint(value), nil
}

// GameReportRequestToPayload converts model.GameReportRequest to
// FlatBuffers bytes suitable for the authenticated gateway transport.
func GameReportRequestToPayload(req *model.GameReportRequest) ([]byte, error) {
	if req == nil {
		return nil, errors.New("encode game report request payload: request is nil")
	}

	builder := flatbuffers.NewBuilder(64)

	fbs.GameReportRequestStart(builder)
	hi, lo := uuidToHiLo(req.GameID)
	fbs.GameReportRequestAddGameId(builder, commonfbs.CreateUUID(builder, hi, lo))
	fbs.GameReportRequestAddTurn(builder, uint32(req.Turn))
	offset := fbs.GameReportRequestEnd(builder)
	fbs.FinishGameReportRequestBuffer(builder, offset)

	return builder.FinishedBytes(), nil
}

// PayloadToGameReportRequest converts FlatBuffers payload bytes into
// model.GameReportRequest.
func PayloadToGameReportRequest(data []byte) (result *model.GameReportRequest, err error) {
	if len(data) == 0 {
		return nil, errors.New("decode game report request payload: data is empty")
	}

	defer func() {
		if recovered := recover(); recovered != nil {
			result = nil
			err = fmt.Errorf("decode game report request payload: panic recovered: %v", recovered)
		}
	}()

	req := fbs.GetRootAsGameReportRequest(data, 0)
	gameID := req.GameId(nil)
	if gameID == nil {
		return nil, errors.New("decode game report request payload: game_id is missing")
	}
	return &model.GameReportRequest{
		GameID: uuidFromHiLo(gameID.Hi(), gameID.Lo()),
		Turn:   uint(req.Turn()),
	}, nil
}
