import React, { useCallback, useEffect, useRef, useState } from 'react';
import { __, _n, sprintf } from '@wordpress/i18n';
import { api } from '../api';
import { Card, Button, Badge, Notice, ChipBar, ScorePill, ScoreRing, Bar, PageSkeleton } from '../components/ui';
import ScreenVerdict from '../components/ScreenVerdict';
import FirstRun from '../components/FirstRun';

/**
 * Every indexable URL of the site, per language.
 *
 * The post list sees WordPress objects; this screen sees what a crawler
 * sees — one row per URL per language, discovered from posts, the
 * translation plugin and the sitemap, then fetched and read as rendered HTML.
 * On a TranslatePress or Weglot site this is the only view that shows the
 * German, French and Arabic versions at all.
 */
const ISSUE_LABEL = {
	noindex: __('Noindex', 'ai-rank-suite'),
	no_title: __('No title', 'ai-rank-suite'),
	title_long: __('Title too long', 'ai-rank-suite'),
	no_desc: __('No description', 'ai-rank-suite'),
	no_h1: __('No H1', 'ai-rank-suite'),
	many_h1: __('Several H1', 'ai-rank-suite'),
	canonical_foreign: __('Canonical elsewhere', 'ai-rank-suite'),
	hreflang_missing: __('Hreflang missing', 'ai-rank-suite'),
	thin: __('Thin (<300 words)', 'ai-rank-suite'),
	html_lang_mismatch: __('html lang differs', 'ai-rank-suite'),
	unreachable: __('Unreachable', 'ai-rank-suite'),
};

const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const when = (ts) => new Date((Number(ts) || 0) * 1000).toLocaleString();
const num = (n) => (Number(n) || 0).toLocaleString();
// A sitemap URL in a progress line: the path is what tells the files apart.
const shortUrl = (u) => String(u || '').replace(/^https?:\/\/[^/]+/, '') || '/';

/**
 * What the screen owes its reader about discovery, in one sentence.
 *
 * The inventory is a snapshot: a run publishes when it finishes, and only a
 * run without a failed sitemap file may say it is complete. Everything here
 * comes from the summary the backend already computed — the running job
 * (phase, sitemap file and offset, files done and failed) and the last run's
 * snapshot (complete or not, failed files, skipped URLs). The one rule that
 * matters: "fully discovered" is said only when the snapshot says complete,
 * never inferred from a finished state or a healthy-looking URL count.
 */
