// Typed shape of a single command entry inside the local order
// draft. Phase 12 intentionally ships exactly one variant
// (`placeholder`) — Phase 14 lands the first real command
// (`planetRename`) together with the inspector UI that constructs
// it and the submit pipeline that drains the draft to the server.
//
// `OrderCommand` is a discriminated union on the `kind` field so
// later variants can extend the union without changing the array
// shape persisted in `Cache`. The whole draft round-trips through
// IndexedDB structured clone, so every variant must use only
// JSON-friendly value types (`string`, `number`, `boolean`,
// nested plain objects, and `Uint8Array`).

/**
 * PlaceholderCommand is the single variant shipped with the Phase 12
 * skeleton. It carries a stable `id` (used by remove and as a
 * `data-testid` suffix) and a human-readable `label` rendered in the
 * order tab's vertical list. The variant is deliberately content-free
 * so test fixtures and the empty composer skeleton do not pre-bias
 * Phase 14's first real command shape.
 */
export interface PlaceholderCommand {
	readonly kind: "placeholder";
	readonly id: string;
	readonly label: string;
}

/**
 * PlanetRenameCommand is the first real command variant — Phase 14
 * lands the rename action together with the submit pipeline. The
 * `name` is locally validated against `validateEntityName` (the TS
 * port of `pkg/util/string.go.ValidateTypeName`) before the entry is
 * accepted into the draft; the same rules run server-side, so a
 * locally-valid command is always accepted at the wire level.
 */
export interface PlanetRenameCommand {
	readonly kind: "planetRename";
	readonly id: string;
	readonly planetNumber: number;
	readonly name: string;
}

/**
 * ProductionType mirrors the engine `PlanetProduction` enum
 * (`pkg/schema/fbs/order.fbs`) and the binding tag on
 * `pkg/model/order/order.go.CommandPlanetProduce.Production`. The
 * values are wire-stable: the submit encoder maps them to the FBS
 * enum, the read-back decoder maps them back, and the optimistic
 * overlay derives the engine's display string from the same set.
 *
 * `MAT` is materials production, `CAP` is industry (the engine names
 * carry historical meaning — "Material" and "Capital" in the display
 * mapping). `DRIVE` / `WEAPONS` / `SHIELDS` / `CARGO` are the four
 * implicit per-tech research tracks (no subject required). `SCIENCE`
 * is research of a custom science package authored via
 * `CommandScienceCreate`; `SHIP` is build of a ship class authored
 * via `CommandShipClassCreate`. Both `SCIENCE` and `SHIP` require a
 * non-empty `subject` that passes `validateEntityName`; the engine
 * validator (`game/internal/router/validator.go`) enforces the same.
 */
export type ProductionType =
	| "MAT"
	| "CAP"
	| "DRIVE"
	| "WEAPONS"
	| "SHIELDS"
	| "CARGO"
	| "SCIENCE"
	| "SHIP";

/**
 * SetProductionTypeCommand switches a planet's production target.
 * Phase 15 is the first variant to carry a collapse-by-target rule:
 * the order draft store keeps at most one `setProductionType` per
 * `planetNumber`, replacing any earlier entry on `add`. `subject` is
 * the science or ship-class name when `productionType` is `SCIENCE`
 * or `SHIP`; for the other six values it is the empty string.
 */
export interface SetProductionTypeCommand {
	readonly kind: "setProductionType";
	readonly id: string;
	readonly planetNumber: number;
	readonly productionType: ProductionType;
	readonly subject: string;
}

/**
 * CargoLoadType mirrors the engine `PlanetRouteLoadType` enum
 * (`pkg/schema/fbs/order.fbs`). The values are wire-stable: the
 * submit encoder maps them to the FBS enum and the read-back
 * decoder maps them back. The four members enumerate the four
 * mutually-exclusive cargo-route slots a planet can drive at any
 * one time.
 *
 *   `COL` — colonists (highest priority on load),
 *   `CAP` — capital / industry crates,
 *   `MAT` — raw materials,
 *   `EMP` — empty ships returning to a producer.
 */
export type CargoLoadType = "COL" | "CAP" | "MAT" | "EMP";

/**
 * SetCargoRouteCommand binds a (source, loadType) slot to a
 * destination planet. Phase 16 carries a collapse-by-(source,
 * loadType) rule: at most one entry per slot lives in the draft at
 * any time. A `removeCargoRoute` for the same slot supersedes a
 * pending set (the engine accepts either order, but keeping the
 * draft minimal avoids confusing the order tab).
 */
