// Reads back the player's stored order for the current turn through
// `user.games.order.get`. Used by `OrderDraftStore` only when the
// local cache row is absent (fresh install, cleared storage, or a
// brand-new device): the local draft is the source of truth, so a
// present-but-empty cache row means "no commands" and is honoured
// over the server snapshot.

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 {
	CommandPayload,
	CommandPlanetProduce,
	CommandPlanetRename,
	CommandPlanetRouteRemove,
	CommandPlanetRouteSet,
	CommandRaceRelation,
	CommandRaceVote,
	CommandScienceCreate,
	CommandScienceRemove,
	CommandShipClassCreate,
	CommandShipClassRemove,
	CommandShipGroupBreak,
	CommandShipGroupDismantle,
	CommandShipGroupJoinFleet,
	CommandShipGroupLoad,
	CommandShipGroupSend,
	CommandShipGroupTransfer,
	CommandShipGroupUnload,
	CommandShipGroupUpgrade,
	PlanetProduction,
	PlanetRouteLoadType,
	Relation,
	ShipGroupCargo,
	ShipGroupUpgradeTech,
	UserGamesOrderGet,
	UserGamesOrderGetResponse,
} 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.get";

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

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

export interface FetchedOrder {
	commands: OrderCommand[];
	updatedAt: number;
}

/**
 * fetchOrder issues `user.games.order.get` for the given game and
 * turn, decodes the response, and returns the typed draft. A
 * `found = false` answer (no order stored on the server) surfaces as
 * an empty `commands` array — the caller treats this as a clean
 * draft. Unknown command kinds in the response are skipped with a
 * console warning so a backend-side schema bump never silently
 * corrupts the local draft.
 */
export async function fetchOrder(
	client: GalaxyClient,
	gameId: string,
	turn: number,
): Promise<FetchedOrder> {
	if (turn < 0) {
		throw new OrderLoadError(
			"invalid_request",
			"invalid_request",
			`turn must be non-negative, got ${turn}`,
		);
	}
	const payload = buildRequest(gameId, turn);
	const result = await client.executeCommand(MESSAGE_TYPE, payload);
	if (result.resultCode !== "ok") {
		const { code, message } = decodeError(result.payloadBytes, result.resultCode);
		throw new OrderLoadError(result.resultCode, code, message);
	}
	return decodeResponse(result.payloadBytes);
}

function buildRequest(gameId: string, turn: number): Uint8Array {
	const builder = new Builder(64);
	const [hi, lo] = uuidToHiLo(gameId);
	const gameIdOffset = UUID.createUUID(builder, hi, lo);
	UserGamesOrderGet.startUserGamesOrderGet(builder);
	UserGamesOrderGet.addGameId(builder, gameIdOffset);
	UserGamesOrderGet.addTurn(builder, BigInt(turn));
	const offset = UserGamesOrderGet.endUserGamesOrderGet(builder);
	builder.finish(offset);
	return builder.asUint8Array();
}

function decodeResponse(payload: Uint8Array): FetchedOrder {
	if (payload.length === 0) {
		throw new OrderLoadError(
			"internal_error",
			"internal_error",
			"empty user.games.order.get payload",
		);
	}
	const buffer = new ByteBuffer(payload);
	const response = UserGamesOrderGetResponse.getRootAsUserGamesOrderGetResponse(buffer);
	if (!response.found()) {
		return { commands: [], updatedAt: 0 };
	}
	const order = response.order();
	if (order === null) {
		throw new OrderLoadError(
			"internal_error",
			"internal_error",
			"order missing while found=true",
		);
	}
	const commands: OrderCommand[] = [];
	const length = order.commandsLength();
	for (let i = 0; i < length; i++) {
		const item = order.commands(i);
		if (item === null) continue;
		const cmd = decodeCommand(item);
		if (cmd === null) continue;
		commands.push(cmd);
	}
	return {
		commands,
		updatedAt: Number(order.updatedAt()),
	};
}

type CommandItemView = NonNullable<
	ReturnType<NonNullable<ReturnType<UserGamesOrderGetResponse["order"]>>["commands"]>
>;

