/**
 * Schedule ULID — monotonic, sortable identifier generator.
 *
 * Format: `01` prefix + 10-char Crockford-Base32 timestamp + 16-char random suffix.
 * Total length: 28 characters.
 *
 * Purpose: generate time-sortable, collision-resistant IDs for schedule entries
 * without an external dependency. Monotonicity within the same millisecond is
 * guaranteed by tracking the last random suffix and incrementing it when needed.
 *
 * Consumer: schedule-store.js appendEntry() to assign entry IDs.
 */
import { randomUUID } from "node:crypto";

// ─── Constants ──────────────────────────────────────────────────────────────

/** Crockford Base32 alphabet (excludes I, L, O, U to avoid ambiguity). */
const CROCKFORD = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";

/** Number of characters for the timestamp portion (50 bits). */
const TS_CHARS = 10;

/** Number of characters for the random portion (80 bits). */
const RANDOM_CHARS = 16;

/** ULID prefix. */
const PREFIX = "01";

/** Mask for 80 bits. */
const RANDOM_MASK = (1n << 80n) - 1n;

// ─── Module State ───────────────────────────────────────────────────────────

/** Last timestamp used (milliseconds since epoch). */
let _lastTimestamp = 0;

/** Last random suffix value (as a BigInt for precise arithmetic). */
let _lastRandomValue = 0n;

// ─── Public API ─────────────────────────────────────────────────────────────

/**
 * Generate a new ULID string.
 *
 * Uses Date.now() for the timestamp and crypto.randomUUID() for entropy.
 * If two calls occur within the same millisecond, the random suffix is
 * strictly incremented to preserve monotonic ordering.
 *
 * Purpose: produce deterministic-sortable IDs that work across processes
 * (no shared counter file) while keeping collision probability negligible.
 *
 * Consumer: schedule-store.js when appending new entries.
 *
 * @returns {string} 28-character ULID
 */
export function generateULID() {
	const ts = Date.now();

	if (ts <= _lastTimestamp) {
		// Clock unchanged or moved backwards — maintain monotonicity by
		// incrementing the random suffix. This matches standard ULID behaviour.
		_lastRandomValue = (_lastRandomValue + 1n) & RANDOM_MASK;
		if (_lastRandomValue === 0n) {
			// 80-bit overflow — bump timestamp and reset random.
			_lastTimestamp = _lastTimestamp + 1;
			_lastRandomValue = _extractRandomFromUUID();
		}
	} else {
		_lastTimestamp = ts;
		_lastRandomValue = _extractRandomFromUUID();
	}

	const tsPart = _encodeBase32(BigInt(_lastTimestamp), TS_CHARS);
	const randPart = _encodeBase32(_lastRandomValue, RANDOM_CHARS);
	return `${PREFIX}${tsPart}${randPart}`;
}

// ─── Internal ───────────────────────────────────────────────────────────────

/**
 * Encode a non-negative integer as a fixed-length Crockford Base32 string.
 *
 * @param {bigint} value
 * @param {number} length
 * @returns {string}
 */
function _encodeBase32(value, length) {
	let result = "";
	for (let i = 0; i < length; i++) {
		const idx = Number(value & 0x1fn);
		result = CROCKFORD[idx] + result;
		value = value >> 5n;
	}
	return result;
}

/**
 * Extract 80 bits of randomness from a UUID v4.
 *
 * @returns {bigint}
 */
function _extractRandomFromUUID() {
	const hex = randomUUID().replace(/-/g, "");
	// Take the lower 80 bits of the 128-bit UUID value.
	const full = BigInt("0x" + hex);
	return full & RANDOM_MASK;
}