export interface SetCargoRouteCommand {
	readonly kind: "setCargoRoute";
	readonly id: string;
	readonly sourcePlanetNumber: number;
	readonly destinationPlanetNumber: number;
	readonly loadType: CargoLoadType;
}

/**
 * RemoveCargoRouteCommand drops the (source, loadType) slot. Same
 * collapse rule as `SetCargoRouteCommand` — a later `set` for the
 * same slot supersedes the remove, and vice versa.
 */
export interface RemoveCargoRouteCommand {
	readonly kind: "removeCargoRoute";
	readonly id: string;
	readonly sourcePlanetNumber: number;
	readonly loadType: CargoLoadType;
}

/**
 * CreateShipClassCommand designs a new ship class with the five
 * tech-derived numbers plus a name. The numeric fields obey
 * `pkg/calc/validator.go.ValidateShipTypeValues`: each of `drive`,
 * `weapons`, `shields`, `cargo` is either zero or ≥ 1; `armament`
 * is a non-negative integer; `armament` and `weapons` are both zero
 * or both nonzero; not all five values may be zero. The TS-side
 * mirror lives in `lib/util/ship-class-validation.ts`. Phase 17
 * lands the CRUD UI; Phase 18 wires `pkg/calc/` for live previews.
 *
 * No collapse rule applies — each create is a distinct user-visible
 * action and the engine refuses duplicate names server-side.
 */
export interface CreateShipClassCommand {
	readonly kind: "createShipClass";
	readonly id: string;
	readonly name: string;
	readonly drive: number;
	readonly armament: number;
	readonly weapons: number;
	readonly shields: number;
	readonly cargo: number;
}

/**
 * RemoveShipClassCommand drops a designed ship class by name. The
 * engine refuses removals when the class is referenced by an active
 * planet production target or by an existing ship group
 * (`game/internal/controller/ship_class.go.shipClassRemove`); both
 * surface as `cmdApplied=false` on the response and the order tab
 * row reads `rejected`. No client-side pre-check is needed — Phase
 * 17 has no projection of ship groups yet.
 */
export interface RemoveShipClassCommand {
	readonly kind: "removeShipClass";
	readonly id: string;
	readonly name: string;
}

/**
 * CreateScienceCommand defines a new science — a named mix of four
 * tech proportions (`drive`, `weapons`, `shields`, `cargo`) that sums
 * to 1.0. The TS-side mirror lives in `lib/util/science-validation.ts`;
 * the designer enters the values as percentages (0..100) with a
 * strict sum-equals-100 gate and converts them to fractions on
 * dispatch, so wire entries always satisfy the engine's
 * `pkg/calc/validator.go.ValidateScienceValues` invariant
 * (every value in `[0, 1]`, sum `≈ 1.0` within float tolerance).
 *
 * No collapse rule applies — each create is a distinct user-visible
 * action and the engine refuses duplicate names server-side
 * (`game/internal/controller/science.go.scienceCreate`). Editing an
 * existing science means dispatching a `removeScience` first and
 * then a `createScience` with the new payload; there is no
 * `UpdateScience` on the wire (Phase 21 decision).
 */
export interface CreateScienceCommand {
	readonly kind: "createScience";
	readonly id: string;
	readonly name: string;
	readonly drive: number;
	readonly weapons: number;
	readonly shields: number;
	readonly cargo: number;
}

/**
 * RemoveScienceCommand drops a defined science by name. The engine
 * refuses removals when the science is referenced by an active planet
 * production target (`game/internal/controller/science.go.scienceRemove`)
 * — that surfaces as `cmdApplied=false` on the response and the order
 * tab row reads `rejected`.
 */
export interface RemoveScienceCommand {
	readonly kind: "removeScience";
	readonly id: string;
	readonly name: string;
}

/**
 * ShipGroupCargo mirrors the engine `ShipGroupCargo` enum
 * (`pkg/schema/fbs/order.fbs`). Three values: colonists, capital
 * (industry crates), and materials. Empty (`EMP`) is a route-level
 * concept (`PlanetRouteLoadType`) and is not a valid cargo type for a
 * ship-group load command — the FBS enum deliberately omits it.
 */
export type ShipGroupCargo = "COL" | "CAP" | "MAT";

/**
 * SHIP_GROUP_CARGO_VALUES is the canonical tuple of `ShipGroupCargo`
 * literals. Used by validators and the FBS converters in
 * `submit.ts` and `order-load.ts` to narrow incoming strings.
 */
export const SHIP_GROUP_CARGO_VALUES = [
	"COL",
	"CAP",
	"MAT",
] as const satisfies readonly ShipGroupCargo[];

/**
 * isShipGroupCargo narrows an arbitrary string to the
 * `ShipGroupCargo` union.
 */
