<?php
/**
 * AI crawler access analyzer.
 *
 * Checks whether AI bots (GPTBot, ClaudeBot, PerplexityBot, etc.)
 * are allowed to crawl the site via robots.txt.
 *
 * @package MonitorCat\Modules\Seo\Analyzer
 */

declare(strict_types=1);

namespace MonitorCat\Modules\Seo\Analyzer;

defined( 'ABSPATH' ) || exit;

/**
 * Analyzes robots.txt for AI crawler access.
 */
final class AiCrawlerAnalyzer {

	/**
	 * Run the AI crawler access analysis.
	 *
	 * @param int $post_id The post ID (unused — this is a site-wide check).
	 * @return array{score: int, status: string, checks: array<int, array{label: string, status: string, message: string, value: string|null}>}
	 */
	public function analyze( int $post_id ): array { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found -- shared analyzer interface.
		$robots_body = $this->fetch_robots_txt();
		$checks      = array();

		$checks[] = $this->check_bot_access( $robots_body, 'GPTBot', __( 'GPTBot Access (ChatGPT)', 'monitor-cat' ) );
		$checks[] = $this->check_bot_access( $robots_body, 'ClaudeBot', __( 'ClaudeBot Access (Claude)', 'monitor-cat' ) );
		$checks[] = $this->check_other_ai_bots( $robots_body );
		$checks[] = $this->check_crawl_delay( $robots_body );

		$score  = $this->calculate_score( $checks );
		$status = $this->determine_status( $score );

		return array(
			'score'  => $score,
			'status' => $status,
			'checks' => $checks,
		);
	}

	/**
	 * Fetch robots.txt content.
	 *
	 * @return string The robots.txt body, or empty string on failure.
	 */
	private function fetch_robots_txt(): string {
		$robots_url = site_url( '/robots.txt' );
		$response   = wp_remote_get(
			$robots_url,
			array(
				'timeout'   => 5,
				'sslverify' => false,
			)
		);

		if ( is_wp_error( $response ) ) {
			return '';
		}

		$status_code = wp_remote_retrieve_response_code( $response );
		if ( 200 !== $status_code ) {
			return '';
		}

		return wp_remote_retrieve_body( $response );
	}

	/**
	 * Check if a specific AI bot is blocked in robots.txt.
	 *
	 * @param string $robots_body The robots.txt content.
	 * @param string $bot_name    The user-agent name to check.
	 * @param string $label       The check label.
	 * @return array{label: string, status: string, message: string, value: string|null}
	 */
	private function check_bot_access( string $robots_body, string $bot_name, string $label ): array {
		if ( empty( $robots_body ) ) {
			return array(
				'label'   => $label,
				'status'  => 'pass',
				'message' => __( 'No robots.txt restrictions found — bot has access.', 'monitor-cat' ),
				'value'   => __( 'Allowed', 'monitor-cat' ),
			);
		}

		if ( $this->is_bot_blocked( $robots_body, $bot_name ) ) {
			return array(
				'label'   => $label,
				'status'  => 'fail',
				/* translators: %s: bot name */
				'message' => sprintf( __( '%s is blocked in robots.txt. This prevents your content from appearing in AI-generated answers.', 'monitor-cat' ), $bot_name ),
				'value'   => __( 'Blocked', 'monitor-cat' ),
			);
		}

		return array(
			'label'   => $label,
			'status'  => 'pass',
			/* translators: %s: bot name */
			'message' => sprintf( __( '%s is not blocked — your content can appear in AI-generated responses.', 'monitor-cat' ), $bot_name ),
			'value'   => __( 'Allowed', 'monitor-cat' ),
		);
	}

	/**
	 * Check other AI bots beyond GPTBot and ClaudeBot.
	 *
	 * Covers: PerplexityBot, ChatGPT-User, Google-Extended,
	 * Meta-ExternalAgent, Amazonbot, Bytespider, cohere-ai.
	 *
	 * @param string $robots_body The robots.txt content.
	 * @return array{label: string, status: string, message: string, value: string|null}
	 */
	private function check_other_ai_bots( string $robots_body ): array {
		$bots = array(
			'PerplexityBot',
			'ChatGPT-User',
			'Google-Extended',
			'Meta-ExternalAgent',
			'Amazonbot',
			'Bytespider',
			'cohere-ai',
		);

		$blocked = array();
		$allowed = array();
		foreach ( $bots as $bot ) {
			if ( $this->is_bot_blocked( $robots_body, $bot ) ) {
				$blocked[] = $bot;
			} else {
				$allowed[] = $bot;
			}
		}

		if ( empty( $blocked ) ) {
			return array(
				'label'   => __( 'Other AI Bots', 'monitor-cat' ),
				'status'  => 'pass',
				/* translators: %d: number of bots */
				'message' => sprintf( __( 'All %d additional AI bots have access to your content.', 'monitor-cat' ), count( $bots ) ),
				'value'   => __( 'All allowed', 'monitor-cat' ),
			);
		}

		if ( count( $blocked ) < count( $bots ) ) {
			return array(
				'label'   => __( 'Other AI Bots', 'monitor-cat' ),
				'status'  => 'warning',
				/* translators: %s: comma-separated list of blocked bots */
				'message' => sprintf( __( 'Some AI bots are blocked: %s. Consider allowing them for wider AI visibility.', 'monitor-cat' ), implode( ', ', $blocked ) ),
				'value'   => sprintf(
					/* translators: %1$d: blocked count, %2$d: total count */
					__( '%1$d of %2$d blocked', 'monitor-cat' ),
					count( $blocked ),
					count( $bots )
				),
			);
		}

		return array(
			'label'   => __( 'Other AI Bots', 'monitor-cat' ),
			'status'  => 'fail',
			'message' => __( 'All additional AI bots are blocked. Your content will not appear in most AI-generated responses.', 'monitor-cat' ),
			'value'   => __( 'All blocked', 'monitor-cat' ),
		);
	}

