// Drives the order submit pipeline: builds a FlatBuffers
// `UserGamesOrder` payload from the local draft, calls
// `client.executeCommand("user.games.order", ...)`, and translates
// the engine response into per-command results the draft store can
// merge with `applyResults`.
//
// The engine populates `cmdApplied` and `cmdErrorCode` on every
// returned command (see `game/openapi.yaml`), so the happy path
// reads real per-command outcomes. An empty response `commands`
// array — the gateway's defensive fallback when no body comes back
// — collapses to a batch-level "all applied" verdict so the player
// is never left with submitted-without-result rows.
//
// Failures fall into two buckets:
//   - the gateway answers with a non-`ok` `resultCode` (auth /
//     transcoder / engine validation); the result is `ok: false`
//     and every submitted entry should flip to `rejected`;
//   - the request itself throws (network, signature mismatch, decoder
//     panic); the exception bubbles up to the caller, which leaves
//     the draft entries in `submitting` for the operator to retry.

import { Builder, ByteBuffer } from "flatbuffers";

import type { GalaxyClient } from "../api/galaxy-client";
import { uuidToHiLo } from "../api/game-state";
import { UUID } from "../proto/galaxy/fbs/common";
import {
	CommandItem,
	CommandPayload,
	CommandPlanetProduce,
	CommandPlanetRename,
	CommandPlanetRouteRemove,
	CommandPlanetRouteSet,
	CommandRaceRelation,
	CommandRaceVote,
	CommandScienceCreate,
	CommandScienceRemove,
	CommandShipClassCreate,
	CommandShipClassRemove,
	CommandShipGroupBreak,
	CommandShipGroupDismantle,
	CommandShipGroupJoinFleet,
	CommandShipGroupLoad,
	CommandShipGroupSend,
	CommandShipGroupTransfer,
	CommandShipGroupUnload,
	CommandShipGroupUpgrade,
	PlanetProduction,
	PlanetRouteLoadType,
	Relation,
	ShipGroupCargo,
	ShipGroupUpgradeTech,
	UserGamesOrder,
	UserGamesOrderResponse,
} from "../proto/galaxy/fbs/order";
import type {
	CargoLoadType,
	OrderCommand,
	ProductionType,
	Relation as RelationLiteral,
	ShipGroupCargo as ShipGroupCargoLiteral,
	ShipGroupUpgradeTech as ShipGroupUpgradeTechLiteral,
} from "./order-types";

const MESSAGE_TYPE = "user.games.order";

export class SubmitError extends Error {
	readonly resultCode: string;
	readonly code: string;

	constructor(resultCode: string, code: string, message: string) {
		super(message);
		this.name = "SubmitError";
		this.resultCode = resultCode;
		this.code = code;
	}
}

export type CommandOutcome = "applied" | "rejected";

export interface SubmitSuccess {
	ok: true;
	results: Map<string, CommandOutcome>;
	errorCodes: Map<string, number | null>;
	updatedAt: number;
}

export interface SubmitFailure {
	ok: false;
	resultCode: string;
	code: string;
	message: string;
}

export type SubmitResult = SubmitSuccess | SubmitFailure;

export interface SubmitOptions {
	updatedAt?: number;
}

/**
 * submitOrder posts the `commands` slice through `user.games.order`,
 * decodes the FBS response, and returns per-command outcomes the
 * caller (the order tab) feeds back into `OrderDraftStore.applyResults`.
 *
 * @param client GalaxyClient owning the signed-gRPC transport.
 * @param gameId Stringified UUID of the game whose order is submitted.
 * @param commands Subset of the local draft to send. The caller has
 *   already filtered out non-`valid` entries.
 * @param options.updatedAt Optional engine-assigned timestamp from a
 *   prior submit — Phase 14 always sends `0` because stale-order
 *   detection is not yet wired client-side.
 */
