import escape from 'regexp.escape';
import { DuplicatePlaceholderError, InvalidRedirectsFileError, InvalidRedirectStatusCodeError, RedirectsFileTooLargeError } from "../errors.js";
import { errorToObject } from "./error-to-object.js";
/**
 * @see https://specs.ipfs.tech/http-gateways/web-redirects-file/#status
 */
const ALLOWED_STATUSES = [
    200, 301, 302, 303, 307, 308, 404, 410, 451
];
/**
 * @see https://specs.ipfs.tech/http-gateways/web-redirects-file/#max-file-size
 */
const MAX_REDIRECTS_FILE_SIZE = 65_536;
/**
 * Examine the passed redirects file and translate the passed path.
 *
 * If `undefined` is returned it means no redirect was necessary.
 * If a `string` is returned, an internal redirect to that path should be performed
 * If a `Response` is returned, the user has signalled they wish to process
 * redirects manually so it should be returned as a response
 */
export function applyRedirects(url, _redirects, options = {}) {
    try {
        // @see https://specs.ipfs.tech/http-gateways/web-redirects-file/#max-file-size
        if (_redirects.length > MAX_REDIRECTS_FILE_SIZE) {
            throw new RedirectsFileTooLargeError(`_redirects file size cannot exceed ${MAX_REDIRECTS_FILE_SIZE} bytes`);
        }
        const redirects = _redirects
            .split('\n')
            .filter(line => {
            line = line.trim();
            return line !== '' && !line.startsWith('#');
        })
            .map(line => {
            const [from, to, status] = line
                .split(/\s+/)
                .map(s => s.trim());
            if (status != null && status !== '' && !status.match(/^\d+$/)) {
                throw new InvalidRedirectsFileError('Invalid status code in _redirects file');
            }
            let statusCode = parseInt(status);
            if (isNaN(statusCode)) {
                // @see https://specs.ipfs.tech/http-gateways/web-redirects-file/#status
                statusCode = 301;
            }
            // @see https://specs.ipfs.tech/http-gateways/web-redirects-file/#status
            if (!ALLOWED_STATUSES.includes(statusCode)) {
                throw new InvalidRedirectStatusCodeError(`Status code ${statusCode} is not allowed`);
            }
            const placeholders = new Set();
            const fromParts = from.split('/');
            const fromPattern = fromParts.map((key, index) => {
                if (key.startsWith(':')) {
                    const placeholder = key.substring(1);
                    if (placeholders.has(placeholder)) {
                        // @see https://specs.ipfs.tech/http-gateways/web-redirects-file/#placeholders
                        throw new DuplicatePlaceholderError('Duplicate placeholders in from path');
                    }
                    placeholders.add(placeholder);
                    // @see https://specs.ipfs.tech/http-gateways/web-redirects-file/#placeholders
                    return `(?<${placeholder}>[^/]+)`;
                }
                if (key === '*' && index === (fromParts.length - 1)) {
                    // @see https://specs.ipfs.tech/http-gateways/web-redirects-file/#catch-all-splat
                    return '(?<splat>.+)';
                }
                // TODO: use RegExp.escape - requires Node.js 24+
                return escape(key);
            })
                .join('\\/');
            return {
                from: new RegExp(fromPattern),
                to,
                status: statusCode
            };
        });
        for (const { from, to, status } of redirects) {
            const match = url.pathname.match(from);
            if (match == null) {
                continue;
            }
            return toResult(url, to, status, match.groups, options);
        }
    }
    catch (err) {
        // must be a 500
        // @see https://specs.ipfs.tech/http-gateways/web-redirects-file/#error-handling
        return new Response(JSON.stringify(errorToObject(err), null, 2), {
            status: 500
        });
    }
}
function toResult(url, to, status, groups, options = {}) {
    // cannot continue with redirect set to 'error'
    if (options.redirect === 'error') {
        throw new TypeError('Failed to fetch');
    }
    if (groups != null) {
        // placeholders or splat present
        for (const [key, value] of Object.entries(groups)) {
            to = to.replaceAll(`:${key}`, value);
        }
    }
    // preserve query/hash/etc from original URL
    // @see https://specs.ipfs.tech/http-gateways/web-redirects-file/#query-parameters
    const search = createSearch(url, new URL(`${url.protocol}//${url.host}${to}`));
    const rawLocation = new URL(`${url.protocol}//${url.host}${to}`);
    const location = new URL(`${url.protocol}//${url.host}${rawLocation.pathname}${formatSearch(search)}${url.hash}`);
    if (options.redirect === 'manual') {
        return new Response('', {
            status,
            headers: {
                location: location.toString()
            }
        });
    }
    return location;
}
function createSearch(userUrl, redirectUrl) {
    const search = {};
    for (const [key, value] of userUrl.searchParams) {
        redirectUrl.searchParams.delete(key);
        addToSearch(key, value, search);
    }
    for (const [key, value] of redirectUrl.searchParams) {
        addToSearch(key, value, search);
    }
    return search;
}
function addToSearch(key, value, search) {
    if (typeof search[key] === 'string') {
        search[key] = [search[key]];
    }
    if (Array.isArray(search[key])) {
        search[key].push(value);
    }
    else {
        search[key] = value;
    }
}
/**
 * Turns a record of key/value pairs into a URL-safe search params string
 *
 * We cannot use the native URLSearchParams to encode as it uses
 * `application/x-www-form-urlencoded` encoding which encodes " " as "+" and
 * not "%20" so use encodeURIComponent instead
 */
export function formatSearch(params) {
    const search = [...Object.entries(params)]
        .map(([key, value]) => {
        if (!Array.isArray(value)) {
            value = [value];
        }
        return value
            .map(val => `${encodeURIComponent(key)}=${encodeURIComponent(val)}`)
            .join('&');
    })
        .join('&');
    if (search === '') {
        return search;
    }
    return `?${search}`;
}
//# sourceMappingURL=apply-redirect.js.map