// Typed wrappers around `GalaxyClient.executeCommand` for the Phase 8
// lobby command catalog. Each wrapper builds a FlatBuffers request
// payload via the generated TS bindings, calls `executeCommand`, then
// decodes the FlatBuffers response payload. Errors carrying a non-`ok`
// `result_code` are surfaced as a thrown `LobbyError` so callers can
// branch on canonical lobby error codes (`invalid_request`,
// `subject_not_found`, `forbidden`, `conflict`, `internal_error`).

import { Builder, ByteBuffer } from "flatbuffers";

import type { GalaxyClient } from "./galaxy-client";
import {
	ApplicationSubmitRequest,
	ApplicationSubmitResponse,
	ApplicationSummary as FbsApplicationSummary,
	ErrorResponse as FbsErrorResponse,
	GameCreateRequest,
	GameCreateResponse,
	GameSummary as FbsGameSummary,
	InviteDeclineRequest,
	InviteDeclineResponse,
	InviteRedeemRequest,
	InviteRedeemResponse,
	InviteSummary as FbsInviteSummary,
	MyApplicationsListRequest,
	MyApplicationsListResponse,
	MyGamesListRequest,
	MyGamesListResponse,
	MyInvitesListRequest,
	MyInvitesListResponse,
	PublicGamesListRequest,
	PublicGamesListResponse,
} from "../proto/galaxy/fbs/lobby";

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

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

export interface GameSummary {
	gameId: string;
	gameName: string;
	gameType: string;
	status: string;
	ownerUserId: string;
	minPlayers: number;
	maxPlayers: number;
	enrollmentEndsAt: Date;
	createdAt: Date;
	updatedAt: Date;
	/**
	 * Most recent turn number observed by backend's runtime
	 * projection. Zero before the engine produces its first
	 * snapshot. The map view uses this to fetch the matching
	 * `user.games.report` without a separate state query.
	 */
	currentTurn: number;
}

export interface PublicGamesPage {
	items: GameSummary[];
	page: number;
	pageSize: number;
	total: number;
}

export interface ApplicationSummary {
	applicationId: string;
	gameId: string;
	applicantUserId: string;
	raceName: string;
	status: string;
	createdAt: Date;
	decidedAt: Date | null;
}

export interface InviteSummary {
	inviteId: string;
	gameId: string;
	inviterUserId: string;
	invitedUserId: string;
	code: string;
	raceName: string;
	status: string;
	createdAt: Date;
	expiresAt: Date;
	decidedAt: Date | null;
}

export interface CreateGameInput {
	gameName: string;
	description: string;
	minPlayers: number;
	maxPlayers: number;
	startGapHours: number;
	startGapPlayers: number;
	enrollmentEndsAt: Date;
	turnSchedule: string;
	targetEngineVersion: string;
}

const RESULT_CODE_OK = "ok";

export async function listMyGames(client: GalaxyClient): Promise<GameSummary[]> {
	const builder = new Builder(32);
	MyGamesListRequest.startMyGamesListRequest(builder);
	builder.finish(MyGamesListRequest.endMyGamesListRequest(builder));
	const payload = await execute(client, "lobby.my.games.list", builder.asUint8Array());
	const response = MyGamesListResponse.getRootAsMyGamesListResponse(new ByteBuffer(payload));
	const out: GameSummary[] = [];
	for (let i = 0; i < response.itemsLength(); i++) {
		const item = response.items(i);
		if (item) {
			out.push(decodeGameSummary(item));
		}
	}
	return out;
}

export async function listPublicGames(
	client: GalaxyClient,
	options: { page?: number; pageSize?: number } = {},
): Promise<PublicGamesPage> {
	const page = options.page ?? 1;
	const pageSize = options.pageSize ?? 50;
	const builder = new Builder(32);
	PublicGamesListRequest.startPublicGamesListRequest(builder);
	PublicGamesListRequest.addPage(builder, page);
	PublicGamesListRequest.addPageSize(builder, pageSize);
	builder.finish(PublicGamesListRequest.endPublicGamesListRequest(builder));
	const payload = await execute(client, "lobby.public.games.list", builder.asUint8Array());
	const response = PublicGamesListResponse.getRootAsPublicGamesListResponse(
		new ByteBuffer(payload),
	);
	const out: GameSummary[] = [];
	for (let i = 0; i < response.itemsLength(); i++) {
		const item = response.items(i);
		if (item) {
			out.push(decodeGameSummary(item));
		}
	}
	return {
		items: out,
		page: response.page(),
		pageSize: response.pageSize(),
		total: response.total(),
	};
}

