import { createServer } from "node:http";
import { Service } from "@deepseek-ai/cordis";
import z from "@deepseek-ai/schemastery";
//#region lib/types/index.js
/**
* @deepseek-ai/dsh-host-webserver — Web route-registration plugin: a node:http
* server plus the `webServer` service (HTTP and upgrade route registries,
* index transform taps, and the single fallback seat for everything no route
* claims). Knows no harness concepts and serves no files; the composing
* application's frontend plugin owns dist serving through the fallback hook.
* Web shape only — Electron loads dist over file:// and carries fetch over an
* IPC bridge. This package never prints: the URL line belongs to the shell.
*/
/**
* The browser HTTP carrier service. Activation listens immediately. Route
* registration order does not affect requests because configured named routes
* must be distinct, and the fallback handler answers anything not yet claimed
* during startup with 404 until its owner registers. A listen failure rejects
* initialization, and the boot process reports the failed fiber.
*/
var WebServer = class extends Service {
	config;
	static Config = z.object({
		host: z.union([z.const("127.0.0.1"), z.const("0.0.0.0")]).required(),
		port: z.natural().max(65535).required()
	});
	exact = /* @__PURE__ */ new Map();
	prefixes = /* @__PURE__ */ new Map();
	upgrades = /* @__PURE__ */ new Map();
	upgradedSockets = /* @__PURE__ */ new Set();
	indexTaps = [];
	fallback;
	server;
	listenedPort;
	constructor(ctx, config) {
		super(ctx, "webServer");
		this.config = config;
	}
	/** The listening port (the OS-assigned value when config.port is 0). */
	get port() {
		return this.listenedPort;
	}
	/** The configured bind host (the loopback or all-interfaces literal). */
	get host() {
		return this.config.host;
	}
	/**
	* Register a named route. Duplicate (kind, path) throws — route patterns are
	* a composition-level contract, so a collision is a misconfiguration.
	* @param route - kind, path, and the owning handler.
	* @returns the disposer removing the route.
	*/
	register(route) {
		const table = route.kind === "exact" ? this.exact : this.prefixes;
		if (table.has(route.path)) throw new Error(`webserver: duplicate ${route.kind} route "${route.path}"`);
		table.set(route.path, route);
		return () => {
			table.delete(route.path);
		};
	}
	/**
	* Register an exact-path HTTP upgrade route. Duplicate paths throw because
	* one socket can have only one protocol owner.
	* @param route - pathname and handler owning negotiation plus socket use.
	* @returns the disposer removing the route.
	*/
	registerUpgrade(route) {
		if (this.upgrades.has(route.path)) throw new Error(`webserver: duplicate upgrade route "${route.path}"`);
		this.upgrades.set(route.path, route);
		return () => {
			this.upgrades.delete(route.path);
		};
	}
	/**
	* Claim the fallback seat: the handler answering every request no named
	* route matches (the SPA dist server in the shipped Web composition). One
	* owner only — a second registration throws, because two fallbacks cannot
	* compose.
	* @param handler - owns the full response lifecycle of unmatched requests.
	* @returns the disposer releasing the seat.
	*/
	registerFallback(handler) {
		if (this.fallback !== void 0) throw new Error("webserver: fallback already registered");
		this.fallback = handler;
		return () => {
			this.fallback = void 0;
		};
	}
	/**
	* Register an index.html transform, applied by the fallback owner to every
	* index response ({@link applyIndexTaps}) in registration order.
	* @param transform - pure html-to-html function.
	* @returns the disposer removing the transform.
	*/
	tapIndex(transform) {
		this.indexTaps.push(transform);
		return () => {
			const at = this.indexTaps.indexOf(transform);
			if (at !== -1) this.indexTaps.splice(at, 1);
		};
	}
	/** Listen; resolves once the socket is bound (rejection = FAILED fiber). */
	async [Service.init]() {
		const handle = async (req, res) => {
			/* v8 ignore next -- `?? '/'` arm: node:http always sets url on server
			requests; the field is only optional on the client-side IncomingMessage type */
			const rawPath = new URL(req.url ?? "/", "http://x").pathname;
			const route = this.match(rawPath);
			if (route !== void 0) {
				await route.handler(req, res);
				return;
			}
			const fallback = this.fallback;
			if (fallback === void 0) {
				res.writeHead(404);
				res.end();
				return;
			}
			await fallback(req, res);
		};
		this.server = createServer((req, res) => {
			handle(req, res).catch((err) => {
				this.ctx.logger.warn(err instanceof Error ? err : new Error(String(err)));
				if (res.headersSent) {
					res.destroy();
					return;
				}
				res.writeHead(400);
				res.end();
			});
		});
		this.server.on("upgrade", (req, socket, head) => {
			const onError = (error) => {
				this.ctx.logger.warn(error);
				socket.destroy();
			};
			socket.on("error", onError);
			socket.once("close", () => {
				socket.off("error", onError);
				this.upgradedSockets.delete(socket);
			});
			let route;
			try {
				/* v8 ignore next -- node:http always sets url on server requests. */
				route = this.upgrades.get(new URL(req.url ?? "/", "http://x").pathname);
			} catch (error) {
				this.ctx.logger.warn(error instanceof Error ? error : new Error(String(error)));
				socket.destroy();
				return;
			}
			if (route === void 0) {
				socket.destroy();
				return;
			}
			this.upgradedSockets.add(socket);
			try {
				Promise.resolve(route.handler(req, socket, head)).catch((error) => {
					this.ctx.logger.warn(error instanceof Error ? error : new Error(String(error)));
					socket.destroy();
				});
			} catch (error) {
				this.ctx.logger.warn(error instanceof Error ? error : new Error(String(error)));
				socket.destroy();
			}
		});
		await new Promise((resolve, reject) => {
			this.server.once("error", reject);
			this.server.listen(this.config.port, this.config.host, () => {
				this.server.off("error", reject);
				this.server.on("error", (err) => {
					this.ctx.logger.error(err);
				});
				this.listenedPort = this.server.address().port;
				resolve();
			});
		});
		this.ctx.effect(() => async () => {
			const serverClosed = new Promise((resolve) => {
				this.server.close(() => {
					resolve();
				});
			});
			this.server.closeAllConnections();
			const upgradedClosed = [...this.upgradedSockets].map((socket) => new Promise((resolve) => {
				socket.once("close", () => {
					resolve();
				});
				socket.destroy();
			}));
			await Promise.all([serverClosed, ...upgradedClosed]);
		}, "webServer.listen");
	}
	/** Longest-prefix-wins over the prefix table after an exact-table miss. */
	match(pathname) {
		const exact = this.exact.get(pathname);
		if (exact !== void 0) return exact;
		let best;
		for (const [prefix, route] of this.prefixes) {
			if (pathname !== prefix && !pathname.startsWith(`${prefix}/`)) continue;
			if (best === void 0 || prefix.length > best.path.length) best = route;
		}
		return best;
	}
	/**
	* Run an index.html body through the registered taps in registration order
	* — called by the fallback owner on every index response it renders.
	* @param html - the raw index.html body.
	* @returns the transformed body.
	*/
	applyIndexTaps(html) {
		let out = html;
		for (const transform of this.indexTaps) out = transform(out);
		return out;
	}
};
//#endregion
export { WebServer, WebServer as default };
