<script lang="ts">
	import { goto } from "$app/navigation";
	import { onMount } from "svelte";

	import { createEdgeGatewayClient } from "../../api/connect";
	import { GalaxyClient } from "../../api/galaxy-client";
	import {
		LobbyError,
		listMyApplications,
		listMyGames,
		listMyInvites,
		listPublicGames,
		redeemInvite,
		declineInvite,
		submitApplication,
		type ApplicationSummary,
		type GameSummary,
		type InviteSummary,
	} from "../../api/lobby";
	import { ByteBuffer } from "flatbuffers";
	import { AccountResponse } from "../../proto/galaxy/fbs/user";
	import { GATEWAY_BASE_URL, GATEWAY_RESPONSE_PUBLIC_KEY } from "$lib/env";
	import {
		SyntheticReportError,
		loadSyntheticReportFromJSON,
	} from "../../api/synthetic-report";
	import { i18n, type TranslationKey } from "$lib/i18n/index.svelte";
	import { loadCore } from "../../platform/core/index";
	import { session } from "$lib/session-store.svelte";
	import { Builder } from "flatbuffers";
	import { GetMyAccountRequest } from "../../proto/galaxy/fbs/user";

	let displayName: string | null = $state(null);
	let configError: string | null = $state(null);
	let listsLoading = $state(true);
	let lobbyError: string | null = $state(null);

	let myGames: GameSummary[] = $state([]);
	let invitations: InviteSummary[] = $state([]);
	let applications: ApplicationSummary[] = $state([]);
	let publicGames: GameSummary[] = $state([]);

	let openApplicationFor: string | null = $state(null);
	let raceNameInput = $state("");
	let raceNameError: string | null = $state(null);
	let submittingApplication = $state(false);

	let inviteActionInFlight: string | null = $state(null);

	let syntheticError: string | null = $state(null);

	let client: GalaxyClient | null = null;

	async function logout(): Promise<void> {
		await session.signOut("user");
	}

	async function sha256(payload: Uint8Array): Promise<Uint8Array> {
		const digest = await crypto.subtle.digest("SHA-256", payload as BufferSource);
		return new Uint8Array(digest);
	}

	function describeLobbyError(err: unknown): string {
		if (err instanceof LobbyError) {
			const key = `lobby.error.${err.code}` as TranslationKey;
			const translated = i18n.t(key);
			if (translated !== key) {
				return translated;
			}
			return i18n.t("lobby.error.unknown", { message: err.message });
		}
		return err instanceof Error ? err.message : "request failed";
	}

	async function refreshAll(): Promise<void> {
		if (client === null) return;
		listsLoading = true;
		lobbyError = null;
		try {
			const [games, invites, apps, publicPage] = await Promise.all([
				listMyGames(client),
				listMyInvites(client),
				listMyApplications(client),
				listPublicGames(client),
			]);
			myGames = games;
			invitations = invites.filter((invite) => invite.status === "pending");
			applications = apps;
			publicGames = publicPage.items;
		} catch (err) {
			lobbyError = describeLobbyError(err);
		} finally {
			listsLoading = false;
		}
	}

	function applicationStatusLabel(status: string): string {
		const key = `lobby.application.status.${status}` as TranslationKey;
		const translated = i18n.t(key);
		if (translated === key) {
			return i18n.t("lobby.application.status.unknown", { status });
		}
		return translated;
	}

	function openApplicationForm(gameId: string): void {
		openApplicationFor = gameId;
		raceNameInput = "";
		raceNameError = null;
	}

	function cancelApplicationForm(): void {
		openApplicationFor = null;
		raceNameInput = "";
		raceNameError = null;
	}

	async function submitApplicationFor(gameId: string): Promise<void> {
		if (client === null) return;
		const trimmed = raceNameInput.trim();
		if (trimmed === "") {
			raceNameError = i18n.t("lobby.application.race_name_required");
			return;
		}
		submittingApplication = true;
		raceNameError = null;
		try {
			const result = await submitApplication(client, gameId, trimmed);
			applications = [result, ...applications];
			openApplicationFor = null;
			raceNameInput = "";
		} catch (err) {
			raceNameError = describeLobbyError(err);
		} finally {
			submittingApplication = false;
		}
	}

	async function acceptInvite(invite: InviteSummary): Promise<void> {
		if (client === null) return;
		inviteActionInFlight = invite.inviteId;
		try {
			await redeemInvite(client, invite.gameId, invite.inviteId);
			invitations = invitations.filter((i) => i.inviteId !== invite.inviteId);
			myGames = await listMyGames(client);
		} catch (err) {
			lobbyError = describeLobbyError(err);
		} finally {
			inviteActionInFlight = null;
		}
	}

	async function rejectInvite(invite: InviteSummary): Promise<void> {
		if (client === null) return;
		inviteActionInFlight = invite.inviteId;
		try {
			await declineInvite(client, invite.gameId, invite.inviteId);
			invitations = invitations.filter((i) => i.inviteId !== invite.inviteId);
		} catch (err) {
			lobbyError = describeLobbyError(err);
		} finally {
			inviteActionInFlight = null;
		}
	}

	async function loadGreeting(c: GalaxyClient): Promise<void> {
		const builder = new Builder(32);
		GetMyAccountRequest.startGetMyAccountRequest(builder);
		builder.finish(GetMyAccountRequest.endGetMyAccountRequest(builder));
		const result = await c.executeCommand("user.account.get", builder.asUint8Array());
		if (result.resultCode !== "ok") {
			return;
		}
		const response = AccountResponse.getRootAsAccountResponse(
			new ByteBuffer(result.payloadBytes),
		);
		const account = response.account();
		if (account === null) {
			return;
		}
		const display = account.displayName();
		const userName = account.userName();
		displayName = display && display.length > 0 ? display : userName;
	}

	function gotoCreate(): void {
		goto("/lobby/create");
	}

	function gotoGame(gameId: string): void {
		goto(`/games/${gameId}/map`);
	}

	async function onSyntheticFileChange(
		event: Event & { currentTarget: HTMLInputElement },
	): Promise<void> {
		// Capture the element synchronously: `event.currentTarget`
		// is nulled by the time any of the awaits below resolve, and
		// reaching for it from the `finally` block then throws
		// "null is not an object". The reset still has to happen so
		// re-selecting the same file fires `change` again.
		const input = event.currentTarget;
		syntheticError = null;
		const file = input.files?.[0];
		if (file === undefined) return;
		try {
			const text = await file.text();
			const json: unknown = JSON.parse(text);
			const { gameId } = loadSyntheticReportFromJSON(json);
			await goto(`/games/${gameId}/map`);
		} catch (err) {
			if (err instanceof SyntheticReportError) {
				syntheticError = err.message;
			} else if (err instanceof SyntaxError) {
				syntheticError = `invalid JSON: ${err.message}`;
			} else if (err instanceof Error) {
				syntheticError = err.message;
			} else {
				syntheticError = "failed to load synthetic report";
			}
		} finally {
			input.value = "";
		}
	}

	// Statuses for which the game has a navigable in-game view.
	// Lobby-internal statuses (draft, enrollment_open, ready_to_start,
	// starting, start_failed) and terminal ones (cancelled) stay
	// non-clickable; clicking them otherwise lands on a 404 because
	// /games/:id/map only meaningfully exists once the runtime has
	// produced game state.
	function isPlayableStatus(status: string): boolean {
		return status === "running" || status === "paused" || status === "finished";
	}

	onMount(async () => {
		if (
			session.keypair === null ||
			session.deviceSessionId === null ||
			GATEWAY_RESPONSE_PUBLIC_KEY.length === 0
		) {
			listsLoading = false;
			if (GATEWAY_RESPONSE_PUBLIC_KEY.length === 0) {
				configError = "VITE_GATEWAY_RESPONSE_PUBLIC_KEY is not configured";
			}
			return;
		}
		const keypair = session.keypair;
		try {
			const core = await loadCore();
			client = new GalaxyClient({
				core,
				edge: createEdgeGatewayClient(GATEWAY_BASE_URL),
				signer: (canonical) => keypair.sign(canonical),
				sha256,
				deviceSessionId: session.deviceSessionId,
				gatewayResponsePublicKey: GATEWAY_RESPONSE_PUBLIC_KEY,
			});
			loadGreeting(client).catch(() => {});
			await refreshAll();
		} catch (err) {
			lobbyError = describeLobbyError(err);
			listsLoading = false;
		}
	});