	/**
	 * Check for excessive crawl delay.
	 *
	 * @param string $robots_body The robots.txt content.
	 * @return array{label: string, status: string, message: string, value: string|null}
	 */
	private function check_crawl_delay( string $robots_body ): array {
		if ( empty( $robots_body ) ) {
			return array(
				'label'   => __( 'Crawl Delay', 'monitor-cat' ),
				'status'  => 'pass',
				'message' => __( 'No crawl delay configured.', 'monitor-cat' ),
				'value'   => null,
			);
		}

		if ( preg_match( '/^Crawl-delay:\s*(\d+)/im', $robots_body, $match ) ) {
			$delay = (int) $match[1];

			if ( $delay > 10 ) {
				return array(
					'label'   => __( 'Crawl Delay', 'monitor-cat' ),
					'status'  => 'warning',
					/* translators: %d: crawl delay in seconds */
					'message' => sprintf( __( 'Crawl delay is set to %d seconds. High delays may reduce AI crawler coverage.', 'monitor-cat' ), $delay ),
					'value'   => (string) $delay . 's',
				);
			}
		}

		return array(
			'label'   => __( 'Crawl Delay', 'monitor-cat' ),
			'status'  => 'pass',
			'message' => __( 'No excessive crawl delay found.', 'monitor-cat' ),
			'value'   => null,
		);
	}

	/**
	 * Parse robots.txt to check if a specific bot is blocked.
	 *
	 * Handles both bot-specific blocks and wildcard blocks.
	 * A bot-specific section takes precedence over the wildcard section.
	 *
	 * @param string $robots_body The robots.txt content.
	 * @param string $bot_name    The user-agent name.
	 * @return bool True if the bot is blocked.
	 */
	private function is_bot_blocked( string $robots_body, string $bot_name ): bool {
		$lines = preg_split( '/\r?\n/', $robots_body );

		if ( ! is_array( $lines ) ) {
			return false;
		}

		// Two-pass approach: first check for bot-specific section,
		// then fall back to wildcard if no specific section exists.
		$has_specific_section = false;
		$blocked_in_specific  = false;
		$blocked_in_wildcard  = false;
		$current_agents       = array();

		foreach ( $lines as $line ) {
			$line = trim( $line );

			// Skip comments.
			if ( '' !== $line && '#' === $line[0] ) {
				continue;
			}

			// Empty lines separate sections — reset the agent group.
			if ( '' === $line ) {
				$current_agents = array();
				continue;
			}

			// Detect user-agent directives (they can be grouped).
			if ( preg_match( '/^User-agent:\s*(.+)$/i', $line, $ua_match ) ) {
				$agent            = trim( $ua_match[1] );
				$current_agents[] = $agent;
				continue;
			}

			// Check disallow rules.
			if ( preg_match( '/^Disallow:\s*\/\s*$/i', $line ) ) {
				$is_for_bot      = false;
				$is_for_wildcard = false;

				foreach ( $current_agents as $agent ) {
					if ( 0 === strcasecmp( $agent, $bot_name ) ) {
						$is_for_bot = true;
					}
					if ( '*' === $agent ) {
						$is_for_wildcard = true;
					}
				}

				if ( $is_for_bot ) {
					$has_specific_section = true;
					$blocked_in_specific  = true;
				}
				if ( $is_for_wildcard ) {
					$blocked_in_wildcard = true;
				}
			}

			// Check allow rules (override disallow for the same section).
			if ( preg_match( '/^Allow:\s*\/\s*$/i', $line ) ) {
				foreach ( $current_agents as $agent ) {
					if ( 0 === strcasecmp( $agent, $bot_name ) ) {
						$has_specific_section = true;
						$blocked_in_specific  = false;
					}
				}
			}

			// Non-directive line resets the current agent group.
			if ( ! preg_match( '/^(User-agent|Disallow|Allow|Crawl-delay|Sitemap):/i', $line ) ) {
				$current_agents = array();
			}
		}

		// Bot-specific section takes precedence over wildcard.
		if ( $has_specific_section ) {
			return $blocked_in_specific;
		}

		return $blocked_in_wildcard;
	}

	/**
	 * Calculate weighted score from checks.
	 *
	 * @param array<int, array{label: string, status: string, message: string, value: string|null}> $checks The checks array.
	 * @return int Score from 0 to 100.
	 */
	private function calculate_score( array $checks ): int {
		if ( empty( $checks ) ) {
			return 0;
		}

		$weights = array( 30, 25, 25, 20 );
		$total   = 0;

		foreach ( $checks as $index => $check ) {
			$weight     = $weights[ $index ] ?? 20;
			$multiplier = 'pass' === $check['status'] ? 1.0 : ( 'warning' === $check['status'] ? 0.5 : 0.0 );
			$total     += $weight * $multiplier;
		}

		return (int) round( $total );
	}

	/**
	 * Determine overall status from score.
	 *
	 * @param int $score The calculated score.
	 * @return string 'pass', 'warning', or 'fail'.
	 */
	private function determine_status( int $score ): string {
		if ( $score >= 80 ) {
			return 'pass';
		}

		if ( $score >= 50 ) {
			return 'warning';
		}

		return 'fail';
	}
}