export function isShipGroupCargo(value: string): value is ShipGroupCargo {
	return (SHIP_GROUP_CARGO_VALUES as readonly string[]).includes(value);
}

/**
 * ShipGroupUpgradeTech mirrors the engine `ShipGroupUpgradeTech`
 * enum (`pkg/schema/fbs/order.fbs`): `ALL` upgrades every applicable
 * block to the player's current race tech (level argument must be 0
 * — see `controller/ship_group_upgrade.go:56`); the four per-block
 * values upgrade exactly that block to the requested level.
 */
export type ShipGroupUpgradeTech =
	| "ALL"
	| "DRIVE"
	| "WEAPONS"
	| "SHIELDS"
	| "CARGO";

/**
 * SHIP_GROUP_UPGRADE_TECH_VALUES is the canonical tuple of
 * `ShipGroupUpgradeTech` literals. The order matches the FBS enum.
 */
export const SHIP_GROUP_UPGRADE_TECH_VALUES = [
	"ALL",
	"DRIVE",
	"WEAPONS",
	"SHIELDS",
	"CARGO",
] as const satisfies readonly ShipGroupUpgradeTech[];

/**
 * isShipGroupUpgradeTech narrows an arbitrary string to the
 * `ShipGroupUpgradeTech` union.
 */
export function isShipGroupUpgradeTech(
	value: string,
): value is ShipGroupUpgradeTech {
	return (SHIP_GROUP_UPGRADE_TECH_VALUES as readonly string[]).includes(value);
}

/**
 * BreakShipGroupCommand splits a player-owned ship group into two:
 * the original keeps `originalCount - quantity` ships and a new group
 * with `newGroupId` carries `quantity`. Used both as a stand-alone
 * action and as the implicit prelude to Send / Load / Unload /
 * Modernize / Dismantle / Transfer when the player picks fewer than
 * all ships. Engine rules (`controller/ship_group.go.breakGroup`):
 * source group must be `StateInOrbit`, `quantity` must be in `[1,
 * originalCount - 1]` for a real split. The new group carries a
 * proportional slice of the cargo and starts unattached to any fleet.
 */
export interface BreakShipGroupCommand {
	readonly kind: "breakShipGroup";
	readonly id: string;
	readonly groupId: string;
	readonly newGroupId: string;
	readonly quantity: number;
}

/**
 * SendShipGroupCommand launches a player-owned ship group toward a
 * destination planet. Engine rules
 * (`controller/ship_group_send.go.shipGroupSend`): group must be
 * `StateInOrbit`; ship class must have a non-zero drive block; the
 * destination must be within the player's current
 * `FlightDistance() = localPlayerDrive * 40` (torus-aware).
 * The picker filters the planet list before emitting, so a draft
 * entry that survives validation is always reachable at submit time.
 */
export interface SendShipGroupCommand {
	readonly kind: "sendShipGroup";
	readonly id: string;
	readonly groupId: string;
	readonly destinationPlanetNumber: number;
}

/**
 * LoadShipGroupCommand loads cargo of one of the three ship-group
 * cargo types onto a player-owned group. Engine rules
 * (`controller/ship_group.go.shipGroupLoad`): group must be
 * `StateInOrbit`; planet must be owned by the player or unowned;
 * ship class must have a non-zero cargo block; the existing cargo
 * type (if any) must equal `cargo`; `quantity` is bounded by the
 * planet's stock and the group's free capacity. The inspector
 * pre-checks each of these so a draft entry is always wire-valid.
 */
export interface LoadShipGroupCommand {
	readonly kind: "loadShipGroup";
	readonly id: string;
	readonly groupId: string;
	readonly cargo: ShipGroupCargo;
	readonly quantity: number;
}

/**
 * UnloadShipGroupCommand drops cargo from a player-owned group at
 * its current orbit. Engine rules
 * (`controller/ship_group.go.shipGroupUnload`): group must be
 * `StateInOrbit`; ship class must have a non-zero cargo block; group
 * must currently carry cargo. Colonists (`COL`) cannot be unloaded
 * over a foreign planet — the inspector disables the action with a
 * tooltip in that case. The cargo type is implicit (whatever the
 * group is carrying); only `quantity` is sent on the wire.
 */
export interface UnloadShipGroupCommand {
	readonly kind: "unloadShipGroup";
	readonly id: string;
	readonly groupId: string;
	readonly quantity: number;
}

