// DEV-only synthetic-report loader. Backs the "Load synthetic report"
// affordance on the lobby (visible behind `import.meta.env.DEV`) and
// the in-game shell layout's bypass for the synthetic game id range.
//
// The accepted JSON shape mirrors `pkg/model/report.Report` as
// emitted by `tools/local-dev/legacy-report/cmd/legacy-report-to-json`.
// Whenever the UI's `decodeReport` (`api/game-state.ts`) is extended
// to read a new field, this decoder must be extended in lock-step
// AND the Go CLI must learn to populate that field — see the
// synthetic-report parity rule in `ui/PLAN.md`.
//
// The in-memory map deliberately does not survive a page reload:
// synthetic mode is a debug affordance, not a session, and the
// layout redirects to /lobby when a synthetic id is opened with no
// matching entry.
//
// Routes are always emitted empty: the legacy text report has no
// dedicated cargo-routes section, and `applyOrderOverlay` already
// handles an empty `routes` array.

import type {
	GameReport,
	ReportBombing,
	ReportIncomingShipGroup,
	ReportLocalFleet,
	ReportLocalShipGroup,
	ReportOtherRace,
	ReportOtherScience,
	ReportOtherShipClass,
	ReportOtherShipGroup,
	ReportPlanet,
	ReportPlayer,
	ReportRoute,
	ReportShipProduction,
	ReportUnidentifiedShipGroup,
	ScienceSummary,
	ShipClassSummary,
	ShipGroupTech,
} from "./game-state";
import type { CargoLoadType, Relation } from "../sync/order-types";
import { isCargoLoadType, isRelation } from "../sync/order-types";
import type { BattleReport } from "./battle-fetch";
import { registerSyntheticBattle } from "./synthetic-battle";

export const SYNTHETIC_GAME_ID_PREFIX = "synthetic-";

const SYNTHETIC_REPORTS = new Map<string, GameReport>();

export function isSyntheticGameId(gameId: string): boolean {
	return gameId.startsWith(SYNTHETIC_GAME_ID_PREFIX);
}

export class SyntheticReportError extends Error {
	constructor(message: string) {
		super(message);
		this.name = "SyntheticReportError";
	}
}

/**
 * loadSyntheticReportFromJSON validates the passed payload, decodes
 * it into a `GameReport`, registers it in the in-memory map under a
 * fresh `synthetic-<uuid>` id, and returns both the id and the
 * decoded report.
 *
 * Accepts two on-disk shapes:
 *
 *   1. Envelope (Phase 27 legacy-report CLI):
 *      `{ "version": 1, "report": <Report>, "battles": { <uuid>: <BattleReport> } }`
 *      — battles are forwarded to `registerSyntheticBattle` so the
 *      Battle Viewer can resolve them offline.
 *   2. Bare Report (pre-envelope synthetic JSON files) — same as
 *      before; battle UUIDs in the report can still be clicked, but
 *      the Viewer page will show "battle not found" because no
 *      fixture was registered.
 *
 * Throws `SyntheticReportError` for malformed input in either shape.
 */
export function loadSyntheticReportFromJSON(json: unknown): {
	gameId: string;
	report: GameReport;
} {
	const { reportPayload, battles } = extractEnvelope(json);
	const report = decodeSyntheticReport(reportPayload);
	for (const battle of battles) {
		registerSyntheticBattle(battle);
	}
	const gameId = SYNTHETIC_GAME_ID_PREFIX + crypto.randomUUID();
	SYNTHETIC_REPORTS.set(gameId, report);
	return { gameId, report };
}

interface SyntheticEnvelope {
	version?: number;
	report?: unknown;
	battles?: Record<string, BattleReport>;
}

/**
 * extractEnvelope distinguishes the v1 envelope shape from a bare
 * Report payload. The envelope check is `version === 1` to leave room
 * for future format bumps and to avoid mistaking a bare Report whose
 * top-level fields happen to include `report`/`battles` (none do
 * today) for an envelope.
 */
function extractEnvelope(json: unknown): {
	reportPayload: unknown;
	battles: BattleReport[];
} {
	if (typeof json !== "object" || json === null) {
		// Defer the error to `decodeSyntheticReport`; it already
		// raises a `SyntheticReportError` with the right message.
		return { reportPayload: json, battles: [] };
	}
	const env = json as SyntheticEnvelope;
	if (env.version === 1 && env.report !== undefined) {
		const battlesMap = env.battles ?? {};
		const battles: BattleReport[] = [];
		for (const value of Object.values(battlesMap)) {
			if (value && typeof value === "object") {
				battles.push(value);
			}
		}
		return { reportPayload: env.report, battles };
	}
	return { reportPayload: json, battles: [] };
}

