<?php
/**
 * GSC Index Checker.
 *
 * Drives the URL Inspection API in small batches, prioritising posts never
 * checked, then the stalest. Respects a self-imposed daily cap below Google's
 * 2,000/day URL-Inspection quota so a big site is covered over a few days
 * without ever tripping the limit.
 *
 * @package Platform\Helm\Modules\GSC
 */

declare( strict_types = 1 );

namespace Platform\Helm\Modules\GSC;

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

final class GscIndexChecker {

	private const DAILY_CAP        = 1500; // Stay safely under Google's 2,000/day.
	private const BATCH_BUDGET     = 12;   // Seconds; return before any server timeout.
	private const DAILY_OPT_PREFIX = 'platform_helm_gsc_index_count_';

	public function __construct(
		private GscClient $client,
		private GscIndexStore $store
	) {}

	/**
	 * @return array{ok:bool,checked:int,problems:int,remaining:int,capped:bool,message:string}
	 */
	public function check_batch( int $limit = 10 ): array {
		if ( ! GscCredentials::is_connected() ) {
			return $this->result( false, 0, 0, 0, false, __( 'Not connected to Search Console.', 'platform-helm' ) );
		}
		$property = GscCredentials::property();
		if ( '' === $property ) {
			return $this->result( false, 0, 0, 0, false, __( 'No Search Console property selected.', 'platform-helm' ) );
		}

		$used      = $this->used_today();
		$remaining_quota = max( 0, self::DAILY_CAP - $used );
		if ( $remaining_quota <= 0 ) {
			return $this->result( true, 0, 0, $this->remaining_posts(), true, __( 'Daily inspection limit reached. Resume tomorrow — Google caps URL Inspection at ~2,000/day.', 'platform-helm' ) );
		}

		$limit = min( $limit, $remaining_quota );
		$ids   = $this->next_posts( $limit );
		if ( empty( $ids ) ) {
			return $this->result( true, 0, 0, 0, false, __( 'All published posts have been checked.', 'platform-helm' ) );
		}

		$deadline = microtime( true ) + self::BATCH_BUDGET; // Return before any proxy/PHP timeout.
		$checked  = 0;
		$problems = 0;
		foreach ( $ids as $pid ) {
			$url = get_permalink( $pid );
			if ( ! $url ) {
				continue;
			}
			$result = $this->client->inspect_url( $property, $url );
			if ( is_wp_error( $result ) ) {
				// Persist progress; surface the error so the user can react (often quota).
				$this->bump_today( $checked );
				return $this->result( false, $checked, $problems, $this->remaining_posts(), false, $result->get_error_message() );
			}
			$parsed = $this->parse( (array) $result, (string) $url );
			$this->store->upsert( $pid, $parsed );
			if ( 'PASS' !== $parsed['verdict'] ) {
				$problems++;
			}
			$checked++;
			if ( microtime( true ) >= $deadline ) {
				break; // Time budget hit — return now; the loop resumes on the next batch.
			}
		}

		$this->bump_today( $checked );
		return $this->result( true, $checked, $problems, $this->remaining_posts(), false, '' );
	}

	/**
	 * Flatten the URL Inspection response to the fields we store.
	 *
	 * @param array<string,mixed> $resp
	 * @return array<string,mixed>
	 */
	private function parse( array $resp, string $url ): array {
		$idx = $resp['inspectionResult']['indexStatusResult'] ?? [];
		$idx = is_array( $idx ) ? $idx : [];
		$crawl = isset( $idx['lastCrawlTime'] ) ? gmdate( 'Y-m-d H:i:s', strtotime( (string) $idx['lastCrawlTime'] ) ?: time() ) : null;
		return [
			'url'              => $url,
			'verdict'          => (string) ( $idx['verdict'] ?? 'NEUTRAL' ),
			'coverage'         => (string) ( $idx['coverageState'] ?? '' ),
			'indexing_state'   => (string) ( $idx['indexingState'] ?? '' ),
			'robots_state'     => (string) ( $idx['robotsTxtState'] ?? '' ),
			'last_crawl'       => $crawl,
			'google_canonical' => (string) ( $idx['googleCanonical'] ?? '' ),
			'user_canonical'   => (string) ( $idx['userCanonical'] ?? '' ),
		];
	}

	/** @return array<int,int> */
	private function next_posts( int $limit ): array {
		// Forward cursor: posts are checked in ascending ID order, so the next
		// unchecked posts are simply those with an ID above the highest checked.
		// Far lighter than a growing post__not_in exclusion list.
		$cursor = $this->store->max_post_id();
		$args   = [
			'post_type'      => 'post',
			'post_status'    => 'publish',
			'fields'         => 'ids',
			'posts_per_page' => $limit,
			'orderby'        => 'ID',
			'order'          => 'ASC',
			'no_found_rows'  => true,
		];
		if ( $cursor > 0 ) {
			add_filter( 'posts_where', $cw = static function ( $where ) use ( $cursor ) {
				global $wpdb;
				return $where . $wpdb->prepare( " AND {$wpdb->posts}.ID > %d", $cursor );
			} );
			$q = new \WP_Query( $args );
			remove_filter( 'posts_where', $cw );
		} else {
			$q = new \WP_Query( $args );
		}
		return array_map( 'intval', $q->posts );
	}

	private function remaining_posts(): int {
		$total = (int) wp_count_posts( 'post' )->publish;
		return max( 0, $total - $this->store->count_checked() );
	}

	private function used_today(): int {
		return (int) get_option( self::DAILY_OPT_PREFIX . gmdate( 'Ymd' ), 0 );
	}

	private function bump_today( int $n ): void {
		if ( $n <= 0 ) {
			return;
		}
		$key = self::DAILY_OPT_PREFIX . gmdate( 'Ymd' );
		update_option( $key, $this->used_today() + $n, false );
	}

	/**
	 * @return array{ok:bool,checked:int,problems:int,remaining:int,capped:bool,message:string}
	 */
	private function result( bool $ok, int $checked, int $problems, int $remaining, bool $capped, string $message ): array {
		return [
			'ok'        => $ok,
			'checked'   => $checked,
			'problems'  => $problems,
			'remaining' => $remaining,
			'capped'    => $capped,
			'message'   => $message,
		];
	}
}
