<!--
F8-05 cargo-routes subsection of the planet inspector. Renders one
compact row: a single `<select>` with the COL/CAP/MAT/EMP load-types
(plus a default placeholder option that absorbs the old section
title) and a context block to its right showing either an `add`
button when the selected type has no route, or `→ destination` plus
`edit` and `remove` buttons when a route is in place.

Picking a load-type out of the dropdown does not commit anything by
itself: the player still has to press add / edit (which hand off to
the renderer-driven `MapPickService`) or remove (which appends a
`removeCargoRoute` command directly). After every action the
dropdown stays on the type that was just acted on, so the result is
visible in place. The `OrderDraftStore.add()` collapse rule keeps at
most one entry per `(source, loadType)` pair, mirroring the engine's
own constraint.

Reach (`40 * driveTech` per `game/internal/model/game/race.go`) is
still computed inline through `torusShortestDelta`; the picker
shipping in `MapPickService.pick(...)` already mirrors the engine's
torus distance via the F8-07 (#50) fix.
-->
<script lang="ts">
	import { getContext } from "svelte";

	import type { ReportPlanet, ReportRoute } from "../../../api/game-state";
	import { i18n, type TranslationKey } from "$lib/i18n/index.svelte";
	import { torusShortestDelta } from "../../../map/math";
	import {
		MAP_PICK_CONTEXT_KEY,
		type MapPickService,
	} from "$lib/map-pick.svelte";
	import {
		ORDER_DRAFT_CONTEXT_KEY,
		OrderDraftStore,
	} from "../../../sync/order-draft.svelte";
	import {
		CARGO_LOAD_TYPE_VALUES,
		type CargoLoadType,
	} from "../../../sync/order-types";

	type Props = {
		planet: ReportPlanet;
		routes: ReportRoute[];
		planets: ReportPlanet[];
		mapWidth: number;
		mapHeight: number;
		localPlayerDrive: number;
	};
	let {
		planet,
		routes,
		planets,
		mapWidth,
		mapHeight,
		localPlayerDrive,
	}: Props = $props();

	const draft = getContext<OrderDraftStore | undefined>(
		ORDER_DRAFT_CONTEXT_KEY,
	);
	const pick = getContext<MapPickService | undefined>(MAP_PICK_CONTEXT_KEY);
	const disabled = $derived(draft === undefined || pick === undefined);

	let pendingSlot: CargoLoadType | null = $state(null);
	let selected = $state<CargoLoadType | "">("");

	$effect(() => {
		// Reset the local UI whenever the inspector switches to a
		// different planet so a stale dropdown selection (or in-flight
		// pick prompt) does not leak across the selection boundary.
		void planet.number;
		pendingSlot = null;
		selected = "";
	});

	const SLOT_LABELS: Record<CargoLoadType, TranslationKey> = {
		COL: "game.inspector.planet.cargo.slot.col",
		CAP: "game.inspector.planet.cargo.slot.cap",
		MAT: "game.inspector.planet.cargo.slot.mat",
		EMP: "game.inspector.planet.cargo.slot.emp",
	};

	const currentEntries = $derived(
		routes.find((r) => r.sourcePlanetNumber === planet.number)?.entries ?? [],
	);
	// Per-slot derived map keeps the template free of the
	// `.find(...)` chain that Svelte 5 sometimes mis-tracks when the
	// source array is freshly cloned by `applyOrderOverlay`.
	const slotEntries = $derived.by(() => {
		const map: Record<CargoLoadType, ReportRoute["entries"][number] | null> = {
			COL: null,
			CAP: null,
			MAT: null,
			EMP: null,
		};
		for (const entry of currentEntries) {
			map[entry.loadType] = entry;
		}
		return map;
	});

	function destinationName(planetNumber: number): string {
		const target = planets.find((p) => p.number === planetNumber);
		if (target === undefined) return `#${planetNumber}`;
		if (target.kind === "unidentified") return `#${planetNumber}`;
		return target.name === "" ? `#${planetNumber}` : target.name;
	}

	const reach = $derived(40 * localPlayerDrive);

	function reachableSet(): Set<number> {
		// The engine accepts a route from a player-owned planet to any
		// planet inside the source's flight distance — own, foreign,
		// uninhabited, and unidentified all qualify (`game/internal/
		// controller/route.go.PlanetRouteSet` only checks ownership of
		// the origin and `util.ShortDistance(...) <= FligthDistance`,
		// see `pkg/calc/race.go`). The picker mirrors that contract;
		// only the source itself is excluded so a self-route cannot be
		// emitted.
		const ids = new Set<number>();
		if (reach <= 0) return ids;
		for (const candidate of planets) {
			if (candidate.number === planet.number) continue;
			const dx = torusShortestDelta(planet.x, candidate.x, mapWidth);
			const dy = torusShortestDelta(planet.y, candidate.y, mapHeight);
			if (Math.hypot(dx, dy) <= reach) {
				ids.add(candidate.number);
			}
		}
		return ids;
	}

	async function startPick(loadType: CargoLoadType): Promise<void> {
		if (draft === undefined || pick === undefined) return;
		if (pendingSlot !== null) return;
		const reachable = reachableSet();
		if (reachable.size === 0) return;
		pendingSlot = loadType;
		try {
			const destination = await pick.pick({
				sourcePlanetNumber: planet.number,
				reachableIds: reachable,
			});
			if (destination === null) return;
			await draft.add({
				kind: "setCargoRoute",
				id: crypto.randomUUID(),
				sourcePlanetNumber: planet.number,
				destinationPlanetNumber: destination,
				loadType,
			});
		} finally {
			pendingSlot = null;
		}
	}

	async function removeRoute(loadType: CargoLoadType): Promise<void> {
		if (draft === undefined) return;
		await draft.add({
			kind: "removeCargoRoute",
			id: crypto.randomUUID(),
			sourcePlanetNumber: planet.number,
			loadType,
		});
	}

	function cancelPick(): void {
		pick?.cancel();
	}

	function pickType(event: Event): void {
		selected = (event.target as HTMLSelectElement).value as CargoLoadType | "";
	}

	const activeEntry = $derived(
		selected === "" ? null : slotEntries[selected],
	);
	const selectedSlug = $derived(
		selected === "" ? "" : selected.toLowerCase(),
	);
</script>

<section class="cargo" data-testid="inspector-planet-cargo">
	<div class="row">
		<select
			class="select"
			data-testid="inspector-planet-cargo-type"
			aria-label={i18n.t("game.inspector.planet.cargo.placeholder")}
			value={selected}
			onchange={pickType}
			disabled={disabled || pendingSlot !== null}
		>
			<option value="" disabled class="placeholder">
				{i18n.t("game.inspector.planet.cargo.placeholder")}
			</option>
			{#each CARGO_LOAD_TYPE_VALUES as loadType (loadType)}
				<option
					value={loadType}
					data-testid={`inspector-planet-cargo-type-option-${loadType.toLowerCase()}`}
				>
					{i18n.t(SLOT_LABELS[loadType])}
				</option>
			{/each}
		</select>

		{#if selected !== ""}
			{#if activeEntry === null}
				<button
					type="button"
					class="action add"
					data-testid={`inspector-planet-cargo-slot-${selectedSlug}-add`}
					disabled={disabled || pendingSlot !== null}
					onclick={() => void startPick(selected as CargoLoadType)}
				>
					{i18n.t("game.inspector.planet.cargo.add")}
				</button>
			{:else}
				<span
					class="destination"
					data-testid={`inspector-planet-cargo-slot-${selectedSlug}-destination`}
				>
					→ {destinationName(activeEntry.destinationPlanetNumber)}
				</span>
				<button
					type="button"
					class="action edit"
					data-testid={`inspector-planet-cargo-slot-${selectedSlug}-edit`}
					disabled={disabled || pendingSlot !== null}
					onclick={() => void startPick(selected as CargoLoadType)}
				>
					{i18n.t("game.inspector.planet.cargo.edit")}
				</button>
				<button
					type="button"
					class="action remove"
					data-testid={`inspector-planet-cargo-slot-${selectedSlug}-remove`}
					disabled={disabled || pendingSlot !== null}
					onclick={() => void removeRoute(selected as CargoLoadType)}
				>
					{i18n.t("game.inspector.planet.cargo.remove")}
				</button>
			{/if}
		{/if}
	</div>

	{#if pendingSlot !== null}
		<div
			class="pick-prompt"
			data-testid="inspector-planet-cargo-pick-prompt"
			role="status"
		>
			<span class="pick-message">
				{i18n.t("game.inspector.planet.cargo.pick.prompt")}
			</span>
			<button
				type="button"
				class="action cancel"
				data-testid="inspector-planet-cargo-pick-cancel"
				onclick={cancelPick}
			>
				{i18n.t("game.inspector.planet.cargo.pick.cancel")}
			</button>
		</div>
	{:else if selected !== "" && reach > 0 && reachableSet().size === 0}
		<p
			class="no-destinations"
			data-testid="inspector-planet-cargo-no-destinations"
		>
			{i18n.t("game.inspector.planet.cargo.pick.no_destinations", {
				reach: reach.toFixed(1),
			})}
		</p>
	{/if}
</section>

<style>
	.cargo {
		display: flex;
		flex-direction: column;
		gap: 0.4rem;
	}
	.row {
		display: flex;
		align-items: center;
		gap: 0.4rem;
		flex-wrap: wrap;
		font-size: 0.9rem;
		font-variant-numeric: tabular-nums;
	}
	.select {
		flex: 1 1 8rem;
		min-width: 0;
		font: inherit;
		font-size: 0.85rem;
		padding: 0.2rem 0.4rem;
		background: var(--color-surface-raised);
		color: var(--color-text);
		border: 1px solid var(--color-border);
		border-radius: 3px;
		cursor: pointer;
	}
	.select:disabled {
		cursor: not-allowed;
		opacity: 0.5;
	}
	.select option.placeholder {
		color: var(--color-text-muted);
		font-style: italic;
	}
	.destination {
		color: var(--color-text);
	}
	.action {
		font: inherit;
		font-size: 0.8rem;
		padding: 0.15rem 0.5rem;
		background: transparent;
		color: var(--color-text-muted);
		border: 1px solid var(--color-border);
		border-radius: 3px;
		cursor: pointer;
	}
	.action:not(:disabled):hover {
		color: var(--color-text);
		border-color: var(--color-accent);
	}
	.action:disabled {
		cursor: not-allowed;
		opacity: 0.5;
	}
	.pick-prompt {
		display: flex;
		gap: 0.4rem;
		align-items: center;
		flex-wrap: wrap;
		padding: 0.3rem 0.5rem;
		background: var(--color-warning-subtle);
		border: 1px solid var(--color-warning);
		border-radius: 4px;
	}
	.pick-message {
		color: var(--color-warning);
		font-size: 0.85rem;
		flex: 1;
	}
	.no-destinations {
		margin: 0;
		font-size: 0.8rem;
		color: var(--color-text-muted);
		font-style: italic;
	}
</style>