/** getSyntheticReport returns the report registered under `gameId`,
 * or `undefined` if the entry was lost (e.g. page reload). */
export function getSyntheticReport(gameId: string): GameReport | undefined {
	return SYNTHETIC_REPORTS.get(gameId);
}

interface SyntheticPlanet {
	number: number;
	name?: string;
	x: number;
	y: number;
	size?: number;
	resources?: number;
	capital?: number;
	material?: number;
	industry?: number;
	population?: number;
	colonists?: number;
	production?: string;
	freeIndustry?: number;
	owner?: string;
}

interface SyntheticShipClass {
	name: string;
	drive: number;
	armament: number;
	weapons: number;
	shields: number;
	cargo: number;
}

interface SyntheticPlayer {
	name: string;
	drive: number;
	weapons: number;
	shields: number;
	cargo: number;
	population?: number;
	industry?: number;
	planets?: number;
	relation?: string;
	votes?: number;
	extinct?: boolean;
}

interface SyntheticShipGroup {
	id?: string;
	number?: number;
	class?: string;
	tech?: Record<string, number>;
	cargo?: string;
	load?: number;
	destination?: number;
	origin?: number;
	range?: number;
	speed?: number;
	mass?: number;
	state?: string;
	fleet?: string;
}

interface SyntheticIncomingGroup {
	origin?: number;
	destination?: number;
	distance?: number;
	speed?: number;
	mass?: number;
}

interface SyntheticUnidentifiedGroup {
	x?: number;
	y?: number;
}

interface SyntheticLocalFleet {
	name?: string;
	groups?: number;
	destination?: number;
	origin?: number;
	range?: number;
	speed?: number;
	state?: string;
}

interface SyntheticScience {
	name?: string;
	drive?: number;
	weapons?: number;
	shields?: number;
	cargo?: number;
}

interface SyntheticOtherScience extends SyntheticScience {
	race?: string;
}

interface SyntheticOtherShipClass extends SyntheticShipClass {
	race?: string;
	mass?: number;
}

interface SyntheticBattle {
	id?: string;
	planet?: number;
	shots?: number;
}

interface SyntheticBombing {
	planet?: number; // wire field "number"
	planetName?: string; // wire field "planetName"
	owner?: string;
	attacker?: string;
	production?: string;
	industry?: number;
	population?: number;
	colonists?: number;
	capital?: number;
	material?: number;
	attack?: number;
	wiped?: boolean;
}

interface SyntheticShipProductionRow {
	planet?: number;
	class?: string;
	cost?: number;
	prodUsed?: number;
	percent?: number;
	free?: number;
}

interface SyntheticReportRoot {
	turn?: number;
	mapWidth?: number;
	mapHeight?: number;
	mapPlanets?: number;
	race?: string;
	votes?: number;
	voteFor?: string;
	player?: SyntheticPlayer[];
	localPlanet?: SyntheticPlanet[];
	otherPlanet?: SyntheticPlanet[];
	uninhabitedPlanet?: SyntheticPlanet[];
	unidentifiedPlanet?: SyntheticPlanet[];
	localShipClass?: SyntheticShipClass[];
	otherShipClass?: SyntheticOtherShipClass[];
	localScience?: SyntheticScience[];
	otherScience?: SyntheticOtherScience[];
	localGroup?: SyntheticShipGroup[];
	otherGroup?: SyntheticShipGroup[];
	incomingGroup?: SyntheticIncomingGroup[];
	unidentifiedGroup?: SyntheticUnidentifiedGroup[];
	localFleet?: SyntheticLocalFleet[];
	battle?: SyntheticBattle[];
	bombing?: SyntheticBombing[];
	shipProduction?: SyntheticShipProductionRow[];
}

