import React, { lazy, Suspense, useEffect, useMemo, useState } from 'react';
import { __, sprintf } from '@wordpress/i18n';
import { api, env, moduleRunning } from './api';
import { readTheme, writeTheme, onThemeChange, syncAdminColour, clearAdminColour } from './theme.js';
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Insights = lazy(() => import('./pages/Insights'));
const Analyzer = lazy(() => import('./pages/Analyzer'));
const Content = lazy(() => import('./pages/Content'));
const LlmsTxt = lazy(() => import('./pages/LlmsTxt'));
const Crawlers = lazy(() => import('./pages/Crawlers'));
const SearchConsole = lazy(() => import('./pages/SearchConsole'));
const Seo = lazy(() => import('./pages/Seo'));
const Schema = lazy(() => import('./pages/Schema'));
const Ai = lazy(() => import('./pages/Ai'));
const AiVisibility = lazy(() => import('./pages/AiVisibility'));
const License = lazy(() => import('./pages/License'));
const SettingsModules = lazy(() => import('./pages/Settings').then((m) => ({ default: m.Modules })));
const DataPrivacy = lazy(() => import('./pages/Settings').then((m) => ({ default: m.DataPrivacy })));
const Environment = lazy(() => import('./pages/Settings').then((m) => ({ default: m.Environment })));
const Links = lazy(() => import('./pages/Links'));
const LinkStructure = lazy(() => import('./pages/LinkStructure'));
const Redirects = lazy(() => import('./pages/Redirects'));
const Bulk = lazy(() => import('./pages/Bulk'));
const Wizard = lazy(() => import('./pages/Wizard'));
const AiWizard = lazy(() => import('./pages/AiWizard'));
const Import = lazy(() => import('./pages/Import'));
const Google = lazy(() => import('./pages/Google'));
const Sitemap = lazy(() => import('./pages/Sitemap'));
const Advanced = lazy(() => import('./pages/Advanced'));
const LocalSeo = lazy(() => import('./pages/LocalSeo'));
const Analytics = lazy(() => import('./pages/Analytics'));
const LinkChecker = lazy(() => import('./pages/LinkChecker'));
const DevTools = lazy(() => import('./pages/DevTools'));
const Feeds = lazy(() => import('./pages/Feeds'));
const Shop = lazy(() => import('./pages/Shop'));
const Measure = lazy(() => import('./pages/Measure'));
const Coexistence = lazy(() => import('./pages/Coexistence'));
const SiteAudit = lazy(() => import('./pages/SiteAudit'));
const Urls = lazy(() => import('./pages/Urls'));
const SiteTests = lazy(() => import('./pages/SiteTests'));
const Autopilot = lazy(() => import('./pages/Autopilot'));
const Backlinks = lazy(() => import('./pages/Backlinks'));
const SeoApis = lazy(() => import('./pages/SeoApis'));
import WordStats from './components/WordStats';
import StoreAudit from './components/StoreAudit';
const Packs = lazy(() => import('./pages/Packs'));
import { Tabs, Verdict, Notice, ScoreRing, PageSkeleton, Card } from './components/ui';
import {
	IconGauge,
	IconFile,
	IconDoc,
	IconBot,
	IconChart,
	IconSearch,
	IconSliders,
	IconSparkle,
	IconSpark,
	IconEye,
	IconCode,
	IconLink,
	IconDownload,
	IconGoogle,
	IconMap,
	IconShield,
	IconPin,
	IconTrend,
	IconMail, IconLayers } from './components/icons';

/* ---------- Hub pages: related tools grouped under one nav item ---------- */

/* Two different jobs on one shop: the feed you send out, and the state of the
   catalogue it is built from. */
/** Shown for the few hundred milliseconds a page chunk takes to arrive. */
function PageLoading() {
	// The module's code is still downloading: show the shape it will take,
	// never an empty frame that reads as a broken screen.
	return <PageSkeleton blocks={3} />;
}

function FeedsHub({ tab, onTab }) {
	return (
		<div className="grid gap-4">
			<Tabs
				tabs={[
					{ id: 'feeds', label: __('Feeds', 'ai-rank-suite') },
					{ id: 'audit', label: __('Catalog audit', 'ai-rank-suite') },
				]}
				active={tab}
				onChange={onTab}
			/>
			{tab === 'audit' ? <StoreAudit /> : <Feeds />}
		</div>
	);
}

/*
 * Sub-tabs whose module is not installed are left out.
 *
 * A screen carries `needs` and gets a Pro badge; its tabs carried nothing, so
 * the free edition offered "URLs & languages" and "Bulk" — two Pro modules —
 * and clicking either produced "no route matched the URL", which reads as a
 * broken plugin rather than as a paid feature.
 */
function ContentHub({ tab, onTab }) {
	const { tabs, active } = usableTabs(
		[
			{ id: 'pages', label: __('Pages', 'ai-rank-suite') },
			{ id: 'urls', label: __('URLs & languages', 'ai-rank-suite'), needs: 'urls' },
			{ id: 'words', label: __('Words', 'ai-rank-suite') },
			{ id: 'bulk', label: __('Bulk', 'ai-rank-suite'), needs: 'bulk' },
		],
		tab
	);

	return (
		<div className="grid gap-4">
			<Tabs tabs={tabs} active={active} onChange={onTab} />
			{active === 'bulk' && <Bulk />}
			{active === 'words' && (
				<WordStats
					onOpen={(id) => {
						window.location.hash = `/content/${id}`;
					}}
				/>
			)}
			{active === 'urls' && <Urls />}
			{active !== 'bulk' && active !== 'words' && active !== 'urls' && <Content />}
		</div>
	);
}

/* Two different questions, so two tabs rather than one long page: which of my
   pages have problems, and is the site itself set up correctly. */
function AuditHub({ tab, onTab }) {
	return (
		<div className="grid gap-4">
			<Tabs
				tabs={[
					{ id: 'pages', label: __('Pages', 'ai-rank-suite') },
					{ id: 'site', label: __('Site tests', 'ai-rank-suite') },
				]}
				active={tab}
				onChange={onTab}
			/>
			{tab === 'site' ? <SiteTests /> : <SiteAudit />}
		</div>
	);
}

function LinksHub({ tab, onTab }) {
	const { tabs, active } = usableTabs(
		[
			{ id: 'checker', label: __('Checker', 'ai-rank-suite'), needs: 'link_checker' },
			{ id: 'internal', label: __('Internal', 'ai-rank-suite'), needs: 'internal_links' },
			// The link graph is built by the InternalLinks module, so this tab
			// needs it just as much as "Internal" does.
			{ id: 'structure', label: __('Structure', 'ai-rank-suite'), needs: 'internal_links' },
			{ id: 'redirects', label: __('Redirects', 'ai-rank-suite') },
		],
		tab
	);
	return (
		<div className="grid gap-4">
			<Tabs tabs={tabs} active={active} onChange={onTab} />
			{active === 'internal' ? (
				<Links />
			) : active === 'structure' ? (
				<LinkStructure />
			) : active === 'redirects' ? (
				<Redirects />
			) : (
				<LinkChecker />
			)}
		</div>
	);
}

