// App-shell navigation state.
//
// The game UI is a single-URL app-shell (served at `/game/`): there are no
// per-screen or per-view routes, so the address bar never changes. Two rune
// singletons hold what the URL used to encode:
//
//   - `appScreen` — the top-level screen (login / lobby / lobby-create /
//     game) and the active game id. It replaces the `goto`-based redirects
//     and the `[id]` route param.
//   - `activeView` — the in-game view (map / table / report / battle / mail /
//     designer-science) and its sub-parameters. It replaces the URL params the
//     old route wrappers read.
//
// Both live in this one module so they can share a single `sessionStorage`
// snapshot (persisted here) without a circular import. The snapshot is read
// once at construction to seed the initial render and rewritten on every
// mutation; `restoredGameId` lets the boot path validate a restored game
// before loading it (a cancelled/removed game falls back to lobby — see the
// dispatcher). Screen-level browser history (Back → lobby) is layered on top
// in the shell via SvelteKit shallow routing; this module is the source of
// truth, history only mirrors it.

import { pushState, replaceState } from "$app/navigation";

// Top-level app-shell screens. The lobby is split into per-page screens
// (F8-04b): `lobby` is the bare alias the shell resolves to the first
// visible games sub-page; the explicit `games-*` literals point at one
// of the four lobby sub-panels; `synthetic-reports` is the DEV-only
// reports screen (build-time gated via VITE_GALAXY_DEV_AFFORDANCES).
// `lobby-create` and `profile` remain as separate top-level screens.
export type AppScreen =
	| "login"
	| "lobby"
	| "lobby-create"
	| "profile"
	| "game"
	| "games-active-past"
	| "games-recruitment"
	| "games-invitations"
	| "games-private-games"
	| "synthetic-reports";

// LOBBY_SUB_SCREENS lists the AppScreen literals that the sidebar
// renders as members of the `games` submenu. The order is the canonical
// "first-visible" order: when the user clicks the `games` header (or
// the screen state is the bare `lobby` alias) the shell picks the
// first entry whose visibility predicate holds.
export const LOBBY_SUB_SCREENS: readonly AppScreen[] = [
	"games-active-past",
	"games-recruitment",
	"games-invitations",
	"games-private-games",
];

// isLobbySubScreen returns true when screen identifies one of the
// `games-*` sub-panels — useful for the sidebar to highlight the
// `games` parent and to decide whether the desktop submenu should be
// rendered expanded.
export function isLobbySubScreen(screen: AppScreen): boolean {
	return LOBBY_SUB_SCREENS.includes(screen);
}

export type GameView =
	| "map"
	| "table"
	| "report"
	| "battle"
	| "mail"
	| "designer-science";

/** In-game view plus the sub-parameters the old route segments carried. */
export interface GameViewState {
	view: GameView;
	/** Table entity slug when `view === "table"` (e.g. `planets`, `sciences`). */
	tableEntity?: string;
	/** Selected battle when `view === "battle"`; empty string = list/none. */
	battleId?: string;
	/** Viewed turn for the battle view; 0 = current. */
	turn?: number;
	/** Science id when `view === "designer-science"`; absent = new-science form. */
	scienceId?: string;
}

const STORAGE_KEY = "galaxy-app-nav";

const APP_SCREENS: readonly AppScreen[] = [
	"login",
	"lobby",
	"lobby-create",
	"profile",
	"game",
	"games-active-past",
	"games-recruitment",
	"games-invitations",
	"games-private-games",
	"synthetic-reports",
];
const GAME_VIEWS: readonly GameView[] = [
	"map",
	"table",
	"report",
	"battle",
	"mail",
	"designer-science",
];

const DEFAULT_VIEW: GameViewState = { view: "map" };

interface NavSnapshot {
	screen: AppScreen;
	gameId: string | null;
	game: GameViewState;
}

function readSnapshot(): NavSnapshot | null {
	if (typeof sessionStorage === "undefined") return null;
	let raw: string | null;
	try {
		raw = sessionStorage.getItem(STORAGE_KEY);
	} catch {
		return null;
	}
	if (raw === null) return null;
	try {
		const parsed = JSON.parse(raw) as Partial<NavSnapshot> | null;
		if (parsed === null || typeof parsed !== "object") return null;
		const screen = APP_SCREENS.includes(parsed.screen as AppScreen)
			? (parsed.screen as AppScreen)
			: "lobby";
		const gameId =
			typeof parsed.gameId === "string" && parsed.gameId.length > 0
				? parsed.gameId
				: null;
		return { screen, gameId, game: sanitizeView(parsed.game) };
	} catch {
		return null;
	}
}

