<!--
Phase 17 ship-class designer. Two modes driven by the optional
`classId` URL segment:

  - **new (no classId)** — empty form with five numeric fields
    plus name. Save is disabled until `validateShipClass` returns
    `ok`; the localised tooltip mirrors `validateEntityName`'s
    invalid-reason messages and the value-rule mirrors of
    `pkg/calc/validator.go.ValidateShipTypeValues`. Save adds a
    `createShipClass` to the local order draft and returns to the
    table.
  - **view (classId set)** — read-only render of the matching row
    from the optimistic overlay. Ship classes are designed once
    and cannot be modified after creation (per
    `game/rules.txt`); the in-game upgrade story lives elsewhere
    (`CommandShipGroupUpgrade`, Phase 19/20). The view exposes a
    Delete affordance (engine-side checks ensure the class is not
    referenced by active production / ship groups) and a Back
    button.

Phase 18 wires `pkg/calc/` (via the `Core` WASM bridge) into the
new-mode form: an `<aside class="preview">` block recomputes mass,
full-load mass, max speed, range at full load, and cargo capacity
on every form change, using the local player's tech levels off the
rendered report. The preview hides itself until the form passes
validation, so it never displays half-cooked numbers.
-->
<script lang="ts">
	import { getContext, tick } from "svelte";
	import { goto } from "$app/navigation";
	import { page } from "$app/state";

	import type { ShipClassSummary } 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 {
		validateShipClass,
		type ShipClassInvalidReason,
	} from "$lib/util/ship-class-validation";
	import {
		CORE_CONTEXT_KEY,
		type CoreHandle,
	} from "$lib/core-context.svelte";

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

	const gameId = $derived(page.params.id ?? "");
	const classId = $derived(page.params.classId ?? "");
	const isViewMode = $derived(classId !== "");

	const localShipClass = $derived<ShipClassSummary[]>(
		rendered?.report?.localShipClass ?? [],
	);
	const existingNames = $derived(localShipClass.map((cls) => cls.name));
	const viewing = $derived(
		isViewMode
			? localShipClass.find((cls) => cls.name === classId) ?? null
			: null,
	);

	let name = $state("");
	let drive = $state(0);
	let armament = $state(0);
	let weapons = $state(0);
	let shields = $state(0);
	let cargo = $state(0);
	let nameInputEl: HTMLInputElement | null = $state(null);

	const invalidReasonKeyMap: Record<ShipClassInvalidReason, TranslationKey> = {
		empty: "game.designer.ship_class.invalid.empty",
		too_long: "game.designer.ship_class.invalid.too_long",
		starts_with_special: "game.designer.ship_class.invalid.starts_with_special",
		ends_with_special: "game.designer.ship_class.invalid.ends_with_special",
		consecutive_specials:
			"game.designer.ship_class.invalid.consecutive_specials",
		whitespace: "game.designer.ship_class.invalid.whitespace",
		disallowed_character:
			"game.designer.ship_class.invalid.disallowed_character",
		duplicate_name: "game.designer.ship_class.invalid.duplicate_name",
		drive_value: "game.designer.ship_class.invalid.drive_value",
		armament_value: "game.designer.ship_class.invalid.armament_value",
		armament_not_integer:
			"game.designer.ship_class.invalid.armament_not_integer",
		weapons_value: "game.designer.ship_class.invalid.weapons_value",
		shields_value: "game.designer.ship_class.invalid.shields_value",
		cargo_value: "game.designer.ship_class.invalid.cargo_value",
		armament_weapons_pair:
			"game.designer.ship_class.invalid.armament_weapons_pair",
		all_zero: "game.designer.ship_class.invalid.all_zero",
	};

	const validation = $derived(
		validateShipClass(
			{ name, drive, armament, weapons, shields, cargo },
			{ existingNames },
		),
	);
	const invalidMessage = $derived(
		validation.ok ? "" : i18n.t(invalidReasonKeyMap[validation.reason]),
	);
	const canSave = $derived(validation.ok && draft !== undefined);

	const driveTech = $derived(rendered?.report?.localPlayerDrive ?? 0);
	const cargoTech = $derived(rendered?.report?.localPlayerCargo ?? 0);

	interface PreviewValues {
		mass: number;
		fullLoadMass: number;
		maxSpeed: number;
		rangeAtFull: number;
		cargoCapacity: number;
	}

	const preview = $derived.by<PreviewValues | null>(() => {
		const core = coreHandle?.core;
		if (core === undefined || core === null) return null;
		if (!validation.ok) return null;
		const v = validation.value;
		const mass = core.emptyMass({
			drive: v.drive,
			weapons: v.weapons,
			armament: v.armament,
			shields: v.shields,
			cargo: v.cargo,
		});
		if (mass === null) return null;
		const cargoCapacity = core.cargoCapacity({
			cargo: v.cargo,
			cargoTech,
		});
		const carryAtFull =
			cargoTech > 0
				? core.carryingMass({ load: cargoCapacity, cargoTech })
				: 0;
		const fullLoadMass = core.fullMass({
			emptyMass: mass,
			carryingMass: carryAtFull,
		});
		const driveEffective = core.driveEffective({
			drive: v.drive,
			driveTech,
		});
		const maxSpeed = core.speed({ driveEffective, fullMass: mass });
		const rangeAtFull = core.speed({
			driveEffective,
			fullMass: fullLoadMass,
		});
		return { mass, fullLoadMass, maxSpeed, rangeAtFull, cargoCapacity };
	});

	$effect(() => {
		if (!isViewMode) {
			void tick().then(() => nameInputEl?.focus());
		}
	});

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

	function backToTable(): void {
		void goto(`/games/${gameId}/table/ship-classes`);
	}

	async function save(): Promise<void> {
		if (!validation.ok || draft === undefined) return;
		await draft.add({
			kind: "createShipClass",
			id: crypto.randomUUID(),
			name: validation.value.name,
			drive: validation.value.drive,
			armament: validation.value.armament,
			weapons: validation.value.weapons,
			shields: validation.value.shields,
			cargo: validation.value.cargo,
		});
		backToTable();
	}

	async function deleteThis(): Promise<void> {
		if (viewing === null || draft === undefined) return;
		await draft.add({
			kind: "removeShipClass",
			id: crypto.randomUUID(),
			name: viewing.name,
		});
		backToTable();
	}