function decodeSyntheticReport(json: unknown): GameReport {
	if (typeof json !== "object" || json === null) {
		throw new SyntheticReportError("synthetic report must be a JSON object");
	}
	const root = json as SyntheticReportRoot;

	const planets: ReportPlanet[] = [];
	for (const p of root.localPlanet ?? []) {
		planets.push(toPlanet(p, "local", null));
	}
	for (const p of root.otherPlanet ?? []) {
		planets.push(toPlanet(p, "other", p.owner ?? null));
	}
	for (const p of root.uninhabitedPlanet ?? []) {
		planets.push(toPlanet(p, "uninhabited", null));
	}
	for (const p of root.unidentifiedPlanet ?? []) {
		planets.push(toPlanet(p, "unidentified", null));
	}

	const localShipClass: ShipClassSummary[] = (root.localShipClass ?? []).map(
		(sc) => ({
			name: sc.name,
			drive: numOr0(sc.drive),
			armament: Math.trunc(numOr0(sc.armament)),
			weapons: numOr0(sc.weapons),
			shields: numOr0(sc.shields),
			cargo: numOr0(sc.cargo),
		}),
	);

	const localScience: ScienceSummary[] = (root.localScience ?? []).map((sc) => ({
		name: typeof sc.name === "string" ? sc.name : "",
		drive: numOr0(sc.drive),
		weapons: numOr0(sc.weapons),
		shields: numOr0(sc.shields),
		cargo: numOr0(sc.cargo),
	}));

	const race = typeof root.race === "string" ? root.race : "";
	const tech = findLocalPlayerTech(root.player ?? [], race);

	const routes: ReportRoute[] = [];

	const localShipGroups: ReportLocalShipGroup[] = (root.localGroup ?? []).map(
		(g, i) => ({
			id: typeof g.id === "string" ? g.id : `synthetic-local-group-${i}`,
			count: numOr0(g.number),
			class: typeof g.class === "string" ? g.class : "",
			tech: toShipGroupTech(g.tech),
			cargo: toCargoType(g.cargo),
			load: numOr0(g.load),
			destination: numOr0(g.destination),
			origin: typeof g.origin === "number" ? g.origin : null,
			range: typeof g.range === "number" ? g.range : null,
			speed: numOr0(g.speed),
			mass: numOr0(g.mass),
			state: typeof g.state === "string" ? g.state : "",
			fleet: typeof g.fleet === "string" ? g.fleet : null,
		}),
	);
	const otherShipGroups: ReportOtherShipGroup[] = (root.otherGroup ?? []).map(
		(g) => ({
			count: numOr0(g.number),
			class: typeof g.class === "string" ? g.class : "",
			tech: toShipGroupTech(g.tech),
			cargo: toCargoType(g.cargo),
			load: numOr0(g.load),
			destination: numOr0(g.destination),
			origin: typeof g.origin === "number" ? g.origin : null,
			range: typeof g.range === "number" ? g.range : null,
			speed: numOr0(g.speed),
			mass: numOr0(g.mass),
		}),
	);
	const incomingShipGroups: ReportIncomingShipGroup[] = (
		root.incomingGroup ?? []
	).map((g) => ({
		origin: numOr0(g.origin),
		destination: numOr0(g.destination),
		distance: numOr0(g.distance),
		speed: numOr0(g.speed),
		mass: numOr0(g.mass),
	}));
	const unidentifiedShipGroups: ReportUnidentifiedShipGroup[] = (
		root.unidentifiedGroup ?? []
	).map((g) => ({ x: numOr0(g.x), y: numOr0(g.y) }));
	const localFleets: ReportLocalFleet[] = (root.localFleet ?? []).map((f) => ({
		name: typeof f.name === "string" ? f.name : "",
		groupCount: numOr0(f.groups),
		destination: numOr0(f.destination),
		origin: typeof f.origin === "number" ? f.origin : null,
		range: typeof f.range === "number" ? f.range : null,
		speed: numOr0(f.speed),
		state: typeof f.state === "string" ? f.state : "",
	}));

	const otherScience: ReportOtherScience[] = (root.otherScience ?? []).map(
		(sc) => ({
			race: typeof sc.race === "string" ? sc.race : "",
			name: typeof sc.name === "string" ? sc.name : "",
			drive: numOr0(sc.drive),
			weapons: numOr0(sc.weapons),
			shields: numOr0(sc.shields),
			cargo: numOr0(sc.cargo),
		}),
	);
	otherScience.sort((a, b) => {
		const byRace = a.race.localeCompare(b.race);
		if (byRace !== 0) return byRace;
		return a.name.localeCompare(b.name);
	});

	const otherShipClass: ReportOtherShipClass[] = (root.otherShipClass ?? []).map(
		(sc) => ({
			race: typeof sc.race === "string" ? sc.race : "",
			name: typeof sc.name === "string" ? sc.name : "",
			drive: numOr0(sc.drive),
			armament: Math.trunc(numOr0(sc.armament)),
			weapons: numOr0(sc.weapons),
			shields: numOr0(sc.shields),
			cargo: numOr0(sc.cargo),
			// `mass` is on the wire but synthetic fixtures may omit
			// it; fall back to 0 rather than reject the row.
			mass: typeof sc.mass === "number" ? sc.mass : 0,
		}),
	);
	otherShipClass.sort((a, b) => {
		const byRace = a.race.localeCompare(b.race);
		if (byRace !== 0) return byRace;
		return a.name.localeCompare(b.name);
	});

	const battles = (root.battle ?? [])
		.filter(
			(v): v is SyntheticBattle =>
				typeof v === "object" && v !== null && typeof v.id === "string" && v.id !== "",
		)
		.map((b) => ({
			id: b.id as string,
			planet: numOr0(b.planet),
			shots: numOr0(b.shots),
		}));
	const battleIds = battles.map((b) => b.id);

	const bombings: ReportBombing[] = (root.bombing ?? []).map((b) => ({
		planetNumber: numOr0(b.planet),
		planet: typeof b.planetName === "string" ? b.planetName : "",
		owner: typeof b.owner === "string" ? b.owner : "",
		attacker: typeof b.attacker === "string" ? b.attacker : "",
		production: typeof b.production === "string" ? b.production : "",
		industry: numOr0(b.industry),
		population: numOr0(b.population),
		colonists: numOr0(b.colonists),
		industryStockpile: numOr0(b.capital),
		materialsStockpile: numOr0(b.material),
		attackPower: numOr0(b.attack),
		wiped: b.wiped === true,
	}));
	bombings.sort((a, b) => a.planetNumber - b.planetNumber);

	const shipProductions: ReportShipProduction[] = (root.shipProduction ?? []).map(
		(sp) => ({
			planetNumber: numOr0(sp.planet),
			class: typeof sp.class === "string" ? sp.class : "",
			cost: numOr0(sp.cost),
			prodUsed: numOr0(sp.prodUsed),
			percent: numOr0(sp.percent),
			freeIndustry: numOr0(sp.free),
		}),
	);
	shipProductions.sort((a, b) => {
		const byPlanet = a.planetNumber - b.planetNumber;
		if (byPlanet !== 0) return byPlanet;
		return a.class.localeCompare(b.class);
	});

	return {
		turn: numOr0(root.turn),
		mapWidth: numOr0(root.mapWidth),
		mapHeight: numOr0(root.mapHeight),
		planetCount: numOr0(root.mapPlanets),
		planets,
		race,
		localShipClass,
		localScience,
		routes,
		localPlayerDrive: tech.drive,
		localPlayerWeapons: tech.weapons,
		localPlayerShields: tech.shields,
		localPlayerCargo: tech.cargo,
		localShipGroups,
		otherShipGroups,
		incomingShipGroups,
		unidentifiedShipGroups,
		localFleets,
		otherRaces: collectOtherRacesFromSynthetic(root, race),
		races: collectOtherRaceRowsFromSynthetic(root, race),
		myVotes: numOr0(root.votes),
		myVoteFor: typeof root.voteFor === "string" ? root.voteFor : "",
		players: collectPlayersFromSynthetic(root, race),
		otherScience,
		otherShipClass,
		battles,
		battleIds,
		bombings,
		shipProductions,
	};
}

