<script lang="ts">
	// Profile screen: a top-level appScreen (peer of `lobby` and
	// `lobby-create`). Loads the caller's account aggregate, lets the
	// user edit `display_name`, `preferred_language`, and `time_zone`,
	// and posts the changes through `user.profile.update` /
	// `user.settings.update`. The form stays on screen after a
	// successful save (the shell-level identity strip picks up the
	// new value through the shared `account` store) — only `cancel`
	// returns to the lobby.
	import { onMount } from "svelte";
	import { appScreen } from "$lib/app-nav.svelte";

	import { createGatewayClient } from "../../api/connect";
	import { GalaxyClient } from "../../api/galaxy-client";
	import {
		AccountError,
		updateMyProfile,
		updateMySettings,
		type Account,
	} from "../../api/account";
	import { gatewayRpcBaseUrl, GATEWAY_RESPONSE_PUBLIC_KEY } from "$lib/env";
	import {
		i18n,
		SUPPORTED_LOCALES,
		type Locale,
		type TranslationKey,
	} from "$lib/i18n/index.svelte";
	import { loadCore } from "../../platform/core/index";
	import { session } from "$lib/session-store.svelte";
	import { account } from "$lib/account-store.svelte";
	import {
		browserTimeZone,
		supportedTimeZones,
		withPreservedValue,
		type TimeZoneGroup,
	} from "$lib/time-zones";
	import LobbyShell from "./lobby-shell.svelte";

	let loaded: Account | null = $state(null);
	let displayNameInput = $state("");
	let preferredLanguageInput = $state("");
	let timeZoneInput = $state("");

	let loadError: string | null = $state(null);
	let configError: string | null = $state(null);
	let saveError: string | null = $state(null);
	let saving = $state(false);
	let savedNotice = $state(false);

	let client: GalaxyClient | null = null;

	const SUPPORTED_LOCALE_CODES: ReadonlySet<string> = new Set(
		SUPPORTED_LOCALES.map((entry) => entry.code),
	);

	// Built once: the IANA list is static for the page lifetime. The
	// stored value is folded in lazily so a zone the runtime no longer
	// advertises still renders.
	const TIME_ZONE_GROUPS_BASE: readonly TimeZoneGroup[] = supportedTimeZones();

	let timeZoneGroups = $derived<readonly TimeZoneGroup[]>(
		withPreservedValue(TIME_ZONE_GROUPS_BASE, timeZoneInput),
	);
	let timeZoneFallbackToText = $derived(TIME_ZONE_GROUPS_BASE.length === 0);

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

	function describe(err: unknown): string {
		if (err instanceof AccountError) {
			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";
	}

	function applyAccount(next: Account): void {
		loaded = next;
		displayNameInput = next.displayName;
		preferredLanguageInput = next.preferredLanguage;
		// Seed an empty stored zone with the browser's current zone so
		// the picker lands on a sensible default rather than the first
		// IANA entry. The form treats "no change" as not posting, so
		// the seeded value is only persisted on an explicit save.
		timeZoneInput = next.timeZone.length > 0 ? next.timeZone : browserTimeZone();
	}

	function markDirty(): void {
		// Any edit invalidates the "Saved" notice.
		savedNotice = false;
		saveError = null;
	}

	async function loadAccount(c: GalaxyClient): Promise<void> {
		try {
			applyAccount(await account.ensure(c));
		} catch (err) {
			loadError = describe(err);
		}
	}

	async function save(event: SubmitEvent): Promise<void> {
		event.preventDefault();
		if (client === null || loaded === null || saving) return;
		const trimmedDisplay = displayNameInput.trim();
		const trimmedLanguage = preferredLanguageInput.trim();
		const trimmedZone = timeZoneInput.trim();
		if (trimmedLanguage === "") {
			saveError = i18n.t("profile.error.language_required");
			return;
		}
		if (trimmedZone === "") {
			saveError = i18n.t("profile.error.time_zone_required");
			return;
		}
		saving = true;
		saveError = null;
		savedNotice = false;
		try {
			let next: Account = loaded;
			if (trimmedDisplay !== loaded.displayName) {
				next = await updateMyProfile(client, trimmedDisplay);
			}
			if (
				trimmedLanguage !== loaded.preferredLanguage ||
				trimmedZone !== loaded.timeZone
			) {
				next = await updateMySettings(client, trimmedLanguage, trimmedZone);
			}
			applyAccount(next);
			account.set(next);
			// When the user picks a language the UI supports, switch the
			// active locale immediately so the rest of the session sees
			// the change without a reload. Unsupported BCP 47 codes are
			// saved on the account but leave the active locale alone.
			if (SUPPORTED_LOCALE_CODES.has(next.preferredLanguage)) {
				i18n.setLocale(next.preferredLanguage as Locale);
			}
			savedNotice = true;
		} catch (err) {
			saveError = describe(err);
		} finally {
			saving = false;
		}
	}

	function cancel(): void {
		appScreen.go("lobby");
	}

	onMount(async () => {
		if (
			session.keypair === null ||
			session.deviceSessionId === null ||
			GATEWAY_RESPONSE_PUBLIC_KEY.length === 0
		) {
			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: createGatewayClient(gatewayRpcBaseUrl()),
				signer: (canonical) => keypair.sign(canonical),
				sha256,
				deviceSessionId: session.deviceSessionId,
				gatewayResponsePublicKey: GATEWAY_RESPONSE_PUBLIC_KEY,
			});
			await loadAccount(client);
		} catch (err) {
			loadError = describe(err);
		}
	});
</script>

