// Battle-report fetcher used by the Battle Viewer page.
//
// Phase 28 migrates this surface off the raw REST passthrough onto the
// `user.games.battle` ConnectRPC command — the same signed envelope the
// other authenticated traffic rides. The synthetic-mode short-circuit
// stays so DEV / e2e tests can render fixtures without a live gateway.

import { Builder, ByteBuffer } from "flatbuffers";

import type { GalaxyClient } from "./galaxy-client";
import { uuidToHiLo } from "./game-state";
import { isSyntheticGameId } from "./synthetic-report";
import { lookupSyntheticBattle } from "./synthetic-battle";
import {
	BattleActionReport as FbsBattleActionReport,
	BattleReport as FbsBattleReport,
	BattleReportGroup as FbsBattleReportGroup,
	GameBattleRequest,
	RaceEntry,
	ShipEntry,
	UUID,
} from "../proto/galaxy/fbs/battle";
import { ErrorResponse as FbsErrorResponse } from "../proto/galaxy/fbs/lobby";

/**
 * BattleReport mirrors the on-wire battle shape the BattleViewer
 * renders. Fields match `pkg/model/report/battle.go`; integer-keyed
 * maps from the underlying model are surfaced as string-keyed
 * `Record`s so the existing components (race / ship lookup, mass
 * scaling, timeline) keep their current types.
 */
export interface BattleReport {
	id: string;
	planet: number;
	planetName: string;
	races: Record<string, string>;
	ships: Record<string, BattleReportGroup>;
	protocol: BattleActionReport[];
}

export interface BattleReportGroup {
	race: string;
	className: string;
	tech: Record<string, number>;
	num: number;
	numLeft: number;
	loadType: string;
	loadQuantity: number;
	inBattle: boolean;
}

export interface BattleActionReport {
	a: number;
	sa: number;
	d: number;
	sd: number;
	x: boolean;
}

export class BattleFetchError extends Error {
	constructor(
		public readonly status: number,
		message: string,
	) {
		super(message);
		this.name = "BattleFetchError";
	}
}

const MESSAGE_TYPE = "user.games.battle";
const RESULT_CODE_OK = "ok";

/**
 * fetchBattle returns the `BattleReport` for the supplied game, turn,
 * and battle id. In synthetic-report mode (DEV / e2e) the lookup is
 * served from `synthetic-battle.ts` and the `client` argument is
 * ignored; the in-game shell's synthetic branch deliberately never
 * publishes a `GalaxyClient`, so callers pass `null` on that path.
 * Otherwise the function calls the `user.games.battle` ConnectRPC
 * command through the supplied `GalaxyClient` and throws
 * `BattleFetchError` with the upstream HTTP status (or `0` for
 * transport-level failures) on error.
 */
export async function fetchBattle(
	client: GalaxyClient | null,
	gameId: string,
	turn: number,
	battleId: string,
): Promise<BattleReport> {
	if (isSyntheticGameId(gameId)) {
		const fixture = lookupSyntheticBattle(battleId);
		if (fixture === null) {
			throw new BattleFetchError(404, "battle not found");
		}
		return fixture;
	}

	if (client === null) {
		throw new BattleFetchError(0, "GalaxyClient is unavailable");
	}

	const payload = encodeRequest(gameId, turn, battleId);
	const result = await client.executeCommand(MESSAGE_TYPE, payload);
	if (result.resultCode !== RESULT_CODE_OK) {
		throw decodeError(result.resultCode, result.payloadBytes);
	}
	return decodeBattleReport(result.payloadBytes);
}

function encodeRequest(
	gameId: string,
	turn: number,
	battleId: string,
): Uint8Array {
	const builder = new Builder(96);
	const [gameHi, gameLo] = uuidToHiLo(gameId);
	const [battleHi, battleLo] = uuidToHiLo(battleId);
	GameBattleRequest.startGameBattleRequest(builder);
	GameBattleRequest.addGameId(
		builder,
		UUID.createUUID(builder, gameHi, gameLo),
	);
	GameBattleRequest.addTurn(builder, turn);
	GameBattleRequest.addBattleId(
		builder,
		UUID.createUUID(builder, battleHi, battleLo),
	);
	builder.finish(GameBattleRequest.endGameBattleRequest(builder));
	return builder.asUint8Array();
}