/*
 * Measure opened with seven tabs, three of which were Pro badges in the free
 * edition. "Overview" and "Insights" answer the same question at two levels of
 * detail, so they are one screen with two tabs; the row above lost an entry
 * without losing a destination.
 */
function MeasureHub({ tab, onTab }) {
	const { tabs, active } = usableTabs(
		[
			{ id: 'overview', label: __('Overview', 'ai-rank-suite') },
			{ id: 'insights', label: __('Insights', 'ai-rank-suite'), needs: 'insights' },
		],
		tab
	);
	return (
		<div className="grid gap-4">
			{tabs.length > 1 && <Tabs tabs={tabs} active={active} onChange={onTab} />}
			{active === 'insights' ? <Insights /> : <Measure />}
		</div>
	);
}

/* The data providers are the keys that feed the backlink and rank numbers, so
   they belong behind the screen that spends them, not beside it. */
function BacklinksHub({ tab, onTab }) {
	const { tabs, active } = usableTabs(
		[
			{ id: 'links', label: __('Backlinks', 'ai-rank-suite'), needs: 'backlinks' },
			{ id: 'providers', label: __('Data providers', 'ai-rank-suite'), needs: 'seo_apis' },
		],
		tab
	);
	return (
		<div className="grid gap-4">
			{tabs.length > 1 && <Tabs tabs={tabs} active={active} onChange={onTab} />}
			{active === 'providers' ? <SeoApis /> : <Backlinks />}
		</div>
	);
}

function SettingsHub({ tab, onTab }) {
	return (
		<div className="grid gap-4">
			<Tabs
				tabs={[
					{ id: 'general', label: __('Modules', 'ai-rank-suite') },
					{ id: 'import', label: __('Import & backup', 'ai-rank-suite') },
					{ id: 'compat', label: __('Compatibility', 'ai-rank-suite') },
					{ id: 'env', label: __('Environment', 'ai-rank-suite') },
					{ id: 'data', label: __('Data', 'ai-rank-suite') },
					{ id: 'license', label: __('License', 'ai-rank-suite') },
				]}
				active={tab}
				onChange={onTab}
			/>
			{tab === 'license' ? (
				<License />
			) : tab === 'import' ? (
				<Import />
			) : tab === 'data' ? (
				<DataPrivacy />
			) : tab === 'compat' ? (
				<Coexistence />
			) : tab === 'env' ? (
				<Environment />
			) : (
				<SettingsModules />
			)}
		</div>
	);
}

/* ---------- Navigation ----------

   Grouped by the job the customer is doing, not by our module boundaries, and
   deliberately capped: "Google search" had grown to ten items, which is a
   dumping ground rather than a group. Anything that is a single card is a tab
   of its parent, never a nav entry of its own.
   ---------------------------------- */

/**
 * Drop the tabs whose module this build does not have.
 *
 * A screen carries `needs` and gets a Pro badge. Its tabs carried nothing, so
 * the free edition offered "URLs & languages", "Bulk", "Keywords" and
 * "Reports" — four Pro modules — and clicking any of them produced "no route
 * matched the URL". To a customer that is not a locked feature, it is a
 * broken plugin.
 *
 * @param {Array<{id: string, label: string, needs?: string}>} tabs
 * @param {string} active The tab the hash asks for.
 * @return {{tabs: Array<object>, active: string}} Filtered list, and a tab
 *         that exists in it — a hash pointing at a missing tab falls back to
 *         the first rather than rendering nothing.
 */
export const usableTabs = (tabs, active) => {
	const usable = tabs.filter((t) => !t.needs || moduleRunning(t.needs));
	// A hub whose every tab belongs to a module this build does not have
	// would otherwise read usable[0].id off nothing and take the screen down
	// with it. The caller renders its own default in that case.
	if (usable.length === 0) return { tabs: [], active };
	return {
		tabs: usable,
		active: usable.some((t) => t.id === active) ? active : usable[0].id,
	};
};


/** What stands where a Pro screen would be in the free edition. */
function ProScreen({ title }) {
	return (
		<div className="ars-page">
			<Card title={title}>
				<div className="ars-pro is-compact">
					<span className="ars-pro-badge">{__('Pro', 'ai-rank-suite')}</span>
					<div className="min-w-0">
						<p className="ars-pro-text">
							{__('This is part of AI Rank Suite Pro. Everything it has already collected on this site is kept — activating a licence brings the screen back with its data.', 'ai-rank-suite')}
						</p>
					</div>
					<a className="ars-btn ars-btn-ghost ars-pro-cta" href="#/settings/license">{__('See Pro', 'ai-rank-suite')}</a>
				</div>
			</Card>
		</div>
	);
}

