import { ImmutableRequest } from '../ImmutableRequest';
import { getRedirectRewriteLocation, isResponse, parseParams } from '../utils/matchers';
import { shouldRunMiddleware } from '../utils/middleware';
/** Internal errors class to indicate that the server has failed
 * @remarks
 * This should be thrown for unexpected errors, so they show up as crashes.
 * Typically malformed project structure, missing manifest, html or other files.
 */
export class ExpoError extends Error {
    constructor(message) {
        super(message);
        this.name = 'ExpoError';
    }
    static isExpoError(error) {
        return !!error && error instanceof ExpoError;
    }
}
function noopBeforeResponse(responseInit, _route) {
    return responseInit;
}
export function createRequestHandler({ getRoutesManifest, getHtml, getApiRoute, getMiddleware, getLoaderData, beforeErrorResponse = noopBeforeResponse, beforeResponse = noopBeforeResponse, beforeHTMLResponse = noopBeforeResponse, beforeAPIResponse = noopBeforeResponse, }) {
    let manifest = null;
    return async function handler(request) {
        manifest = await getRoutesManifest();
        return requestHandler(request, manifest);
    };
    async function requestHandler(incomingRequest, manifest) {
        if (!manifest) {
            // NOTE(@EvanBacon): Development error when Expo Router is not setup.
            // NOTE(@kitten): If the manifest is not found, we treat this as
            // an SSG deployment and do nothing
            return createResponse(null, null, 'Not found', {
                status: 404,
                headers: new Headers({
                    'Content-Type': 'text/plain',
                }),
            });
        }
        let request = incomingRequest;
        let url = new URL(request.url);
        if (manifest.middleware) {
            const middleware = await getMiddleware(manifest.middleware);
            if (shouldRunMiddleware(request, middleware)) {
                const middlewareResponse = await middleware.default(new ImmutableRequest(request));
                if (middlewareResponse instanceof Response) {
                    return middlewareResponse;
                }
                // If middleware returns undefined/void, continue to route matching
            }
        }
        if (manifest.redirects) {
            for (const route of manifest.redirects) {
                if (!route.namedRegex.test(url.pathname)) {
                    continue;
                }
                if (route.methods && !route.methods.includes(request.method)) {
                    continue;
                }
                return respondRedirect(url, request, route);
            }
        }
        if (manifest.rewrites) {
            for (const route of manifest.rewrites) {
                if (!route.namedRegex.test(url.pathname)) {
                    continue;
                }
                if (route.methods && !route.methods.includes(request.method)) {
                    continue;
                }
                // Replace URL and Request with rewrite target
                url = getRedirectRewriteLocation(url, request, route);
                request = new Request(url, request);
            }
        }
        // First, test static routes and loader data requests
        if (request.method === 'GET' || request.method === 'HEAD') {
            const isLoaderRequest = url.pathname.startsWith('/_expo/loaders/');
            const matchedPath = isLoaderRequest
                ? url.pathname.replace('/_expo/loaders', '').replace(/\/index$/, '/')
                : url.pathname;
            for (const route of manifest.htmlRoutes) {
                if (!route.namedRegex.test(matchedPath)) {
                    continue;
                }
                // Handle loader data requests for client-side navigation
                if (isLoaderRequest) {
                    if (!route.loader) {
                        continue; // Route matched but has no loader
                    }
                    // Create a request with the actual route path so `parseParams()` works correctly
                    // NOTE(@hassankhan): Relocate the request rewriting logic from here
                    url.pathname = matchedPath;
                    const loaderRequest = new Request(url, request);
                    return createResponseFrom('api', route, await getLoaderData(loaderRequest, route));
                }
                const html = await getHtml(request, route);
                return respondHTML(html, route);
            }
        }
        // Next, test API routes
        for (const route of manifest.apiRoutes) {
            if (!route.namedRegex.test(url.pathname)) {
                continue;
            }
            const mod = await getApiRoute(route);
            return await respondAPI(mod, request, route);
        }
        // Finally, test 404 routes
        if (request.method === 'GET' || request.method === 'HEAD') {
            for (const route of manifest.notFoundRoutes) {
                if (!route.namedRegex.test(url.pathname)) {
                    continue;
                }
                try {
                    const contents = await getHtml(request, route);
                    return respondNotFoundHTML(contents, route);
                }
                catch {
                    // NOTE(@krystofwoldrich): Should we show a dismissible RedBox in development?
                    // Handle missing/corrupted not found route files
                    continue;
                }
            }
        }
        // 404
        return createResponse(null, null, 'Not found', {
            status: 404,
            headers: new Headers({ 'Content-Type': 'text/plain' }),
        });
    }
    function createResponse(routeType = null, route, bodyInit, responseInit) {
        const originalStatus = responseInit.status;
        let callbackRoute;
        if (route && routeType) {
            route.type = routeType;
            callbackRoute = route;
        }
        else {
            callbackRoute = { type: null };
        }
        let modifiedResponseInit = responseInit;
        // Apply user-defined headers, if provided
        if (manifest?.headers) {
            for (const headerName in manifest.headers) {
                if (Array.isArray(manifest.headers[headerName])) {
                    for (const headerValue of manifest.headers[headerName]) {
                        modifiedResponseInit.headers.append(headerName, headerValue);
                    }
                }
                else if (manifest.headers[headerName] != null &&
                    !modifiedResponseInit.headers.has(headerName)) {
                    modifiedResponseInit.headers.set(headerName, manifest.headers[headerName]);
                }
            }
        }
        // Callback call order matters, general rule is to call more specific callbacks first.
        if (routeType === 'html') {
            modifiedResponseInit = beforeHTMLResponse(modifiedResponseInit, callbackRoute);
        }
        if (routeType === 'api') {
            modifiedResponseInit = beforeAPIResponse(modifiedResponseInit, callbackRoute);
        }
        // Second to last is error response callback
        if (typeof originalStatus === 'number' &&
            (originalStatus === 0 /* Response.error() */ || originalStatus > 399)) {
            modifiedResponseInit = beforeErrorResponse(modifiedResponseInit, callbackRoute);
        }
        // Generic before response callback last
        modifiedResponseInit = beforeResponse(modifiedResponseInit, callbackRoute);
        if (originalStatus === 0) {
            // Response.error() results in status 0, which will cause new Response() to fail.
            // We convert it to 500 only if originally 0, if cbs set the values to 0, we don't protect against it.
            modifiedResponseInit.status = 500;
        }
        return new Response(bodyInit, modifiedResponseInit);
    }
    function createResponseFrom(routeType = null, route, response) {
        const modifiedResponseInit = {
            headers: new Headers(response.headers),
            status: response.status,
            statusText: response.statusText,
            cf: response.cf,
            webSocket: response.webSocket,
        };
        return createResponse(routeType, route, response.body, modifiedResponseInit);
    }
    async function respondNotFoundHTML(html, route) {
        if (typeof html === 'string') {
            return createResponse('notFoundHtml', route, html, {
                status: 404,
                headers: new Headers({
                    'Content-Type': 'text/html',
                }),
            });
        }
        if (isResponse(html)) {
            // Only used for development errors
            return html;
        }
        throw new ExpoError(`HTML route file ${route.page}.html could not be loaded`);
    }
    async function respondAPI(mod, request, route) {
        if (!mod || typeof mod !== 'object') {
            throw new ExpoError(`API route module ${route.page} could not be loaded`);
        }
        if (isResponse(mod)) {
            // Only used for development API route bundling errors
            return mod;
        }
        const handler = mod[request.method];
        if (!handler || typeof handler !== 'function') {
            return createResponse('notAllowedApi', route, 'Method not allowed', {
                status: 405,
                headers: new Headers({
                    'Content-Type': 'text/plain',
                }),
            });
        }
        const params = parseParams(request, route);
        const response = await handler(request, params);
        if (!isResponse(response)) {
            throw new ExpoError(`API route ${request.method} handler ${route.page} resolved to a non-Response result`);
        }
        return createResponseFrom('api', route, response);
    }
    function respondHTML(html, route) {
        if (typeof html === 'string') {
            return createResponse('html', route, html, {
                status: 200,
                headers: new Headers({
                    'Content-Type': 'text/html',
                }),
            });
        }
        if (isResponse(html)) {
            // Only used for development error responses
            return html;
        }
        throw new ExpoError(`HTML route file ${route.page}.html could not be loaded`);
    }
    function respondRedirect(url, request, route) {
        // NOTE(@krystofwoldrich): @expo/server would not redirect when location was empty,
        // it would keep searching for match and eventually return 404. Worker redirects to origin.
        const target = getRedirectRewriteLocation(url, request, route);
        let status;
        if (request.method === 'GET' || request.method === 'HEAD') {
            status = route.permanent ? 301 : 302;
        }
        else {
            status = route.permanent ? 308 : 307;
        }
        return Response.redirect(target, status);
    }
}
//# sourceMappingURL=abstract.js.map