/**
 * TanStack Router configuration for EmDash Admin
 *
 * Defines all admin routes and their components.
 */

import { Button, Loader, Toast } from "@cloudflare/kumo";
import { plural } from "@lingui/core/macro";
import { useLingui } from "@lingui/react/macro";
import type { QueryClient } from "@tanstack/react-query";
import { useQuery, useInfiniteQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import {
	createRouter,
	createRootRouteWithContext,
	createRoute,
	Outlet,
	Link,
	useParams,
	useNavigate,
	useSearch,
} from "@tanstack/react-router";
import * as React from "react";

import { CommentInbox } from "./components/comments/CommentInbox";
import { ContentEditor } from "./components/ContentEditor";
import {
	ContentList,
	EMPTY_DATE_FILTER,
	type ContentDateFilter,
	type ContentListSort,
	type ContentStatusFilter,
} from "./components/ContentList";
import { ContentTypeEditor } from "./components/ContentTypeEditor";
import { ContentTypeList } from "./components/ContentTypeList";
import { Dashboard } from "./components/Dashboard";
import { DeviceAuthorizePage } from "./components/DeviceAuthorizePage";
import { InviteAcceptPage } from "./components/InviteAcceptPage";
import { LoginPage } from "./components/LoginPage";
import { MarketplaceBrowse } from "./components/MarketplaceBrowse";
import { MarketplacePluginDetail } from "./components/MarketplacePluginDetail";
import { MediaLibrary } from "./components/MediaLibrary";
import { MenuEditor } from "./components/MenuEditor";
import { MenuList } from "./components/MenuList";
import { PluginManager } from "./components/PluginManager";
import { Redirects } from "./components/Redirects";
import { RegistryBrowse } from "./components/RegistryBrowse";
import { RegistryPluginDetail } from "./components/RegistryPluginDetail";
import { SandboxedPluginPage } from "./components/SandboxedPluginPage";
import { SectionEditor } from "./components/SectionEditor";
import { Sections } from "./components/Sections";
import { Settings } from "./components/Settings";
import { AllowedDomainsSettings } from "./components/settings/AllowedDomainsSettings";
import { ApiTokenSettings } from "./components/settings/ApiTokenSettings";
import { EmailSettings } from "./components/settings/EmailSettings";
import { GeneralSettings } from "./components/settings/GeneralSettings";
import { SecuritySettings } from "./components/settings/SecuritySettings";
import { SeoSettings } from "./components/settings/SeoSettings";
import { SocialSettings } from "./components/settings/SocialSettings";
import { SetupWizard } from "./components/SetupWizard";
import { Shell } from "./components/Shell";
import { SignupPage } from "./components/SignupPage";
import { TaxonomyManager } from "./components/TaxonomyManager";
import { ThemeMarketplaceBrowse } from "./components/ThemeMarketplaceBrowse";
import { ThemeMarketplaceDetail } from "./components/ThemeMarketplaceDetail";
import { Widgets } from "./components/Widgets";
import { WordPressImport } from "./components/WordPressImport";
import {
	apiFetch,
	parseApiResponse,
	fetchManifest,
	fetchContentList,
	fetchContentAuthors,
	fetchContent,
	createContent,
	updateContent,
	deleteContent,
	fetchTranslations,
	fetchMediaList,
	uploadMedia,
	deleteMedia,
	fetchCollections,
	fetchCollection,
	createCollection,
	updateCollection,
	deleteCollection,
	createField,
	updateField,
	deleteField,
	reorderFields,
	fetchOrphanedTables,
	registerOrphanedTable,
	fetchUsers,
	fetchBylines,
	createByline,
	updateByline,
	setSearchEnabled,
	fetchTrashedContent,
	restoreContent,
	permanentDeleteContent,
	duplicateContent,
	scheduleContent,
	unscheduleContent,
	publishContent,
	unpublishContent,
	discardDraft,
	fetchRevision,
	type CreateCollectionInput,
	type UpdateCollectionInput,
	type CreateFieldInput,
	type BylineCreditInput,
	type ContentSeoInput,
	type ContentItem,
	type Revision,
} from "./lib/api";
import {
	fetchComments,
	fetchCommentCounts,
	updateCommentStatus,
	deleteComment,
	bulkCommentAction,
	type CommentStatus,
} from "./lib/api/comments";
import { usePluginPage } from "./lib/plugin-context";
import { getPluginBlocks } from "./lib/pluginBlocks";
import { sanitizeRedirectUrl } from "./lib/url";
import { BylineSchemaPage } from "./routes/byline-schema";
import { BylinesPage } from "./routes/bylines";
import { UsersPage } from "./routes/users";

// Router context type
interface RouterContext {
	queryClient: QueryClient;
}

function patchAutosaveQueries(
	queryClient: QueryClient,
	params: {
		collection: string;
		id: string;
		savedItem: ContentItem;
		payload: {
			data?: Record<string, unknown>;
			slug?: string;
		};
		locale?: string;
	},
) {
	const { collection, id, savedItem, payload, locale } = params;
	const draftRevisionId = savedItem.draftRevisionId;

	if (draftRevisionId) {
		queryClient.setQueryData<Revision>(["revision", draftRevisionId], (existing) => {
			const nextData: Record<string, unknown> = {
				...existing?.data,
				...payload.data,
			};

			if (payload.slug !== undefined) {
				nextData._slug = payload.slug;
			}

			return {
				id: draftRevisionId,
				collection,
				entryId: id,
				data: nextData,
				authorId: existing?.authorId ?? savedItem.authorId,
				createdAt: existing?.createdAt ?? savedItem.updatedAt,
			};
		});
	}

	queryClient.setQueryData<ContentItem>(
		locale ? ["content", collection, id, { locale }] : ["content", collection, id],
		savedItem,
	);
}

// Create a base root route without Shell for setup
const baseRootRoute = createRootRouteWithContext<RouterContext>()({
	component: () => <Outlet />,
});

// Setup route (standalone, no Shell)
const setupRoute = createRoute({
	getParentRoute: () => baseRootRoute,
	path: "/setup",
	component: SetupWizard,
});

// Login route (standalone, no Shell)
const loginRoute = createRoute({
	getParentRoute: () => baseRootRoute,
	path: "/login",
	component: LoginPageWrapper,
});

function LoginPageWrapper() {
	// Extract redirect URL from query params, sanitized to prevent open redirect / XSS
	const searchParams = new URLSearchParams(window.location.search);
	const redirect = sanitizeRedirectUrl(searchParams.get("redirect") || "/_emdash/admin");
	return <LoginPage redirectUrl={redirect} />;
}

// Signup route (standalone, no Shell)
const signupRoute = createRoute({
	getParentRoute: () => baseRootRoute,
	path: "/signup",
	component: SignupPage,
});

// Invite accept route (standalone, no Shell)
const inviteAcceptRoute = createRoute({
	getParentRoute: () => baseRootRoute,
	path: "/invite/accept",
	component: InviteAcceptPage,
	validateSearch: (search: Record<string, unknown>) => ({
		token: typeof search.token === "string" ? search.token : undefined,
	}),
});

// Device authorization route (standalone, no Shell)
const deviceRoute = createRoute({
	getParentRoute: () => baseRootRoute,
	path: "/device",
	component: DeviceAuthorizePage,
});

// Layout route with Shell wrapper for admin pages (pathless - matches all admin routes)
const adminLayoutRoute = createRoute({
	getParentRoute: () => baseRootRoute,
	id: "_admin",
	component: RootComponent,
});

// Isomorphic requestIdleCallback polyfill
if (typeof window !== "undefined" && typeof window.requestIdleCallback === "undefined") {
	window.requestIdleCallback = (cb) => setTimeout(cb, 50);
	window.cancelIdleCallback = (id) => clearTimeout(id);
}

function RootComponent() {
	const { t } = useLingui();
	const {
		data: manifest,
		isLoading,
		error,
	} = useQuery({
		queryKey: ["manifest"],
		queryFn: fetchManifest,
	});

	if (isLoading) {
		return <LoadingScreen />;
	}

	if (error || !manifest) {
		return <ErrorScreen error={error?.message ?? t`Failed to load admin`} />;
	}

	// Plugin admin components are passed via props and available through PluginAdminContext
	return (
		<Shell manifest={manifest}>
			<Outlet />
		</Shell>
	);
}

// Dashboard route - matches the index path "/"
const dashboardRoute = createRoute({
	getParentRoute: () => adminLayoutRoute,
	path: "/",
	component: DashboardPage,
});

function DashboardPage() {
	const { data: manifest } = useQuery({
		queryKey: ["manifest"],
		queryFn: fetchManifest,
	});

	if (!manifest) return null;

	return <Dashboard manifest={manifest} />;
}

// Content list route
const contentListRoute = createRoute({
	getParentRoute: () => adminLayoutRoute,
	path: "/content/$collection",
	component: ContentListPage,
	validateSearch: (search: Record<string, unknown>) => ({
		locale: typeof search.locale === "string" ? search.locale : undefined,
	}),
});

function ContentListPage() {
	const { t } = useLingui();
	const { collection } = useParams({ from: "/_admin/content/$collection" });
	const { locale: localeParam } = useSearch({ from: "/_admin/content/$collection" });
	const queryClient = useQueryClient();
	const navigate = useNavigate();
	const toastManager = Toast.useToastManager();

	const { data: manifest } = useQuery({
		queryKey: ["manifest"],
		queryFn: fetchManifest,
	});

	const i18n = manifest?.i18n;

	// Default to defaultLocale when i18n is enabled and no locale specified
	const activeLocale = i18n ? (localeParam ?? i18n.defaultLocale) : undefined;

	// Controlled sort state — passed to the list, and included in the query
	// key so changing direction invalidates the current cursor chain.
	const [sort, setSort] = React.useState<ContentListSort>({
		field: "updatedAt",
		direction: "desc",
	});

	// Server-side search term (debounced inside ContentList). Part of the query
	// key so a new term restarts the cursor chain from a filtered first page.
	const [searchTerm, setSearchTerm] = React.useState("");

	// Filter state (#1288). All are part of the query key so changing any of
	// them restarts the cursor chain from a filtered first page.
	const [statusFilter, setStatusFilter] = React.useState<ContentStatusFilter>("all");
	const [authorFilter, setAuthorFilter] = React.useState("");
	const [dateFilter, setDateFilter] = React.useState<ContentDateFilter>(EMPTY_DATE_FILTER);

	// The date inputs yield calendar dates; widen them to UTC day boundaries so
	// the inclusive `dateTo` covers the whole day (timestamps are stored in UTC).
	const dateApiParams = React.useMemo(() => {
		const hasRange = !!dateFilter.from || !!dateFilter.to;
		if (!hasRange) return undefined;
		return {
			dateField: dateFilter.field,
			dateFrom: dateFilter.from ? `${dateFilter.from}T00:00:00.000Z` : undefined,
			dateTo: dateFilter.to ? `${dateFilter.to}T23:59:59.999Z` : undefined,
		};
	}, [dateFilter]);

	// Authors are collection-wide (the endpoint doesn't scope by locale), so the
	// query key omits locale to avoid refetching/cache-fragmenting on locale
	// switches, and the selection stays valid across locales.
	const { data: authors } = useQuery({
		queryKey: ["content", collection, "authors"],
		queryFn: () => fetchContentAuthors(collection),
		enabled: !!manifest,
	});

	const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading, error } =
		useInfiniteQuery({
			queryKey: [
				"content",
				collection,
				{
					locale: activeLocale,
					sort,
					search: searchTerm,
					status: statusFilter,
					author: authorFilter,
					date: dateApiParams,
				},
			],
			queryFn: ({ pageParam }) =>
				fetchContentList(collection, {
					locale: activeLocale,
					cursor: pageParam,
					limit: 100,
					orderBy: sort.field,
					order: sort.direction,
					search: searchTerm || undefined,
					status: statusFilter === "all" ? undefined : statusFilter,
					authorId: authorFilter || undefined,
					...dateApiParams,
				}),
			initialPageParam: undefined as string | undefined,
			getNextPageParam: (lastPage) => lastPage.nextCursor,
			enabled: !!manifest,
		});

	// Fetch trashed items
	const { data: trashedData, isLoading: isTrashedLoading } = useQuery({
		queryKey: ["content", collection, "trash"],
		queryFn: () => fetchTrashedContent(collection),
	});

	const deleteMutation = useMutation({
		mutationFn: (id: string) => deleteContent(collection, id),
		onSuccess: () => {
			void queryClient.invalidateQueries({ queryKey: ["content", collection] });
			void queryClient.invalidateQueries({ queryKey: ["content", collection, "trash"] });
		},
		onError: (mutationError) => {
			toastManager.add({
				title: t`Failed to delete`,
				description: mutationError instanceof Error ? mutationError.message : t`An error occurred`,
				type: "error",
			});
		},
	});

	const restoreMutation = useMutation({
		mutationFn: (id: string) => restoreContent(collection, id),
		onSuccess: () => {
			void queryClient.invalidateQueries({ queryKey: ["content", collection] });
			void queryClient.invalidateQueries({ queryKey: ["content", collection, "trash"] });
		},
		onError: (mutationError) => {
			toastManager.add({
				title: t`Failed to restore`,
				description: mutationError instanceof Error ? mutationError.message : t`An error occurred`,
				type: "error",
			});
		},
	});

	const permanentDeleteMutation = useMutation({
		mutationFn: (id: string) => permanentDeleteContent(collection, id),
		onSuccess: () => {
			void queryClient.invalidateQueries({ queryKey: ["content", collection, "trash"] });
		},
		onError: (mutationError) => {
			toastManager.add({
				title: t`Failed to delete`,
				description: mutationError instanceof Error ? mutationError.message : t`An error occurred`,
				type: "error",
			});
		},
	});

	const duplicateMutation = useMutation({
		mutationFn: (id: string) => duplicateContent(collection, id),
		onSuccess: () => {
			void queryClient.invalidateQueries({ queryKey: ["content", collection] });
		},
		onError: (mutationError) => {
			toastManager.add({
				title: t`Failed to duplicate`,
				description: mutationError instanceof Error ? mutationError.message : t`An error occurred`,
				type: "error",
			});
		},
	});

	const items = React.useMemo(() => {
		return data?.pages.flatMap((page) => page.items) || [];
	}, [data]);

	// Server returns `total` on every page; the first page is authoritative
	// because filters don't change within a fetch cycle. Fall back to the
	// loaded count so old servers (pre-total) still render a denominator.
	const total = data?.pages[0]?.total ?? items.length;

	// Keep every hook above the early returns below — a render that takes a
	// guard (e.g. `error`) must run the same number of hooks as a full render,
	// or React throws #300 "Rendered fewer hooks than expected" (#1415).
	const handleLoadMore = React.useCallback(() => void fetchNextPage(), [fetchNextPage]);

	if (!manifest) {
		return <LoadingScreen />;
	}

	const collectionConfig = manifest.collections[collection];

	if (!collectionConfig) {
		return <NotFoundPage message={`Collection "${collection}" not found`} />;
	}

	if (error) {
		return <ErrorScreen error={error.message} />;
	}

	const handleLocaleChange = (locale: string) => {
		// Update URL search params without full navigation
		void navigate({
			to: "/content/$collection",
			params: { collection },
			search: { locale: locale || undefined },
		});
	};

	return (
		<ContentList
			collection={collection}
			collectionLabel={collectionConfig.label}
			items={items}
			trashedItems={trashedData?.items || []}
			isLoading={isLoading || isFetchingNextPage}
			isTrashedLoading={isTrashedLoading}
			hasMore={!!hasNextPage}
			onLoadMore={handleLoadMore}
			trashedCount={trashedData?.items?.length || 0}
			onDelete={(id) => deleteMutation.mutate(id)}
			onRestore={(id) => restoreMutation.mutate(id)}
			onPermanentDelete={(id) => permanentDeleteMutation.mutate(id)}
			onDuplicate={(id) => duplicateMutation.mutate(id)}
			i18n={i18n}
			activeLocale={activeLocale}
			onLocaleChange={handleLocaleChange}
			urlPattern={collectionConfig.urlPattern}
			sort={sort}
			onSortChange={setSort}
			total={total}
			onSearchChange={setSearchTerm}
			statusFilter={statusFilter}
			onStatusFilterChange={setStatusFilter}
			authors={authors}
			authorFilter={authorFilter}
			onAuthorFilterChange={setAuthorFilter}
			dateFilter={dateFilter}
			onDateFilterChange={setDateFilter}
		/>
	);
}

