// Typed wrappers around `GalaxyClient.executeCommand` for the eight
// `user.games.mail.*` Phase 28 ConnectRPC commands. Each wrapper
// builds the matching FlatBuffers request, decodes the FlatBuffers
// response, and surfaces backend errors through `MailError` so callers
// branch on canonical codes (`invalid_request`, `forbidden`,
// `not_found`, `conflict`).

import { Builder, ByteBuffer } from "flatbuffers";

import type { GalaxyClient } from "./galaxy-client";
import { uuidToHiLo } from "./game-state";
import {
	AdminRequest,
	AdminResponse,
	BroadcastRequest,
	BroadcastResponse,
	DeleteRequest,
	DeleteResponse,
	InboxRequest,
	InboxResponse,
	MailMessage as FbsMailMessage,
	MailRecipientState as FbsMailRecipientState,
	MailBroadcastReceipt as FbsMailBroadcastReceipt,
	MessageGetRequest,
	MessageGetResponse,
	ReadRequest,
	ReadResponse,
	SendRequest,
	SendResponse,
	SentRequest,
	SentResponse,
} from "../proto/galaxy/fbs/diplomail";
import { UUID } from "../proto/galaxy/fbs/common";
import { ErrorResponse as FbsErrorResponse } from "../proto/galaxy/fbs/lobby";

/**
 * MailError represents a non-`ok` response from a mail RPC. Callers
 * branch on `code` for canonical error handling and use `message` for
 * inline UI surfacing.
 */