/**
 * UpgradeShipGroupCommand schedules a tech upgrade for a player-
 * owned group at its current orbit. Engine rules
 * (`controller/ship_group_upgrade.go.shipGroupUpgrade`): group must
 * be `StateInOrbit`; the planet must be owned by the player or
 * unowned; for per-block techs the requested `level` must be in
 * `(group.tech, race.tech]`; for `tech === "ALL"` the `level` must
 * be 0 (the engine fans the upgrade out to every block whose mass is
 * non-zero). The inspector renders a live cost preview through
 * `core.blockUpgradeCost` to make the production cost visible before
 * the player commits.
 */
export interface UpgradeShipGroupCommand {
	readonly kind: "upgradeShipGroup";
	readonly id: string;
	readonly groupId: string;
	readonly tech: ShipGroupUpgradeTech;
	readonly level: number;
}

/**
 * DismantleShipGroupCommand deconstructs a player-owned group at its
 * current orbit, returning the empty mass to the planet's materials
 * stockpile. Engine rules (`controller/ship_group.go.shipGroupDismantle`):
 * group must be `StateInOrbit`; over a foreign planet, colonists
 * (`COL`) on board are *lost* — the inspector surfaces an explicit
 * two-step confirmation in that case before adding the command to
 * the draft.
 */
export interface DismantleShipGroupCommand {
	readonly kind: "dismantleShipGroup";
	readonly id: string;
	readonly groupId: string;
}

/**
 * TransferShipGroupCommand hands a player-owned group to another
 * race. Engine rules (`controller/ship_group.go.shipGroupTransfer`):
 * acceptor must be a different, non-extinct race; group must not
 * already be in `StateTransfer`. The inspector restricts the
 * acceptor picker to `GameReport.otherRaces` (non-extinct ≠ self),
 * so a draft entry always names a real race.
 */
export interface TransferShipGroupCommand {
	readonly kind: "transferShipGroup";
	readonly id: string;
	readonly groupId: string;
	readonly acceptor: string;
}

/**
 * JoinFleetShipGroupCommand attaches a player-owned group to a fleet
 * (creating it on the fly if no fleet by that name exists). Engine
 * rules (`controller/fleet.go.ShipGroupJoinFleet`): group must be
 * `StateInOrbit`; the target fleet, when it already exists, must
 * sit in the same orbit as the group; `name` must pass
 * `validateEntityName`. Because the engine handles the whole-group
 * attach atomically (no per-ship counter), this command does not
 * support implicit-split — the inspector exposes Split as a
 * separate explicit action when partial detachment is desired.
 */
export interface JoinFleetShipGroupCommand {
	readonly kind: "joinFleetShipGroup";
	readonly id: string;
	readonly groupId: string;
	readonly name: string;
}

/**
 * Relation mirrors the engine `Relation` enum
 * (`pkg/schema/fbs/order.fbs`). Two wire-stable values: `WAR` (the
 * local player declares hostilities toward the named race) and
 * `PEACE` (the local player declares peaceful relations). The engine
 * stores relations per-actor and asymmetrically — race A can be at
 * war with race B while race B is at peace with race A
 * (`game/internal/controller/race.go.UpdateRelation`). The FBS
 * `UNKNOWN = 0` sentinel is never emitted by the client.
 */
export type Relation = "WAR" | "PEACE";

/**
 * RELATION_VALUES is the canonical tuple of `Relation` literals.
 * Used by validators and by the FBS converters in `submit.ts` and
 * `order-load.ts` to narrow incoming strings.
 */
export const RELATION_VALUES = [
	"WAR",
	"PEACE",
] as const satisfies readonly Relation[];

/**
 * isRelation narrows an arbitrary string to the `Relation` union.
 * The decoder uses this when reading back a server-stored command
 * whose `relation` arrived as a generic string.
 */
export function isRelation(value: string): value is Relation {
	return (RELATION_VALUES as readonly string[]).includes(value);
}

/**
 * SetDiplomaticStanceCommand declares the local player's relation
 * (war or peace) toward another race. Mirrors the engine
 * `CommandRaceRelation` (`pkg/schema/fbs/order.fbs`,
 * `game/internal/controller/command.go.RaceRelation`). The relation
 * is unilateral; the targeted race keeps its own opinion of us.
 *
 * Phase 22 carries a collapse-by-`acceptor` rule: a newer entry
 * supersedes any prior `setDiplomaticStance` for the same opponent,
 * so the draft holds at most one stance intent per other race.
 */
export interface SetDiplomaticStanceCommand {
	readonly kind: "setDiplomaticStance";
	readonly id: string;
	readonly acceptor: string;
	readonly relation: Relation;
}