// Content new route
const contentNewRoute = createRoute({
	getParentRoute: () => adminLayoutRoute,
	path: "/content/$collection/new",
	component: ContentNewPage,
	validateSearch: (search: Record<string, unknown>) => ({
		locale: typeof search.locale === "string" ? search.locale : undefined,
	}),
});

function ContentNewPage() {
	const { collection } = useParams({ from: "/_admin/content/$collection/new" });
	const { locale } = useSearch({ from: "/_admin/content/$collection/new" });
	const navigate = useNavigate();
	const queryClient = useQueryClient();
	const [selectedBylines, setSelectedBylines] = React.useState<BylineCreditInput[]>([]);

	const { data: manifest } = useQuery({
		queryKey: ["manifest"],
		queryFn: fetchManifest,
	});

	// Locale the picker should scope to. URL `?locale=` wins; otherwise
	// fall back to the configured defaultLocale. Single-locale installs
	// resolve to `defaultLocale` too — the server treats that as "use the
	// configured default" so behaviour matches pre-i18n in that case.
	const pickerLocale = locale ?? manifest?.i18n?.defaultLocale;

	// Send the resolved picker locale so the new entry's locale matches
	// the locale the byline picker was scoped to.
	const createMutation = useMutation({
		mutationFn: (data: {
			data: Record<string, unknown>;
			slug?: string;
			bylines?: BylineCreditInput[];
		}) => createContent(collection, { ...data, locale: pickerLocale }),
		onSuccess: (result) => {
			void queryClient.invalidateQueries({ queryKey: ["content", collection] });
			void navigate({
				to: "/content/$collection/$id",
				params: { collection, id: result.id },
				search: { locale: result.locale },
			});
		},
	});

	const pluginBlocks = React.useMemo(() => (manifest ? getPluginBlocks(manifest) : []), [manifest]);

	// The picker is locale-pinned to the entry being created so editors
	// only see bylines that will actually hydrate at this locale (per the
	// strict per-locale model from migration 040). Locale is part of the
	// query key so switching locales fetches a fresh slice rather than
	// reusing a stale cache.
	const { data: bylinesData, isSuccess: bylinesLoaded } = useQuery({
		queryKey: ["bylines", "picker", pickerLocale ?? null],
		queryFn: () => fetchBylines({ locale: pickerLocale, limit: 100 }),
		enabled: !!manifest,
	});

	const createBylineMutation = useMutation({
		mutationFn: (input: { slug: string; displayName: string }) =>
			createByline({ ...input, isGuest: true, locale: pickerLocale }),
		onSuccess: () => {
			void queryClient.invalidateQueries({ queryKey: ["bylines"] });
		},
	});

	const updateBylineMutation = useMutation({
		mutationFn: (input: { id: string; slug: string; displayName: string }) =>
			updateByline(input.id, {
				slug: input.slug,
				displayName: input.displayName,
			}),
		onSuccess: () => {
			void queryClient.invalidateQueries({ queryKey: ["bylines"] });
		},
	});

	if (!manifest) {
		return <LoadingScreen />;
	}

	const collectionConfig = manifest.collections[collection];

	if (!collectionConfig) {
		return <NotFoundPage message={`Collection "${collection}" not found`} />;
	}

	const handleSave = (payload: {
		data: Record<string, unknown>;
		slug?: string;
		bylines?: BylineCreditInput[];
	}) => {
		createMutation.mutate(payload);
	};

	return (
		<ContentEditor
			collection={collection}
			collectionLabel={collectionConfig.labelSingular || collectionConfig.label}
			fields={collectionConfig.fields}
			isNew
			entryLocale={pickerLocale}
			i18n={manifest?.i18n}
			isSaving={createMutation.isPending}
			onSave={handleSave}
			pluginBlocks={pluginBlocks}
			availableBylines={bylinesData?.items}
			availableBylinesLoaded={bylinesLoaded}
			selectedBylines={selectedBylines}
			onBylinesChange={setSelectedBylines}
			onQuickCreateByline={async (input) => {
				const created = await createBylineMutation.mutateAsync(input);
				return created;
			}}
			onQuickEditByline={async (bylineId, input) => {
				const updated = await updateBylineMutation.mutateAsync({ id: bylineId, ...input });
				return updated;
			}}
			manifest={manifest ?? null}
		/>
	);
}

