export const SEARCH_SCOPE = Object.freeze({
	SINGLE: "single",
	MULTI: "multi",
	ALL: "all",
});

// Backend constraint (app/api/search/route.js): limit must be 50..200.
// We expose a strict allowed set in the UI to keep URLs predictable and avoid 400s.
export const SEARCH_LIMITS = Object.freeze([50, 100, 200]);
export const DEFAULT_SEARCH_LIMIT = 100;

/**
 * Read a query parameter from either:
 * - URLSearchParams (client-side `useSearchParams()`)
 * - Next.js server `searchParams` object (plain object with string or string[])
 *
 * @param {any} searchParams
 * @param {string} key
 * @returns {string|null}
 */
function readParam(searchParams, key) {
	if (!searchParams) return null;

	// URLSearchParams-like object: has .get()
	if (typeof searchParams.get === "function") {
		const value = searchParams.get(key);
		return typeof value === "string" ? value : null;
	}

	// Next.js server searchParams: plain object
	const raw = searchParams[key];

	if (Array.isArray(raw)) {
		return typeof raw[0] === "string" ? raw[0] : null;
	}

	return typeof raw === "string" ? raw : null;
}

function normalizeTrimmedOrNull(value) {
	if (typeof value !== "string") return null;
	const s = value.trim();
	return s ? s : null;
}

function normalizeBranchId(value) {
	if (typeof value !== "string") return null;

	const trimmed = value.trim();
	if (!trimmed) return null;

	// Keep it lenient (fail-open): uppercase for consistency.
	// Validation is enforced later (backend + error mapping).
	return trimmed.toUpperCase();
}

function toBranchNumber(branchId) {
	const m = /^NL(\d+)$/i.exec(String(branchId || "").trim());
	if (!m) return null;

	const n = Number(m[1]);
	return Number.isInteger(n) ? n : null;
}

function compareBranchIds(a, b) {
	const aa = String(a || "");
	const bb = String(b || "");

	const na = toBranchNumber(aa);
	const nb = toBranchNumber(bb);

	// Prefer numeric ordering for NLxx patterns when possible.
	if (na !== null && nb !== null) return na - nb;

	// Stable fallback:
	// - valid NL<num> come before unknown shapes
	// - otherwise lexicographic
	if (na !== null && nb === null) return -1;
	if (na === null && nb !== null) return 1;

	return aa.localeCompare(bb, "en");
}

function uniqueSorted(items) {
	const cleaned = [];

	for (const it of Array.isArray(items) ? items : []) {
		const id = normalizeBranchId(String(it));
		if (!id) continue;
		cleaned.push(id);
	}

	const unique = Array.from(new Set(cleaned));
	unique.sort(compareBranchIds);
	return unique;
}

/**
 * Parse comma-separated branches param (branches=NL01,NL02).
 *
 * @param {string|null} raw
 * @returns {string[]}
 */
export function parseBranchesCsv(raw) {
	const s = typeof raw === "string" ? raw.trim() : "";
	if (!s) return [];

	return uniqueSorted(
		s
			.split(",")
			.map((x) => String(x).trim())
			.filter(Boolean)
	);
}

/**
 * Serialize branches as comma-separated string.
 *
 * @param {string[]|null|undefined} branches
 * @returns {string|null}
 */
export function serializeBranchesCsv(branches) {
	if (!Array.isArray(branches) || branches.length === 0) return null;

	const cleaned = uniqueSorted(branches);
	return cleaned.length > 0 ? cleaned.join(",") : null;
}

function normalizeLimit(raw) {
	const s = normalizeTrimmedOrNull(raw);
	if (!s) return DEFAULT_SEARCH_LIMIT;

	if (!/^\d+$/.test(s)) return DEFAULT_SEARCH_LIMIT;

	const n = Number(s);
	if (!Number.isInteger(n)) return DEFAULT_SEARCH_LIMIT;

	return SEARCH_LIMITS.includes(n) ? n : DEFAULT_SEARCH_LIMIT;
}

