import { version } from "../package.json"
import { devMsg } from "./lib/devMsg"

/** The query parameter used to indicate if the client is in development mode to the API. */
const PRISMIC_DEV_PARAM = "x-d"

/** The query parameter used to indicate the version of the client to the API. */
const PRISMIC_CLIENT_VERSION_PARAM = "x-c"

/**
 * Create a union of the given object's values, and optionally specify which keys to get the values
 * from.
 *
 * Taken from the `type-fest` package.
 *
 * See:
 * https://github.com/sindresorhus/type-fest/blob/61c35052f09caa23de5eef96d95196375d8ed498/source/value-of.d.ts
 */
type ValueOf<
	ObjectType,
	ValueType extends keyof ObjectType = keyof ObjectType,
> = ObjectType[ValueType]

/**
 * An `orderings` parameter that orders the results by the specified field.
 *
 * {@link https://prismic.io/docs/rest-api-technical-reference#orderings}
 */
export interface Ordering {
	field: string
	direction?: "asc" | "desc"
}

/**
 * A `routes` parameter that determines how a page's URL field is resolved.
 *
 * {@link https://prismic.io/docs/route-resolver}
 *
 * @example
 * 	With a page's UID field.
 *
 * 	```ts
 * 	{
 * 	"type": "page",
 * 	"path": "/:uid"
 * 	}
 * 	```
 *
 * @example
 * 	With a Content Relationship `parent` field.
 *
 * 	```ts
 * 	{
 * 	"type": "page",
 * 	"path": "/:parent?/:uid",
 * 	"resolvers": {
 * 	"parent": "parent"
 * 	}
 * 	}
 * 	```
 */
export interface Route {
	/** The custom type of the page. */
	type: string

	/**
	 * A specific UID to which this route definition is scoped. The route is only defined for the page
	 * whose UID matches the given UID.
	 */
	uid?: string

	/**
	 * A specific language to which this route definition is scoped. The route is only defined for
	 * pages whose language matches the given language.
	 */
	lang?: string

	/** The resolved path of the page with optional placeholders. */
	path: string

	/** An object that lists the API IDs of the Content Relationships in the route. */
	resolvers?: Record<string, string>
}

/**
 * Parameters for the Prismic Content API.
 *
 * @see Learn how to fetch content from Prismic: {@link https://prismic.io/docs/fetch-content}
 */
export interface QueryParams {
	/**
	 * The secure token for accessing the API (only needed if your repository is set to private).
	 *
	 * {@link https://prismic.io/docs/access-token}
	 */
	accessToken?: string

	/**
	 * The `pageSize` parameter defines the maximum number of pages that the API will return for your
	 * query.
	 *
	 * {@link https://prismic.io/docs/rest-api-technical-reference#pagesize}
	 */
	pageSize?: number

	/**
	 * The `page` parameter defines the pagination for the result of your query.
	 *
	 * {@link https://prismic.io/docs/rest-api-technical-reference#page}
	 */
	page?: number

	/**
	 * The `after` parameter can be used along with the orderings option. It will remove all the pages
	 * except for those after the specified page in the list.
	 *
	 * {@link https://prismic.io/docs/rest-api-technical-reference#after}
	 */
	after?: string

	/**
	 * The `fetch` parameter is used to make queries faster by only retrieving the specified field(s).
	 *
	 * {@link https://prismic.io/docs/rest-api-technical-reference#fetch}
	 */
	fetch?: string | string[]

	/**
	 * The `fetchLinks` parameter allows you to retrieve a specific content field from a linked page
	 * and add it to the page response object.
	 *
	 * {@link https://prismic.io/docs/rest-api-technical-reference#fetchlinks}
	 */
	fetchLinks?: string | string[]

	/**
	 * The `graphQuery` parameter allows you to specify which fields to retrieve and what content to
	 * retrieve from Linked Documents / Content Relationships.
	 *
	 * {@link https://prismic.io/docs/graphquery-rest-api}
	 */
	graphQuery?: string

	/**
	 * The `lang` option defines the language code for the results of your query.
	 *
	 * {@link https://prismic.io/docs/rest-api-technical-reference#lang}
	 */
	lang?: string

	/**
	 * The `orderings` parameter orders the results by the specified field(s). You can specify as many
	 * fields as you want.
	 *
	 * {@link https://prismic.io/docs/rest-api-technical-reference#orderings}
	 *
	 * @remarks
	 *   Strings and arrays of strings are deprecated as of `@prismicio/client@7.0.0`. Please migrate
	 *   to the more explicit array of objects.
	 * @example
	 * 	;```typescript
	 * 	buildQueryURL(endpoint, {
	 * 		orderings: [
	 * 			{ field: "my.product.price", direction: "desc" },
	 * 			{ field: "my.product.title" },
	 * 		],
	 * 	})
	 * 	```
	 */
	// TODO: Update TSDoc with deprecated API removal in v8
	orderings?: string | Ordering | (string | Ordering)[]

	/**
	 * The `routes` option allows you to define how a page's `url` field is resolved.
	 *
	 * {@link https://prismic.io/docs/route-resolver}
	 */
	routes?: Route | string | (Route | string)[]

	/**
	 * The `brokenRoute` option allows you to define the route populated in the `url` property for
	 * broken link or content relationship fields. A broken link is a link or content relationship
	 * field whose linked page has been unpublished or deleted.
	 *
	 * {@link https://prismic.io/docs/route-resolver}
	 */
	brokenRoute?: string
}