// Content edit route
const contentEditRoute = createRoute({
	getParentRoute: () => adminLayoutRoute,
	path: "/content/$collection/$id",
	component: ContentEditPage,
	validateSearch: (search) => ({
		...(typeof search.field === "string" && { field: search.field }),
		...(typeof search.locale === "string" && { locale: search.locale }),
	}),
});

// Editor role level from @emdash-cms/auth
const ROLE_EDITOR = 40;

function ContentEditPage() {
	const { t } = useLingui();
	const { collection, id } = useParams({
		from: "/_admin/content/$collection/$id",
	});
	const searchParams = useSearch({
		from: "/_admin/content/$collection/$id",
	});
	const queryClient = useQueryClient();
	const navigate = useNavigate();
	const toastManager = Toast.useToastManager();

	const { data: manifest } = useQuery({
		queryKey: ["manifest"],
		queryFn: fetchManifest,
	});

	const i18n = manifest?.i18n;
	const activeLocale = i18n ? (searchParams.locale ?? i18n.defaultLocale) : undefined;

	const { data: rawItem, isLoading } = useQuery({
		queryKey: ["content", collection, id, { locale: activeLocale }],
		queryFn: () => fetchContent(collection, id, { locale: activeLocale }),
		enabled: !i18n || !!activeLocale,
	});

	React.useEffect(() => {
		if (typeof searchParams.field !== "string" || isLoading) return;

		const timeoutId = requestIdleCallback(() => {
			const el = document.getElementById(`field-${searchParams.field}`);
			if (el) {
				el.scrollIntoView({ behavior: "smooth", block: "center" });
				el.focus();
				const { field: _, ...preservedSearch } = searchParams;
				void navigate({ search: preservedSearch as never, replace: true });
			}
		});
		return () => cancelIdleCallback(timeoutId);
	}, [searchParams, isLoading, navigate]);

	// Fetch translations when i18n is enabled
	const { data: translationsData } = useQuery({
		queryKey: ["translations", collection, id],
		queryFn: () => fetchTranslations(collection, id),
		enabled: !!i18n && !!rawItem,
	});

	// When a draft revision exists, fetch its data for the editor form.
	// The content table holds published data; the draft revision holds
	// the editor's working copy.
	const { data: draftRevision } = useQuery({
		queryKey: ["revision", rawItem?.draftRevisionId],
		queryFn: () => fetchRevision(rawItem!.draftRevisionId!),
		enabled: !!rawItem?.draftRevisionId,
	});

	// Merge draft revision data into the item for the editor.
	// The item's metadata (id, status, slug, etc.) comes from the content table;
	// the data fields come from the draft revision if available.
	const item = React.useMemo(() => {
		if (!rawItem) return undefined;
		if (!draftRevision?.data) return rawItem;
		// Strip revision metadata keys (prefixed with _)
		const draftData: Record<string, unknown> = {};
		for (const [key, value] of Object.entries(draftRevision.data)) {
			if (!key.startsWith("_")) {
				draftData[key] = value;
			}
		}
		// Draft slug override
		const draftSlug =
			typeof draftRevision.data._slug === "string" ? draftRevision.data._slug : rawItem.slug;
		return {
			...rawItem,
			slug: draftSlug,
			data: { ...rawItem.data, ...draftData },
		};
	}, [rawItem, draftRevision]);

	// Fetch current user for permission checks
	const { data: currentUser } = useQuery({
		queryKey: ["currentUser"],
		queryFn: async (): Promise<{ id: string; role: number }> => {
			const response = await apiFetch("/_emdash/api/auth/me");
			return parseApiResponse<{ id: string; role: number }>(response, t`Failed to fetch user`);
		},
		staleTime: 5 * 60 * 1000,
	});

	// Fetch users list for author selector (only if user is editor+)
	const { data: usersData } = useQuery({
		queryKey: ["users"],
		queryFn: () => fetchUsers({ limit: 100 }),
		enabled: !!currentUser && currentUser.role >= ROLE_EDITOR,
		staleTime: 5 * 60 * 1000,
	});

	// Picker is locale-pinned to the entry being edited. The credit
	// hydration server-side is strict per locale (migration 040), so the
	// picker must show only bylines that will actually render at this
	// locale — otherwise the editor adds a credit that silently vanishes
	// after autosave. Query disabled until `rawItem.locale` resolves so a
	// transient `undefined` doesn't populate the cache with default-locale
	// data.
	const itemLocale = rawItem?.locale ?? undefined;
	const { data: bylinesData, isSuccess: bylinesLoaded } = useQuery({
		queryKey: ["bylines", "picker", itemLocale ?? null],
		queryFn: () => fetchBylines({ locale: itemLocale, limit: 100 }),
		enabled: !!itemLocale,
	});

	const createBylineMutation = useMutation({
		mutationFn: (input: { slug: string; displayName: string }) =>
			createByline({ ...input, isGuest: true, locale: itemLocale }),
		onSuccess: () => {
			void queryClient.invalidateQueries({ queryKey: ["bylines"] });
		},
	});

	const updateBylineMutation = useMutation({
		mutationFn: (input: { id: string; slug: string; displayName: string }) =>
			updateByline(input.id, {
				slug: input.slug,
				displayName: input.displayName,
			}),
		onSuccess: () => {
			void queryClient.invalidateQueries({ queryKey: ["bylines"] });
		},
	});

	const updateMutation = useMutation({
		mutationFn: (data: {
			data?: Record<string, unknown>;
			slug?: string;
			authorId?: string | null;
			bylines?: BylineCreditInput[];
			skipRevision?: boolean;
			seo?: ContentSeoInput;
		}) => updateContent(collection, id, data, { locale: rawItem?.locale ?? activeLocale }),
		onSuccess: () => {
			// Invalidate by (collection, id) prefix without the locale object: the
			// editor's read query is keyed `{ locale: activeLocale }` (undefined when
			// i18n is off) while `rawItem.locale` is the DB default "en", so a
			// locale-scoped invalidation key would not match and the item would never
			// refetch — leaving the publish/save buttons stale until a hard refresh.
			void queryClient.invalidateQueries({
				queryKey: ["content", collection, id],
			});
			// Also invalidate revisions since a new one was created
			void queryClient.invalidateQueries({ queryKey: ["revisions", collection, id] });
			// Invalidate the cached draft revision so stale data doesn't overwrite the form
			if (rawItem?.draftRevisionId) {
				void queryClient.invalidateQueries({
					queryKey: ["revision", rawItem.draftRevisionId],
				});
			}
		},
		onError: (error) => {
			toastManager.add({
				title: t`Failed to save`,
				description: error instanceof Error ? error.message : t`An error occurred`,
				type: "error",
			});
		},
	});

	// Autosave mutation - skips revision creation
	const [lastAutosaveAt, setLastAutosaveAt] = React.useState<Date | null>(null);
	const autosaveMutation = useMutation({
		mutationFn: (data: {
			data?: Record<string, unknown>;
			slug?: string;
			bylines?: BylineCreditInput[];
		}) =>
			updateContent(
				collection,
				id,
				{ ...data, skipRevision: true },
				{ locale: rawItem?.locale ?? activeLocale },
			),
		onSuccess: (savedItem, variables) => {
			patchAutosaveQueries(queryClient, {
				collection,
				id,
				savedItem,
				payload: {
					data: variables.data,
					slug: variables.slug,
				},
				locale: rawItem?.locale ?? activeLocale,
			});
			setLastAutosaveAt(new Date());
			// Keep the cache fresh without refetching older server state back into the form
			// while the user is still typing.
		},
		onError: (err) => {
			toastManager.add({
				title: t`Autosave failed`,
				description: err instanceof Error ? err.message : t`An error occurred`,
				type: "error",
			});
		},
	});

	const publishMutation = useMutation({
		mutationFn: () => publishContent(collection, id, { locale: rawItem?.locale ?? activeLocale }),
		onSuccess: () => {
			void queryClient.invalidateQueries({
				queryKey: ["content", collection, id],
			});
			void queryClient.invalidateQueries({ queryKey: ["revisions", collection, id] });
			toastManager.add({ title: t`Published`, description: t`Content is now live` });
		},
		onError: (error) => {
			toastManager.add({
				title: t`Failed to publish`,
				description: error instanceof Error ? error.message : t`An error occurred`,
				type: "error",
			});
		},
	});

	const unpublishMutation = useMutation({
		mutationFn: () => unpublishContent(collection, id, { locale: rawItem?.locale ?? activeLocale }),
		onSuccess: () => {
			void queryClient.invalidateQueries({
				queryKey: ["content", collection, id],
			});
			void queryClient.invalidateQueries({ queryKey: ["revisions", collection, id] });
			toastManager.add({ title: t`Unpublished`, description: t`Content removed from public view` });
		},
		onError: (error) => {
			toastManager.add({
				title: t`Failed to unpublish`,
				description: error instanceof Error ? error.message : t`An error occurred`,
				type: "error",
			});
		},
	});

	const discardDraftMutation = useMutation({
		mutationFn: () => discardDraft(collection, id, { locale: rawItem?.locale ?? activeLocale }),
		onSuccess: () => {
			void queryClient.invalidateQueries({
				queryKey: ["content", collection, id],
			});
			void queryClient.invalidateQueries({ queryKey: ["revisions", collection, id] });
			toastManager.add({
				title: t`Changes discarded`,
				description: t`Reverted to published version`,
			});
		},
		onError: (error) => {
			toastManager.add({
				title: t`Failed to discard changes`,
				description: error instanceof Error ? error.message : t`An error occurred`,
				type: "error",
			});
		},
	});

	const scheduleMutation = useMutation({
		mutationFn: (scheduledAt: string) =>
			scheduleContent(collection, id, scheduledAt, { locale: rawItem?.locale ?? activeLocale }),
		onSuccess: () => {
			void queryClient.invalidateQueries({
				queryKey: ["content", collection, id],
			});
			toastManager.add({
				title: t`Scheduled`,
				description: t`Content has been scheduled for publishing`,
			});
		},
		onError: (error) => {
			toastManager.add({
				title: t`Failed to schedule`,
				description: error instanceof Error ? error.message : t`An error occurred`,
				type: "error",
			});
		},
	});

	const unscheduleMutation = useMutation({
		mutationFn: () =>
			unscheduleContent(collection, id, { locale: rawItem?.locale ?? activeLocale }),
		onSuccess: () => {
			void queryClient.invalidateQueries({
				queryKey: ["content", collection, id],
			});
			toastManager.add({
				title: t`Unscheduled`,
				description: t`Content reverted to draft`,
			});
		},
		onError: (error) => {
			toastManager.add({
				title: t`Failed to unschedule`,
				description: error instanceof Error ? error.message : t`An error occurred`,
				type: "error",
			});
		},
	});

	// Create translation mutation
	const translateMutation = useMutation({
		mutationFn: (locale: string) =>
			createContent(collection, {
				data: rawItem?.data ?? {},
				slug: rawItem?.slug ?? undefined,
				locale,
				translationOf: id,
			}),
		onSuccess: (result) => {
			void queryClient.invalidateQueries({ queryKey: ["translations", collection, id] });
			void queryClient.invalidateQueries({ queryKey: ["content", collection] });
			void navigate({
				to: "/content/$collection/$id",
				params: { collection, id: result.id },
				search: { locale: result.locale },
			});
			toastManager.add({
				title: t`Translation created`,
				description: t`Created ${result.locale?.toUpperCase() ?? t`new`} translation`,
			});
		},
		onError: (error) => {
			toastManager.add({
				title: t`Failed to create translation`,
				description: error instanceof Error ? error.message : t`An error occurred`,
				type: "error",
			});
		},
	});

	const deleteMutation = useMutation({
		mutationFn: () => deleteContent(collection, id, { locale: rawItem?.locale ?? activeLocale }),
		onSuccess: () => {
			void queryClient.invalidateQueries({ queryKey: ["content", collection] });
			void queryClient.invalidateQueries({ queryKey: ["content", collection, "trash"] });
			void navigate({
				to: "/content/$collection",
				params: { collection },
				search: { locale: activeLocale },
			});
		},
		onError: (error) => {
			toastManager.add({
				title: t`Failed to delete`,
				description: error instanceof Error ? error.message : t`An error occurred`,
				type: "error",
			});
		},
	});

	const pluginBlocks = React.useMemo(() => (manifest ? getPluginBlocks(manifest) : []), [manifest]);

	if (!manifest) {
		return <LoadingScreen />;
	}

	const collectionConfig = manifest.collections[collection];

	if (!collectionConfig) {
		return <NotFoundPage message={`Collection "${collection}" not found`} />;
	}

	if (isLoading) {
		return <LoadingScreen />;
	}

	const handleSave = (payload: {
		data: Record<string, unknown>;
		slug?: string;
		bylines?: BylineCreditInput[];
	}) => {
		updateMutation.mutate(payload);
	};

	const handleAutosave = (payload: {
		data: Record<string, unknown>;
		slug?: string;
		bylines?: BylineCreditInput[];
	}) => {
		autosaveMutation.mutate(payload);
	};

	const handleAuthorChange = (authorId: string | null) => {
		updateMutation.mutate({ authorId });
	};

	const handleSeoChange = (seo: ContentSeoInput) => {
		updateMutation.mutate({ seo });
	};

	return (
		<ContentEditor
			collection={collection}
			collectionLabel={collectionConfig.labelSingular || collectionConfig.label}
			item={item}
			fields={collectionConfig.fields}
			isSaving={updateMutation.isPending}
			onSave={handleSave}
			onAutosave={handleAutosave}
			isAutosaving={autosaveMutation.isPending}
			lastAutosaveAt={lastAutosaveAt}
			onPublish={() => publishMutation.mutate()}
			onUnpublish={() => unpublishMutation.mutate()}
			onDiscardDraft={() => discardDraftMutation.mutate()}
			onSchedule={(scheduledAt) => scheduleMutation.mutate(scheduledAt)}
			onUnschedule={() => unscheduleMutation.mutate()}
			isScheduling={scheduleMutation.isPending}
			onDelete={() => deleteMutation.mutate()}
			isDeleting={deleteMutation.isPending}
			supportsDrafts={collectionConfig.supports.includes("drafts")}
			supportsRevisions={collectionConfig.supports.includes("revisions")}
			supportsPreview={collectionConfig.supports.includes("preview")}
			currentUser={currentUser}
			users={usersData?.items}
			onAuthorChange={handleAuthorChange}
			i18n={i18n}
			translations={translationsData?.translations}
			onTranslate={(locale) => translateMutation.mutate(locale)}
			pluginBlocks={pluginBlocks}
			hasSeo={collectionConfig.hasSeo}
			onSeoChange={handleSeoChange}
			availableBylines={bylinesData?.items}
			availableBylinesLoaded={bylinesLoaded}
			onQuickCreateByline={async (input) => {
				const created = await createBylineMutation.mutateAsync(input);
				return created;
			}}
			onQuickEditByline={async (bylineId, input) => {
				const updated = await updateBylineMutation.mutateAsync({ id: bylineId, ...input });
				return updated;
			}}
			manifest={manifest ?? null}
		/>
	);
}