function discoveryVerdict({ job, last, running, urls, onCancel, onDiscover, cancelling }) {
	const d = job || {};
	if (running) {
		let text;
		if (d.phase === 'sitemap') {
			text = d.current_sitemap
				? sprintf(
					/* translators: 1: sitemap file path, 2: URL offset within it, 3: files done, 4: files failed. */
					__('Discovering — reading sitemap file %1$s from URL %2$s; %3$s files done, %4$s failed', 'ai-rank-suite'),
					shortUrl(d.current_sitemap),
					num(d.current_offset),
					num(d.sitemaps_done),
					num(d.sitemaps_failed)
				)
				: sprintf(
					/* translators: 1: files done, 2: files failed. */
					__('Discovering — reading sitemap files; %1$s done, %2$s failed', 'ai-rank-suite'),
					num(d.sitemaps_done),
					num(d.sitemaps_failed)
				);
		} else if (d.phase === 'finish') {
			text = __('Discovering — publishing the run…', 'ai-rank-suite');
		} else {
			text = sprintf(
				/* translators: 1: posts read, 2: posts total. */
				__('Discovering — reading posts %1$s / %2$s', 'ai-rank-suite'),
				num(d.done),
				num(d.total)
			);
		}
		const sofar = sprintf(
			/* translators: 1: new URLs, 2: URLs confirmed. */
			__('%1$s new URLs so far, %2$s confirmed.', 'ai-rank-suite'),
			num(d.found),
			num(d.seen)
		);
		return {
			tone: 'neutral',
			text,
			why: sofar + ' ' + (last ? __('The numbers on this screen are still the last completed discovery until this run finishes.', 'ai-rank-suite') : __('Nothing is published yet — the inventory appears when this run completes.', 'ai-rank-suite')),
			progress: d.phase === 'posts' && d.total > 0 ? Math.min(100, Math.round((100 * (d.done || 0)) / d.total)) : null,
			action: __('Cancel discovery', 'ai-rank-suite'),
			onAction: onCancel,
			busy: cancelling,
		};
	}
	if (last && last.complete === true) {
		let why = sprintf(
			/* translators: 1: URLs in the inventory, 2: sitemap files read, 3: URLs removed. */
			__('%1$s URLs from posts, the translation plugin and %2$s sitemap files; %3$s URLs no longer on the site were removed.', 'ai-rank-suite'),
			num(last.urls ?? urls),
			num(last.sitemaps_done),
			num(last.removed)
		);
		if (d.state === 'cancelled' && (d.started || 0) > (last.ended || 0)) {
			why += ' ' + sprintf(__('A later discovery was cancelled on %s; the inventory is unchanged.', 'ai-rank-suite'), when(d.ended));
		}
		return { tone: 'good', text: sprintf(__('Fully discovered on %s', 'ai-rank-suite'), when(last.ended)), why };
	}
	if (last && last.complete === false) {
		return {
			tone: 'bad',
			text: sprintf(
				/* translators: 1: failed sitemap files, 2: skipped URLs. */
				_n('Incomplete — %1$s sitemap file failed, %2$s URLs skipped', 'Incomplete — %1$s sitemap files failed, %2$s URLs skipped', Number(last.sitemaps_failed) || 0, 'ai-rank-suite'),
				num(last.sitemaps_failed),
				num(last.skipped_urls)
			),
			why: sprintf(
				/* translators: %s: date and time of the run. */
				__('The run on %s published what it found and removed nothing — a partial run cannot tell a vanished URL from an unread sitemap. Fix the files below, then discover again; the last complete inventory stays until a run finishes without failures.', 'ai-rank-suite'),
				when(last.ended)
			),
			failed: Array.isArray(last.failed) ? last.failed : [],
			action: __('Discover again', 'ai-rank-suite'),
			onAction: onDiscover,
		};
	}
	if (d.state === 'cancelled') {
		return {
			tone: 'warning',
			text: sprintf(__('Discovery cancelled on %s — no complete discovery on record.', 'ai-rank-suite'), when(d.ended)),
			why: __('Nothing from the cancelled run was published. Discover again to get a verified inventory.', 'ai-rank-suite'),
			action: __('Discover again', 'ai-rank-suite'),
			onAction: onDiscover,
		};
	}
	if (urls > 0) {
		// Rows from before runs were recorded: nobody can say whether they are
		// the whole site, so the screen must not either.
		return {
			tone: 'warning',
			text: __('Discovered before completeness was tracked — whether this inventory is complete is unknown.', 'ai-rank-suite'),
			why: __('Discover again to get a verified inventory: the run reports every sitemap file it read and every one it could not.', 'ai-rank-suite'),
			action: __('Discover again', 'ai-rank-suite'),
			onAction: onDiscover,
		};
	}
	return null;
}

/**
 * Whether the scores on the screen come from a finished scan.
 *
 * A rescan stages its scores and publishes them in one statement when it
 * completes, so while it runs the numbers above are still the previous
 * scan's — the screen says so rather than letting the reader watch a bar
 * move and assume the averages move with it.
 */
function scanNote({ job, last, running, scanned }) {
	const j = job || {};
	if (running) {
		if (j.phase === 'publish') {
			return {
				tone: 'info',
				text: __('Rescan complete — publishing the new scores in chunks; the numbers above switch when the last chunk lands', 'ai-rank-suite'),
			};
		}
		return {
			tone: 'info',
			text: j.rescan
				? __('Rescan running — the numbers above are from the last completed scan until it finishes', 'ai-rank-suite')
				: __('First scan running — each URL is scored as soon as it has been fetched, so the averages move with it', 'ai-rank-suite'),
		};
	}
	if (last) {
		let text = sprintf(
			/* translators: 1: date and time, 2: URLs fetched, 3: URLs that failed. */
			__('Scores from the scan completed on %1$s — %2$s URLs fetched, %3$s failed.', 'ai-rank-suite'),
			when(last.ended),
			num(last.done),
			num(last.failed)
		);
		if (j.state === 'cancelled' && (j.started || 0) > (last.ended || 0)) {
			text += ' ' + __('A later scan was cancelled; the published scores are unchanged.', 'ai-rank-suite');
		}
		return { tone: 'good', text };
	}
	if (j.state === 'cancelled') {
		return { tone: 'warning', text: __('The scan was cancelled before it completed — the scores shown are partial. Rescan to get a consistent set.', 'ai-rank-suite') };
	}
	if (scanned > 0) {
		return { tone: 'warning', text: __('These scores are not from a completed scan — the last scan was interrupted or predates scan tracking. Rescan to get a consistent set.', 'ai-rank-suite') };
	}
	return null;
}