export async function submitOrder(
	client: GalaxyClient,
	gameId: string,
	commands: OrderCommand[],
	options: SubmitOptions = {},
): Promise<SubmitResult> {
	const payload = buildOrderPayload(gameId, commands, options.updatedAt ?? 0);
	const result = await client.executeCommand(MESSAGE_TYPE, payload);
	if (result.resultCode !== "ok") {
		const { code, message } = decodeError(result.payloadBytes, result.resultCode);
		return {
			ok: false,
			resultCode: result.resultCode,
			code,
			message,
		};
	}
	return decodeOrderResponse(result.payloadBytes, commands);
}

function buildOrderPayload(
	gameId: string,
	commands: OrderCommand[],
	updatedAt: number,
): Uint8Array {
	const builder = new Builder(256);
	const itemOffsets = commands.map((cmd) => encodeCommandItem(builder, cmd));
	const commandsVec = UserGamesOrder.createCommandsVector(builder, itemOffsets);
	const [hi, lo] = uuidToHiLo(gameId);
	const gameIdOffset = UUID.createUUID(builder, hi, lo);
	UserGamesOrder.startUserGamesOrder(builder);
	UserGamesOrder.addGameId(builder, gameIdOffset);
	UserGamesOrder.addUpdatedAt(builder, BigInt(updatedAt));
	UserGamesOrder.addCommands(builder, commandsVec);
	const offset = UserGamesOrder.endUserGamesOrder(builder);
	builder.finish(offset);
	return builder.asUint8Array();
}

function encodeCommandItem(builder: Builder, cmd: OrderCommand): number {
	const cmdIdOffset = builder.createString(cmd.id);
	const { payloadType, payloadOffset } = encodeCommandPayload(builder, cmd);
	CommandItem.startCommandItem(builder);
	CommandItem.addCmdId(builder, cmdIdOffset);
	CommandItem.addPayloadType(builder, payloadType);
	CommandItem.addPayload(builder, payloadOffset);
	return CommandItem.endCommandItem(builder);
}

