// TS port of `pkg/util/string.go.ValidateTypeName` — every entity
// name (planet, ship class, science, …) the player edits goes
// through this validator before reaching the order draft, so the
// client-side check is identical to the server-side one. A
// locally-valid name is always accepted at the wire level; an
// invalid name never produces a network round-trip.

const MAX_LENGTH = 30;

const ALLOWED_SPECIALS = new Set<string>("!@#$%^*-_=+~()[]{}");

const SPECIAL_RUN_LIMIT = 2;

/**
 * EntityNameInvalidReason is the closed enumeration of reasons a
 * name can fail validation. The values are stable identifiers so
 * the inspector tooltip and the order-tab status row can map them
 * to localised copy via `i18n.t("game.order.invalid." + reason)`.
 */
export type EntityNameInvalidReason =
	| "empty"
	| "too_long"
	| "starts_with_special"
	| "ends_with_special"
	| "consecutive_specials"
	| "whitespace"
	| "disallowed_character";

export type EntityNameValidation =
	| { ok: true; value: string }
	| { ok: false; reason: EntityNameInvalidReason };

/**
 * validateEntityName mirrors `ValidateTypeName` exactly: the input
 * is trimmed, must be non-empty, must fit in 30 runes, must not
 * start or end with a special character, and must contain only
 * letters, digits, combining marks, or the allowed specials with at
 * most two in a row. Returns the trimmed value on success or a
 * structured reason on failure.
 */
export function validateEntityName(input: string): EntityNameValidation {
	const trimmed = input.trim();
	if (trimmed.length === 0) {
		return { ok: false, reason: "empty" };
	}

	const runes = Array.from(trimmed);
	if (runes.length > MAX_LENGTH) {
		return { ok: false, reason: "too_long" };
	}

	const first = runes[0]!;
	const last = runes[runes.length - 1]!;
	if (ALLOWED_SPECIALS.has(first)) {
		return { ok: false, reason: "starts_with_special" };
	}
	if (ALLOWED_SPECIALS.has(last)) {
		return { ok: false, reason: "ends_with_special" };
	}

	let specialRun = 0;
	for (const rune of runes) {
		if (isWhitespace(rune)) {
			return { ok: false, reason: "whitespace" };
		}
		if (isLetter(rune) || isDigit(rune) || isCombiningMark(rune)) {
			specialRun = 0;
			continue;
		}
		if (ALLOWED_SPECIALS.has(rune)) {
			specialRun += 1;
			if (specialRun > SPECIAL_RUN_LIMIT) {
				return { ok: false, reason: "consecutive_specials" };
			}
			continue;
		}
		return { ok: false, reason: "disallowed_character" };
	}

	return { ok: true, value: trimmed };
}

function isWhitespace(rune: string): boolean {
	// Matches Go's `unicode.IsSpace`.
	return /\s/u.test(rune);
}

function isLetter(rune: string): boolean {
	return /\p{L}/u.test(rune);
}

function isDigit(rune: string): boolean {
	return /\p{N}/u.test(rune);
}

function isCombiningMark(rune: string): boolean {
	return /\p{M}/u.test(rune);
}
