// Pure orchestration for the ship-class calculator. The calculator
// renders three areas — ship design, derived results, planet build — and
// supports single-target "goal-seek": the player pins one derived result
// and the model back-solves the single input it claims. All numeric math
// lives in `pkg/calc` (reached through `Core`); this module only decides
// which `Core` call to make, in what order, and how to fold the result
// back into the field set. Keeping it a pure function of
// `(CalculatorInput, Core)` makes the goal-seek logic unit-testable
// without booting WASM or mounting a component.

import type { Core } from "../../platform/core/index";
import {
	validateShipClassValues,
	type ShipClassValueInvalidReason,
} from "../util/ship-class-validation";

/** LockableOutputId names every derived result the player may pin. */
export type LockableOutputId =
	| "emptyMass"
	| "loadedMass"
	| "speedEmpty"
	| "speedLoaded"
	| "attack"
	| "defense";

/** ClaimedInput names every input a locked result can back-solve. */
export type ClaimedInput = "drive" | "weapons" | "shields" | "cargo" | "load";

/**
 * CLAIM_MAP fixes which single input each lockable result back-solves.
 * The pairing is the natural lever for each result: attack rides on the
 * weapons block, defence on shields, both speeds on the drive block,
 * empty mass on the cargo block (the free filler), and loaded mass on the
 * cargo load.
 */
export const CLAIM_MAP: Record<LockableOutputId, ClaimedInput> = {
	emptyMass: "cargo",
	loadedMass: "load",
	speedEmpty: "drive",
	speedLoaded: "drive",
	attack: "weapons",
	defense: "shields",
};

export type LoadMode = "empty" | "full" | "custom";

export interface DesignBlocks {
	drive: number;
	armament: number;
	weapons: number;
	shields: number;
	cargo: number;
}

export interface CalculatorInput {
	blocks: DesignBlocks;
	// Effective tech levels (the caller resolves default vs. override).
	driveTech: number;
	weaponsTech: number;
	shieldsTech: number;
	cargoTech: number;
	loadMode: LoadMode;
	customLoad: number;
	// The single pinned result, or null when nothing is locked.
	lock: { output: LockableOutputId; value: number } | null;
}

export interface CalculatorOutputs {
	emptyMass: number;
	loadedMass: number;
	speedEmpty: number;
	speedLoaded: number;
	attack: number;
	defense: number;
	bombing: number;
}

export interface CalculatorResult {
	/** Blocks after goal-seek may have overwritten the claimed one. */
	blocks: DesignBlocks;
	/** Which input the active lock drove, or null. */
	computedInput: ClaimedInput | null;
	/** False when the lock's target cannot be reached. */
	lockFeasible: boolean;
	/** Whether the resolved blocks pass the engine value rules. */
	valuesValid: boolean;
	valueReason: ShipClassValueInvalidReason | null;
	/** Resolved cargo load in cargo units. */
	load: number;
	cargoCapacity: number;
	/** Derived results, or null when invalid / no Core. */
	outputs: CalculatorOutputs | null;
}

// isClaimedBlockValid checks that a solver result, before we apply it
// to the resolved blocks, satisfies the same per-field rules the live
// validator enforces on user-typed values (`pkg/calc/validator.go` /
// `lib/util/ship-class-validation`). The four claimable blocks all
// share the DWSC rule, so a single predicate suffices. Used to flag
// a goal-seek target as infeasible when the only block that would
// reach it falls in the (0, 1) gap.
function isClaimedBlockValid(solved: number): boolean {
	if (!Number.isFinite(solved)) return false;
	return solved === 0 || solved >= 1;
}

function resolveLoad(
	mode: LoadMode,
	customLoad: number,
	cargo: number,
	cargoTech: number,
	core: Core,
): number {
	if (mode === "empty") return 0;
	if (mode === "custom") return customLoad > 0 ? customLoad : 0;
	return core.cargoCapacity({ cargo, cargoTech });
}

