import { n as InvalidCursorError } from "./types-CuW0zWDA.mjs";
import { t as OptionsRepository } from "./options-DTTML-Tx.mjs";
import { a as validateDestinationParams, i as matchPattern, n as interpolateDestination, o as validatePattern, r as isPattern, t as compilePattern } from "./patterns-BKmjvM7K.mjs";
import { t as RedirectRepository } from "./redirect-IyfNirgS.mjs";
import { n as isTerminalStatus } from "./status-CqJQX1QN.mjs";

//#region src/redirects/loops.ts
/**
* Redirect loop and chain detection utilities.
*
* Builds a directed graph from redirect rules and detects:
* - Cycles (loops): /a → /b → /c → /a
* - Long chains: /a → /b → /c → /d → /e (exceeding a warning threshold)
*
* Handles both exact and pattern redirects. When the walker encounters
* a path with no exact source match, it tests against compiled pattern
* sources and resolves the destination using captured parameters.
*/
/**
* Compile all enabled pattern redirects for matching during graph walks.
*/
function compilePatterns(edges) {
	const result = [];
	for (const edge of edges) if (edge.enabled && edge.isPattern) result.push({
		id: edge.id,
		compiled: compilePattern(edge.source),
		destination: edge.destination
	});
	return result;
}
/** Single-segment dummy value for representative path generation */
const DUMMY_SEGMENT = "__p__";
/** Splat pattern: [...paramName] */
const SPLAT_RE = /\[\.\.\.(\w+)\]/g;
/** Param pattern: [paramName] */
const PARAM_RE = /\[(\w+)\]/g;
/**
* Extract the literal prefix from a pattern source (everything before the
* first placeholder), stripped of leading segments shared with a base path.
* e.g., "/new/docs/[slug]" → "docs/__p__" (the part after "/new/")
*/
function extractPatternSuffix(patternSource) {
	let result = patternSource.replace(SPLAT_RE, DUMMY_SEGMENT);
	SPLAT_RE.lastIndex = 0;
	result = result.replace(PARAM_RE, DUMMY_SEGMENT);
	return result.split("/").filter(Boolean).slice(1).join("/");
}
/**
* Generate representative concrete paths from a template string.
* Replaces [param] with a dummy segment and [...rest] with multiple
* depth variants. For catch-alls, also generates representatives using
* literal prefixes from existing pattern sources to catch cross-pattern loops.
*/
function generateRepresentatives(template, existingEdges) {
	const hasSplat = SPLAT_RE.test(template);
	SPLAT_RE.lastIndex = 0;
	if (hasSplat) {
		const splatIndex = template.indexOf("[...");
		const prefix = template.slice(0, splatIndex);
		const reps = [
			template.replace(SPLAT_RE, DUMMY_SEGMENT).replace(PARAM_RE, DUMMY_SEGMENT),
			template.replace(SPLAT_RE, `${DUMMY_SEGMENT}/${DUMMY_SEGMENT}`).replace(PARAM_RE, DUMMY_SEGMENT),
			template.replace(SPLAT_RE, `${DUMMY_SEGMENT}/${DUMMY_SEGMENT}/${DUMMY_SEGMENT}`).replace(PARAM_RE, DUMMY_SEGMENT)
		];
		if (existingEdges) {
			for (const edge of existingEdges) if (edge.enabled && edge.isPattern && edge.source !== template) {
				const suffix = extractPatternSuffix(edge.source);
				if (suffix) reps.push(`${prefix}${suffix}`);
			}
		}
		return reps;
	}
	return [template.replace(PARAM_RE, DUMMY_SEGMENT)];
}
/**
* Resolve the next hop for a given path. Tries exact match first,
* then pattern matching with parameter interpolation for concrete paths,
* then representative-based matching for template strings.
*/
function resolveNext(path, graph, patterns, edges) {
	const exact = graph.get(path);
	if (exact) return exact;
	if (!path.includes("[")) for (const pr of patterns) {
		const params = matchPattern(pr.compiled, path);
		if (params) return {
			destination: interpolateDestination(pr.destination, params),
			id: pr.id
		};
	}
	else {
		const representatives = generateRepresentatives(path, edges);
		for (const pr of patterns) for (const rep of representatives) {
			const params = matchPattern(pr.compiled, rep);
			if (params) return {
				destination: interpolateDestination(pr.destination, params),
				id: pr.id
			};
		}
	}
	return null;
}
/**
* Build an adjacency map from redirect edges.
* Includes both exact and pattern redirects — pattern redirects use their
* template strings as literal graph edges, which works because EmDash
* patterns pass parameters through without transformation.
*/
function buildGraph(edges) {
	const graph = /* @__PURE__ */ new Map();
	for (const edge of edges) if (edge.enabled) graph.set(edge.source, {
		destination: edge.destination,
		id: edge.id
	});
	return graph;
}
/**
* Detect all redirect IDs that participate in cycles.
* Walks every node in the graph once, collecting IDs from any cycles found.
*
* @returns Array of redirect IDs that are part of a loop
*/
function detectLoops(edges) {
	const graph = buildGraph(edges);
	const patterns = compilePatterns(edges);
	const visited = /* @__PURE__ */ new Set();
	const loopRedirectIds = /* @__PURE__ */ new Set();
	for (const [startSource] of graph) {
		if (visited.has(startSource)) continue;
		const path = [];
		const pathSet = /* @__PURE__ */ new Set();
		const pathIds = [];
		let current = startSource;
		while (current) {
			if (pathSet.has(current)) {
				const loopStart = path.indexOf(current);
				for (const id of pathIds.slice(loopStart)) loopRedirectIds.add(id);
				break;
			}
			if (visited.has(current)) break;
			const next = resolveNext(current, graph, patterns, edges);
			if (!next) break;
			path.push(current);
			pathSet.add(current);
			pathIds.push(next.id);
			current = next.destination;
		}
		for (const node of path) visited.add(node);
	}
	return [...loopRedirectIds];
}
/**
* Find a compiled pattern redirect whose source matches the given resolved path,
* returning the source template string for display purposes.
*/
function findMatchingTemplate(resolvedPath, patterns) {
	for (const pr of patterns) if (matchPattern(pr.compiled, resolvedPath) !== null) return pr.compiled.source;
	return null;
}
/**
* Check if adding or updating a redirect would create a loop.
*
* Walks the chain from `destination` through existing redirects.
* If it reaches `source`, a cycle would form.
*
* @returns The loop path if a cycle would be created, or null if safe
*/
function wouldCreateLoop(source, destination, existingEdges, excludeId) {
	const filtered = excludeId ? existingEdges.filter((e) => e.id !== excludeId) : existingEdges;
	const graph = buildGraph(filtered);
	const patterns = compilePatterns(filtered);
	const compiledSource = source.includes("[") ? compilePattern(source) : null;
	let startingPoints;
	if (destination.includes("[")) {
		const reps = generateRepresentatives(destination, filtered);
		const compiled = compilePattern(destination);
		for (const [key] of graph) if (!key.includes("[") && matchPattern(compiled, key) !== null) reps.push(key);
		reps.push(destination);
		startingPoints = reps;
	} else startingPoints = [destination];
	for (const start of startingPoints) {
		const path = [source, destination];
		let current = start;
		const seen = new Set([
			source,
			destination,
			start
		]);
		while (true) {
			const next = resolveNext(current, graph, patterns, filtered);
			if (!next) break;
			if (seen.has(next.destination) || compiledSource !== null && matchPattern(compiledSource, next.destination) !== null) {
				const displayPath = !seen.has(next.destination) && compiledSource !== null ? source : next.destination;
				path.push(displayPath);
				return path;
			}
			const cleanDest = next.destination.includes(DUMMY_SEGMENT) ? findMatchingTemplate(next.destination, patterns) ?? next.destination : next.destination;
			path.push(cleanDest);
			seen.add(next.destination);
			current = next.destination;
		}
	}
	return null;
}