// Media library route
const mediaRoute = createRoute({
	getParentRoute: () => adminLayoutRoute,
	path: "/media",
	component: MediaPage,
});

function MediaPage() {
	const queryClient = useQueryClient();

	// Filename search + MIME type filter for the local library (server-side).
	const [search, setSearch] = React.useState("");
	const [mimeFilter, setMimeFilter] = React.useState<string | string[] | undefined>(undefined);
	const mimeKey = Array.isArray(mimeFilter) ? mimeFilter.join(",") : (mimeFilter ?? "");

	const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading, error } =
		useInfiniteQuery({
			queryKey: ["media", { search, mime: mimeKey }],
			queryFn: ({ pageParam }) =>
				fetchMediaList({
					cursor: pageParam,
					limit: 100,
					search: search || undefined,
					mimeType: mimeFilter,
				}),
			initialPageParam: undefined as string | undefined,
			getNextPageParam: (lastPage) => lastPage.nextCursor,
		});

	const uploadMutation = useMutation({
		mutationFn: (file: File) => uploadMedia(file),
		onSuccess: () => {
			void queryClient.invalidateQueries({ queryKey: ["media"] });
		},
	});

	const deleteMutation = useMutation({
		mutationFn: (id: string) => deleteMedia(id),
		onSuccess: () => {
			void queryClient.invalidateQueries({ queryKey: ["media"] });
		},
	});

	const items = React.useMemo(() => {
		return data?.pages.flatMap((page) => page.items) || [];
	}, [data]);

	if (error) {
		return <ErrorScreen error={error.message} />;
	}

	return (
		<MediaLibrary
			items={items}
			isLoading={isLoading || isFetchingNextPage}
			hasMore={!!hasNextPage}
			onLoadMore={() => void fetchNextPage()}
			onUpload={(file) => uploadMutation.mutate(file)}
			onDelete={(id) => deleteMutation.mutate(id)}
			onLocalSearchChange={setSearch}
			onLocalMimeFilterChange={setMimeFilter}
		/>
	);
}

