import { __, _n, sprintf } from '@wordpress/i18n';
import React, { useEffect, useRef, useState } from 'react';
import { api } from '../api';
import { Card, Switch, Badge, Notice, Button, Sparkline, MetricGrid, ChipBar, PageState } from '../components/ui';
import Explain from '../components/Explain';
import ScreenVerdict from '../components/ScreenVerdict';
import { IconShield, IconRefresh, IconBot, IconExternal } from '../components/icons';

/**
 * AI crawler controls.
 *
 * The matrix on its own is a list of decisions made in the dark: nobody knows
 * whether GPTBot has ever visited their site, or — the question that matters
 * most — whether the crawler they blocked last month actually stopped. So the
 * evidence comes first and the switches sit beside it.
 *
 * The honest framing runs through the whole screen. robots.txt is a request,
 * not a fence. A crawler that keeps arriving after being disallowed is
 * ignoring it, and this is the only place that becomes visible.
 */

const REGIONS = [
	{
		id: 'us',
		label: __('United States', 'ai-rank-suite'),
		hint: __('ChatGPT, Claude, Gemini, Perplexity — the assistants most of your visitors use today', 'ai-rank-suite'),
	},
	{ id: 'eu', label: __('Europe', 'ai-rank-suite'), hint: __('Mistral and Yandex', 'ai-rank-suite') },
	{
		id: 'cn',
		label: __('China', 'ai-rank-suite'),
		hint: __('Doubao, Qwen, ERNIE, Kimi, Huawei and DeepSeek — the fastest-growing source of AI answers outside the US, and the most aggressive crawlers on this list', 'ai-rank-suite'),
	},
];

const PURPOSE = {
	training: {
		label: __('Training', 'ai-rank-suite'),
		tone: 'neutral',
		why: __('Reads your pages to train a model. Blocking costs you nothing today and nothing tomorrow — no citation comes from this.', 'ai-rank-suite'),
	},
	search: {
		label: __('Search index', 'ai-rank-suite'),
		tone: 'brand',
		why: __('Builds the index the assistant answers from. Block this and you cannot be cited at all.', 'ai-rank-suite'),
	},
	'on-demand': {
		label: __('On demand', 'ai-rank-suite'),
		tone: 'brand',
		why: __('Fetches your page because a user asked about it right now. Blocking this is blocking a visitor.', 'ai-rank-suite'),
	},
};

