/// <reference types="@fastify/swagger" />
import { renderApiReference } from '@scalar/client-side-rendering';
import { normalize, toJson, toYaml } from '@scalar/openapi-parser';
import fp from 'fastify-plugin';
import { slug } from 'github-slugger';
import { getJavaScriptFile } from './utils/getJavaScriptFile.js';
/**
 * Path to the bundled Scalar JavaScript file
 */
const RELATIVE_JAVASCRIPT_PATH = 'js/scalar.js';
/**
 * This Schema is used to hide the route from the documentation.
 *
 * We don't know whether `@fastify/swagger` is registered, but it doesn't hurt to add a schema anyway.
 *
 * @see https://github.com/fastify/fastify-swagger#hide-a-route
 */
// Typed as FastifySchema so route inference uses FastifySchema (not the narrow
// literal { hide: boolean }), keeping hook handler types compatible across
// Fastify v4 and v5 even when @fastify/swagger augments FastifySchema with `hide`.
const schemaToHideRoute = {
    hide: true,
};
const getRoutePrefix = (routePrefix) => {
    const prefix = routePrefix ?? '/reference';
    // Remove trailing slash if present
    return prefix.endsWith('/') ? prefix.slice(0, -1) : prefix;
};
/**
 * Get the endpoints for the OpenAPI specification.
 */
const getOpenApiDocumentEndpoints = (openApiDocumentEndpoints) => {
    const { json = '/openapi.json', yaml = '/openapi.yaml' } = openApiDocumentEndpoints ?? {};
    return { json, yaml };
};
/**
 * Get the URL for the Scalar JavaScript file.
 */