// Comments moderation inbox route
const commentsRoute = createRoute({
	getParentRoute: () => adminLayoutRoute,
	path: "/comments",
	component: CommentsPage,
});

// Admin role level from @emdash-cms/auth
const ROLE_ADMIN = 50;

function CommentsPage() {
	const { t } = useLingui();
	const queryClient = useQueryClient();
	const toastManager = Toast.useToastManager();

	const { data: manifest } = useQuery({
		queryKey: ["manifest"],
		queryFn: fetchManifest,
	});

	// Current user for ADMIN check (hard delete)
	const { data: currentUser } = useQuery({
		queryKey: ["currentUser"],
		queryFn: async (): Promise<{ id: string; role: number }> => {
			const response = await apiFetch("/_emdash/api/auth/me");
			return parseApiResponse<{ id: string; role: number }>(response, t`Failed to fetch user`);
		},
		staleTime: 5 * 60 * 1000,
	});

	// Filter state
	const [activeStatus, setActiveStatus] = React.useState<CommentStatus>("pending");
	const [collectionFilter, setCollectionFilter] = React.useState("");
	const [searchQuery, setSearchQuery] = React.useState("");
	const [debouncedSearch, setDebouncedSearch] = React.useState("");

	// Debounce search
	React.useEffect(() => {
		const timer = setTimeout(setDebouncedSearch, 300, searchQuery);
		return () => clearTimeout(timer);
	}, [searchQuery]);

	// Fetch comments
	const {
		data: commentsData,
		isLoading,
		fetchNextPage,
		hasNextPage,
	} = useInfiniteQuery({
		queryKey: ["comments", activeStatus, collectionFilter, debouncedSearch],
		queryFn: ({ pageParam }) =>
			fetchComments({
				status: activeStatus,
				collection: collectionFilter || undefined,
				search: debouncedSearch || undefined,
				cursor: pageParam,
				limit: 50,
			}),
		initialPageParam: undefined as string | undefined,
		getNextPageParam: (lastPage) => lastPage.nextCursor,
	});

	// Fetch counts
	const { data: counts } = useQuery({
		queryKey: ["commentCounts"],
		queryFn: fetchCommentCounts,
	});

	// Status change mutation
	const statusMutation = useMutation({
		mutationFn: ({ id, status }: { id: string; status: CommentStatus }) =>
			updateCommentStatus(id, status),
		onSuccess: () => {
			void queryClient.invalidateQueries({ queryKey: ["comments"] });
			void queryClient.invalidateQueries({ queryKey: ["commentCounts"] });
		},
		onError: (error) => {
			toastManager.add({
				title: t`Failed to update status`,
				description: error instanceof Error ? error.message : t`An error occurred`,
				type: "error",
			});
		},
	});

	// Delete mutation
	const deleteMutation = useMutation({
		mutationFn: (id: string) => deleteComment(id),
		onSuccess: () => {
			void queryClient.invalidateQueries({ queryKey: ["comments"] });
			void queryClient.invalidateQueries({ queryKey: ["commentCounts"] });
		},
		onError: (error) => {
			toastManager.add({
				title: t`Failed to delete comment`,
				description: error instanceof Error ? error.message : t`An error occurred`,
				type: "error",
			});
		},
	});

	// Bulk action mutation
	const bulkMutation = useMutation({
		mutationFn: ({
			ids,
			action,
		}: {
			ids: string[];
			action: "approve" | "spam" | "trash" | "delete";
		}) => bulkCommentAction(ids, action),
		onSuccess: (result) => {
			void queryClient.invalidateQueries({ queryKey: ["comments"] });
			void queryClient.invalidateQueries({ queryKey: ["commentCounts"] });
			toastManager.add({
				title: plural(result.affected, { one: "# comment updated", other: "# comments updated" }),
			});
		},
		onError: (error) => {
			toastManager.add({
				title: t`Failed to perform bulk action`,
				description: error instanceof Error ? error.message : t`An error occurred`,
				type: "error",
			});
		},
	});

	const allComments = commentsData?.pages.flatMap((p) => p.items) ?? [];
	const lastPage = commentsData?.pages[commentsData.pages.length - 1];

	// Require EDITOR role for comment moderation
	if (currentUser && currentUser.role < ROLE_EDITOR) {
		return (
			<div className="flex items-center justify-center min-h-[50vh]">
				<div className="text-center">
					<h1 className="text-2xl font-bold">{t`Access Denied`}</h1>
					<p className="mt-2 text-kumo-subtle">{t`You need Editor permissions to moderate comments.`}</p>
				</div>
			</div>
		);
	}

	return (
		<CommentInbox
			comments={allComments}
			counts={counts ?? { pending: 0, approved: 0, spam: 0, trash: 0 }}
			isLoading={isLoading}
			nextCursor={lastPage?.nextCursor}
			collections={manifest?.collections ?? {}}
			activeStatus={activeStatus}
			onStatusChange={setActiveStatus}
			collectionFilter={collectionFilter}
			onCollectionFilterChange={setCollectionFilter}
			searchQuery={searchQuery}
			onSearchChange={setSearchQuery}
			onCommentStatusChange={(id, status) =>
				statusMutation.mutateAsync({ id, status }).catch(() => {})
			}
			onCommentDelete={(id) => deleteMutation.mutateAsync(id).catch(() => {})}
			onBulkAction={(ids, action) => bulkMutation.mutateAsync({ ids, action }).catch(() => {})}
			onLoadMore={() => {
				if (hasNextPage) void fetchNextPage();
			}}
			isAdmin={(currentUser?.role ?? 0) >= ROLE_ADMIN}
			isStatusPending={
				statusMutation.isPending || deleteMutation.isPending || bulkMutation.isPending
			}
			deleteError={deleteMutation.error}
			onDeleteErrorReset={() => deleteMutation.reset()}
		/>
	);
}

