import { isLoopbackHost } from "./host.js";
import net from "node:net";
//#region src/gateway/client-ip.ts
function stripIpv6Brackets(value) {
	if (value.startsWith("[") && value.endsWith("]")) return value.slice(1, -1);
	return value;
}
function stripOptionalPort(ip) {
	if (ip.startsWith("[")) {
		const end = ip.indexOf("]");
		if (end !== -1) return ip.slice(1, end);
	}
	if (net.isIP(ip)) return ip;
	const lastColon = ip.lastIndexOf(":");
	if (lastColon > -1 && ip.includes(".") && ip.indexOf(":") === lastColon) {
		const candidate = ip.slice(0, lastColon);
		if (net.isIP(candidate) === 4) return candidate;
	}
	return ip;
}
/** Normalize IP for comparison (strip zone id, brackets, IPv4-mapped prefix). */
function normalizeIpAddress(ip) {
	const trimmed = ip?.trim();
	if (!trimmed) return;
	let value = stripOptionalPort(stripIpv6Brackets(trimmed));
	const zoneIdx = value.indexOf("%");
	if (zoneIdx > -1) value = value.slice(0, zoneIdx);
	if (value.startsWith("::ffff:")) {
		const mapped = value.slice(7);
		if (net.isIP(mapped) === 4) return mapped;
	}
	if (net.isIP(value) === 0) return;
	return value;
}
function isLoopbackIpAddress(ip) {
	const normalized = normalizeIpAddress(ip);
	if (!normalized) return false;
	return isLoopbackHost(normalized);
}
function ipv4ToInt(ip) {
	const parts = ip.split(".").map((part) => Number.parseInt(part, 10));
	return (parts[0] << 24 | parts[1] << 16 | parts[2] << 8 | parts[3]) >>> 0;
}
function isIpv4InCidr(ip, base, prefix) {
	const mask = prefix === 0 ? 0 : -1 << 32 - prefix >>> 0;
	return (ipv4ToInt(ip) & mask) === (ipv4ToInt(base) & mask);
}
function parseIpv6BigInt(ip) {
	try {
		const parts = ip.split(":");
		if (parts.length < 3) return;
		let hextets = [];
		const emptyIdx = parts.indexOf("");
		if (emptyIdx !== -1) {
			const before = parts.slice(0, emptyIdx).filter(Boolean).map((h) => Number.parseInt(h, 16));
			const after = parts.slice(emptyIdx + 1).filter(Boolean).map((h) => Number.parseInt(h, 16));
			const missing = 8 - before.length - after.length;
			hextets = [
				...before,
				...Array(Math.max(0, missing)).fill(0),
				...after
			];
		} else hextets = parts.map((h) => Number.parseInt(h, 16));
		if (hextets.length !== 8 || hextets.some((h) => !Number.isFinite(h) || h < 0 || h > 65535)) return;
		let value = 0n;
		for (const hextet of hextets) value = (value << 16n) + BigInt(hextet);
		return value;
	} catch {
		return;
	}
}
function isIpv6InCidr(ip, base, prefix) {
	const ipValue = parseIpv6BigInt(ip);
	const baseValue = parseIpv6BigInt(base);
	if (ipValue === void 0 || baseValue === void 0) return false;
	if (prefix <= 0) return true;
	if (prefix >= 128) return ipValue === baseValue;
	const shift = BigInt(128 - prefix);
	return ipValue >> shift === baseValue >> shift;
}
/** Match an IP against a CIDR or exact address string. */
function isIpInCidr(ip, cidr) {
	const normalizedIp = normalizeIpAddress(ip);
	if (!normalizedIp) return false;
	const candidate = cidr.trim();
	if (!candidate) return false;
	if (!candidate.includes("/")) return normalizeIpAddress(candidate) === normalizedIp;
	const [baseRaw, prefixRaw] = candidate.split("/");
	const base = normalizeIpAddress(baseRaw);
	const prefix = Number.parseInt(prefixRaw ?? "", 10);
	if (!base || !Number.isInteger(prefix)) return false;
	const ipKind = net.isIP(normalizedIp);
	if (ipKind !== net.isIP(base)) return false;
	if (ipKind === 4) {
		if (prefix < 0 || prefix > 32) return false;
		return isIpv4InCidr(normalizedIp, base, prefix);
	}
	if (prefix < 0 || prefix > 128) return false;
	return isIpv6InCidr(normalizedIp, base, prefix);
}
function isTrustedProxyAddress(ip, trustedProxies) {
	const normalized = normalizeIpAddress(ip);
	if (!normalized || !trustedProxies?.length) return false;
	return trustedProxies.some((proxy) => {
		const candidate = proxy.trim();
		return candidate.length > 0 && isIpInCidr(normalized, candidate);
	});
}
function parseIpLiteral(raw) {
	return normalizeIpAddress(raw);
}
function resolveForwardedClientIp(params) {
	const { forwardedFor, trustedProxies } = params;
	if (!trustedProxies?.length) return;
	const forwardedChain = [];
	for (const entry of forwardedFor?.split(",") ?? []) {
		const normalized = parseIpLiteral(entry);
		if (normalized) forwardedChain.push(normalized);
	}
	if (forwardedChain.length === 0) return;
	for (let index = forwardedChain.length - 1; index >= 0; index -= 1) {
		const hop = forwardedChain[index];
		if (isLoopbackIpAddress(hop)) continue;
		if (!isTrustedProxyAddress(hop, trustedProxies)) return hop;
	}
}
function resolveClientIp(params) {
	const remote = normalizeIpAddress(params.remoteAddr);
	if (!remote) return;
	if (!isTrustedProxyAddress(remote, params.trustedProxies)) return remote;
	const forwardedIp = resolveForwardedClientIp({
		forwardedFor: params.forwardedFor,
		trustedProxies: params.trustedProxies
	});
	if (forwardedIp) return forwardedIp;
	if (params.allowRealIpFallback) return parseIpLiteral(params.realIp);
}
function headerValue(value) {
	return Array.isArray(value) ? value[0] : value;
}
function resolveRequestClientIp(req, trustedProxies, allowRealIpFallback = false) {
	if (!req) return;
	return resolveClientIp({
		remoteAddr: req.socket?.remoteAddress ?? "",
		forwardedFor: headerValue(req.headers?.["x-forwarded-for"]),
		realIp: headerValue(req.headers?.["x-real-ip"]),
		trustedProxies,
		allowRealIpFallback
	});
}
function resolveClientIpFromRequest(params) {
	return resolveClientIp({
		remoteAddr: params.remoteAddress,
		forwardedFor: params.getHeader("x-forwarded-for"),
		realIp: params.getHeader("x-real-ip"),
		trustedProxies: params.trustedProxies,
		allowRealIpFallback: params.allowRealIpFallback
	}) ?? "unknown";
}
//#endregion
export { isIpInCidr, isLoopbackIpAddress, isTrustedProxyAddress, normalizeIpAddress, resolveClientIp, resolveClientIpFromRequest, resolveRequestClientIp };

//# sourceMappingURL=client-ip.js.map