function encodeCommandPayload(
	builder: Builder,
	cmd: OrderCommand,
): { payloadType: CommandPayload; payloadOffset: number } {
	switch (cmd.kind) {
		case "planetRename": {
			const nameOffset = builder.createString(cmd.name);
			const offset = CommandPlanetRename.createCommandPlanetRename(
				builder,
				BigInt(cmd.planetNumber),
				nameOffset,
			);
			return {
				payloadType: CommandPayload.CommandPlanetRename,
				payloadOffset: offset,
			};
		}
		case "setProductionType": {
			const subjectOffset = builder.createString(cmd.subject);
			const offset = CommandPlanetProduce.createCommandPlanetProduce(
				builder,
				BigInt(cmd.planetNumber),
				productionTypeToFBS(cmd.productionType),
				subjectOffset,
			);
			return {
				payloadType: CommandPayload.CommandPlanetProduce,
				payloadOffset: offset,
			};
		}
		case "setCargoRoute": {
			const offset = CommandPlanetRouteSet.createCommandPlanetRouteSet(
				builder,
				BigInt(cmd.sourcePlanetNumber),
				BigInt(cmd.destinationPlanetNumber),
				cargoLoadTypeToFBS(cmd.loadType),
			);
			return {
				payloadType: CommandPayload.CommandPlanetRouteSet,
				payloadOffset: offset,
			};
		}
		case "removeCargoRoute": {
			const offset = CommandPlanetRouteRemove.createCommandPlanetRouteRemove(
				builder,
				BigInt(cmd.sourcePlanetNumber),
				cargoLoadTypeToFBS(cmd.loadType),
			);
			return {
				payloadType: CommandPayload.CommandPlanetRouteRemove,
				payloadOffset: offset,
			};
		}
		case "createShipClass": {
			const nameOffset = builder.createString(cmd.name);
			const offset = CommandShipClassCreate.createCommandShipClassCreate(
				builder,
				nameOffset,
				cmd.drive,
				BigInt(cmd.armament),
				cmd.weapons,
				cmd.shields,
				cmd.cargo,
			);
			return {
				payloadType: CommandPayload.CommandShipClassCreate,
				payloadOffset: offset,
			};
		}
		case "removeShipClass": {
			const nameOffset = builder.createString(cmd.name);
			const offset = CommandShipClassRemove.createCommandShipClassRemove(
				builder,
				nameOffset,
			);
			return {
				payloadType: CommandPayload.CommandShipClassRemove,
				payloadOffset: offset,
			};
		}
		case "createScience": {
			const nameOffset = builder.createString(cmd.name);
			const offset = CommandScienceCreate.createCommandScienceCreate(
				builder,
				nameOffset,
				cmd.drive,
				cmd.weapons,
				cmd.shields,
				cmd.cargo,
			);
			return {
				payloadType: CommandPayload.CommandScienceCreate,
				payloadOffset: offset,
			};
		}
		case "removeScience": {
			const nameOffset = builder.createString(cmd.name);
			const offset = CommandScienceRemove.createCommandScienceRemove(
				builder,
				nameOffset,
			);
			return {
				payloadType: CommandPayload.CommandScienceRemove,
				payloadOffset: offset,
			};
		}
		case "breakShipGroup": {
			const idOffset = builder.createString(cmd.groupId);
			const newIdOffset = builder.createString(cmd.newGroupId);
			CommandShipGroupBreak.startCommandShipGroupBreak(builder);
			CommandShipGroupBreak.addId(builder, idOffset);
			CommandShipGroupBreak.addNewId(builder, newIdOffset);
			CommandShipGroupBreak.addQuantity(builder, BigInt(cmd.quantity));
			const offset = CommandShipGroupBreak.endCommandShipGroupBreak(builder);
			return {
				payloadType: CommandPayload.CommandShipGroupBreak,
				payloadOffset: offset,
			};
		}
		case "sendShipGroup": {
			const idOffset = builder.createString(cmd.groupId);
			const offset = CommandShipGroupSend.createCommandShipGroupSend(
				builder,
				idOffset,
				BigInt(cmd.destinationPlanetNumber),
			);
			return {
				payloadType: CommandPayload.CommandShipGroupSend,
				payloadOffset: offset,
			};
		}
		case "loadShipGroup": {
			const idOffset = builder.createString(cmd.groupId);
			CommandShipGroupLoad.startCommandShipGroupLoad(builder);
			CommandShipGroupLoad.addId(builder, idOffset);
			CommandShipGroupLoad.addCargo(builder, shipGroupCargoToFBS(cmd.cargo));
			CommandShipGroupLoad.addQuantity(builder, cmd.quantity);
			const offset = CommandShipGroupLoad.endCommandShipGroupLoad(builder);
			return {
				payloadType: CommandPayload.CommandShipGroupLoad,
				payloadOffset: offset,
			};
		}
		case "unloadShipGroup": {
			const idOffset = builder.createString(cmd.groupId);
			const offset = CommandShipGroupUnload.createCommandShipGroupUnload(
				builder,
				idOffset,
				cmd.quantity,
			);
			return {
				payloadType: CommandPayload.CommandShipGroupUnload,
				payloadOffset: offset,
			};
		}
		case "upgradeShipGroup": {
			const idOffset = builder.createString(cmd.groupId);
			CommandShipGroupUpgrade.startCommandShipGroupUpgrade(builder);
			CommandShipGroupUpgrade.addId(builder, idOffset);
			CommandShipGroupUpgrade.addTech(
				builder,
				shipGroupUpgradeTechToFBS(cmd.tech),
			);
			CommandShipGroupUpgrade.addLevel(builder, cmd.level);
			const offset = CommandShipGroupUpgrade.endCommandShipGroupUpgrade(builder);
			return {
				payloadType: CommandPayload.CommandShipGroupUpgrade,
				payloadOffset: offset,
			};
		}
		case "dismantleShipGroup": {
			const idOffset = builder.createString(cmd.groupId);
			const offset =
				CommandShipGroupDismantle.createCommandShipGroupDismantle(
					builder,
					idOffset,
				);
			return {
				payloadType: CommandPayload.CommandShipGroupDismantle,
				payloadOffset: offset,
			};
		}
		case "transferShipGroup": {
			const idOffset = builder.createString(cmd.groupId);
			const acceptorOffset = builder.createString(cmd.acceptor);
			const offset = CommandShipGroupTransfer.createCommandShipGroupTransfer(
				builder,
				idOffset,
				acceptorOffset,
			);
			return {
				payloadType: CommandPayload.CommandShipGroupTransfer,
				payloadOffset: offset,
			};
		}
		case "joinFleetShipGroup": {
			const idOffset = builder.createString(cmd.groupId);
			const nameOffset = builder.createString(cmd.name);
			const offset =
				CommandShipGroupJoinFleet.createCommandShipGroupJoinFleet(
					builder,
					idOffset,
					nameOffset,
				);
			return {
				payloadType: CommandPayload.CommandShipGroupJoinFleet,
				payloadOffset: offset,
			};
		}
		case "setDiplomaticStance": {
			const acceptorOffset = builder.createString(cmd.acceptor);
			const offset = CommandRaceRelation.createCommandRaceRelation(
				builder,
				acceptorOffset,
				relationToFBS(cmd.relation),
			);
			return {
				payloadType: CommandPayload.CommandRaceRelation,
				payloadOffset: offset,
			};
		}
		case "setVoteRecipient": {
			const acceptorOffset = builder.createString(cmd.acceptor);
			const offset = CommandRaceVote.createCommandRaceVote(
				builder,
				acceptorOffset,
			);
			return {
				payloadType: CommandPayload.CommandRaceVote,
				payloadOffset: offset,
			};
		}
		case "placeholder":
			throw new SubmitError(
				"invalid_request",
				"invalid_request",
				`placeholder commands cannot be submitted (cmd id ${cmd.id})`,
			);
	}
}

