<?php

namespace SerpIQ\Gsc;

use SerpIQ\Helpers\Crypto;
use SerpIQ\Helpers\Options;

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

/**
 * Google OAuth 2.0 (authorization code) for Search Console.
 *
 * Security model (for code review):
 * - Starting the flow (admin “Connect” link or REST POST) is protected by WordPress
 *   (admin referer / REST capability plus wp_rest nonce on REST). That mitigates
 *   cross-site initiation of the flow from our entrypoints.
 * - The round-trip to Google uses the standard OAuth 2.0 state query parameter
 *   (RFC 6749) as the anti-CSRF token: we store a secret per user, send it to Google,
 *   and require an exact match on callback. Google does not receive or echo a
 *   WordPress _wpnonce; the authorize URL must not be wrapped with the Settings UI
 *   nonce helper, because the redirect back only carries code and state (plus any
 *   parameters Google adds).
 */
final class OAuth {
	private static ?OAuth $instance = null;

	public static function instance(): OAuth {
		if ( null === self::$instance ) {
			self::$instance = new self();
		}
		return self::$instance;
	}

	public function redirect_uri(): string {
		return admin_url( 'admin.php?page=serpiq-settings' );
	}

	public function is_library_available(): bool {
		return class_exists( '\Google_Client' );
	}

	public function has_token(): bool {
		return (bool) get_option( Options::google_token_encrypted_key(), '' );
	}

	/**
	 * Builds Google’s OAuth 2.0 authorization URL (browser redirect).
	 *
	 * Embeds a high-entropy state value for CSRF protection on the callback.
	 * This URL is external; do not pass it through SerpIQ\Admin\Admin URL signing that
	 * appends _wpnonce for on-screen Settings links. Google’s return contract is
	 * code plus state, not WordPress nonces.
	 *
	 * @return string|\WP_Error
	 */
	public function build_auth_url() {
		if ( ! $this->is_library_available() ) {
			return new \WP_Error( 'serpiq_missing_google_lib', __( 'Google API client library not found. Bundle vendor/ or install dependencies.', 'seotune-search-insights' ) );
		}

		$client_id = (string) get_option( Options::google_client_id_key(), '' );
		$client_secret = (string) get_option( Options::google_client_secret_key(), '' );
		if ( '' === $client_id || '' === $client_secret ) {
			return new \WP_Error( 'serpiq_missing_google_credentials', __( 'Missing Google Client ID/Secret.', 'seotune-search-insights' ) );
		}

		$client = new \Google_Client();
		$client->setClientId( $client_id );
		$client->setClientSecret( $client_secret );
		$client->setRedirectUri( $this->redirect_uri() );
		$client->setScopes( array( 'https://www.googleapis.com/auth/webmasters.readonly' ) );
		$client->setAccessType( 'offline' );
		$client->setPrompt( 'consent' );
		$client->setIncludeGrantedScopes( true );

		// OAuth 2.0 state: CSRF token for this authorization request (RFC 6749).
		$state = bin2hex( random_bytes( 16 ) );
		set_transient( 'serpiq_oauth_state_' . get_current_user_id(), $state, 10 * MINUTE_IN_SECONDS );
		$client->setState( $state );

		$url = $client->createAuthUrl();
		return $url;
	}