// solveClaimedBlock back-solves the block claimed by a locked result
// (everything except a `load` claim, which is resolved with the cargo
// load). Returns null when the target is unreachable or the design's
// weapons/armament pairing is invalid.
function solveClaimedBlock(
	lock: { output: LockableOutputId; value: number },
	raw: DesignBlocks,
	input: CalculatorInput,
	prelimLoad: number,
	core: Core,
): number | null {
	switch (lock.output) {
		case "attack":
			return core.weaponsForAttack({
				targetAttack: lock.value,
				weaponsTech: input.weaponsTech,
			});
		case "defense": {
			const restExclShields = core.emptyMass({ ...raw, shields: 0 });
			if (restExclShields === null) return null;
			const carrying = core.carryingMass({
				load: prelimLoad,
				cargoTech: input.cargoTech,
			});
			return core.shieldsForDefence({
				targetDefence: lock.value,
				shieldsTech: input.shieldsTech,
				restMass: restExclShields + carrying,
			});
		}
		case "speedEmpty": {
			const restExclDrive = core.emptyMass({ ...raw, drive: 0 });
			if (restExclDrive === null) return null;
			return core.driveForSpeed({
				targetSpeed: lock.value,
				driveTech: input.driveTech,
				restMass: restExclDrive,
			});
		}
		case "speedLoaded": {
			const restExclDrive = core.emptyMass({ ...raw, drive: 0 });
			if (restExclDrive === null) return null;
			const carrying = core.carryingMass({
				load: prelimLoad,
				cargoTech: input.cargoTech,
			});
			return core.driveForSpeed({
				targetSpeed: lock.value,
				driveTech: input.driveTech,
				restMass: restExclDrive + carrying,
			});
		}
		case "emptyMass": {
			const restExclCargo = core.emptyMass({ ...raw, cargo: 0 });
			if (restExclCargo === null) return null;
			return core.cargoForEmptyMass({
				targetEmptyMass: lock.value,
				restMass: restExclCargo,
			});
		}
		case "loadedMass":
			// Claims the cargo load, resolved alongside the load below.
			return null;
	}
}

/**
 * computeCalculator resolves the full calculator state for one input
 * snapshot: it applies the active goal-seek lock (if any), resolves the
 * cargo load, validates the blocks, and computes every derived result.
 * `outputs` is null when no `Core` is available or the blocks are
 * invalid, mirroring the Phase 18 designer rule of hiding the preview
 * until the design is sound.
 */