const getJavaScriptUrl = (routePrefix) => `${getRoutePrefix(routePrefix)}/${RELATIVE_JAVASCRIPT_PATH}`.replace(/\/\//g, '/');
/**
 * The default configuration for Fastify
 */
const DEFAULT_CONFIGURATION = {
    _integration: 'fastify',
};
const fastifyApiReference = fp((fastify, options, next) => {
    const { configuration: givenConfiguration } = options;
    // Merge the defaults
    let configuration = {
        ...DEFAULT_CONFIGURATION,
        ...givenConfiguration,
    };
    const specSource = (() => {
        const { content, url } = configuration ?? {};
        if (content) {
            return {
                type: 'content',
                get: () => {
                    if (typeof content === 'function') {
                        return content();
                    }
                    return content;
                },
            };
        }
        if (url) {
            return {
                type: 'url',
                get: () => url,
            };
        }
        // Even if @fastify/swagger is loaded, when the `decorator` option is set, the `swagger` function is not available.
        if (fastify.hasPlugin('@fastify/swagger') && typeof fastify.swagger === 'function') {
            return {
                type: 'swagger',
                get: () => fastify.swagger(),
            };
        }
        return void 0;
    })();
    // If no OpenAPI specification is passed and @fastify/swagger isn't loaded, show a warning.
    if (!specSource && !configuration.sources) {
        fastify.log.warn("[@scalar/fastify-api-reference] You didn't provide a `content`, `url`, `sources` or @fastify/swagger could not be found. Please provide one of these options.");
        return next();
    }
    // Read the JavaScript file once.
    const fileContent = getJavaScriptFile();
    const hooks = {};
    if (options.hooks) {
        const additionalHooks = ['onRequest', 'preHandler'];
        for (const hook of additionalHooks) {
            if (options.hooks[hook]) {
                hooks[hook] = options.hooks[hook];
            }
        }
    }
    const getSpecFilenameSlug = (spec) => {
        // Same GitHub Slugger and default file name as in `@scalar/api-reference`, when generating the download
        return slug(spec?.specification?.info?.title ?? 'spec');
    };
    // Only expose the endpoints if specSource is available
    if (specSource) {
        const openApiSpecUrlJson = `${getRoutePrefix(options.routePrefix)}${getOpenApiDocumentEndpoints(options.openApiDocumentEndpoints).json}`;
        fastify.route({
            method: 'GET',
            url: openApiSpecUrlJson,
            schema: schemaToHideRoute,
            ...hooks,
            ...(options.logLevel && { logLevel: options.logLevel }),
            handler(_, reply) {
                const spec = normalize(specSource.get());
                const filename = getSpecFilenameSlug(spec);
                const json = JSON.parse(toJson(spec)); // parsing minifies the JSON
                return reply
                    .header('Content-Type', 'application/json')
                    .header('Content-Disposition', `filename=${filename}.json`)
                    .header('Access-Control-Allow-Origin', '*')
                    .header('Access-Control-Allow-Methods', '*')
                    .send(json);
            },
        });
        const openApiSpecUrlYaml = `${getRoutePrefix(options.routePrefix)}${getOpenApiDocumentEndpoints(options.openApiDocumentEndpoints).yaml}`;
        fastify.route({
            method: 'GET',
            url: openApiSpecUrlYaml,
            schema: schemaToHideRoute,
            ...hooks,
            ...(options.logLevel && { logLevel: options.logLevel }),
            handler(_, reply) {
                const spec = normalize(specSource.get());
                const filename = getSpecFilenameSlug(spec);
                const yaml = toYaml(spec);
                return reply
                    .header('Content-Type', 'application/yaml')
                    .header('Content-Disposition', `filename=${filename}.yaml`)
                    .header('Access-Control-Allow-Origin', '*')
                    .header('Access-Control-Allow-Methods', '*')
                    .send(yaml);
            },
        });
    }
    // Redirect route without a trailing slash to force a trailing slash:
    // We need this so the request to the JS file is relative.
    // With ignoreTrailingSlash: true, fastify responds to both routes anyway.
    const ignoreTrailingSlash = 
    // @ts-expect-error We're still on Fastify 4, this is introduced in Fastify 5
    fastify.initialConfig?.routerOptions?.ignoreTrailingSlash === true ||
        fastify.initialConfig?.ignoreTrailingSlash === true;
    if (!ignoreTrailingSlash && getRoutePrefix(options.routePrefix)) {
        fastify.route({
            method: 'GET',
            url: getRoutePrefix(options.routePrefix),
            schema: schemaToHideRoute,
            ...hooks,
            ...(options.logLevel && { logLevel: options.logLevel }),
            handler(request, reply) {
                // we are in a route without a trailing slash so redirect directly to the one with a trailing slash
                const currentUrl = new URL(request.url, `${request.protocol}://${request.hostname}`);
                return reply.redirect(`${currentUrl.pathname}/`, 301);
            },
        });
    }
    // If no theme is passed, use the default theme.
    fastify.route({
        method: 'GET',
        url: `${getRoutePrefix(options.routePrefix)}/`,
        // We don't know whether @fastify/swagger is registered, but it doesn't hurt to add a schema anyway.
        schema: schemaToHideRoute,
        ...hooks,
        ...(options.logLevel && { logLevel: options.logLevel }),
        handler(request, reply) {
            // Redirect if it's the route without a slash
            const currentUrl = new URL(request.url, `${request.protocol}://${request.hostname}`);
            if (!currentUrl.pathname.endsWith('/')) {
                return reply.redirect(`${currentUrl.pathname}/`, 301);
            }
            /**
             * Regardless of where we source the spec from, provide it as a URL, to have the
             * download button point to the exposed endpoint.
             * If the URL is explicitly passed, defer to that URL instead.
             */
            if (specSource && specSource.type !== 'url') {
                configuration = {
                    ...configuration,
                    // Use a relative URL in case we're proxied
                    url: `.${getOpenApiDocumentEndpoints(options.openApiDocumentEndpoints).json}`,
                };
            }
            // Respond with the HTML document
            const { cdn, pageTitle, nonce, ...config } = configuration;
            return reply.header('Content-Type', 'text/html; charset=utf-8').send(renderApiReference({
                config,
                // We're using the bundled JS here by default, but the user can pass a CDN URL.
                cdn: cdn ?? RELATIVE_JAVASCRIPT_PATH,
                pageTitle,
                nonce,
            }));
        },
    });
    fastify.route({
        method: 'GET',
        url: getJavaScriptUrl(options.routePrefix),
        // We don't know whether @fastify/swagger is registered, but it doesn't hurt to add a schema anyway.
        schema: schemaToHideRoute,
        ...hooks,
        ...(options.logLevel && { logLevel: options.logLevel }),
        handler(_, reply) {
            return reply.header('Content-Type', 'application/javascript; charset=utf-8').send(fileContent);
        },
    });
    next();
}, {
    name: '@scalar/fastify-api-reference',
});
export default fastifyApiReference;
