import { createLogger } from "../utils/logger/index.js";
import { init_logger } from "../utils/logger.js";
import { request } from "undici";
import { Readable } from "node:stream";
import * as ws from "ws";
//#region src/share/site-proxy.ts
init_logger();
const wsAny = ws;
const WSClient = wsAny.WebSocket ?? wsAny.default;
const WebSocketServer = wsAny.WebSocketServer ?? wsAny.default?.WebSocketServer;
const log = createLogger("SiteProxy");
const HOP_BY_HOP_HEADERS = new Set([
	"connection",
	"keep-alive",
	"proxy-authenticate",
	"proxy-authorization",
	"te",
	"trailer",
	"transfer-encoding",
	"upgrade",
	"host",
	"content-length"
]);
const FORWARDED_HEADERS_SKIP = new Set([
	"host",
	"x-forwarded-for",
	"x-forwarded-host",
	"x-forwarded-proto",
	"x-real-ip",
	"connection",
	"keep-alive",
	"upgrade",
	"transfer-encoding"
]);
/** Build the upstream URL preserving search params from the incoming request. */
function buildUpstreamUrl(source, innerPath, search) {
	return `${source.upstreamUrl.replace(/\/+$/, "")}${innerPath.startsWith("/") ? innerPath : `/${innerPath}`}${search}`;
}
function filterRequestHeaders(headers) {
	const out = {};
	headers.forEach((value, key) => {
		const lower = key.toLowerCase();
		if (FORWARDED_HEADERS_SKIP.has(lower)) return;
		out[lower] = value;
	});
	return out;
}
function rewriteSetCookie(value, basePrefix) {
	if (!basePrefix) return value;
	return value.replace(/(;\s*Path=)([^;]*)/gi, (_match, prefix, path) => {
		if (path.startsWith(basePrefix)) return _match;
		if (path.startsWith("/")) return `${prefix}${basePrefix}${path}`;
		return `${prefix}${basePrefix}/${path}`;
	});
}
function rewriteLocation(value, source, basePrefix) {
	try {
		const upstream = new URL(source.upstreamUrl);
		const target = new URL(value, source.upstreamUrl);
		if (target.host === upstream.host) return `${basePrefix}${target.pathname}${target.search}${target.hash}`;
		return value;
	} catch {
		if (value.startsWith("/") && !value.startsWith("//")) return `${basePrefix}${value}`;
		return value;
	}
}
/**
* Forward an HTTP request to the upstream and return a streaming Response.
* Handles header filtering, forwarded headers, Set-Cookie / Location rewriting,
* and a per-request timeout.
*/
async function forwardHttpRequest(record, store, req, ctx) {
	if (record.source.kind !== "proxy") return { response: new Response("not_proxy", { status: 400 }) };
	const source = record.source;
	const cfg = store.getConfig().proxy;
	const upstreamUrl = buildUpstreamUrl(source, ctx.innerPath, ctx.originalUrl.search);
	const requestHeaders = filterRequestHeaders(req.headers);
	const forwardedFor = req.headers.get("x-forwarded-for");
	requestHeaders["x-forwarded-for"] = forwardedFor ? `${forwardedFor}, ${ctx.clientIp}` : ctx.clientIp;
	requestHeaders["x-forwarded-host"] = req.headers.get("host") ?? "";
	requestHeaders["x-forwarded-proto"] = ctx.forwardedProto;
	requestHeaders["x-real-ip"] = ctx.clientIp;
	requestHeaders.host = new URL(source.upstreamUrl).host;
	const controller = new AbortController();
	const timer = setTimeout(() => controller.abort(), cfg.requestTimeoutMs);
	timer.unref?.();
	try {
		const method = req.method.toUpperCase();
		const upstream = await request(upstreamUrl, {
			method,
			headers: requestHeaders,
			body: method !== "GET" && method !== "HEAD" && req.body != null ? Readable.fromWeb(req.body) : void 0,
			signal: controller.signal,
			bodyTimeout: cfg.requestTimeoutMs,
			headersTimeout: cfg.requestTimeoutMs
		});
		const responseHeaders = new Headers();
		for (const [rawKey, rawValue] of Object.entries(upstream.headers)) {
			if (rawValue === void 0) continue;
			const lower = rawKey.toLowerCase();
			if (HOP_BY_HOP_HEADERS.has(lower)) continue;
			const values = Array.isArray(rawValue) ? rawValue : [rawValue];
			for (const value of values) {
				if (typeof value !== "string") continue;
				let outValue = value;
				if (lower === "set-cookie" && source.rewriteSetCookiePath) outValue = rewriteSetCookie(value, ctx.basePrefix);
				else if (lower === "location") outValue = rewriteLocation(value, source, ctx.basePrefix);
				responseHeaders.append(rawKey, outValue);
			}
		}
		const body = upstream.body ? Readable.toWeb(upstream.body) : null;
		store.recordRequest(record.id, ctx.clientIp);
		return { response: new Response(body, {
			status: upstream.statusCode,
			headers: responseHeaders
		}) };
	} catch (err) {
		log.warn({
			err,
			shareId: record.id,
			upstreamUrl
		}, `Proxy request failed: ${err instanceof Error ? err.message : String(err)}`);
		return { response: new Response("Bad gateway", { status: 502 }) };
	} finally {
		clearTimeout(timer);
	}
}
const wsServer = new WebSocketServer({ noServer: true });
function handleProxyWebSocketUpgrade(args) {
	const { record, store, req, socket, head, innerPath, basePrefix, clientIp } = args;
	if (record.source.kind !== "proxy") {
		socket.destroy();
		return;
	}
	const source = record.source;
	const cfg = store.getConfig().proxy;
	if (!cfg.forwardWebSocket || !source.forwardWebSocket) {
		socket.destroy();
		return;
	}
	const upstreamBase = new URL(source.upstreamUrl);
	const wsProto = upstreamBase.protocol === "https:" ? "wss:" : "ws:";
	const search = req.url ? new URL(req.url, "http://localhost").search : "";
	const path = innerPath.startsWith("/") ? innerPath : `/${innerPath}`;
	const upstreamWsUrl = `${wsProto}//${upstreamBase.host}${path}${search}`;
	const forwardHeaders = {};
	for (const [rawKey, rawValue] of Object.entries(req.headers)) {
		if (rawValue === void 0) continue;
		const lower = rawKey.toLowerCase();
		if (FORWARDED_HEADERS_SKIP.has(lower)) continue;
		if (lower.startsWith("sec-websocket-")) continue;
		forwardHeaders[lower] = Array.isArray(rawValue) ? rawValue.join(", ") : String(rawValue);
	}
	const xff = req.headers["x-forwarded-for"];
	forwardHeaders["x-forwarded-for"] = xff ? `${xff}, ${clientIp}` : clientIp;
	forwardHeaders["x-forwarded-host"] = String(req.headers.host ?? "");
	forwardHeaders["x-forwarded-proto"] = "http";
	forwardHeaders["x-real-ip"] = clientIp;
	forwardHeaders.host = upstreamBase.host;
	let upstreamWs;
	try {
		upstreamWs = new WSClient(upstreamWsUrl, {
			headers: forwardHeaders,
			handshakeTimeout: cfg.requestTimeoutMs
		});
	} catch (err) {
		log.warn({
			err,
			shareId: record.id
		}, "WebSocket upstream connect threw");
		socket.destroy();
		return;
	}
	let idleTimer = null;
	const resetIdleTimer = (kill) => {
		if (idleTimer) clearTimeout(idleTimer);
		idleTimer = setTimeout(kill, cfg.wsIdleTimeoutMs);
		idleTimer.unref?.();
	};
	upstreamWs.once("open", () => {
		wsServer.handleUpgrade(req, socket, head, (clientWs) => {
			store.recordRequest(record.id, clientIp);
			const killAll = () => {
				if (idleTimer) clearTimeout(idleTimer);
				try {
					clientWs.close();
				} catch {}
				try {
					upstreamWs.close();
				} catch {}
			};
			resetIdleTimer(killAll);
			const pump = (from, to) => {
				from.on("message", (data, isBinary) => {
					resetIdleTimer(killAll);
					try {
						to.send(data, { binary: isBinary });
					} catch (err) {
						log.warn({
							err,
							shareId: record.id
						}, "WS forward failed");
						killAll();
					}
				});
				from.on("close", killAll);
				from.on("error", (err) => {
					log.warn({
						err,
						shareId: record.id
					}, "WS error");
					killAll();
				});
			};
			pump(clientWs, upstreamWs);
			pump(upstreamWs, clientWs);
		});
	});
	upstreamWs.once("error", (err) => {
		log.warn({
			err,
			shareId: record.id,
			upstreamWsUrl
		}, "WS upstream error");
		try {
			socket.write("HTTP/1.1 502 Bad Gateway\r\nConnection: close\r\n\r\n");
		} catch {}
		socket.destroy();
	});
}
//#endregion
export { forwardHttpRequest, handleProxyWebSocketUpgrade };

//# sourceMappingURL=site-proxy.js.map