import React, { useCallback, useEffect, useRef, useState } from 'react';
import { __, _n, sprintf } from '@wordpress/i18n';
import { api } from '../api';
import FirstRun from '../components/FirstRun';
import { Card, Button, Notice, MetricGrid, ChipBar, PageState, Progress } from '../components/ui';
import Explain from '../components/Explain';
import { IconExternal, IconRefresh, IconLink, IconArrows } from '../components/icons';

/**
 * A list of pages with the numbers that explain why each one is on it.
 * One table shape across five different questions keeps the screen readable.
 */
function PageTable({ rows, empty, columns = ['in', 'out', 'depth', 'words'] }) {
	if (!rows || rows.length === 0) {
		return (
			<p className="m-0 text-[13px]" style={{ color: 'var(--ars-ink-2)' }}>
				{empty}
			</p>
		);
	}
	const head = {
		in: __('Links in', 'ai-rank-suite'),
		out: __('Links out', 'ai-rank-suite'),
		depth: __('Clicks from home', 'ai-rank-suite'),
		words: __('Words', 'ai-rank-suite'),
		authority: 'Authority',
		score: 'GEO',
	};
	return (
		<div className="ars-table-wrap">
			<table className="ars-table">
				<thead>
					<tr>
						<th>{__('Page', 'ai-rank-suite')}</th>
						{columns.map((c) => (
							<th key={c} className="is-num">
								{head[c]}
							</th>
						))}
						<th className="is-action" />
					</tr>
				</thead>
				<tbody>
					{rows.map((r) => (
						<tr key={r.id}>
							<td>
								<div className="font-medium truncate" style={{ maxWidth: 440 }}>
									{r.title || __('(no title)', 'ai-rank-suite')}
								</div>
								<div className="t-caption">{r.type}</div>
							</td>
							{columns.map((c) => (
								<td key={c} className="is-num">
									{c === 'depth' ? (
										r.depth < 0 ? (
											'—'
										) : (
											r.depth
										)
									) : c === 'authority' ? (
										<AuthorityBar value={r.authority ?? 0} />
									) : (
										r[c] ?? '—'
									)}
								</td>
							))}
							<td className="is-action">
								<a href={r.url} target="_blank" rel="noreferrer noopener" title={__('Open page', 'ai-rank-suite')}>
									<IconExternal size={12} />
								</a>
							</td>
						</tr>
					))}
				</tbody>
			</table>
		</div>
	);
}

/*
 * A function, not a constant.
 *
 * These eight paragraphs were bare English strings: never wrapped in __(),
 * so every one of them showed in English on a German install, and the
 * translation test could not see them because an unmarked string is not a
 * translatable string. Wrapping them is the fix; making this a function is
 * what makes the wrapping work, because a module-level constant is built
 * when the bundle loads, which can be before wp.i18n has its catalogue.
 */
const viewCopy = () => ({
	orphans:
		__('Nothing on the site points at these. A crawler arriving from the sitemap can still reach them, but nothing tells it — or an AI assistant — that they matter.', 'ai-rank-suite'),
	deep: __('Four or more clicks from the home page. Depth is the one signal writing cannot fix: it is decided entirely by what links where.', 'ai-rank-suite'),
	underlinked:
		__('Eight hundred words or more, with at most one page pointing at them. The work here is already done — these are the cheapest wins on the site.', 'ai-rank-suite'),
	dead_ends:
		__('These link out to nothing. A reader who finishes has nowhere to go, and the link equity that arrived stops here instead of flowing on.', 'ai-rank-suite'),
	hubs: __('The most linked-to pages on the site. If this is not roughly your list of most important pages, your internal linking is arguing against your own priorities.', 'ai-rank-suite'),
	authority:
		__('Where the site’s own authority actually ends up. This is not a count of links: authority flows, so a page linked once from a strong page can outrank one linked twenty times from weak ones. If your privacy policy is near the top, that is a footer link on every page doing exactly what a footer link does.', 'ai-rank-suite'),
	wasted:
		__('These hold real internal authority and score badly. Every one is authority you already have, pointed at a page that cannot use it — usually a legal page, a tag archive or an old post the navigation never stopped linking to.', 'ai-rank-suite'),
	starved:
		__('The opposite: pages that are good and that nothing points at. Adding two or three links from your strongest pages is the cheapest ranking change available on any site.', 'ai-rank-suite'),
});

	/*
	 * Name the phase the run is actually in.
	 *
	 * "Checking pages" was printed for all five, so the crawl announced itself
	 * as reading pages long after it had finished reading them and moved on to
	 * resolving links, scoring authority and publishing. Five phases, one
	 * sentence, and a bar stuck at 100% while it happened.
	 */
	const phaseLabel = (p) => {
		switch (p?.phase) {
			case 'collect':
				return p.total > 0
					? sprintf(
						/* translators: 1: pages done, 2: pages total. */
						__('Reading pages — %1$s of %2$s', 'ai-rank-suite'),
						Number(p.done).toLocaleString(),
						Number(p.total).toLocaleString()
					)
					: __('Reading every published page…', 'ai-rank-suite');
			case 'resolve':
				return __('Resolving every link to the page it points at…', 'ai-rank-suite');
			case 'aggregate':
				return __('Counting what links to what…', 'ai-rank-suite');
			case 'authority':
				return __('Working out where your internal authority ends up…', 'ai-rank-suite');
			case 'publish':
				return __('Publishing the graph…', 'ai-rank-suite');
			default:
				return __('Building the graph…', 'ai-rank-suite');
		}
	};

