import chalk from "./chalk";
import { escapeStringRegexp } from "../shared/lib/escape-regexp";
import { tryToParsePath } from "./try-to-parse-path";
import { allowedStatusCodes } from "./redirect-status";
const allowedHasTypes = new Set([
    "header",
    "cookie",
    "query",
    "host"
]);
const namedGroupsRegex = /\(\?<([a-zA-Z][a-zA-Z0-9]*)>/g;
export function normalizeRouteRegex(regex) {
    // clean up un-necessary escaping from regex.source which turns / into \\/
    return regex.replace(/\\\//g, "/");
}
function checkRedirect(route) {
    const invalidParts = [];
    let hadInvalidStatus = false;
    if (route.statusCode && !allowedStatusCodes.has(route.statusCode)) {
        hadInvalidStatus = true;
        invalidParts.push(`\`statusCode\` is not undefined or valid statusCode`);
    }
    if (typeof route.permanent !== "boolean" && !route.statusCode) {
        invalidParts.push(`\`permanent\` is not set to \`true\` or \`false\``);
    }
    return {
        invalidParts,
        hadInvalidStatus
    };
}
function checkHeader(route) {
    const invalidParts = [];
    if (!Array.isArray(route.headers)) {
        invalidParts.push("`headers` field must be an array");
    } else if (route.headers.length === 0) {
        invalidParts.push("`headers` field cannot be empty");
    } else {
        for (const header of route.headers){
            if (!header || typeof header !== "object") {
                invalidParts.push("`headers` items must be object with { key: '', value: '' }");
                break;
            }
            if (typeof header.key !== "string") {
                invalidParts.push("`key` in header item must be string");
                break;
            }
            if (typeof header.value !== "string") {
                invalidParts.push("`value` in header item must be string");
                break;
            }
        }
    }
    return invalidParts;
}
export function checkCustomRoutes(routes, type) {
    if (!Array.isArray(routes)) {
        console.error(`Error: ${type}s must return an array, received ${typeof routes}.\n` + `See here for more info: https://nextjs.org/docs/messages/routes-must-be-array`);
        process.exit(1);
    }
    let numInvalidRoutes = 0;
    let hadInvalidStatus = false;
    let hadInvalidHas = false;
    let hadInvalidMissing = false;
    const allowedKeys = new Set([
        "source",
        "locale",
        "has",
        "missing"
    ]);
    if (type === "rewrite") {
        allowedKeys.add("basePath");
        allowedKeys.add("destination");
    }
    if (type === "redirect") {
        allowedKeys.add("basePath");
        allowedKeys.add("statusCode");
        allowedKeys.add("permanent");
        allowedKeys.add("destination");
    }
    if (type === "header") {
        allowedKeys.add("basePath");
        allowedKeys.add("headers");
    }
    for (const route of routes){
        if (!route || typeof route !== "object") {
            console.error(`The route ${JSON.stringify(route)} is not a valid object with \`source\`${type !== "middleware" ? ` and \`${type === "header" ? "headers" : "destination"}\`` : ""}`);
            numInvalidRoutes++;
            continue;
        }
        if (type === "rewrite" && route.basePath === false && !(route.destination.startsWith("http://") || route.destination.startsWith("https://"))) {
            console.error(`The route ${route.source} rewrites urls outside of the basePath. Please use a destination that starts with \`http://\` or \`https://\` https://nextjs.org/docs/messages/invalid-external-rewrite`);
            numInvalidRoutes++;
            continue;
        }
        const keys = Object.keys(route);
        const invalidKeys = keys.filter((key)=>!allowedKeys.has(key));
        const invalidParts = [];
        if ("basePath" in route && typeof route.basePath !== "undefined" && route.basePath !== false) {
            invalidParts.push("`basePath` must be undefined or false");
        }
        if (typeof route.locale !== "undefined" && route.locale !== false) {
            invalidParts.push("`locale` must be undefined or false");
        }
        const checkInvalidHasMissing = (items, fieldName)=>{
            let hadInvalidItem = false;
            if (typeof items !== "undefined" && !Array.isArray(items)) {
                invalidParts.push(`\`${fieldName}\` must be undefined or valid has object`);
                hadInvalidItem = true;
            } else if (items) {
                const invalidHasItems = [];
                for (const hasItem of items){
                    let invalidHasParts = [];
                    if (!allowedHasTypes.has(hasItem.type)) {
                        invalidHasParts.push(`invalid type "${hasItem.type}"`);
                    }
                    if (typeof hasItem.key !== "string" && hasItem.type !== "host") {
                        invalidHasParts.push(`invalid key "${hasItem.key}"`);
                    }
                    if (typeof hasItem.value !== "undefined" && typeof hasItem.value !== "string") {
                        invalidHasParts.push(`invalid value "${hasItem.value}"`);
                    }
                    if (typeof hasItem.value === "undefined" && hasItem.type === "host") {
                        invalidHasParts.push(`value is required for "host" type`);
                    }
                    if (invalidHasParts.length > 0) {
                        invalidHasItems.push(`${invalidHasParts.join(", ")} for ${JSON.stringify(hasItem)}`);
                    }
                }
                if (invalidHasItems.length > 0) {
                    hadInvalidItem = true;
                    const itemStr = `item${invalidHasItems.length === 1 ? "" : "s"}`;
                    console.error(`Invalid \`${fieldName}\` ${itemStr}:\n` + invalidHasItems.join("\n"));
                    console.error();
                    invalidParts.push(`invalid \`${fieldName}\` ${itemStr} found`);
                }
            }
            return hadInvalidItem;
        };
        if (checkInvalidHasMissing(route.has, "has")) {
            hadInvalidHas = true;
        }
        if (checkInvalidHasMissing(route.missing, "missing")) {
            hadInvalidMissing = true;
        }
        if (!route.source) {
            invalidParts.push("`source` is missing");
        } else if (typeof route.source !== "string") {
            invalidParts.push("`source` is not a string");
        } else if (!route.source.startsWith("/")) {
            invalidParts.push("`source` does not start with /");
        }
        if (type === "header") {
            invalidParts.push(...checkHeader(route));
        } else if (type !== "middleware") {
            let _route = route;
            if (!_route.destination) {
                invalidParts.push("`destination` is missing");
            } else if (typeof _route.destination !== "string") {
                invalidParts.push("`destination` is not a string");
            } else if (type === "rewrite" && !_route.destination.match(/^(\/|https:\/\/|http:\/\/)/)) {
                invalidParts.push("`destination` does not start with `/`, `http://`, or `https://`");
            }
        }
        if (type === "redirect") {
            const result = checkRedirect(route);
            hadInvalidStatus = hadInvalidStatus || result.hadInvalidStatus;
            invalidParts.push(...result.invalidParts);
        }
        let sourceTokens;
        if (typeof route.source === "string" && route.source.startsWith("/")) {
            // only show parse error if we didn't already show error
            // for not being a string
            const { tokens , error , regexStr  } = tryToParsePath(route.source);
            if (error) {
                invalidParts.push("`source` parse failed");
            }
            if (regexStr && regexStr.length > 4096) {
                invalidParts.push("`source` exceeds max built length of 4096");
            }
            sourceTokens = tokens;
        }
        const hasSegments = new Set();
        if (route.has) {
            for (const hasItem of route.has){
                if (!hasItem.value && hasItem.key) {
                    hasSegments.add(hasItem.key);
                }
                if (hasItem.value) {
                    for (const match of hasItem.value.matchAll(namedGroupsRegex)){
                        if (match[1]) {
                            hasSegments.add(match[1]);
                        }
                    }
                    if (hasItem.type === "host") {
                        hasSegments.add("host");
                    }
                }
            }
        }
        // make sure no unnamed patterns are attempted to be used in the
        // destination as this can cause confusion and is not allowed
        if (typeof route.destination === "string") {
            if (route.destination.startsWith("/") && Array.isArray(sourceTokens)) {
                const unnamedInDest = new Set();
                for (const token of sourceTokens){
                    if (typeof token === "object" && typeof token.name === "number") {
                        const unnamedIndex = new RegExp(`:${token.name}(?!\\d)`);
                        if (route.destination.match(unnamedIndex)) {
                            unnamedInDest.add(`:${token.name}`);
                        }
                    }
                }
                if (unnamedInDest.size > 0) {
                    invalidParts.push(`\`destination\` has unnamed params ${[
                        ...unnamedInDest
                    ].join(", ")}`);
                } else {
                    const { tokens: destTokens , regexStr: destRegexStr , error: destinationParseFailed ,  } = tryToParsePath(route.destination, {
                        handleUrl: true
                    });
                    if (destRegexStr && destRegexStr.length > 4096) {
                        invalidParts.push("`destination` exceeds max built length of 4096");
                    }
                    if (destinationParseFailed) {
                        invalidParts.push("`destination` parse failed");
                    } else {
                        const sourceSegments = new Set(sourceTokens.map((item)=>typeof item === "object" && item.name).filter(Boolean));
                        const invalidDestSegments = new Set();
                        for (const token of destTokens){
                            if (typeof token === "object" && !sourceSegments.has(token.name) && !hasSegments.has(token.name)) {
                                invalidDestSegments.add(token.name);
                            }
                        }
                        if (invalidDestSegments.size) {
                            invalidParts.push(`\`destination\` has segments not in \`source\` or \`has\` (${[
                                ...invalidDestSegments, 
                            ].join(", ")})`);
                        }
                    }
                }
            }
        }
        const hasInvalidKeys = invalidKeys.length > 0;
        const hasInvalidParts = invalidParts.length > 0;
        if (hasInvalidKeys || hasInvalidParts) {
            console.error(`${invalidParts.join(", ")}${invalidKeys.length ? (hasInvalidParts ? "," : "") + ` invalid field${invalidKeys.length === 1 ? "" : "s"}: ` + invalidKeys.join(",") : ""} for route ${JSON.stringify(route)}`);
            console.error();
            numInvalidRoutes++;
        }
    }
    if (numInvalidRoutes > 0) {
        if (hadInvalidStatus) {
            console.error(`\nValid redirect statusCode values are ${[
                ...allowedStatusCodes
            ].join(", ")}`);
        }
        if (hadInvalidHas) {
            console.error(`\nValid \`has\` object shape is ${JSON.stringify({
                type: [
                    ...allowedHasTypes
                ].join(", "),
                key: "the key to check for",
                value: "undefined or a value string to match against"
            }, null, 2)}`);
        }
        if (hadInvalidMissing) {
            console.error(`\nValid \`missing\` object shape is ${JSON.stringify({
                type: [
                    ...allowedHasTypes
                ].join(", "),
                key: "the key to check for",
                value: "undefined or a value string to match against"
            }, null, 2)}`);
        }
        console.error();
        console.error(`Error: Invalid ${type}${numInvalidRoutes === 1 ? "" : "s"} found`);
        process.exit(1);
    }
}
function processRoutes(routes, config, type) {
    const _routes = routes;
    const newRoutes = [];
    const defaultLocales = [];
    if (config.i18n && type === "redirect") {
        var ref;
        for (const item of ((ref = config.i18n) == null ? void 0 : ref.domains) || []){
            defaultLocales.push({
                locale: item.defaultLocale,
                base: `http${item.http ? "" : "s"}://${item.domain}`
            });
        }
        defaultLocales.push({
            locale: config.i18n.defaultLocale,
            base: ""
        });
    }
    for (const r of _routes){
        var ref1;
        const srcBasePath = config.basePath && r.basePath !== false ? config.basePath : "";
        const isExternal = !((ref1 = r.destination) == null ? void 0 : ref1.startsWith("/"));
        const destBasePath = srcBasePath && !isExternal ? srcBasePath : "";
        if (config.i18n && r.locale !== false) {
            var ref2;
            if (!isExternal) {
                defaultLocales.forEach((item)=>{
                    let destination;
                    if (r.destination) {
                        destination = item.base ? `${item.base}${destBasePath}${r.destination}` : `${destBasePath}${r.destination}`;
                    }
                    newRoutes.push({
                        ...r,
                        destination,
                        source: `${srcBasePath}/${item.locale}${r.source}`
                    });
                });
            }
            r.source = `/:nextInternalLocale(${config.i18n.locales.map((locale)=>escapeStringRegexp(locale)).join("|")})${r.source === "/" && !config.trailingSlash ? "" : r.source}`;
            if (r.destination && ((ref2 = r.destination) == null ? void 0 : ref2.startsWith("/"))) {
                r.destination = `/:nextInternalLocale${r.destination === "/" && !config.trailingSlash ? "" : r.destination}`;
            }
        }
        r.source = `${srcBasePath}${r.source === "/" && srcBasePath ? "" : r.source}`;
        if (r.destination) {
            r.destination = `${destBasePath}${r.destination === "/" && destBasePath ? "" : r.destination}`;
        }
        newRoutes.push(r);
    }
    return newRoutes;
}
async function loadRedirects(config) {
    if (typeof config.redirects !== "function") {
        return [];
    }
    let redirects = await config.redirects();
    // check before we process the routes and after to ensure
    // they are still valid
    checkCustomRoutes(redirects, "redirect");
    redirects = processRoutes(redirects, config, "redirect");
    checkCustomRoutes(redirects, "redirect");
    return redirects;
}
async function loadRewrites(config) {
    if (typeof config.rewrites !== "function") {
        return {
            beforeFiles: [],
            afterFiles: [],
            fallback: []
        };
    }
    const _rewrites = await config.rewrites();
    let beforeFiles = [];
    let afterFiles = [];
    let fallback = [];
    if (!Array.isArray(_rewrites) && typeof _rewrites === "object" && Object.keys(_rewrites).every((key)=>key === "beforeFiles" || key === "afterFiles" || key === "fallback")) {
        beforeFiles = _rewrites.beforeFiles || [];
        afterFiles = _rewrites.afterFiles || [];
        fallback = _rewrites.fallback || [];
    } else {
        afterFiles = _rewrites;
    }
    // check before we process the routes and after to ensure
    // they are still valid
    checkCustomRoutes(beforeFiles, "rewrite");
    checkCustomRoutes(afterFiles, "rewrite");
    checkCustomRoutes(fallback, "rewrite");
    beforeFiles = processRoutes(beforeFiles, config, "rewrite");
    afterFiles = processRoutes(afterFiles, config, "rewrite");
    fallback = processRoutes(fallback, config, "rewrite");
    checkCustomRoutes(beforeFiles, "rewrite");
    checkCustomRoutes(afterFiles, "rewrite");
    checkCustomRoutes(fallback, "rewrite");
    return {
        beforeFiles,
        afterFiles,
        fallback
    };
}
async function loadHeaders(config) {
    if (typeof config.headers !== "function") {
        return [];
    }
    let headers = await config.headers();
    // check before we process the routes and after to ensure
    // they are still valid
    checkCustomRoutes(headers, "header");
    headers = processRoutes(headers, config, "header");
    checkCustomRoutes(headers, "header");
    return headers;
}
export default async function loadCustomRoutes(config) {
    const [headers, rewrites, redirects] = await Promise.all([
        loadHeaders(config),
        loadRewrites(config),
        loadRedirects(config), 
    ]);
    const totalRewrites = rewrites.beforeFiles.length + rewrites.afterFiles.length + rewrites.fallback.length;
    const totalRoutes = headers.length + redirects.length + totalRewrites;
    if (totalRoutes > 1000) {
        console.warn(chalk.bold.yellow(`Warning: `) + `total number of custom routes exceeds 1000, this can reduce performance. Route counts:\n` + `headers: ${headers.length}\n` + `rewrites: ${totalRewrites}\n` + `redirects: ${redirects.length}\n` + `See more info: https://nextjs.org/docs/messages/max-custom-routes-reached`);
    }
    if (!config.skipTrailingSlashRedirect) {
        if (config.trailingSlash) {
            redirects.unshift({
                source: "/:file((?!\\.well-known(?:/.*)?)(?:[^/]+/)*[^/]+\\.\\w+)/",
                destination: "/:file",
                permanent: true,
                locale: config.i18n ? false : undefined,
                internal: true,
                // don't run this redirect for _next/data requests
                missing: [
                    {
                        type: "header",
                        key: "x-nextjs-data"
                    }, 
                ]
            }, {
                source: "/:notfile((?!\\.well-known(?:/.*)?)(?:[^/]+/)*[^/\\.]+)",
                destination: "/:notfile/",
                permanent: true,
                locale: config.i18n ? false : undefined,
                internal: true
            });
            if (config.basePath) {
                redirects.unshift({
                    source: config.basePath,
                    destination: config.basePath + "/",
                    permanent: true,
                    basePath: false,
                    locale: config.i18n ? false : undefined,
                    internal: true
                });
            }
        } else {
            redirects.unshift({
                source: "/:path+/",
                destination: "/:path+",
                permanent: true,
                locale: config.i18n ? false : undefined,
                internal: true
            });
            if (config.basePath) {
                redirects.unshift({
                    source: config.basePath + "/",
                    destination: config.basePath,
                    permanent: true,
                    basePath: false,
                    locale: config.i18n ? false : undefined,
                    internal: true
                });
            }
        }
    }
    return {
        headers,
        rewrites,
        redirects
    };
};

//# sourceMappingURL=load-custom-routes.js.map