/**
 * Guards for inspecting errors thrown by the {@link HttpRequestClient}.
 *
 * The client rejects on connection failures, and unless `ignoreHttpStatusErrors` is set, on non-2xx responses.
 * The rejected error carries the underlying transport's `code` and `response.status`.
 * These guards read them so callers can branch on the failure without coupling to a specific HTTP library.
 *
 */

/**
 * Whether the error represents a refused TCP connection (`ECONNREFUSED`).
 */
export function isConnectionRefusedError(error: unknown): boolean {
	return (
		typeof error === 'object' &&
		error !== null &&
		'code' in error &&
		(error as { code?: unknown }).code === 'ECONNREFUSED'
	);
}

const TRANSPORT_FAILURE_CODES: ReadonlySet<string> = new Set([
	// libuv errno names, surfaced by Node as `err.code` on socket operations.
	// https://docs.libuv.org/en/v1.x/errors.html
	'ECONNABORTED',
	'ECONNREFUSED',
	'ECONNRESET',
	'EHOSTUNREACH',
	'ENETDOWN',
	'ENETUNREACH',
	'EPIPE',
	'ETIMEDOUT',
	// undici error codes, thrown by fetch and by the SDKs built on it.
	// https://github.com/nodejs/undici/blob/main/docs/docs/api/Errors.md
	'UND_ERR_BODY_TIMEOUT',
	'UND_ERR_CONNECT_TIMEOUT',
	'UND_ERR_HEADERS_TIMEOUT',
	'UND_ERR_SOCKET',
]);

// getaddrinfo codes, not libuv errnos: Node exposes them on `err.code` for name resolution.
// https://nodejs.org/api/dns.html#error-codes
const DNS_FAILURE_CODES: ReadonlySet<string> = new Set(['EAI_AGAIN', 'ENOTFOUND']);

/**
 * Whether the connection underlying a request broke: refused, reset, timed out
 * at the socket, or destroyed mid-response. Excludes name resolution, see
 * {@link isDnsFailure}.
 *
 * undici collapses a mid-stream body destruction into a bare
 * `TypeError: terminated`, sometimes without a `cause`, hence the shape check.
 */
export function isTransportFailure(error: unknown): boolean {
	return (
		(error instanceof Error && error.name === 'TypeError' && error.message === 'terminated') ||
		hasErrorCode(error, TRANSPORT_FAILURE_CODES)
	);
}

/**
 * Whether a request failed because the hostname could not be resolved.
 * Kept apart from {@link isTransportFailure}: callers disagree on whether that is a
 * transient blip to retry or a misconfiguration to surface.
 */
export function isDnsFailure(error: unknown): boolean {
	return hasErrorCode(error, DNS_FAILURE_CODES);
}

function hasErrorCode(
	error: unknown,
	codes: ReadonlySet<string>,
	visited = new WeakSet<object>(),
): boolean {
	if (typeof error !== 'object' || error === null) {
		return false;
	}
	if (visited.has(error)) {
		return false;
	}

	visited.add(error);

	if ('code' in error && typeof error.code === 'string' && codes.has(error.code)) {
		return true;
	}

	return 'cause' in error && hasErrorCode(error.cause, codes, visited);
}

/**
 * Whether the error originates from axios, recognized by the `isAxiosError` brand
 * axios stamps on every error it throws.
 *
 * Lets callers classify axios failures without importing axios themselves, now that
 * the axios instance lives behind this package's HTTP client.
 *
 * @deprecated Axios details should not be exposed outside of @n8n/backend-network
 */
export function isAxiosError(error: unknown): boolean {
	return (
		typeof error === 'object' &&
		error !== null &&
		(error as { isAxiosError?: unknown }).isAxiosError === true
	);
}

/**
 * The HTTP status code carried by a request error, when the call received a response.
 */
export function httpStatusFromError(error: unknown): number | undefined {
	if (typeof error === 'object' && error !== null && 'response' in error) {
		const response = (error as { response?: { status?: unknown } }).response;
		if (response && typeof response.status === 'number') {
			return response.status;
		}
	}
	return undefined;
}

/**
 * The shape of an error rejected by the {@link HttpRequestClient}
 * It carries the transport's `code` and, when a response was received, its `status` and parsed `data`.
 */
export interface HttpRequestError extends Error {
	code?: string;
	response?: { status?: number; data?: unknown };
}

/**
 * Process-global marker tagged onto every error the {@link HttpRequestClient} rejects with.
 * Uses `Symbol.for` (the global registry), not a module-local `Symbol()`,
 * so the tag still matches when this package is loaded as more than one module instance (src + dist).
 */
const HTTP_REQUEST_ERROR = Symbol.for('n8n.backend-network.http-request-error');

/**
 * Tags an error as rejected by the {@link HttpRequestClient}.
 * Called by the client on its reject path so {@link isHttpRequestError} can recognize it
 * without coupling to the transport library.
 */
export function markHttpRequestError<E>(error: E): E {
	if (typeof error === 'object' && error !== null) {
		Object.defineProperty(error, HTTP_REQUEST_ERROR, {
			value: true,
			enumerable: false, // it never leaks into logs, `JSON.stringify`, or `Object.keys`.
			configurable: true,
		});
	}
	return error;
}

/**
 * Whether the error was rejected by the {@link HttpRequestClient} (a transport
 * failure or a non-2xx response) rather than being an unrelated runtime error.
 * Lets callers read `response.data` without coupling to a specific HTTP library.
 */
export function isHttpRequestError(error: unknown): error is HttpRequestError {
	return (
		error instanceof Error &&
		(error as unknown as Record<symbol, unknown>)[HTTP_REQUEST_ERROR] === true
	);
}
