<?php
/**
 * Google Search Console client.
 *
 * Two surfaces are exposed:
 *   - queries(): top search queries / pages / dimensions for a property,
 *     scoped to a window of days (default 28d). Used to surface "what
 *     queries actually drive traffic to this URL?" in the SEO Agent.
 *   - inspect(): URL Inspection API — returns indexability state, last
 *     crawl, mobile usability, rich results, etc. Used to detect noindex
 *     issues without waiting for the next GSC report.
 *
 * Authentication is via a service account configured in GoogleAuthService.
 * The service account email must be added as a user (any role) on the
 * GSC property the user wants to query.
 *
 * @package Antimanual\Seo\Google
 * @since 3.7.0
 */

namespace Antimanual\Seo\Google;

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

/**
 * GSC Search Analytics + URL Inspection client.
 */
class SearchConsoleClient {

	private const SCOPE_READONLY = 'https://www.googleapis.com/auth/webmasters.readonly';

	private const SEARCH_ANALYTICS_ENDPOINT = 'https://www.googleapis.com/webmasters/v3/sites/%s/searchAnalytics/query';

	private const URL_INSPECTION_ENDPOINT = 'https://searchconsole.googleapis.com/v1/urlInspection/index:inspect';

	private const SITES_ENDPOINT = 'https://www.googleapis.com/webmasters/v3/sites';

	private const CACHE_PREFIX = 'atml_gsc_';

	private const CACHE_TTL = 3 * HOUR_IN_SECONDS;

	/**
	 * List the GSC properties the configured service account has access to.
	 *
	 * Used to populate a property dropdown in the admin UI so the user does
	 * not have to type the URL-prefix or domain-property string manually.
	 *
	 * @return array|\WP_Error
	 */
	public function list_sites() {
		$token = GoogleAuthService::instance()->get_access_token( self::SCOPE_READONLY );
		if ( is_wp_error( $token ) ) {
			return $token;
		}

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

		$result = $this->parse_response( $response );
		if ( is_wp_error( $result ) ) {
			return $result;
		}

		$entries = is_array( $result['siteEntry'] ?? null ) ? $result['siteEntry'] : array();
		$sites   = array();
		foreach ( $entries as $entry ) {
			if ( ! is_array( $entry ) || empty( $entry['siteUrl'] ) ) {
				continue;
			}
			$sites[] = array(
				'site_url'         => sanitize_text_field( (string) $entry['siteUrl'] ),
				'permission_level' => sanitize_text_field( (string) ( $entry['permissionLevel'] ?? '' ) ),
			);
		}

		return $sites;
	}

