// Phase 11 helpers for forging FlatBuffers report payloads in e2e
// tests. Mirrors the engine's `report.Report` shape so the mocked
// gateway can return realistic data without standing up the real
// engine container.
//
// Phase 11 only renders planets, so the helpers keep the report shape
// minimal (turn / dimensions / planet vectors). Phase 13 extended the
// fixture with the optional rich planet fields (size, resources,
// stockpiles, population, industry, colonists, production, free
// industry) so the inspector e2e can drive the read-only display
// against realistic values. Phase 15 adds a minimal `LocalShipClass`
// projection so the planet inspector's Build-Ship sub-picker has data
// in e2e specs (`name` only — Phase 17 widens this when ship-class
// CRUD lands). Phase 21 adds a `LocalScience` projection so the
// sciences table and the planet production picker's Research sub-row
// have data in e2e specs.

import { Builder } from "flatbuffers";

import { UUID } from "../../../src/proto/galaxy/fbs/common";
import {
	BattleSummary,
	Bombing,
	LocalPlanet,
	OtherPlanet,
	OtherScience,
	OthersShipClass,
	Player,
	Report,
	Route,
	RouteEntry,
	Science,
	ShipClass,
	ShipProduction,
	UnidentifiedPlanet,
	UninhabitedPlanet,
} from "../../../src/proto/galaxy/fbs/report";

export interface PlanetFixture {
	number: number;
	name: string;
	x: number;
	y: number;
	size?: number;
	resources?: number;
	capital?: number;
	material?: number;
}

export interface InhabitedFixture extends PlanetFixture {
	population?: number;
	colonists?: number;
	industry?: number;
	production?: string;
	freeIndustry?: number;
}

export interface OtherPlanetFixture extends InhabitedFixture {
	owner: string;
}

export interface ShipClassFixture {
	name: string;
	drive?: number;
	armament?: number;
	weapons?: number;
	shields?: number;
	cargo?: number;
}

export interface ScienceFixture {
	name: string;
	drive?: number;
	weapons?: number;
	shields?: number;
	cargo?: number;
}

export interface PlayerFixture {
	name: string;
	drive?: number;
	weapons?: number;
	shields?: number;
	cargo?: number;
	population?: number;
	industry?: number;
	planets?: number;
	relation?: "WAR" | "PEACE" | "-";
	votes?: number;
	extinct?: boolean;
}

export interface RouteEntryFixture {
	loadType: "COL" | "CAP" | "MAT" | "EMP";
	destinationPlanetNumber: number;
}

export interface RouteFixture {
	sourcePlanetNumber: number;
	entries: RouteEntryFixture[];
}

export interface OtherScienceFixture extends ScienceFixture {
	race: string;
}

export interface OtherShipClassFixture extends ShipClassFixture {
	race: string;
	mass?: number;
}

export interface BattleSummaryFixture {
	id: string;
	planet: number;
	shots: number;
}

export interface BombingFixture {
	planetNumber: number;
	planet: string;
	owner: string;
	attacker: string;
	production?: string;
	industry?: number;
	population?: number;
	colonists?: number;
	capital?: number;
	material?: number;
	attackPower?: number;
	wiped?: boolean;
}

export interface ShipProductionFixture {
	planet: number;
	class: string;
	cost?: number;
	prodUsed?: number;
	percent?: number;
	free?: number;
}

export interface ReportFixture {
	turn: number;
	mapWidth?: number;
	mapHeight?: number;
	localPlanets?: InhabitedFixture[];
	otherPlanets?: OtherPlanetFixture[];
	uninhabitedPlanets?: PlanetFixture[];
	unidentifiedPlanets?: { number: number; x: number; y: number }[];
	localShipClass?: ShipClassFixture[];
	localScience?: ScienceFixture[];
	race?: string;
	players?: PlayerFixture[];
	routes?: RouteFixture[];
	myVotes?: number;
	myVoteFor?: string;
	otherScience?: OtherScienceFixture[];
	otherShipClass?: OtherShipClassFixture[];
	battles?: BattleSummaryFixture[];
	bombings?: BombingFixture[];
	shipProductions?: ShipProductionFixture[];
}