/**
 * SetVoteRecipientCommand binds the local player's single vote slot
 * to a race. Mirrors the engine `CommandRaceVote`
 * (`pkg/schema/fbs/order.fbs`,
 * `game/internal/controller/command.go.RaceVote`). The engine
 * tallies votes at turn cutoff (`rules.txt` "Процесс голосования");
 * between turns the player can change their pick freely. The
 * acceptor may be the local race itself — the engine treats
 * self-vote as the neutral default and re-applies it whenever a
 * voted-for race goes extinct (`controller/race.go`).
 *
 * Phase 22 carries a singleton collapse rule: a newer entry replaces
 * any prior `setVoteRecipient`, regardless of target — the player
 * has only one outgoing vote slot.
 */
export interface SetVoteRecipientCommand {
	readonly kind: "setVoteRecipient";
	readonly id: string;
	readonly acceptor: string;
}

/**
 * OrderCommand is the discriminated union of every command shape the
 * local order draft can hold. The `kind` field is the discriminator;
 * narrowing on it enables exhaustive `switch` statements at every
 * call site.
 */
export type OrderCommand =
	| PlaceholderCommand
	| PlanetRenameCommand
	| SetProductionTypeCommand
	| SetCargoRouteCommand
	| RemoveCargoRouteCommand
	| CreateShipClassCommand
	| RemoveShipClassCommand
	| CreateScienceCommand
	| RemoveScienceCommand
	| BreakShipGroupCommand
	| SendShipGroupCommand
	| LoadShipGroupCommand
	| UnloadShipGroupCommand
	| UpgradeShipGroupCommand
	| DismantleShipGroupCommand
	| TransferShipGroupCommand
	| JoinFleetShipGroupCommand
	| SetDiplomaticStanceCommand
	| SetVoteRecipientCommand;

/**
 * PRODUCTION_TYPE_VALUES is the canonical tuple of `ProductionType`
 * literals. Used by validators and by the FBS converters in
 * `submit.ts` and `order-load.ts` to assert that an incoming string
 * is one of the wire-stable values.
 */
export const PRODUCTION_TYPE_VALUES = [
	"MAT",
	"CAP",
	"DRIVE",
	"WEAPONS",
	"SHIELDS",
	"CARGO",
	"SCIENCE",
	"SHIP",
] as const satisfies readonly ProductionType[];

/**
 * isProductionType narrows an arbitrary string to the
 * `ProductionType` union.
 */
export function isProductionType(value: string): value is ProductionType {
	return (PRODUCTION_TYPE_VALUES as readonly string[]).includes(value);
}

/**
 * CARGO_LOAD_TYPE_VALUES is the canonical tuple of `CargoLoadType`
 * literals in turn-cutoff priority order
 * (`game/internal/controller/route.go.SendRoutedGroups`):
 * colonists first, then capital, then materials, then empty ships.
 * The inspector renders slots in this order so visual order
 * matches engine behaviour. Used by validators and by the FBS
 * converters in `submit.ts` and `order-load.ts`.
 */
export const CARGO_LOAD_TYPE_VALUES = [
	"COL",
	"CAP",
	"MAT",
	"EMP",
] as const satisfies readonly CargoLoadType[];

/**
 * isCargoLoadType narrows an arbitrary string to the
 * `CargoLoadType` union. The decoder uses this when the engine
 * report's `RouteEntry.value` carries the load-type string.
 */
export function isCargoLoadType(value: string): value is CargoLoadType {
	return (CARGO_LOAD_TYPE_VALUES as readonly string[]).includes(value);
}

/**
 * CommandStatus is the lifecycle of a single command from the moment
 * it lands in the draft to the moment the server resolves it. Phase
 * 14 adds the `valid` / `invalid` transitions driven by local
 * validation and the `submitting` / `applied` / `rejected` triplet
 * driven by the submit pipeline; Phase 25 adds `conflict` for
 * commands whose submit landed after the turn cutoff
 * (`turn_already_closed` from the gateway).
 *
 * The state machine is:
 *
 *	draft → valid     → submitting → applied
 *	      ↘ invalid                ↘ rejected
 *	                              ↘ conflict
 *
 * A command is `draft` until local validation has run, then `valid`
 * or `invalid`. On submit the entry transitions to `submitting`,
 * then to `applied` / `rejected` / `conflict` once the gateway
 * responds. A `conflict` row stays in the draft until the next
 * `game.turn.ready` triggers a `resetForNewTurn`, or the user edits
 * the draft (any mutation re-validates the conflict back to `valid`
 * or `invalid`).
 */
export type CommandStatus =
	| "draft"
	| "valid"
	| "invalid"
	| "submitting"
	| "applied"
	| "rejected"
	| "conflict";