// Settings route
const settingsRoute = createRoute({
	getParentRoute: () => adminLayoutRoute,
	path: "/settings",
	component: Settings,
});

// Security settings route
const securitySettingsRoute = createRoute({
	getParentRoute: () => adminLayoutRoute,
	path: "/settings/security",
	component: SecuritySettings,
});

// Allowed domains settings route
const allowedDomainsSettingsRoute = createRoute({
	getParentRoute: () => adminLayoutRoute,
	path: "/settings/allowed-domains",
	component: AllowedDomainsSettings,
});

// API tokens settings route
const apiTokenSettingsRoute = createRoute({
	getParentRoute: () => adminLayoutRoute,
	path: "/settings/api-tokens",
	component: ApiTokenSettings,
});

// Email settings route
const emailSettingsRoute = createRoute({
	getParentRoute: () => adminLayoutRoute,
	path: "/settings/email",
	component: EmailSettings,
});

// General settings route
const generalSettingsRoute = createRoute({
	getParentRoute: () => adminLayoutRoute,
	path: "/settings/general",
	component: GeneralSettings,
});

// Social settings route
const socialSettingsRoute = createRoute({
	getParentRoute: () => adminLayoutRoute,
	path: "/settings/social",
	component: SocialSettings,
});

// SEO settings route
const seoSettingsRoute = createRoute({
	getParentRoute: () => adminLayoutRoute,
	path: "/settings/seo",
	component: SeoSettings,
});

// Plugin manager route
const pluginManagerRoute = createRoute({
	getParentRoute: () => adminLayoutRoute,
	path: "/plugins-manager",
	component: PluginManagerPage,
});

function PluginManagerPage() {
	const { data: manifest } = useQuery({
		queryKey: ["manifest"],
		queryFn: fetchManifest,
	});
	return <PluginManager manifest={manifest} />;
}

// Marketplace browse route
const marketplaceBrowseRoute = createRoute({
	getParentRoute: () => adminLayoutRoute,
	path: "/plugins/marketplace",
	component: MarketplaceBrowsePage,
});

function MarketplaceBrowsePage() {
	const { data: manifest } = useQuery({
		queryKey: ["manifest"],
		queryFn: fetchManifest,
	});

	const { data: plugins } = useQuery({
		queryKey: ["plugins"],
		queryFn: async () => {
			const { fetchPlugins } = await import("./lib/api/plugins.js");
			return fetchPlugins();
		},
	});

	const installedIds = React.useMemo(() => {
		if (!plugins) return new Set<string>();
		return new Set(plugins.map((p) => p.id));
	}, [plugins]);

	// When `experimental.registry` is configured, the registry browse
	// replaces the centralized marketplace browse on this route. Existing
	// sidebar / deep links stay valid; users see the registry without any
	// path change.
	if (manifest?.registry) {
		// Map installed registry plugins to their AT URIs for the
		// "Installed" badge on browse cards.
		const installedRegistryUris = new Set<string>(
			(plugins ?? [])
				.filter((p) => p.source === "registry" && p.registryPublisherDid && p.registrySlug)
				.map(
					(p) =>
						`at://${p.registryPublisherDid}/com.emdashcms.experimental.package.profile/${p.registrySlug}`,
				),
		);
		return (
			<RegistryBrowse config={manifest.registry} installedRegistryUris={installedRegistryUris} />
		);
	}

	return <MarketplaceBrowse installedPluginIds={installedIds} />;
}