//#endregion
//#region src/api/handlers/redirects.ts
/**
* List redirects with cursor pagination and optional filters
*/
async function handleRedirectList(db, params) {
	try {
		const result = await new RedirectRepository(db).findMany(params);
		const loopRedirectIds = await getLoopRedirectIds(db);
		return {
			success: true,
			data: {
				...result,
				...loopRedirectIds.length > 0 ? { loopRedirectIds } : {}
			}
		};
	} catch (error) {
		if (error instanceof InvalidCursorError) return {
			success: false,
			error: {
				code: "INVALID_CURSOR",
				message: error.message
			}
		};
		return {
			success: false,
			error: {
				code: "REDIRECT_LIST_ERROR",
				message: "Failed to fetch redirects"
			}
		};
	}
}
/**
* Create a redirect rule
*/
async function handleRedirectCreate(db, input) {
	try {
		const repo = new RedirectRepository(db);
		const type = input.type ?? 301;
		const terminal = isTerminalStatus(type);
		const destination = terminal ? "" : input.destination ?? "";
		if (!terminal && input.source === destination) return {
			success: false,
			error: {
				code: "VALIDATION_ERROR",
				message: "Source and destination must be different"
			}
		};
		const sourceIsPattern = isPattern(input.source);
		if (sourceIsPattern) {
			const patternError = validatePattern(input.source);
			if (patternError) return {
				success: false,
				error: {
					code: "VALIDATION_ERROR",
					message: `Invalid source pattern: ${patternError}`
				}
			};
			if (!terminal) {
				const destError = validateDestinationParams(input.source, destination);
				if (destError) return {
					success: false,
					error: {
						code: "VALIDATION_ERROR",
						message: destError
					}
				};
			}
		}
		if (await repo.findBySource(input.source)) return {
			success: false,
			error: {
				code: "CONFLICT",
				message: `A redirect from "${input.source}" already exists`
			}
		};
		if (!terminal && input.enabled !== false) {
			const edges = toEdges(await repo.findAllEnabled());
			const loopPath = wouldCreateLoop(input.source, destination, edges);
			if (loopPath) return loopError(loopPath);
		}
		return {
			success: true,
			data: await repo.create({
				source: input.source,
				destination,
				type,
				isPattern: sourceIsPattern,
				enabled: input.enabled ?? true,
				groupName: input.groupName ?? null
			})
		};
	} catch {
		return {
			success: false,
			error: {
				code: "REDIRECT_CREATE_ERROR",
				message: "Failed to create redirect"
			}
		};
	}
}
/**
* Get a redirect by ID
*/
async function handleRedirectGet(db, id) {
	try {
		const redirect = await new RedirectRepository(db).findById(id);
		if (!redirect) return {
			success: false,
			error: {
				code: "NOT_FOUND",
				message: `Redirect "${id}" not found`
			}
		};
		return {
			success: true,
			data: redirect
		};
	} catch {
		return {
			success: false,
			error: {
				code: "REDIRECT_GET_ERROR",
				message: "Failed to fetch redirect"
			}
		};
	}
}
/**
* Update a redirect by ID
*/
async function handleRedirectUpdate(db, id, input) {
	try {
		const repo = new RedirectRepository(db);
		const existing = await repo.findById(id);
		if (!existing) return {
			success: false,
			error: {
				code: "NOT_FOUND",
				message: `Redirect "${id}" not found`
			}
		};
		const newSource = input.source ?? existing.source;
		const newDest = input.destination ?? existing.destination;
		if (newSource === newDest) return {
			success: false,
			error: {
				code: "VALIDATION_ERROR",
				message: "Source and destination must be different"
			}
		};
		if (input.source !== void 0) {
			if (isPattern(input.source)) {
				const patternError = validatePattern(input.source);
				if (patternError) return {
					success: false,
					error: {
						code: "VALIDATION_ERROR",
						message: `Invalid source pattern: ${patternError}`
					}
				};
			}
			const dup = await repo.findBySource(input.source);
			if (dup && dup.id !== id) return {
				success: false,
				error: {
					code: "CONFLICT",
					message: `A redirect from "${input.source}" already exists`
				}
			};
		}
		if (isPattern(newSource)) {
			const destError = validateDestinationParams(newSource, newDest);
			if (destError) return {
				success: false,
				error: {
					code: "VALIDATION_ERROR",
					message: destError
				}
			};
		}
		if (input.source !== void 0 || input.destination !== void 0) {
			const loopPath = wouldCreateLoop(newSource, newDest, toEdges(await repo.findAllEnabled()), id);
			if (loopPath) return loopError(loopPath);
		}
		const updated = await repo.update(id, {
			source: input.source,
			destination: input.destination,
			type: input.type,
			enabled: input.enabled,
			groupName: input.groupName
		});
		if (!updated) return {
			success: false,
			error: {
				code: "REDIRECT_UPDATE_ERROR",
				message: "Failed to update redirect"
			}
		};
		await updateLoopCache(db);
		return {
			success: true,
			data: updated
		};
	} catch {
		return {
			success: false,
			error: {
				code: "REDIRECT_UPDATE_ERROR",
				message: "Failed to update redirect"
			}
		};
	}
}
/**
* Delete a redirect by ID
*/
async function handleRedirectDelete(db, id) {
	try {
		if (!await new RedirectRepository(db).delete(id)) return {
			success: false,
			error: {
				code: "NOT_FOUND",
				message: `Redirect "${id}" not found`
			}
		};
		await updateLoopCache(db);
		return {
			success: true,
			data: { deleted: true }
		};
	} catch {
		return {
			success: false,
			error: {
				code: "REDIRECT_DELETE_ERROR",
				message: "Failed to delete redirect"
			}
		};
	}
}
function loopError(loopPath) {
	return {
		success: false,
		error: {
			code: "VALIDATION_ERROR",
			message: `This redirect would create a loop:\n${loopPath.slice(0, -1).map((p, i) => `${p} \u2192 ${loopPath[i + 1]}`).join("\n")}`
		}
	};
}
function toEdges(redirects) {
	return redirects.map((r) => ({
		id: r.id,
		source: r.source,
		destination: r.destination,
		enabled: r.enabled,
		isPattern: r.isPattern
	}));
}
const LOOP_CACHE_KEY = "_redirect_loop_ids";
/**
* Recompute loop redirect IDs and store in the options table.
*/
async function updateLoopCache(db) {
	try {
		const options = new OptionsRepository(db);
		const loopRedirectIds = detectLoops(toEdges(await new RedirectRepository(db).findAllEnabled()));
		await options.set(LOOP_CACHE_KEY, loopRedirectIds);
	} catch (error) {
		console.error("Failed to update redirect loop cache:", error);
	}
}
/**
* Get loop redirect IDs from cache, computing lazily on first access.
*/
async function getLoopRedirectIds(db) {
	try {
		const options = new OptionsRepository(db);
		const cached = await options.get(LOOP_CACHE_KEY);
		if (cached !== null) return cached;
		await updateLoopCache(db);
		return await options.get(LOOP_CACHE_KEY) ?? [];
	} catch {
		return [];
	}
}
/**
* List 404 log entries with cursor pagination
*/
async function handleNotFoundList(db, params) {
	try {
		return {
			success: true,
			data: await new RedirectRepository(db).find404s(params)
		};
	} catch (error) {
		if (error instanceof InvalidCursorError) return {
			success: false,
			error: {
				code: "INVALID_CURSOR",
				message: error.message
			}
		};
		return {
			success: false,
			error: {
				code: "NOT_FOUND_LIST_ERROR",
				message: "Failed to fetch 404 log"
			}
		};
	}
}
/**
* Get 404 summary (grouped by path, sorted by count)
*/
async function handleNotFoundSummary(db, limit) {
	try {
		return {
			success: true,
			data: { items: await new RedirectRepository(db).get404Summary(limit) }
		};
	} catch {
		return {
			success: false,
			error: {
				code: "NOT_FOUND_SUMMARY_ERROR",
				message: "Failed to fetch 404 summary"
			}
		};
	}
}
/**
* Clear all 404 log entries
*/
async function handleNotFoundClear(db) {
	try {
		return {
			success: true,
			data: { deleted: await new RedirectRepository(db).clear404s() }
		};
	} catch {
		return {
			success: false,
			error: {
				code: "NOT_FOUND_CLEAR_ERROR",
				message: "Failed to clear 404 log"
			}
		};
	}
}
/**
* Prune 404 log entries older than a given date
*/
async function handleNotFoundPrune(db, olderThan) {
	try {
		return {
			success: true,
			data: { deleted: await new RedirectRepository(db).prune404s(olderThan) }
		};
	} catch {
		return {
			success: false,
			error: {
				code: "NOT_FOUND_PRUNE_ERROR",
				message: "Failed to prune 404 log"
			}
		};
	}
}

//#endregion
export { handleRedirectCreate as a, handleRedirectList as c, handleNotFoundSummary as i, handleRedirectUpdate as l, handleNotFoundList as n, handleRedirectDelete as o, handleNotFoundPrune as r, handleRedirectGet as s, handleNotFoundClear as t };
//# sourceMappingURL=redirects-Bs1g42Ri.mjs.map