export async function listMyApplications(
	client: GalaxyClient,
): Promise<ApplicationSummary[]> {
	const builder = new Builder(32);
	MyApplicationsListRequest.startMyApplicationsListRequest(builder);
	builder.finish(MyApplicationsListRequest.endMyApplicationsListRequest(builder));
	const payload = await execute(client, "lobby.my.applications.list", builder.asUint8Array());
	const response = MyApplicationsListResponse.getRootAsMyApplicationsListResponse(
		new ByteBuffer(payload),
	);
	const out: ApplicationSummary[] = [];
	for (let i = 0; i < response.itemsLength(); i++) {
		const item = response.items(i);
		if (item) {
			out.push(decodeApplicationSummary(item));
		}
	}
	return out;
}

export async function listMyInvites(client: GalaxyClient): Promise<InviteSummary[]> {
	const builder = new Builder(32);
	MyInvitesListRequest.startMyInvitesListRequest(builder);
	builder.finish(MyInvitesListRequest.endMyInvitesListRequest(builder));
	const payload = await execute(client, "lobby.my.invites.list", builder.asUint8Array());
	const response = MyInvitesListResponse.getRootAsMyInvitesListResponse(new ByteBuffer(payload));
	const out: InviteSummary[] = [];
	for (let i = 0; i < response.itemsLength(); i++) {
		const item = response.items(i);
		if (item) {
			out.push(decodeInviteSummary(item));
		}
	}
	return out;
}

export async function createGame(
	client: GalaxyClient,
	input: CreateGameInput,
): Promise<GameSummary> {
	const builder = new Builder(256);
	const gameNameOff = builder.createString(input.gameName);
	const descriptionOff = builder.createString(input.description);
	const turnScheduleOff = builder.createString(input.turnSchedule);
	const targetEngineVersionOff = builder.createString(input.targetEngineVersion);
	GameCreateRequest.startGameCreateRequest(builder);
	GameCreateRequest.addGameName(builder, gameNameOff);
	GameCreateRequest.addDescription(builder, descriptionOff);
	GameCreateRequest.addMinPlayers(builder, input.minPlayers);
	GameCreateRequest.addMaxPlayers(builder, input.maxPlayers);
	GameCreateRequest.addStartGapHours(builder, input.startGapHours);
	GameCreateRequest.addStartGapPlayers(builder, input.startGapPlayers);
	GameCreateRequest.addEnrollmentEndsAtMs(builder, BigInt(input.enrollmentEndsAt.getTime()));
	GameCreateRequest.addTurnSchedule(builder, turnScheduleOff);
	GameCreateRequest.addTargetEngineVersion(builder, targetEngineVersionOff);
	builder.finish(GameCreateRequest.endGameCreateRequest(builder));
	const payload = await execute(client, "lobby.game.create", builder.asUint8Array());
	const response = GameCreateResponse.getRootAsGameCreateResponse(new ByteBuffer(payload));
	const game = response.game();
	if (game === null) {
		throw new LobbyError("internal_error", "internal_error", "game missing in response");
	}
	return decodeGameSummary(game);
}

export async function submitApplication(
	client: GalaxyClient,
	gameId: string,
	raceName: string,
): Promise<ApplicationSummary> {
	const builder = new Builder(128);
	const gameIdOff = builder.createString(gameId);
	const raceNameOff = builder.createString(raceName);
	ApplicationSubmitRequest.startApplicationSubmitRequest(builder);
	ApplicationSubmitRequest.addGameId(builder, gameIdOff);
	ApplicationSubmitRequest.addRaceName(builder, raceNameOff);
	builder.finish(ApplicationSubmitRequest.endApplicationSubmitRequest(builder));
	const payload = await execute(client, "lobby.application.submit", builder.asUint8Array());
	const response = ApplicationSubmitResponse.getRootAsApplicationSubmitResponse(
		new ByteBuffer(payload),
	);
	const application = response.application();
	if (application === null) {
		throw new LobbyError("internal_error", "internal_error", "application missing in response");
	}
	return decodeApplicationSummary(application);
}