const buildNav = () => [
	{ id: 'dashboard', label: __('Dashboard', 'ai-rank-suite'), icon: IconGauge, view: Dashboard, group: 'Overview' },
	{ id: 'measure', label: __('Overview', 'ai-rank-suite'), icon: IconChart, view: MeasureHub, group: 'Measure', tabs: ['overview', 'insights'] },
	{ id: 'content', label: __('Content', 'ai-rank-suite'), icon: IconFile, view: ContentHub, group: 'Optimize', tabs: ['pages', 'urls', 'words', 'bulk'] },
	{ id: 'audit', label: __('Site Audit', 'ai-rank-suite'), icon: IconShield, view: AuditHub, group: 'Optimize', tabs: ['pages', 'site'] },
	{ id: 'analyzer', label: __('Site analysis', 'ai-rank-suite'), icon: IconGauge, view: Analyzer, group: 'Overview' },
	{ id: 'packs', label: __('Industry packs', 'ai-rank-suite'), icon: IconSpark, view: Packs, group: 'Overview' , setup: true },

	{ id: 'autopilot', label: __('AI Autopilot', 'ai-rank-suite'), icon: IconSparkle, view: Autopilot, group: 'AI search', needs: 'autopilot' },
	{ id: 'ai-visibility', label: __('AI Answer Simulation', 'ai-rank-suite'), icon: IconEye, view: AiVisibility, group: 'AI search' , tabs: ['track', 'prompts', 'brand'], needs: 'ai_visibility' },
	{ id: 'llms-txt', label: __('llms.txt', 'ai-rank-suite'), icon: IconDoc, view: LlmsTxt, group: 'AI search' , tabs: ['short', 'full', 'dev'] , setup: true },
	{ id: 'crawlers', label: __('AI Crawlers', 'ai-rank-suite'), icon: IconBot, view: Crawlers, group: 'AI search' , setup: true },

	{ id: 'seo', label: __('SEO', 'ai-rank-suite'), icon: IconSearch, view: Seo, group: 'Search', tabs: ['general', 'templates', 'social', 'rss'] , setup: true },
	{ id: 'schema', label: __('Schema', 'ai-rank-suite'), icon: IconCode, view: Schema, group: 'Search' , tabs: ['overview', 'identity', 'templates', 'library', 'products'] , setup: true },
	{ id: 'sitemap', label: __('Sitemap', 'ai-rank-suite'), icon: IconMap, view: Sitemap, group: 'Search' , tabs: ['index', 'xml', 'content', 'special'] , setup: true },
	{ id: 'local-seo', label: __('Local SEO', 'ai-rank-suite'), icon: IconPin, view: LocalSeo, group: 'Search' , setup: true },
	{ id: 'links', label: __('Links', 'ai-rank-suite'), icon: IconLink, view: LinksHub, group: 'Optimize', tabs: ['checker', 'internal', 'structure', 'redirects'] },
	{ id: 'backlinks', label: __('Backlinks', 'ai-rank-suite'), icon: IconTrend, view: BacklinksHub, group: 'Measure', tabs: ['links', 'providers'], needs: 'backlinks' },

	{ id: 'search-console', label: __('Search Console', 'ai-rank-suite'), icon: IconChart, view: SearchConsole, group: 'Measure', tabs: ['performance', 'keywords', 'bing'] },
	{ id: 'google', label: __('Indexing', 'ai-rank-suite'), icon: IconGoogle, view: Google, group: 'Measure' , tabs: ['indexing', 'rich', 'bing', 'indexnow', 'verify'] },
	{ id: 'analytics', label: __('Analytics', 'ai-rank-suite'), icon: IconMail, view: Analytics, group: 'Measure' , tabs: ['ga4', 'matomo', 'clarity', 'consent', 'reports', 'speed'] },

	// Hidden entirely when there is no shop: a nav item whose only message is
	// "install WooCommerce" costs a row on every screen for nothing.
	{ id: 'shop', label: __('Shop', 'ai-rank-suite'), icon: IconLayers, view: Shop, group: 'Shop', when: () => !!env.hasWoo },
	{ id: 'feeds', label: __('Catalog feeds', 'ai-rank-suite'), icon: IconDownload, view: FeedsHub, group: 'Shop', when: () => !!env.hasWoo, tabs: ['feeds', 'audit'], needs: 'feeds' },

	{ id: 'ai', label: __('AI Engine', 'ai-rank-suite'), icon: IconSparkle, view: Ai, group: 'AI search', needs: 'ai' },
	{ id: 'advanced', label: __('Advanced', 'ai-rank-suite'), icon: IconShield, view: Advanced, group: 'System' , tabs: ['robots', 'htaccess', 'breadcrumbs', 'images', 'ghost'] , setup: true },
	/*
	 * Hooks, webhooks and the REST reference are developer reference, and on a
	 * site that never calls them the entry is a row of chrome on every screen
	 * in this group. It appears when WP_DEBUG is on, or when somebody turns it
	 * on under Settings, and the route itself keeps working either way so an
	 * existing bookmark does not break.
	 */
	{ id: 'developer', label: __('Developer', 'ai-rank-suite'), icon: IconCode, view: DevTools, group: 'System' , tabs: ['status', 'integrations', 'hooks', 'webhooks', 'api'] , setup: true, hidden: () => !env.showDeveloper },
	{ id: 'settings', label: __('Settings', 'ai-rank-suite'), icon: IconSliders, view: SettingsHub, group: 'System', tabs: ['general', 'import', 'compat', 'env', 'data', 'license'] , setup: true },
];

/**
 * The nav, built on first use rather than at module scope.
 *
 * A module-level const is evaluated when the bundle is imported, which is
 * before WordPress has delivered the locale data to wp.i18n — so every label
 * would be captured in English and stay English whatever language the site is
 * in. Memoised, because it is read on every render and the labels cannot
 * change without a reload.
 */
let NAV_CACHE = null;
const nav = () => {
	if (null === NAV_CACHE) NAV_CACHE = buildNav();
	return NAV_CACHE;
};

const VISIBLE = () => nav().filter((n) => !n.when || n.when());

/*
 * Two planes, not one list.
 *
 * Of twenty-five navigation entries, thirteen were configuration screens —
 * schema rules, sitemap options, robots, snippets, the wizard. They sat beside
 * the five screens that actually show you something, at the same size and in
 * the same type, so the plugin read as a settings application that happened to
 * contain some reports. Every comparable tool does the opposite: Semrush,
 * Ahrefs and Rank Math all put findings in the sidebar and fold setup behind
 * one entry, because you open the tool to learn something and configure it
 * three times a year.
 *
 * So: WORK is what the sidebar shows. SETUP is reached from one entry at the
 * bottom, which opens an index of every configuration screen. Nothing was
 * removed and every deep link still resolves — the difference is only which of
 * the two you have to walk past to reach the other.
 */
const WORK_GROUPS = ['Overview', 'Shop', 'Optimize', 'Measure', 'AI search'];
const SETUP_GROUPS = ['Search', 'AI search', 'Overview', 'System'];
const GROUPS = ['Overview', 'Shop', 'Optimize', 'Measure', 'AI search', 'Search', 'System'];

const WORK = () => VISIBLE().filter((n) => !n.setup);
const SETUP = () => VISIBLE().filter((n) => n.setup);

/**
 * Group captions are translated at render, never in the data.
 *
 * The group is an identity — it decides which items sit together and which
 * heading they sit under. Translating the key itself meant a German site
 * grouped items by "KI-Suche" while the section list still said "AI search",
 * so four of the six sections rendered their heading with nothing underneath.
 * An id and its caption are different things, and this is what happens when
 * they are the same string.
 */
const groupLabel = (group) =>
	({
		Overview: __('Overview', 'ai-rank-suite'),
		Shop: __('Shop', 'ai-rank-suite'),
		Optimize: __('Optimize', 'ai-rank-suite'),
		Measure: __('Measure', 'ai-rank-suite'),
		'AI search': __('AI search', 'ai-rank-suite'),
		Search: __('Search', 'ai-rank-suite'),
		System: __('System', 'ai-rank-suite'),
	})[group] || group;

/**
 * Which WordPress admin page owns a group of screens.
 *
 * The menu is six submenus, one per group; the screens inside a group are a
 * nav-tab row on that page. Two things need this map. A submenu has to open
 * its group's first screen — that comes from `data-route` on the mount node.
 * And a link from one group to another has to load the other group's page:
 * changing only the hash would leave WordPress highlighting the submenu the
 * visitor came from while the content is somewhere else entirely, which is
 * the single most disorienting thing an admin screen can do.
 *
 * PHP is the source (CoreModule::screens); this is the same map, delivered.
 */
const adminPages = () => (Array.isArray(env.screens) ? env.screens : []);
const pageForGroup = (group) => adminPages().find((p) => p.group === group) || null;
/**
 * The group on screen right now.
 *
 * Normally the admin page decides it — that is what `data-route` is for. But a
 * deep link saved before this menu existed can land on a screen that belongs
 * to another group, and then the row of tabs has to follow the content rather
 * than the URL: showing "Overview" above a schema editor helps nobody.
 */