function decodeCommand(item: CommandItemView): OrderCommand | null {
	if (item === null) return null;
	const id = item.cmdId();
	if (id === null) return null;
	const payloadType = item.payloadType();
	switch (payloadType) {
		case CommandPayload.CommandPlanetRename: {
			const inner = new CommandPlanetRename();
			item.payload(inner);
			return {
				kind: "planetRename",
				id,
				planetNumber: Number(inner.number()),
				name: inner.name() ?? "",
			};
		}
		case CommandPayload.CommandPlanetProduce: {
			const inner = new CommandPlanetProduce();
			item.payload(inner);
			const productionType = productionTypeFromFBS(inner.production());
			if (productionType === null) {
				console.warn(
					`fetchOrder: skipping CommandPlanetProduce with unknown production enum (${inner.production()})`,
				);
				return null;
			}
			return {
				kind: "setProductionType",
				id,
				planetNumber: Number(inner.number()),
				productionType,
				subject: inner.subject() ?? "",
			};
		}
		case CommandPayload.CommandPlanetRouteSet: {
			const inner = new CommandPlanetRouteSet();
			item.payload(inner);
			const loadType = cargoLoadTypeFromFBS(inner.loadType());
			if (loadType === null) {
				console.warn(
					`fetchOrder: skipping CommandPlanetRouteSet with unknown load_type enum (${inner.loadType()})`,
				);
				return null;
			}
			return {
				kind: "setCargoRoute",
				id,
				sourcePlanetNumber: Number(inner.origin()),
				destinationPlanetNumber: Number(inner.destination()),
				loadType,
			};
		}
		case CommandPayload.CommandPlanetRouteRemove: {
			const inner = new CommandPlanetRouteRemove();
			item.payload(inner);
			const loadType = cargoLoadTypeFromFBS(inner.loadType());
			if (loadType === null) {
				console.warn(
					`fetchOrder: skipping CommandPlanetRouteRemove with unknown load_type enum (${inner.loadType()})`,
				);
				return null;
			}
			return {
				kind: "removeCargoRoute",
				id,
				sourcePlanetNumber: Number(inner.origin()),
				loadType,
			};
		}
		case CommandPayload.CommandShipClassCreate: {
			const inner = new CommandShipClassCreate();
			item.payload(inner);
			return {
				kind: "createShipClass",
				id,
				name: inner.name() ?? "",
				drive: inner.drive(),
				armament: Number(inner.armament()),
				weapons: inner.weapons(),
				shields: inner.shields(),
				cargo: inner.cargo(),
			};
		}
		case CommandPayload.CommandShipClassRemove: {
			const inner = new CommandShipClassRemove();
			item.payload(inner);
			return {
				kind: "removeShipClass",
				id,
				name: inner.name() ?? "",
			};
		}
		case CommandPayload.CommandScienceCreate: {
			const inner = new CommandScienceCreate();
			item.payload(inner);
			return {
				kind: "createScience",
				id,
				name: inner.name() ?? "",
				drive: inner.drive(),
				weapons: inner.weapons(),
				shields: inner.shields(),
				cargo: inner.cargo(),
			};
		}
		case CommandPayload.CommandScienceRemove: {
			const inner = new CommandScienceRemove();
			item.payload(inner);
			return {
				kind: "removeScience",
				id,
				name: inner.name() ?? "",
			};
		}
		case CommandPayload.CommandShipGroupBreak: {
			const inner = new CommandShipGroupBreak();
			item.payload(inner);
			return {
				kind: "breakShipGroup",
				id,
				groupId: inner.id() ?? "",
				newGroupId: inner.newId() ?? "",
				quantity: Number(inner.quantity()),
			};
		}
		case CommandPayload.CommandShipGroupSend: {
			const inner = new CommandShipGroupSend();
			item.payload(inner);
			return {
				kind: "sendShipGroup",
				id,
				groupId: inner.id() ?? "",
				destinationPlanetNumber: Number(inner.destination()),
			};
		}
		case CommandPayload.CommandShipGroupLoad: {
			const inner = new CommandShipGroupLoad();
			item.payload(inner);
			const cargo = shipGroupCargoFromFBS(inner.cargo());
			if (cargo === null) {
				console.warn(
					`fetchOrder: skipping CommandShipGroupLoad with unknown cargo enum (${inner.cargo()})`,
				);
				return null;
			}
			return {
				kind: "loadShipGroup",
				id,
				groupId: inner.id() ?? "",
				cargo,
				quantity: inner.quantity(),
			};
		}
		case CommandPayload.CommandShipGroupUnload: {
			const inner = new CommandShipGroupUnload();
			item.payload(inner);
			return {
				kind: "unloadShipGroup",
				id,
				groupId: inner.id() ?? "",
				quantity: inner.quantity(),
			};
		}
		case CommandPayload.CommandShipGroupUpgrade: {
			const inner = new CommandShipGroupUpgrade();
			item.payload(inner);
			const tech = shipGroupUpgradeTechFromFBS(inner.tech());
			if (tech === null) {
				console.warn(
					`fetchOrder: skipping CommandShipGroupUpgrade with unknown tech enum (${inner.tech()})`,
				);
				return null;
			}
			return {
				kind: "upgradeShipGroup",
				id,
				groupId: inner.id() ?? "",
				tech,
				level: inner.level(),
			};
		}
		case CommandPayload.CommandShipGroupDismantle: {
			const inner = new CommandShipGroupDismantle();
			item.payload(inner);
			return {
				kind: "dismantleShipGroup",
				id,
				groupId: inner.id() ?? "",
			};
		}
		case CommandPayload.CommandShipGroupTransfer: {
			const inner = new CommandShipGroupTransfer();
			item.payload(inner);
			return {
				kind: "transferShipGroup",
				id,
				groupId: inner.id() ?? "",
				acceptor: inner.acceptor() ?? "",
			};
		}
		case CommandPayload.CommandShipGroupJoinFleet: {
			const inner = new CommandShipGroupJoinFleet();
			item.payload(inner);
			return {
				kind: "joinFleetShipGroup",
				id,
				groupId: inner.id() ?? "",
				name: inner.name() ?? "",
			};
		}
		case CommandPayload.CommandRaceRelation: {
			const inner = new CommandRaceRelation();
			item.payload(inner);
			const relation = relationFromFBS(inner.relation());
			if (relation === null) {
				console.warn(
					`fetchOrder: skipping CommandRaceRelation with unknown relation enum (${inner.relation()})`,
				);
				return null;
			}
			return {
				kind: "setDiplomaticStance",
				id,
				acceptor: inner.acceptor() ?? "",
				relation,
			};
		}
		case CommandPayload.CommandRaceVote: {
			const inner = new CommandRaceVote();
			item.payload(inner);
			return {
				kind: "setVoteRecipient",
				id,
				acceptor: inner.acceptor() ?? "",
			};
		}
		default:
			console.warn(
				`fetchOrder: skipping unknown command kind (payloadType=${payloadType})`,
			);
			return null;
	}
}

