import { __, _n, sprintf } from '@wordpress/i18n';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { api, env } from '../api';
import { Card, Button, Badge, Notice, Switch, ChipBar, Panel, Empty, ScoreRing, LineChart, Delta, Composition, PageSkeleton, Progress } from '../components/ui';
import { IconExternal, IconRefresh } from '../components/icons';
import Explain from '../components/Explain';
import AuditCopilot from '../components/AuditCopilot';

const SEV_TONE = { critical: 'red', warning: 'yellow', notice: 'blue' };
const SEV_LABEL = { critical: 'Critical', warning: 'Warning', notice: 'Notice' };


/**
 * Alerts on new findings.
 *
 * Deliberately scoped to what is *new*: an alert that repeats itself is an
 * alert people write a mail rule for, and the rule outlives the problem.
 */
function Alerts() {
	const [data, setData] = useState(null);
	const [busy, setBusy] = useState(false);
	const [note, setNote] = useState(null);
	const [error, setError] = useState(null);

	useEffect(() => {
		api.get('/audit/alerts').then(setData).catch((e) => setError(e.message));
	}, []);

	if (!data) return <PageSkeleton blocks={3} label={__('Loading the audit', 'ai-rank-suite')} />;
	const s = data.settings;

	const put = async (patch) => {
		setBusy(true);
		setError(null);
		try {
			setData(await api.put('/audit/alerts', { ...s, ...patch }));
		} catch (e) {
			setError(e.message);
		} finally {
			setBusy(false);
		}
	};

	const test = async () => {
		setBusy(true);
		setNote(null);
		try {
			const r = await api.post('/audit/alerts/test', {});
			setNote(
				r.new === 0
					? __('Nothing new since the last check — no mail sent, which is the point.', 'ai-rank-suite')
					: sprintf(_n('%d new issue found and mailed.', '%d new issues found and mailed.', r.new, 'ai-rank-suite'), r.new)
			);
		} catch (e) {
			setError(e.message);
		} finally {
			setBusy(false);
		}
	};

	return (
		<Card
			title={__('Alerts', 'ai-rank-suite')}
			subtitle={__('An email the day something breaks, not at the end of the month', 'ai-rank-suite')}
			action={
				s.enabled && data.next ? (
					<Badge tone="good">Next check {new Date(data.next * 1000).toLocaleString()}</Badge>
				) : (
					<Badge tone="neutral">{__('Off', 'ai-rank-suite')}</Badge>
				)
			}
		>
			<div className="grid gap-3">
				<Switch
					checked={!!s.enabled}
					onChange={(v) => put({ enabled: v })}
					label={__('Audit the site on a schedule and email new issues', 'ai-rank-suite')}
				/>

				{s.enabled && (
					<>
						<div className="grid gap-3 sm:grid-cols-3">
							<div>
								<label className="block font-medium mb-1">{__('How often', 'ai-rank-suite')}</label>
								<select value={s.frequency} onChange={(e) => put({ frequency: e.target.value })}>
									<option value="daily">{__('Daily', 'ai-rank-suite')}</option>
									<option value="weekly">{__('Weekly', 'ai-rank-suite')}</option>
								</select>
							</div>
							<div>
								<label className="block font-medium mb-1">{__('Alert on', 'ai-rank-suite')}</label>
								<select value={s.severity} onChange={(e) => put({ severity: e.target.value })}>
									<option value="critical">{__('Critical only', 'ai-rank-suite')}</option>
									<option value="warning">{__('Critical and warnings', 'ai-rank-suite')}</option>
								</select>
							</div>
							<div>
								<label className="block font-medium mb-1">{__('Send to', 'ai-rank-suite')}</label>
								<input
									type="text"
									placeholder={__('Defaults to the admin email', 'ai-rank-suite')}
									defaultValue={s.recipients}
									onBlur={(e) => put({ recipients: e.target.value })}
									/>
							</div>
						</div>
						<div className="flex items-center gap-2 flex-wrap">
							<Button variant="ghost" busy={busy} onClick={test}>{__('Check now', 'ai-rank-suite')}</Button>
							{note && <span className="text-[12.5px]" style={{ color: 'var(--ars-ink-2)' }}>{note}</span>}
						</div>
					</>
				)}

				<Explain title={__('What counts as an alert', 'ai-rank-suite')}>
					<p>
						{__('Only findings that were not present at the previous check. A weekly summary of everything already known gets read once and filtered forever; the failures that cost money are the sudden ones — a deploy carrying “discourage search engines”, a theme update dropping the canonical, a plugin noindexing a category.', 'ai-rank-suite')}
					</p>
					<p>{__('A problem you fixed that comes back alerts again, because that is genuinely news rather than a repeat.', 'ai-rank-suite')}</p>
				</Explain>

				{error && <Notice tone="serious">{error}</Notice>}
			</div>
		</Card>
	);
}