const currentGroup = (pageId) => {
	const screen = pageId ? VISIBLE().find((n) => n.id === pageId) : null;
	if (screen && screen.group) return screen.group;
	const page = adminPages().find((p) => p.route === defaultRoute());
	return page ? page.group : 'Overview';
};

/* Old deep links keep working forever: page renames and merges must never
   break saved bookmarks or the cross-links baked into earlier releases. */
const ALIASES = {
	bulk: ['content', 'bulk'],
	'link-checker': ['links', 'checker'],
	'internal-links': ['links', 'internal'],
	redirects: ['links', 'redirects'],
	license: ['settings', 'license'],
	social: ['seo', 'social'],
	'rank-tracker': ['search-console', 'keywords'],
	coexistence: ['settings', 'compat'],
	bing: ['search-console', 'bing'],
	htaccess: ['advanced', 'htaccess'],
	compatibility: ['settings', 'compat'],
	// Moved in 0.2.46: Insights became a tab of Overview, the data providers a
	// tab of Backlinks, and Import a tab of Settings.
	insights: ['measure', 'insights'],
	'seo-apis': ['backlinks', 'providers'],
	import: ['settings', 'import'],
	// The short names people actually type, and the ones used in our own
	// documentation before these screens were renamed. Without them the hash
	// simply does not match, the router falls back to the dashboard, and the
	// person concludes the feature was removed. That failure is silent by
	// design — which is exactly why the list has to be generous.
	visibility: ['ai-visibility', null],
	local: ['local-seo', null],
	'local-business': ['local-seo', null],
	indexing: ['google', 'indexing'],
	'rich-results': ['google', 'rich'],
	indexnow: ['google', 'indexnow'],
	backup: ['import', null],
	export: ['import', null],
	tools: ['developer', 'status'],
	dev: ['developer', 'status'],
	matomo: ['analytics', 'matomo'],
	clarity: ['analytics', 'clarity'],
	ga4: ['analytics', 'ga4'],
	pagespeed: ['analytics', 'speed'],
	reports: ['analytics', 'reports'],
	robots: ['advanced', 'robots'],
	breadcrumbs: ['advanced', 'breadcrumbs'],
	images: ['advanced', 'images'],
	words: ['content', 'words'],
	products: ['schema', 'products'],
	store: ['feeds', 'audit'],
	woocommerce: ['feeds', 'audit'],
};

/**
 * The setup index.
 *
 * One screen listing every configuration area, so the thirteen entries it
 * replaces stop competing with the five screens that show you something. Each
 * card says what the area decides rather than repeating its own name, because
 * "Schema" tells somebody who already knows what schema is precisely nothing.
 */