// Marketplace plugin detail route
const marketplaceDetailRoute = createRoute({
	getParentRoute: () => adminLayoutRoute,
	path: "/plugins/marketplace/$pluginId",
	component: MarketplaceDetailPage,
});

function MarketplaceDetailPage() {
	const { pluginId } = useParams({ from: "/_admin/plugins/marketplace/$pluginId" });

	const { data: manifest } = useQuery({
		queryKey: ["manifest"],
		queryFn: fetchManifest,
	});

	const { data: plugins } = useQuery({
		queryKey: ["plugins"],
		queryFn: async () => {
			const { fetchPlugins } = await import("./lib/api/plugins.js");
			return fetchPlugins();
		},
	});

	const installedIds = React.useMemo(() => {
		if (!plugins) return new Set<string>();
		return new Set(plugins.map((p) => p.id));
	}, [plugins]);

	// Discriminate by param shape, not by the manifest flag. A registry
	// pluginId is always `${handle}/${slug}` and contains exactly one `/`;
	// a marketplace pluginId is a single segment with no `/`. This keeps
	// deep links to marketplace-installed plugins working on sites that
	// later opt into the registry, instead of unconditionally routing
	// every visit to RegistryPluginDetail.
	const looksLikeRegistryId = pluginId.includes("/");
	if (manifest?.registry && looksLikeRegistryId) {
		return <RegistryPluginDetail pluginId={pluginId} config={manifest.registry} />;
	}

	return <MarketplacePluginDetail pluginId={pluginId} installedPluginIds={installedIds} />;
}

// Theme marketplace browse route
const themeMarketplaceBrowseRoute = createRoute({
	getParentRoute: () => adminLayoutRoute,
	path: "/themes/marketplace",
	component: ThemeMarketplaceBrowse,
});

// Theme marketplace detail route
const themeMarketplaceDetailRoute = createRoute({
	getParentRoute: () => adminLayoutRoute,
	path: "/themes/marketplace/$themeId",
	component: ThemeDetailPage,
});

function ThemeDetailPage() {
	const { themeId } = useParams({ from: "/_admin/themes/marketplace/$themeId" });
	return <ThemeMarketplaceDetail themeId={themeId} />;
}

// WordPress import route
const wordpressImportRoute = createRoute({
	getParentRoute: () => adminLayoutRoute,
	path: "/import/wordpress",
	component: WordPressImport,
});

// Menu routes
const menuListRoute = createRoute({
	getParentRoute: () => adminLayoutRoute,
	path: "/menus",
	component: MenuList,
});

const menuEditorRoute = createRoute({
	getParentRoute: () => adminLayoutRoute,
	path: "/menus/$name",
	component: MenuEditor,
	validateSearch: (search: Record<string, unknown>) => {
		return {
			locale: typeof search.locale === "string" ? search.locale : undefined,
		};
	},
});

// Taxonomy manager route
const taxonomyRoute = createRoute({
	getParentRoute: () => adminLayoutRoute,
	path: "/taxonomies/$taxonomy",
	component: TaxonomyPage,
});

function TaxonomyPage() {
	const { taxonomy } = useParams({ from: "/_admin/taxonomies/$taxonomy" });
	return <TaxonomyManager taxonomyName={taxonomy} />;
}

// Widgets route
const widgetsRoute = createRoute({
	getParentRoute: () => adminLayoutRoute,
	path: "/widgets",
	component: Widgets,
});

// Sections routes
const redirectsRoute = createRoute({
	getParentRoute: () => adminLayoutRoute,
	path: "/redirects",
	component: Redirects,
});

const sectionsListRoute = createRoute({
	getParentRoute: () => adminLayoutRoute,
	path: "/sections",
	component: Sections,
});

const sectionEditRoute = createRoute({
	getParentRoute: () => adminLayoutRoute,
	path: "/sections/$slug",
	component: SectionEditor,
});

// Users route
const usersRoute = createRoute({
	getParentRoute: () => adminLayoutRoute,
	path: "/users",
	component: UsersPage,
});

// Bylines route
//
// `validateSearch` rejects empty-string locale (`?locale=`) — left as `""`
// it would land in component state and silently drop the locale filter
// from `fetchBylines`, fetching every locale's rows while the UI thinks
// it's scoped to one.
export function parseBylinesLocaleSearch(search: Record<string, unknown>): {
	locale: string | undefined;
} {
	if (typeof search.locale === "string" && search.locale.length > 0) {
		return { locale: search.locale };
	}
	return { locale: undefined };
}

const bylinesRoute = createRoute({
	getParentRoute: () => adminLayoutRoute,
	path: "/bylines",
	component: BylinesPage,
	validateSearch: parseBylinesLocaleSearch,
});

// Byline schema management route (Discussion #1174, Phase 5).
// `minRole: ROLE_ADMIN` is enforced both in the sidebar (entry hidden
// for non-admins) and inside `BylineSchemaPage` (URL-direct navigation).
const bylineSchemaRoute = createRoute({
	getParentRoute: () => adminLayoutRoute,
	path: "/byline-schema",
	component: BylineSchemaPage,
});

// Content Types routes
const contentTypesListRoute = createRoute({
	getParentRoute: () => adminLayoutRoute,
	path: "/content-types",
	component: ContentTypesListPage,
});

function ContentTypesListPage() {
	const queryClient = useQueryClient();

	const {
		data: collections,
		isLoading: collectionsLoading,
		error: collectionsError,
	} = useQuery({
		queryKey: ["schema", "collections"],
		queryFn: fetchCollections,
	});

	const {
		data: orphanedTables,
		isLoading: orphansLoading,
		error: orphansError,
	} = useQuery({
		queryKey: ["schema", "orphans"],
		queryFn: fetchOrphanedTables,
	});

	const deleteMutation = useMutation({
		mutationFn: (slug: string) => deleteCollection(slug, true),
		onSuccess: () => {
			void queryClient.invalidateQueries({ queryKey: ["schema", "collections"] });
			void queryClient.invalidateQueries({ queryKey: ["manifest"] });
		},
	});

	const registerOrphanMutation = useMutation({
		mutationFn: (slug: string) => registerOrphanedTable(slug),
		onSuccess: () => {
			void queryClient.invalidateQueries({ queryKey: ["schema", "collections"] });
			void queryClient.invalidateQueries({ queryKey: ["schema", "orphans"] });
			void queryClient.invalidateQueries({ queryKey: ["manifest"] });
		},
	});

	const error = collectionsError || orphansError;
	if (error) {
		return <ErrorScreen error={error.message} />;
	}

	return (
		<ContentTypeList
			collections={collections ?? []}
			orphanedTables={orphanedTables}
			isLoading={collectionsLoading || orphansLoading}
			onDelete={(slug) => deleteMutation.mutate(slug)}
			onRegisterOrphan={(slug) => registerOrphanMutation.mutate(slug)}
		/>
	);
}

const contentTypesNewRoute = createRoute({
	getParentRoute: () => adminLayoutRoute,
	path: "/content-types/new",
	component: ContentTypesNewPage,
});

function ContentTypesNewPage() {
	const navigate = useNavigate();
	const queryClient = useQueryClient();

	const createMutation = useMutation({
		mutationFn: (input: CreateCollectionInput) => createCollection(input),
		onSuccess: (result) => {
			void queryClient.invalidateQueries({ queryKey: ["schema", "collections"] });
			void queryClient.invalidateQueries({ queryKey: ["manifest"] });
			void navigate({
				to: "/content-types/$slug",
				params: { slug: result.slug },
			});
		},
	});

	return (
		<ContentTypeEditor
			isNew
			isSaving={createMutation.isPending}
			onSave={(input) => {
				createMutation.mutate(input as CreateCollectionInput);
			}}
		/>
	);
}

