<?php
/**
 * Google Indexing API client.
 *
 * Sends URL_UPDATED / URL_DELETED notifications to Google so newly published
 * or deleted pages are crawled faster than waiting for the natural crawl.
 *
 * The Indexing API is officially restricted to JobPosting and BroadcastEvent
 * structured-data pages, but the request itself is harmless to send for any
 * URL — Google simply ignores types it does not support, while still
 * crediting the ping toward the daily "URL submitted" quota. We document this
 * caveat in the admin UI so users are not surprised when the quota burns.
 *
 * Submissions are also recorded in a WP option-backed log (last 100 entries)
 * so the admin UI can show a recent-activity feed.
 *
 * @package Antimanual\Seo\Google
 * @since 3.7.0
 */

namespace Antimanual\Seo\Google;

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

/**
 * Indexing API submitter.
 */
class IndexingApiClient {

	private const SCOPE_INDEXING = 'https://www.googleapis.com/auth/indexing';

	private const PUBLISH_ENDPOINT = 'https://indexing.googleapis.com/v3/urlNotifications:publish';

	private const STATUS_ENDPOINT = 'https://indexing.googleapis.com/v3/urlNotifications/metadata';

	/** WP option storing recent submissions (capped). */
	private const OPT_LOG = 'antimanual_indexing_api_log';

	/** WP option toggling auto-submission on publish. */
	private const OPT_AUTO_SUBMIT = 'antimanual_indexing_api_auto_submit';

	/** Max log entries retained. */
	private const LOG_LIMIT = 100;

	/**
	 * Whether auto-submission on publish is enabled.
	 *
	 * @return bool
	 */
	public function is_auto_submit_enabled(): bool {
		return (bool) get_option( self::OPT_AUTO_SUBMIT, false );
	}

	/**
	 * Toggle auto-submission on publish.
	 *
	 * @param bool $enabled
	 * @return bool
	 */
	public function set_auto_submit_enabled( bool $enabled ): bool {
		return update_option( self::OPT_AUTO_SUBMIT, $enabled, false );
	}

	/**
	 * Submit a single URL.
	 *
	 * @param string $url  Page URL.
	 * @param string $type 'URL_UPDATED' or 'URL_DELETED'.
	 * @return array|\WP_Error Normalized response or error.
	 */
	public function submit( string $url, string $type = 'URL_UPDATED' ) {
		$url  = esc_url_raw( trim( $url ) );
		$type = strtoupper( trim( $type ) );

		if ( '' === $url || ! filter_var( $url, FILTER_VALIDATE_URL ) ) {
			return new \WP_Error( 'invalid_url', __( 'A valid URL is required.', 'antimanual' ) );
		}
		if ( ! in_array( $type, array( 'URL_UPDATED', 'URL_DELETED' ), true ) ) {
			return new \WP_Error( 'invalid_type', __( 'Notification type must be URL_UPDATED or URL_DELETED.', 'antimanual' ) );
		}

		$token = GoogleAuthService::instance()->get_access_token( self::SCOPE_INDEXING );
		if ( is_wp_error( $token ) ) {
			$this->log_submission( $url, $type, false, $token->get_error_message() );
			return $token;
		}

		$response = wp_remote_post(
			self::PUBLISH_ENDPOINT,
			array(
				'timeout' => 30,
				'headers' => array(
					'Authorization' => 'Bearer ' . $token,
					'Content-Type'  => 'application/json',
				),
				'body'    => wp_json_encode(
					array(
						'url'  => $url,
						'type' => $type,
					)
				),
			)
		);

		if ( is_wp_error( $response ) ) {
			$this->log_submission( $url, $type, false, $response->get_error_message() );
			return $response;
		}

		$status = (int) wp_remote_retrieve_response_code( $response );
		$body   = wp_remote_retrieve_body( $response );
		$data   = json_decode( $body, true );

		if ( $status < 200 || $status >= 300 ) {
			$msg = is_array( $data ) && isset( $data['error']['message'] )
				? (string) $data['error']['message']
				: sprintf(
					/* translators: %d: HTTP status code. */
					__( 'Indexing API returned HTTP %d.', 'antimanual' ),
					$status
				);
			$this->log_submission( $url, $type, false, $msg );
			return new \WP_Error( 'indexing_error', sanitize_text_field( $msg ), array( 'status' => $status ) );
		}

		$notify_time = '';
		if ( is_array( $data ) ) {
			$notify_time = (string) (
				$data['urlNotificationMetadata']['latestUpdate']['notifyTime'] ?? ''
			);
		}

		$this->log_submission( $url, $type, true, '' );

		return array(
			'url'         => $url,
			'type'        => $type,
			'notify_time' => sanitize_text_field( $notify_time ),
			'fetched_at'  => gmdate( 'c' ),
		);
	}

