/**
 * The Activity search parameters, validated as pure functions so the route's
 * `validateSearch` stays a one-liner and every rejection is testable without a
 * router.
 *
 * A search value arrives from a URL, from history state, or from a hand-typed
 * link, so nothing about its type is given. Anything that is not exactly the
 * value the API can filter on becomes `undefined`: an unbounded page is honest,
 * a page filtered on a coerced guess is not.
 */

import type { QueriesFilter } from "@/lib/types";

export const ACTIVITY_MODES = ["history", "live"] as const;
export type ActivityMode = (typeof ACTIVITY_MODES)[number];

export interface ActivitySearch {
	mode: ActivityMode;
	since: number | undefined;
	until: number | undefined;
	domain: string | undefined;
	client: string | undefined;
	blocked: boolean | undefined;
}

/** History is the surface a bare `/activity` should open on: it answers questions. */
export function validateMode(value: unknown): ActivityMode {
	return value === "live" ? "live" : "history";
}

/**
 * A unix-second bound. `Number.isSafeInteger` is the whole test: it rejects a
 * fraction, an infinity, a NaN and a magnitude past 2^53 in one step, and a
 * string never passes, so `?since=now` cannot reach the API as garbage.
 */
export function validateTimestamp(value: unknown): number | undefined {
	return Number.isSafeInteger(value) ? (value as number) : undefined;
}

/**
 * The blocked filter. `false` is a real filter — "allowed only" — so it must
 * survive; only a genuine boolean does, because `"false"` out of a URL parser
 * that did not decode JSON would otherwise read as true.
 */
export function validateBlocked(value: unknown): boolean | undefined {
	return typeof value === "boolean" ? value : undefined;
}

/**
 * A text filter, trimmed. An empty result becomes `undefined` rather than `""`:
 * the server treats an empty filter as no filter, and a URL that showed
 * `domain=` as applied state would claim a filter that is not filtering.
 */
export function validateText(value: unknown): string | undefined {
	if (typeof value !== "string") return undefined;
	const trimmed = value.trim();
	return trimmed === "" ? undefined : trimmed;
}

/**
 * The API filter for a validated search, built field by field.
 *
 * Only the fields that are actually set are written, so an unfiltered request
 * carries no keys at all: `GET /api/queries` rejects a parameter it does not
 * know, and the infinite query's cache key is the filter object, so a key
 * present-but-undefined and a key absent must not be two different windows onto
 * the same rows. `mode` never appears — it selects the surface, not the rows.
 */
export function queriesFilterOf(search: Omit<ActivitySearch, "mode">): QueriesFilter {
	const filter: QueriesFilter = {};
	if (search.domain !== undefined) filter.domain = search.domain;
	if (search.client !== undefined) filter.client = search.client;
	if (search.blocked !== undefined) filter.blocked = search.blocked;
	if (search.since !== undefined) filter.since = search.since;
	if (search.until !== undefined) filter.until = search.until;
	return filter;
}

export function validateActivitySearch(search: Record<string, unknown>): ActivitySearch {
	return {
		mode: validateMode(search["mode"]),
		since: validateTimestamp(search["since"]),
		until: validateTimestamp(search["until"]),
		domain: validateText(search["domain"]),
		client: validateText(search["client"]),
		blocked: validateBlocked(search["blocked"]),
	};
}