export default function LinkStructure() {
	const [graph, setGraph] = useState(null);
	const [scanning, setScanning] = useState(false);
	const [error, setError] = useState(null);
	const [view, setView] = useState('orphans');
	const pumping = useRef(false);

	/*
	 * The run is driven from this screen, one slice at a time.
	 *
	 * Before this, the button posted /links/graph/build once and stopped. That
	 * request ticks for twenty seconds and hands the rest to WP-Cron, which on
	 * a WordPress site runs on page views rather than by the clock. On a quiet
	 * site the rest arrived hours later or never, while the screen sat on a
	 * progress bar frozen at whatever that one request reached. The site audit
	 * has pumped its own run from the browser for months; this is the same
	 * loop, and /links/graph/tick is the slice it asks for.
	 */
	const pump = useCallback(async () => {
		if (pumping.current) return;
		pumping.current = true;
		try {
			for (;;) {
				const next = await api.post('/links/graph/tick', {});
				setGraph(next);
				if (!next.progress?.running) break;
				/*
				 * A slice runs for up to twenty seconds, so this pause costs
				 * nothing. It matters when the lease is held by another worker
				 * (cron, a second tab): that tick returns immediately, and
				 * without a pause the loop would hammer the REST endpoint as
				 * fast as the network allows for as long as the run lasts.
				 */
				await new Promise((r) => setTimeout(r, 800));
			}
		} catch (e) {
			setError(e.message);
		} finally {
			pumping.current = false;
			setScanning(false);
		}
	}, []);

	useEffect(() => {
		api.get('/links/graph').then(setGraph).catch((e) => setError(e.message));
	}, []);

	/*
	 * A run whose tab was closed resumes when somebody opens this screen.
	 * Cron picks one up too; this is the faster of the two, because the
	 * customer is already looking at it.
	 */
	useEffect(() => {
		if (graph?.progress?.running && !pumping.current) {
			setScanning(true);
			pump();
		}
	}, [graph?.progress?.running, pump]);

	const scan = async () => {
		setScanning(true);
		setError(null);
		try {
			setGraph(await api.post('/links/graph/build'));
			pump();
		} catch (e) {
			setError(e.message);
			setScanning(false);
		}
	};

	if (!graph) return <PageState error={error} />;

	if (!graph.ready) {
		return (
			<div className="ars-page">
				{error && <Notice tone="serious">{error}</Notice>}

				{/*
				  * A run that died says so.
				  *
				  * The reason was written to the run row and never read back,
				  * so a build that failed on a missing database privilege
				  * looked exactly like one nobody had started: "not built yet",
				  * a fresh button, and no hint that anything had gone wrong.
				  * Somebody could press Build for ever and never learn why.
				  */}
				{graph.progress?.status === 'failed' && (
					<Notice tone="serious">
						<strong>{__('The last build did not finish.', 'ai-rank-suite')}</strong>{' '}
						{graph.progress.error
							? graph.progress.error
							: __('No reason was recorded.', 'ai-rank-suite')}
						{' '}
						{__('Pressing Build tries again from the start; nothing from the failed run is kept.', 'ai-rank-suite')}
					</Notice>
				)}

				{/*
				  * The same progress block the site audit uses, for the same
				  * reason: a crawl of a few hundred pages does not finish
				  * inside one request. It works for twenty seconds and carries
				  * on in the background, and a button that only says
				  * "Working…" while that happens is indistinguishable from one
				  * that has hung. The stripes move while the run does.
				  */}
				{!error && graph.progress?.running && (
					<Card
						icon={<IconArrows size={16} />}
						title={__('Link structure', 'ai-rank-suite')}
						subtitle={__('One pass over every published page — nothing leaves your server', 'ai-rank-suite')}
					>
						<Progress
							label={phaseLabel(graph.progress)}
							total={100}
							percent={graph.progress.percent}
							note={__('The run continues in slices — leave this tab open, or come back and it will have carried on.', 'ai-rank-suite')}
						/>
					</Card>
				)}
				<FirstRun
					badge={__('Graph not built', 'ai-rank-suite')}
					ringLabel={__('Structure', 'ai-rank-suite')}
					title={__('The link graph of this site has not been built yet.', 'ai-rank-suite')}
					why={__('One pass over every published page follows every internal link and works out which pages nothing points at, how many clicks each one sits from the home page, which anchor texts you re-use and where you link out to. Nothing leaves your server.', 'ai-rank-suite')}
					action={scanning ? 'Scanning…' : __('Build the graph', 'ai-rank-suite')}
					onAction={scan}
					busy={scanning}
					progress={
						graph.progress?.running
							? {
								running: true,
								done: graph.progress.done,
								total: graph.progress.total,
								percent: graph.progress.percent,
								label: phaseLabel(graph.progress),
								note: __('The run continues in slices — leave this tab open, or come back and it will have carried on.', 'ai-rank-suite'),
							}
							: null
					}
					stats={[{ label: __('Pages in graph', 'ai-rank-suite') }, { label: __('Orphans', 'ai-rank-suite') }, { label: __('Deep pages', 'ai-rank-suite') }, { label: __('Outbound hosts', 'ai-rank-suite') }]}
					previews={[
						{ title: __('Orphans', 'ai-rank-suite'), text: __('Pages no other page links to — invisible to a crawler that follows links.', 'ai-rank-suite') },
						{ title: __('Click depth', 'ai-rank-suite'), text: __('How far each page sits from the home page; beyond three clicks is where crawl budget dies.', 'ai-rank-suite') },
						{ title: __('Anchor texts', 'ai-rank-suite'), text: __('What you call your own pages when you link them — the words an engine learns them by.', 'ai-rank-suite') },
					]}
				/>
				<Card icon={<IconArrows size={16} />} title={__('Link structure', 'ai-rank-suite')} subtitle={__('One pass over every published page — nothing leaves your server', 'ai-rank-suite')}>
					<div className="grid gap-3">
						<p className="m-0 text-[13px]" style={{ color: 'var(--ars-ink-2)' }}>
							{__('The graph has not been built yet. Scanning reads every published page, follows every link, and works out which pages nothing points at, how many clicks each one sits from the home page, which anchor text you re-use, and where you link out to.', 'ai-rank-suite')}
						</p>
						<div>
							<Button onClick={scan} busy={scanning}>
								<IconRefresh size={13} />{__('Scan the site', 'ai-rank-suite')}</Button>
						</div>
					</div>
				</Card>
			</div>
		);
	}

	/*
	 * Count the rows, do not ask for a count that is not sent.
	 *
	 * These chips read graph.orphan_count, graph.deep_count and friends. The
	 * report has never returned any of them — it returns the lists — so every
	 * chip on a built graph read "Orphans (undefined)". Two of them used
	 * `?? 0` and were worse: a confident nought next to a table with rows in
	 * it. Counting the list that the chip actually switches to cannot drift
	 * from what the table then shows.
	 */
	const n = (rows) => (Array.isArray(rows) ? rows.length : 0);
	const views = [
		{ id: 'orphans', label: sprintf(__('Orphans (%s)', 'ai-rank-suite'), n(graph.orphans)) },
		{ id: 'deep', label: sprintf(__('Buried (%s)', 'ai-rank-suite'), n(graph.deep)) },
		{ id: 'underlinked', label: sprintf(__('Under-linked (%s)', 'ai-rank-suite'), n(graph.thin)) },
		{ id: 'dead_ends', label: sprintf(__('Dead ends (%s)', 'ai-rank-suite'), n(graph.dead_ends)) },
		{ id: 'hubs', label: __('Hubs', 'ai-rank-suite') },
		{ id: 'authority', label: __('Authority', 'ai-rank-suite') },
		{ id: 'wasted', label: sprintf(__('Wasted (%s)', 'ai-rank-suite'), n(graph.wasted)) },
		{ id: 'starved', label: sprintf(__('Starved (%s)', 'ai-rank-suite'), n(graph.starved)) },
	];

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

			{/* The four headline numbers, in the dashboard's dense grid. The tone
			   washes are gone — four coloured cards each made their own claim —
			   and colour now marks only the two states that are actually
			   warnings: orphans exist, or the site is on average too deep. */}
			<MetricGrid
				columns={4}
				metrics={[
					{
						id: 'pages',
						label: __('Pages in the graph', 'ai-rank-suite'),
						hint: __('Every published page the scan reached, and every internal link found between them.', 'ai-rank-suite'),
						value: graph.pages.toLocaleString(),
						sub: sprintf(__('%s internal links', 'ai-rank-suite'), graph.edges.toLocaleString()),
					},
					{
						id: 'orphans',
						label: __('Orphan pages', 'ai-rank-suite'),
						hint: __('Nothing on the site links to these. A crawler can still reach them from the sitemap, but nothing tells it they matter.', 'ai-rank-suite'),
						value: n(graph.orphans),
						accent: n(graph.orphans) > 0 ? 'var(--ars-accent)' : undefined,
						sub: __('nothing links to them', 'ai-rank-suite'),
					},
					{
						id: 'depth',
						label: __('Average click depth', 'ai-rank-suite'),
						hint: __('How many clicks a page sits from the home page, averaged. Three or fewer is the usual target — only links can change it.', 'ai-rank-suite'),
						value: graph.avg_depth,
						accent: graph.avg_depth > 3 ? 'var(--ars-accent)' : undefined,
						sub: graph.unreached ? sprintf(__('%s unreachable by links', 'ai-rank-suite'), graph.unreached) : __('from the home page', 'ai-rank-suite'),
					},
					{
						id: 'underlinked',
						label: __('Under-linked pages', 'ai-rank-suite'),
						hint: __('Eight hundred words or more with at most one page pointing at them — the cheapest wins on the site.', 'ai-rank-suite'),
						value: n(graph.thin),
						sub: __('long, but barely linked', 'ai-rank-suite'),
					},
				]}
			/>

			<Card
				icon={<IconArrows size={16} />} title={__('Link structure', 'ai-rank-suite')}
				subtitle={__('Where the links actually go, and which pages the structure is failing', 'ai-rank-suite')}
				action={
					<Button variant="ghost" onClick={scan} busy={scanning}>
						<IconRefresh size={13} />{__('Rescan', 'ai-rank-suite')}</Button>
				}
			>
				<div className="grid gap-3">
					<ChipBar options={views} value={view} onChange={setView} />
					<p className="m-0 text-[12.5px]" style={{ color: 'var(--ars-ink-2)' }}>
						{viewCopy()[view]}
					</p>
					<PageTable
						rows={graph[view]}
						empty="Nothing in this category — that is the result you want."
						columns={
							view === 'hubs'
								? ['in', 'out', 'depth']
								: ['authority', 'wasted', 'starved'].includes(view)
									? ['authority', 'in', 'depth', 'score']
									: ['in', 'out', 'depth', 'words']
						}
					/>
					{graph.broken.length > 0 && (
						<Notice tone="warning">
							{sprintf(_n('%d internal link points at a URL that does not', '%d internal links point at a URL that does not', graph.broken.length, 'ai-rank-suite'), graph.broken.length)}
							resolve. The Checker tab verifies each one over HTTP and offers a redirect.
						</Notice>
					)}
					<Explain title={__('What click depth is, and why it is not a vanity metric', 'ai-rank-suite')}>
						<p>
							{__('Click depth is the shortest number of links from the home page to a page. A crawler spends a budget per site: the deeper a page sits, the less often it is recrawled and the weaker the signal that it matters. Three clicks or fewer is the usual target. Unlike a word count or a title tag, nothing written on the page itself can change it — only a link from somewhere shallower can.', 'ai-rank-suite')}
						</p>
					</Explain>
				</div>
			</Card>

			<Card
				icon={<IconLink size={16} />} title={__('Anchor text', 'ai-rank-suite')}
				subtitle={__('The words you link with are how you tell an engine what the target page is about — when they say anything at all', 'ai-rank-suite')}
			>
				{graph.anchors.length === 0 ? (
					<p className="m-0 text-[13px]" style={{ color: 'var(--ars-ink-2)' }}>{__('No internal anchors found yet.', 'ai-rank-suite')}</p>
				) : (
					<div className="ars-table-wrap">
						<table className="ars-table">
							<thead>
								<tr>
									<th>{__('Anchor', 'ai-rank-suite')}</th>
									<th className="is-num">{__('Uses', 'ai-rank-suite')}</th>
									<th className="is-num">{__('Distinct targets', 'ai-rank-suite')}</th>
									<th className="is-mid">{__('Verdict', 'ai-rank-suite')}</th>
								</tr>
							</thead>
							<tbody>
								{graph.anchors.map((a, i) => (
									<tr key={i}>
										<td>
											<span className="truncate inline-block" style={{ maxWidth: 440 }}>
												{a.anchor}
											</span>
										</td>
										<td className="is-num">{a.count}</td>
										<td className="is-num">{a.targets}</td>
										<td className="is-mid">
											{a.generic ? (
												<span
													className="ars-tag"
													style={{ color: 'var(--ars-c-yellow-ink)', background: 'var(--ars-c-yellow)' }}
												>{__('Generic', 'ai-rank-suite')}</span>
											) : a.targets > 1 ? (
												<span
													className="ars-tag"
													style={{ color: 'var(--ars-c-blue-ink)', background: 'var(--ars-c-blue)' }}
												>{__('Ambiguous', 'ai-rank-suite')}</span>
											) : (
												<span className="t-caption">{__('Descriptive', 'ai-rank-suite')}</span>
											)}
										</td>
									</tr>
								))}
							</tbody>
						</table>
					</div>
				)}
				<Explain title={__('Generic and ambiguous, and what each one costs', 'ai-rank-suite')}>
					<p>
						“Read more” describes nothing, so the link carries no topic at all — the one place on the page where you get
						to name what the target is about is spent on two wasted words. An anchor marked ambiguous points at several
						different pages: the same phrase claiming to be about several things teaches an engine it is about none of
						them. Neither is a penalty; both are simply signals thrown away.
					</p>
				</Explain>
			</Card>

			<Card icon={<IconExternal size={16} />} title={__('Outbound domains', 'ai-rank-suite')} subtitle={__('Who you link out to, and how much of it you have marked nofollow', 'ai-rank-suite')}>
				{graph.external.length === 0 ? (
					<p className="m-0 text-[13px]" style={{ color: 'var(--ars-ink-2)' }}>
						{__('No external links found. Citing a source is one of the strongest quality signals there is — for Google, and for an assistant deciding whether a page is worth quoting.', 'ai-rank-suite')}
					</p>
				) : (
					<div className="ars-table-wrap">
						<table className="ars-table">
							<thead>
								<tr>
									<th>{__('Domain', 'ai-rank-suite')}</th>
									<th className="is-num">{__('Links', 'ai-rank-suite')}</th>
									<th className="is-num">{__('Nofollow', 'ai-rank-suite')}</th>
								</tr>
							</thead>
							<tbody>
								{graph.external.map((e) => (
									<tr key={e.domain}>
										<td className="ars-mono">{e.domain}</td>
										<td className="is-num">{e.count}</td>
										<td className="is-num">{e.nofollow}</td>
									</tr>
								))}
							</tbody>
						</table>
					</div>
				)}
			</Card>
		</div>
	);
}

/**
 * Authority as a bar, not a number.
 *
 * The absolute value is meaningless — it is normalised so the strongest page
 * on this site is 100 — and showing it as a bare figure invites people to
 * compare it against another site, where it would mean something different.
 * A bar reads as "relative to the rest of this site", which is what it is.
 */
function AuthorityBar({ value }) {
	return (
		<span className="ars-auth" title={`${value} of 100, relative to the strongest page on this site`}>
			<span className="ars-auth-bar">
				<i style={{ width: `${Math.max(2, value)}%` }} />
			</span>
			<b className="tabular-nums">{value}</b>
		</span>
	);
}
