import { isSome } from "@jet/environment";
import { URL } from "@jet/environment/util/urls";
import { Path, Parameters } from "../../foundation/network/url-constants";
import { normalizeLocale, deriveLocaleForUrl } from "../locale";
/**
 * Generate the routes property for a `RouteProvider` and a canonical URL factory
 * function for a {@link RoutableIntent} (non-detail page).
 *
 * `makeIntent` should be a function that accepts a single parameter,
 * `options: Omit<YourIntent, '$kind'>`, and returns the intent.
 *
 * `path` is the static part of the path. For example, in `'/us/browse?l=es'`,
 * `'/browse'` is the static part of the path. Path can contain multiple
 * segments like `'/foo/bar'`, but it does not support parameters like
 * `'/foo/{id}'`.
 *
 * `expectedQueryParameters` are the names of the query parameters for the routes
 * that also double as properties on the intent. This instructs the intent
 * builder on which properties to provide to `makeIntent`. For example,
 * `SearchResultsPageIntent`'s `RouteProvider` recognizes a `'term'` query param, which
 * is then stored in the intent as `{ term: 'valueFromParam' }`. For that case,
 * this array should be `['term']`. Put another way, this should be a list of the
 * non-optional (non-nullable, etc.) string properties on the intent:
 *
 * ```typescript
 * interface SearchResultsPageIntent extends RouteableIntent {
 *     $kind: 'SearchResultsPageIntent'; // not this one
 *     term: string; // this one should be included!
 *     facet: Opt<String>; // but it doesn't include this; it's optional
 * }
 * ```
 *
 * NOTE: If you receive a type error like `Type 'string' is not assignable
 * to type 'never'` for this argument, it means that your array of parameters
 * doesn't match the intent definition. In the above example, if we added a
 * `originalTerm: string` to the intent, then passing ['term'] would fail,
 * because 'originalTerm' is missing. Adding it does mean, however, that you
 * expect users to be able to pass it as a query parameter
 * (ex. /search?term=foo&originalTerm=bar). If that's unintended, you may want
 * to consider making this property Opt<> (and also maybe ?).
 *
 * Usage Example: For a contrived intent `FooIntent`,
 *
 * ```typescript
 * interface FooIntent extends RouteableIntent {
 *     $kind: 'FooIntent';
 *     bar: string;
 * }
 * ```
 *
 * If we expect users to be able to route to this intent via `/us/foo?bar=baz`
 * or `/foo?bar=baz&l=es`, then in the IntentController's file:
 *
 * ```typescript
 * const {
 *     routes,
 *     makeCanonicalUrl,
 * } = generateRoutes(makeFooIntent, '/foo', ['bar']);
 * ```
 *
 * And then, `routes` can be used in the `RouteProvider`:
 *
 * ```typescript
 * export const FooIntentController: RouteProvider<FooIntent> & ... = {
 *     routes,
 *
 *     // ...
 * };
 * ```
 *
 * And then in perform, when returning a `Page`, append the `canonicalURL`:
 *
 * ```typescript
 * async perform(intent: FooIntent, objectGraph: ObjectGraph): Promise<Page> {
 *     // ...
 *     return {
 *         ...page,
 *         canonicalURL: makeCanonicalUrl(intent),
 *     };
 * },
 * ```
 */
export function generateRoutes(makeIntent, path, requiredQueryParameters = [], extras) {
    const ruleExtras = { exclusions: extras === null || extras === void 0 ? void 0 : extras.exclusions };
    const queryParameters = requiredQueryParameters.slice();
    if (extras === null || extras === void 0 ? void 0 : extras.optionalQuery) {
        queryParameters.push(...extras.optionalQuery.map((p) => `${p}?`));
    }
    return {
        routes(objectGraph) {
            var _a;
            return [
                {
                    rules: [
                        {
                            ...ruleExtras,
                            path,
                            query: [`${Parameters.language}?`, ...queryParameters],
                        },
                        {
                            ...ruleExtras,
                            path: `/{${Path.storeFront}}${path}`,
                            query: [`${Parameters.language}?`, ...queryParameters],
                        },
                        ...((_a = extras === null || extras === void 0 ? void 0 : extras.extraRules) !== null && _a !== void 0 ? _a : []),
                    ],
                    handler(_, parameters) {
                        const { [Path.storeFront]: storefront, [Parameters.language]: language, ...rest } = parameters;
                        const typedRestParams = rest;
                        return makeIntent({
                            ...typedRestParams,
                            ...normalizeLocale(objectGraph, {
                                storefront,
                                language,
                            }),
                        });
                    },
                },
            ];
        },
        makeCanonicalUrl(objectGraph, intent) {
            const url = buildBaseUrl(objectGraph, path, intent);
            // Replace dynamic segments with `Intent` properties
            const pathComponents = url.pathComponents();
            for (const [i, pathComponent] of pathComponents.entries()) {
                if (pathComponent.startsWith("{") && pathComponent.endsWith("}")) {
                    const variableInPathComponent = pathComponent.substring(1, pathComponent.length - 1);
                    pathComponents[i] = intent[variableInPathComponent];
                }
            }
            url.set("pathname", "/" + pathComponents.join("/"));
            // Set query params
            const sortedQueryParameters = requiredQueryParameters.slice();
            // Sorted for consistent order
            sortedQueryParameters.sort((a, b) => a.localeCompare(b));
            for (const queryParameter of sortedQueryParameters) {
                if (!(queryParameter in intent)) {
                    throw new Error(`expected queryParmeters to contain: ${queryParameter}`);
                }
                const value = intent[queryParameter];
                // NOTE: TypeScript is unfortunately not able to see that
                // parameters[queryParameter] will always be string. We prove
                // this in the type. queryParameters is required to contain the
                // string names of all keys on I with a string value (see
                // RequiredIntentOptions<I>). This escape hatch is preferable
                // within this function as it means that the function is
                // externally typesafe. If a user forgets to pass in params,
                // there will be a type error.
                url.param(queryParameter, value);
            }
            if (extras === null || extras === void 0 ? void 0 : extras.optionalQuery) {
                const optionalParams = extras.optionalQuery.slice();
                optionalParams.sort((a, b) => a.localeCompare(b));
                optionalParams.forEach((query) => {
                    const value = intent[query];
                    if (value) {
                        url.param(query, value);
                    }
                });
            }
            return url.toString();
        },
    };
}
/**
 * Builds the basis for a "canonical URL" given a {@linkcode path} and {@linkcode locale}
 *
 * The locale data will be encoded into the resulting {@linkcode URL}; other "dynamic segments"
 * within {@linkcode path} are expected to be replaced separately
 */
function buildBaseUrl(objectGraph, path, locale) {
    const url = new URL("https://apps.apple.com");
    const { storefront, language } = deriveLocaleForUrl(objectGraph, locale);
    url.path(storefront);
    // Actual path (ex. browse in https://apps.apple.com/us/browse)
    // substr(1), because URL.path doesn't expect a leading slash
    url.path(path.substr(1));
    if (isSome(language)) {
        url.param("l", language);
    }
    return url;
}
//# sourceMappingURL=generate-routes.js.map