const contentTypesEditRoute = createRoute({
	getParentRoute: () => adminLayoutRoute,
	path: "/content-types/$slug",
	component: ContentTypesEditPage,
});

function ContentTypesEditPage() {
	const { slug } = useParams({ from: "/_admin/content-types/$slug" });
	const queryClient = useQueryClient();
	const toastManager = Toast.useToastManager();
	const { t } = useLingui();

	const {
		data: collection,
		isLoading,
		error,
	} = useQuery({
		queryKey: ["schema", "collections", slug],
		queryFn: () => fetchCollection(slug),
	});

	const updateMutation = useMutation({
		mutationFn: async (input: UpdateCollectionInput) => {
			// Check if search support is being toggled
			const oldSupports = collection?.supports ?? [];
			const newSupports = input.supports ?? oldSupports;
			const hadSearch = oldSupports.includes("search");
			const hasSearch = newSupports.includes("search");

			// Update the collection first
			const result = await updateCollection(slug, input);

			// If search support changed, enable/disable search
			if (hadSearch !== hasSearch) {
				try {
					await setSearchEnabled(slug, hasSearch);
				} catch (err) {
					// Log but don't fail the mutation - search can be enabled manually
					console.error("Failed to toggle search:", err);
				}
			}

			return result;
		},
		onSuccess: () => {
			void queryClient.invalidateQueries({
				queryKey: ["schema", "collections", slug],
			});
			void queryClient.invalidateQueries({ queryKey: ["schema", "collections"] });
			void queryClient.invalidateQueries({ queryKey: ["manifest"] });
		},
		onError: (mutationError) => {
			toastManager.add({
				title: t`Failed to save`,
				description: mutationError instanceof Error ? mutationError.message : t`An error occurred`,
				type: "error",
			});
		},
	});

	const addFieldMutation = useMutation({
		mutationFn: (input: CreateFieldInput) => createField(slug, input),
		onSuccess: () => {
			void queryClient.invalidateQueries({
				queryKey: ["schema", "collections", slug],
			});
			void queryClient.invalidateQueries({ queryKey: ["manifest"] });
		},
	});

	const updateFieldMutation = useMutation({
		mutationFn: ({ fieldSlug, input }: { fieldSlug: string; input: CreateFieldInput }) =>
			updateField(slug, fieldSlug, input),
		onSuccess: () => {
			void queryClient.invalidateQueries({
				queryKey: ["schema", "collections", slug],
			});
			void queryClient.invalidateQueries({ queryKey: ["manifest"] });
		},
	});

	const deleteFieldMutation = useMutation({
		mutationFn: (fieldSlug: string) => deleteField(slug, fieldSlug),
		onSuccess: () => {
			void queryClient.invalidateQueries({
				queryKey: ["schema", "collections", slug],
			});
			void queryClient.invalidateQueries({ queryKey: ["manifest"] });
		},
	});

	const reorderFieldsMutation = useMutation({
		mutationFn: (fieldSlugs: string[]) => reorderFields(slug, fieldSlugs),
		onSuccess: () => {
			void queryClient.invalidateQueries({
				queryKey: ["schema", "collections", slug],
			});
			void queryClient.invalidateQueries({ queryKey: ["manifest"] });
		},
	});

	if (error) {
		return <ErrorScreen error={error.message} />;
	}

	if (isLoading) {
		return <LoadingScreen />;
	}

	return (
		<ContentTypeEditor
			collection={collection}
			isSaving={updateMutation.isPending}
			onSave={(input) => updateMutation.mutate(input as UpdateCollectionInput)}
			onAddField={(input) => addFieldMutation.mutateAsync(input)}
			onUpdateField={(fieldSlug, input) => updateFieldMutation.mutateAsync({ fieldSlug, input })}
			onDeleteField={(fieldSlug) => deleteFieldMutation.mutate(fieldSlug)}
			onReorderFields={(fieldSlugs) => reorderFieldsMutation.mutate(fieldSlugs)}
		/>
	);
}

// Plugin page route
const pluginRoute = createRoute({
	getParentRoute: () => adminLayoutRoute,
	path: "/plugins/$pluginId/$",
	component: PluginPage,
});

function PluginPage() {
	const { pluginId } = useParams({ from: "/_admin/plugins/$pluginId/$" });
	const { _splat } = useParams({ from: "/_admin/plugins/$pluginId/$" });
	const pagePath = "/" + (_splat || "");

	// Get plugin page component from context (trusted plugins with React)
	const PluginComponent = usePluginPage(pluginId, pagePath);

	if (PluginComponent) {
		return <PluginComponent />;
	}

	// No React component — fall back to Block Kit rendering
	return <SandboxedPluginPage pluginId={pluginId} page={pagePath} />;
}

// Catch-all 404 route
const notFoundRoute = createRoute({
	getParentRoute: () => adminLayoutRoute,
	path: "*",
	component: () => <NotFoundPage />,
});

// Create route tree with admin routes under layout and setup route separate
const adminRoutes = adminLayoutRoute.addChildren([
	dashboardRoute,
	contentListRoute,
	contentNewRoute,
	contentEditRoute,
	contentTypesListRoute,
	contentTypesNewRoute,
	contentTypesEditRoute,
	mediaRoute,
	commentsRoute,
	menuListRoute,
	menuEditorRoute,
	pluginManagerRoute,
	marketplaceDetailRoute,
	marketplaceBrowseRoute,
	themeMarketplaceBrowseRoute,
	themeMarketplaceDetailRoute,
	pluginRoute,
	redirectsRoute,
	sectionsListRoute,
	sectionEditRoute,
	taxonomyRoute,
	usersRoute,
	bylinesRoute,
	bylineSchemaRoute,
	widgetsRoute,
	settingsRoute,
	generalSettingsRoute,
	socialSettingsRoute,
	seoSettingsRoute,
	securitySettingsRoute,
	allowedDomainsSettingsRoute,
	apiTokenSettingsRoute,
	emailSettingsRoute,
	wordpressImportRoute,
	notFoundRoute,
]);

const routeTree = baseRootRoute.addChildren([
	setupRoute,
	loginRoute,
	signupRoute,
	inviteAcceptRoute,
	deviceRoute,
	adminRoutes,
]);

// Create router
export function createAdminRouter(queryClient: QueryClient) {
	return createRouter({
		routeTree,
		context: { queryClient },
		basepath: "/_emdash/admin",
		defaultPreload: "intent",
	});
}

// Declare router type
declare module "@tanstack/react-router" {
	interface Register {
		router: ReturnType<typeof createAdminRouter>;
	}
}

// Shared components

function LoadingScreen() {
	const { t } = useLingui();
	return (
		<div className="flex items-center justify-center min-h-screen">
			<div className="text-center">
				<Loader />
				<p className="mt-4 text-kumo-subtle">{t`Loading configuration...`}</p>
			</div>
		</div>
	);
}

function ErrorScreen({ error }: { error: string }) {
	const { t } = useLingui();
	return (
		<div className="flex items-center justify-center min-h-screen">
			<div className="text-center">
				<h1 className="text-xl font-bold text-kumo-danger">{t`Error`}</h1>
				<p className="mt-2 text-kumo-subtle">{error}</p>
				<Button onClick={() => window.location.reload()} className="mt-4">
					{t`Retry`}
				</Button>
			</div>
		</div>
	);
}

function NotFoundPage({ message }: { message?: string }) {
	const { t } = useLingui();
	return (
		<div className="flex items-center justify-center min-h-[50vh]">
			<div className="text-center">
				<h1 className="text-2xl font-bold">{t`Page Not Found`}</h1>
				<p className="mt-2 text-kumo-subtle">
					{message ?? t`The page you're looking for doesn't exist.`}
				</p>
				<Link to="/" className="mt-4 inline-block text-kumo-brand">
					{t`Go to Dashboard`}
				</Link>
			</div>
		</div>
	);
}

export { Link, useNavigate, useParams };