	/**
	 * Completes the authorization code exchange after Google redirects back.
	 *
	 * Validates state against the value stored when build_auth_url() ran.
	 * WordPress URL nonces are not used here: Google cannot return them.
	 *
	 * @param string $code  Authorization code from Google.
	 * @param string $state Must match the pending per-user transient (anti-CSRF).
	 * @return true|\WP_Error
	 */
	public function handle_oauth_callback( string $code, string $state ) {
		if ( ! $this->is_library_available() ) {
			return new \WP_Error( 'serpiq_missing_google_lib', __( 'Google API client library not found.', 'seotune-search-insights' ) );
		}

		$expected = (string) get_transient( 'serpiq_oauth_state_' . get_current_user_id() );
		delete_transient( 'serpiq_oauth_state_' . get_current_user_id() );
		if ( '' === $expected || ! hash_equals( $expected, $state ) ) {
			return new \WP_Error( 'serpiq_oauth_state_mismatch', __( 'Invalid OAuth state.', 'seotune-search-insights' ) );
		}

		$client_id = (string) get_option( Options::google_client_id_key(), '' );
		$client_secret = (string) get_option( Options::google_client_secret_key(), '' );
		if ( '' === $client_id || '' === $client_secret ) {
			return new \WP_Error( 'serpiq_missing_google_credentials', __( 'Missing Google Client ID/Secret.', 'seotune-search-insights' ) );
		}

		$client = new \Google_Client();
		$client->setClientId( $client_id );
		$client->setClientSecret( $client_secret );
		$client->setRedirectUri( $this->redirect_uri() );

		$token = $client->fetchAccessTokenWithAuthCode( $code );
		if ( ! is_array( $token ) || isset( $token['error'] ) ) {
			$msg = is_array( $token ) && isset( $token['error_description'] ) ? (string) $token['error_description'] : __( 'OAuth token exchange failed.', 'seotune-search-insights' );
			return new \WP_Error( 'serpiq_oauth_exchange_failed', $msg );
		}

		$existing = $this->get_token();
		if ( ( ! isset( $token['refresh_token'] ) || '' === (string) $token['refresh_token'] ) && is_array( $existing ) && isset( $existing['refresh_token'] ) ) {
			$token['refresh_token'] = $existing['refresh_token'];
		}

		$this->store_token( $token );
		return true;
	}

	/**
	 * @return array<string,mixed>|null
	 */
	public function get_token(): ?array {
		$ciphertext = (string) get_option( Options::google_token_encrypted_key(), '' );
		$iv = (string) get_option( Options::google_token_iv_key(), '' );
		$json = Crypto::decrypt_string( $ciphertext, $iv );
		if ( '' === $json ) {
			return null;
		}
		$token = json_decode( $json, true );
		return is_array( $token ) ? $token : null;
	}

	/**
	 * @param array<string,mixed> $token
	 */
	public function store_token( array $token ): void {
		$json = wp_json_encode( $token );
		$enc = Crypto::encrypt_string( $json ? $json : '' );
		update_option( Options::google_token_encrypted_key(), $enc['ciphertext'], false );
		update_option( Options::google_token_iv_key(), $enc['iv'], false );
	}

	public function disconnect(): void {
		delete_option( Options::google_token_encrypted_key() );
		delete_option( Options::google_token_iv_key() );
		delete_option( Options::google_property_key() );
	}

	/**
	 * Returns an authenticated Google_Client.
	 */
	public function client() {
		if ( ! $this->is_library_available() ) {
			return new \WP_Error( 'serpiq_missing_google_lib', __( 'Google API client library not found.', 'seotune-search-insights' ) );
		}

		$client_id = (string) get_option( Options::google_client_id_key(), '' );
		$client_secret = (string) get_option( Options::google_client_secret_key(), '' );
		if ( '' === $client_id || '' === $client_secret ) {
			return new \WP_Error( 'serpiq_missing_google_credentials', __( 'Missing Google Client ID/Secret.', 'seotune-search-insights' ) );
		}

		$client = new \Google_Client();
		$client->setClientId( $client_id );
		$client->setClientSecret( $client_secret );
		$client->setRedirectUri( $this->redirect_uri() );
		$client->setScopes( array( 'https://www.googleapis.com/auth/webmasters.readonly' ) );
		$client->setAccessType( 'offline' );

		$token = $this->get_token();
		if ( $token ) {
			$client->setAccessToken( $token );
			if ( method_exists( $client, 'isAccessTokenExpired' ) && $client->isAccessTokenExpired() && isset( $token['refresh_token'] ) ) {
				$new = $client->fetchAccessTokenWithRefreshToken( (string) $token['refresh_token'] );
				if ( is_array( $new ) && ! isset( $new['error'] ) ) {
					$new['refresh_token'] = (string) $token['refresh_token'];
					$this->store_token( $new );
					$client->setAccessToken( $new );
				}
			}
		}

		return $client;
	}
}

