<?php
/**
 * Livada SEO — AI Bots Access Control
 *
 * Manages AI crawler access at the WordPress level:
 *   1. Registry of 35+ known AI/LLM bots with category metadata.
 *   2. Per-bot allow/disallow setting (stored under `ai_bots` in settings).
 *   3. Robots.txt enrichment — appends explicit User-agent blocks so the
 *      directive is unambiguous even when an upstream layer ignores robots
 *      heuristics.
 *   4. Live diagnostic — curl-tests each bot's user-agent against the home URL
 *      and returns the HTTP status, so the admin can see which bots are
 *      actually blocked at the edge (Cloudflare, host WAF) regardless of what
 *      WordPress says.
 *
 * Differentiator: no other WP SEO plugin gives end clients direct control over
 * AI bot access from inside WordPress. This is the "And by AI." promise made
 * configurable.
 *
 * @package Livada_SEO
 * @since   1.12.0
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

class Livada_SEO_AI_Bots {

	/**
	 * Registry of supported bots.
	 *
	 * Each entry:
	 *   - slug:    User-Agent token used in robots.txt and HTTP headers.
	 *   - name:    Human label shown in admin UI.
	 *   - owner:   Operator (Anthropic, OpenAI, Google, etc.).
	 *   - category: 'search' | 'citation' | 'training' | 'archive' | 'seo_scraper' | 'opaque'.
	 *   - default: bool — true = allowed by default, false = blocked by default.
	 *   - ua:      Full User-Agent string used for live diagnostic curl test.
	 *
	 * @return array<string, array{slug:string,name:string,owner:string,category:string,default:bool,ua:string}>
	 */
	public static function registry(): array {
		return [
			// ── Search engines (always allowed — SEO classique) ──────────────
			'Googlebot' => [
				'slug' => 'Googlebot', 'name' => 'Googlebot', 'owner' => 'Google',
				'category' => 'search', 'default' => true,
				'ua' => 'Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)',
			],
			'Bingbot' => [
				'slug' => 'bingbot', 'name' => 'Bingbot', 'owner' => 'Microsoft',
				'category' => 'search', 'default' => true,
				'ua' => 'Mozilla/5.0 (compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm)',
			],
			'Applebot' => [
				'slug' => 'Applebot', 'name' => 'Applebot', 'owner' => 'Apple',
				'category' => 'search', 'default' => true,
				'ua' => 'Mozilla/5.0 (compatible; Applebot/0.1; +http://www.apple.com/go/applebot)',
			],
			'DuckDuckBot' => [
				'slug' => 'DuckDuckBot', 'name' => 'DuckDuckBot', 'owner' => 'DuckDuckGo',
				'category' => 'search', 'default' => true,
				'ua' => 'DuckDuckBot/1.0; (+http://duckduckgo.com/duckduckbot.html)',
			],

			// ── AI citation bots (live answers, cite the source URL) ─────────
			'OAI-SearchBot' => [
				'slug' => 'OAI-SearchBot', 'name' => 'OAI-SearchBot', 'owner' => 'OpenAI',
				'category' => 'citation', 'default' => true,
				'ua' => 'Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; OAI-SearchBot/1.0; +https://openai.com/searchbot)',
			],
			'ChatGPT-User' => [
				'slug' => 'ChatGPT-User', 'name' => 'ChatGPT-User', 'owner' => 'OpenAI',
				'category' => 'citation', 'default' => true,
				'ua' => 'Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; ChatGPT-User/1.0; +https://openai.com/bot)',
			],
			'PerplexityBot' => [
				'slug' => 'PerplexityBot', 'name' => 'PerplexityBot', 'owner' => 'Perplexity',
				'category' => 'citation', 'default' => true,
				'ua' => 'Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; PerplexityBot/1.0; +https://perplexity.ai/perplexitybot)',
			],
			'Perplexity-User' => [
				'slug' => 'Perplexity-User', 'name' => 'Perplexity-User', 'owner' => 'Perplexity',
				'category' => 'citation', 'default' => true,
				'ua' => 'Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; Perplexity-User/1.0; +https://perplexity.ai/perplexity-user)',
			],
			'ClaudeBot' => [
				'slug' => 'ClaudeBot', 'name' => 'ClaudeBot', 'owner' => 'Anthropic',
				'category' => 'citation', 'default' => true,
				'ua' => 'Mozilla/5.0 (compatible; ClaudeBot/1.0; +claudebot@anthropic.com)',
			],
			'Claude-SearchBot' => [
				'slug' => 'Claude-SearchBot', 'name' => 'Claude-SearchBot', 'owner' => 'Anthropic',
				'category' => 'citation', 'default' => true,
				'ua' => 'Mozilla/5.0 (compatible; Claude-SearchBot/1.0; +https://www.anthropic.com)',
			],
			'Claude-User' => [
				'slug' => 'Claude-User', 'name' => 'Claude-User', 'owner' => 'Anthropic',
				'category' => 'citation', 'default' => true,
				'ua' => 'Mozilla/5.0 (compatible; Claude-User/1.0; +https://www.anthropic.com)',
			],
			'MistralAI-User' => [
				'slug' => 'MistralAI-User', 'name' => 'MistralAI-User', 'owner' => 'Mistral',
				'category' => 'citation', 'default' => true,
				'ua' => 'Mozilla/5.0 (compatible; MistralAI-User/1.0)',
			],
			'DuckAssistBot' => [
				'slug' => 'DuckAssistBot', 'name' => 'DuckAssistBot', 'owner' => 'DuckDuckGo',
				'category' => 'citation', 'default' => true,
				'ua' => 'Mozilla/5.0 (compatible; DuckAssistBot/1.0)',
			],
			'Meta-ExternalFetcher' => [
				'slug' => 'meta-externalfetcher', 'name' => 'Meta-ExternalFetcher', 'owner' => 'Meta',
				'category' => 'citation', 'default' => true,
				'ua' => 'Mozilla/5.0 (compatible; meta-externalfetcher/1.1)',
			],

			// ── AI training bots (feed the model's memory) ───────────────────
			'GPTBot' => [
				'slug' => 'GPTBot', 'name' => 'GPTBot', 'owner' => 'OpenAI',
				'category' => 'training', 'default' => true,
				'ua' => 'Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko); compatible; GPTBot/1.2; +https://openai.com/gptbot',
			],
			'CCBot' => [
				'slug' => 'CCBot', 'name' => 'CCBot (Common Crawl)', 'owner' => 'Common Crawl',
				'category' => 'training', 'default' => true,
				'ua' => 'CCBot/2.0 (https://commoncrawl.org/faq/)',
			],
			'Google-Extended' => [
				'slug' => 'Google-Extended', 'name' => 'Google-Extended', 'owner' => 'Google',
				'category' => 'training', 'default' => true,
				'ua' => 'Mozilla/5.0 (compatible; Google-Extended)',
			],
			'anthropic-ai' => [
				'slug' => 'anthropic-ai', 'name' => 'anthropic-ai (legacy)', 'owner' => 'Anthropic',
				'category' => 'training', 'default' => true,
				'ua' => 'anthropic-ai/1.0',
			],
			'Amazonbot' => [
				'slug' => 'Amazonbot', 'name' => 'Amazonbot', 'owner' => 'Amazon',
				'category' => 'training', 'default' => true,
				'ua' => 'Mozilla/5.0 (compatible; Amazonbot/0.1; +https://developer.amazon.com/amazonbot)',
			],
			'Bytespider' => [
				'slug' => 'Bytespider', 'name' => 'Bytespider (TikTok)', 'owner' => 'ByteDance',
				'category' => 'training', 'default' => true,
				'ua' => 'Mozilla/5.0 (compatible; Bytespider; spider-feedback@bytedance.com)',
			],
			'FacebookBot' => [
				'slug' => 'FacebookBot', 'name' => 'FacebookBot', 'owner' => 'Meta',
				'category' => 'training', 'default' => true,
				'ua' => 'facebookexternalhit/1.1',
			],
			'Meta-ExternalAgent' => [
				'slug' => 'meta-externalagent', 'name' => 'Meta-ExternalAgent', 'owner' => 'Meta',
				'category' => 'training', 'default' => true,
				'ua' => 'meta-externalagent/1.1',
			],
			'Google-CloudVertexBot' => [
				'slug' => 'Google-CloudVertexBot', 'name' => 'Google-CloudVertexBot', 'owner' => 'Google',
				'category' => 'training', 'default' => true,
				'ua' => 'Mozilla/5.0 (compatible; Google-CloudVertexBot)',
			],
			'cohere-ai' => [
				'slug' => 'cohere-ai', 'name' => 'cohere-ai', 'owner' => 'Cohere',
				'category' => 'training', 'default' => true,
				'ua' => 'cohere-ai/1.0',
			],

			// ── Archive bots (Wayback, etc.) ──────────────────────────────────
			'ia_archiver' => [
				'slug' => 'ia_archiver', 'name' => 'archive.org_bot', 'owner' => 'Internet Archive',
				'category' => 'archive', 'default' => true,
				'ua' => 'Mozilla/5.0 (compatible; archive.org_bot +http://archive.org/details/archive.org_bot)',
			],

			// ── SEO scrapers (default BLOCKED — competitors espionnent ton SEO) ──
			'SemrushBot' => [
				'slug' => 'SemrushBot', 'name' => 'SemrushBot', 'owner' => 'Semrush',
				'category' => 'seo_scraper', 'default' => false,
				'ua' => 'Mozilla/5.0 (compatible; SemrushBot/7~bl; +http://www.semrush.com/bot.html)',
			],
			'AhrefsBot' => [
				'slug' => 'AhrefsBot', 'name' => 'AhrefsBot', 'owner' => 'Ahrefs',
				'category' => 'seo_scraper', 'default' => false,
				'ua' => 'Mozilla/5.0 (compatible; AhrefsBot/7.0; +http://ahrefs.com/robot/)',
			],
			'MJ12bot' => [
				'slug' => 'MJ12bot', 'name' => 'MJ12bot (Majestic)', 'owner' => 'Majestic',
				'category' => 'seo_scraper', 'default' => false,
				'ua' => 'Mozilla/5.0 (compatible; MJ12bot/v1.4.8; http://mj12bot.com/)',
			],
			'DotBot' => [
				'slug' => 'DotBot', 'name' => 'DotBot (Moz)', 'owner' => 'Moz',
				'category' => 'seo_scraper', 'default' => false,
				'ua' => 'Mozilla/5.0 (compatible; DotBot/1.2; +https://opensiteexplorer.org/dotbot)',
			],
			'DataForSeoBot' => [
				'slug' => 'DataForSeoBot', 'name' => 'DataForSeoBot', 'owner' => 'DataForSEO',
				'category' => 'seo_scraper', 'default' => false,
				'ua' => 'Mozilla/5.0 (compatible; DataForSeoBot/1.0; +https://dataforseo.com/dataforseo-bot)',
			],
			'BLEXBot' => [
				'slug' => 'BLEXBot', 'name' => 'BLEXBot', 'owner' => 'WebMeUp',
				'category' => 'seo_scraper', 'default' => false,
				'ua' => 'Mozilla/5.0 (compatible; BLEXBot/1.0; +http://webmeup-crawler.com/)',
			],
			'PetalBot' => [
				'slug' => 'PetalBot', 'name' => 'PetalBot (Huawei)', 'owner' => 'Huawei',
				'category' => 'seo_scraper', 'default' => false,
				'ua' => 'Mozilla/5.0 (compatible; PetalBot; +https://webmaster.petalsearch.com/site/petalbot)',
			],

			// ── Opaque / scrapers déguisés (default BLOCKED) ─────────────────
			'Anchor-Browser' => [
				'slug' => 'Anchor', 'name' => 'Anchor Browser', 'owner' => 'Anchor',
				'category' => 'opaque', 'default' => false,
				'ua' => 'Mozilla/5.0 (compatible; AnchorBrowser/1.0)',
			],
		];
	}

	/**
	 * Category metadata shown in the admin UI.
	 *
	 * @return array<string, array{label:string,description:string,recommendation:string}>
	 */
	public static function categories(): array {
		return [
			'search' => [
				'label' => __( 'Search Engines', 'livada-seo' ),
				'description' => __( 'Classic search engine crawlers — never block these unless you know exactly what you are doing.', 'livada-seo' ),
				'recommendation' => __( 'Recommended: ALLOW', 'livada-seo' ),
			],
			'citation' => [
				'label' => __( 'AI Citation', 'livada-seo' ),
				'description' => __( 'AI assistants that crawl on-demand to cite your site in their answers. This is the core of "And by AI." visibility.', 'livada-seo' ),
				'recommendation' => __( 'Recommended: ALLOW', 'livada-seo' ),
			],
			'training' => [
				'label' => __( 'AI Training', 'livada-seo' ),
				'description' => __( 'Crawlers that feed the training corpus of large language models. Allowing them lets the model "know" your site from memory in future versions.', 'livada-seo' ),
				'recommendation' => __( 'Recommended: ALLOW (for visibility) or block to protect proprietary content', 'livada-seo' ),
			],
			'archive' => [
				'label' => __( 'Archives', 'livada-seo' ),
				'description' => __( 'Public archives like the Wayback Machine. Useful for brand history.', 'livada-seo' ),
				'recommendation' => __( 'Recommended: ALLOW', 'livada-seo' ),
			],
			'seo_scraper' => [
				'label' => __( 'SEO Scrapers (competitor tools)', 'livada-seo' ),
				'description' => __( 'Tools used by your competitors to spy on your SEO. They consume bandwidth without sending you any visitors.', 'livada-seo' ),
				'recommendation' => __( 'Recommended: BLOCK', 'livada-seo' ),
			],
			'opaque' => [
				'label' => __( 'Opaque scrapers', 'livada-seo' ),
				'description' => __( 'Bots with unclear identity, often used as a relay by data scrapers.', 'livada-seo' ),
				'recommendation' => __( 'Recommended: BLOCK', 'livada-seo' ),
			],
		];
	}

	/**
	 * Returns the per-bot allow map, merging defaults with stored settings.
	 *
	 * @return array<string, bool>
	 */
	public static function get_allow_map(): array {
		$saved = (array) Livada_SEO_Loader::get_setting( 'ai_bots', [] );

		$out = [];
		foreach ( self::registry() as $key => $bot ) {
			if ( array_key_exists( $key, $saved ) ) {
				$out[ $key ] = (bool) $saved[ $key ];
			} else {
				$out[ $key ] = (bool) $bot['default'];
			}
		}
		return $out;
	}

	/**
	 * Returns true when the given bot key is currently allowed.
	 */
	public static function is_allowed( string $key ): bool {
		$map = self::get_allow_map();
		return ! empty( $map[ $key ] );
	}

	/**
	 * Builds the robots.txt block that this module appends.
	 *
	 * Format (one block per bot):
	 *
	 *   # Livada SEO — AI bot access
	 *   User-agent: GPTBot
	 *   Allow: /
	 *
	 *   User-agent: SemrushBot
	 *   Disallow: /
	 */
	public static function build_robots_block(): string {
		$registry = self::registry();
		$allow    = self::get_allow_map();

		$lines = [ '', '# Livada SEO — AI bot access control' ];

		foreach ( $registry as $key => $bot ) {
			$lines[] = 'User-agent: ' . $bot['slug'];
			$lines[] = ! empty( $allow[ $key ] ) ? 'Allow: /' : 'Disallow: /';
			$lines[] = '';
		}

		return implode( "\n", $lines );
	}

	/**
	 * Live diagnostic — issues an HTTP GET against the home URL with the bot's
	 * user-agent and returns the status code so the admin can verify whether
	 * an upstream layer (Cloudflare, host WAF) blocks the bot regardless of
	 * what robots.txt says.
	 *
	 * Cached per bot for 60s to avoid hammering the origin when the admin
	 * panel is refreshed.
	 *
	 * @param string $key Registry key.
	 * @return array{code:int|null,error:string,cached:bool}
	 */
	public static function test_bot( string $key ): array {
		$registry = self::registry();
		if ( ! isset( $registry[ $key ] ) ) {
			return [ 'code' => null, 'error' => 'unknown_bot', 'cached' => false ];
		}

		$cache_key = 'livada_seo_ai_bot_test_' . md5( $key );
		$cached    = get_transient( $cache_key );
		if ( is_array( $cached ) ) {
			$cached['cached'] = true;
			return $cached;
		}

		$url = home_url( '/' );
		$response = wp_remote_get(
			$url,
			[
				'timeout'    => 8,
				'redirection' => 0,
				'sslverify'  => true,
				'user-agent' => $registry[ $key ]['ua'],
				'headers'    => [ 'Accept' => 'text/html,application/xhtml+xml' ],
			]
		);

		if ( is_wp_error( $response ) ) {
			$result = [ 'code' => null, 'error' => $response->get_error_message(), 'cached' => false ];
		} else {
			$result = [ 'code' => (int) wp_remote_retrieve_response_code( $response ), 'error' => '', 'cached' => false ];
		}

		set_transient( $cache_key, $result, 60 );
		return $result;
	}

	/**
	 * Clears the diagnostic cache for all bots.
	 */
	public static function flush_test_cache(): void {
		foreach ( array_keys( self::registry() ) as $key ) {
			delete_transient( 'livada_seo_ai_bot_test_' . md5( $key ) );
		}
	}

	/**
	 * Hard block: returns true when the current request's user-agent matches a
	 * bot that the operator has chosen to block. This is what turns the
	 * robots.txt Disallow directive into an actual server-side block.
	 *
	 * Strategy:
	 *   - Read User-Agent from the request.
	 *   - Compare (case-insensitive) against every registry slug that is
	 *     currently set to NOT allowed.
	 *   - Return true on the first match.
	 *
	 * Performance: this runs on every public request, so we short-circuit
	 * aggressively. We skip admin requests, REST API requests, AJAX requests,
	 * WP-Cron and CLI contexts — bots never use those.
	 *
	 * @return string Slug of the matching blocked bot, or empty string.
	 */
	public static function detect_blocked_request(): string {
		if ( is_admin() || ( defined( 'WP_CLI' ) && WP_CLI ) || ( defined( 'DOING_CRON' ) && DOING_CRON ) || ( defined( 'DOING_AJAX' ) && DOING_AJAX ) || ( defined( 'REST_REQUEST' ) && REST_REQUEST ) ) {
			return '';
		}

		$ua = isset( $_SERVER['HTTP_USER_AGENT'] ) ? (string) wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) : ''; // phpcs:ignore
		if ( '' === $ua ) {
			return '';
		}

		$ua_lower = strtolower( $ua );
		$allow    = self::get_allow_map();
		$registry = self::registry();

		foreach ( $registry as $key => $bot ) {
			if ( ! empty( $allow[ $key ] ) ) {
				continue; // bot is allowed → don't block
			}
			$slug_lower = strtolower( $bot['slug'] );
			if ( '' === $slug_lower ) {
				continue;
			}
			if ( false !== strpos( $ua_lower, $slug_lower ) ) {
				return $key;
			}
		}

		return '';
	}

	/**
	 * Hook: filters every public request at the earliest safe point and
	 * returns a 403 Forbidden when the user-agent belongs to a bot that has
	 * been blocked in the AI Bots Access settings.
	 *
	 * Wired to `parse_request` (priority 1) so the block kicks in BEFORE
	 * WordPress loads the heavy parts of the request (theme, content, etc.).
	 */
	public static function enforce_hard_block(): void {
		$bot = self::detect_blocked_request();
		if ( '' === $bot ) {
			return;
		}

		// Plain text 403 — fast, no theme, no DB hit, no rendering cost.
		status_header( 403 );
		nocache_headers();
		header( 'Content-Type: text/plain; charset=UTF-8' );
		header( 'X-Robots-Tag: noindex, nofollow' );
		echo "403 Forbidden\n\nAccess denied by Livada SEO AI Bots access policy.\n";
		exit;
	}

	// ── Bootstrap ─────────────────────────────────────────────────────────────

	/**
	 * Registers AJAX handlers and (optionally) the hard-block hook.
	 *
	 * The hard block is opt-in via the `ai_bots_hard_block` setting (default
	 * ON). When OFF, only the robots.txt directive is published — well-behaved
	 * bots will obey, but scrapers can still reach the site.
	 */
	public static function init(): void {
		add_action( 'wp_ajax_livada_seo_test_ai_bot',  [ __CLASS__, 'ajax_test_bot' ] );
		add_action( 'wp_ajax_livada_seo_test_ai_bots', [ __CLASS__, 'ajax_test_all' ] );

		$hard_block = (bool) Livada_SEO_Loader::get_setting( 'ai_bots_hard_block', true );
		if ( $hard_block ) {
			// parse_request fires on every public hit, before the heavy lifting.
			// Priority 1 so we run before other plugins.
			add_action( 'parse_request', [ __CLASS__, 'enforce_hard_block' ], 1 );
		}
	}

	/**
	 * AJAX: test a single bot. Returns JSON `{ code, error, cached }`.
	 */
	public static function ajax_test_bot(): void {
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( [ 'message' => 'forbidden' ], 403 );
		}
		check_ajax_referer( 'livada_seo_admin_nonce', 'nonce' );

		$key = isset( $_POST['bot'] ) ? sanitize_text_field( wp_unslash( $_POST['bot'] ) ) : '';
		if ( '' === $key ) {
			wp_send_json_error( [ 'message' => 'missing_bot' ], 400 );
		}

		wp_send_json_success( self::test_bot( $key ) );
	}

	/**
	 * AJAX: test all bots. Heavier — runs sequentially up to ~30 HTTP calls.
	 * Returns a map of `bot_key => { code, error }`.
	 */
	public static function ajax_test_all(): void {
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( [ 'message' => 'forbidden' ], 403 );
		}
		check_ajax_referer( 'livada_seo_admin_nonce', 'nonce' );

		// Force-refresh: clear cache so the admin gets fresh results.
		self::flush_test_cache();

		$results = [];
		foreach ( array_keys( self::registry() ) as $key ) {
			$results[ $key ] = self::test_bot( $key );
		}

		wp_send_json_success( $results );
	}
}