/** The discovery verdict with what belongs under it: a progress bar while running, the failed files when incomplete. */
function DiscoveryVerdict({ verdict }) {
	if (!verdict) return null;
	const { progress, failed, ...v } = verdict;
	return (
		<ScreenVerdict {...v}>
			{progress !== null && progress !== undefined && (
				<div className="mt-2" style={{ maxWidth: 360 }}>
					<Bar value={progress} />
				</div>
			)}
			{Array.isArray(failed) && failed.length > 0 && (
				<ul className="m-0 mt-2 p-0 grid gap-1" style={{ listStyle: 'none' }}>
					{failed.map((f) => (
						<li key={f.url} className="text-[12px] flex gap-2 flex-wrap items-baseline">
							<Badge tone="serious">{__('Failed', 'ai-rank-suite')}</Badge>
							<code className="ars-mono" style={{ wordBreak: 'break-all' }}>{f.url}</code>
							<span style={{ color: 'var(--ars-ink-2)' }}>{f.error}</span>
						</li>
					))}
				</ul>
			)}
		</ScreenVerdict>
	);
}

export default function Urls() {
	const [summary, setSummary] = useState(null);
	const [items, setItems] = useState([]);
	const [total, setTotal] = useState(0);
	const [page, setPage] = useState(1);
	const [lang, setLang] = useState('');
	const [issue, setIssue] = useState('');
	const [orderby, setOrderby] = useState('id');
	const [search, setSearch] = useState('');
	const [error, setError] = useState(null);
	const [busy, setBusy] = useState('');
	const [cancelling, setCancelling] = useState('');
	const running = useRef(false);

	const loadSummary = useCallback(() => api.get('/urls/summary').then(setSummary).catch((e) => setError(e.message)), []);

	// A job started in another tab (or by an earlier visit) has nobody here
	// pumping ticks; its progress still has to move on this screen, and a
	// cancel pressed here has to be seen. Poll while a job runs and this tab
	// is not the one driving it.
	const jobRunning = summary?.discover?.state === 'running' || summary?.scan?.state === 'running';
	useEffect(() => {
		if (!jobRunning || busy) return undefined;
		const id = setInterval(loadSummary, 4000);
		return () => clearInterval(id);
	}, [jobRunning, busy, loadSummary]);

	// Cancel is a route of its own: the job's state flips to cancelled, an
	// unpublished discovery's rows and a rescan's staged scores are dropped,
	// and the pump loop (here or elsewhere) sees the state on its next tick.
	const cancel = async (name, path) => {
		setCancelling(name);
		setError(null);
		try {
			await api.post(path, {});
			await loadSummary();
		} catch (e) {
			setError(e.message);
		} finally {
			setCancelling('');
		}
	};
	const loadList = useCallback(async () => {
		try {
			const params = new URLSearchParams({ lang, issue, orderby, search, page: String(page), per_page: '50' });
			const res = await fetch(`${window.airanksuiteData.restUrl}/urls?${params}`, { headers: { 'X-WP-Nonce': window.airanksuiteData.nonce }, credentials: 'same-origin' });
			if (!res.ok) throw new Error(sprintf(__('Request failed (%s)', 'ai-rank-suite'), res.status));
			setTotal(Number(res.headers.get('X-WP-Total') || 0));
			setItems(await res.json());
		} catch (e) {
			setError(e.message);
		}
	}, [lang, issue, orderby, search, page]);

	useEffect(() => {
		loadSummary();
	}, [loadSummary]);
	useEffect(() => {
		loadList();
	}, [loadList]);

	// Discovery and scan are cursor jobs behind the job guard: pump one tick at
	// a time, wait a beat when locked or backing off, refresh the head as it moves.
	const pump = async (name, startPath, tickPath, body = {}) => {
		if (running.current) return;
		running.current = true;
		setBusy(name);
		setError(null);
		try {
			let s = await api.post(startPath, body);
			// The head shows the run's phase from its first tick, not from its fifth.
			loadSummary();
			let n = 0;
			while (s.state === 'running') {
				if (s.busy || s.error) await sleep(2000);
				s = await api.post(tickPath, {});
				if (++n % 5 === 0) loadSummary();
			}
			await loadSummary();
			await loadList();
		} catch (e) {
			setError(e.message);
		} finally {
			running.current = false;
			setBusy('');
		}
	};

	if (!summary) {
		// Error first: a failed request must not sit behind a shimmer forever.
		return error ? (
			<div className="ars-page">
				<Notice tone="serious">{sprintf(__('URL inventory could not be loaded: %s', 'ai-rank-suite'), error)}</Notice>
			</div>
		) : (
			<PageSkeleton blocks={3} label={__('Loading URLs', 'ai-rank-suite')} />
		);
	}

	const s = summary;
	const scanPct = s.urls ? Math.round((100 * s.scanned) / s.urls) : 0;
	const discoverRunning = s.discover?.state === 'running' || busy === 'discover';
	const scanRunning = s.scan?.state === 'running' || busy === 'scan';
	const startDiscover = () => pump('discover', '/urls/discover/start', '/urls/discover/tick');
	const discovery = discoveryVerdict({
		job: s.discover,
		last: s.discover_last || null,
		running: discoverRunning,
		urls: s.urls,
		onCancel: () => cancel('discover', '/urls/discover/cancel'),
		onDiscover: startDiscover,
		cancelling: cancelling === 'discover',
	});
	const scanState = scanNote({ job: s.scan, last: s.scan_last || null, running: scanRunning, scanned: s.scanned });

	// A discovery in progress with nothing published yet: the first-run card
	// would hide the only progress there is, so the verdict stands alone.
	if (s.urls === 0 && discoverRunning && discovery) {
		return (
			<div className="ars-page">
				{error && <Notice tone="serious">{error}</Notice>}
				<DiscoveryVerdict verdict={discovery} />
			</div>
		);
	}

	if (s.urls === 0 && !discoverRunning) {
		return (
			<div className="ars-page">
				{error && <Notice tone="serious">{error}</Notice>}
				<FirstRun
					badge={__('Not discovered yet', 'ai-rank-suite')}
					ringLabel={__('URL score', 'ai-rank-suite')}
					title={__('No URL of this site has been inventoried yet.', 'ai-rank-suite')}
					why={
						s.languages > 1
							? sprintf(
								/* translators: 1: number of languages, 2: " through <plugin>" or empty. */
								__('This site serves %1$d languages%2$s. The post list sees one object per page; the URL inventory sees every language version as its own URL — discovered from posts, the translation plugin and the sitemap, then fetched and read as rendered HTML.', 'ai-rank-suite'),
								s.languages,
								s.plugin ? sprintf(__(' through %s', 'ai-rank-suite'), s.plugin) : ''
							)
							: __('The URL inventory lists every indexable URL a crawler would find — from posts and the sitemap — and scans each one as rendered HTML: title, description, H1, canonical, hreflang, robots and word count, scored per URL.', 'ai-rank-suite')
					}
					action={__('Discover URLs', 'ai-rank-suite')}
					onAction={startDiscover}
					busy={busy === 'discover'}
					stats={[{ label: __('URLs', 'ai-rank-suite') }, { label: __('Languages', 'ai-rank-suite') }, { label: __('Scanned', 'ai-rank-suite') }, { label: __('Indexable', 'ai-rank-suite') }]}
					previews={[
						{ title: __('Per language', 'ai-rank-suite'), text: __('URLs, scanned share, average SEO and GEO, missing hreflang and broken pages — one row per language.', 'ai-rank-suite') },
						{ title: __('Every URL, filtered', 'ai-rank-suite'), text: __('By language, by issue, by post type; sorted by the weakest score first; server-paged.', 'ai-rank-suite') },
						{ title: __('What the page really says', 'ai-rank-suite'), text: __('The scan reads the rendered HTML, not the database — a translated title that never made it into the page is caught here.', 'ai-rank-suite') },
					]}
				/>
			</div>
		);
	}

	const tone = s.avg_seo === null ? 'neutral' : s.avg_seo >= 80 ? 'good' : s.avg_seo >= 50 ? 'warning' : 'bad';
	const weakLang = [...(s.by_lang || [])].filter((l) => l.avg_seo !== null).sort((a, b) => a.avg_seo - b.avg_seo)[0];

	// The hero badge is about the inventory first: an incomplete or unverified
	// inventory must not wear "Healthy" however good the average of what it holds.
	const inventoryBadge =
		discovery && discovery.tone === 'bad'
			? { tone: 'bad', label: __('Incomplete inventory', 'ai-rank-suite') }
			: discovery && discovery.tone === 'warning'
				? { tone: 'warning', label: __('Completeness unknown', 'ai-rank-suite') }
				: null;
	const badge = inventoryBadge || {
		tone,
		label: s.scanned === 0 ? __('Discovered, not scanned', 'ai-rank-suite') : tone === 'good' ? __('Healthy', 'ai-rank-suite') : tone === 'warning' ? __('Room to improve', 'ai-rank-suite') : __('Needs attention', 'ai-rank-suite'),
	};

	return (
		<div className="ars-page">
			{error && <Notice tone="serious">{error}</Notice>}

			<DiscoveryVerdict verdict={discovery} />

			<div className="ars-hero">
				<div className="ars-hero-ring">
					<ScoreRing value={s.avg_seo} size={132} label={__('Avg URL SEO', 'ai-rank-suite')} onLight />
				</div>
				<div className="ars-hero-body">
					<span className={`ars-hero-badge is-${badge.tone}`}>{badge.label}</span>
					<h2 className="ars-hero-title">
						{sprintf(
							/* translators: 1: URL count, 2: "in N languages" or "in one language", 3: scanned count. */
							__('%1$s URLs %2$s — %3$s scanned', 'ai-rank-suite'),
							num(s.urls),
							s.languages > 1 ? sprintf(_n('in %d language', 'in %d languages', s.languages, 'ai-rank-suite'), s.languages) : __('in one language', 'ai-rank-suite'),
							num(s.scanned)
						)}
						{s.avg_seo !== null ? sprintf(__(', average SEO %1$d, GEO %2$d.', 'ai-rank-suite'), s.avg_seo, s.avg_geo) : '.'}
					</h2>
					<p className="ars-hero-why">
						{s.scanned === 0
							? __('Scanning fetches every URL as a visitor and reads what the page says about itself — title, description, H1, canonical, hreflang, robots, words. On a large site it runs in slices; you can leave and it continues.', 'ai-rank-suite')
							: weakLang && s.by_lang.length > 1
								? sprintf(
										/* translators: 1: language, 2: SEO score, 3: " with N URLs missing hreflang" or empty. */
										__('%1$s is the weakest language at SEO %2$d%3$s. Every language is scored with the same rubric.', 'ai-rank-suite'),
										weakLang.label,
										weakLang.avg_seo,
										weakLang.hreflang_missing
											? sprintf(__(' with %s URLs missing hreflang', 'ai-rank-suite'), weakLang.hreflang_missing.toLocaleString())
											: ''
									)
								: __('Scores are per URL and per language, from the rendered page — the same rubric everywhere.', 'ai-rank-suite')}
					</p>
					<div className="ars-hero-actions">
						<button type="button" className="ars-btn ars-btn-primary" onClick={() => pump('scan', '/urls/scan/start', '/urls/scan/tick', { rescan: s.scanned >= s.urls })} disabled={!!busy || scanRunning}>
							{scanRunning
								? sprintf(
									/* translators: 1: URLs done, 2: URLs total. */
									s.scan?.rescan ? __('Rescanning… %1$s / %2$s', 'ai-rank-suite') : __('Scanning… %1$s / %2$s', 'ai-rank-suite'),
									num(s.scan?.done),
									num(s.scan?.total ?? s.urls)
								)
								: s.scanned === 0
									? __('Scan all URLs', 'ai-rank-suite')
									: s.scanned < s.urls
										? sprintf(__('Scan %s unscanned', 'ai-rank-suite'), num(s.urls - s.scanned))
										: __('Rescan everything', 'ai-rank-suite')}
						</button>
						{scanRunning && (
							<Button variant="ghost" busy={cancelling === 'scan'} onClick={() => cancel('scan', '/urls/scan/cancel')}>
								{__('Cancel scan', 'ai-rank-suite')}
							</Button>
						)}
						{!discoverRunning && (
							<button type="button" className="ars-btn ars-btn-ghost" onClick={startDiscover} disabled={!!busy}>
								{__('Discover again', 'ai-rank-suite')}
							</button>
						)}
						{scanRunning && s.scan?.total > 0 && (
							<span style={{ minWidth: 160 }}>
								<Bar value={Math.round((100 * (s.scan.done || 0)) / Math.max(1, s.scan.total))} />
							</span>
						)}
					</div>
					{scanState && (
						<p className="m-0 text-[12.5px]" style={{ color: scanState.tone === 'warning' ? 'var(--ars-warning)' : 'var(--ars-ink-2)' }}>
							{scanState.text}
						</p>
					)}
				</div>
				<div className="ars-hero-stats">
					{[
						{ label: __('URLs', 'ai-rank-suite'), value: s.urls.toLocaleString() },
						{ label: __('Languages', 'ai-rank-suite'), value: s.languages },
						{ label: __('Scanned', 'ai-rank-suite'), value: `${scanPct}%` },
						{ label: __('Indexable', 'ai-rank-suite'), value: s.indexable.toLocaleString() },
					].map((st) => (
						<div key={st.label} className="ars-hero-stat" style={{ cursor: 'default' }}>
							<span className="ars-hero-stat-v">{st.value}</span>
							<span className="ars-hero-stat-l">{st.label}</span>
						</div>
					))}
				</div>
			</div>

			{s.by_lang.length > 0 && (
				<Card title={__('By language', 'ai-rank-suite')} subtitle={__('Click a language to filter the list — the same rubric on every one', 'ai-rank-suite')} flush>
					<div className="ars-table-wrap">
						<table className="ars-table">
							<thead>
								<tr>
									<th>{__('Language', 'ai-rank-suite')}</th>
									<th className="is-num">{__('URLs', 'ai-rank-suite')}</th>
									<th className="is-num">{__('Scanned', 'ai-rank-suite')}</th>
									<th className="is-num">{__('Indexable', 'ai-rank-suite')}</th>
									<th className="is-num">{__('SEO', 'ai-rank-suite')}</th>
									<th className="is-num">{__('GEO', 'ai-rank-suite')}</th>
									<th className="is-num">{__('Hreflang missing', 'ai-rank-suite')}</th>
									<th className="is-num">{__('Broken', 'ai-rank-suite')}</th>
								</tr>
							</thead>
							<tbody>
								{s.by_lang.map((l) => (
									<tr key={l.lang} onClick={() => { setLang(lang === l.lang ? '' : l.lang); setPage(1); }} style={{ cursor: 'pointer' }} className={lang === l.lang ? 'is-selected' : ''}>
										<td className="font-medium">
											{l.label} {l.lang && <code className="ars-mono t-caption">{l.lang}</code>}
										</td>
										<td className="is-num">{l.urls.toLocaleString()}</td>
										<td className="is-num">{l.scanned.toLocaleString()}</td>
										<td className="is-num">{l.indexable.toLocaleString()}</td>
										<td className="is-num"><ScorePill value={l.avg_seo} /></td>
										<td className="is-num"><ScorePill value={l.avg_geo} /></td>
										<td className="is-num" style={l.hreflang_missing ? { color: 'var(--ars-warning)', fontWeight: 600 } : undefined}>{l.hreflang_missing || '—'}</td>
										<td className="is-num" style={l.broken ? { color: 'var(--ars-serious)', fontWeight: 600 } : undefined}>{l.broken || '—'}</td>
									</tr>
								))}
							</tbody>
						</table>
					</div>
				</Card>
			)}

			<Card
				title={__('URLs', 'ai-rank-suite')}
				subtitle={sprintf(__('%s on this filter — click a row’s Edit to fix the page it renders', 'ai-rank-suite'), total.toLocaleString())}
				action={
					<div className="flex items-center gap-2 flex-wrap">
						<ChipBar label={__('Sort', 'ai-rank-suite')} value={orderby} onChange={setOrderby} options={[{ id: 'id', label: __('Discovered', 'ai-rank-suite') }, { id: 'seo', label: __('Weakest SEO', 'ai-rank-suite') }, { id: 'geo', label: __('Weakest GEO', 'ai-rank-suite') }, { id: 'words', label: __('Fewest words', 'ai-rank-suite') }]} />
					</div>
				}
				flush
			>
				<div className="p-3 grid gap-2" style={{ borderBottom: '1px solid var(--ars-border)' }}>
					<div className="flex items-center gap-2 flex-wrap">
						<ChipBar
							label={__('Issue', 'ai-rank-suite')}
							value={issue}
							onChange={(v) => { setIssue(v); setPage(1); }}
							options={[{ id: '', label: __('All', 'ai-rank-suite') }, ...Object.entries(ISSUE_LABEL).map(([id, label]) => ({ id, label }))]}
						/>
						<input type="search" placeholder={__('Search URL or title…', 'ai-rank-suite')} value={search} onChange={(e) => { setSearch(e.target.value); setPage(1); }} style={{ minWidth: 220 }} />
					</div>
				</div>
				<div className="ars-table-wrap">
					<table className="ars-table">
						<thead>
							<tr>
								<th>{__('URL', 'ai-rank-suite')}</th>
								<th style={{ width: 70 }}>{__('Lang', 'ai-rank-suite')}</th>
								<th className="is-num" style={{ width: 60 }}>{__('HTTP', 'ai-rank-suite')}</th>
								<th className="is-num" style={{ width: 70 }}>{__('Words', 'ai-rank-suite')}</th>
								<th className="is-num" style={{ width: 60 }}>{__('SEO', 'ai-rank-suite')}</th>
								<th className="is-num" style={{ width: 60 }}>{__('GEO', 'ai-rank-suite')}</th>
								<th>{__('Issues', 'ai-rank-suite')}</th>
								<th style={{ width: 90 }} />
							</tr>
						</thead>
						<tbody>
							{items.map((u) => (
								<tr key={u.id}>
									<td style={{ maxWidth: 420 }}>
										<div className="ars-mono text-[12px] truncate" title={u.url}>{u.url.replace(/^https?:\/\/[^/]+/, '') || '/'}</div>
										{u.title && <div className="t-caption truncate">{u.title}</div>}
									</td>
									<td><code className="ars-mono text-[11.5px]">{u.lang || '—'}</code></td>
									<td className="is-num">{u.scanned_at ? <Badge tone={u.status === 200 ? 'good' : 'serious'}>{u.status || '×'}</Badge> : <span className="is-muted">—</span>}</td>
									<td className="is-num">{u.scanned_at ? u.words.toLocaleString() : '—'}</td>
									<td className="is-num"><ScorePill value={u.seo_score} /></td>
									<td className="is-num"><ScorePill value={u.geo_score} /></td>
									<td>
										<span className="flex flex-wrap gap-1">
											{(u.issues || []).slice(0, 4).map((i) => (
												<Badge key={i} tone={['noindex', 'canonical_foreign', 'unreachable'].includes(i) ? 'serious' : 'warning'}>{ISSUE_LABEL[i] || i}</Badge>
											))}
											{(u.issues || []).length > 4 && <span className="t-caption">+{u.issues.length - 4}</span>}
										</span>
									</td>
									<td className="is-num">
										<span className="ars-rowacts" style={{ opacity: 1 }}>
											{u.edit_url && <a href={u.edit_url} target="_blank" rel="noreferrer">{__('Edit', 'ai-rank-suite')}</a>}
											<a href={u.url} target="_blank" rel="noreferrer">{__('View', 'ai-rank-suite')}</a>
										</span>
									</td>
								</tr>
							))}
						</tbody>
					</table>
				</div>
				{total > 50 && (
					<div className="flex items-center justify-between px-3 py-2">
						<span className="t-caption">{sprintf(__('Page %1$s of %2$s', 'ai-rank-suite'), page, Math.ceil(total / 50))}</span>
						<span className="flex gap-2">
							<Button variant="ghost" disabled={page <= 1} onClick={() => setPage(page - 1)}>{__('← Prev', 'ai-rank-suite')}</Button>
							<Button variant="ghost" disabled={page >= Math.ceil(total / 50)} onClick={() => setPage(page + 1)}>{__('Next →', 'ai-rank-suite')}</Button>
						</span>
					</div>
				)}
			</Card>
		</div>
	);
}