function decodeError(resultCode: string, payload: Uint8Array): BattleFetchError {
	let message = resultCode;
	try {
		const errorResponse = FbsErrorResponse.getRootAsErrorResponse(
			new ByteBuffer(payload),
		);
		const body = errorResponse.error();
		if (body) {
			message = body.message() ?? resultCode;
		}
	} catch (_err) {
		// fall through to the raw result code
	}
	const status = mapResultCodeToStatus(resultCode);
	return new BattleFetchError(status, message);
}

function mapResultCodeToStatus(resultCode: string): number {
	switch (resultCode) {
		case "not_found":
			return 404;
		case "invalid_request":
			return 400;
		case "forbidden":
			return 403;
		case "conflict":
			return 409;
		case "service_unavailable":
			return 503;
		default:
			return 500;
	}
}

function decodeBattleReport(bytes: Uint8Array): BattleReport {
	const fb = FbsBattleReport.getRootAsBattleReport(new ByteBuffer(bytes));
	const id = uuidStringFromFB(fb.id());
	if (id === null) {
		throw new BattleFetchError(500, "battle response missing id");
	}
	return {
		id,
		planet: Number(fb.planet()),
		planetName: fb.planetName() ?? "",
		races: decodeRaces(fb),
		ships: decodeShips(fb),
		protocol: decodeProtocol(fb),
	};
}

function decodeRaces(fb: FbsBattleReport): Record<string, string> {
	const out: Record<string, string> = {};
	const total = fb.racesLength();
	const item = new RaceEntry();
	for (let i = 0; i < total; i++) {
		if (!fb.races(i, item)) continue;
		const valueUUID = item.value();
		const value = uuidStringFromFB(valueUUID);
		if (value === null) continue;
		out[item.key().toString()] = value;
	}
	return out;
}

function decodeShips(fb: FbsBattleReport): Record<string, BattleReportGroup> {
	const out: Record<string, BattleReportGroup> = {};
	const total = fb.shipsLength();
	const entry = new ShipEntry();
	for (let i = 0; i < total; i++) {
		if (!fb.ships(i, entry)) continue;
		const group = entry.value();
		if (group === null) continue;
		out[entry.key().toString()] = decodeGroup(group);
	}
	return out;
}

function decodeGroup(group: FbsBattleReportGroup): BattleReportGroup {
	const tech: Record<string, number> = {};
	const techLen = group.techLength();
	for (let i = 0; i < techLen; i++) {
		const t = group.tech(i);
		if (!t) continue;
		const key = t.key();
		if (key === null) continue;
		tech[key] = t.value();
	}
	return {
		race: (group.race() ?? "") as string,
		className: (group.className() ?? "") as string,
		tech,
		num: Number(group.number()),
		numLeft: Number(group.numberLeft()),
		loadType: (group.loadType() ?? "") as string,
		loadQuantity: group.loadQuantity(),
		inBattle: group.inBattle(),
	};
}

function decodeProtocol(fb: FbsBattleReport): BattleActionReport[] {
	const out: BattleActionReport[] = [];
	const total = fb.protocolLength();
	const item = new FbsBattleActionReport();
	for (let i = 0; i < total; i++) {
		if (!fb.protocol(i, item)) continue;
		out.push({
			a: Number(item.attacker()),
			sa: Number(item.attackerShipClass()),
			d: Number(item.defender()),
			sd: Number(item.defenderShipClass()),
			x: item.destroyed(),
		});
	}
	return out;
}

function uuidStringFromFB(uuid: UUID | null): string | null {
	if (uuid === null) return null;
	const hi = uuid.hi();
	const lo = uuid.lo();
	const hex = bigUintTo16Hex(hi) + bigUintTo16Hex(lo);
	return (
		hex.slice(0, 8) +
		"-" +
		hex.slice(8, 12) +
		"-" +
		hex.slice(12, 16) +
		"-" +
		hex.slice(16, 20) +
		"-" +
		hex.slice(20, 32)
	);
}

function bigUintTo16Hex(value: bigint): string {
	return value.toString(16).padStart(16, "0");
}