/**
 * productionTypeFromFBS reverses `productionTypeToFBS` from
 * `submit.ts`. `PlanetProduction.UNKNOWN` and any out-of-band value
 * yield `null` so the caller drops the entry instead of fabricating a
 * synthetic kind.
 */
export function productionTypeFromFBS(
	value: PlanetProduction,
): ProductionType | null {
	switch (value) {
		case PlanetProduction.MAT:
			return "MAT";
		case PlanetProduction.CAP:
			return "CAP";
		case PlanetProduction.DRIVE:
			return "DRIVE";
		case PlanetProduction.WEAPONS:
			return "WEAPONS";
		case PlanetProduction.SHIELDS:
			return "SHIELDS";
		case PlanetProduction.CARGO:
			return "CARGO";
		case PlanetProduction.SCIENCE:
			return "SCIENCE";
		case PlanetProduction.SHIP:
			return "SHIP";
		case PlanetProduction.UNKNOWN:
			return null;
		default:
			return null;
	}
}

/**
 * cargoLoadTypeFromFBS reverses `cargoLoadTypeToFBS` from
 * `submit.ts`. `PlanetRouteLoadType.UNKNOWN` and any out-of-band
 * value yield `null` so the caller drops the entry rather than
 * fabricating a synthetic load type.
 */
export function cargoLoadTypeFromFBS(
	value: PlanetRouteLoadType,
): CargoLoadType | null {
	switch (value) {
		case PlanetRouteLoadType.COL:
			return "COL";
		case PlanetRouteLoadType.CAP:
			return "CAP";
		case PlanetRouteLoadType.MAT:
			return "MAT";
		case PlanetRouteLoadType.EMP:
			return "EMP";
		case PlanetRouteLoadType.UNKNOWN:
			return null;
		default:
			return null;
	}
}

/**
 * shipGroupCargoFromFBS reverses `shipGroupCargoToFBS` from
 * `submit.ts`. `ShipGroupCargo.UNKNOWN` and any out-of-band value
 * yield `null` so the caller drops the entry rather than
 * fabricating a synthetic cargo type.
 */
export function shipGroupCargoFromFBS(
	value: ShipGroupCargo,
): ShipGroupCargoLiteral | null {
	switch (value) {
		case ShipGroupCargo.COL:
			return "COL";
		case ShipGroupCargo.CAP:
			return "CAP";
		case ShipGroupCargo.MAT:
			return "MAT";
		case ShipGroupCargo.UNKNOWN:
			return null;
		default:
			return null;
	}
}

/**
 * shipGroupUpgradeTechFromFBS reverses `shipGroupUpgradeTechToFBS`
 * from `submit.ts`. `ShipGroupUpgradeTech.UNKNOWN` and any
 * out-of-band value yield `null`.
 */
export function shipGroupUpgradeTechFromFBS(
	value: ShipGroupUpgradeTech,
): ShipGroupUpgradeTechLiteral | null {
	switch (value) {
		case ShipGroupUpgradeTech.ALL:
			return "ALL";
		case ShipGroupUpgradeTech.DRIVE:
			return "DRIVE";
		case ShipGroupUpgradeTech.WEAPONS:
			return "WEAPONS";
		case ShipGroupUpgradeTech.SHIELDS:
			return "SHIELDS";
		case ShipGroupUpgradeTech.CARGO:
			return "CARGO";
		case ShipGroupUpgradeTech.UNKNOWN:
			return null;
		default:
			return null;
	}
}

/**
 * relationFromFBS reverses `relationToFBS` from `submit.ts`.
 * `Relation.UNKNOWN` and any out-of-band value yield `null` so the
 * caller drops the entry rather than fabricating a synthetic stance.
 */
export function relationFromFBS(value: Relation): RelationLiteral | null {
	switch (value) {
		case Relation.WAR:
			return "WAR";
		case Relation.PEACE:
			return "PEACE";
		case Relation.UNKNOWN:
			return null;
		default:
			return null;
	}
}

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 };
	}
}