function collectPlayersFromSynthetic(
	root: SyntheticReportRoot,
	raceName: string,
): ReportPlayer[] {
	const out: ReportPlayer[] = [];
	for (const player of root.player ?? []) {
		const name = typeof player.name === "string" ? player.name : "";
		if (name === "") continue;
		out.push({
			name,
			drive: numOr0(player.drive),
			weapons: numOr0(player.weapons),
			shields: numOr0(player.shields),
			cargo: numOr0(player.cargo),
			population: numOr0(player.population),
			industry: numOr0(player.industry),
			planets: Math.trunc(numOr0(player.planets)),
			votesReceived: numOr0(player.votes),
			extinct: player.extinct === true,
			isLocal: name === raceName,
		});
	}
	out.sort((a, b) =>
		a.name.toLowerCase().localeCompare(b.name.toLowerCase()),
	);
	return out;
}

function collectOtherRacesFromSynthetic(
	root: SyntheticReportRoot,
	raceName: string,
): string[] {
	const out: string[] = [];
	for (const player of root.player ?? []) {
		if (player.extinct === true) continue;
		const name = typeof player.name === "string" ? player.name : "";
		if (name === "" || name === raceName) continue;
		out.push(name);
	}
	out.sort((a, b) => a.localeCompare(b));
	return out;
}