/**
 * productionTypeToFBS converts the wire-stable `ProductionType` literal
 * to the FlatBuffers enum value. Mirrors `planetProductionToFBS` in
 * `pkg/transcoder/order.go`. The two sides are kept in lock-step so the
 * gateway can decode whatever the frontend produces without a
 * translation step.
 */
export function productionTypeToFBS(value: ProductionType): PlanetProduction {
	switch (value) {
		case "MAT":
			return PlanetProduction.MAT;
		case "CAP":
			return PlanetProduction.CAP;
		case "DRIVE":
			return PlanetProduction.DRIVE;
		case "WEAPONS":
			return PlanetProduction.WEAPONS;
		case "SHIELDS":
			return PlanetProduction.SHIELDS;
		case "CARGO":
			return PlanetProduction.CARGO;
		case "SCIENCE":
			return PlanetProduction.SCIENCE;
		case "SHIP":
			return PlanetProduction.SHIP;
	}
}

/**
 * cargoLoadTypeToFBS converts the wire-stable `CargoLoadType` literal
 * to the FlatBuffers enum value. Mirrors the engine
 * `PlanetRouteLoadType` enum (`pkg/schema/fbs/order.fbs`).
 */
export function cargoLoadTypeToFBS(value: CargoLoadType): PlanetRouteLoadType {
	switch (value) {
		case "COL":
			return PlanetRouteLoadType.COL;
		case "CAP":
			return PlanetRouteLoadType.CAP;
		case "MAT":
			return PlanetRouteLoadType.MAT;
		case "EMP":
			return PlanetRouteLoadType.EMP;
	}
}

/**
 * shipGroupCargoToFBS converts the wire-stable `ShipGroupCargo`
 * literal to the FlatBuffers enum value. Mirrors the engine
 * `ShipGroupCargo` enum (`pkg/schema/fbs/order.fbs`). The FBS enum
 * carries an `UNKNOWN` zero value as the default; the encoder always
 * emits one of the three real values.
 */