export default function SiteAudit() {
	const [status, setStatus] = useState(null);
	const [report, setReport] = useState(null);
	const [exporting, setExporting] = useState(false);
	const [trend, setTrend] = useState(null);
	const [group, setGroup] = useState('');

	// The server hands over rows, the browser writes the file. Quotes are
	// doubled here; cells that could be read as a formula were already
	// prefixed server side.
	const exportCsv = useCallback(async () => {
		setExporting(true);
		try {
			const res = await api.get('/audit/export');
			const rows = Array.isArray(res?.rows) ? res.rows : [];
			const csv = '\uFEFF' + rows
				.map((row) => row.map((cell) => `"${String(cell ?? '').replace(/"/g, '""')}"`).join(','))
				.join('\r\n');
			const url = URL.createObjectURL(new Blob([csv], { type: 'text/csv;charset=utf-8' }));
			const link = document.createElement('a');
			link.href = url;
			link.download = res?.filename || 'site-audit.csv';
			document.body.appendChild(link);
			link.click();
			link.remove();
			URL.revokeObjectURL(url);
		} finally {
			setExporting(false);
		}
	}, []);
	const [lang, setLang] = useState('');
	const [severity, setSeverity] = useState('');
	const [ptype, setPtype] = useState('');
	const [types, setTypes] = useState([]);
	const [page, setPage] = useState(1);
	const [busy, setBusy] = useState(false);
	const [error, setError] = useState(null);
	const running = useRef(false);
	const findingsRef = useRef(null);
	const i18n = env.i18n || { multilingual: false, perPost: false, languages: [] };

	// Clicking a weak area or a top issue filters the Findings list below — but
	// that list is under two cards, so the filter looked like it did nothing.
	// Bring it into view so the click has a visible destination.
	const revealFindings = () => {
		requestAnimationFrame(() => {
			findingsRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' });
		});
	};

	const loadReport = useCallback(
		(g = group, s = severity, p = page, l = lang, t = ptype) =>
			api
				.get(`/audit/report?group=${g}&severity=${s}&page=${p}&per_page=50&lang=${encodeURIComponent(l)}&type=${encodeURIComponent(t)}`)
				.then(setReport)
				.catch((e) => setError(e.message)),
		[group, severity, page, lang, ptype]
	);

	useEffect(() => {
		api.get('/audit/status').then(setStatus).catch((e) => setError(e.message));
		api.get('/audit/trend?limit=20').then((r) => setTrend(r.runs || [])).catch(() => setTrend([]));
		loadReport();
		// eslint-disable-next-line react-hooks/exhaustive-deps
	}, []);

	useEffect(() => {
		loadReport(group, severity, page, lang, ptype);
		// eslint-disable-next-line react-hooks/exhaustive-deps
	}, [group, severity, page, lang, ptype]);

	// Post types for the filter chips — the same eligible list every counter uses.
	useEffect(() => {
		api.get('/content/library').then((r) => setTypes((r.types || []).filter((t) => t.total > 0))).catch(() => {});
	}, []);

	// The run is driven from the browser one slice at a time: no cron, no
	// long-running request, and the customer can watch it move.
	const pump = useCallback(async () => {
		if (running.current) return;
		running.current = true;
		try {
			for (;;) {
				const next = await api.post('/audit/tick', {});
				setStatus(next);
				if (next.state !== 'running') break;
			}
			await loadReport();
		} catch (e) {
			setError(e.message);
		} finally {
			running.current = false;
			setBusy(false);
		}
	}, [loadReport]);

	/*
	 * A run left behind resumes when somebody opens the screen.
	 *
	 * The pump only ever started from the Start button, so a run whose tab was
	 * closed sat at the slice it was abandoned on and drew a progress bar that
	 * never moved again. On a live site that was 66 of 156 pages, three hours
	 * old, and a single tick took it to 88 — the work was fine, nothing was
	 * asking for it. Cron picks up an abandoned run too now; this is the
	 * faster of the two, because the customer is already looking at it.
	 */
	useEffect(() => {
		if ('running' === status?.state && ! running.current) {
			setBusy(true);
			pump();
		}
	}, [status?.state, pump]);

	const start = async () => {
		setBusy(true);
		setError(null);
		try {
			setStatus(await api.post('/audit/start', {}));
			pump();
		} catch (e) {
			setError(e.message);
			setBusy(false);
		}
	};

	const cancel = async () => {
		try {
			setStatus(await api.post('/audit/cancel', {}));
		} catch (e) {
			setError(e.message);
		}
	};

	const acceptRule = async (checkId, on = true) => {
		try {
			await api.post('/audit/ignore', { check_id: checkId, on });
			await loadReport();
		} catch (e) {
			setError(e.message);
		}
	};

	const ignore = async (fingerprint) => {
		try {
			await api.post('/audit/ignore', { fingerprint, on: true });
			await loadReport();
		} catch (e) {
			setError(e.message);
		}
	};

	const isRunning = status?.state === 'running';
	const winMax = Math.max(1, ...(report?.checks || []).slice(0, 8).map((c) => c.count || 0));
	const totals = report?.totals || { critical: 0, warning: 0, notice: 0 };
	// "Passing" is the rules that produced nothing at all. The breakdown needs
	// it: a meter made only of problems always looks like a site on fire,
	// whatever the score beside it says. Counting rules rather than pages keeps
	// the row comparable between a ten-page site and a ten-thousand-page one.
	// How many rules exist comes from the run, not from a number typed here:
	// `const RULES = 30` was right the day it was written and silently wrong
	// the first time a rule was added.
	const RULES = report?.rules_total || 30;
	const rulesFailing = new Set((report?.checks || []).map((c) => c.id)).size;
	const rulesCritical = new Set((report?.checks || []).filter((c) => c.severity === 'critical').map((c) => c.id)).size;
	const passing = Math.max(0, RULES - rulesFailing);
	// `report` is null until the first fetch lands; treat that as loading, not
	// as "never audited", or the summary branch dereferences null.
	const loaded = !!report;
	const never = loaded && !report.run_id;

	// One sentence for the whole run: what the score means and what to do
	// first. Every number in it appears again below, so it can be checked.
	const verdict = (() => {
		if (!loaded || never) return null;
		const c = totals.critical;
		const w = totals.warning;
		const n = totals.notice;
		const pages = report.checked || 0;
		if (c === 0 && w === 0 && n === 0) {
			return {
				tone: 'good',
				badge: __('All clear', 'ai-rank-suite'),
				text: sprintf(
					/* translators: 1: pages checked, 2: number of rules. */
					_n('The single page checked passed all %2$s rules.', 'Every one of %1$s pages passed all %2$s rules.', pages, 'ai-rank-suite'),
					pages.toLocaleString(),
					RULES.toLocaleString()
				),
				why: __('Nothing comparative is wrong: no duplicates, no orphans, no blocked pages. Run again after a publishing sprint.', 'ai-rank-suite'),
				action: null,
			};
		}
		if (c > 0) {
			return {
				tone: 'bad',
				badge: __('Needs attention', 'ai-rank-suite'),
				/*
				 * Rules, not findings.
				 *
				 * "2,531 critical problems are costing you search traffic
				 * today" was true and useless: the 2,531 were one rule firing
				 * on one page each, and the work is one fix, not 2,531. Naming
				 * the rule count first makes the job look like the size it is,
				 * and the finding count stays as the scale of the effect.
				 */
				text: sprintf(
					_n('%d critical rule is costing you search traffic today.', '%d critical rules are costing you search traffic today.', rulesCritical || 1, 'ai-rank-suite'),
					rulesCritical || 1
				),
				why: sprintf(
					/* translators: 1: critical findings, 2: pages, 3: warnings, 4: notices. */
					__('That is %1$s findings across %2$s pages, with %3$s warnings and %4$s notices behind them. Fix a rule once and every page it touches clears at the same time.', 'ai-rank-suite'),
					c.toLocaleString(),
					pages.toLocaleString(),
					w.toLocaleString(),
					n.toLocaleString()
				),
				action: __('Show the criticals', 'ai-rank-suite'),
				severity: 'critical',
			};
		}
		if (w > 0) {
			return {
				tone: 'warning',
				badge: __('Room to improve', 'ai-rank-suite'),
				text: sprintf(
					/* translators: 1: warnings, 2: pages. */
					__('Nothing is blocking, but %1$s warnings across %2$s pages hold the site back.', 'ai-rank-suite'),
					w.toLocaleString(),
					pages.toLocaleString()
				),
				why: __('Thin content, duplicate descriptions and unlinked pages are the usual weight here. Work them by rule, not by page — one fix clears hundreds.', 'ai-rank-suite'),
				action: __('Show the warnings', 'ai-rank-suite'),
				severity: 'warning',
			};
		}
		return {
			tone: 'good',
			badge: __('Healthy', 'ai-rank-suite'),
			text: sprintf(
				/* translators: 1: notices, 2: pages. */
				__('Only %1$s notices remain across %2$s pages.', 'ai-rank-suite'),
				n.toLocaleString(),
				pages.toLocaleString()
			),
			why: __('Nothing here costs traffic. Worth clearing on a quiet afternoon.', 'ai-rank-suite'),
			action: __('Show the notices', 'ai-rank-suite'),
			severity: 'notice',
		};
	})();

	const focus = (sev, grp = '') => {
		setSeverity(sev);
		setGroup(grp);
		setPage(1);
		revealFindings();
	};

	const runButton = isRunning ? (
		<Button variant="ghost" onClick={cancel}>{__('Stop', 'ai-rank-suite')}</Button>
	) : (
		<Button onClick={start} busy={busy}>
			<IconRefresh size={13} /> {never ? __('Run first audit', 'ai-rank-suite') : __('Run audit again', 'ai-rank-suite')}
		</Button>
	);

	return (
		<div className="ars-page">
			{/* The run itself: progress while it moves, the invitation before the
			    first run. Once a report exists the hero below carries the result. */}
			{(isRunning || !loaded || never) && (
				<Card
					title={__('Site Audit', 'ai-rank-suite')}
					subtitle={__('Every published page checked against 37 rules: the comparative problems a single-page score cannot see', 'ai-rank-suite')}
					action={runButton}
				>
					{isRunning ? (
						<Progress
							label={
								status.phase === 'pages'
									? sprintf(__('Checking pages — %1$s of %2$s', 'ai-rank-suite'), status.done, status.total)
									: status.phase === 'duplicates'
										? __('Comparing every page against every other…', 'ai-rank-suite')
										: __('Checking site foundations…', 'ai-rank-suite')
							}
							total={100}
							percent={status.percent}
							note={sprintf(
								/* translators: %s: number of findings so far. */
								_n('%s finding so far. The run continues in slices — leave this tab open.', '%s findings so far. The run continues in slices — leave this tab open.', status.issues, 'ai-rank-suite'),
								status.issues.toLocaleString()
							)}
						/>
					) : !loaded ? (
						<p className="m-0 text-[12.5px]" style={{ color: 'var(--ars-ink-3)' }}>{__('Loading…', 'ai-rank-suite')}</p>
					) : (
						<div className="grid gap-3">
							<p className="m-0 text-[13px]" style={{ color: 'var(--ars-ink-2)' }}>{__('Nothing audited yet. The first run reads every published page, then compares them against each other.', 'ai-rank-suite')}</p>
							<Explain title={__('What the audit finds that a page score cannot', 'ai-rank-suite')}>
								<p>
									{__('A per-page score is blind to everything comparative. Forty product pages can each score 80 and still share one description; a category can be noindexed by a theme setting nobody remembers; a page can be perfect and completely unlinked, so no crawler reaches it.', 'ai-rank-suite')}
								</p>
								<p>
									{__('The audit runs those checks across the whole site: duplicate titles, descriptions and H1s, orphan pages, foreign canonicals, thin content, missing structured data, and — on a shop — products without an image, price, identifier, brand or description, which is exactly what Google Merchant rejects.', 'ai-rank-suite')}
								</p>
							</Explain>
						</div>
					)}
					{error && <Notice tone="serious">{error}</Notice>}
				</Card>
			)}

			{/*
			 * The result as one hero: the ring, the sentence, the one button, and
			 * the four counts that make the sentence checkable — same anatomy as
			 * the dashboard, so the two screens read as one tool. The previous
			 * layout said the same numbers twice, once as bars and once as tiles,
			 * and never said what they meant.
			 */}
			{loaded && !never && !isRunning && verdict && (
				<div className="ars-hero">
					<div className="ars-hero-ring">
						<ScoreRing value={report.score} size={132} label={__('Audit score', 'ai-rank-suite')} onLight />
					</div>
					<div className="ars-hero-body">
						<span className={`ars-hero-badge is-${verdict.tone}`}>{verdict.badge}</span>
						<h2 className="ars-hero-title">{verdict.text}</h2>
						<p className="ars-hero-why">{verdict.why}</p>
						<div className="ars-hero-actions">
							{verdict.action && (
								<button type="button" className="ars-btn ars-btn-primary" onClick={() => focus(verdict.severity)}>
									{verdict.action}
								</button>
							)}
							{runButton}
							{report.delta && (
								<span
									className="ars-delta-strip"
									title={sprintf(
										/* translators: %s: identifier of the previous run. */
										__('Compared with the previous run %s', 'ai-rank-suite'),
										report.delta.previous
									)}
								>
									<span className="is-new">{sprintf(__('+%s new', 'ai-rank-suite'), report.delta.new.toLocaleString())}</span>
									<span className="is-fixed">{sprintf(__('−%s fixed', 'ai-rank-suite'), report.delta.fixed.toLocaleString())}</span>
									<span className="is-recurring">{sprintf(__('%s still there', 'ai-rank-suite'), report.delta.recurring.toLocaleString())}</span>
								</span>
							)}
							{/*
							  * Built as a template literal, so none of it was
							  * ever marked for translation and a German site
							  * read "156 pages · 30 rules · 21 passing" in
							  * English under a German heading. An unmarked
							  * string is invisible to the translation test as
							  * well, which is why it survived this long.
							  */}
							<span className="t-caption">
								{sprintf(
									/* translators: 1: pages checked, 2: rules in total, 3: rules passing. */
									_n('%1$s page · %2$s rules · %3$s passing', '%1$s pages · %2$s rules · %3$s passing', report.checked, 'ai-rank-suite'),
									report.checked.toLocaleString(),
									RULES.toLocaleString(),
									passing.toLocaleString()
								)}
								{report.ended ? ` · ${new Date(report.ended * 1000).toLocaleString()}` : ''}
							</span>
						</div>
					</div>
					<div className="ars-hero-stats">
						{[
							{ label: __('Critical', 'ai-rank-suite'), value: totals.critical, sev: 'critical', tone: totals.critical ? 'bad' : 'good' },
							{ label: __('Warnings', 'ai-rank-suite'), value: totals.warning, sev: 'warning', tone: totals.warning ? 'warn' : 'good' },
							{ label: __('Notices', 'ai-rank-suite'), value: totals.notice, sev: 'notice' },
							{ label: __('Accepted', 'ai-rank-suite'), value: report.ignored || 0, sev: null },
						].map((st) => (
							<button
								key={st.label}
								type="button"
								className="ars-hero-stat"
								onClick={() => st.sev !== null && focus(st.sev)}
								title={st.sev === null ? __('Findings you accepted — they stay out of the score', 'ai-rank-suite') : sprintf(__('Show only %s', 'ai-rank-suite'), st.label)}
							>
								<span className={`ars-hero-stat-v${st.tone ? ' is-' + st.tone : ''}`}>{st.value.toLocaleString()}</span>
								<span className="ars-hero-stat-l">{st.label}</span>
							</button>
						))}
					</div>
				</div>
			)}
			{error && loaded && !never && !isRunning && <Notice tone="serious">{error}</Notice>}
			{/* Findings over time: one point per audit run, straight out of the
			    run_id stamps already in the table. Two runs is the minimum for a
			    line to mean anything, so below that this stays out of the way.

			    The Delta gets the real change, not its mirror image: it used
			    to be passed first minus last, so a fall from 342 findings to
			    151 arrived as +191 and drew an up arrow beside the words "the
			    number that should be falling". `invert` keeps the colour
			    right — fewer findings is the good direction. */}
			{trend && trend.length > 0 && (
				<Card
					title={__('Findings over time', 'ai-rank-suite')}
					subtitle={__('One point per audit run — the number that should be falling', 'ai-rank-suite')}
					action={
						<Delta
							value={(trend[trend.length - 1]?.value ?? 0) - (trend[0]?.value ?? 0)}
							invert
							suffix={' ' + __('since first run', 'ai-rank-suite')}
						/>
					}
				>
					{/* The same rule as the dashboard's card, and now the same
					    components: a line when it has a shape worth drawing,
					    the composition of the total when it has not. Two runs
					    holding the same number drew a flat bar across the
					    card, which reads as a measurement and is the absence
					    of one. */}
					{trend.length >= 3 && new Set(trend.map((p) => p.value)).size > 1 ? (
						<LineChart
							data={trend}
							label={__('Open findings', 'ai-rank-suite')}
							color="var(--ars-series-3)"
							height={140}
							formatValue={(v) => `${v}`}
							bleed
						/>
					) : (
						<Composition
							parts={[
								{ label: __('Critical', 'ai-rank-suite'), value: trend[trend.length - 1]?.critical ?? 0, color: 'var(--ars-c-red-ink)' },
								{ label: __('Warning', 'ai-rank-suite'), value: trend[trend.length - 1]?.warning ?? 0, color: 'var(--ars-accent)' },
								{ label: __('Notice', 'ai-rank-suite'), value: trend[trend.length - 1]?.notice ?? 0, color: 'var(--ars-brand)' },
							]}
							total={trend[trend.length - 1]?.value ?? null}
						/>
					)}
					<div className="grid gap-3 sm:grid-cols-3 mt-3">
						{[
							['critical', __('Critical', 'ai-rank-suite'), 'var(--ars-c-red-ink)'],
							['warning', __('Warning', 'ai-rank-suite'), 'var(--ars-accent)'],
							['notice', __('Notice', 'ai-rank-suite'), 'var(--ars-brand)'],
						].map(([key, label, color]) => {
							const now = trend[trend.length - 1]?.[key] ?? 0;
							const was = trend[0]?.[key] ?? 0;
							return (
								<div key={key} className="ars-r-md px-3 py-2.5" style={{ background: 'var(--ars-surface-2)' }}>
									<div className="flex items-center justify-between gap-2">
										<span className="text-[12px] font-semibold" style={{ color }}>
											{label}
										</span>
										<Delta value={was - now} invert />
									</div>
									<div className="t-num text-[18px] font-bold mt-0.5">{now}</div>
								</div>
							);
						})}
					</div>
				</Card>
			)}

			{report && report.run_id && (
				<>
					{/*
					 * Where this site is weak, by area.
					 *
					 * The three severity tiles above say how many problems exist;
					 * they cannot say where. An SEO opening this screen wants the
					 * shape of the site — which part of it is dragging, how badly,
					 * and across how much of the content — before they read a
					 * single finding. Same instrument as the dashboard, so the two
					 * screens read as one tool.
					 *
					 * The score is a real measurement, not decoration: the share of
					 * this area's rules that pass, weighted so a critical costs
					 * four times a notice. An area with nothing wrong scores 100
					 * and says so rather than disappearing.
					 */}
					<Card title={__('Where this site is weak', 'ai-rank-suite')} subtitle={__('Every audited area, worst first — click one to see only its findings', 'ai-rank-suite')}>
						<div className="ars-areagrid">
							{areaRows(report).map((a) => {
								const total = a.segments.reduce((n, sg) => n + sg.value, 0) || 1;
								const band = a.score >= 80 ? 'good' : a.score >= 50 ? 'mid' : 'bad';
								return (
									<button
										key={a.id}
										type="button"
										className={`ars-areacard is-${band}${group === a.id ? ' is-active' : ''}`}
										onClick={() => focus(severity, a.id)}
									>
										<span className="ars-areacard-score t-num">{a.score}</span>
										<span className="ars-areacard-body">
											<span className="ars-areacard-label">{a.label}</span>
											<span className="ars-areacard-sub">{a.sub}</span>
											<span className="ars-areacard-bar" aria-hidden>
												{a.segments
													.filter((sg) => sg.value > 0)
													.map((sg) => (
														<span key={sg.label} style={{ width: `${(sg.value / total) * 100}%`, background: sg.color }} />
													))}
											</span>
										</span>
										<span className={`ars-areacard-reach${a.findings ? '' : ' is-clear'}`}>{a.reach}</span>
									</button>
								);
							})}
						</div>
					</Card>
					<AuditCopilot hasRun={(report?.checks || []).length > 0} />

					<Card title={__('Biggest wins first', 'ai-rank-suite')} subtitle={__('Ranked by severity, then by how many pages are affected — the bar is reach', 'ai-rank-suite')}>
						<div className="grid gap-1.5">
							{(report.checks || []).slice(0, 8).map((c) => (
								<button
									key={c.id}
									type="button"
									onClick={() => focus(c.severity, c.group)}
									className="flex items-center justify-between gap-3 px-3 py-2 text-left cursor-pointer"
									style={{
										border: '1px solid var(--ars-border)',
										borderRadius: 'var(--ars-r-sm)',
										background: 'var(--ars-surface)',
									}}
								>
									<span className="flex items-center gap-2 min-w-0">
										<span
											className="ars-tag"
											style={{
												background: `var(--ars-c-${SEV_TONE[c.severity]})`,
												color: `var(--ars-c-${SEV_TONE[c.severity]}-ink)`,
											}}
										>
											{SEV_LABEL[c.severity]}
										</span>
										<span className="font-medium text-[13px] truncate">{c.label}</span>
									</span>
									<span className="flex items-center gap-2 shrink-0">
										<span className="ars-winbar" aria-hidden>
											<span
												style={{
													width: `${Math.max(4, Math.round((c.count / winMax) * 100))}%`,
													background: `var(--ars-c-${SEV_TONE[c.severity]}-ink)`,
												}}
											/>
										</span>
										<span className="t-num font-semibold" style={{ minWidth: 48, textAlign: 'right' }}>
											{c.count.toLocaleString()}
										</span>
									</span>
								</button>
							))}
						</div>
					</Card>

					<div ref={findingsRef} aria-hidden style={{ scrollMarginTop: 12 }} />
					<Card
						title={__('Findings', 'ai-rank-suite')}
						subtitle={sprintf(__('%d on this filter', 'ai-rank-suite'), report.total)}
						action={
							<div className="flex items-center gap-2 flex-wrap">
								{report.total > 0 && (
									<button
										type="button"
										className="ars-btn ars-btn-ghost"
										disabled={exporting}
										onClick={exportCsv}
										title={__('Every finding of the last completed audit, one row each, for a spreadsheet or a ticket.', 'ai-rank-suite')}
									>{exporting ? __('Preparing…', 'ai-rank-suite') : __('Export CSV', 'ai-rank-suite')}</button>
								)}
								{i18n.multilingual && !i18n.perPost && (
									<a
										href="#/content/urls"
										className="ars-btn ars-btn-ghost"
										title={__('This site translates by URL, not by post. Filtering by language here shows findings on posts that have a page in that language; per-URL findings live in the URL inventory.', 'ai-rank-suite')}
									>{__('Per language → URLs', 'ai-rank-suite')}</a>
								)}
								{i18n.multilingual && (i18n.perPost || i18n.urlLangs) && (
									<select
										value={lang}
										onChange={(e) => {
											setLang(e.target.value);
											setPage(1);
										}}
										style={{ minWidth: '11rem' }}
										aria-label={__('Language', 'ai-rank-suite')}
									>
										<option value="">{__('All languages', 'ai-rank-suite')}</option>
										{i18n.languages.map((l) => (
											<option key={l.code} value={l.code}>
												{l.label}
											</option>
										))}
									</select>
								)}
								<ChipBar
										label={__('Severity', 'ai-rank-suite')}
									value={severity}
									onChange={(v) => {
										setSeverity(v);
										setPage(1);
									}}
									options={[
										{ id: '', label: __('All', 'ai-rank-suite') },
										{ id: 'critical', label: __('Critical', 'ai-rank-suite') },
										{ id: 'warning', label: __('Warning', 'ai-rank-suite') },
										{ id: 'notice', label: __('Notice', 'ai-rank-suite') },
									]}
									/>
							</div>
						}
					>
						<div className="grid gap-3">
							{/* Severity and area are the same kind of control — pick one
							    value, narrow the list — and they were wearing two different
							    costumes stacked on top of each other: a tonal segmented
							    control above a row of solid-blue bordered buttons. One chip
							    for both. */}
							<ChipBar
									label={__('Area', 'ai-rank-suite')}
								value={group}
								onChange={(v) => {
									setGroup(v);
									setPage(1);
								}}
								options={[
									{ id: '', label: __('All areas', 'ai-rank-suite') },
									...(report.groups || [])
										.filter((g) => g.count > 0)
										.map((g) => ({ id: g.id, label: g.label, count: g.count })),
								]}
								/>

							{types.length > 1 && (
								<ChipBar
									label={__('Type', 'ai-rank-suite')}
									value={ptype}
									onChange={(v) => {
										setPtype(v);
										setPage(1);
									}}
									options={[{ id: '', label: __('All types', 'ai-rank-suite') }, ...types.map((t) => ({ id: t.name, label: t.label }))]}
								/>
							)}

							{(report.ignored_rules || []).length > 0 && (
								<div className="ars-accepted-rules">
									<span className="t-caption">{__('Accepted rules (not reported):', 'ai-rank-suite')}</span>
									{report.ignored_rules.map((id) => (
										<button key={id} type="button" className="ars-chip" onClick={() => acceptRule(id, false)} title={__('Report this rule again', 'ai-rank-suite')}>
											{id} ×
										</button>
									))}
								</div>
							)}

							{report.items.length === 0 ? (
								<Empty title={__('Nothing to fix in this area', 'ai-rank-suite')} compact>{__('Every rule in this area passed on every page checked.', 'ai-rank-suite')}</Empty>
							) : (
								/*
								 * Grouped by issue, not by page.
								 *
								 * The flat list printed one row per page per failing
								 * rule, each carrying the same sentence of advice. Ten
								 * pages missing internal links produced ten identical
								 * paragraphs of "link to this page from a related
								 * article", and a thirty-row screen contained about
								 * eight distinct pieces of information.
								 *
								 * It also modelled the work wrongly. Nobody fixes a
								 * site page by page down a list; they pick a problem
								 * and clear it everywhere. So the issue is the row,
								 * the advice is written once, and the pages it affects
								 * fold out underneath it.
								 */
								<div className="grid gap-2">
									{groupByIssue(report.items).map((grp) => (
										<IssueGroup key={grp.key} group={grp} onAccept={ignore} onAcceptRule={acceptRule} />
									))}
								</div>
							)}

							{report.total > report.per_page && (
								<div className="flex items-center gap-2">
									<Button variant="ghost" disabled={page <= 1} onClick={() => setPage(page - 1)}>{__('Previous', 'ai-rank-suite')}</Button>
									<span className="text-[12px]" style={{ color: 'var(--ars-ink-3)' }}>
										{sprintf(
											/* translators: 1: current page number, 2: total pages. */
											__('Page %1$s of %2$s', 'ai-rank-suite'),
											page.toLocaleString(),
											Math.ceil(report.total / report.per_page).toLocaleString()
										)}
									</span>
									<Button
										variant="ghost"
										disabled={page >= Math.ceil(report.total / report.per_page)}
										onClick={() => setPage(page + 1)}
									>{__('Next', 'ai-rank-suite')}</Button>
								</div>
							)}
						</div>
					</Card>

					<Panel title={__('Audit alerts', 'ai-rank-suite')} note={__('Email on new findings', 'ai-rank-suite')}>
						<Alerts />
					</Panel>
				</>
			)}
		</div>
	);
}

