<!--
Phase 20 ship-group action panel.

Mounted by `lib/inspectors/ship-group.svelte` for `selection.variant
=== "local"`. Renders eight inline forms — Split, Send, Load, Unload,
Modernize, Dismantle, Transfer, Join Fleet — each gated by the same
state-and-context predicates that the engine enforces server-side. All
real actions either append a single command to the local order draft
or, when the player chose fewer ships than the group holds, prepend
an implicit `breakShipGroup` and route the action at the freshly-
minted sub-group ID.

The component owns the open/closed state for each form (so opening
one closes the others), the ephemeral form fields, and the live
modernize cost preview backed by `core.blockUpgradeCost`.
-->
<script lang="ts">
	import { getContext } from "svelte";

	import type {
		ReportLocalFleet,
		ReportLocalShipGroup,
		ReportPlanet,
		ShipClassSummary,
	} from "../../../api/game-state";
	import { i18n, type TranslationKey } from "$lib/i18n/index.svelte";
	import { CORE_CONTEXT_KEY, type CoreHandle } from "$lib/core-context.svelte";
	import {
		MAP_PICK_CONTEXT_KEY,
		type MapPickService,
	} from "$lib/map-pick.svelte";
	import { torusShortestDelta } from "../../../map/math";
	import {
		ORDER_DRAFT_CONTEXT_KEY,
		OrderDraftStore,
	} from "../../../sync/order-draft.svelte";
	import {
		SHIP_GROUP_CARGO_VALUES,
		SHIP_GROUP_UPGRADE_TECH_VALUES,
		type OrderCommand,
		type ShipGroupCargo,
		type ShipGroupUpgradeTech,
	} from "../../../sync/order-types";
	import { validateEntityName } from "$lib/util/entity-name";

	type Props = {
		group: ReportLocalShipGroup;
		planets: ReportPlanet[];
		localShipClass: ShipClassSummary[];
		localFleets: ReportLocalFleet[];
		otherRaces: string[];
		mapWidth: number;
		mapHeight: number;
		localPlayerDrive: number;
		localPlayerWeapons: number;
		localPlayerShields: number;
		localPlayerCargo: number;
	};
	let {
		group,
		planets,
		localShipClass,
		localFleets,
		otherRaces,
		mapWidth,
		mapHeight,
		localPlayerDrive,
		localPlayerWeapons,
		localPlayerShields,
		localPlayerCargo,
	}: Props = $props();

	const draft = getContext<OrderDraftStore | undefined>(
		ORDER_DRAFT_CONTEXT_KEY,
	);
	const pick = getContext<MapPickService | undefined>(MAP_PICK_CONTEXT_KEY);
	const coreHandle = getContext<CoreHandle | undefined>(CORE_CONTEXT_KEY);

	type FormName =
		| "split"
		| "send"
		| "load"
		| "unload"
		| "modernize"
		| "dismantle"
		| "transfer"
		| "joinFleet";

	let openForm: FormName | null = $state(null);

	// Per-form ephemeral state. Resets via the form-open helpers below
	// so a stale value from a previous selection cannot leak into the
	// next one.
	let splitShips = $state(1);
	let sendShips = $state(1);
	let sendDestination: number | null = $state(null);
	let sendPicking = $state(false);
	let loadShips = $state(1);
	let loadCargo: ShipGroupCargo = $state("COL");
	let loadQuantity = $state(0);
	let unloadShips = $state(1);
	let unloadQuantity = $state(0);
	let modernizeShips = $state(1);
	let modernizeTech: ShipGroupUpgradeTech = $state("ALL");
	let modernizeLevel = $state(0);
	let dismantleShips = $state(1);
	let dismantleConfirmed = $state(false);
	let transferShips = $state(1);
	let transferAcceptor = $state("");
	let joinFleetMode: "existing" | "new" = $state("new");
	let joinFleetExisting = $state("");
	let joinFleetNew = $state("");

	// Reset all per-form state whenever the selected group changes so a
	// stale form does not leak across selection switches.
	$effect(() => {
		void group.id;
		openForm = null;
	});

	// Close any open form the moment the group becomes locked
	// (a destructive command landed in the draft from elsewhere —
	// e.g. another browser tab editing the same draft, or a
	// concurrent action on this inspector). Without this guard a
	// pending form would still allow Confirm despite the locked
	// banner above it.
	$effect(() => {
		if (pendingLockingCommand !== null) {
			if (sendPicking) {
				pick?.cancel();
				sendPicking = false;
			}
			openForm = null;
		}
	});

	const inOrbit = $derived(group.state === "In_Orbit");
	const orbitPlanet = $derived(
		inOrbit ? (planets.find((p) => p.number === group.destination) ?? null) : null,
	);
	const shipClass = $derived(
		localShipClass.find((sc) => sc.name === group.class) ?? null,
	);
	const ownPlanet = $derived(orbitPlanet?.kind === "local");
	const uninhabitedPlanet = $derived(orbitPlanet?.kind === "uninhabited");
	const friendlyPlanet = $derived(ownPlanet || uninhabitedPlanet);
	const hasCargoBlock = $derived(shipClass !== null && shipClass.cargo > 0);
	const hasDriveBlock = $derived(shipClass !== null && shipClass.drive > 0);
	const reach = $derived(40 * localPlayerDrive);
	const cargoLoaded = $derived(group.cargo !== "NONE" && group.load > 0);
	const carryingColonists = $derived(group.cargo === "COL" && group.load > 0);

	const reachableSet = $derived.by(() => {
		const ids = new Set<number>();
		if (!inOrbit || !hasDriveBlock || reach <= 0 || orbitPlanet === null)
			return ids;
		for (const candidate of planets) {
			if (candidate.number === orbitPlanet.number) continue;
			const dx = torusShortestDelta(orbitPlanet.x, candidate.x, mapWidth);
			const dy = torusShortestDelta(orbitPlanet.y, candidate.y, mapHeight);
			if (Math.hypot(dx, dy) <= reach) ids.add(candidate.number);
		}
		return ids;
	});

	const techHeadroom = $derived.by(() => {
		// At least one block must have race tech > group tech AND a non-
		// zero block mass for modernize to do anything (engine refuses
		// when the requested block has zero mass — see
		// controller/ship_group_upgrade.go:54).
		if (shipClass === null) return false;
		const blocks: Array<[number, number, number]> = [
			[shipClass.drive, group.tech.drive, localPlayerDrive],
			[shipClass.shields, group.tech.shields, localPlayerShields],
			[shipClass.cargo, group.tech.cargo, localPlayerCargo],
		];
		// Weapons block mass requires the bridge.
		const core = coreHandle?.core ?? null;
		if (core !== null) {
			const wm = core.weaponsBlockMass({
				weapons: shipClass.weapons,
				armament: shipClass.armament,
			});
			if (wm !== null) blocks.push([wm, group.tech.weapons, localPlayerWeapons]);
		} else if (shipClass.weapons > 0) {
			// Without Core, fall back to a weak heuristic: if weapons > 0
			// the block exists. The engine still re-validates server-
			// side so a false-positive only delays the rejection.
			blocks.push([1, group.tech.weapons, localPlayerWeapons]);
		}
		return blocks.some(
			([mass, current, race]) => mass > 0 && race > current,
		);
	});

	const planetStock = $derived.by(() => {
		if (orbitPlanet === null || orbitPlanet.kind === "unidentified") {
			return { COL: 0, CAP: 0, MAT: 0 };
		}
		return {
			COL: orbitPlanet.colonists ?? 0,
			CAP: orbitPlanet.industryStockpile ?? 0,
			MAT: orbitPlanet.materialsStockpile ?? 0,
		};
	});

	const cargoCapacityPerShip = $derived.by(() => {
		const core = coreHandle?.core ?? null;
		if (core === null || shipClass === null) return null;
		return core.cargoCapacity({
			cargo: shipClass.cargo,
			cargoTech: localPlayerCargo,
		});
	});

	const groupCapacity = $derived(
		cargoCapacityPerShip === null
			? null
			: cargoCapacityPerShip * group.count,
	);
	const freeCapacity = $derived(
		groupCapacity === null ? null : Math.max(0, groupCapacity - group.load),
	);

	const fleetsOnSamePlanet = $derived(
		localFleets.filter(
			(f) =>
				f.state === "In_Orbit" &&
				f.destination === group.destination &&
				f.origin === null,
		),
	);

	function disabledStateTooltip(): string | null {
		if (!inOrbit) {
			return i18n.t(
				"game.inspector.ship_group.action.disabled.not_in_orbit",
				{ state: group.state },
			);
		}
		return null;
	}

	function reasonTooltip(reason: TranslationKey | null): string | null {
		return reason === null ? null : i18n.t(reason);
	}

	// pendingLockingCommand watches the order draft for any send /
	// modernize / dismantle / transfer command targeting this group.
	// Once the player queues one of those four, every action on the
	// group is disabled until the draft entry is removed: each is
	// state-changing at turn cutoff (Send → state Launched, Modernize
	// → state Upgrade, Transfer → state Transfer, Dismantle → group
	// removed), so a follow-up action would race the engine's
	// pre-condition check and noisy-fail server-side. The lock
	// surfaces the commitment up-front and points the player at the
	// order list as the way to release it. Load / Unload / Split /
	// Join Fleet stay non-locking — the engine accepts them stacked,
	// and Send + the three destructive variants are the only commands
	// that flip the group out of `In_Orbit` on the next tick.
	const pendingLockingCommand = $derived.by(() => {
		if (draft === undefined) return null;
		for (const cmd of draft.commands) {
			if (
				cmd.kind !== "sendShipGroup" &&
				cmd.kind !== "upgradeShipGroup" &&
				cmd.kind !== "dismantleShipGroup" &&
				cmd.kind !== "transferShipGroup"
			)
				continue;
			if (cmd.groupId !== group.id) continue;
			const status = draft.statuses[cmd.id];
			if (status === "rejected" || status === "invalid") continue;
			return cmd;
		}
		return null;
	});
	const lockedReason: TranslationKey | null = $derived(
		pendingLockingCommand === null
			? null
			: "game.inspector.ship_group.action.disabled.locked",
	);
	const lockedKindLabel = $derived.by(() => {
		const cmd = pendingLockingCommand;
		if (cmd === null) return "";
		switch (cmd.kind) {
			case "sendShipGroup":
				return i18n.t("game.inspector.ship_group.action.locked.kind.send");
			case "upgradeShipGroup":
				return i18n.t("game.inspector.ship_group.action.locked.kind.modernize");
			case "dismantleShipGroup":
				return i18n.t("game.inspector.ship_group.action.locked.kind.dismantle");
			case "transferShipGroup":
				return i18n.t("game.inspector.ship_group.action.locked.kind.transfer");
		}
	});

	const splitDisabledReason = $derived.by((): TranslationKey | null => {
		if (lockedReason !== null) return lockedReason;
		if (!inOrbit) return "game.inspector.ship_group.action.disabled.not_in_orbit";
		if (group.count < 2) return "game.inspector.ship_group.action.invalid.ship_count";
		return null;
	});
	const sendDisabledReason = $derived.by((): TranslationKey | null => {
		if (lockedReason !== null) return lockedReason;
		if (!inOrbit) return "game.inspector.ship_group.action.disabled.not_in_orbit";
		if (!hasDriveBlock) return "game.inspector.ship_group.action.disabled.no_drive";
		if (orbitPlanet === null) return "game.inspector.ship_group.action.disabled.no_planet";
		if (reachableSet.size === 0) return "game.inspector.ship_group.action.disabled.no_reach";
		return null;
	});
	const loadDisabledReason = $derived.by((): TranslationKey | null => {
		if (lockedReason !== null) return lockedReason;
		if (!inOrbit) return "game.inspector.ship_group.action.disabled.not_in_orbit";
		if (!hasCargoBlock) return "game.inspector.ship_group.action.disabled.no_cargo_block";
		if (orbitPlanet === null) return "game.inspector.ship_group.action.disabled.no_planet";
		if (!friendlyPlanet) return "game.inspector.ship_group.action.disabled.foreign_planet";
		if (freeCapacity !== null && freeCapacity <= 0)
			return "game.inspector.ship_group.action.disabled.full_load";
		const stock = planetStock;
		if (stock.COL <= 0 && stock.CAP <= 0 && stock.MAT <= 0)
			return "game.inspector.ship_group.action.disabled.no_planet_stock";
		return null;
	});
	const unloadDisabledReason = $derived.by((): TranslationKey | null => {
		if (lockedReason !== null) return lockedReason;
		if (!inOrbit) return "game.inspector.ship_group.action.disabled.not_in_orbit";
		if (!hasCargoBlock) return "game.inspector.ship_group.action.disabled.no_cargo_block";
		if (!cargoLoaded) return "game.inspector.ship_group.action.disabled.empty_cargo";
		if (group.cargo === "COL" && !ownPlanet && !uninhabitedPlanet)
			return "game.inspector.ship_group.action.disabled.foreign_unload_col";
		return null;
	});
	const modernizeDisabledReason = $derived.by((): TranslationKey | null => {
		if (lockedReason !== null) return lockedReason;
		if (!inOrbit) return "game.inspector.ship_group.action.disabled.not_in_orbit";
		if (orbitPlanet === null) return "game.inspector.ship_group.action.disabled.no_planet";
		if (!friendlyPlanet) return "game.inspector.ship_group.action.disabled.foreign_planet";
		if (shipClass === null) return "game.inspector.ship_group.action.disabled.unknown_class";
		if (!techHeadroom) return "game.inspector.ship_group.action.disabled.no_headroom";
		return null;
	});
	const dismantleDisabledReason = $derived.by((): TranslationKey | null => {
		if (lockedReason !== null) return lockedReason;
		if (!inOrbit) return "game.inspector.ship_group.action.disabled.not_in_orbit";
		return null;
	});
	const transferDisabledReason = $derived.by((): TranslationKey | null => {
		if (lockedReason !== null) return lockedReason;
		if (!inOrbit) return "game.inspector.ship_group.action.disabled.not_in_orbit";
		if (otherRaces.length === 0)
			return "game.inspector.ship_group.action.disabled.no_other_races";
		return null;
	});
	const joinFleetDisabledReason = $derived.by((): TranslationKey | null => {
		if (lockedReason !== null) return lockedReason;
		if (!inOrbit) return "game.inspector.ship_group.action.disabled.not_in_orbit";
		return null;
	});

	function maxShips(): number {
		return group.count;
	}

	function clampShips(value: number): number {
		if (!Number.isFinite(value)) return 1;
		const v = Math.floor(value);
		if (v < 1) return 1;
		if (v > group.count) return group.count;
		return v;
	}

	function openSplit(): void {
		closeOthers("split");
		splitShips = Math.max(1, Math.min(Math.floor(group.count / 2), group.count - 1));
	}
	async function openSend(): Promise<void> {
		// "Send" is a two-stage flow: the click drops the inspector
		// straight into map-pick mode, and the form (ship count +
		// confirm) only appears after the player chooses a destination
		// or cancels. The destination is therefore never an editable
		// control inside the form — picking is the entry point, not a
		// sub-step. Re-clicking the action while picking cancels the
		// session; re-clicking while the form is open closes it.
		if (sendPicking) {
			pick?.cancel();
			sendPicking = false;
			openForm = null;
			return;
		}
		if (openForm === "send") {
			openForm = null;
			return;
		}
		openForm = null;
		if (pick === undefined || draft === undefined) return;
		if (orbitPlanet === null || reachableSet.size === 0) return;
		sendShips = group.count;
		sendDestination = null;
		sendPicking = true;
		let picked: number | null = null;
		try {
			picked = await pick.pick({
				sourcePlanetNumber: orbitPlanet.number,
				reachableIds: reachableSet,
			});
		} finally {
			sendPicking = false;
		}
		if (picked === null) return;
		sendDestination = picked;
		openForm = "send";
	}
	function openLoad(): void {
		closeOthers("load");
		loadShips = group.count;
		// Initialise cargo type to whatever the group already carries
		// so the validator does not require an extra click; if empty,
		// default to colonists (the highest priority cargo on routes).
		loadCargo = group.cargo === "NONE" ? "COL" : (group.cargo as ShipGroupCargo);
		loadQuantity = 0;
	}
	function openUnload(): void {
		closeOthers("unload");
		unloadShips = group.count;
		unloadQuantity = group.load;
	}
	function openModernize(): void {
		closeOthers("modernize");
		modernizeShips = group.count;
		modernizeTech = "ALL";
		modernizeLevel = 0;
	}
	function openDismantle(): void {
		closeOthers("dismantle");
		dismantleShips = group.count;
		dismantleConfirmed = false;
	}
	function openTransfer(): void {
		closeOthers("transfer");
		transferShips = group.count;
		transferAcceptor = otherRaces[0] ?? "";
	}
	function openJoinFleet(): void {
		closeOthers("joinFleet");
		const candidates = fleetsOnSamePlanet;
		joinFleetMode = candidates.length > 0 ? "existing" : "new";
		joinFleetExisting = candidates[0]?.name ?? "";
		joinFleetNew = "";
	}

	function closeOthers(target: FormName): void {
		if (openForm === target) {
			openForm = null;
			return;
		}
		openForm = target;
	}

	function cancelForm(): void {
		// Cancel any in-flight pick session so the renderer drops the
		// dim mask before the form re-opens for a different action.
		if (sendPicking) {
			pick?.cancel();
			sendPicking = false;
		}
		openForm = null;
	}

	function actionGroupId(targetShips: number): {
		groupId: string;
		breakCommand: OrderCommand | null;
	} {
		// When the action targets fewer ships than the group holds, the
		// engine needs a fresh sub-group; we mint its UUID up front and
		// emit a `breakShipGroup` command that carves it out before the
		// real action runs against the new id.
		if (targetShips >= group.count) {
			return { groupId: group.id, breakCommand: null };
		}
		const newGroupId = crypto.randomUUID();
		return {
			groupId: newGroupId,
			breakCommand: {
				kind: "breakShipGroup",
				id: crypto.randomUUID(),
				groupId: group.id,
				newGroupId,
				quantity: targetShips,
			},
		};
	}

	async function emit(
		breakCommand: OrderCommand | null,
		action: OrderCommand,
	): Promise<void> {
		if (draft === undefined) return;
		if (breakCommand !== null) await draft.add(breakCommand);
		await draft.add(action);
	}

	async function confirmSplit(): Promise<void> {
		const ships = clampShips(splitShips);
		if (ships < 1 || ships >= group.count || draft === undefined) return;
		await draft.add({
			kind: "breakShipGroup",
			id: crypto.randomUUID(),
			groupId: group.id,
			newGroupId: crypto.randomUUID(),
			quantity: ships,
		});
		openForm = null;
	}

	async function confirmSend(): Promise<void> {
		if (sendDestination === null || draft === undefined) return;
		const ships = clampShips(sendShips);
		const { groupId, breakCommand } = actionGroupId(ships);
		await emit(breakCommand, {
			kind: "sendShipGroup",
			id: crypto.randomUUID(),
			groupId,
			destinationPlanetNumber: sendDestination,
		});
		openForm = null;
	}

	async function confirmLoad(): Promise<void> {
		const ships = clampShips(loadShips);
		const quantity = Number(loadQuantity);
		if (!(quantity > 0) || draft === undefined) return;
		const { groupId, breakCommand } = actionGroupId(ships);
		await emit(breakCommand, {
			kind: "loadShipGroup",
			id: crypto.randomUUID(),
			groupId,
			cargo: loadCargo,
			quantity,
		});
		openForm = null;
	}

	async function confirmUnload(): Promise<void> {
		const ships = clampShips(unloadShips);
		const quantity = Number(unloadQuantity);
		if (!(quantity > 0) || draft === undefined) return;
		const { groupId, breakCommand } = actionGroupId(ships);
		await emit(breakCommand, {
			kind: "unloadShipGroup",
			id: crypto.randomUUID(),
			groupId,
			quantity,
		});
		openForm = null;
	}

	async function confirmModernize(): Promise<void> {
		if (draft === undefined) return;
		const ships = clampShips(modernizeShips);
		const tech = modernizeTech;
		const level = tech === "ALL" ? 0 : Number(modernizeLevel);
		if (tech !== "ALL" && !(level > 0)) return;
		const { groupId, breakCommand } = actionGroupId(ships);
		await emit(breakCommand, {
			kind: "upgradeShipGroup",
			id: crypto.randomUUID(),
			groupId,
			tech,
			level,
		});
		openForm = null;
	}

	async function confirmDismantle(): Promise<void> {
		if (draft === undefined) return;
		const ships = clampShips(dismantleShips);
		const subset = ships < group.count;
		const colonistsOverForeign = !ownPlanet && !uninhabitedPlanet && subset
			? group.cargo === "COL" && group.load > 0
			: !ownPlanet && !uninhabitedPlanet && carryingColonists;
		if (colonistsOverForeign && !dismantleConfirmed) {
			dismantleConfirmed = true;
			return;
		}
		const { groupId, breakCommand } = actionGroupId(ships);
		await emit(breakCommand, {
			kind: "dismantleShipGroup",
			id: crypto.randomUUID(),
			groupId,
		});
		openForm = null;
	}

	async function confirmTransfer(): Promise<void> {
		if (draft === undefined) return;
		const ships = clampShips(transferShips);
		const acceptor = transferAcceptor.trim();
		if (acceptor === "") return;
		const { groupId, breakCommand } = actionGroupId(ships);
		await emit(breakCommand, {
			kind: "transferShipGroup",
			id: crypto.randomUUID(),
			groupId,
			acceptor,
		});
		openForm = null;
	}

	async function confirmJoinFleet(): Promise<void> {
		if (draft === undefined) return;
		const name = joinFleetMode === "existing" ? joinFleetExisting : joinFleetNew;
		if (!validateEntityName(name).ok) return;
		await draft.add({
			kind: "joinFleetShipGroup",
			id: crypto.randomUUID(),
			groupId: group.id,
			name,
		});
		openForm = null;
	}

	const modernizeCostPreview = $derived.by(() => {
		const core = coreHandle?.core ?? null;
		if (core === null || shipClass === null) return null;
		const ships = clampShips(modernizeShips);
		const targets: Record<"DRIVE" | "WEAPONS" | "SHIELDS" | "CARGO", number> =
			modernizeTech === "ALL"
				? {
						DRIVE: localPlayerDrive,
						WEAPONS: localPlayerWeapons,
						SHIELDS: localPlayerShields,
						CARGO: localPlayerCargo,
					}
				: {
						DRIVE: 0,
						WEAPONS: 0,
						SHIELDS: 0,
						CARGO: 0,
						[modernizeTech]: Number(modernizeLevel) || 0,
					};
		const weapons = core.weaponsBlockMass({
			weapons: shipClass.weapons,
			armament: shipClass.armament,
		});
		const blocks: Array<[number, number, number]> = [
			[shipClass.drive, group.tech.drive, targets.DRIVE],
			[weapons ?? 0, group.tech.weapons, targets.WEAPONS],
			[shipClass.shields, group.tech.shields, targets.SHIELDS],
			[shipClass.cargo, group.tech.cargo, targets.CARGO],
		];
		let perShip = 0;
		for (const [mass, current, target] of blocks) {
			if (target <= 0) continue;
			perShip += core.blockUpgradeCost({
				blockMass: mass,
				currentTech: current,
				targetTech: target,
			});
		}
		if (perShip <= 0) return 0;
		return perShip * ships;
	});

	const cargoLabelKey: Record<ShipGroupCargo, TranslationKey> = {
		COL: "game.inspector.ship_group.cargo.col",
		CAP: "game.inspector.ship_group.cargo.cap",
		MAT: "game.inspector.ship_group.cargo.mat",
	};
	const techLabelKey: Record<ShipGroupUpgradeTech, TranslationKey> = {
		ALL: "game.inspector.ship_group.action.tech.all",
		DRIVE: "game.inspector.ship_group.action.tech.drive",
		WEAPONS: "game.inspector.ship_group.action.tech.weapons",
		SHIELDS: "game.inspector.ship_group.action.tech.shields",
		CARGO: "game.inspector.ship_group.action.tech.cargo",
	};

	function formatNumber(value: number): string {
		return value.toLocaleString(undefined, { maximumFractionDigits: 2 });
	}