/**
 * Parse search URL state from query params.
 *
 * Precedence:
 * 1) scope=all        -> ALL
 * 2) scope=multi OR branches=... -> MULTI
 * 3) otherwise -> SINGLE (routeBranch is the source of truth)
 *
 * @param {URLSearchParams|Record<string, any>|any} searchParams
 * @param {{ routeBranch?: string|null }=} options
 * @returns {{
 *   q: string|null,
 *   scope: "single"|"multi"|"all",
 *   branch: string|null,
 *   branches: string[],
 *   limit: number,
 *   from: string|null,
 *   to: string|null
 * }}
 */
export function parseSearchUrlState(searchParams, { routeBranch = null } = {}) {
	const q = normalizeTrimmedOrNull(readParam(searchParams, "q"));

	const scopeRaw = normalizeTrimmedOrNull(readParam(searchParams, "scope"));
	const branchRaw = normalizeBranchId(readParam(searchParams, "branch"));

	const branches = parseBranchesCsv(readParam(searchParams, "branches"));

	const limit = normalizeLimit(readParam(searchParams, "limit"));

	const from = normalizeTrimmedOrNull(readParam(searchParams, "from"));
	const to = normalizeTrimmedOrNull(readParam(searchParams, "to"));

	let scope = SEARCH_SCOPE.SINGLE;

	if (scopeRaw === "all") {
		scope = SEARCH_SCOPE.ALL;
	} else if (scopeRaw === "multi" || branches.length > 0) {
		scope = SEARCH_SCOPE.MULTI;
	}

	if (scope === SEARCH_SCOPE.SINGLE) {
		const fallbackRouteBranch = normalizeBranchId(routeBranch);
		const branch = branchRaw || fallbackRouteBranch || null;

		return {
			q,
			scope,
			branch,
			branches: [],
			limit,
			from,
			to,
		};
	}

	if (scope === SEARCH_SCOPE.MULTI) {
		return {
			q,
			scope,
			branch: null,
			branches,
			limit,
			from,
			to,
		};
	}

	// ALL
	return {
		q,
		scope: SEARCH_SCOPE.ALL,
		branch: null,
		branches: [],
		limit,
		from,
		to,
	};
}

/**
 * Serialize search URL state into a stable query string (no leading "?").
 *
 * Stable param ordering:
 * - q
 * - scope
 * - branches
 * - limit
 * - from
 * - to
 *
 * SINGLE policy:
 * - We do NOT emit `branch=` because the path segment `/:branch/search` is the source of truth.
 *
 * @param {{
 *   q?: string|null,
 *   scope?: "single"|"multi"|"all"|string|null,
 *   branch?: string|null,
 *   branches?: string[]|null,
 *   limit?: number|null,
 *   from?: string|null,
 *   to?: string|null
 * }} state
 * @returns {string}
 */
export function serializeSearchUrlState(state) {
	const s = state || {};
	const params = new URLSearchParams();

	const q = normalizeTrimmedOrNull(s.q);
	const from = normalizeTrimmedOrNull(s.from);
	const to = normalizeTrimmedOrNull(s.to);

	const limit =
		Number.isInteger(s.limit) && SEARCH_LIMITS.includes(s.limit)
			? s.limit
			: DEFAULT_SEARCH_LIMIT;

	if (q) params.set("q", q);

	// Scope: we only emit what the UI needs for shareability.
	if (s.scope === SEARCH_SCOPE.ALL) {
		params.set("scope", "all");
	} else if (s.scope === SEARCH_SCOPE.MULTI) {
		params.set("scope", "multi");

		const csv = serializeBranchesCsv(s.branches);
		if (csv) params.set("branches", csv);
	} else {
		// SINGLE: no `branch=` in URL (path is SoT)
	}

	// Only include non-default limit to keep URLs shorter.
	if (limit !== DEFAULT_SEARCH_LIMIT) {
		params.set("limit", String(limit));
	}

	// from/to are additive for RHL-025. We allow carrying them already.
	if (from) params.set("from", from);
	if (to) params.set("to", to);

	return params.toString();
}