</script>

<main>
	<header>
		<h1>{i18n.t("lobby.title")}</h1>
		<p>
			{i18n.t("lobby.device_session_id_label")}:
			<code data-testid="device-session-id">{session.deviceSessionId ?? ""}</code>
		</p>
		{#if displayName !== null}
			<p data-testid="account-greeting">
				{i18n.t("lobby.greeting", { name: displayName })}
			</p>
		{/if}
		<button onclick={logout} data-testid="lobby-logout">
			{i18n.t("lobby.logout")}
		</button>
	</header>

	{#if configError !== null}
		<p role="alert" data-testid="account-error">{configError}</p>
	{:else if lobbyError !== null}
		<p role="alert" data-testid="lobby-error">{lobbyError}</p>
	{/if}

	<section data-testid="lobby-create-section">
		<button onclick={gotoCreate} data-testid="lobby-create-button">
			{i18n.t("lobby.create_button")}
		</button>
	</section>

	<section data-testid="lobby-my-games-section">
		<h2>{i18n.t("lobby.section.my_games")}</h2>
		{#if listsLoading}
			<p>{i18n.t("lobby.list_loading")}</p>
		{:else if myGames.length === 0}
			<p data-testid="lobby-my-games-empty">{i18n.t("lobby.my_games.empty")}</p>
		{:else}
			<ul class="card-list">
				{#each myGames as game (game.gameId)}
					<li>
						<button
							class="card"
							onclick={() => gotoGame(game.gameId)}
							disabled={!isPlayableStatus(game.status)}
							data-testid="lobby-my-game-card"
						>
							<strong>{game.gameName}</strong>
							<span class="meta">{game.status}</span>
							<span class="meta">{game.minPlayers}–{game.maxPlayers} players</span>
						</button>
					</li>
				{/each}
			</ul>
		{/if}
	</section>

	<section data-testid="lobby-invitations-section">
		<h2>{i18n.t("lobby.section.invitations")}</h2>
		{#if listsLoading}
			<p>{i18n.t("lobby.list_loading")}</p>
		{:else if invitations.length === 0}
			<p data-testid="lobby-invitations-empty">{i18n.t("lobby.invitations.empty")}</p>
		{:else}
			<ul class="card-list">
				{#each invitations as invite (invite.inviteId)}
					<li class="card">
						<strong>{invite.raceName}</strong>
						<span class="meta">{invite.gameId}</span>
						<div class="actions">
							<button
								onclick={() => acceptInvite(invite)}
								disabled={inviteActionInFlight === invite.inviteId}
								data-testid="lobby-invite-accept"
							>
								{i18n.t("lobby.invitation.accept")}
							</button>
							<button
								onclick={() => rejectInvite(invite)}
								disabled={inviteActionInFlight === invite.inviteId}
								data-testid="lobby-invite-decline"
							>
								{i18n.t("lobby.invitation.decline")}
							</button>
						</div>
					</li>
				{/each}
			</ul>
		{/if}
	</section>

	<section data-testid="lobby-applications-section">
		<h2>{i18n.t("lobby.section.applications")}</h2>
		{#if listsLoading}
			<p>{i18n.t("lobby.list_loading")}</p>
		{:else if applications.length === 0}
			<p data-testid="lobby-applications-empty">{i18n.t("lobby.applications.empty")}</p>
		{:else}
			<ul class="card-list">
				{#each applications as app (app.applicationId)}
					<li class="card" data-testid="lobby-application-card">
						<strong>{app.raceName}</strong>
						<span class="meta">{app.gameId}</span>
						<span class="status" data-status={app.status}>
							{applicationStatusLabel(app.status)}
						</span>
					</li>
				{/each}
			</ul>
		{/if}
	</section>

	{#if import.meta.env.DEV}
		<!--
		Synthetic-report loader. DEV-only affordance for visual testing
		against rich game states without playing many turns. The JSON
		is produced offline by the Go CLI in
		`tools/local-dev/legacy-report/`; see
		`ui/docs/testing.md#synthetic-reports` for the workflow.
		-->
		<section data-testid="lobby-synthetic-section">
			<h2>Synthetic test reports (DEV)</h2>
			<p class="meta">
				Load a JSON file produced by
				<code>legacy-report-to-json</code> to open the map view
				against a synthetic snapshot. Orders compose locally but
				never reach the server.
			</p>
			<label class="synthetic-loader">
				Load JSON…
				<input
					type="file"
					accept=".json,application/json"
					onchange={onSyntheticFileChange}
					data-testid="lobby-synthetic-file"
				/>
			</label>
			{#if syntheticError !== null}
				<p role="alert" data-testid="lobby-synthetic-error">
					{syntheticError}
				</p>
			{/if}
		</section>
	{/if}

	<section data-testid="lobby-public-games-section">
		<h2>{i18n.t("lobby.section.public_games")}</h2>
		{#if listsLoading}
			<p>{i18n.t("lobby.list_loading")}</p>
		{:else if publicGames.length === 0}
			<p data-testid="lobby-public-games-empty">{i18n.t("lobby.public_games.empty")}</p>
		{:else}
			<ul class="card-list">
				{#each publicGames as game (game.gameId)}
					<li class="card">
						<strong>{game.gameName}</strong>
						<span class="meta">{game.status}</span>
						<span class="meta">{game.minPlayers}–{game.maxPlayers} players</span>
						{#if openApplicationFor === game.gameId}
							<form
								onsubmit={(event) => {
									event.preventDefault();
									submitApplicationFor(game.gameId);
								}}
								data-testid="lobby-application-form"
							>
								<label>
									{i18n.t("lobby.application.race_name_label")}
									<input
										type="text"
										bind:value={raceNameInput}
										data-testid="lobby-application-race-name"
										autocomplete="off"
									/>
								</label>
								{#if raceNameError !== null}
									<p role="alert" data-testid="lobby-application-error">
										{raceNameError}
									</p>
								{/if}
								<div class="actions">
									<button
										type="submit"
										disabled={submittingApplication}
										data-testid="lobby-application-submit"
									>
										{i18n.t("lobby.application.submit")}
									</button>
									<button
										type="button"
										onclick={cancelApplicationForm}
										data-testid="lobby-application-cancel"
									>
										{i18n.t("lobby.application.cancel")}
									</button>
								</div>
							</form>
						{:else}
							<button
								onclick={() => openApplicationForm(game.gameId)}
								data-testid="lobby-public-game-apply"
							>
								{i18n.t("lobby.application.submit_for", { name: game.gameName })}
							</button>
						{/if}
					</li>
				{/each}
			</ul>
		{/if}
	</section>
</main>

<style>
	main {
		padding: 1.5rem 1rem;
		max-width: 32rem;
		margin: 0 auto;
		font-family: system-ui, sans-serif;
	}

	header {
		display: flex;
		flex-direction: column;
		gap: 0.5rem;
		margin-bottom: 1.5rem;
	}

	header h1 {
		margin: 0;
	}

	header button {
		align-self: flex-start;
	}

	section {
		margin-bottom: 2rem;
	}

	section h2 {
		font-size: 1.1rem;
		margin: 0 0 0.75rem;
	}

	.card-list {
		list-style: none;
		padding: 0;
		margin: 0;
		display: flex;
		flex-direction: column;
		gap: 0.5rem;
	}

	.card {
		display: flex;
		flex-direction: column;
		gap: 0.25rem;
		padding: 0.75rem;
		border: 1px solid #ccc;
		border-radius: 0.4rem;
		background: #fafafa;
		text-align: left;
		font: inherit;
		cursor: pointer;
		width: 100%;
	}

	button.card:disabled {
		cursor: not-allowed;
		color: #777;
		background: #f0f0f0;
	}

	li.card {
		cursor: default;
	}

	.meta {
		color: #555;
		font-size: 0.9rem;
	}

	.status {
		align-self: flex-start;
		padding: 0.1rem 0.5rem;
		border-radius: 999px;
		background: #e7e7e7;
		font-size: 0.8rem;
	}

	.actions {
		display: flex;
		gap: 0.5rem;
		margin-top: 0.25rem;
	}

	form {
		display: flex;
		flex-direction: column;
		gap: 0.5rem;
		margin-top: 0.5rem;
	}

	label {
		display: flex;
		flex-direction: column;
		gap: 0.25rem;
	}

	input[type="text"] {
		font-size: 1rem;
		padding: 0.4rem 0.5rem;
	}

	.synthetic-loader {
		display: inline-flex;
		align-items: center;
		gap: 0.5rem;
		padding: 0.4rem 0.75rem;
		border: 1px dashed #888;
		border-radius: 0.4rem;
		background: #f7f7f7;
		cursor: pointer;
		font: inherit;
	}

	.synthetic-loader input[type="file"] {
		font-size: 0.9rem;
	}
</style>
