<?php
/**
 * GSC Client.
 *
 * A lightweight Google Search Console API client. It authenticates as a
 * service account by signing a JWT with the account's private key
 * (RS256 via OpenSSL) and exchanging it for an access token — no Google
 * SDK, no Composer dependency, keeping the plugin lean per house rules.
 *
 * Endpoints used:
 *   - GET  webmasters/v3/sites                              (verify + list properties)
 *   - POST webmasters/v3/sites/{site}/searchAnalytics/query (clicks/impressions/etc.)
 *   - POST searchconsole/v1/urlInspection/index:inspect     (index coverage; used later)
 *
 * Every method returns a plain array on success or a WP_Error on failure,
 * surfacing Google's own error message so the UI can show what went wrong.
 *
 * @package Platform\Helm\Modules\GSC
 */

declare( strict_types = 1 );

namespace Platform\Helm\Modules\GSC;

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

final class GscClient {

	private const SCOPE         = 'https://www.googleapis.com/auth/webmasters.readonly';
	private const TOKEN_TTL     = 3000; // ~50 min; Google tokens last 60.
	private const HTTP_TIMEOUT  = 25;
	private const WEBMASTERS    = 'https://www.googleapis.com/webmasters/v3';
	private const URLINSPECTION = 'https://searchconsole.googleapis.com/v1/urlInspection/index:inspect';

	/**
	 * Fetch (and cache) an access token for the stored service account.
	 *
	 * @return string|\WP_Error
	 */
	/**
	 * Get an access token using whichever auth method is configured.
	 *
	 * @return string|\WP_Error
	 */
	private function access_token() {
		if ( GscCredentials::METHOD_OAUTH === GscCredentials::auth_method() ) {
			return $this->oauth_token();
		}
		return $this->service_account_token();
	}

	/**
	 * OAuth: exchange the stored refresh token for a fresh access token.
	 *
	 * @return string|\WP_Error
	 */
	private function oauth_token() {
		$client_id     = GscCredentials::oauth_client_id();
		$client_secret = GscCredentials::oauth_client_secret();
		$refresh_token = GscCredentials::oauth_refresh_token();
		if ( '' === $client_id || '' === $refresh_token ) {
			return new \WP_Error( 'gsc_not_connected', __( 'Not connected with Google. Click “Connect with Google” to authorise.', 'platform-helm' ) );
		}

		$cache_key = 'platform_helm_gsc_token_oauth_' . md5( $client_id );
		$cached    = get_transient( $cache_key );
		if ( is_string( $cached ) && '' !== $cached ) {
			return $cached;
		}

		$resp = wp_remote_post( 'https://oauth2.googleapis.com/token', [
			'timeout' => self::HTTP_TIMEOUT,
			'body'    => [
				'grant_type'    => 'refresh_token',
				'refresh_token' => $refresh_token,
				'client_id'     => $client_id,
				'client_secret' => $client_secret,
			],
		] );
		$data = $this->decode_response( $resp );
		if ( is_wp_error( $data ) ) {
			return $data;
		}
		$token = (string) ( $data['access_token'] ?? '' );
		if ( '' === $token ) {
			return new \WP_Error( 'gsc_no_token', __( 'Google did not return an access token. You may need to reconnect.', 'platform-helm' ) );
		}
		set_transient( $cache_key, $token, self::TOKEN_TTL );
		return $token;
	}

	/**
	 * Service account: sign a JWT and exchange it for an access token.
	 *
	 * @return string|\WP_Error
	 */
	private function service_account_token() {
		$email = GscCredentials::client_email();
		$key   = GscCredentials::private_key();
		$aud   = GscCredentials::token_uri();
		if ( '' === $email || '' === $key ) {
			return new \WP_Error( 'gsc_not_connected', __( 'No Google service account is connected.', 'platform-helm' ) );
		}

		$cache_key = 'platform_helm_gsc_token_' . md5( $email );
		$cached    = get_transient( $cache_key );
		if ( is_string( $cached ) && '' !== $cached ) {
			return $cached;
		}

		$now    = time();
		$header = $this->b64url( (string) wp_json_encode( [ 'alg' => 'RS256', 'typ' => 'JWT' ] ) );
		$claims = $this->b64url( (string) wp_json_encode( [
			'iss'   => $email,
			'scope' => self::SCOPE,
			'aud'   => $aud,
			'exp'   => $now + 3600,
			'iat'   => $now,
		] ) );

		$signing_input = $header . '.' . $claims;
		$signature     = '';
		$ok            = openssl_sign( $signing_input, $signature, $key, 'SHA256' );
		if ( ! $ok ) {
			return new \WP_Error( 'gsc_sign_failed', __( 'Could not sign the auth request — the private key may be malformed.', 'platform-helm' ) );
		}
		$jwt = $signing_input . '.' . $this->b64url( $signature );

		$resp = wp_remote_post( $aud, [
			'timeout' => self::HTTP_TIMEOUT,
			'body'    => [
				'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer',
				'assertion'  => $jwt,
			],
		] );
		$data = $this->decode_response( $resp );
		if ( is_wp_error( $data ) ) {
			return $data;
		}
		$token = (string) ( $data['access_token'] ?? '' );
		if ( '' === $token ) {
			return new \WP_Error( 'gsc_no_token', __( 'Google did not return an access token. Check that the Search Console API is enabled for the service account.', 'platform-helm' ) );
		}
		set_transient( $cache_key, $token, self::TOKEN_TTL );
		return $token;
	}