export function computeCalculator(
	input: CalculatorInput,
	core: Core | null,
): CalculatorResult {
	const raw = input.blocks;
	if (core === null) {
		return {
			blocks: raw,
			computedInput: null,
			lockFeasible: true,
			valuesValid: false,
			valueReason: null,
			load: 0,
			cargoCapacity: 0,
			outputs: null,
		};
	}

	const blocks: DesignBlocks = { ...raw };
	let computedInput: ClaimedInput | null = null;
	let lockFeasible = true;

	// Preliminary load from the raw cargo, used by solvers that need the
	// carrying mass (speedLoaded, defence). It matches the final load for
	// every claim except `emptyMass` (which solves cargo without load) and
	// `loadedMass` (which solves the load itself).
	const prelimLoad = resolveLoad(
		input.loadMode,
		input.customLoad,
		raw.cargo,
		input.cargoTech,
		core,
	);

	if (input.lock !== null) {
		const claimed = CLAIM_MAP[input.lock.output];
		if (claimed !== "load") {
			const solved = solveClaimedBlock(
				input.lock,
				raw,
				input,
				prelimLoad,
				core,
			);
			if (solved === null) {
				lockFeasible = false;
			} else {
				// The solver may produce a value that is mathematically
				// correct yet rejected by the ship-class value rules —
				// most commonly a DWSC block in the (0, 1) gap. Surface
				// that as an infeasible lock so the lock input flips
				// red and the outputs are suppressed, instead of
				// silently showing an invalid design.
				if (!isClaimedBlockValid(solved)) {
					lockFeasible = false;
				} else {
					blocks[claimed] = solved;
					computedInput = claimed;
				}
			}
		}
	}

	let load: number;
	if (input.lock !== null && CLAIM_MAP[input.lock.output] === "load") {
		const emptyMass = core.emptyMass(blocks);
		const solvedLoad =
			emptyMass === null
				? null
				: core.loadForFullMass({
						targetFullMass: input.lock.value,
						emptyMass,
						cargoTech: input.cargoTech,
					});
		if (solvedLoad === null) {
			lockFeasible = false;
			load = resolveLoad(
				input.loadMode,
				input.customLoad,
				blocks.cargo,
				input.cargoTech,
				core,
			);
		} else {
			load = solvedLoad;
			computedInput = "load";
		}
	} else {
		load = resolveLoad(
			input.loadMode,
			input.customLoad,
			blocks.cargo,
			input.cargoTech,
			core,
		);
	}

	const valuesValidation = validateShipClassValues(blocks);
	const valuesValid = valuesValidation.ok;
	const valueReason = valuesValidation.ok ? null : valuesValidation.reason;
	const cargoCapacity = core.cargoCapacity({
		cargo: blocks.cargo,
		cargoTech: input.cargoTech,
	});

	let outputs: CalculatorOutputs | null = null;
	if (valuesValid) {
		const emptyMass = core.emptyMass(blocks);
		if (emptyMass !== null) {
			const carrying = core.carryingMass({ load, cargoTech: input.cargoTech });
			const loadedMass = core.fullMass({ emptyMass, carryingMass: carrying });
			const driveEffective = core.driveEffective({
				drive: blocks.drive,
				driveTech: input.driveTech,
			});
			outputs = {
				emptyMass,
				loadedMass,
				speedEmpty: core.speed({ driveEffective, fullMass: emptyMass }),
				speedLoaded: core.speed({ driveEffective, fullMass: loadedMass }),
				attack: core.effectiveAttack({
					weapons: blocks.weapons,
					weaponsTech: input.weaponsTech,
				}),
				defense: core.effectiveDefence({
					shields: blocks.shields,
					shieldsTech: input.shieldsTech,
					fullMass: loadedMass,
				}),
				bombing: core.bombingPower({
					weapons: blocks.weapons,
					weaponsTech: input.weaponsTech,
					armament: blocks.armament,
					number: 1,
				}),
			};
		}
	}

	return {
		blocks,
		computedInput,
		lockFeasible,
		valuesValid,
		valueReason,
		load,
		cargoCapacity,
		outputs,
	};
}

export interface PlanetBuildInput {
	/** The designed ship's empty mass. */
	shipMass: number;
	/** Free industrial potential (the "L" parameter, FreeIndustry). */
	freeIndustry: number;
	/** Material stockpile (resolved: planet value or the player override). */
	material: number;
	/** Planet resources rating. */
	resources: number;
}

export interface PlanetBuildResult {
	/** Whole ships plus fractional progress completable this turn. */
	shipsPerTurn: number;
	wholeShips: number;
	progress: number;
	/** Turns to finish one ship, or null when none can be produced. */
	turnsPerShip: number | null;
}

/**
 * computePlanetBuild folds one turn of ship production into the headline
 * "ships per turn" and "turns per ship" the planet area shows. It assumes
 * the planet keeps building this ship at the current (or overridden) MAT;
 * the realistic multi-turn forecast with population growth and CAP/COL
 * supply lands in Phase 34. Returns null without a `Core`.
 */
export function computePlanetBuild(
	input: PlanetBuildInput,
	core: Core | null,
): PlanetBuildResult | null {
	if (core === null) return null;
	if (input.shipMass <= 0 || input.freeIndustry <= 0) {
		return { shipsPerTurn: 0, wholeShips: 0, progress: 0, turnsPerShip: null };
	}
	const r = core.produceShipsInTurn({
		productionAvailable: input.freeIndustry,
		material: input.material,
		resources: input.resources,
		shipMass: input.shipMass,
	});
	const shipsPerTurn = r.ships + r.progress;
	return {
		shipsPerTurn,
		wholeShips: r.ships,
		progress: r.progress,
		turnsPerShip: shipsPerTurn > 0 ? 1 / shipsPerTurn : null,
	};
}