</script>

<section class="actions" data-testid="inspector-ship-group-actions">
	{#if pendingLockingCommand !== null}
		<p class="locked" data-testid="inspector-ship-group-actions-locked">
			{i18n.t("game.inspector.ship_group.action.locked.banner", {
				command: lockedKindLabel,
			})}
		</p>
	{/if}
	<div class="row">
		<button
			type="button"
			class="action"
			data-testid="inspector-ship-group-action-split"
			disabled={splitDisabledReason !== null || draft === undefined}
			title={reasonTooltip(splitDisabledReason) ?? ""}
			onclick={openSplit}
		>
			{i18n.t("game.inspector.ship_group.action.split")}
		</button>
		<button
			type="button"
			class="action"
			data-testid="inspector-ship-group-action-send"
			disabled={sendDisabledReason !== null || draft === undefined}
			title={reasonTooltip(sendDisabledReason) ?? ""}
			onclick={() => void openSend()}
		>
			{i18n.t("game.inspector.ship_group.action.send")}
		</button>
		<button
			type="button"
			class="action"
			data-testid="inspector-ship-group-action-load"
			disabled={loadDisabledReason !== null || draft === undefined}
			title={reasonTooltip(loadDisabledReason) ?? ""}
			onclick={openLoad}
		>
			{i18n.t("game.inspector.ship_group.action.load")}
		</button>
		<button
			type="button"
			class="action"
			data-testid="inspector-ship-group-action-unload"
			disabled={unloadDisabledReason !== null || draft === undefined}
			title={reasonTooltip(unloadDisabledReason) ?? ""}
			onclick={openUnload}
		>
			{i18n.t("game.inspector.ship_group.action.unload")}
		</button>
		<button
			type="button"
			class="action"
			data-testid="inspector-ship-group-action-modernize"
			disabled={modernizeDisabledReason !== null || draft === undefined}
			title={reasonTooltip(modernizeDisabledReason) ?? ""}
			onclick={openModernize}
		>
			{i18n.t("game.inspector.ship_group.action.modernize")}
		</button>
		<button
			type="button"
			class="action"
			data-testid="inspector-ship-group-action-dismantle"
			disabled={dismantleDisabledReason !== null || draft === undefined}
			title={reasonTooltip(dismantleDisabledReason) ?? ""}
			onclick={openDismantle}
		>
			{i18n.t("game.inspector.ship_group.action.dismantle")}
		</button>
		<button
			type="button"
			class="action"
			data-testid="inspector-ship-group-action-transfer"
			disabled={transferDisabledReason !== null || draft === undefined}
			title={reasonTooltip(transferDisabledReason) ?? ""}
			onclick={openTransfer}
		>
			{i18n.t("game.inspector.ship_group.action.transfer")}
		</button>
		<button
			type="button"
			class="action"
			data-testid="inspector-ship-group-action-join-fleet"
			disabled={joinFleetDisabledReason !== null || draft === undefined}
			title={reasonTooltip(joinFleetDisabledReason) ?? ""}
			onclick={openJoinFleet}
		>
			{i18n.t("game.inspector.ship_group.action.join_fleet")}
		</button>
	</div>

	{#if openForm === "split"}
		<form class="form" data-testid="inspector-ship-group-form-split" onsubmit={(e) => { e.preventDefault(); void confirmSplit(); }}>
			<label>
				{i18n.t("game.inspector.ship_group.action.field.ships", { max: String(maxShips()) })}
				<input
					type="number"
					min="1"
					max={String(group.count - 1)}
					step="1"
					data-testid="inspector-ship-group-form-split-ships"
					bind:value={splitShips}
				/>
			</label>
			<div class="form-actions">
				<button type="button" data-testid="inspector-ship-group-form-split-cancel" onclick={cancelForm}>
					{i18n.t("game.inspector.ship_group.action.cancel")}
				</button>
				<button
					type="submit"
					class="primary"
					data-testid="inspector-ship-group-form-split-confirm"
					disabled={splitShips < 1 || splitShips >= group.count}
				>
					{i18n.t("game.inspector.ship_group.action.confirm")}
				</button>
			</div>
		</form>
	{/if}

	{#if openForm === "send" && sendDestination !== null}
		<form class="form" data-testid="inspector-ship-group-form-send" onsubmit={(e) => { e.preventDefault(); void confirmSend(); }}>
			<p class="destination-readonly">
				<span class="label">{i18n.t("game.inspector.ship_group.action.field.destination")}</span>
				<span data-testid="inspector-ship-group-form-send-destination">
					{planets.find((p) => p.number === sendDestination)?.name ?? `#${sendDestination}`}
				</span>
			</p>
			<label>
				{i18n.t("game.inspector.ship_group.action.field.ships", { max: String(maxShips()) })}
				<input
					type="number"
					min="1"
					max={String(group.count)}
					step="1"
					data-testid="inspector-ship-group-form-send-ships"
					bind:value={sendShips}
				/>
			</label>
			<div class="form-actions">
				<button type="button" data-testid="inspector-ship-group-form-send-cancel" onclick={cancelForm}>
					{i18n.t("game.inspector.ship_group.action.cancel")}
				</button>
				<button
					type="submit"
					class="primary"
					data-testid="inspector-ship-group-form-send-confirm"
				>
					{i18n.t("game.inspector.ship_group.action.confirm")}
				</button>
			</div>
		</form>
	{/if}

	{#if sendPicking}
		<p class="hint" data-testid="inspector-ship-group-form-send-pick-prompt">
			{i18n.t("game.inspector.ship_group.action.send.pick_prompt")}
		</p>
	{/if}

	{#if openForm === "load"}
		<form class="form" data-testid="inspector-ship-group-form-load" onsubmit={(e) => { e.preventDefault(); void confirmLoad(); }}>
			<label>
				{i18n.t("game.inspector.ship_group.action.field.ships", { max: String(maxShips()) })}
				<input
					type="number"
					min="1"
					max={String(group.count)}
					step="1"
					data-testid="inspector-ship-group-form-load-ships"
					bind:value={loadShips}
				/>
			</label>
			<label>
				{i18n.t("game.inspector.ship_group.action.field.cargo")}
				<select
					data-testid="inspector-ship-group-form-load-cargo"
					bind:value={loadCargo}
					disabled={group.cargo !== "NONE"}
				>
					{#each SHIP_GROUP_CARGO_VALUES as value (value)}
						<option {value} disabled={planetStock[value] <= 0}>
							{i18n.t(cargoLabelKey[value])}
						</option>
					{/each}
				</select>
			</label>
			<label>
				{i18n.t("game.inspector.ship_group.action.field.quantity")}
				<input
					type="number"
					min="0"
					step="0.01"
					data-testid="inspector-ship-group-form-load-quantity"
					bind:value={loadQuantity}
				/>
			</label>
			<div class="form-actions">
				<button type="button" data-testid="inspector-ship-group-form-load-cancel" onclick={cancelForm}>
					{i18n.t("game.inspector.ship_group.action.cancel")}
				</button>
				<button
					type="submit"
					class="primary"
					data-testid="inspector-ship-group-form-load-confirm"
					disabled={!(loadQuantity > 0)}
				>
					{i18n.t("game.inspector.ship_group.action.confirm")}
				</button>
			</div>
		</form>
	{/if}

	{#if openForm === "unload"}
		<form class="form" data-testid="inspector-ship-group-form-unload" onsubmit={(e) => { e.preventDefault(); void confirmUnload(); }}>
			<label>
				{i18n.t("game.inspector.ship_group.action.field.ships", { max: String(maxShips()) })}
				<input
					type="number"
					min="1"
					max={String(group.count)}
					step="1"
					data-testid="inspector-ship-group-form-unload-ships"
					bind:value={unloadShips}
				/>
			</label>
			<label>
				{i18n.t("game.inspector.ship_group.action.field.quantity")}
				<input
					type="number"
					min="0"
					step="0.01"
					data-testid="inspector-ship-group-form-unload-quantity"
					bind:value={unloadQuantity}
				/>
			</label>
			<div class="form-actions">
				<button type="button" data-testid="inspector-ship-group-form-unload-cancel" onclick={cancelForm}>
					{i18n.t("game.inspector.ship_group.action.cancel")}
				</button>
				<button
					type="submit"
					class="primary"
					data-testid="inspector-ship-group-form-unload-confirm"
					disabled={!(unloadQuantity > 0)}
				>
					{i18n.t("game.inspector.ship_group.action.confirm")}
				</button>
			</div>
		</form>
	{/if}

	{#if openForm === "modernize"}
		<form class="form" data-testid="inspector-ship-group-form-modernize" onsubmit={(e) => { e.preventDefault(); void confirmModernize(); }}>
			<label>
				{i18n.t("game.inspector.ship_group.action.field.ships", { max: String(maxShips()) })}
				<input
					type="number"
					min="1"
					max={String(group.count)}
					step="1"
					data-testid="inspector-ship-group-form-modernize-ships"
					bind:value={modernizeShips}
				/>
			</label>
			<label>
				{i18n.t("game.inspector.ship_group.action.field.tech")}
				<select
					data-testid="inspector-ship-group-form-modernize-tech"
					bind:value={modernizeTech}
				>
					{#each SHIP_GROUP_UPGRADE_TECH_VALUES as value (value)}
						<option {value}>{i18n.t(techLabelKey[value])}</option>
					{/each}
				</select>
			</label>
			{#if modernizeTech !== "ALL"}
				<label>
					{i18n.t("game.inspector.ship_group.action.field.level")}
					<input
						type="number"
						min="0"
						step="0.01"
						data-testid="inspector-ship-group-form-modernize-level"
						bind:value={modernizeLevel}
					/>
				</label>
			{/if}
			<p class="preview" data-testid="inspector-ship-group-form-modernize-cost">
				{#if modernizeCostPreview === null}
					{i18n.t("game.inspector.ship_group.action.modernize.cost_unavailable")}
				{:else}
					{i18n.t("game.inspector.ship_group.action.modernize.cost", {
						cost: formatNumber(modernizeCostPreview),
					})}
				{/if}
			</p>
			<div class="form-actions">
				<button type="button" data-testid="inspector-ship-group-form-modernize-cancel" onclick={cancelForm}>
					{i18n.t("game.inspector.ship_group.action.cancel")}
				</button>
				<button
					type="submit"
					class="primary"
					data-testid="inspector-ship-group-form-modernize-confirm"
					disabled={modernizeTech !== "ALL" && !(modernizeLevel > 0)}
				>
					{i18n.t("game.inspector.ship_group.action.confirm")}
				</button>
			</div>
		</form>
	{/if}

	{#if openForm === "dismantle"}
		<form class="form" data-testid="inspector-ship-group-form-dismantle" onsubmit={(e) => { e.preventDefault(); void confirmDismantle(); }}>
			<label>
				{i18n.t("game.inspector.ship_group.action.field.ships", { max: String(maxShips()) })}
				<input
					type="number"
					min="1"
					max={String(group.count)}
					step="1"
					data-testid="inspector-ship-group-form-dismantle-ships"
					bind:value={dismantleShips}
				/>
			</label>
			{#if !ownPlanet && !uninhabitedPlanet && carryingColonists}
				<p class="warning" data-testid="inspector-ship-group-form-dismantle-warning">
					{i18n.t("game.inspector.ship_group.action.dismantle.warning")}
				</p>
			{/if}
			<div class="form-actions">
				<button type="button" data-testid="inspector-ship-group-form-dismantle-cancel" onclick={cancelForm}>
					{i18n.t("game.inspector.ship_group.action.cancel")}
				</button>
				<button
					type="submit"
					class="primary"
					data-testid="inspector-ship-group-form-dismantle-confirm"
				>
					{#if !ownPlanet && !uninhabitedPlanet && carryingColonists && !dismantleConfirmed}
						{i18n.t("game.inspector.ship_group.action.confirm_destroy")}
					{:else}
						{i18n.t("game.inspector.ship_group.action.confirm")}
					{/if}
				</button>
			</div>
		</form>
	{/if}

	{#if openForm === "transfer"}
		<form class="form" data-testid="inspector-ship-group-form-transfer" onsubmit={(e) => { e.preventDefault(); void confirmTransfer(); }}>
			<label>
				{i18n.t("game.inspector.ship_group.action.field.ships", { max: String(maxShips()) })}
				<input
					type="number"
					min="1"
					max={String(group.count)}
					step="1"
					data-testid="inspector-ship-group-form-transfer-ships"
					bind:value={transferShips}
				/>
			</label>
			<label>
				{i18n.t("game.inspector.ship_group.action.field.acceptor")}
				<select
					data-testid="inspector-ship-group-form-transfer-acceptor"
					bind:value={transferAcceptor}
				>
					{#each otherRaces as race (race)}
						<option value={race}>{race}</option>
					{/each}
				</select>
			</label>
			<div class="form-actions">
				<button type="button" data-testid="inspector-ship-group-form-transfer-cancel" onclick={cancelForm}>
					{i18n.t("game.inspector.ship_group.action.cancel")}
				</button>
				<button
					type="submit"
					class="primary"
					data-testid="inspector-ship-group-form-transfer-confirm"
					disabled={transferAcceptor === ""}
				>
					{i18n.t("game.inspector.ship_group.action.confirm")}
				</button>
			</div>
		</form>
	{/if}

	{#if openForm === "joinFleet"}
		<form class="form" data-testid="inspector-ship-group-form-join-fleet" onsubmit={(e) => { e.preventDefault(); void confirmJoinFleet(); }}>
			{#if fleetsOnSamePlanet.length > 0}
				<label>
					<input
						type="radio"
						name="fleet-mode"
						value="existing"
						bind:group={joinFleetMode}
						data-testid="inspector-ship-group-form-join-fleet-mode-existing"
					/>
					{i18n.t("game.inspector.ship_group.action.field.fleet")}
				</label>
				{#if joinFleetMode === "existing"}
					<select
						data-testid="inspector-ship-group-form-join-fleet-existing"
						bind:value={joinFleetExisting}
					>
						{#each fleetsOnSamePlanet as fleet (fleet.name)}
							<option value={fleet.name}>{fleet.name}</option>
						{/each}
					</select>
				{/if}
			{/if}
			<label>
				<input
					type="radio"
					name="fleet-mode"
					value="new"
					bind:group={joinFleetMode}
					data-testid="inspector-ship-group-form-join-fleet-mode-new"
				/>
				{i18n.t("game.inspector.ship_group.action.fleet.create_new")}
			</label>
			{#if joinFleetMode === "new"}
				<input
					type="text"
					data-testid="inspector-ship-group-form-join-fleet-new"
					bind:value={joinFleetNew}
					placeholder={i18n.t("game.inspector.ship_group.action.field.fleet")}
				/>
			{/if}
			<div class="form-actions">
				<button type="button" data-testid="inspector-ship-group-form-join-fleet-cancel" onclick={cancelForm}>
					{i18n.t("game.inspector.ship_group.action.cancel")}
				</button>
				<button
					type="submit"
					class="primary"
					data-testid="inspector-ship-group-form-join-fleet-confirm"
					disabled={(joinFleetMode === "existing" ? joinFleetExisting : joinFleetNew) === ""}
				>
					{i18n.t("game.inspector.ship_group.action.confirm")}
				</button>
			</div>
		</form>
	{/if}

	{#if disabledStateTooltip() !== null && openForm === null}
		<p class="hint" data-testid="inspector-ship-group-actions-state-hint">
			{disabledStateTooltip()}
		</p>
	{/if}
</section>

<style>
	.actions {
		display: flex;
		flex-direction: column;
		gap: 0.5rem;
	}
	.row {
		display: flex;
		flex-wrap: wrap;
		gap: 0.35rem;
	}
	.action {
		font: inherit;
		font-size: 0.85rem;
		padding: 0.2rem 0.55rem;
		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.4;
	}
	.form {
		display: flex;
		flex-direction: column;
		gap: 0.4rem;
		padding: 0.5rem;
		border: 1px solid var(--color-border);
		border-radius: 3px;
		background: var(--color-surface-raised);
	}
	.form label {
		display: flex;
		flex-direction: column;
		gap: 0.2rem;
		font-size: 0.85rem;
		color: var(--color-text-muted);
	}
	.form input[type="number"],
	.form input[type="text"],
	.form select {
		font: inherit;
		padding: 0.25rem 0.4rem;
		background: var(--color-bg);
		color: var(--color-text);
		border: 1px solid var(--color-border);
		border-radius: 3px;
	}
	.form .destination-readonly {
		margin: 0;
		display: flex;
		flex-wrap: wrap;
		align-items: baseline;
		gap: 0.4rem;
		font-size: 0.85rem;
	}
	.form .destination-readonly .label {
		color: var(--color-text-muted);
	}
	.form-actions {
		display: flex;
		gap: 0.4rem;
		margin-top: 0.25rem;
	}
	.form-actions button {
		font: inherit;
		font-size: 0.85rem;
		padding: 0.25rem 0.65rem;
		background: transparent;
		color: var(--color-text-muted);
		border: 1px solid var(--color-border);
		border-radius: 3px;
		cursor: pointer;
	}
	.form-actions button.primary {
		color: var(--color-text);
		border-color: var(--color-accent);
	}
	.form-actions button:disabled {
		cursor: not-allowed;
		opacity: 0.5;
	}
	.preview {
		margin: 0;
		font-size: 0.85rem;
		color: var(--color-text-muted);
	}
	.warning {
		margin: 0;
		font-size: 0.85rem;
		color: var(--color-warning);
	}
	.locked {
		margin: 0;
		padding: 0.4rem 0.55rem;
		font-size: 0.85rem;
		color: var(--color-text-muted);
		background: var(--color-surface-overlay);
		border: 1px solid var(--color-border);
		border-radius: 3px;
	}
	.hint {
		margin: 0;
		font-size: 0.8rem;
		color: var(--color-text-muted);
	}
</style>