</script>

<section
	class="active-view"
	data-testid="active-view-designer-ship-class"
	data-mode={isViewMode ? "view" : "new"}
>
	{#if isViewMode}
		{#if viewing === null}
			<h2>{i18n.t("game.view.designer.ship_class")}</h2>
			<p class="not-found" data-testid="designer-ship-class-not-found">
				{i18n.t("game.designer.ship_class.not_found", { name: classId })}
			</p>
			<div class="actions">
				<button
					type="button"
					data-testid="designer-ship-class-back"
					onclick={backToTable}
				>
					{i18n.t("game.designer.ship_class.action.back")}
				</button>
			</div>
		{:else}
			<h2 data-testid="designer-ship-class-title">
				{i18n.t("game.designer.ship_class.title.view", { name: viewing.name })}
			</h2>
			<p class="notice" data-testid="designer-ship-class-notice">
				{i18n.t("game.designer.ship_class.read_only_notice")}
			</p>
			<dl class="fields">
				<div class="field">
					<dt>{i18n.t("game.designer.ship_class.field.name")}</dt>
					<dd data-testid="designer-ship-class-view-name">{viewing.name}</dd>
				</div>
				<div class="field">
					<dt>{i18n.t("game.designer.ship_class.field.drive")}</dt>
					<dd data-testid="designer-ship-class-view-drive">
						{formatNumber(viewing.drive)}
					</dd>
				</div>
				<div class="field">
					<dt>{i18n.t("game.designer.ship_class.field.armament")}</dt>
					<dd data-testid="designer-ship-class-view-armament">
						{viewing.armament}
					</dd>
				</div>
				<div class="field">
					<dt>{i18n.t("game.designer.ship_class.field.weapons")}</dt>
					<dd data-testid="designer-ship-class-view-weapons">
						{formatNumber(viewing.weapons)}
					</dd>
				</div>
				<div class="field">
					<dt>{i18n.t("game.designer.ship_class.field.shields")}</dt>
					<dd data-testid="designer-ship-class-view-shields">
						{formatNumber(viewing.shields)}
					</dd>
				</div>
				<div class="field">
					<dt>{i18n.t("game.designer.ship_class.field.cargo")}</dt>
					<dd data-testid="designer-ship-class-view-cargo">
						{formatNumber(viewing.cargo)}
					</dd>
				</div>
			</dl>
			<div class="actions">
				<button
					type="button"
					class="back"
					data-testid="designer-ship-class-back"
					onclick={backToTable}
				>
					{i18n.t("game.designer.ship_class.action.back")}
				</button>
				<button
					type="button"
					class="delete"
					data-testid="designer-ship-class-delete"
					disabled={draft === undefined}
					onclick={() => void deleteThis()}
				>
					{i18n.t("game.designer.ship_class.action.delete")}
				</button>
			</div>
		{/if}
	{:else}
		<h2 data-testid="designer-ship-class-title">
			{i18n.t("game.designer.ship_class.title.new")}
		</h2>
		<p class="hint" data-testid="designer-ship-class-hint">
			{i18n.t("game.designer.ship_class.hint.values")}
		</p>
		<form
			class="form"
			data-testid="designer-ship-class-form"
			onsubmit={(event) => {
				event.preventDefault();
				void save();
			}}
		>
			<label class="row">
				<span>{i18n.t("game.designer.ship_class.field.name")}</span>
				<input
					type="text"
					bind:this={nameInputEl}
					bind:value={name}
					data-testid="designer-ship-class-input-name"
					maxlength="30"
					aria-invalid={validation.ok ? "false" : "true"}
				/>
			</label>
			<label class="row">
				<span>{i18n.t("game.designer.ship_class.field.drive")}</span>
				<input
					type="number"
					step="0.01"
					min="0"
					bind:value={drive}
					data-testid="designer-ship-class-input-drive"
				/>
			</label>
			<label class="row">
				<span>{i18n.t("game.designer.ship_class.field.armament")}</span>
				<input
					type="number"
					step="1"
					min="0"
					bind:value={armament}
					data-testid="designer-ship-class-input-armament"
				/>
			</label>
			<label class="row">
				<span>{i18n.t("game.designer.ship_class.field.weapons")}</span>
				<input
					type="number"
					step="0.01"
					min="0"
					bind:value={weapons}
					data-testid="designer-ship-class-input-weapons"
				/>
			</label>
			<label class="row">
				<span>{i18n.t("game.designer.ship_class.field.shields")}</span>
				<input
					type="number"
					step="0.01"
					min="0"
					bind:value={shields}
					data-testid="designer-ship-class-input-shields"
				/>
			</label>
			<label class="row">
				<span>{i18n.t("game.designer.ship_class.field.cargo")}</span>
				<input
					type="number"
					step="0.01"
					min="0"
					bind:value={cargo}
					data-testid="designer-ship-class-input-cargo"
				/>
			</label>
			{#if !validation.ok}
				<p class="error" data-testid="designer-ship-class-error">
					{invalidMessage}
				</p>
			{/if}
			{#if preview !== null}
				<aside
					class="preview"
					data-testid="designer-ship-class-preview"
				>
					<h3>{i18n.t("game.designer.ship_class.preview.title")}</h3>
					<dl>
						<div class="row">
							<dt>{i18n.t("game.designer.ship_class.preview.mass")}</dt>
							<dd data-testid="designer-ship-class-preview-mass">
								{formatNumber(preview.mass)}
							</dd>
						</div>
						<div class="row">
							<dt>
								{i18n.t("game.designer.ship_class.preview.full_load_mass")}
							</dt>
							<dd data-testid="designer-ship-class-preview-full-load-mass">
								{formatNumber(preview.fullLoadMass)}
							</dd>
						</div>
						<div class="row">
							<dt>
								{i18n.t("game.designer.ship_class.preview.max_speed")}
							</dt>
							<dd data-testid="designer-ship-class-preview-max-speed">
								{formatNumber(preview.maxSpeed)}
							</dd>
						</div>
						<div class="row">
							<dt>{i18n.t("game.designer.ship_class.preview.range")}</dt>
							<dd data-testid="designer-ship-class-preview-range">
								{formatNumber(preview.rangeAtFull)}
							</dd>
						</div>
						<div class="row">
							<dt>
								{i18n.t("game.designer.ship_class.preview.cargo_capacity")}
							</dt>
							<dd data-testid="designer-ship-class-preview-cargo-capacity">
								{formatNumber(preview.cargoCapacity)}
							</dd>
						</div>
					</dl>
				</aside>
			{/if}
			<div class="actions">
				<button
					type="button"
					class="cancel"
					data-testid="designer-ship-class-cancel"
					onclick={backToTable}
				>
					{i18n.t("game.designer.ship_class.action.cancel")}
				</button>
				<button
					type="submit"
					class="save"
					data-testid="designer-ship-class-save"
					disabled={!canSave}
					title={canSave ? "" : invalidMessage}
				>
					{i18n.t("game.designer.ship_class.action.save")}
				</button>
			</div>
		</form>
	{/if}
</section>

<style>
	.active-view {
		padding: 1.5rem;
		font-family: system-ui, sans-serif;
		display: flex;
		flex-direction: column;
		gap: 0.85rem;
	}
	.active-view h2 {
		margin: 0;
		font-size: 1.1rem;
	}
	.notice,
	.hint,
	.not-found {
		margin: 0;
		color: #888;
		font-size: 0.85rem;
	}
	.form {
		display: flex;
		flex-direction: column;
		gap: 0.55rem;
		max-width: 30rem;
	}
	.row {
		display: grid;
		grid-template-columns: 8rem 1fr;
		align-items: center;
		gap: 0.6rem;
	}
	.row span {
		color: #aab;
		font-size: 0.85rem;
	}
	.row input {
		font: inherit;
		padding: 0.3rem 0.5rem;
		background: #0a0e1a;
		color: #e8eaf6;
		border: 1px solid #2a3150;
		border-radius: 3px;
	}
	.row input[aria-invalid="true"] {
		border-color: #d97a7a;
	}
	.error {
		margin: 0;
		font-size: 0.8rem;
		color: #d97a7a;
	}
	.preview {
		display: flex;
		flex-direction: column;
		gap: 0.4rem;
		padding: 0.75rem 0.85rem;
		background: #0a0e1a;
		border: 1px solid #2a3150;
		border-radius: 4px;
		max-width: 30rem;
	}
	.preview h3 {
		margin: 0;
		font-size: 0.85rem;
		color: #aab;
		font-weight: 500;
	}
	.preview dl {
		margin: 0;
		display: grid;
		grid-template-columns: max-content 1fr;
		row-gap: 0.2rem;
		column-gap: 0.75rem;
	}
	.preview .row {
		display: contents;
	}
	.preview dt {
		color: #aab;
		font-size: 0.85rem;
	}
	.preview dd {
		margin: 0;
		font-variant-numeric: tabular-nums;
		font-size: 0.9rem;
		text-align: right;
	}
	.fields {
		margin: 0;
		display: grid;
		grid-template-columns: max-content 1fr;
		row-gap: 0.25rem;
		column-gap: 0.75rem;
		max-width: 30rem;
	}
	.field {
		display: contents;
	}
	.field dt {
		color: #aab;
		font-size: 0.85rem;
	}
	.field dd {
		margin: 0;
		font-variant-numeric: tabular-nums;
		font-size: 0.9rem;
	}
	.actions {
		display: flex;
		gap: 0.5rem;
	}
	.actions button {
		font: inherit;
		font-size: 0.9rem;
		padding: 0.3rem 0.7rem;
		background: transparent;
		color: #aab;
		border: 1px solid #2a3150;
		border-radius: 3px;
		cursor: pointer;
	}
	.actions button:not(:disabled):hover {
		color: #e8eaf6;
		border-color: #6d8cff;
	}
	.actions button:disabled {
		cursor: not-allowed;
		opacity: 0.5;
	}
	.actions .delete {
		color: #d97a7a;
	}
	.actions .delete:not(:disabled):hover {
		border-color: #d97a7a;
		color: #f0a0a0;
	}
</style>