export default function Crawlers() {
	const [data, setData] = useState(null);
	const [error, setError] = useState(null);
	const [busy, setBusy] = useState(false);
	const [verified, setVerified] = useState(null);
	const [view, setView] = useState('all');
	const [vendor, setVendor] = useState('');
	const listRef = useRef(null);

	const load = () => api.get('/crawlers').then(setData).catch((e) => setError(e.message));

	useEffect(() => {
		load();
	}, []);

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

	const { settings, bots, traffic } = data;
	const blocked = new Set(settings.blocked);
	const hits = traffic?.bots || {};

	const persist = async (next) => {
		setBusy(true);
		try {
			setData(await api.put('/crawlers', { ...settings, ...next }));
			setError(null);
		} catch (e) {
			setError(e.message);
		} finally {
			setBusy(false);
		}
	};

	const toggleBot = (id) => {
		const next = new Set(blocked);
		if (next.has(id)) next.delete(id);
		else next.add(id);
		persist({ blocked: [...next] });
	};

	const bulk = (predicate, block) => {
		const next = new Set(blocked);
		Object.entries(bots).forEach(([id, b]) => {
			if (!predicate(b, id)) return;
			if (block) next.add(id);
			else next.delete(id);
		});
		persist({ blocked: [...next] });
	};

	const verify = async () => {
		setBusy(true);
		setVerified(null);
		try {
			setVerified(await api.post('/crawlers/verify'));
		} catch (e) {
			setError(e.message);
		} finally {
			setBusy(false);
		}
	};

	const entries = Object.entries(bots);
	const visible = (region) =>
		entries.filter(([id, b]) => {
			if ((b.region || 'us') !== region) return false;
			if (vendor && b.vendor !== vendor) return false;
			if (view === 'seen') return (hits[id]?.hits || 0) > 0;
			if (view === 'blocked') return blocked.has(id);
			if (view === 'ignoring') return hits[id]?.ignoring;
			return true;
		});

	// One tile per operator, the big four first, then by how many crawlers
	// they run — so the strip reads "OpenAI, Anthropic, Google, Perplexity…"
	// rather than in registry order.
	const LEAD = ['OpenAI', 'Anthropic', 'Google', 'Perplexity', 'Meta', 'Microsoft', 'Apple', 'Amazon'];
	const operators = (() => {
		const map = new Map();
		entries.forEach(([id, b]) => {
			const o = map.get(b.vendor) || { name: b.vendor, total: 0, allowed: 0, seen: 0, hits: 0 };
			o.total += 1;
			if (!blocked.has(id)) o.allowed += 1;
			if ((hits[id]?.hits || 0) > 0) {
				o.seen += 1;
				o.hits += hits[id].hits;
			}
			map.set(b.vendor, o);
		});
		return [...map.values()]
			.map((o) => ({ ...o, tone: o.allowed === 0 ? 'blocked' : o.seen > 0 ? 'seen' : 'quiet' }))
			.sort((a, b) => {
				const la = LEAD.indexOf(a.name);
				const lb = LEAD.indexOf(b.name);
				if (la !== -1 || lb !== -1) return (la === -1 ? 99 : la) - (lb === -1 ? 99 : lb);
				return b.total - a.total || a.name.localeCompare(b.name);
			})
			.slice(0, 8);
	})();

	// The state of the door, before the list of who may come through it.
	const botCount = Object.keys(data.bots || {}).length;
	const blockedCount = blocked.size;
	const verdict = !settings.enabled
		? {
				tone: 'warning',
				text: __('Crawler rules are switched off, so robots.txt says nothing about AI crawlers.', 'ai-rank-suite'),
				why: __('Without a rule every AI crawler reads the whole site by default. That is a decision worth making on purpose rather than by omission.', 'ai-rank-suite'),
			}
		: blockedCount === 0
			? {
					tone: 'good',
					text: sprintf(__('All %d known AI crawlers may read this site.', 'ai-rank-suite'), botCount),
					why: __('That is the right default for most sites: being read is how a page gets cited. Block one only when you have a reason.', 'ai-rank-suite'),
				}
			: {
					tone: 'neutral',
					text: sprintf(_n('%1$d of %2$d AI crawlers is blocked.', '%1$d of %2$d AI crawlers are blocked.', blockedCount, 'ai-rank-suite'), blockedCount, botCount),
					why: __('A blocked crawler cannot cite you. The log below shows whether it actually stopped — several ignore robots.txt.', 'ai-rank-suite'),
				};

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

			<ScreenVerdict {...verdict} />

			{/*
			 * The operators, by name, before anything else. "Is OpenAI reading
			 * my site?" is the question this screen is opened for, and the
			 * answer used to be thirty rows down. One tile per operator: how
			 * many of its crawlers may read the site, how many were seen, and a
			 * click narrows the list below to that operator.
			 */}
			<div className="ars-opstrip" role="list">
				{operators.map((op) => (
					<button
						key={op.name}
						type="button"
						role="listitem"
						className={`ars-optile is-${op.tone}${vendor === op.name ? ' is-active' : ''}`}
						onClick={() => {
							setVendor(vendor === op.name ? '' : op.name);
							requestAnimationFrame(() => listRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' }));
						}}
						title={
							settings.log
								? sprintf(__('%1$s: %2$d of %3$d crawlers allowed, %4$d seen here', 'ai-rank-suite'), op.name, op.allowed, op.total, op.seen)
								: sprintf(__('%1$s: %2$d of %3$d crawlers allowed', 'ai-rank-suite'), op.name, op.allowed, op.total)
						}
					>
						<span className="ars-optile-name">{op.name}</span>
						<span className="ars-optile-state">
							{op.allowed === op.total ? __('All allowed', 'ai-rank-suite') : op.allowed === 0 ? __('Blocked', 'ai-rank-suite') : sprintf(__('%1$d/%2$d allowed', 'ai-rank-suite'), op.allowed, op.total)}
						</span>
						<span className="ars-optile-seen">
							{settings.log ? (op.seen > 0 ? sprintf(_n('%s request', '%s requests', op.hits, 'ai-rank-suite'), op.hits.toLocaleString()) : __('not seen yet', 'ai-rank-suite')) : __('not recording', 'ai-rank-suite')}
						</span>
					</button>
				))}
			</div>

			{data.physical_robots_txt && (
				<Notice tone="warning">{__('A physical', 'ai-rank-suite')}{' '}<code>robots.txt</code>{' '}
					{__('file exists in the site root. WordPress cannot modify it, so nothing on this screen reaches the file crawlers actually read — delete it and let WordPress serve robots.txt, or copy these rules into it by hand.', 'ai-rank-suite')}
				</Notice>
			)}

			{traffic?.defiant > 0 && (
				<Notice tone="serious">
					{sprintf(_n('%d crawler you have disallowed is still fetching pages. robots.txt is a request, not a fence — turn on', '%d crawlers you have disallowed are still fetching pages. robots.txt is a request, not a fence — turn on', traffic.defiant, 'ai-rank-suite'), traffic.defiant)}{' '}
					<b>{__('Refuse blocked crawlers', 'ai-rank-suite')}</b> {__('below to answer them with a 403 instead.', 'ai-rank-suite')}
				</Notice>
			)}

			{/* With recording off there is no measurement, and a zero would claim
			    there was one — the em-dash is the honest value. */}
			<MetricGrid
				columns={4}
				onSelect={settings.log ? undefined : (id) => id === 'seen' && persist({ log: true })}
				metrics={[
					{
						id: 'seen',
						label: __('Crawlers seen', 'ai-rank-suite'),
						hint: __('Distinct AI crawlers that have fetched at least one page since recording began.', 'ai-rank-suite'),
						value: settings.log || (traffic?.seen || 0) > 0 ? traffic?.seen || 0 : '—',
						accent: settings.log ? undefined : 'var(--ars-accent)',
						sub: settings.log ? sprintf(__('of %d known', 'ai-rank-suite'), entries.length) : __('recording is off — click to turn it on', 'ai-rank-suite'),
					},
					{
						id: 'requests',
						label: __('Requests recorded', 'ai-rank-suite'),
						hint: __('Every crawler request that reached WordPress. A full-page cache serves most requests without PHP running, so this is a floor.', 'ai-rank-suite'),
						value: settings.log || (traffic?.total || 0) > 0 ? (traffic?.total || 0).toLocaleString() : '—',
						sub: __('since recording began', 'ai-rank-suite'),
					},
					{
						id: 'blocked',
						label: __('Disallowed', 'ai-rank-suite'),
						hint: __('Crawlers your rules tell to stay away. robots.txt is a request, not a fence — the last cell shows who ignores it.', 'ai-rank-suite'),
						value: blocked.size,
						sub: __('in robots.txt', 'ai-rank-suite'),
					},
					{
						id: 'defiant',
						label: __('Ignoring the rules', 'ai-rank-suite'),
						hint: __('Disallowed crawlers that kept fetching pages afterwards. For these, only the 403 has teeth.', 'ai-rank-suite'),
						value: traffic?.defiant || 0,
						accent: traffic?.defiant > 0 ? 'var(--ars-c-red-ink)' : undefined,
						sub: traffic?.defiant > 0 ? __('still arriving after being blocked', 'ai-rank-suite') : __('every blocked crawler stopped', 'ai-rank-suite'),
					},
				]}
			/>

			<Card
				icon={<IconShield size={16} />}
				title={__('Enforcement', 'ai-rank-suite')}
				subtitle={__('What happens to a crawler you have disallowed', 'ai-rank-suite')}
				action={
					<div className="flex items-center gap-2">
						<Button variant="ghost" busy={busy} onClick={verify}>
							<IconRefresh size={13} />{__('Check the live robots.txt', 'ai-rank-suite')}</Button>
						<a
							href={data.robots_url}
							target="_blank"
							rel="noreferrer"
							className="inline-flex items-center gap-1 text-[12px]"
						>{__('View', 'ai-rank-suite')}<IconExternal size={10} />
						</a>
					</div>
				}
			>
				<div className="grid gap-2">
					<Switch
						checked={!!settings.enabled}
						disabled={busy}
						onChange={(v) => persist({ enabled: v })}
						label={__('Write the rules into robots.txt', 'ai-rank-suite')}
					/>
					<Switch
						checked={!!settings.log}
						disabled={busy}
						onChange={(v) => persist({ log: v })}
						label={__('Record which crawlers arrive', 'ai-rank-suite')}
					/>
					<p className="m-0 ml-[48px] text-[12px]" style={{ color: 'var(--ars-ink-2)' }}>
						{__('One small database write per crawler request. That is a fraction of a percent of a normal site\'s traffic, and it is the only way to know whether any of this is working — but it is your decision, so it is off until you make it.', 'ai-rank-suite')}
					</p>
					<Switch
						checked={!!settings.enforce}
						disabled={busy}
						onChange={(v) => persist({ enforce: v })}
						label={__('Refuse blocked crawlers with a 403', 'ai-rank-suite')}
					/>
					<p className="m-0 ml-[48px] text-[12px]" style={{ color: 'var(--ars-ink-2)' }}>
						{__('robots.txt is a request. This is not — a disallowed crawler gets a 403 and no content. Use it for the ones the table below shows are ignoring you. A 403 rather than a 404 on purpose: a crawler told the page does not exist may drop the URL permanently.', 'ai-rank-suite')}
					</p>
				</div>

				{verified && !verified.ok && <Notice tone="serious">{verified.reason}</Notice>}
				{verified && verified.ok && (
					<div className="mt-3">
						{verified.missing.length === 0 ? (
							<Notice tone="good">
								{sprintf(__('All %d disallow rules are present in the robots.txt the site actually serves.', 'ai-rank-suite'), verified.present)}
							</Notice>
						) : (
							<Notice tone="serious">
								{sprintf(_n('%1$d rule you set is not in the served file: %2$s. A CDN cache or a security plugin rewriting robots.txt is the usual cause.', '%1$d rules you set are not in the served file: %2$s. A CDN cache or a security plugin rewriting robots.txt is the usual cause.', verified.missing.length, 'ai-rank-suite'), verified.missing.length, verified.missing.map((m) => m.agent).join(', '))}
							</Notice>
						)}
						<pre className="ars-head-pre is-tall mt-2">{verified.body}</pre>
					</div>
				)}
			</Card>

			{/*
			  * Agentic browsing as one decision. The on-demand fetchers —
			  * ChatGPT-User, Claude-User, Perplexity-User and friends — are
			  * what runs when a person asks an assistant to open this site
			  * for them. They are a different question from training
			  * crawlers, and most people want to answer it once, not per bot.
			  */}
			{(() => {
				const agentIds = Object.entries(bots).filter(([, b]) => b.purpose === 'on-demand').map(([id]) => id);
				const blockedAgents = agentIds.filter((id) => blocked.has(id));
				const allOn = blockedAgents.length === 0;
				return (
					<Card
						title={__('Agentic browsing', 'ai-rank-suite')}
						subtitle={__('When someone asks ChatGPT, Claude or Perplexity to open your site for them, these fetchers do the visit', 'ai-rank-suite')}
						action={
							<Switch
								checked={allOn}
								onChange={(v) => bulk((b) => b.purpose === 'on-demand', !v)}
								label={allOn ? __('Allowed', 'ai-rank-suite') : __('Blocked', 'ai-rank-suite')}
							/>
						}
					>
						<p className="m-0 text-[13px]" style={{ color: 'var(--ars-ink-2)' }}>
							{allOn
								? sprintf(__('All %d on-demand fetchers may visit. This is how an assistant reads your prices or opening hours when a customer asks — blocking them makes you invisible in exactly those moments.', 'ai-rank-suite'), agentIds.length)
								: sprintf(_n('%1$d of %2$d on-demand fetchers is blocked. An assistant asked about your site will answer from memory or from your competitors instead.', '%1$d of %2$d on-demand fetchers are blocked. An assistant asked about your site will answer from memory or from your competitors instead.', blockedAgents.length, 'ai-rank-suite'), blockedAgents.length, agentIds.length)}
						</p>
					</Card>
				);
			})()}

			<Card
				icon={<IconBot size={16} />}
				title={__('Who may use your content', 'ai-rank-suite')}
				subtitle={__('Grouped by where the operator is, because that is the decision people actually make', 'ai-rank-suite')}
				action={
					<div className="flex items-center gap-2 flex-wrap">
						<Button variant="ghost" onClick={() => bulk((b) => b.purpose === 'training', true)}>{__('Block all training', 'ai-rank-suite')}</Button>
						<Button variant="ghost" onClick={() => bulk((b) => b.purpose !== 'training', false)}>{__('Allow all search', 'ai-rank-suite')}</Button>
						{blocked.size > 0 && (
							<Button variant="ghost" onClick={() => persist({ blocked: [] })}>{__('Allow everything', 'ai-rank-suite')}</Button>
						)}
					</div>
				}
				flush
			>
				<div ref={listRef} className="p-3" style={{ borderBottom: '1px solid var(--ars-border)', scrollMarginTop: 12 }}>
					<ChipBar
						label={__('Show', 'ai-rank-suite')}
						value={view}
						onChange={setView}
						options={[
							{ id: 'all', label: vendor ? vendor : 'All', count: vendor ? entries.filter(([, b]) => b.vendor === vendor).length : entries.length },
							{ id: 'seen', label: __('Seen here', 'ai-rank-suite'), count: traffic?.seen || 0 },
							{ id: 'blocked', label: __('Disallowed', 'ai-rank-suite'), count: blocked.size },
							{ id: 'ignoring', label: __('Ignoring you', 'ai-rank-suite'), count: traffic?.defiant || 0 },
						]}
					/>
				</div>

				{REGIONS.map((region) => {
					const rows = visible(region.id);
					if (rows.length === 0) return null;
					return (
						<div key={region.id}>
							<div className="ars-crawl-region">
								<b>{region.label}</b>
								<span>{region.hint}</span>
							</div>
							{rows.map(([id, bot]) => {
								const t = hits[id] || {};
								const purpose = PURPOSE[bot.purpose] || PURPOSE.training;
								const isBlocked = blocked.has(id);
								return (
									<div key={id} className={`ars-crawl-row${t.ignoring ? ' is-defiant' : ''}`}>
										<div className="min-w-0">
											<div className="flex items-center gap-2 flex-wrap">
												<b className="text-[13px]">{bot.vendor}</b>
												<code className="ars-head-code">{bot.ua}</code>
												<span title={purpose.why}>
													<Badge tone={purpose.tone}>{purpose.label}</Badge>
												</span>
												{bot.note === 'unreliable' && (
													<span
														className="ars-tag"
														title={__('This vendor is widely reported to fetch without a named identifier, so the rule is a best effort.', 'ai-rank-suite')}
													>
														{__('identifier unreliable', 'ai-rank-suite')}
													</span>
												)}
												{t.ignoring && <Badge tone="serious">{__('ignoring robots.txt', 'ai-rank-suite')}</Badge>}
											</div>
											<div className="ars-crawl-meta">
												{t.hits > 0 ? (
													<>
														<b className="tabular-nums">{t.hits.toLocaleString()}</b> {_n('request', 'requests', t.hits, 'ai-rank-suite')}
														{t.recent > 0 && <> · {sprintf(__('%s in the last 7 days', 'ai-rank-suite'), t.recent.toLocaleString())}</>}
														{t.last > 0 && <> · {sprintf(__('last %s', 'ai-rank-suite'), new Date(t.last * 1000).toLocaleDateString())}</>}
														{t.blocked > 0 && <> · {sprintf(__('%s refused', 'ai-rank-suite'), t.blocked.toLocaleString())}</>}
													</>
												) : settings.log ? (
													__('never seen here', 'ai-rank-suite')
												) : (
													purpose.why
												)}
											</div>
										</div>

										{t.series && t.hits > 0 && (
											<div className="ars-crawl-spark">
												<Sparkline data={t.series} height={26} color="var(--ars-brand)" />
											</div>
										)}

										<div className="ars-crawl-switch">
											<span className="t-caption">{isBlocked ? __('Blocked', 'ai-rank-suite') : __('Allowed', 'ai-rank-suite')}</span>
											<Switch checked={isBlocked} disabled={busy} onChange={() => toggleBot(id)} />
										</div>
									</div>
								);
							})}
						</div>
					);
				})}
			</Card>

			<Explain title={__('What blocking actually costs you', 'ai-rank-suite')}>
				<p>
					{__('The three purposes are not interchangeable, and the switch means something different for each. A', 'ai-rank-suite')}{' '}
					<b>{__('training', 'ai-rank-suite')}</b> {__('crawler reads your pages to train a model; blocking it costs you nothing you would ever have received. A', 'ai-rank-suite')}{' '}
					<b>{__('search index', 'ai-rank-suite')}</b> {__('crawler builds the index the assistant answers from — block that and you cannot be cited at all, which is the opposite of what this plugin is for. An', 'ai-rank-suite')}{' '}
					<b>{__('on-demand', 'ai-rank-suite')}</b> {__('fetch happens because a user asked about your page a second ago; blocking it is blocking a visitor.', 'ai-rank-suite')}
				</p>
				<p>
					{__('Blocking every AI crawler is a legitimate position, and it is the position that guarantees you are never in an AI answer. Most sites want the middle: no training, yes search, yes on demand — which is exactly what the two bulk buttons above do.', 'ai-rank-suite')}
				</p>
				<p>
					{__('robots.txt is honoured voluntarily. The table shows which crawlers keep arriving after being told not to; for those, the 403 is the only thing that has teeth. Note that a full-page cache serves most requests without PHP running, so both the recording and the 403 only see what reaches WordPress.', 'ai-rank-suite')}
				</p>
			</Explain>
		</div>
	);
}