	/**
	 * Run a Search Analytics query.
	 *
	 * @param string $site_url   GSC property URL (e.g. 'sc-domain:example.com' or 'https://example.com/').
	 * @param array  $args       {
	 *     @type int      $days        Look-back window (default 28).
	 *     @type string[] $dimensions  E.g. ['query'], ['page'], ['query','page'].
	 *     @type string   $page        Optional: filter to a specific page URL.
	 *     @type int      $row_limit   Max rows (default 50, max 250).
	 * }
	 * @param bool   $force      Skip the cache.
	 * @return array|\WP_Error
	 */
	public function query( string $site_url, array $args = array(), bool $force = false ) {
		$site_url = trim( $site_url );
		if ( '' === $site_url ) {
			return new \WP_Error( 'no_site', __( 'A Search Console property is required.', 'antimanual' ) );
		}

		$args = wp_parse_args(
			$args,
			array(
				'days'       => 28,
				'dimensions' => array( 'query' ),
				'page'       => '',
				'row_limit'  => 50,
			)
		);

		$days       = max( 1, min( 90, (int) $args['days'] ) );
		$row_limit  = max( 1, min( 250, (int) $args['row_limit'] ) );
		$dimensions = array_values(
			array_intersect(
				array_map( 'sanitize_key', (array) $args['dimensions'] ),
				array( 'query', 'page', 'country', 'device', 'date', 'searchAppearance' )
			)
		);
		if ( empty( $dimensions ) ) {
			$dimensions = array( 'query' );
		}

		// Use the data freshness lag-aware window: GSC data has a ~3 day delay,
		// so we anchor end_date to 2 days ago to avoid empty trailing days.
		$end_date   = gmdate( 'Y-m-d', time() - 2 * DAY_IN_SECONDS );
		$start_date = gmdate( 'Y-m-d', time() - ( $days + 2 ) * DAY_IN_SECONDS );

		$body = array(
			'startDate'  => $start_date,
			'endDate'    => $end_date,
			'dimensions' => $dimensions,
			'rowLimit'   => $row_limit,
			'dataState'  => 'final',
		);

		if ( ! empty( $args['page'] ) ) {
			$body['dimensionFilterGroups'] = array(
				array(
					'filters' => array(
						array(
							'dimension' => 'page',
							'operator'  => 'equals',
							'expression' => esc_url_raw( (string) $args['page'] ),
						),
					),
				),
			);
		}

		$cache_key = self::CACHE_PREFIX . md5( wp_json_encode( array( $site_url, $body ) ) );
		if ( ! $force ) {
			$cached = get_transient( $cache_key );
			if ( is_array( $cached ) ) {
				$cached['cached'] = true;
				return $cached;
			}
		}

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

		$endpoint = sprintf( self::SEARCH_ANALYTICS_ENDPOINT, rawurlencode( $site_url ) );
		$response = wp_remote_post(
			$endpoint,
			array(
				'timeout' => 60,
				'headers' => array(
					'Authorization' => 'Bearer ' . $token,
					'Content-Type'  => 'application/json',
				),
				'body'    => wp_json_encode( $body ),
			)
		);

		$result = $this->parse_response( $response );
		if ( is_wp_error( $result ) ) {
			return $result;
		}

		$rows       = is_array( $result['rows'] ?? null ) ? $result['rows'] : array();
		$normalized = array();
		foreach ( $rows as $row ) {
			if ( ! is_array( $row ) ) {
				continue;
			}
			$entry = array(
				'clicks'      => isset( $row['clicks'] ) ? (int) $row['clicks'] : 0,
				'impressions' => isset( $row['impressions'] ) ? (int) $row['impressions'] : 0,
				'ctr'         => isset( $row['ctr'] ) ? round( (float) $row['ctr'], 4 ) : 0.0,
				'position'    => isset( $row['position'] ) ? round( (float) $row['position'], 2 ) : 0.0,
			);
			$keys = is_array( $row['keys'] ?? null ) ? $row['keys'] : array();
			foreach ( $dimensions as $idx => $dim ) {
				$entry[ $dim ] = isset( $keys[ $idx ] ) ? sanitize_text_field( (string) $keys[ $idx ] ) : '';
			}
			$normalized[] = $entry;
		}

		$payload = array(
			'site_url'   => $site_url,
			'start_date' => $start_date,
			'end_date'   => $end_date,
			'dimensions' => $dimensions,
			'rows'       => $normalized,
			'fetched_at' => gmdate( 'c' ),
			'cached'     => false,
		);

		set_transient( $cache_key, $payload, self::CACHE_TTL );

		return $payload;
	}