export class MailError extends Error {
	readonly resultCode: string;
	readonly code: string;

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

/**
 * MailMessage is the typed UI view of a `MailMessage` FlatBuffers row.
 * Nullable wire fields (`sender_user_id`, timestamps, translation
 * slots) become `null` here; the empty string from FB readers is
 * normalised to either `""` or `null` based on field semantics.
 */
export interface MailMessage {
	messageId: string;
	gameId: string;
	gameName: string;
	kind: string;
	senderKind: string;
	senderUserId: string | null;
	senderUsername: string | null;
	senderRaceName: string | null;
	subject: string;
	body: string;
	bodyLang: string;
	broadcastScope: string;
	createdAt: Date;
	recipientUserId: string;
	recipientUserName: string;
	recipientRaceName: string | null;
	readAt: Date | null;
	deletedAt: Date | null;
	translatedSubject: string | null;
	translatedBody: string | null;
	translationLang: string | null;
	translator: string | null;
}

export interface MailRecipientState {
	messageId: string;
	readAt: Date | null;
	deletedAt: Date | null;
}

export interface MailBroadcastReceipt {
	messageId: string;
	gameId: string;
	gameName: string;
	kind: string;
	senderKind: string;
	subject: string;
	body: string;
	bodyLang: string;
	broadcastScope: string;
	createdAt: Date;
	recipientCount: number;
}

export interface SendPersonalArgs {
	gameId: string;
	raceName: string;
	subject?: string;
	body: string;
}

export interface SendBroadcastArgs {
	gameId: string;
	subject?: string;
	body: string;
}

export type AdminTarget = "user" | "all";

export interface SendAdminArgs {
	gameId: string;
	target: AdminTarget;
	raceName?: string;
	recipientUserId?: string;
	recipients?: string;
	subject?: string;
	body: string;
}

const MESSAGE_TYPE_INBOX = "user.games.mail.inbox";
const MESSAGE_TYPE_SENT = "user.games.mail.sent";
const MESSAGE_TYPE_GET = "user.games.mail.message.get";
const MESSAGE_TYPE_SEND = "user.games.mail.send";
const MESSAGE_TYPE_BROADCAST = "user.games.mail.broadcast";
const MESSAGE_TYPE_ADMIN = "user.games.mail.admin";
const MESSAGE_TYPE_READ = "user.games.mail.read";
const MESSAGE_TYPE_DELETE = "user.games.mail.delete";

const RESULT_CODE_OK = "ok";

export async function fetchInbox(
	client: GalaxyClient,
	gameId: string,
): Promise<MailMessage[]> {
	const builder = new Builder(64);
	const [hi, lo] = uuidToHiLo(gameId);
	InboxRequest.startInboxRequest(builder);
	InboxRequest.addGameId(builder, UUID.createUUID(builder, hi, lo));
	builder.finish(InboxRequest.endInboxRequest(builder));
	const payload = await execute(client, MESSAGE_TYPE_INBOX, builder.asUint8Array());
	const response = InboxResponse.getRootAsInboxResponse(new ByteBuffer(payload));
	return readMessageList(response.itemsLength.bind(response), (i) => response.items(i));
}

export async function fetchSent(
	client: GalaxyClient,
	gameId: string,
): Promise<MailMessage[]> {
	const builder = new Builder(64);
	const [hi, lo] = uuidToHiLo(gameId);
	SentRequest.startSentRequest(builder);
	SentRequest.addGameId(builder, UUID.createUUID(builder, hi, lo));
	builder.finish(SentRequest.endSentRequest(builder));
	const payload = await execute(client, MESSAGE_TYPE_SENT, builder.asUint8Array());
	const response = SentResponse.getRootAsSentResponse(new ByteBuffer(payload));
	return readMessageList(response.itemsLength.bind(response), (i) => response.items(i));
}

export async function fetchMessage(
	client: GalaxyClient,
	gameId: string,
	messageId: string,
): Promise<MailMessage> {
	const builder = new Builder(64);
	const [ghi, glo] = uuidToHiLo(gameId);
	const [mhi, mlo] = uuidToHiLo(messageId);
	MessageGetRequest.startMessageGetRequest(builder);
	MessageGetRequest.addGameId(builder, UUID.createUUID(builder, ghi, glo));
	MessageGetRequest.addMessageId(builder, UUID.createUUID(builder, mhi, mlo));
	builder.finish(MessageGetRequest.endMessageGetRequest(builder));
	const payload = await execute(client, MESSAGE_TYPE_GET, builder.asUint8Array());
	const response = MessageGetResponse.getRootAsMessageGetResponse(new ByteBuffer(payload));
	const fb = response.message();
	if (fb === null) {
		throw new MailError("internal_error", "internal_error", "message missing in response");
	}
	return decodeMailMessage(fb);
}

export async function sendPersonal(
	client: GalaxyClient,
	input: SendPersonalArgs,
): Promise<MailMessage> {
	const builder = new Builder(256);
	const [hi, lo] = uuidToHiLo(input.gameId);
	const raceOff = builder.createString(input.raceName);
	const subjectOff = builder.createString(input.subject ?? "");
	const bodyOff = builder.createString(input.body);
	SendRequest.startSendRequest(builder);
	SendRequest.addGameId(builder, UUID.createUUID(builder, hi, lo));
	SendRequest.addRecipientRaceName(builder, raceOff);
	SendRequest.addSubject(builder, subjectOff);
	SendRequest.addBody(builder, bodyOff);
	builder.finish(SendRequest.endSendRequest(builder));
	const payload = await execute(client, MESSAGE_TYPE_SEND, builder.asUint8Array());
	const response = SendResponse.getRootAsSendResponse(new ByteBuffer(payload));
	const fb = response.message();
	if (fb === null) {
		throw new MailError("internal_error", "internal_error", "message missing in response");
	}
	return decodeMailMessage(fb);
}

export async function sendBroadcast(
	client: GalaxyClient,
	input: SendBroadcastArgs,
): Promise<MailBroadcastReceipt> {
	const builder = new Builder(256);
	const [hi, lo] = uuidToHiLo(input.gameId);
	const subjectOff = builder.createString(input.subject ?? "");
	const bodyOff = builder.createString(input.body);
	BroadcastRequest.startBroadcastRequest(builder);
	BroadcastRequest.addGameId(builder, UUID.createUUID(builder, hi, lo));
	BroadcastRequest.addSubject(builder, subjectOff);
	BroadcastRequest.addBody(builder, bodyOff);
	builder.finish(BroadcastRequest.endBroadcastRequest(builder));
	const payload = await execute(client, MESSAGE_TYPE_BROADCAST, builder.asUint8Array());
	const response = BroadcastResponse.getRootAsBroadcastResponse(new ByteBuffer(payload));
	const fb = response.receipt();
	if (fb === null) {
		throw new MailError("internal_error", "internal_error", "receipt missing in response");
	}
	return decodeMailBroadcastReceipt(fb);
}

export async function sendAdmin(
	client: GalaxyClient,
	input: SendAdminArgs,
): Promise<MailMessage | MailBroadcastReceipt> {
	const builder = new Builder(256);
	const [hi, lo] = uuidToHiLo(input.gameId);
	const targetOff = builder.createString(input.target);
	const recipientUserOff = builder.createString(input.recipientUserId ?? "");
	const recipientRaceOff = builder.createString(input.raceName ?? "");
	const recipientsOff = builder.createString(input.recipients ?? "");
	const subjectOff = builder.createString(input.subject ?? "");
	const bodyOff = builder.createString(input.body);
	AdminRequest.startAdminRequest(builder);
	AdminRequest.addGameId(builder, UUID.createUUID(builder, hi, lo));
	AdminRequest.addTarget(builder, targetOff);
	AdminRequest.addRecipientUserId(builder, recipientUserOff);
	AdminRequest.addRecipientRaceName(builder, recipientRaceOff);
	AdminRequest.addRecipients(builder, recipientsOff);
	AdminRequest.addSubject(builder, subjectOff);
	AdminRequest.addBody(builder, bodyOff);
	builder.finish(AdminRequest.endAdminRequest(builder));
	const payload = await execute(client, MESSAGE_TYPE_ADMIN, builder.asUint8Array());
	const response = AdminResponse.getRootAsAdminResponse(new ByteBuffer(payload));
	const receipt = response.receipt();
	if (receipt !== null) {
		return decodeMailBroadcastReceipt(receipt);
	}
	const message = response.message();
	if (message !== null) {
		return decodeMailMessage(message);
	}
	throw new MailError("internal_error", "internal_error", "admin response carried neither message nor receipt");
}

export async function markRead(
	client: GalaxyClient,
	gameId: string,
	messageId: string,
): Promise<MailRecipientState> {
	const builder = new Builder(64);
	const [ghi, glo] = uuidToHiLo(gameId);
	const [mhi, mlo] = uuidToHiLo(messageId);
	ReadRequest.startReadRequest(builder);
	ReadRequest.addGameId(builder, UUID.createUUID(builder, ghi, glo));
	ReadRequest.addMessageId(builder, UUID.createUUID(builder, mhi, mlo));
	builder.finish(ReadRequest.endReadRequest(builder));
	const payload = await execute(client, MESSAGE_TYPE_READ, builder.asUint8Array());
	const response = ReadResponse.getRootAsReadResponse(new ByteBuffer(payload));
	const fb = response.state();
	if (fb === null) {
		throw new MailError("internal_error", "internal_error", "state missing in response");
	}
	return decodeMailRecipientState(fb);
}

export async function deleteMessage(
	client: GalaxyClient,
	gameId: string,
	messageId: string,
): Promise<MailRecipientState> {
	const builder = new Builder(64);
	const [ghi, glo] = uuidToHiLo(gameId);
	const [mhi, mlo] = uuidToHiLo(messageId);
	DeleteRequest.startDeleteRequest(builder);
	DeleteRequest.addGameId(builder, UUID.createUUID(builder, ghi, glo));
	DeleteRequest.addMessageId(builder, UUID.createUUID(builder, mhi, mlo));
	builder.finish(DeleteRequest.endDeleteRequest(builder));
	const payload = await execute(client, MESSAGE_TYPE_DELETE, builder.asUint8Array());
	const response = DeleteResponse.getRootAsDeleteResponse(new ByteBuffer(payload));
	const fb = response.state();
	if (fb === null) {
		throw new MailError("internal_error", "internal_error", "state missing in response");
	}
	return decodeMailRecipientState(fb);
}

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 decodeMailError(result.resultCode, result.payloadBytes);
	}
	return result.payloadBytes;
}