/**
 * Every audited area as one weakness row.
 *
 * The score is the share of the area's weight that passes. A critical counts
 * four, a warning two, a notice one — so an area with one critical scores worse
 * than an area with three notices, which is the order somebody should work in.
 * The denominator is the area's own rules, so areas stay comparable even though
 * some carry three rules and others carry fifteen.
 */
function areaRows(report) {
	const WEIGHT = { critical: 4, warning: 2, notice: 1 };
	const byGroup = new Map((report.groups || []).map((g) => [g.id, { ...g, critical: 0, warning: 0, notice: 0, load: 0 }]));

	(report.checks || []).forEach((c) => {
		const row = byGroup.get(c.group);
		if (!row) return;
		row[c.severity] = (row[c.severity] || 0) + (c.count || 0);
		row.load += (WEIGHT[c.severity] || 1) * (c.count || 0);
	});

	const pages = Math.max(1, report.checked || 1);

	return [...byGroup.values()]
		.map((g) => {
			const findings = g.critical + g.warning + g.notice;
			// Load is capped against the pages checked so a single page with ten
			// problems cannot drive an area to zero on a thousand-page site.
			// Load is relative to the site, so on a large site a handful of
			// findings barely dents the number — but an area carrying a
			// critical cannot honestly read 100, and one carrying warnings
			// cannot read "clear". The caps keep the number truthful.
			let score = findings === 0 ? 100 : Math.max(0, Math.round(100 - Math.min(100, (g.load / (pages * 4)) * 100)));
			if (g.critical > 0) score = Math.min(score, 45);
			else if (g.warning > 0) score = Math.min(score, 79);
			else if (g.notice > 0) score = Math.min(score, 94);
			return {
				id: g.id,
				label: g.label,
				score,
				sub:
					findings === 0
						? __('nothing outstanding', 'ai-rank-suite')
						: [
								g.critical ? sprintf(__('%s critical', 'ai-rank-suite'), g.critical.toLocaleString()) : null,
								g.warning ? sprintf(__('%s warning', 'ai-rank-suite'), g.warning.toLocaleString()) : null,
								g.notice ? sprintf(__('%s notice', 'ai-rank-suite'), g.notice.toLocaleString()) : null,
							]
								.filter(Boolean)
								.join(' · '),
				segments: [
					{ label: __('Critical', 'ai-rank-suite'), value: g.critical, color: 'var(--ars-c-red-ink)' },
					{ label: __('Warning', 'ai-rank-suite'), value: g.warning, color: 'var(--ars-accent)' },
					{ label: __('Notice', 'ai-rank-suite'), value: g.notice, color: 'var(--ars-brand)' },
					{ label: __('Clear', 'ai-rank-suite'), value: findings === 0 ? 1 : 0, color: 'var(--ars-c-green-ink)' },
				],
				reach: findings === 0 ? __('clear', 'ai-rank-suite') : sprintf(_n('%d to fix', '%d to fix', findings, 'ai-rank-suite'), findings),
				findings,
			};
		})
		.sort((a, b) => a.score - b.score || b.findings - a.findings);
}