/** Arguments for `buildQueryURL` to construct a Query URL. */
type BuildQueryURLParams = {
	/**
	 * Ref used to query documents.
	 *
	 * {@link https://prismic.io/docs/api#refs-and-the-entry-api}
	 */
	ref: string

	/**
	 * Ref used to populate integration fields with the latest content.
	 *
	 * {@link https://prismic.io/docs/integration-fields}
	 */
	integrationFieldsRef?: string

	/**
	 * One or more filters to filter documents for the query.
	 *
	 * {@link https://prismic.io/docs/rest-api-technical-reference#q}
	 */
	filters?: string | string[]

	/**
	 * @deprecated Renamed to `filters`. Ensure the value is an array of filters,
	 *   not a single, non-array filter.
	 */
	predicates?: string | string[]
}

/**
 * Parameters in this map have been renamed from the official Prismic REST API V2 specification for
 * better developer ergonomics.
 *
 * These parameters are renamed to their mapped value.
 */
const RENAMED_PARAMS = {
	accessToken: "access_token",
} as const

/** A valid parameter name for the Prismic REST API V2. */
type ValidParamName =
	| Exclude<keyof QueryParams, keyof typeof RENAMED_PARAMS | keyof BuildQueryURLParams>
	| ValueOf<typeof RENAMED_PARAMS>

/**
 * Converts an Ordering to a string that is compatible with Prismic's REST API. If the value
 * provided is already a string, no conversion is performed.
 *
 * @param ordering - Ordering to convert.
 * @returns String representation of the Ordering.
 */
const castOrderingToString = (ordering: Ordering | string): string => {
	// TODO: Remove the following when `orderings` strings are no longer supported.
	if (typeof ordering === "string") {
		if (process.env.NODE_ENV === "development") {
			const [field, direction] = ordering.split(" ")

			const objectForm =
				direction === "desc" ? `{ field: "${field}", direction: "desc" }` : `{ field: "${field}" }`

			console.warn(
				`[@prismicio/client] A string value was provided to the \`orderings\` query parameter. Strings are deprecated. Please convert it to the object form: ${objectForm}. For more details, see ${devMsg(
					"orderings-must-be-an-array-of-objects",
				)}`,
			)
		}

		return ordering
	}

	return ordering.direction === "desc" ? `${ordering.field} desc` : ordering.field
}

export type BuildQueryURLArgs = QueryParams & BuildQueryURLParams

/**
 * Builds a Prismic Content API URL to request pages from a repository. The paginated response for
 * this URL includes pages matching the parameters.
 *
 * A ref is required to make a request. Request the `endpoint` URL to retrieve a list of available
 * refs.
 *
 * Type the JSON response with `Query`.
 *
 * @example
 * 	;```ts
 * 	const url = buildQueryURL("https://my-repo.cdn.prismic.io/api/v2", {
 * 		ref: "my-ref",
 * 		filters: [filter.at("document.type", "blog_post")],
 * 	})
 * 	```
 *
 * @param endpoint - URL to the repository's Content API.
 * @param args - Arguments to filter and scope the query.
 * @returns URL that can be used to request pages from the repository.
 * @see Prismic Content API technical reference: {@link https://prismic.io/docs/content-api}
 */
export const buildQueryURL = (endpoint: string, args: BuildQueryURLArgs): string => {
	const { filters, predicates, ...params } = args

	if (!endpoint.endsWith("/")) {
		endpoint += "/"
	}
	const url = new URL(`documents/search`, endpoint)

	if (filters) {
		// TODO: Remove warning when we remove support for string `filters` values.
		if (process.env.NODE_ENV === "development" && !Array.isArray(filters)) {
			console.warn(
				`[@prismicio/client] A non-array value was provided to the \`filters\` query parameter (\`${filters}\`). Non-array values are deprecated. Please convert it to an array. For more details, see ${devMsg(
					"filters-must-be-an-array",
				)}`,
			)
		}

		// TODO: Remove `castArray` when we remove support for string `filters` values.
		for (const filter of castArray(filters)) {
			url.searchParams.append("q", `[${filter}]`)
		}
	}

	// TODO: Remove when we remove support for deprecated `predicates` argument.
	if (predicates) {
		for (const predicate of castArray(predicates)) {
			url.searchParams.append("q", `[${predicate}]`)
		}
	}

	// Iterate over each parameter and add it to the URL. In some cases, the
	// parameter value needs to be transformed to fit the REST API.
	for (const k in params) {
		const name = (RENAMED_PARAMS[k as keyof typeof RENAMED_PARAMS] || k) as ValidParamName

		let value = params[k as keyof typeof params]

		if (name === "orderings") {
			const scopedValue = params[name]

			if (scopedValue != null) {
				// TODO: Remove the following warning when `orderings` strings are no longer supported.
				if (process.env.NODE_ENV === "development" && typeof scopedValue === "string") {
					console.warn(
						`[@prismicio/client] A string value was provided to the \`orderings\` query parameter. Strings are deprecated. Please convert it to an array of objects. For more details, see ${devMsg(
							"orderings-must-be-an-array-of-objects",
						)}`,
					)
				}

				const v = castArray(scopedValue)
					.map((ordering) => castOrderingToString(ordering))
					.join(",")

				value = `[${v}]`
			}
		} else if (name === "routes") {
			if (typeof params[name] === "object") {
				value = JSON.stringify(castArray(params[name]))
			}
		}

		if (value != null) {
			url.searchParams.set(name, castArray<string | number | Route | Ordering>(value).join(","))
		}
	}

	url.searchParams.set(PRISMIC_CLIENT_VERSION_PARAM, `js-${version}`)

	if (process.env.NODE_ENV === "development") {
		url.searchParams.set(PRISMIC_DEV_PARAM, "1")
	}

	return url.toString()
}

function castArray<A>(a: A | A[]): A[] {
	return Array.isArray(a) ? a : [a]
}
