<?php
/**
 * Site-level checks: robots.txt and the XML sitemap.
 *
 * Both fetch only this site's own URLs.
 *
 * @package SEO_Flight_Recorder
 */

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

/**
 * Class SFRec_Sitewide
 */
class SFRec_Sitewide {

	/**
	 * Run both checks. Change rows land at page_id = 0.
	 */
	public static function run_site_checks() {
		self::check_robots();
		self::check_sitemap();
	}

	/**
	 * robots.txt: reachability, content drift, blanket disallow.
	 */
	private static function check_robots() {
		global $wpdb;

		$state  = get_option( 'sfrec_state', array() );
		$result = SFRec_Fetcher::fetch( home_url( '/robots.txt' ) );

		if ( is_wp_error( $result ) || (int) $result['status'] >= 500 ) {
			SFRec_Scanner::insert_change( 0, 'robots_txt', __( 'reachable', 'buildsbyluke-seo-flight-recorder' ), __( 'unreachable (5xx or network error)', 'buildsbyluke-seo-flight-recorder' ), 'warning', 'site_change', array() );
			return;
		}

		$body = (string) $result['body'];
		$hash = md5( $body );

		// Blanket disallow for all agents = de-facto delisting request.
		$blanket = self::has_blanket_disallow( $body );
		$open_id = (int) $wpdb->get_var( "SELECT id FROM {$wpdb->prefix}sfrec_changes WHERE page_id = 0 AND field = 'robots_blocking' AND status IN ('new','seen') LIMIT 1" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- own table.
		if ( $blanket && 0 === $open_id ) {
			SFRec_Scanner::insert_change( 0, 'robots_blocking', __( 'crawling allowed', 'buildsbyluke-seo-flight-recorder' ), __( 'robots.txt now blocks ALL crawlers (User-agent: * / Disallow: /)', 'buildsbyluke-seo-flight-recorder' ), 'critical', 'site_change', array() );
		} elseif ( ! $blanket && $open_id > 0 ) {
			$wpdb->update( $wpdb->prefix . 'sfrec_changes', array( 'status' => 'resolved' ), array( 'id' => $open_id ), array( '%s' ), array( '%d' ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- own table.
		}

		// Content drift (non-blocking edits are still worth a line in the log).
		if ( '' !== (string) ( isset( $state['robots_hash'] ) ? $state['robots_hash'] : '' ) && $state['robots_hash'] !== $hash && ! $blanket ) {
			SFRec_Scanner::insert_change(
				0,
				'robots_txt',
				isset( $state['robots_excerpt'] ) ? (string) $state['robots_excerpt'] : '',
				SFRec_Parser::clean_text( substr( $body, 0, 400 ) ),
				'warning',
				'site_change',
				array()
			);
		}

		$state['robots_hash']    = $hash;
		$state['robots_excerpt'] = SFRec_Parser::clean_text( substr( $body, 0, 400 ) );
		update_option( 'sfrec_state', $state, false );
	}

	/**
	 * Sitemap: discover, verify, watch the URL count.
	 */
	private static function check_sitemap() {
		global $wpdb;

		$state       = get_option( 'sfrec_state', array() );
		$sitemap_url = isset( $state['sitemap_url'] ) ? (string) $state['sitemap_url'] : '';

		if ( '' === $sitemap_url ) {
			$sitemap_url = self::discover_sitemap();
			if ( '' === $sitemap_url ) {
				return; // No sitemap is not a finding — many small sites run without one.
			}
		}

		$result  = SFRec_Fetcher::fetch( $sitemap_url );
		$open_id = (int) $wpdb->get_var( "SELECT id FROM {$wpdb->prefix}sfrec_changes WHERE page_id = 0 AND field = 'sitemap' AND status IN ('new','seen') LIMIT 1" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- own table.

		$count = -1;
		if ( ! is_wp_error( $result ) && 200 === (int) $result['status'] ) {
			$count = self::count_sitemap_entries( (string) $result['body'] );
		}

		if ( is_wp_error( $result ) || (int) $result['status'] >= 400 || -1 === $count ) {
			if ( 0 === $open_id ) {
				$after = is_wp_error( $result )
					? $result->get_error_message()
					: ( -1 === $count ? __( 'response is not valid sitemap XML', 'buildsbyluke-seo-flight-recorder' ) : sprintf( 'HTTP %d', (int) $result['status'] ) );
				SFRec_Scanner::insert_change( 0, 'sitemap', $sitemap_url, SFRec_Parser::clean_text( $after ), 'critical', 'site_change', array() );
			}
			return;
		}

		if ( $open_id > 0 ) {
			$wpdb->update( $wpdb->prefix . 'sfrec_changes', array( 'status' => 'resolved' ), array( 'id' => $open_id ), array( '%s' ), array( '%d' ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- own table.
		}

		$previous_count = isset( $state['sitemap_count'] ) ? (int) $state['sitemap_count'] : 0;
		if ( $previous_count > 10 && $count < $previous_count * 0.8 ) {
			SFRec_Scanner::insert_change(
				0,
				'sitemap_count',
				sprintf( /* translators: %d: number of URLs. */ __( '%d URLs', 'buildsbyluke-seo-flight-recorder' ), $previous_count ),
				sprintf( /* translators: %d: number of URLs. */ __( '%d URLs', 'buildsbyluke-seo-flight-recorder' ), $count ),
				'warning',
				'site_change',
				array()
			);
		}

		$state['sitemap_url']   = $sitemap_url;
		$state['sitemap_count'] = $count;
		update_option( 'sfrec_state', $state, false );
	}

	/**
	 * Find the sitemap: robots.txt "Sitemap:" line first, then the usual paths.
	 *
	 * @return string URL or ''.
	 */
	public static function discover_sitemap() {
		$robots = SFRec_Fetcher::fetch( home_url( '/robots.txt' ) );
		if ( ! is_wp_error( $robots ) && 200 === (int) $robots['status'] ) {
			if ( preg_match( '/^\s*sitemap:\s*(\S+)/im', (string) $robots['body'], $m ) ) {
				$candidate = esc_url_raw( trim( $m[1] ) );
				if ( '' !== $candidate ) {
					return $candidate;
				}
			}
		}

		foreach ( array( '/sitemap_index.xml', '/sitemap.xml', '/wp-sitemap.xml' ) as $path ) {
			$probe = SFRec_Fetcher::fetch( home_url( $path ) );
			if ( ! is_wp_error( $probe ) && 200 === (int) $probe['status'] && -1 !== self::count_sitemap_entries( (string) $probe['body'] ) ) {
				return home_url( $path );
			}
		}

		return '';
	}

	/**
	 * Count <url>/<sitemap> entries. -1 when the body is not sitemap XML.
	 *
	 * @param string $xml Body.
	 * @return int
	 */
	public static function count_sitemap_entries( $xml ) {
		if ( '' === trim( $xml ) ) {
			return -1;
		}
		$previous = libxml_use_internal_errors( true );
		$doc      = simplexml_load_string( $xml );
		libxml_clear_errors();
		libxml_use_internal_errors( $previous );
		if ( false === $doc ) {
			return -1;
		}
		$name = strtolower( $doc->getName() );
		if ( 'urlset' === $name ) {
			return count( $doc->url );
		}
		if ( 'sitemapindex' === $name ) {
			return count( $doc->sitemap );
		}
		return -1;
	}

	/**
	 * Does robots.txt contain a blanket "Disallow: /" under "User-agent: *"?
	 *
	 * @param string $body robots.txt content.
	 * @return bool
	 */
	public static function has_blanket_disallow( $body ) {
		$lines         = preg_split( '/\r\n|\r|\n/', strtolower( (string) $body ) );
		$in_star_group = false;
		foreach ( $lines as $line ) {
			$line = trim( preg_replace( '/#.*$/', '', $line ) );
			if ( '' === $line ) {
				continue;
			}
			if ( 0 === strpos( $line, 'user-agent:' ) ) {
				$agent         = trim( substr( $line, 11 ) );
				$in_star_group = ( '*' === $agent );
				continue;
			}
			if ( $in_star_group && preg_match( '/^disallow:\s*\/\s*$/', $line ) ) {
				return true;
			}
		}
		return false;
	}
}
