<!--
Phase 16 cargo-routes subsection of the planet inspector. Shows a
fixed COL/CAP/MAT/EMP four-slot table for the active local planet,
each slot either empty (with a single Add button) or filled (with
the destination planet's name plus Edit and Remove buttons). Add
and Edit hand off to the renderer-driven `MapPickService`: the map
dims out-of-reach planets, draws the cursor-line anchor, and
resolves with either a chosen destination id or `null` (cancel).

The component is purposely deferential to the existing infrastructure:
- `OrderDraftStore` enforces the `(source, loadType)` collapse rule,
  so the optimistic overlay always matches what the server sees.
- `MapPickService.pick(...)` is a renderer-side abstraction; its
  source/destination semantics live in `lib/active-view/map.svelte`.
- Reach (`40 * driveTech` per `game/internal/model/game/race.go`)
  is computed inline using `torusShortestDelta` to mirror the
  engine's torus distance — see `pkg/util/map.go.deltas`.
-->
<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);

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

	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's {#each} block free of
	// the {@const}/`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();
	}
</script>

<section class="cargo" data-testid="inspector-planet-cargo">
	<h4 class="title">
		{i18n.t("game.inspector.planet.cargo.title")}
	</h4>
	<dl class="slots">
		{#each CARGO_LOAD_TYPE_VALUES as loadType (loadType)}
			{@const entry = slotEntries[loadType]}
			{@const slug = loadType.toLowerCase()}
			<div class="slot" data-testid={`inspector-planet-cargo-slot-${slug}`}>
				<dt class="slot-label" data-testid={`inspector-planet-cargo-slot-${slug}-label`}>
					{i18n.t(SLOT_LABELS[loadType])}
				</dt>
				<dd class="slot-body">
					{#if entry === null}
						<span
							class="empty"
							data-testid={`inspector-planet-cargo-slot-${slug}-empty`}
						>
							{i18n.t("game.inspector.planet.cargo.empty")}
						</span>
						<button
							type="button"
							class="action add"
							data-testid={`inspector-planet-cargo-slot-${slug}-add`}
							disabled={disabled || pendingSlot !== null}
							onclick={() => void startPick(loadType)}
						>
							{i18n.t("game.inspector.planet.cargo.add")}
						</button>
					{:else}
						<span
							class="destination"
							data-testid={`inspector-planet-cargo-slot-${slug}-destination`}
						>
							→ {destinationName(entry.destinationPlanetNumber)}
						</span>
						<button
							type="button"
							class="action edit"
							data-testid={`inspector-planet-cargo-slot-${slug}-edit`}
							disabled={disabled || pendingSlot !== null}
							onclick={() => void startPick(loadType)}
						>
							{i18n.t("game.inspector.planet.cargo.edit")}
						</button>
						<button
							type="button"
							class="action remove"
							data-testid={`inspector-planet-cargo-slot-${slug}-remove`}
							disabled={disabled || pendingSlot !== null}
							onclick={() => void removeRoute(loadType)}
						>
							{i18n.t("game.inspector.planet.cargo.remove")}
						</button>
					{/if}
				</dd>
			</div>
		{/each}
	</dl>
	{#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 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;
	}
	.title {
		margin: 0;
		font-size: 0.85rem;
		color: var(--color-text-muted);
		font-weight: 500;
	}
	.slots {
		margin: 0;
		display: grid;
		grid-template-columns: max-content 1fr;
		row-gap: 0.25rem;
		column-gap: 0.6rem;
	}
	.slot {
		display: contents;
	}
	.slot-label {
		color: var(--color-text-muted);
		font-size: 0.85rem;
		align-self: center;
	}
	.slot-body {
		margin: 0;
		display: flex;
		align-items: center;
		gap: 0.4rem;
		flex-wrap: wrap;
		font-size: 0.9rem;
		font-variant-numeric: tabular-nums;
	}
	.empty {
		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>
