/**
 * Removes unpaired Unicode surrogate characters from a string.
 *
 * Unpaired surrogates (high surrogates 0xD800-0xDBFF without matching low surrogates 0xDC00-0xDFFF,
 * or vice versa) cause JSON serialization errors in many API providers.
 *
 * Valid emoji and other characters outside the Basic Multilingual Plane use properly paired
 * surrogates and will NOT be affected by this function.
 *
 * @param text - The text to sanitize
 * @returns The sanitized text with unpaired surrogates removed
 *
 * @example
 * // Valid emoji (properly paired surrogates) are preserved
 * sanitizeSurrogates("Hello 🙈 World") // => "Hello 🙈 World"
 *
 * // Unpaired high surrogate is removed
 * const unpaired = String.fromCharCode(0xD83D); // high surrogate without low
 * sanitizeSurrogates(`Text ${unpaired} here`) // => "Text  here"
 */
export function sanitizeSurrogates(text: string): string {
	// Replace unpaired high surrogates (0xD800-0xDBFF not followed by low surrogate)
	// Replace unpaired low surrogates (0xDC00-0xDFFF not preceded by high surrogate)
	return text.replace(/[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g, "");
}

/**
 * Recursively removes unpaired Unicode surrogate characters from every string reachable
 * inside a value (strings, arrays, and plain-object values), preserving structure and
 * non-string primitives.
 *
 * Use this for structured payloads that get JSON-serialized into a provider request body
 * but do not pass through {@link sanitizeSurrogates} on a per-field basis — most importantly
 * tool-call argument objects, whose string fields are model-generated and can contain lone
 * surrogates that make the request body invalid JSON ("no low surrogate in string").
 *
 * @param value - Any value; strings are sanitized, containers are walked, others returned as-is
 * @returns A value of the same shape with all reachable strings sanitized
 */
export function sanitizeSurrogatesDeep<T>(value: T): T {
	if (typeof value === "string") {
		return sanitizeSurrogates(value) as unknown as T;
	}
	if (Array.isArray(value)) {
		return value.map((item) => sanitizeSurrogatesDeep(item)) as unknown as T;
	}
	if (value !== null && typeof value === "object") {
		const source = value as Record<string, unknown>;
		const result: Record<string, unknown> = {};
		for (const key of Object.keys(source)) {
			result[key] = sanitizeSurrogatesDeep(source[key]);
		}
		return result as unknown as T;
	}
	return value;
}
