/**
 * 服务端渲染入口（构建期预渲染使用）。
 *
 * 为什么显式提供而非用 @react-router/dev 的默认入口：
 * 默认 entry.server.node.tsx 位于 @react-router/dev 包的嵌套 node_modules 中，pnpm 严格隔离下
 * 它无法从自身位置解析到 `react-router`（报 "Cannot find module 'react-router'"）。把同样的入口
 * 放到项目 src 下，其 import 从项目根 node_modules 正常解析，问题消除。
 *
 * 注意：ssr:false 时运行时无服务端渲染，但构建期仍用本入口把 root 与 prerender 路由渲染成静态 HTML。
 * 内容与官方默认模板一致，仅位置不同。
 */
import { PassThrough } from 'node:stream';

import { createReadableStreamFromReadable } from '@react-router/node';
import { isbot } from 'isbot';
import { renderToPipeableStream } from 'react-dom/server';
import { ServerRouter } from 'react-router';

import type { RenderToPipeableStreamOptions } from 'react-dom/server';
import type { AppLoadContext, EntryContext } from 'react-router';

export const streamTimeout = 5_000;

export default function handleRequest(
    request: Request,
    responseStatusCode: number,
    responseHeaders: Headers,
    routerContext: EntryContext,

    _loadContext: AppLoadContext
) {
    // https://httpwg.org/specs/rfc9110.html#HEAD
    if (request.method.toUpperCase() === 'HEAD') {
        return new Response(null, {
            status: responseStatusCode,
            headers: responseHeaders,
        });
    }

    return new Promise((resolve, reject) => {
        let shellRendered = false;
        const userAgent = request.headers.get('user-agent');

        // 爬虫请求与 SPA/静态生成需等全部内容就绪后再响应
        const readyOption: keyof RenderToPipeableStreamOptions =
            (userAgent && isbot(userAgent)) || routerContext.isSpaMode
                ? 'onAllReady'
                : 'onShellReady';

        let timeoutId: ReturnType<typeof setTimeout> | undefined = setTimeout(
            () => abort(),
            streamTimeout + 1000
        );

        const { pipe, abort } = renderToPipeableStream(
            <ServerRouter context={routerContext} url={request.url} />,
            {
                [readyOption]() {
                    shellRendered = true;
                    const body = new PassThrough({
                        final(callback) {
                            clearTimeout(timeoutId);
                            timeoutId = undefined;
                            callback();
                        },
                    });
                    const stream = createReadableStreamFromReadable(body);

                    responseHeaders.set('Content-Type', 'text/html');

                    pipe(body);

                    resolve(
                        new Response(stream, {
                            headers: responseHeaders,
                            status: responseStatusCode,
                        })
                    );
                },
                onShellError(error: unknown) {
                    reject(error);
                },
                onError(error: unknown) {
                    responseStatusCode = 500;
                    if (shellRendered) {
                        console.error(error);
                    }
                },
            }
        );
    });
}
