import http from "node:http";
import { AsyncLocalStorage } from "node:async_hooks";
import { createServer, } from "tiny-stdio-mcp-server";
import { PROTECTED_RESOURCE_METADATA_CACHE_CONTROL, PROTECTED_RESOURCE_METADATA_PATH, authorizeBearerRequest, } from "./auth.js";
import { StreamableHttpTransport, } from "./http-transport.js";
function normalizePath(path) {
    if (path.length === 0 || path === "/") {
        return "/mcp";
    }
    if (path.includes("?") || path.includes("#")) {
        throw new Error("path must not include a query or fragment");
    }
    const normalizedPath = path.startsWith("/") ? path : `/${path}`;
    return normalizedPath.length > 1 && normalizedPath.endsWith("/")
        ? normalizedPath.slice(0, -1)
        : normalizedPath;
}
function getProtectedResourceMetadataPaths(path) {
    if (path === "/") {
        return [PROTECTED_RESOURCE_METADATA_PATH];
    }
    return [`${PROTECTED_RESOURCE_METADATA_PATH}${path}`];
}
function formatHostnameForUrl(hostname) {
    if (hostname.includes(":") && !hostname.startsWith("[")) {
        return `[${hostname}]`;
    }
    return hostname;
}
function buildUrl(hostname, port, path) {
    const url = new URL("http://127.0.0.1");
    url.hostname = formatHostnameForUrl(hostname);
    url.port = String(port);
    url.pathname = path;
    url.search = "";
    url.hash = "";
    return url.toString();
}
function listen(server, port, hostname) {
    return new Promise((resolve, reject) => {
        const onError = (error) => {
            server.off("listening", onListening);
            reject(error);
        };
        const onListening = () => {
            server.off("error", onError);
            resolve();
        };
        server.once("error", onError);
        server.once("listening", onListening);
        server.listen(port, hostname);
    });
}
function closeServer(server) {
    if (!server.listening) {
        return Promise.resolve();
    }
    return new Promise((resolve, reject) => {
        server.close((error) => {
            if (error !== undefined) {
                reject(error);
                return;
            }
            resolve();
        });
        server.closeIdleConnections?.();
    });
}
function toUrlString(value) {
    return value instanceof URL ? value.toString() : value;
}
export function createProtectedResourceMetadataDocument(options) {
    return {
        resource: toUrlString(options.resource),
        authorization_servers: options.authorizationServers.map(toUrlString),
        ...(options.bearerMethodsSupported !== undefined
            ? {
                bearer_methods_supported: [...options.bearerMethodsSupported],
            }
            : {}),
        ...(options.scopesSupported !== undefined
            ? {
                scopes_supported: [...options.scopesSupported],
            }
            : {}),
    };
}
export function createHttpServer(options) {
    const requestContextStorage = new AsyncLocalStorage();
    const supportsSessions = !hasOwnProperty(options, "sessionIdGenerator") || options.sessionIdGenerator !== undefined;
    const server = createServer({
        ...options,
        supportNotifications: supportsSessions,
        supportResourceSubscriptions: supportsSessions,
    });
    const transport = new StreamableHttpTransport(server, options, async (req, callback) => requestContextStorage.run({
        request: req,
    }, callback));
    const protectedResourceMetadataBody = options.oauth === undefined
        ? undefined
        : JSON.stringify(createProtectedResourceMetadataDocument(options.oauth));
    const httpServer = server;
    const registerTool = server.tool.bind(server);
    const registerRichTool = server.registerTool.bind(server);
    const defaultContext = {
        request: {},
    };
    const authorizeHttpRequest = async (req, res, protectedResourcePath) => {
        if (options.oauth === undefined) {
            return true;
        }
        const authenticatedRequest = req;
        if (authenticatedRequest.auth !== undefined) {
            return true;
        }
        const authorization = await authorizeBearerRequest(authenticatedRequest, {
            ...options.oauth,
            protectedResourcePath,
            trustedProxy: options.trustedProxy,
        });
        if (!authorization.ok) {
            options.observability?.onEvent?.({
                type: "auth.failure",
                statusCode: authorization.statusCode,
                challenge: authorization.challenge,
                sessionId: Array.isArray(req.headers["mcp-session-id"])
                    ? req.headers["mcp-session-id"][0]
                    : req.headers["mcp-session-id"],
            });
            res.writeHead(authorization.statusCode, {
                "WWW-Authenticate": authorization.challenge,
                "X-Content-Type-Options": "nosniff",
                "Referrer-Policy": "no-referrer",
            });
            res.end();
            return false;
        }
        return true;
    };
    httpServer.tool = (name, description, inputSchema, handler, outputSchema) => {
        registerTool(name, description, inputSchema, (args) => handler(args, requestContextStorage.getStore() ?? defaultContext), outputSchema);
        return httpServer;
    };
    httpServer.registerTool = (definition, handler) => {
        registerRichTool(definition, (args) => handler(args, requestContextStorage.getStore() ?? defaultContext));
        return httpServer;
    };
    httpServer.listenHttp = async (listenOptions = {}) => {
        const { port = 0, hostname = "127.0.0.1", path: requestedPath = "/mcp", signal, requestTimeoutMs, headersTimeoutMs, keepAliveTimeoutMs, } = listenOptions;
        const path = normalizePath(requestedPath);
        const nodeServer = http.createServer(async (req, res) => {
            const requestUrl = new URL(req.url ?? "/", "http://127.0.0.1");
            try {
                if (protectedResourceMetadataBody !== undefined &&
                    req.method === "GET" &&
                    getProtectedResourceMetadataPaths(path).includes(requestUrl.pathname)) {
                    res.writeHead(200, {
                        "Cache-Control": PROTECTED_RESOURCE_METADATA_CACHE_CONTROL,
                        "Content-Type": "application/json; charset=utf-8",
                        "X-Content-Type-Options": "nosniff",
                        "Referrer-Policy": "no-referrer",
                    });
                    res.end(protectedResourceMetadataBody);
                    return;
                }
                if (requestUrl.pathname !== path) {
                    res.writeHead(404, {
                        "X-Content-Type-Options": "nosniff",
                        "Referrer-Policy": "no-referrer",
                    });
                    res.end();
                    return;
                }
                if (!(await authorizeHttpRequest(req, res, path))) {
                    return;
                }
                await transport.handleRequest(req, res);
            }
            catch {
                if (!res.headersSent) {
                    res.writeHead(500, {
                        "X-Content-Type-Options": "nosniff",
                        "Referrer-Policy": "no-referrer",
                    });
                }
                if (!res.writableEnded) {
                    res.end();
                }
            }
        });
        if (requestTimeoutMs !== undefined) {
            nodeServer.requestTimeout = requestTimeoutMs;
        }
        if (headersTimeoutMs !== undefined) {
            nodeServer.headersTimeout = headersTimeoutMs;
        }
        if (keepAliveTimeoutMs !== undefined) {
            nodeServer.keepAliveTimeout = keepAliveTimeoutMs;
        }
        await listen(nodeServer, port, hostname);
        const address = nodeServer.address();
        if (address === null || typeof address === "string") {
            throw new Error("Expected Node HTTP server to bind to a TCP port");
        }
        const resolvedPort = address.port;
        let closePromise;
        let removeAbortListener = () => undefined;
        const close = async () => {
            if (closePromise !== undefined) {
                return closePromise;
            }
            closePromise = (async () => {
                removeAbortListener();
                await transport.close();
                await closeServer(nodeServer);
            })().catch((error) => {
                closePromise = undefined;
                throw error;
            });
            return closePromise;
        };
        if (signal !== undefined) {
            const onAbort = () => {
                void close();
            };
            if (signal.aborted) {
                onAbort();
            }
            else {
                signal.addEventListener("abort", onAbort, { once: true });
                removeAbortListener = () => {
                    signal.removeEventListener("abort", onAbort);
                };
            }
        }
        return {
            url: buildUrl(hostname, resolvedPort, path),
            port: resolvedPort,
            close,
        };
    };
    httpServer.handleRequest = async (req, res) => {
        if (!(await authorizeHttpRequest(req, res, "/mcp"))) {
            return;
        }
        await transport.handleRequest(req, res);
    };
    return httpServer;
}
function hasOwnProperty(value, key) {
    return Object.prototype.hasOwnProperty.call(value, key);
}
