// Timeline builder for the BattleViewer.
//
// Given a `BattleReport`, expands the flat `protocol` into a
// sequence of frames. Frame 0 carries the initial state; frame N
// (1 ≤ N ≤ protocol.length) reflects the state right after the
// (N-1)-th action has been applied. Each frame is self-contained so
// stepping forward and backward is a constant-time index lookup, no
// rewind logic needed.

import type {
	BattleActionReport,
	BattleReport,
	BattleReportGroup,
} from "../../api/battle-fetch";

/**
 * Frame is one tick of the battle playback. `remaining` carries the
 * surviving ship count for each ship-group key from
 * `BattleReport.ships`; `activeRaceIds` are the race indices with at
 * least one surviving in-battle group. `lastAction` is the action
 * applied to produce this frame, or `null` for the initial frame.
 */
export interface Frame {
	shotIndex: number;
	remaining: Map<number, number>;
	activeRaceIds: number[];
	lastAction: BattleActionReport | null;
}

export interface NormalisedGroup {
	key: number;
	group: BattleReportGroup;
	raceId: number;
}

/**
 * normaliseGroups returns the in-battle ship groups from a
 * BattleReport indexed by their integer key. Observer groups
 * (`inBattle === false`) are skipped because they are neither
 * targeted nor drawn. The race index per group is derived from the
 * protocol — every in-battle group appears at least once as
 * attacker or defender, and the engine's pairing (a, sa) / (d, sd)
 * defines the relationship.
 */
export function normaliseGroups(report: BattleReport): NormalisedGroup[] {
	const raceByKey = buildGroupRaceMap(report.protocol);
	const out: NormalisedGroup[] = [];
	for (const [keyRaw, group] of Object.entries(report.ships)) {
		if (!group.inBattle) continue;
		const key = Number(keyRaw);
		if (!Number.isFinite(key)) continue;
		const raceId = raceByKey.get(key);
		if (raceId === undefined) continue;
		out.push({ key, group, raceId });
	}
	return out;
}

/**
 * buildGroupRaceMap extracts the `ship-group key → race index`
 * mapping from a battle protocol. Same key appearing twice always
 * carries the same race index — protocol entries are emitted by the
 * engine, which never crosses these wires.
 */
export function buildGroupRaceMap(
	protocol: BattleActionReport[],
): Map<number, number> {
	const out = new Map<number, number>();
	for (const action of protocol) {
		if (!out.has(action.sa)) out.set(action.sa, action.a);
		if (!out.has(action.sd)) out.set(action.sd, action.d);
	}
	return out;
}

/**
 * buildFrames walks the protocol once and emits a frame after each
 * applied action plus the initial frame. The remaining-ships map is
 * cloned per frame so callers can step backward without manual
 * bookkeeping. Eliminated races drop out of `activeRaceIds` as soon
 * as their last in-battle group hits zero.
 */
export function buildFrames(report: BattleReport): Frame[] {
	const groups = normaliseGroups(report);
	const initialRemaining = new Map<number, number>();
	const raceTotals = new Map<number, number>();
	for (const g of groups) {
		initialRemaining.set(g.key, g.group.num);
		raceTotals.set(g.raceId, (raceTotals.get(g.raceId) ?? 0) + g.group.num);
	}

	const frames: Frame[] = [];
	frames.push({
		shotIndex: 0,
		remaining: new Map(initialRemaining),
		activeRaceIds: collectActiveRaces(raceTotals),
		lastAction: null,
	});

	const groupRaceByKey = new Map<number, number>();
	for (const g of groups) groupRaceByKey.set(g.key, g.raceId);

	const current = new Map(initialRemaining);
	const runningRaceTotals = new Map(raceTotals);
	for (let i = 0; i < report.protocol.length; i++) {
		const action = report.protocol[i];
		if (action.x) {
			// Defence in depth: a malformed protocol that fires more
			// `Destroyed` rows than the group has ships would push
			// `runningRaceTotals` below zero and drop the race from
			// `activeRaceIds` prematurely. Real legacy data folds
			// duplicate `(race, className)` roster rows into the
			// same `BattleReportGroup` (parser + engine), so this
			// branch is hit only on a real shrink — but the clamp
			// keeps the math from going negative either way.
			const left = current.get(action.sd) ?? 0;
			if (left > 0) {
				current.set(action.sd, left - 1);
				const raceId = groupRaceByKey.get(action.sd);
				if (raceId !== undefined) {
					const t = (runningRaceTotals.get(raceId) ?? 0) - 1;
					runningRaceTotals.set(raceId, Math.max(0, t));
				}
			}
		}
		frames.push({
			shotIndex: i + 1,
			remaining: new Map(current),
			activeRaceIds: collectActiveRaces(runningRaceTotals),
			lastAction: action,
		});
	}

	return frames;
}

function collectActiveRaces(totals: Map<number, number>): number[] {
	const out: number[] = [];
	for (const [raceId, total] of totals.entries()) {
		if (total > 0) out.push(raceId);
	}
	return out.sort((a, b) => a - b);
}