export async function redeemInvite(
	client: GalaxyClient,
	gameId: string,
	inviteId: string,
): Promise<InviteSummary> {
	const builder = new Builder(128);
	const gameIdOff = builder.createString(gameId);
	const inviteIdOff = builder.createString(inviteId);
	InviteRedeemRequest.startInviteRedeemRequest(builder);
	InviteRedeemRequest.addGameId(builder, gameIdOff);
	InviteRedeemRequest.addInviteId(builder, inviteIdOff);
	builder.finish(InviteRedeemRequest.endInviteRedeemRequest(builder));
	const payload = await execute(client, "lobby.invite.redeem", builder.asUint8Array());
	const response = InviteRedeemResponse.getRootAsInviteRedeemResponse(new ByteBuffer(payload));
	const invite = response.invite();
	if (invite === null) {
		throw new LobbyError("internal_error", "internal_error", "invite missing in response");
	}
	return decodeInviteSummary(invite);
}

export async function declineInvite(
	client: GalaxyClient,
	gameId: string,
	inviteId: string,
): Promise<InviteSummary> {
	const builder = new Builder(128);
	const gameIdOff = builder.createString(gameId);
	const inviteIdOff = builder.createString(inviteId);
	InviteDeclineRequest.startInviteDeclineRequest(builder);
	InviteDeclineRequest.addGameId(builder, gameIdOff);
	InviteDeclineRequest.addInviteId(builder, inviteIdOff);
	builder.finish(InviteDeclineRequest.endInviteDeclineRequest(builder));
	const payload = await execute(client, "lobby.invite.decline", builder.asUint8Array());
	const response = InviteDeclineResponse.getRootAsInviteDeclineResponse(
		new ByteBuffer(payload),
	);
	const invite = response.invite();
	if (invite === null) {
		throw new LobbyError("internal_error", "internal_error", "invite missing in response");
	}
	return decodeInviteSummary(invite);
}

async function execute(
	client: GalaxyClient,
	messageType: string,
	payloadBytes: Uint8Array,
): Promise<Uint8Array> {
	const result = await client.executeCommand(messageType, payloadBytes);
	if (result.resultCode !== RESULT_CODE_OK) {
		throw decodeLobbyError(result.resultCode, result.payloadBytes);
	}
	return result.payloadBytes;
}

function decodeLobbyError(resultCode: string, payload: Uint8Array): LobbyError {
	let code = resultCode;
	let message = resultCode;
	try {
		const errorResponse = FbsErrorResponse.getRootAsErrorResponse(new ByteBuffer(payload));
		const body = errorResponse.error();
		if (body) {
			code = body.code() ?? resultCode;
			message = body.message() ?? resultCode;
		}
	} catch (_err) {
		// fall through to use raw resultCode as both code and message
	}
	return new LobbyError(resultCode, code, message);
}

function decodeGameSummary(summary: FbsGameSummary): GameSummary {
	return {
		gameId: summary.gameId() ?? "",
		gameName: summary.gameName() ?? "",
		gameType: summary.gameType() ?? "",
		status: summary.status() ?? "",
		ownerUserId: summary.ownerUserId() ?? "",
		minPlayers: summary.minPlayers(),
		maxPlayers: summary.maxPlayers(),
		enrollmentEndsAt: dateFromMs(summary.enrollmentEndsAtMs()),
		createdAt: dateFromMs(summary.createdAtMs()),
		updatedAt: dateFromMs(summary.updatedAtMs()),
		currentTurn: summary.currentTurn(),
	};
}

function decodeApplicationSummary(app: FbsApplicationSummary): ApplicationSummary {
	return {
		applicationId: app.applicationId() ?? "",
		gameId: app.gameId() ?? "",
		applicantUserId: app.applicantUserId() ?? "",
		raceName: app.raceName() ?? "",
		status: app.status() ?? "",
		createdAt: dateFromMs(app.createdAtMs()),
		decidedAt: optionalDateFromMs(app.decidedAtMs()),
	};
}

function decodeInviteSummary(invite: FbsInviteSummary): InviteSummary {
	return {
		inviteId: invite.inviteId() ?? "",
		gameId: invite.gameId() ?? "",
		inviterUserId: invite.inviterUserId() ?? "",
		invitedUserId: invite.invitedUserId() ?? "",
		code: invite.code() ?? "",
		raceName: invite.raceName() ?? "",
		status: invite.status() ?? "",
		createdAt: dateFromMs(invite.createdAtMs()),
		expiresAt: dateFromMs(invite.expiresAtMs()),
		decidedAt: optionalDateFromMs(invite.decidedAtMs()),
	};
}

function dateFromMs(ms: bigint): Date {
	return new Date(Number(ms));
}

function optionalDateFromMs(ms: bigint): Date | null {
	if (ms === 0n) {
		return null;
	}
	return new Date(Number(ms));
}