export function shipGroupCargoToFBS(
	value: ShipGroupCargoLiteral,
): ShipGroupCargo {
	switch (value) {
		case "COL":
			return ShipGroupCargo.COL;
		case "CAP":
			return ShipGroupCargo.CAP;
		case "MAT":
			return ShipGroupCargo.MAT;
	}
}

/**
 * shipGroupUpgradeTechToFBS converts the wire-stable
 * `ShipGroupUpgradeTech` literal to the FlatBuffers enum value.
 * Mirrors the engine `ShipGroupUpgradeTech` enum
 * (`pkg/schema/fbs/order.fbs`).
 */
export function shipGroupUpgradeTechToFBS(
	value: ShipGroupUpgradeTechLiteral,
): ShipGroupUpgradeTech {
	switch (value) {
		case "ALL":
			return ShipGroupUpgradeTech.ALL;
		case "DRIVE":
			return ShipGroupUpgradeTech.DRIVE;
		case "WEAPONS":
			return ShipGroupUpgradeTech.WEAPONS;
		case "SHIELDS":
			return ShipGroupUpgradeTech.SHIELDS;
		case "CARGO":
			return ShipGroupUpgradeTech.CARGO;
	}
}

/**
 * relationToFBS converts the wire-stable `Relation` literal to the
 * FlatBuffers enum value. Mirrors `pkg/transcoder/order.go`. The FBS
 * enum carries an `UNKNOWN` zero default; the encoder always emits
 * one of the two real values (`WAR` or `PEACE`).
 */
export function relationToFBS(value: RelationLiteral): Relation {
	switch (value) {
		case "WAR":
			return Relation.WAR;
		case "PEACE":
			return Relation.PEACE;
	}
}

function decodeOrderResponse(
	payload: Uint8Array,
	commands: OrderCommand[],
): SubmitSuccess {
	const results = new Map<string, CommandOutcome>();
	const errorCodes = new Map<string, number | null>();
	let updatedAt = 0;

	if (payload.length === 0) {
		// Empty envelope (gateway fallback). Apply batch-level verdict.
		for (const cmd of commands) {
			results.set(cmd.id, "applied");
			errorCodes.set(cmd.id, null);
		}
		return { ok: true, results, errorCodes, updatedAt };
	}

	const buffer = new ByteBuffer(payload);
	const response = UserGamesOrderResponse.getRootAsUserGamesOrderResponse(buffer);
	updatedAt = Number(response.updatedAt());

	const length = response.commandsLength();
	if (length === 0) {
		for (const cmd of commands) {
			results.set(cmd.id, "applied");
			errorCodes.set(cmd.id, null);
		}
		return { ok: true, results, errorCodes, updatedAt };
	}

	for (let i = 0; i < length; i++) {
		const item = response.commands(i);
		if (item === null) continue;
		const cmdId = item.cmdId();
		if (cmdId === null) continue;
		const applied = item.cmdApplied();
		const errorCode = item.cmdErrorCode();
		results.set(cmdId, applied === false ? "rejected" : "applied");
		errorCodes.set(cmdId, errorCode === null ? null : Number(errorCode));
	}

	// Defensive: any submitted command not echoed back falls back to
	// applied so the draft entry leaves `submitting`.
	for (const cmd of commands) {
		if (!results.has(cmd.id)) {
			results.set(cmd.id, "applied");
			errorCodes.set(cmd.id, null);
		}
	}

	return { ok: true, results, errorCodes, updatedAt };
}

function decodeError(
	payload: Uint8Array,
	resultCode: string,
): { code: string; message: string } {
	if (payload.length === 0) {
		return { code: resultCode, message: resultCode };
	}
	try {
		const text = new TextDecoder().decode(payload);
		const parsed = JSON.parse(text) as { code?: string; message?: string };
		return {
			code: typeof parsed.code === "string" ? parsed.code : resultCode,
			message: typeof parsed.message === "string" ? parsed.message : text,
		};
	} catch {
		return { code: resultCode, message: resultCode };
	}
}