	/**
	 * Inspect a URL with the URL Inspection API.
	 *
	 * @param string $site_url    GSC property URL.
	 * @param string $inspect_url Page URL to inspect (must belong to the property).
	 * @param bool   $force       Skip the cache.
	 * @return array|\WP_Error
	 */
	public function inspect( string $site_url, string $inspect_url, bool $force = false ) {
		$site_url    = trim( $site_url );
		$inspect_url = esc_url_raw( trim( $inspect_url ) );

		if ( '' === $site_url || '' === $inspect_url ) {
			return new \WP_Error(
				'invalid_input',
				__( 'A Search Console property and an inspectable URL are required.', 'antimanual' )
			);
		}

		$cache_key = self::CACHE_PREFIX . 'inspect_' . md5( $site_url . '|' . $inspect_url );
		if ( ! $force ) {
			$cached = get_transient( $cache_key );
			if ( is_array( $cached ) ) {
				$cached['cached'] = true;
				return $cached;
			}
		}

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

		$response = wp_remote_post(
			self::URL_INSPECTION_ENDPOINT,
			array(
				'timeout' => 60,
				'headers' => array(
					'Authorization' => 'Bearer ' . $token,
					'Content-Type'  => 'application/json',
				),
				'body'    => wp_json_encode(
					array(
						'inspectionUrl' => $inspect_url,
						'siteUrl'       => $site_url,
					)
				),
			)
		);

		$result = $this->parse_response( $response );
		if ( is_wp_error( $result ) ) {
			return $result;
		}

		$index_status = is_array( $result['inspectionResult']['indexStatusResult'] ?? null )
			? $result['inspectionResult']['indexStatusResult']
			: array();
		$mobile       = is_array( $result['inspectionResult']['mobileUsabilityResult'] ?? null )
			? $result['inspectionResult']['mobileUsabilityResult']
			: array();

		$payload = array(
			'inspect_url'        => $inspect_url,
			'site_url'           => $site_url,
			'fetched_at'         => gmdate( 'c' ),
			'cached'             => false,
			'verdict'            => isset( $index_status['verdict'] ) ? sanitize_text_field( (string) $index_status['verdict'] ) : '',
			'coverage_state'     => isset( $index_status['coverageState'] ) ? sanitize_text_field( (string) $index_status['coverageState'] ) : '',
			'indexing_state'     => isset( $index_status['indexingState'] ) ? sanitize_text_field( (string) $index_status['indexingState'] ) : '',
			'robots_txt_state'   => isset( $index_status['robotsTxtState'] ) ? sanitize_text_field( (string) $index_status['robotsTxtState'] ) : '',
			'page_fetch_state'   => isset( $index_status['pageFetchState'] ) ? sanitize_text_field( (string) $index_status['pageFetchState'] ) : '',
			'last_crawl_time'    => isset( $index_status['lastCrawlTime'] ) ? sanitize_text_field( (string) $index_status['lastCrawlTime'] ) : '',
			'crawled_as'         => isset( $index_status['crawledAs'] ) ? sanitize_text_field( (string) $index_status['crawledAs'] ) : '',
			'google_canonical'   => isset( $index_status['googleCanonical'] ) ? esc_url_raw( (string) $index_status['googleCanonical'] ) : '',
			'user_canonical'     => isset( $index_status['userCanonical'] ) ? esc_url_raw( (string) $index_status['userCanonical'] ) : '',
			'mobile_verdict'     => isset( $mobile['verdict'] ) ? sanitize_text_field( (string) $mobile['verdict'] ) : '',
			'inspection_url'     => isset( $result['inspectionResult']['inspectionResultLink'] )
				? esc_url_raw( (string) $result['inspectionResult']['inspectionResultLink'] )
				: '',
		);

		set_transient( $cache_key, $payload, self::CACHE_TTL );

		return $payload;
	}

	/**
	 * Decode a Google API response and surface errors as WP_Error.
	 *
	 * @param array|\WP_Error $response wp_remote_*() response.
	 * @return array|\WP_Error
	 */
	private function parse_response( $response ) {
		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 ( $status < 200 || $status >= 300 ) {
			$msg = is_array( $data ) && isset( $data['error']['message'] )
				? (string) $data['error']['message']
				: sprintf(
					/* translators: %d: HTTP status code. */
					__( 'Search Console returned HTTP %d.', 'antimanual' ),
					$status
				);
			return new \WP_Error( 'gsc_error', sanitize_text_field( $msg ), array( 'status' => $status ) );
		}

		if ( ! is_array( $data ) ) {
			return new \WP_Error( 'gsc_invalid', __( 'Search Console returned an invalid response.', 'antimanual' ) );
		}

		return $data;
	}
}