<LobbyShell>
	<h1>{i18n.t("profile.title")}</h1>
	{#if configError !== null}
		<p role="alert" data-testid="profile-config-error">{configError}</p>
	{:else if loaded === null && loadError === null}
		<p role="status" data-testid="profile-loading">{i18n.t("profile.loading")}</p>
	{:else if loadError !== null}
		<p role="alert" data-testid="profile-load-error">{loadError}</p>
	{:else if loaded !== null}
		<dl class="identity" data-testid="profile-identity">
			<dt>{i18n.t("profile.field.user_name")}</dt>
			<dd>{loaded.userName}</dd>
			<dt>{i18n.t("profile.field.email")}</dt>
			<dd>{loaded.email}</dd>
		</dl>

		<form onsubmit={save} data-testid="profile-form">
			<label>
				<span>{i18n.t("profile.field.display_name")}</span>
				<input
					type="text"
					bind:value={displayNameInput}
					oninput={markDirty}
					autocomplete="nickname"
					data-testid="profile-display-name"
				/>
				<small>{i18n.t("profile.hint.display_name")}</small>
			</label>

			<label>
				<span>{i18n.t("profile.field.preferred_language")}</span>
				<select
					bind:value={preferredLanguageInput}
					onchange={markDirty}
					data-testid="profile-preferred-language"
				>
					{#each SUPPORTED_LOCALES as entry (entry.code)}
						<option value={entry.code}>{entry.nativeName}</option>
					{/each}
					{#if !SUPPORTED_LOCALE_CODES.has(preferredLanguageInput) && preferredLanguageInput !== ""}
						<!--
						Backend stores arbitrary BCP 47 tags, but the UI only
						ships translations for the codes in `SUPPORTED_LOCALES`.
						Preserve the saved value so saving the form unchanged
						does not silently switch it.
						-->
						<option value={preferredLanguageInput}>{preferredLanguageInput}</option>
					{/if}
				</select>
			</label>

			<label>
				<span>{i18n.t("profile.field.time_zone")}</span>
				{#if timeZoneFallbackToText}
					<!--
					Browser lacks `Intl.supportedValuesOf("timeZone")` —
					fall back to a free-text field so a viable runtime can
					still save a zone. The backend remains the validator.
					-->
					<input
						type="text"
						bind:value={timeZoneInput}
						oninput={markDirty}
						placeholder={browserTimeZone()}
						autocomplete="off"
						data-testid="profile-time-zone"
					/>
				{:else}
					<select
						bind:value={timeZoneInput}
						onchange={markDirty}
						data-testid="profile-time-zone"
					>
						{#each timeZoneGroups as group (group.label)}
							<optgroup label={group.label}>
								{#each group.values as zone (zone)}
									<option value={zone}>{zone}</option>
								{/each}
							</optgroup>
						{/each}
					</select>
				{/if}
				<small>{i18n.t("profile.hint.time_zone")}</small>
			</label>

			{#if saveError !== null}
				<p role="alert" data-testid="profile-save-error">{saveError}</p>
			{:else if savedNotice}
				<p role="status" data-testid="profile-saved-notice">
					{i18n.t("profile.saved")}
				</p>
			{/if}

			<div class="actions">
				<button type="submit" disabled={saving} data-testid="profile-save">
					{saving ? i18n.t("profile.saving") : i18n.t("profile.save")}
				</button>
				<button
					type="button"
					onclick={cancel}
					disabled={saving}
					data-testid="profile-cancel"
				>
					{i18n.t("profile.cancel")}
				</button>
			</div>
		</form>
	{/if}
</LobbyShell>

<style>
	h1 {
		font-size: var(--text-xl);
		margin: 0 0 var(--space-4);
	}

	.identity {
		display: grid;
		grid-template-columns: max-content 1fr;
		gap: var(--space-1) var(--space-4);
		margin: 0 0 var(--space-5);
		padding: var(--space-3) var(--space-4);
		border: 1px solid var(--color-border-subtle);
		border-radius: var(--radius-md);
		background: var(--color-surface-raised);
	}

	.identity dt {
		color: var(--color-text-muted);
		font-size: var(--text-sm);
	}

	.identity dd {
		margin: 0;
		font-size: var(--text-base);
	}

	form {
		display: flex;
		flex-direction: column;
		gap: var(--space-4);
	}

	label {
		display: flex;
		flex-direction: column;
		gap: var(--space-1);
	}

	label > span {
		font-size: var(--text-sm);
		color: var(--color-text-muted);
	}

	input[type="text"],
	select {
		font: inherit;
		font-size: var(--text-md);
		padding: var(--space-1) var(--space-2);
		background: var(--color-surface);
		color: var(--color-text);
		border: 1px solid var(--color-border);
		border-radius: var(--radius-sm);
	}

	small {
		color: var(--color-text-muted);
		font-size: var(--text-xs);
	}

	[data-testid="profile-saved-notice"] {
		color: var(--color-text-muted);
		font-size: var(--text-sm);
		margin: 0;
	}

	.actions {
		display: flex;
		gap: var(--space-3);
		margin-top: var(--space-2);
	}

	.actions button {
		font: inherit;
		font-size: var(--text-md);
		padding: var(--space-2) var(--space-4);
		background: var(--color-surface-raised);
		color: var(--color-text);
		border: 1px solid var(--color-border);
		border-radius: var(--radius-sm);
		cursor: pointer;
	}

	.actions button[type="submit"] {
		background: var(--color-accent);
		color: var(--color-accent-contrast);
		border-color: var(--color-accent);
	}

	.actions button:disabled {
		cursor: not-allowed;
		opacity: 0.6;
	}
</style>