function collectOtherRaceRowsFromSynthetic(
	root: SyntheticReportRoot,
	raceName: string,
): ReportOtherRace[] {
	const out: ReportOtherRace[] = [];
	for (const player of root.player ?? []) {
		if (player.extinct === true) continue;
		const name = typeof player.name === "string" ? player.name : "";
		if (name === "" || name === raceName) continue;
		const wire = typeof player.relation === "string" ? player.relation : "";
		const relation: Relation = isRelation(wire) ? wire : "PEACE";
		out.push({
			name,
			drive: numOr0(player.drive),
			weapons: numOr0(player.weapons),
			shields: numOr0(player.shields),
			cargo: numOr0(player.cargo),
			population: numOr0(player.population),
			industry: numOr0(player.industry),
			planets: Math.trunc(numOr0(player.planets)),
			relation,
			votesReceived: numOr0(player.votes),
		});
	}
	out.sort((a, b) => a.name.localeCompare(b.name));
	return out;
}

function toShipGroupTech(raw: Record<string, number> | undefined): ShipGroupTech {
	const out: ShipGroupTech = { drive: 0, weapons: 0, shields: 0, cargo: 0 };
	if (raw === undefined || raw === null) return out;
	if (typeof raw.drive === "number") out.drive = raw.drive;
	if (typeof raw.weapons === "number") out.weapons = raw.weapons;
	if (typeof raw.shields === "number") out.shields = raw.shields;
	if (typeof raw.cargo === "number") out.cargo = raw.cargo;
	return out;
}

function toCargoType(raw: string | undefined): CargoLoadType | "NONE" {
	if (raw === undefined || raw === "" || raw === "-") return "NONE";
	if (isCargoLoadType(raw)) return raw;
	return "NONE";
}

function toPlanet(
	p: SyntheticPlanet,
	kind: ReportPlanet["kind"],
	owner: string | null,
): ReportPlanet {
	const has = (v: number | undefined): number | null =>
		typeof v === "number" ? v : null;
	if (kind === "unidentified") {
		return {
			number: numOr0(p.number),
			name: "",
			x: numOr0(p.x),
			y: numOr0(p.y),
			kind,
			owner,
			size: null,
			resources: null,
			industryStockpile: null,
			materialsStockpile: null,
			industry: null,
			population: null,
			colonists: null,
			production: null,
			freeIndustry: null,
		};
	}
	if (kind === "uninhabited") {
		return {
			number: numOr0(p.number),
			name: typeof p.name === "string" ? p.name : "",
			x: numOr0(p.x),
			y: numOr0(p.y),
			kind,
			owner,
			size: has(p.size),
			resources: has(p.resources),
			industryStockpile: has(p.capital),
			materialsStockpile: has(p.material),
			industry: null,
			population: null,
			colonists: null,
			production: null,
			freeIndustry: null,
		};
	}
	return {
		number: numOr0(p.number),
		name: typeof p.name === "string" ? p.name : "",
		x: numOr0(p.x),
		y: numOr0(p.y),
		kind,
		owner,
		size: has(p.size),
		resources: has(p.resources),
		industryStockpile: has(p.capital),
		materialsStockpile: has(p.material),
		industry: has(p.industry),
		population: has(p.population),
		colonists: has(p.colonists),
		production: typeof p.production === "string" ? p.production : null,
		freeIndustry: has(p.freeIndustry),
	};
}

function findLocalPlayerTech(
	players: SyntheticPlayer[],
	race: string,
): { drive: number; weapons: number; shields: number; cargo: number } {
	if (race === "") {
		return { drive: 0, weapons: 0, shields: 0, cargo: 0 };
	}
	const local = players.find((p) => p.name === race);
	if (local === undefined) {
		return { drive: 0, weapons: 0, shields: 0, cargo: 0 };
	}
	return {
		drive: numOr0(local.drive),
		weapons: numOr0(local.weapons),
		shields: numOr0(local.shields),
		cargo: numOr0(local.cargo),
	};
}

function numOr0(v: unknown): number {
	return typeof v === "number" && Number.isFinite(v) ? v : 0;
}