	/**
	 * List the Search Console properties this service account can read.
	 *
	 * @return array<int,array{siteUrl:string,permissionLevel:string}>|\WP_Error
	 */
	public function list_sites() {
		$token = $this->access_token();
		if ( is_wp_error( $token ) ) {
			return $token;
		}
		$resp = wp_remote_get( self::WEBMASTERS . '/sites', [
			'timeout' => self::HTTP_TIMEOUT,
			'headers' => [ 'Authorization' => 'Bearer ' . $token ],
		] );
		$data = $this->decode_response( $resp );
		if ( is_wp_error( $data ) ) {
			return $data;
		}
		$out = [];
		foreach ( (array) ( $data['siteEntry'] ?? [] ) as $entry ) {
			$out[] = [
				'siteUrl'         => (string) ( $entry['siteUrl'] ?? '' ),
				'permissionLevel' => (string) ( $entry['permissionLevel'] ?? '' ),
			];
		}
		return $out;
	}

	/**
	 * Query Search Analytics for one page of results.
	 *
	 * @param array<int,string> $dimensions e.g. ['page'] or ['page','query'].
	 * @return array{rows:array<int,array{keys:array<int,string>,clicks:float,impressions:float,ctr:float,position:float}>}|\WP_Error
	 */
	public function query_search_analytics( string $property, string $start_date, string $end_date, int $start_row = 0, int $row_limit = 25000, array $dimensions = [ 'page' ] ) {
		$token = $this->access_token();
		if ( is_wp_error( $token ) ) {
			return $token;
		}
		$url  = self::WEBMASTERS . '/sites/' . rawurlencode( $property ) . '/searchAnalytics/query';
		$body = wp_json_encode( [
			'startDate'  => $start_date,
			'endDate'    => $end_date,
			'dimensions' => array_values( $dimensions ),
			'rowLimit'   => max( 1, min( 25000, $row_limit ) ),
			'startRow'   => max( 0, $start_row ),
			'dataState'  => 'all',
		] );
		$resp = wp_remote_post( $url, [
			'timeout' => self::HTTP_TIMEOUT,
			'headers' => [
				'Authorization' => 'Bearer ' . $token,
				'Content-Type'  => 'application/json',
			],
			'body'    => $body,
		] );
		$data = $this->decode_response( $resp );
		if ( is_wp_error( $data ) ) {
			return $data;
		}
		$rows = [];
		foreach ( (array) ( $data['rows'] ?? [] ) as $row ) {
			$rows[] = [
				'keys'        => array_map( 'strval', (array) ( $row['keys'] ?? [] ) ),
				'clicks'      => (float) ( $row['clicks'] ?? 0 ),
				'impressions' => (float) ( $row['impressions'] ?? 0 ),
				'ctr'         => (float) ( $row['ctr'] ?? 0 ),
				'position'    => (float) ( $row['position'] ?? 0 ),
			];
		}
		return [ 'rows' => $rows ];
	}

	/**
	 * Inspect a single URL's index status. Quota: 2,000/day per property.
	 *
	 * @return array<string,mixed>|\WP_Error
	 */
	public function inspect_url( string $property, string $inspection_url ) {
		$token = $this->access_token();
		if ( is_wp_error( $token ) ) {
			return $token;
		}
		$resp = wp_remote_post( self::URLINSPECTION, [
			'timeout' => 8, // Short, so one slow URL can't blow the batch budget.
			'headers' => [
				'Authorization' => 'Bearer ' . $token,
				'Content-Type'  => 'application/json',
			],
			'body'    => wp_json_encode( [
				'inspectionUrl' => $inspection_url,
				'siteUrl'       => $property,
			] ),
		] );
		return $this->decode_response( $resp );
	}

	/* ---------- helpers ---------- */

	private function b64url( string $bin ): string {
		return rtrim( strtr( base64_encode( $bin ), '+/', '-_' ), '=' );
	}

	/**
	 * Decode a wp_remote_* response into an array, or a WP_Error carrying
	 * Google's error message when the HTTP status is not 2xx.
	 *
	 * @param array|\WP_Error $resp
	 * @return array<string,mixed>|\WP_Error
	 */
	private function decode_response( $resp ) {
		if ( is_wp_error( $resp ) ) {
			return $resp;
		}
		$code = (int) wp_remote_retrieve_response_code( $resp );
		$body = (string) wp_remote_retrieve_body( $resp );
		$data = json_decode( $body, true );
		if ( ! is_array( $data ) ) {
			$data = [];
		}
		if ( $code < 200 || $code >= 300 ) {
			$msg = '';
			if ( isset( $data['error']['message'] ) ) {
				$msg = (string) $data['error']['message'];
			} elseif ( isset( $data['error_description'] ) ) {
				$msg = (string) $data['error_description'];
			} elseif ( isset( $data['error'] ) && is_string( $data['error'] ) ) {
				$msg = (string) $data['error'];
			}
			if ( '' === $msg ) {
				$msg = sprintf(
					/* translators: %d: HTTP status code */
					__( 'Google API returned HTTP %d.', 'platform-helm' ),
					$code
				);
			}
			return new \WP_Error( 'gsc_api_error', $msg, [ 'status' => $code ] );
		}
		return $data;
	}
}