	/**
	 * Read the metadata Google has stored for a URL.
	 *
	 * Useful for confirming Google actually received our last submission.
	 *
	 * @param string $url Page URL.
	 * @return array|\WP_Error
	 */
	public function get_status( string $url ) {
		$url = esc_url_raw( trim( $url ) );
		if ( '' === $url || ! filter_var( $url, FILTER_VALIDATE_URL ) ) {
			return new \WP_Error( 'invalid_url', __( 'A valid URL is required.', 'antimanual' ) );
		}

		$token = GoogleAuthService::instance()->get_access_token( self::SCOPE_INDEXING );
		if ( is_wp_error( $token ) ) {
			return $token;
		}

		$response = wp_remote_get(
			add_query_arg( array( 'url' => $url ), self::STATUS_ENDPOINT ),
			array(
				'timeout' => 30,
				'headers' => array(
					'Authorization' => 'Bearer ' . $token,
				),
			)
		);

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

		$status = (int) wp_remote_retrieve_response_code( $response );
		$body   = wp_remote_retrieve_body( $response );
		$data   = json_decode( $body, true );

		if ( 404 === $status ) {
			return array(
				'url'        => $url,
				'submitted'  => false,
				'fetched_at' => gmdate( 'c' ),
			);
		}

		if ( $status < 200 || $status >= 300 ) {
			$msg = is_array( $data ) && isset( $data['error']['message'] )
				? (string) $data['error']['message']
				: sprintf(
					/* translators: %d: HTTP status code. */
					__( 'Indexing API returned HTTP %d.', 'antimanual' ),
					$status
				);
			return new \WP_Error( 'indexing_error', sanitize_text_field( $msg ), array( 'status' => $status ) );
		}

		$latest = is_array( $data['latestUpdate'] ?? null ) ? $data['latestUpdate'] : array();

		return array(
			'url'         => $url,
			'submitted'   => true,
			'type'        => isset( $latest['type'] ) ? sanitize_text_field( (string) $latest['type'] ) : '',
			'notify_time' => isset( $latest['notifyTime'] ) ? sanitize_text_field( (string) $latest['notifyTime'] ) : '',
			'fetched_at'  => gmdate( 'c' ),
		);
	}

	/**
	 * Append a submission record to the activity log (capped at LOG_LIMIT).
	 *
	 * @param string $url     Submitted URL.
	 * @param string $type    Notification type.
	 * @param bool   $success Whether the request succeeded.
	 * @param string $error   Error message on failure.
	 */
	private function log_submission( string $url, string $type, bool $success, string $error ): void {
		$log = get_option( self::OPT_LOG, array() );
		if ( ! is_array( $log ) ) {
			$log = array();
		}

		array_unshift(
			$log,
			array(
				'url'         => $url,
				'type'        => $type,
				'success'     => $success,
				'error'       => sanitize_text_field( $error ),
				'submitted_at' => gmdate( 'c' ),
			)
		);

		if ( count( $log ) > self::LOG_LIMIT ) {
			$log = array_slice( $log, 0, self::LOG_LIMIT );
		}

		update_option( self::OPT_LOG, $log, false );
	}

	/**
	 * Read the activity log.
	 *
	 * @param int $limit Max entries to return.
	 * @return array
	 */
	public function get_log( int $limit = 25 ): array {
		$log = get_option( self::OPT_LOG, array() );
		if ( ! is_array( $log ) ) {
			return array();
		}
		$limit = max( 1, min( self::LOG_LIMIT, $limit ) );
		return array_slice( $log, 0, $limit );
	}
}