function Setup({ onOpen }) {
	const WHAT = {
		seo: __('Title and description templates, social cards, and what gets indexed.', 'ai-rank-suite'),
		schema: __('Structured data: what each page tells Google and an assistant it is.', 'ai-rank-suite'),
		sitemap: __('Which URLs are listed for crawlers, and which are deliberately left out.', 'ai-rank-suite'),
		'local-seo': __('Business details, opening hours and locations, for local results and maps.', 'ai-rank-suite'),
		'llms-txt': __('The curated map of your site that AI crawlers read instead of guessing.', 'ai-rank-suite'),
		crawlers: __('Which AI crawlers may read the site, one by one.', 'ai-rank-suite'),
		packs: __('Preset schema, prompts and rules for your industry.', 'ai-rank-suite'),
		snippets: __('Your own PHP, CSS and JavaScript, without a child theme.', 'ai-rank-suite'),
		advanced: __('robots.txt, .htaccess, breadcrumbs, image SEO.', 'ai-rank-suite'),
		developer: __('System status, integrations, hooks and the REST API.', 'ai-rank-suite'),
		settings: __('Modules, import and backup, compatibility, environment, data and your licence.', 'ai-rank-suite'),
	};

	/*
	 * A status per area, read from facts the plugin already has rather than
	 * invented: the setup endpoint says which connections exist, and the site
	 * checks say which configured surface is failing. An area with no signal
	 * either way says nothing — a green tick that means "we did not look" is
	 * a lie, and this screen is where people decide what to look at next.
	 */
	const [setup, setSetup] = useState(null);
	const [checks, setChecks] = useState(null);
	useEffect(() => {
		api.get('/setup/status').then(setSetup).catch(() => setSetup({}));
		api.get('/score').then((d) => setChecks(d.checks || [])).catch(() => setChecks([]));
	}, []);

	const failing = (prefixes) =>
		(checks || []).filter((c) => c.status === 'fail' && prefixes.some((p) => c.id.startsWith(p))).length;

	const statusOf = (id) => {
		if (!setup || !checks) return null;
		switch (id) {
			case 'llms-txt': {
				const on = checks.some((c) => c.id === 'llms_txt_enabled' && c.status === 'pass');
				return on ? { tone: 'good', label: __('Live', 'ai-rank-suite') } : { tone: 'warning', label: __('Off', 'ai-rank-suite') };
			}
			case 'sitemap': {
				const on = checks.some((c) => c.id === 'sitemap_present' && c.status === 'pass');
				return on ? { tone: 'good', label: __('Live', 'ai-rank-suite') } : { tone: 'warning', label: __('Missing', 'ai-rank-suite') };
			}
			case 'seo': {
				const n = failing(['site_indexable', 'pretty_permalinks', 'tagline_set']);
				return n ? { tone: 'serious', label: `${n} ${__('to fix', 'ai-rank-suite')}` } : { tone: 'good', label: __('OK', 'ai-rank-suite') };
			}
			case 'schema': {
				const n = failing(['schema_']);
				return n ? { tone: 'serious', label: `${n} ${__('to fix', 'ai-rank-suite')}` } : { tone: 'good', label: __('OK', 'ai-rank-suite') };
			}
			case 'developer':
			case 'settings':
				return setup.other_seo_plugin ? { tone: 'warning', label: __('Conflict', 'ai-rank-suite') } : null;
			default:
				return null;
		}
	};

	const items = SETUP();
	const withStatus = items.map((i) => ({ ...i, status: statusOf(i.id) }));
	const known = withStatus.filter((i) => i.status);
	const good = known.filter((i) => i.status.tone === 'good').length;
	const pct = known.length ? Math.round((good / known.length) * 100) : 0;
	const attention = known.filter((i) => i.status.tone !== 'good');

	// Grouped the way the sidebar groups them, so the two never disagree.
	const GROUPS = [
		{ id: 'AI search', label: __('AI search', 'ai-rank-suite'), lede: __('What assistants and AI crawlers are allowed to read, and how the site describes itself to them.', 'ai-rank-suite') },
		{ id: 'Search', label: __('Search', 'ai-rank-suite'), lede: __('What Google sees: titles, structured data, sitemap, local presence.', 'ai-rank-suite') },
		{ id: 'Overview', label: __('Content', 'ai-rank-suite'), lede: __('Presets that shape how content is optimised.', 'ai-rank-suite') },
		{ id: 'Store', label: __('Store', 'ai-rank-suite'), lede: __('Product feeds and store checks.', 'ai-rank-suite') },
		{ id: 'System', label: __('System', 'ai-rank-suite'), lede: __('The plumbing: code, imports, environment, licence.', 'ai-rank-suite') },
	];

	return (
		<div className="grid gap-5">
			{/* The reading first: how much of the setup is in a good state, and
			    the two doors people actually want — the guided wizard and the
			    AI that configures from a description of the business. */}
			<div className="ars-setup-hero">
				<div className="ars-setup-hero-ring">
					<ScoreRing value={known.length ? pct : null} size={112} label={__('Configured', 'ai-rank-suite')} onLight />
				</div>
				<div className="grid gap-2 min-w-0">
					<div className="t-overline">{__('Setup', 'ai-rank-suite')}</div>
					<h2 className="m-0 text-[20px] font-semibold tracking-tight">
						{!checks || !setup
							? __('Reading the site…', 'ai-rank-suite')
							: attention.length === 0
								? __('Every area we can check is in a good state.', 'ai-rank-suite')
								: `${attention.length} ${attention.length === 1 ? __('area needs attention', 'ai-rank-suite') : __('areas need attention', 'ai-rank-suite')}: ${attention.map((a) => a.label).join(', ')}.`}
					</h2>
					<p className="m-0 text-[13px]" style={{ color: 'var(--ars-ink-2)' }}>
						{__('These are configured once and then left alone. The wizard walks the essentials in a few minutes; the AI setup reads a description of your business and proposes the rest.', 'ai-rank-suite')}
					</p>
					<div className="flex items-center gap-2 flex-wrap mt-1">
						<button type="button" className="ars-btn ars-btn-primary" onClick={() => { window.location.hash = '/wizard'; }}>
							{__('Run the setup wizard', 'ai-rank-suite')}
						</button>
						<button type="button" className="ars-btn ars-btn-ghost" onClick={() => { window.location.hash = '/ai-wizard'; }}>
							{__('AI setup from a description', 'ai-rank-suite')}
						</button>
						<button type="button" className="ars-linkbtn" onClick={() => onOpen('dashboard')}>
							{__('Back to reports', 'ai-rank-suite')} →
						</button>
					</div>
				</div>
			</div>

			{/* The ordered checklist. Twelve screens each saying "not connected"
			    is twelve failures; this is one sequence with three required
			    steps, and it marks what is genuinely optional as optional. */}
			{setup?.checklist && (
				<section className="ars-card">
					<div className="ars-card-head">
						<div className="grid gap-1 min-w-0">
							<h3 className="ars-card-title">{__('Setup checklist', 'ai-rank-suite')}</h3>
							<p className="ars-card-sub">
								{setup.checklist.required_done >= setup.checklist.required_total
									? __('The essentials are done. Everything below them is worth having, not required.', 'ai-rank-suite')
									: `${setup.checklist.required_done} / ${setup.checklist.required_total} ${__('essentials done — do these in order first.', 'ai-rank-suite')}`}
							</p>
						</div>
						<span className="ars-num" style={{ fontSize: 22 }}>
							{setup.checklist.done}/{setup.checklist.total}
						</span>
					</div>
					{/*
					  * The open steps are numbered 1, 2, 3 — not by their
					  * position in the whole list. A done step shows a tick,
					  * so numbering by index left the reader with 2, 6, 7, 8,
					  * 9 and no 1, 3, 4 or 5 anywhere: it looks like items are
					  * missing rather than finished. The count is "what is
					  * left, in this order".
					  */}
					<ol className="ars-checklist">
						{(() => {
							let open = 0;
							return setup.checklist.steps.map((step) => {
								if (!step.done) open += 1;
								const n = open;
								return (
							<li key={step.id} className={`ars-checklist-item${step.done ? ' is-done' : ''}`}>
								<span className={`ars-checklist-mark${step.done ? ' is-done' : ''}`} aria-hidden>
									{step.done ? '✓' : n}
								</span>
								<span className="min-w-0 grid gap-0.5">
									<span className="flex items-center gap-2 flex-wrap">
										<a href={step.route} className="font-medium text-[13px]">
											{step.label}
										</a>
										<span className={`ars-tier is-${step.tier}`}>
											{step.tier === 'required'
												? __('required', 'ai-rank-suite')
												: step.tier === 'recommended'
													? __('recommended', 'ai-rank-suite')
													: __('optional', 'ai-rank-suite')}
										</span>
										{step.note && <span className="t-caption">{step.note}</span>}
									</span>
									<span className="t-caption">{step.why}</span>
								</span>
							</li>
								);
							});
						})()}
					</ol>
				</section>
			)}

			{GROUPS.map((g) => {
				const inGroup = withStatus.filter((i) => i.group === g.id);
				if (inGroup.length === 0) return null;
				return (
					<section key={g.id} className="grid gap-3">
						<div className="ars-setup-group">
							<h3 className="m-0 text-[14px] font-semibold">{g.label}</h3>
							<p className="m-0 t-caption">{g.lede}</p>
						</div>
						<div className="ars-setup-grid">
							{inGroup.map((item) => {
								const Icon = item.icon;
								return (
									<button key={item.id} type="button" className={`ars-setup-card${item.status ? ' is-' + item.status.tone : ''}`} onClick={() => onOpen(item.id)}>
										<span className="ars-setup-icon">
											<Icon size={17} />
										</span>
										<span className="ars-setup-text">
											<span className="ars-setup-name">
												{item.label}
												{item.status && <span className={`ars-setup-status is-${item.status.tone}`}>{item.status.label}</span>}
											</span>
											<span className="ars-setup-what">{WHAT[item.id] || ''}</span>
										</span>
										<span className="ars-setup-go" aria-hidden>›</span>
									</button>
								);
							})}
						</div>
					</section>
				);
			})}
		</div>
	);
}

/**
 * Where a menu page starts when the URL names no route.
 *
 * Each WordPress submenu is its own admin page and prints the route it stands
 * for on the mount node. Without this every submenu would open the dashboard
 * and the menu would be decoration: six entries, one screen.
 */
const defaultRoute = () => {
	const root = document.getElementById('airanksuite-root');
	return (root && root.dataset && root.dataset.route) || 'dashboard';
};

/**
 * The screen a menu page actually opens on.
 *
 * `data-route` names the group's lead screen, which in the free edition can
 * be one whose module was never installed: click Measure and the first thing
 * the plugin says is "buy Pro". A locked screen is worth showing when someone
 * asks for it — it is a poor greeting. So the page opens on the first screen
 * of its group that can run, and the locked ones keep their place in the tab
 * row with the mark that says why.
 */
const startScreen = () => {
	const wanted = VISIBLE().find((n) => n.id === defaultRoute());
	if (wanted && (!wanted.needs || moduleRunning(wanted.needs))) return wanted;
	const group = wanted ? wanted.group : null;
	const open = group
		? VISIBLE().find((n) => n.group === group && (!n.needs || moduleRunning(n.needs)))
		: null;
	return open || wanted || VISIBLE().find((n) => n.id === 'dashboard') || null;
};

