<!--
Phase 22 races table. Lists every non-extinct other race with the
local player's per-row stance toggle (WAR / PEACE — two segmented
buttons, the active stance highlighted) and a single vote slot
above the table. Both controls dispatch through the per-game
`OrderDraftStore` (context), so the optimistic overlay flips
immediately and the auto-sync pipeline drives the server in the
background.

The alliance graph and the 2/3 victory check are NOT computed
here: `rules.txt` keeps each race's outgoing vote target private
(only the votes a race RECEIVED in the last tally and the local
player's own pick are observable), and the acceptance criterion
"vote counts match server state byte-for-byte" rules out
client-side recomputation. The sub-header explains this explicitly
so the player knows where the win condition lives.

The component sits inside the active-view slot owned by
`routes/games/[id]/+layout.svelte`, so it inherits the per-game
`OrderDraftStore` and `RenderedReportSource` through context. No
data fetching is performed here — the layout is responsible.
-->
<script lang="ts">
	import { getContext } from "svelte";

	import type { ReportOtherRace } from "../../api/game-state";
	import { i18n, type TranslationKey } from "$lib/i18n/index.svelte";
	import {
		RENDERED_REPORT_CONTEXT_KEY,
		type RenderedReportSource,
	} from "$lib/rendered-report.svelte";
	import {
		ORDER_DRAFT_CONTEXT_KEY,
		OrderDraftStore,
	} from "../../sync/order-draft.svelte";
	import type { Relation } from "../../sync/order-types";

	type SortColumn =
		| "name"
		| "drive"
		| "weapons"
		| "shields"
		| "cargo"
		| "population"
		| "industry"
		| "planets"
		| "votesReceived";
	type SortDirection = "asc" | "desc";

	const COLUMN_LABELS: Record<SortColumn, TranslationKey> = {
		name: "game.table.races.column.name",
		drive: "game.table.races.column.drive",
		weapons: "game.table.races.column.weapons",
		shields: "game.table.races.column.shields",
		cargo: "game.table.races.column.cargo",
		population: "game.table.races.column.population",
		industry: "game.table.races.column.industry",
		planets: "game.table.races.column.planets",
		votesReceived: "game.table.races.column.votes",
	};

	const COLUMNS: readonly SortColumn[] = [
		"name",
		"drive",
		"weapons",
		"shields",
		"cargo",
		"population",
		"industry",
		"planets",
		"votesReceived",
	];

	const rendered = getContext<RenderedReportSource | undefined>(
		RENDERED_REPORT_CONTEXT_KEY,
	);
	const draft = getContext<OrderDraftStore | undefined>(
		ORDER_DRAFT_CONTEXT_KEY,
	);

	let sortColumn: SortColumn = $state("name");
	let sortDirection: SortDirection = $state("asc");
	let filter: string = $state("");

	const races = $derived<ReportOtherRace[]>(rendered?.report?.races ?? []);
	const myVotes = $derived<number>(rendered?.report?.myVotes ?? 0);
	const myVoteFor = $derived<string>(rendered?.report?.myVoteFor ?? "");
	const reportLoaded = $derived(
		rendered?.report !== null && rendered?.report !== undefined,
	);

	const filtered = $derived.by(() => {
		const needle = filter.trim().toLowerCase();
		if (needle === "") return races;
		return races.filter((r) => r.name.toLowerCase().includes(needle));
	});

	const sorted = $derived.by(() => {
		const list = [...filtered];
		const dir = sortDirection === "asc" ? 1 : -1;
		list.sort((a, b) => {
			if (sortColumn === "name") {
				return a.name.localeCompare(b.name) * dir;
			}
			return (a[sortColumn] - b[sortColumn]) * dir;
		});
		return list;
	});

	function toggleSort(column: SortColumn): void {
		if (sortColumn === column) {
			sortDirection = sortDirection === "asc" ? "desc" : "asc";
			return;
		}
		sortColumn = column;
		sortDirection = "asc";
	}

	function ariaSort(column: SortColumn): "ascending" | "descending" | "none" {
		if (sortColumn !== column) return "none";
		return sortDirection === "asc" ? "ascending" : "descending";
	}

	// Render a fraction in `[0, 1]` as a one-decimal percent
	// (`0.225` → `"22.5"`). The conversion is value-only — no `%`
	// suffix — so the column header carries the unit. Matches the
	// sciences-table convention.
	function formatPercent(fraction: number): string {
		return (fraction * 100).toLocaleString(undefined, {
			minimumFractionDigits: 0,
			maximumFractionDigits: 1,
		});
	}

	function formatCount(value: number): string {
		return value.toLocaleString(undefined, {
			minimumFractionDigits: 0,
			maximumFractionDigits: 0,
		});
	}

	function formatVotes(value: number): string {
		return value.toLocaleString(undefined, {
			minimumFractionDigits: 0,
			maximumFractionDigits: 2,
		});
	}

	async function setStance(acceptor: string, relation: Relation): Promise<void> {
		if (draft === undefined) return;
		// No-op when the row already reflects the requested stance — the
		// engine would accept the duplicate, but queueing a wire entry
		// that re-states the current state inflates the order tab and
		// the auto-sync envelope for nothing. The current stance reads
		// off the overlay (`races[i].relation`), so a queued-but-not-
		// applied stance change correctly suppresses a duplicate click
		// that matches the *queued* intent, not just the server snapshot.
		const current = races.find((r) => r.name === acceptor)?.relation;
		if (current === relation) return;
		await draft.add({
			kind: "setDiplomaticStance",
			id: crypto.randomUUID(),
			acceptor,
			relation,
		});
	}

	async function pickVote(event: Event): Promise<void> {
		if (draft === undefined) return;
		const select = event.currentTarget as HTMLSelectElement;
		const acceptor = select.value;
		if (acceptor === "") return;
		if (acceptor === myVoteFor) return;
		await draft.add({
			kind: "setVoteRecipient",
			id: crypto.randomUUID(),
			acceptor,
		});
	}
</script>

<section
	class="active-view"
	data-testid="active-view-table"
	data-entity="races"
>
	<header>
		<h2>{i18n.t("game.table.races.title")}</h2>
		<div class="summary">
			<span class="summary-cell" data-testid="races-my-votes">
				<span class="summary-label">
					{i18n.t("game.table.races.votes.mine")}:
				</span>
				<span class="summary-value">{formatVotes(myVotes)}</span>
			</span>
			<label class="summary-cell vote-picker">
				<span class="summary-label">
					{i18n.t("game.table.races.votes.target")}:
				</span>
				<select
					data-testid="races-vote-target"
					value={myVoteFor}
					disabled={!reportLoaded || races.length === 0}
					onchange={pickVote}
				>
					<option value="" disabled>
						{i18n.t("game.table.races.votes.target_placeholder")}
					</option>
					{#each races as r (r.name)}
						<option value={r.name}>{r.name}</option>
					{/each}
				</select>
			</label>
		</div>
		<p class="note" data-testid="races-alliance-note">
			{i18n.t("game.table.races.note.alliance_server_side")}
		</p>
		<div class="controls">
			<input
				type="search"
				class="filter"
				data-testid="races-filter"
				placeholder={i18n.t("game.table.races.filter.placeholder")}
				bind:value={filter}
			/>
		</div>
	</header>

	{#if !reportLoaded}
		<p class="status" data-testid="races-loading">
			{i18n.t("game.table.races.loading")}
		</p>
	{:else if races.length === 0}
		<p class="status" data-testid="races-empty">
			{i18n.t("game.table.races.empty")}
		</p>
	{:else}
		<table class="grid" data-testid="races-table">
			<thead>
				<tr>
					{#each COLUMNS as column (column)}
						<th aria-sort={ariaSort(column)}>
							<button
								type="button"
								class="sort"
								data-testid="races-column-{column}"
								onclick={() => toggleSort(column)}
							>
								{i18n.t(COLUMN_LABELS[column])}
								{#if sortColumn === column}
									<span class="sort-indicator" aria-hidden="true">
										{sortDirection === "asc" ? "▲" : "▼"}
									</span>
								{/if}
							</button>
						</th>
					{/each}
					<th>{i18n.t("game.table.races.column.relation")}</th>
				</tr>
			</thead>
			<tbody>
				{#each sorted as r (r.name)}
					<tr data-testid="races-row" data-name={r.name}>
						<td data-testid="races-cell-name">{r.name}</td>
						<td data-testid="races-cell-drive">{formatPercent(r.drive)}</td>
						<td data-testid="races-cell-weapons">
							{formatPercent(r.weapons)}
						</td>
						<td data-testid="races-cell-shields">
							{formatPercent(r.shields)}
						</td>
						<td data-testid="races-cell-cargo">{formatPercent(r.cargo)}</td>
						<td data-testid="races-cell-population">
							{formatCount(r.population)}
						</td>
						<td data-testid="races-cell-industry">
							{formatCount(r.industry)}
						</td>
						<td data-testid="races-cell-planets">{formatCount(r.planets)}</td>
						<td data-testid="races-cell-votes">
							{formatVotes(r.votesReceived)}
						</td>
						<td>
							<div
								class="stance"
								role="group"
								aria-label={i18n.t("game.table.races.column.relation")}
							>
								<button
									type="button"
									class="stance-button war"
									class:active={r.relation === "WAR"}
									aria-pressed={r.relation === "WAR"}
									data-testid="races-stance-war"
									onclick={() => void setStance(r.name, "WAR")}
								>
									{i18n.t("game.table.races.action.war")}
								</button>
								<button
									type="button"
									class="stance-button peace"
									class:active={r.relation === "PEACE"}
									aria-pressed={r.relation === "PEACE"}
									data-testid="races-stance-peace"
									onclick={() => void setStance(r.name, "PEACE")}
								>
									{i18n.t("game.table.races.action.peace")}
								</button>
							</div>
						</td>
					</tr>
				{/each}
			</tbody>
		</table>
	{/if}
</section>

<style>
	.active-view {
		padding: 1.5rem;
		font-family: system-ui, sans-serif;
		display: flex;
		flex-direction: column;
		gap: 1rem;
	}
	header {
		display: flex;
		flex-direction: column;
		gap: 0.5rem;
	}
	header h2 {
		margin: 0;
		font-size: 1.1rem;
	}
	.summary {
		display: flex;
		gap: 1.25rem;
		align-items: center;
		flex-wrap: wrap;
		font-size: 0.9rem;
	}
	.summary-cell {
		display: inline-flex;
		gap: 0.4rem;
		align-items: baseline;
	}
	.summary-label {
		color: #aab;
		text-transform: uppercase;
		letter-spacing: 0.04em;
		font-size: 0.75rem;
	}
	.summary-value {
		color: #e8eaf6;
		font-variant-numeric: tabular-nums;
	}
	.vote-picker select {
		font: inherit;
		padding: 0.2rem 0.4rem;
		background: #0a0e1a;
		color: #e8eaf6;
		border: 1px solid #2a3150;
		border-radius: 3px;
	}
	.vote-picker select:disabled {
		opacity: 0.5;
		cursor: not-allowed;
	}
	.note {
		margin: 0;
		color: #889;
		font-size: 0.8rem;
		line-height: 1.35;
	}
	.controls {
		display: flex;
		gap: 0.5rem;
		align-items: center;
		flex-wrap: wrap;
	}
	.filter {
		font: inherit;
		padding: 0.3rem 0.5rem;
		background: #0a0e1a;
		color: #e8eaf6;
		border: 1px solid #2a3150;
		border-radius: 3px;
		flex: 1 1 12rem;
		min-width: 8rem;
	}
	.status {
		margin: 0;
		color: #888;
		font-size: 0.9rem;
	}
	.grid {
		border-collapse: collapse;
		width: 100%;
		font-variant-numeric: tabular-nums;
	}
	.grid th,
	.grid td {
		padding: 0.4rem 0.6rem;
		text-align: left;
		border-bottom: 1px solid #1c2240;
		font-size: 0.9rem;
	}
	.grid th {
		color: #aab;
		text-transform: uppercase;
		letter-spacing: 0.04em;
		font-size: 0.75rem;
	}
	.grid tbody tr:hover {
		background: #11172a;
	}
	.sort {
		font: inherit;
		font-size: inherit;
		text-transform: inherit;
		letter-spacing: inherit;
		color: inherit;
		background: transparent;
		border: none;
		padding: 0;
		cursor: pointer;
		display: inline-flex;
		gap: 0.3rem;
		align-items: baseline;
	}
	.sort-indicator {
		font-size: 0.7em;
	}
	.stance {
		display: inline-flex;
		gap: 0.25rem;
	}
	.stance-button {
		font: inherit;
		font-size: 0.8rem;
		letter-spacing: 0.05em;
		padding: 0.2rem 0.55rem;
		background: transparent;
		color: #aab;
		border: 1px solid #2a3150;
		border-radius: 3px;
		cursor: pointer;
	}
	.stance-button:hover {
		color: #e8eaf6;
	}
	.stance-button.war.active {
		background: #4a1010;
		color: #ffcaca;
		border-color: #8a3030;
	}
	.stance-button.peace.active {
		background: #103a1a;
		color: #c8f2cf;
		border-color: #2f7a45;
	}
</style>