function sanitizeView(value: unknown): GameViewState {
	if (value === null || typeof value !== "object") return { ...DEFAULT_VIEW };
	const v = value as Partial<GameViewState>;
	const view = GAME_VIEWS.includes(v.view as GameView)
		? (v.view as GameView)
		: "map";
	const out: GameViewState = { view };
	if (typeof v.tableEntity === "string") out.tableEntity = v.tableEntity;
	if (typeof v.battleId === "string") out.battleId = v.battleId;
	if (typeof v.turn === "number" && Number.isFinite(v.turn) && v.turn >= 0) {
		out.turn = Math.trunc(v.turn);
	}
	if (typeof v.scienceId === "string") out.scienceId = v.scienceId;
	return out;
}

function persist(): void {
	if (typeof sessionStorage === "undefined") return;
	const snapshot: NavSnapshot = {
		screen: appScreen.screen,
		gameId: appScreen.gameId,
		game: activeView.state,
	};
	try {
		sessionStorage.setItem(STORAGE_KEY, JSON.stringify(snapshot));
	} catch {
		// Storage full / disabled / private-mode quota — navigation still
		// works in memory; only refresh-restore is lost.
	}
}

const initial = readSnapshot();

/**
 * AppScreenStore owns the top-level screen and the active game id. Anonymous
 * vs authenticated gating is applied by the dispatcher on top of `screen`.
 */
class AppScreenStore {
	#screen = $state<AppScreen>(initial?.screen ?? "lobby");
	#gameId = $state<string | null>(initial?.gameId ?? null);

	/** The game id captured from a restored snapshot, for boot-time validation. */
	readonly restoredGameId: string | null = initial?.gameId ?? null;

	get screen(): AppScreen {
		return this.#screen;
	}

	get gameId(): string | null {
		return this.#gameId;
	}

	/**
	 * go switches the top-level screen. Entering a game requires a `gameId`;
	 * leaving a game clears it. Persists the snapshot. History wiring (Back →
	 * lobby) is added by the shell, which observes `screen`.
	 */
	go(screen: AppScreen, options: { gameId?: string } = {}): void {
		this.#screen = screen;
		if (screen === "game") {
			if (options.gameId !== undefined) this.#gameId = options.gameId;
		} else {
			this.#gameId = null;
		}
		persist();
		this.#syncHistory();
	}

	/**
	 * syncFromHistory applies a screen restored from browser history (a
	 * Back/Forward popstate) WITHOUT pushing a new entry. An absent/unknown
	 * screen (the load entry beneath an overlay) falls back to lobby.
	 */
	syncFromHistory(screen: AppScreen | undefined, gameId: string | null): void {
		const next =
			screen !== undefined && APP_SCREENS.includes(screen) ? screen : "lobby";
		this.#screen = next;
		this.#gameId = next === "game" ? gameId : null;
		persist();
	}

	// Mirror the screen into browser history via shallow routing (the URL is
	// unchanged — the address bar stays at /game/). Overlays (game,
	// lobby-create) push a new entry so browser Back returns to the lobby
	// beneath; lobby/login replace in place.
	#syncHistory(): void {
		if (typeof window === "undefined") return;
		const state: App.PageState = { screen: this.#screen, gameId: this.#gameId };
		if (
			this.#screen === "game" ||
			this.#screen === "lobby-create" ||
			this.#screen === "profile"
		) {
			pushState("", state);
		} else {
			replaceState("", state);
		}
	}
}

/**
 * ActiveViewStore owns the in-game view and its sub-parameters. It is only
 * meaningful while `appScreen.screen === "game"`.
 */
class ActiveViewStore {
	#state = $state<GameViewState>(initial?.game ?? { ...DEFAULT_VIEW });

	get state(): GameViewState {
		return this.#state;
	}

	get view(): GameView {
		return this.#state.view;
	}

	/** Replace the active in-game view and its sub-parameters. Persists. */
	select(view: GameView, params: Omit<GameViewState, "view"> = {}): void {
		this.#state = { view, ...params };
		persist();
	}

	/** Reset to the default view (map). Used when entering a fresh game. */
	reset(): void {
		this.#state = { ...DEFAULT_VIEW };
		persist();
	}
}

export const appScreen = new AppScreenStore();
export const activeView = new ActiveViewStore();