/**
 * Collapse the findings into one row per rule.
 *
 * Severity first, then reach: a critical on one page still outranks a notice on
 * forty, because "critical" already means it is costing something today.
 */
function groupByIssue(items) {
	const rank = { critical: 0, warning: 1, notice: 2 };
	const map = new Map();
	items.forEach((it) => {
		const key = it.check_label + '|' + it.severity;
		if (!map.has(key)) {
			map.set(key, { key, label: it.check_label, severity: it.severity, fix: it.fix, items: [] });
		}
		map.get(key).items.push(it);
	});
	return [...map.values()].sort(
		(a, b) => (rank[a.severity] ?? 3) - (rank[b.severity] ?? 3) || b.items.length - a.items.length
	);
}

/** One rule, its advice written once, and every page it affects. */
function IssueGroup({ group, onAccept, onAcceptRule }) {
	// A single-page issue has nothing to fold, so it opens: making somebody
	// click to reveal one row is a worse deal than the row itself.
	const [open, setOpen] = React.useState(group.items.length === 1);
	return (
		<section className={`ars-issue is-${group.severity}${open ? ' is-open' : ''}`}>
			<button type="button" className="ars-issue-head" onClick={() => setOpen((v) => !v)} aria-expanded={open}>
				<span className="ars-issue-sev">{SEV_LABEL[group.severity]}</span>
				<span className="ars-issue-label">{group.label}</span>
				<span className="ars-issue-count tabular-nums">
					{group.items.length} {group.items.length === 1 ? 'page' : 'pages'}
				</span>
				<svg className="ars-issue-caret" viewBox="0 0 24 24" fill="none" aria-hidden>
					<path d="m9 6 6 6-6 6" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round" />
				</svg>
			</button>
			{open && (
				<div className="ars-issue-body">
					<p className="ars-issue-fix">{group.fix}</p>
					{group.items.length > 1 && onAcceptRule && group.items[0]?.check && (
						<p className="m-0 mb-2">
							<button
								type="button"
								className="ars-chip"
								onClick={() => onAcceptRule(group.items[0].check)}
								title={__('Accept this rule for the whole site: every current finding is removed and future runs skip it. Reversible above.', 'ai-rank-suite')}
							>
								{sprintf(_n('Accept this rule for the %s page', 'Accept this rule for all %s pages', group.items.length, 'ai-rank-suite'), group.items.length.toLocaleString())}
							</button>
						</p>
					)}
					<ul className="ars-issue-pages">
						{group.items.map((it) => (
							<li key={it.id}>
								<span className="ars-issue-page" title={it.label}>
									{it.label}
								</span>
								{it.detail && <span className="ars-issue-detail">{it.detail}</span>}
								<span className="ars-issue-links">
									{it.post_id ? (
										<a href={`#/content/${it.post_id}`} title={__('Open this page in the optimiser: keyword, title, description and the failing checks in one place', 'ai-rank-suite')}>{__('Optimise', 'ai-rank-suite')}</a>
									) : null}
									{it.edit_url && (
										<a href={it.edit_url} target="_blank" rel="noreferrer">{__('Edit', 'ai-rank-suite')}</a>
									)}
									<a href={it.url} target="_blank" rel="noreferrer" className="inline-flex items-center gap-1">{__('View', 'ai-rank-suite')}<IconExternal size={10} />
									</a>
									<button
										type="button"
										onClick={() => onAccept(it.fingerprint)}
										className="ars-chip"
										title={__('Accept this finding — it stops being reported', 'ai-rank-suite')}
									>{__('Accept', 'ai-rank-suite')}</button>
								</span>
							</li>
						))}
					</ul>
				</div>
			)}
		</section>
	);
}
