/**
 * Abort Signal Utilities
 *
 * Provides utilities for consistent AbortSignal propagation across the application.
 * These utilities help ensure that async operations can be properly cancelled
 * when needed (e.g., user stops generation, navigates away, etc.).
 */

// the standard DOMException name for a cancelled operation
const ABORT_ERROR_NAME = 'AbortError';

// browser specific TypeError messages emitted when a fetch reader is cut by page unload,
// navigation, or a transient network drop. functionally aborts, not actionable errors
const ABORT_LIKE_MESSAGE_PATTERNS = [
	/input stream/i, // Firefox: stream cut at unload
	/network connection was lost/i, // Safari: transient network drop
	/load failed/i // Safari: page navigation during fetch
];

/**
 * Throws an AbortError if the signal is aborted.
 * Use this at the start of async operations to fail fast.
 *
 * @param signal - Optional AbortSignal to check
 * @throws DOMException with name 'AbortError' if signal is aborted
 *
 * @example
 * ```ts
 * async function fetchData(signal?: AbortSignal) {
 *   throwIfAborted(signal);
 *   // ... proceed with operation
 * }
 * ```
 */
export function throwIfAborted(signal?: AbortSignal): void {
	if (signal?.aborted) {
		throw new DOMException('Operation was aborted', ABORT_ERROR_NAME);
	}
}

/**
 * Checks if an error is an AbortError.
 * Use this to distinguish between user-initiated cancellation and actual errors.
 *
 * @param error - Error to check
 * @returns true if the error is an AbortError
 *
 * @example
 * ```ts
 * try {
 *   await fetchData(signal);
 * } catch (error) {
 *   if (isAbortError(error)) {
 *     // User cancelled - no error dialog needed
 *     return;
 *   }
 *   // Handle actual error
 * }
 * ```
 */
export function isAbortError(error: unknown): boolean {
	if (error instanceof DOMException && error.name === ABORT_ERROR_NAME) {
		return true;
	}
	if (error instanceof Error) {
		if (error.name === ABORT_ERROR_NAME) {
			return true;
		}
		// these patterns are functionally aborts, keep them out of the red console
		if (error instanceof TypeError) {
			const msg = error.message ?? '';
			if (ABORT_LIKE_MESSAGE_PATTERNS.some((re) => re.test(msg))) return true;
		}
	}
	return false;
}

/**
 * Creates a new AbortController that is linked to one or more parent signals.
 * When any parent signal aborts, the returned controller also aborts.
 *
 * Useful for creating child operations that should be cancelled when
 * either the parent operation or their own timeout/condition triggers.
 *
 * @param signals - Parent signals to link to (undefined signals are ignored)
 * @returns A new AbortController linked to all provided signals
 *
 * @example
 * ```ts
 * // Link to user's abort signal and add a timeout
 * const linked = createLinkedController(userSignal, timeoutSignal);
 * await fetch(url, { signal: linked.signal });
 * ```
 */
export function createLinkedController(...signals: (AbortSignal | undefined)[]): AbortController {
	const controller = new AbortController();

	for (const signal of signals) {
		if (!signal) continue;

		// If already aborted, abort immediately
		if (signal.aborted) {
			controller.abort(signal.reason);
			return controller;
		}

		// Link to parent signal
		signal.addEventListener('abort', () => controller.abort(signal.reason), { once: true });
	}

	return controller;
}

/**
 * Creates an AbortSignal that times out after the specified duration.
 *
 * @param ms - Timeout duration in milliseconds
 * @returns AbortSignal that will abort after the timeout
 *
 * @example
 * ```ts
 * const signal = createTimeoutSignal(5000); // 5 second timeout
 * await fetch(url, { signal });
 * ```
 */
export function createTimeoutSignal(ms: number): AbortSignal {
	return AbortSignal.timeout(ms);
}

/**
 * Wraps a promise to reject if the signal is aborted.
 * Useful for making non-abortable promises respect an AbortSignal.
 *
 * @param promise - Promise to wrap
 * @param signal - AbortSignal to respect
 * @returns Promise that rejects with AbortError if signal aborts
 *
 * @example
 * ```ts
 * // Make a non-abortable operation respect abort signal
 * const result = await withAbortSignal(
 *   someNonAbortableOperation(),
 *   signal
 * );
 * ```
 */
export async function withAbortSignal<T>(promise: Promise<T>, signal?: AbortSignal): Promise<T> {
	if (!signal) return promise;

	throwIfAborted(signal);

	return new Promise<T>((resolve, reject) => {
		const abortHandler = () => {
			reject(new DOMException('Operation was aborted', ABORT_ERROR_NAME));
		};

		signal.addEventListener('abort', abortHandler, { once: true });

		promise
			.then((value) => {
				signal.removeEventListener('abort', abortHandler);
				resolve(value);
			})
			.catch((error) => {
				signal.removeEventListener('abort', abortHandler);
				reject(error);
			});
	});
}