function decodeMailError(resultCode: string, payload: Uint8Array): MailError {
	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
	}
	return new MailError(resultCode, code, message);
}

function readMessageList(
	lengthFn: () => number,
	getFn: (i: number) => FbsMailMessage | null,
): MailMessage[] {
	const total = lengthFn();
	const out: MailMessage[] = [];
	for (let i = 0; i < total; i++) {
		const item = getFn(i);
		if (item) {
			out.push(decodeMailMessage(item));
		}
	}
	return out;
}

function decodeMailMessage(fb: FbsMailMessage): MailMessage {
	return {
		messageId: fb.messageId() ?? "",
		gameId: fb.gameId() ?? "",
		gameName: fb.gameName() ?? "",
		kind: fb.kind() ?? "",
		senderKind: fb.senderKind() ?? "",
		senderUserId: optionalString(fb.senderUserId()),
		senderUsername: optionalString(fb.senderUsername()),
		senderRaceName: optionalString(fb.senderRaceName()),
		subject: fb.subject() ?? "",
		body: fb.body() ?? "",
		bodyLang: fb.bodyLang() ?? "",
		broadcastScope: fb.broadcastScope() ?? "",
		createdAt: dateFromMs(fb.createdAtMs()),
		recipientUserId: fb.recipientUserId() ?? "",
		recipientUserName: fb.recipientUserName() ?? "",
		recipientRaceName: optionalString(fb.recipientRaceName()),
		readAt: optionalDateFromMs(fb.readAtMs()),
		deletedAt: optionalDateFromMs(fb.deletedAtMs()),
		translatedSubject: optionalString(fb.translatedSubject()),
		translatedBody: optionalString(fb.translatedBody()),
		translationLang: optionalString(fb.translationLang()),
		translator: optionalString(fb.translator()),
	};
}

function decodeMailRecipientState(fb: FbsMailRecipientState): MailRecipientState {
	return {
		messageId: fb.messageId() ?? "",
		readAt: optionalDateFromMs(fb.readAtMs()),
		deletedAt: optionalDateFromMs(fb.deletedAtMs()),
	};
}

function decodeMailBroadcastReceipt(fb: FbsMailBroadcastReceipt): MailBroadcastReceipt {
	return {
		messageId: fb.messageId() ?? "",
		gameId: fb.gameId() ?? "",
		gameName: fb.gameName() ?? "",
		kind: fb.kind() ?? "",
		senderKind: fb.senderKind() ?? "",
		subject: fb.subject() ?? "",
		body: fb.body() ?? "",
		bodyLang: fb.bodyLang() ?? "",
		broadcastScope: fb.broadcastScope() ?? "",
		createdAt: dateFromMs(fb.createdAtMs()),
		recipientCount: fb.recipientCount(),
	};
}

function optionalString(value: string | null | undefined): string | null {
	if (value === null || value === undefined || value === "") {
		return null;
	}
	return value;
}

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