const fromHash = () => {
	const m = window.location.hash.match(/^#\/([a-z-]+)(?:\/([a-z-]+))?/);
	if (!m) {
		const start = startScreen();
		return start ? { page: start.id, tab: start.tabs ? start.tabs[0] : null } : { page: 'dashboard', tab: null };
	}
	if (m[1] === 'setup') return { page: 'setup', tab: null };
	const [id, wanted] = ALIASES[m[1]] ? ALIASES[m[1]] : [m[1], m[2]];
	const entry = VISIBLE().find((n) => n.id === id);
	if (!entry) {
		const start = startScreen();
		return start ? { page: start.id, tab: start.tabs ? start.tabs[0] : null } : { page: 'dashboard', tab: null };
	}
	// An alias that names no tab must still land on the screen's first tab, not
	// on `null` — a screen opened with no tab selected renders its frame and
	// nothing inside it.
	const tab = entry.tabs && entry.tabs.includes(wanted) ? wanted : entry.tabs ? entry.tabs[0] : null;
	return { page: entry.id, tab };
};

export default function App() {
	const [route, setRoute] = useState(fromHash);

	/*
	 * A save that fails has to say so, wherever it happened.
	 *
	 * Several screens call the API and do not render the rejection, so a refused
	 * write used to die as an unhandled promise rejection in the console: the
	 * form looked like it worked, the value came back empty on the next load,
	 * and there was nothing anywhere to explain why. That is the single worst
	 * failure mode a settings screen has, because it is indistinguishable from a
	 * plugin that does not work at all.
	 *
	 * This is the backstop. It sits above every screen, it names the status
	 * code, and for the statuses that mean "your host refused this" it says so
	 * in words the customer can forward to their host.
	 */
	const [writeError, setWriteError] = useState(null);
	useEffect(() => {
		const onFail = (e) => setWriteError(e.detail || null);
		window.addEventListener('airanksuite:request-failed', onFail);
		return () => window.removeEventListener('airanksuite:request-failed', onFail);
	}, []);
	// The wizard is not a one-time gate. Somebody who skipped it, changed
	// industry, moved the business or simply wants to see what it asks must be
	// able to open it again — #/wizard does that from anywhere, and Settings
	// has a button for people who do not type URLs.
	const [wizard, setWizard] = useState(
		() => window.location.hash.replace(/^#\/?/, '') === 'wizard' || (env.setupDone === false && !window.location.hash)
	);

	const [aiWizard, setAiWizard] = useState(() => window.location.hash.replace(/^#\/?/, '') === 'ai-wizard');

	useEffect(() => {
		// Both directions. These two flags sit in front of an early return that
		// replaces the whole application, so a listener that can only turn them
		// on makes every link *out* of a wizard do nothing at all: the hash
		// changes, the router behind moves, and the overlay stays on top of it.
		// That is what "Open AI Engine" did — and "Open Autopilot", and the
		// Search Console fallback in the setup wizard.
		const onHash = () => {
			const h = window.location.hash.replace(/^#\/?/, '');
			setWizard(h === 'wizard');
			setAiWizard(h === 'ai-wizard');
		};
		window.addEventListener('hashchange', onHash);
		return () => window.removeEventListener('hashchange', onHash);
	}, []);
	const [query, setQuery] = useState('');
	// 250px of permanent navigation is a lot on a 1280px laptop. The choice is
	// remembered, because a preference the user has to re-make every visit is
	// not a preference.
	const [rail, setRail] = useState(() => {
		try {
			const saved = window.localStorage.getItem('airanksuite-rail');
			if (saved === 'mini') return false;
			if (saved === 'wide') return true;
		} catch {
			/* no storage: fall through to the width rule */
		}
		// No explicit choice yet: WordPress' own sidebar already takes 160px,
		// so under ~1280px the full rail leaves too little for the tables.
		// Start collapsed there; the toggle still overrides and is remembered.
		return window.innerWidth >= 1280;
	});

	// On a phone the rail is a sheet, not a column: thirty destinations
	// stacked above the content is a scroll, not navigation. Closed by default
	// and closed again the moment a destination is chosen.
	const [navOpen, setNavOpen] = useState(false);

	const toggleRail = () => {
		setRail((open) => {
			try {
				window.localStorage.setItem('airanksuite-rail', open ? 'mini' : 'wide');
			} catch {
				/* Private browsing: the toggle still works, it just forgets. */
			}
			return !open;
		});
	};

	// Theme is opt-in and remembered: a light wp-admin is never flipped to dark
	// without asking, and a choice made once survives every visit. The dark
	// palette lives entirely in styles.css; here we only flip the attribute the
	// tokens hang on.
	const [theme, setTheme] = useState(readTheme);

	// The attribute the dark tokens hang on. The settings card writes through
	// the same module, so a change made there arrives here without a reload.
	useEffect(() => {
		writeTheme(theme);
		// And then take the accent from the admin colour scheme the person
		// actually chose, measured off a real core button. Light only: the
		// dark palette is lifted for a dark ground and a scheme colour picked
		// for a white page is not that colour.
		if (theme === 'dark') {
			clearAdminColour();
		} else {
			syncAdminColour();
		}
	}, [theme]);
	useEffect(() => onThemeChange((t) => setTheme(t)), []);

	const toggleTheme = () => setTheme((t) => (t === 'dark' ? 'light' : 'dark'));

	useEffect(() => {
		const onHash = () => setRoute(fromHash());
		window.addEventListener('hashchange', onHash);
		return () => window.removeEventListener('hashchange', onHash);
	}, []);

	const go = (id, tab) => {
		const hash = tab ? `/${id}/${tab}` : `/${id}`;
		// Leaving the group means leaving the admin page that owns it. Any
		// other behaviour lights the wrong entry in the WordPress menu, and a
		// menu that says you are somewhere you are not is worse than no menu.
		const target = VISIBLE().find((n) => n.id === id);
		const page = target && target.group !== currentGroup(route.page) ? pageForGroup(target.group) : null;
		if (page && page.url) {
			window.location.assign(`${page.url}#${hash}`);
			return;
		}
		window.location.hash = hash;
		setRoute({ page: id, tab: tab || (nav().find((n) => n.id === id)?.tabs?.[0] ?? null) });
		setQuery('');
	};

	// Every screen this admin page owns, in the order the screen table lists
	// them. `when` still applies: a WooCommerce-only screen stays out of the
	// row on a site without WooCommerce.
	const groupScreens = useMemo(() => {
		const group = currentGroup(route.page);
		const items = VISIBLE().filter(
			(n) => n.group === group && (!n.hidden || !n.hidden() || n.id === route.page)
		);
		// The screen the submenu opens leads its own row. Anything else and the
		// first tab is not the page you just clicked, which reads as a jump.
		const first = pageForGroup(group);
		if (first) {
			const i = items.findIndex((n) => n.id === first.route);
			if (i > 0) items.unshift(items.splice(i, 1)[0]);
		}
		// The index of every configuration screen used to hang off the sidebar,
		// which the WordPress menu has replaced. It is still the fastest way to
		// find a setting whose group you cannot guess, so it keeps a door — at
		// the end of the group that owns configuration.
		if (group === 'System') items.push({ id: 'setup', label: __('All settings', 'ai-rank-suite'), setup: true, group: 'System' });
		return items;
	}, [route.page]);

	const filtered = useMemo(() => {
		const q = query.trim().toLowerCase();
		if (!q) return null;
		return VISIBLE().filter(
			(n) => n.label.toLowerCase().includes(q) || groupLabel(n.group).toLowerCase().includes(q)
		);
	}, [query]);

	if (aiWizard) {
		return (
			<Suspense fallback={<PageLoading />}>
			<AiWizard
				onDone={() => {
					setAiWizard(false);
					if (window.location.hash.replace(/^#\/?/, '') === 'ai-wizard') {
						window.location.hash = '/dashboard';
					}
				}}
			/>
			</Suspense>
		);
	}

	if (wizard) {
		return (
			<Suspense fallback={<PageLoading />}>
			<Wizard
				onDone={() => {
					setWizard(false);
					// Leaving the wizard on #/wizard would reopen it on the next
					// hashchange, and the back button would trap the user in it.
					if (window.location.hash.replace(/^#\/?/, '') === 'wizard') {
						window.location.hash = '/dashboard';
					}
				}}
			/>
			</Suspense>
		);
	}

	const current = VISIBLE().find((n) => n.id === route.page) || nav()[0];
	// The sidebar shows the setup list while the customer is inside setup, and
	// the work list otherwise. `route.page` rather than `current`, because the
	// setup index is not itself a nav entry.
	const inSetup = route.page === 'setup' || !!current.setup;
	const View = current.view;

	return (
		<div className="pr-4 py-4">
			<div
				className={`ars-app grid items-stretch${rail ? ' ars-app-wide' : ' ars-app-mini'}${
					navOpen ? ' is-nav-open' : ''
				}`}
			>
				{/* The scrim only exists on small screens; on a desktop the rail
				    is a column and there is nothing to dismiss. */}
				<button
					type="button"
					className="ars-nav-scrim"
					aria-label={__('Close menu', 'ai-rank-suite')}
					onClick={() => setNavOpen(false)}
				/>
				<nav
					className={`ars-rail flex flex-col py-3 px-2${rail ? '' : ' is-mini'}`}
					aria-label={__('AI Rank Suite', 'ai-rank-suite')}
					onClick={(e) => {
						// Any navigation choice closes the sheet. Delegated
						// rather than wired into every item, because the rail
						// renders its links in four different places.
						if (e.target.closest('.ars-nav-item')) setNavOpen(false);
					}}
				>
					<button
						type="button"
						onClick={toggleRail}
						className="ars-brand"
						title={rail ? __('Collapse navigation', 'ai-rank-suite') : __('Expand navigation', 'ai-rank-suite')}
						aria-expanded={rail}
					>
						<span className="ars-brand-mark ars-brand-mono" aria-hidden>
							{String(env.brand || 'AI Rank Suite').replace(/[^A-Za-z0-9]/g, '').slice(0, 1).toUpperCase() || 'A'}
						</span>
						{rail && (
							<span className="leading-tight text-left min-w-0">
								<span className="block font-bold text-[15px] truncate ars-brand-name">
									{env.brand && env.brand !== 'AI Rank Suite' ? (
										<span className="ars-grad-text">{env.brand}</span>
									) : (
										<>
											AI Rank <span className="ars-grad-text">{__('Suite', 'ai-rank-suite')}</span>
										</>
									)}
								</span>
								<span className="block text-[10.5px]" style={{ color: 'var(--ars-ink-3)' }}>
									v{env.version}
								</span>
							</span>
						)}
					</button>

					{rail && (
						<div className="px-1 pb-1.5">
							{/*
							 * Search, not a form field.
							 *
							 * As a bare text input this was a magnet for browser
							 * autofill and password managers: Chrome filled it with
							 * the account e-mail on load, the filter matched nothing,
							 * and the entire navigation vanished behind "Nothing
							 * matches" — with no obvious way back. type="search"
							 * plus the autofill opt-outs and a name nothing recognises
							 * keeps every filler away from it, and the clear button
							 * gives a way out even if something still writes here.
							 */}
							<div className="ars-jump-wrap">
								<input
									type="search"
									name="airanksuite-nav-filter"
									id="airanksuite-nav-filter"
									autoComplete="off"
									autoCorrect="off"
									autoCapitalize="off"
									spellCheck="false"
									data-form-type="other"
									data-lpignore="true"
									data-1p-ignore="true"
									placeholder={__('Jump to…', 'ai-rank-suite')}
									value={query}
									onChange={(e) => setQuery(e.target.value)}
									onKeyDown={(e) => {
										if (e.key === 'Enter' && filtered && filtered[0]) go(filtered[0].id);
										if (e.key === 'Escape') setQuery('');
									}}
									className="ars-jump"
									aria-label={__('Jump to a page', 'ai-rank-suite')}
								/>
								{query && (
									<button
										type="button"
										className="ars-jump-clear"
										onClick={() => setQuery('')}
										aria-label={__('Clear the filter', 'ai-rank-suite')}
										title={__('Clear the filter', 'ai-rank-suite')}
									>
										✕
									</button>
								)}
							</div>
						</div>
					)}

					<div className="grid gap-0.5 overflow-y-auto" style={{ scrollbarWidth: 'thin' }}>
						{filtered ? (
							<div className="grid gap-0.5">
								{filtered.length === 0 && (
									<div className="px-4 py-2 text-[12px] grid gap-1" style={{ color: 'var(--ars-ink-3)' }}>
										<span>{sprintf(__('No screen matches “%s”.', 'ai-rank-suite'), query)}</span>
										<button type="button" className="ars-linkbtn" onClick={() => setQuery('')}>{__('Show all screens', 'ai-rank-suite')}</button>
									</div>
								)}
								{filtered.map((item) => {
									const Icon = item.icon;
									return (
										<button key={item.id} type="button" onClick={() => go(item.id)} className="ars-nav-item">
											<Icon size={16} />
											{item.label}
											<span className="ml-auto text-[10px]" style={{ color: 'var(--ars-ink-3)' }}>
												{groupLabel(item.group)}
											</span>
										</button>
									);
								})}
							</div>
						) : inSetup ? (
							/* Inside setup, the sidebar becomes the setup list. Two
							   sidebars would be worse than one that changes: the
							   customer is doing one job at a time and the other
							   half is noise while they do it. */
							<div className="grid gap-0.5">
								<button
									type="button"
									onClick={() => go('dashboard')}
									className="ars-nav-item is-back"
									title={__('Back to the reports', 'ai-rank-suite')}
								>
									<IconGauge size={16} />
									{rail && __('Back to reports', 'ai-rank-suite')}
								</button>
								{rail && <div className="ars-nav-group">{__('Setup', 'ai-rank-suite')}</div>}
								{SETUP().map((item) => {
									const Icon = item.icon;
									return (
										<button
											key={item.id}
											type="button"
											onClick={() => go(item.id)}
											title={rail ? undefined : item.label}
											aria-label={item.label}
											className={`ars-nav-item${route.page === item.id ? ' is-active' : ''}`}
										>
											<Icon size={16} />
											{rail && item.label}
										</button>
									);
								})}
							</div>
						) : (
							<>
								{WORK_GROUPS.map((group) => {
									const items = WORK().filter((n) => n.group === group);
									if (!items.length) return null;
									return (
										<div key={group} className="grid gap-0.5">
											{rail ? (
												<div className="ars-nav-group">{groupLabel(group)}</div>
											) : (
												<div className="ars-nav-rule" aria-hidden />
											)}
											{items.map((item) => {
												const Icon = item.icon;
												return (
													<button
														key={item.id}
														type="button"
														onClick={() => go(item.id)}
														title={rail ? undefined : `${item.label} — ${group}`}
														aria-label={item.label}
														className={`ars-nav-item${route.page === item.id ? ' is-active' : ''}`}
													>
														<Icon size={16} />
														{rail && item.label}
													</button>
												);
											})}
										</div>
									);
								})}

								{/* One entry for everything that is configuration. It
								    used to be thirteen of the twenty-five rows above. */}
								<div className="grid gap-0.5">
									<div className="ars-nav-rule" aria-hidden />
									<button
										type="button"
										onClick={() => go('setup')}
										title={rail ? undefined : __('Setup', 'ai-rank-suite')}
										className="ars-nav-item is-setup"
									>
										<IconSliders size={16} />
										{rail && __('Setup', 'ai-rank-suite')}
									</button>
								</div>
							</>
						)}
					</div>

					{rail && !env.whiteLabel && (
						<div className="mt-auto px-3 pt-3 text-[10.5px]" style={{ color: 'var(--ars-ink-3)' }}>{__('Built for AI answers and Google search', 'ai-rank-suite')}</div>
					)}
				</nav>

				<div className="ars-main min-w-0 flex flex-col">
					<header className="ars-header flex items-center justify-between gap-3 px-6">
						<button
							type="button"
							className="ars-nav-toggle"
							aria-label={__('Menu', 'ai-rank-suite')}
							aria-expanded={navOpen}
							onClick={() => setNavOpen((v) => !v)}
						>
							<span aria-hidden />
						</button>
						{/* Group over title, not site name beside it. "Search ›
						    Local SEO" tells you where you are in the product;
						    "Local SEO — Nami Test Site" tells you the site name,
						    which the WordPress admin bar has already said. */}
						<div className="min-w-0">
							<div className="t-overline" style={{ fontSize: 10, marginBottom: 1 }}>
								{route.page === 'setup'
									? __('Setup', 'ai-rank-suite')
									: current.setup
										? __('Setup', 'ai-rank-suite')
										: groupLabel(current.group)}
							</div>
							<h1 className="ars-page-title truncate">
								{route.page === 'setup' ? __('Setup', 'ai-rank-suite') : current.label}
							</h1>
						</div>
						<div className="flex items-center gap-3 shrink-0">
							<button
								type="button"
								className="ars-theme-toggle"
								onClick={toggleTheme}
								aria-label={theme === 'dark' ? __('Switch to light theme', 'ai-rank-suite') : __('Switch to dark theme', 'ai-rank-suite')}
								title={theme === 'dark' ? __('Light', 'ai-rank-suite') : __('Dark', 'ai-rank-suite')}
							>
								{theme === 'dark' ? (
									<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden>
										<circle cx="12" cy="12" r="4" />
										<path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4" />
									</svg>
								) : (
									<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden>
										<path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z" />
									</svg>
								)}
							</button>
							<a
								href={`${env.homeUrl.replace(/\/$/, '')}/llms.txt`}
								target="_blank"
								rel="noreferrer"
								className="ars-theme-toggle ars-header-chip"
								title={__('View your live llms.txt', 'ai-rank-suite')}
							>
								/llms.txt ↗
							</a>
						</div>
					</header>
					{writeError && (
						<div className="px-6 pt-4">
							<Notice tone="serious">
								<div className="flex items-start justify-between gap-4">
									<div>
										<strong>{__('That change was not saved.', 'ai-rank-suite')}</strong>{' '}
										{writeError.message ||
											(writeError.status === 403 || writeError.status === 405
												? sprintf(__('The server refused a %1$s request (%2$s). Some hosts and firewalls block it — send them this message and ask them to allow it for /wp-json/.', 'ai-rank-suite'), writeError.method, writeError.status)
												: sprintf(__('The server answered %s.', 'ai-rank-suite'), writeError.status))}
										<div className="t-caption mt-1">
											{writeError.method} {writeError.path}
										</div>
									</div>
									<button type="button" className="ars-chip shrink-0" onClick={() => setWriteError(null)}>{__('Dismiss', 'ai-rank-suite')}</button>
								</div>
							</Notice>
						</div>
					)}
					{/* The screens this admin page owns, as core's nav-tab row.
					    Six submenus reach twenty-six screens this way, which is
					    how every SEO plugin a WordPress user already has is
					    built — the menu names the area, the tabs name the
					    screen. Screens whose module is not installed stay in
					    the row and open the Pro card: a feature you cannot see
					    is a feature you will not buy. */}
					{groupScreens.length > 1 && (
						<nav className="ars-grouptabs" aria-label={groupLabel(currentGroup(route.page))}>
							{groupScreens.map((item) => (
								<button
									key={item.id}
									type="button"
									onClick={() => go(item.id)}
									aria-current={route.page === item.id ? 'page' : undefined}
									className={`ars-grouptab${route.page === item.id ? ' is-active' : ''}`}
								>
									{item.label}
									{item.needs && !moduleRunning(item.needs) && <span className="ars-grouptab-pro">{__('Pro', 'ai-rank-suite')}</span>}
								</button>
							))}
						</nav>
					)}
					<main className="min-w-0 px-6 py-5 ars-view" key={route.page + (route.tab || '')}>
						{route.page === 'setup' ? (
							<Setup onOpen={(id) => go(id)} />
						) : current.needs && !moduleRunning(current.needs) ? (
							// The module behind this screen is not installed —
							// the free edition ships without the Pro folders.
							// Mounting the page anyway would fire requests at
							// routes that do not exist and fill the screen with
							// "no route was found", which reads as a broken
							// plugin rather than a paid feature.
							<ProScreen title={current.label} />
						) : current.tabs ? (
							<Suspense fallback={<PageLoading />}>
								<View tab={route.tab || current.tabs[0]} onTab={(t) => go(current.id, t)} />
							</Suspense>
						) : (
							<Suspense fallback={<PageLoading />}>
								<View />
							</Suspense>
						)}
					</main>
				</div>
			</div>
		</div>
	);
}