export function buildReportPayload(fixture: ReportFixture): Uint8Array {
	const builder = new Builder(512);

	const localOffsets = (fixture.localPlanets ?? []).map((planet) => {
		const name = builder.createString(planet.name);
		const production =
			planet.production !== undefined
				? builder.createString(planet.production)
				: null;
		LocalPlanet.startLocalPlanet(builder);
		LocalPlanet.addNumber(builder, BigInt(planet.number));
		LocalPlanet.addX(builder, planet.x);
		LocalPlanet.addY(builder, planet.y);
		LocalPlanet.addName(builder, name);
		LocalPlanet.addSize(builder, planet.size ?? 10);
		LocalPlanet.addResources(builder, planet.resources ?? 0.5);
		LocalPlanet.addCapital(builder, planet.capital ?? 0);
		LocalPlanet.addMaterial(builder, planet.material ?? 0);
		LocalPlanet.addPopulation(builder, planet.population ?? 0);
		LocalPlanet.addIndustry(builder, planet.industry ?? 0);
		LocalPlanet.addColonists(builder, planet.colonists ?? 0);
		if (production !== null) LocalPlanet.addProduction(builder, production);
		LocalPlanet.addFreeIndustry(builder, planet.freeIndustry ?? 0);
		return LocalPlanet.endLocalPlanet(builder);
	});

	const otherOffsets = (fixture.otherPlanets ?? []).map((planet) => {
		const name = builder.createString(planet.name);
		const owner = builder.createString(planet.owner);
		const production =
			planet.production !== undefined
				? builder.createString(planet.production)
				: null;
		OtherPlanet.startOtherPlanet(builder);
		OtherPlanet.addNumber(builder, BigInt(planet.number));
		OtherPlanet.addX(builder, planet.x);
		OtherPlanet.addY(builder, planet.y);
		OtherPlanet.addName(builder, name);
		OtherPlanet.addOwner(builder, owner);
		OtherPlanet.addSize(builder, planet.size ?? 9);
		OtherPlanet.addResources(builder, planet.resources ?? 0.5);
		OtherPlanet.addCapital(builder, planet.capital ?? 0);
		OtherPlanet.addMaterial(builder, planet.material ?? 0);
		OtherPlanet.addPopulation(builder, planet.population ?? 0);
		OtherPlanet.addIndustry(builder, planet.industry ?? 0);
		OtherPlanet.addColonists(builder, planet.colonists ?? 0);
		if (production !== null) OtherPlanet.addProduction(builder, production);
		OtherPlanet.addFreeIndustry(builder, planet.freeIndustry ?? 0);
		return OtherPlanet.endOtherPlanet(builder);
	});

	const uninhabitedOffsets = (fixture.uninhabitedPlanets ?? []).map(
		(planet) => {
			const name = builder.createString(planet.name);
			UninhabitedPlanet.startUninhabitedPlanet(builder);
			UninhabitedPlanet.addNumber(builder, BigInt(planet.number));
			UninhabitedPlanet.addX(builder, planet.x);
			UninhabitedPlanet.addY(builder, planet.y);
			UninhabitedPlanet.addName(builder, name);
			UninhabitedPlanet.addSize(builder, planet.size ?? 6);
			UninhabitedPlanet.addResources(builder, planet.resources ?? 0.5);
			UninhabitedPlanet.addCapital(builder, planet.capital ?? 0);
			UninhabitedPlanet.addMaterial(builder, planet.material ?? 0);
			return UninhabitedPlanet.endUninhabitedPlanet(builder);
		},
	);

	const unidentifiedOffsets = (fixture.unidentifiedPlanets ?? []).map(
		(planet) => {
			UnidentifiedPlanet.startUnidentifiedPlanet(builder);
			UnidentifiedPlanet.addNumber(builder, BigInt(planet.number));
			UnidentifiedPlanet.addX(builder, planet.x);
			UnidentifiedPlanet.addY(builder, planet.y);
			return UnidentifiedPlanet.endUnidentifiedPlanet(builder);
		},
	);

	const localShipClassOffsets = (fixture.localShipClass ?? []).map((cls) => {
		const name = builder.createString(cls.name);
		ShipClass.startShipClass(builder);
		ShipClass.addName(builder, name);
		ShipClass.addDrive(builder, cls.drive ?? 0);
		ShipClass.addArmament(builder, BigInt(cls.armament ?? 0));
		ShipClass.addWeapons(builder, cls.weapons ?? 0);
		ShipClass.addShields(builder, cls.shields ?? 0);
		ShipClass.addCargo(builder, cls.cargo ?? 0);
		return ShipClass.endShipClass(builder);
	});

	const localScienceOffsets = (fixture.localScience ?? []).map((sci) => {
		const name = builder.createString(sci.name);
		Science.startScience(builder);
		Science.addName(builder, name);
		Science.addDrive(builder, sci.drive ?? 0);
		Science.addWeapons(builder, sci.weapons ?? 0);
		Science.addShields(builder, sci.shields ?? 0);
		Science.addCargo(builder, sci.cargo ?? 0);
		return Science.endScience(builder);
	});

	const playerOffsets = (fixture.players ?? []).map((p) => {
		const name = builder.createString(p.name);
		const relation =
			p.relation === undefined ? null : builder.createString(p.relation);
		Player.startPlayer(builder);
		Player.addName(builder, name);
		Player.addDrive(builder, p.drive ?? 1);
		Player.addWeapons(builder, p.weapons ?? 0);
		Player.addShields(builder, p.shields ?? 0);
		Player.addCargo(builder, p.cargo ?? 0);
		Player.addPopulation(builder, p.population ?? 0);
		Player.addIndustry(builder, p.industry ?? 0);
		Player.addPlanets(builder, p.planets ?? 0);
		if (relation !== null) Player.addRelation(builder, relation);
		Player.addVotes(builder, p.votes ?? 0);
		Player.addExtinct(builder, p.extinct ?? false);
		return Player.endPlayer(builder);
	});

	const routeOffsets = (fixture.routes ?? []).map((route) => {
		const entryOffsets = route.entries.map((entry) => {
			const valueOffset = builder.createString(entry.loadType);
			RouteEntry.startRouteEntry(builder);
			RouteEntry.addKey(builder, BigInt(entry.destinationPlanetNumber));
			RouteEntry.addValue(builder, valueOffset);
			return RouteEntry.endRouteEntry(builder);
		});
		const entriesVec = Route.createRouteVector(builder, entryOffsets);
		Route.startRoute(builder);
		Route.addPlanet(builder, BigInt(route.sourcePlanetNumber));
		Route.addRoute(builder, entriesVec);
		return Route.endRoute(builder);
	});

	const otherScienceOffsets = (fixture.otherScience ?? []).map((sci) => {
		const race = builder.createString(sci.race);
		const name = builder.createString(sci.name);
		OtherScience.startOtherScience(builder);
		OtherScience.addRace(builder, race);
		OtherScience.addName(builder, name);
		OtherScience.addDrive(builder, sci.drive ?? 0);
		OtherScience.addWeapons(builder, sci.weapons ?? 0);
		OtherScience.addShields(builder, sci.shields ?? 0);
		OtherScience.addCargo(builder, sci.cargo ?? 0);
		return OtherScience.endOtherScience(builder);
	});

	const otherShipClassOffsets = (fixture.otherShipClass ?? []).map((cls) => {
		const race = builder.createString(cls.race);
		const name = builder.createString(cls.name);
		OthersShipClass.startOthersShipClass(builder);
		OthersShipClass.addRace(builder, race);
		OthersShipClass.addName(builder, name);
		OthersShipClass.addDrive(builder, cls.drive ?? 0);
		OthersShipClass.addArmament(builder, BigInt(cls.armament ?? 0));
		OthersShipClass.addWeapons(builder, cls.weapons ?? 0);
		OthersShipClass.addShields(builder, cls.shields ?? 0);
		OthersShipClass.addCargo(builder, cls.cargo ?? 0);
		OthersShipClass.addMass(builder, cls.mass ?? 0);
		return OthersShipClass.endOthersShipClass(builder);
	});

	const bombingOffsets = (fixture.bombings ?? []).map((b) => {
		const planet = builder.createString(b.planet);
		const owner = builder.createString(b.owner);
		const attacker = builder.createString(b.attacker);
		const production = builder.createString(b.production ?? "");
		Bombing.startBombing(builder);
		Bombing.addNumber(builder, BigInt(b.planetNumber));
		Bombing.addPlanet(builder, planet);
		Bombing.addOwner(builder, owner);
		Bombing.addAttacker(builder, attacker);
		Bombing.addProduction(builder, production);
		Bombing.addIndustry(builder, b.industry ?? 0);
		Bombing.addPopulation(builder, b.population ?? 0);
		Bombing.addColonists(builder, b.colonists ?? 0);
		Bombing.addCapital(builder, b.capital ?? 0);
		Bombing.addMaterial(builder, b.material ?? 0);
		Bombing.addAttackPower(builder, b.attackPower ?? 0);
		Bombing.addWiped(builder, b.wiped ?? false);
		return Bombing.endBombing(builder);
	});

	const shipProductionOffsets = (fixture.shipProductions ?? []).map((sp) => {
		const className = builder.createString(sp.class);
		ShipProduction.startShipProduction(builder);
		ShipProduction.addPlanet(builder, BigInt(sp.planet));
		ShipProduction.addClass(builder, className);
		ShipProduction.addCost(builder, sp.cost ?? 0);
		ShipProduction.addProdUsed(builder, sp.prodUsed ?? 0);
		ShipProduction.addPercent(builder, sp.percent ?? 0);
		ShipProduction.addFree(builder, sp.free ?? 0);
		return ShipProduction.endShipProduction(builder);
	});

	const localVec =
		localOffsets.length === 0
			? null
			: Report.createLocalPlanetVector(builder, localOffsets);
	const otherVec =
		otherOffsets.length === 0
			? null
			: Report.createOtherPlanetVector(builder, otherOffsets);
	const uninhabitedVec =
		uninhabitedOffsets.length === 0
			? null
			: Report.createUninhabitedPlanetVector(builder, uninhabitedOffsets);
	const unidentifiedVec =
		unidentifiedOffsets.length === 0
			? null
			: Report.createUnidentifiedPlanetVector(builder, unidentifiedOffsets);
	const localShipClassVec =
		localShipClassOffsets.length === 0
			? null
			: Report.createLocalShipClassVector(builder, localShipClassOffsets);
	const localScienceVec =
		localScienceOffsets.length === 0
			? null
			: Report.createLocalScienceVector(builder, localScienceOffsets);
	const playerVec =
		playerOffsets.length === 0
			? null
			: Report.createPlayerVector(builder, playerOffsets);
	const routeVec =
		routeOffsets.length === 0
			? null
			: Report.createRouteVector(builder, routeOffsets);
	const otherScienceVec =
		otherScienceOffsets.length === 0
			? null
			: Report.createOtherScienceVector(builder, otherScienceOffsets);
	const otherShipClassVec =
		otherShipClassOffsets.length === 0
			? null
			: Report.createOtherShipClassVector(builder, otherShipClassOffsets);
	const bombingVec =
		bombingOffsets.length === 0
			? null
			: Report.createBombingVector(builder, bombingOffsets);
	const shipProductionVec =
		shipProductionOffsets.length === 0
			? null
			: Report.createShipProductionVector(builder, shipProductionOffsets);
	// Phase 27 — `battle` carries `BattleSummary` tables, each with
	// an inline `id:UUID` struct plus `planet` and `shots` slots.
	const battleVec = (() => {
		const summaries = fixture.battles ?? [];
		if (summaries.length === 0) return null;
		const offsets = summaries.map((s) => {
			const [hi, lo] = uuidToHiLo(s.id);
			BattleSummary.startBattleSummary(builder);
			BattleSummary.addId(builder, UUID.createUUID(builder, hi, lo));
			BattleSummary.addPlanet(builder, BigInt(s.planet));
			BattleSummary.addShots(builder, BigInt(s.shots));
			return BattleSummary.endBattleSummary(builder);
		});
		Report.startBattleVector(builder, offsets.length);
		for (let i = offsets.length - 1; i >= 0; i--) {
			builder.addOffset(offsets[i]);
		}
		return builder.endVector();
	})();
	const raceOffset =
		fixture.race === undefined ? null : builder.createString(fixture.race);
	const voteForOffset =
		fixture.myVoteFor === undefined
			? null
			: builder.createString(fixture.myVoteFor);

	const totalPlanets =
		(fixture.localPlanets ?? []).length +
		(fixture.otherPlanets ?? []).length +
		(fixture.uninhabitedPlanets ?? []).length +
		(fixture.unidentifiedPlanets ?? []).length;

	Report.startReport(builder);
	Report.addTurn(builder, BigInt(fixture.turn));
	Report.addWidth(builder, fixture.mapWidth ?? 4000);
	Report.addHeight(builder, fixture.mapHeight ?? 4000);
	Report.addPlanetCount(builder, totalPlanets);
	if (raceOffset !== null) Report.addRace(builder, raceOffset);
	if (fixture.myVotes !== undefined) Report.addVotes(builder, fixture.myVotes);
	if (voteForOffset !== null) Report.addVoteFor(builder, voteForOffset);
	if (playerVec !== null) Report.addPlayer(builder, playerVec);
	if (localVec !== null) Report.addLocalPlanet(builder, localVec);
	if (otherVec !== null) Report.addOtherPlanet(builder, otherVec);
	if (uninhabitedVec !== null) Report.addUninhabitedPlanet(builder, uninhabitedVec);
	if (unidentifiedVec !== null) Report.addUnidentifiedPlanet(builder, unidentifiedVec);
	if (localShipClassVec !== null)
		Report.addLocalShipClass(builder, localShipClassVec);
	if (localScienceVec !== null)
		Report.addLocalScience(builder, localScienceVec);
	if (routeVec !== null) Report.addRoute(builder, routeVec);
	if (otherScienceVec !== null)
		Report.addOtherScience(builder, otherScienceVec);
	if (otherShipClassVec !== null)
		Report.addOtherShipClass(builder, otherShipClassVec);
	if (battleVec !== null) Report.addBattle(builder, battleVec);
	if (bombingVec !== null) Report.addBombing(builder, bombingVec);
	if (shipProductionVec !== null)
		Report.addShipProduction(builder, shipProductionVec);
	const reportOff = Report.endReport(builder);
	builder.finish(reportOff);
	return builder.asUint8Array();
}

function uuidToHiLo(value: string): [bigint, bigint] {
	const hex = value.replace(/-/g, "").toLowerCase();
	if (hex.length !== 32 || /[^0-9a-f]/.test(hex)) {
		throw new Error(`buildReportPayload: invalid battle uuid ${value}`);
	}
	const hi = BigInt(`0x${hex.slice(0, 16)}`);
	const lo = BigInt(`0x${hex.slice(16, 32)}`);
	return [hi, lo];
}
