<?php
/**
 * Low-level HTTP client for the Google Sheets / Drive REST APIs.
 *
 * @package MetForm_Pro\Core\Integrations\Google_Sheet
 */

namespace MetForm_Pro\Core\Integrations\Google_Sheet;

defined( 'ABSPATH' ) || exit;

/**
 * Wraps authenticated requests to Google's Sheets v4 and Drive v3 APIs.
 *
 * Handles:
 * - Reading/refreshing the stored OAuth access token.
 * - Firing authenticated HTTP requests with automatic 401 retry-after-refresh.
 * - Thin wrappers for the specific Sheets/Drive endpoints MetForm needs.
 */
class Google_API_Client {

	/**
	 * Request timeout in seconds.
	 *
	 * @var int
	 */
	const TIMEOUT = 20;

	/**
	 * Google OAuth client ID.
	 *
	 * @var string
	 */
	private $client_id;

	/**
	 * Google OAuth client secret.
	 *
	 * @var string
	 */
	private $client_secret;

	/**
	 * @param string $client_id     Google OAuth client ID.
	 * @param string $client_secret Google OAuth client secret.
	 */
	public function __construct( $client_id, $client_secret ) {
		$this->client_id     = $client_id;
		$this->client_secret = $client_secret;
	}

	/**
	 * Gets a valid access token, refreshing it first if the cached copy has expired.
	 *
	 * @return string|\WP_Error Access token string, or WP_Error on failure.
	 */
	public function get_access_token() {
		$token = $this->stored_token();

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

		if ( ! get_transient( 'mf_google_sheet_token' ) ) {
			$token = $this->refresh_access_token();
		}

		return is_wp_error( $token ) ? $token : $token['access_token'];
	}

	/**
	 * Exchanges the stored refresh token for a new access token and persists it.
	 *
	 * @return array|\WP_Error Decoded token data on success, WP_Error on failure.
	 */
	public function refresh_access_token() {
		$stored = $this->stored_token();

		if ( is_wp_error( $stored ) || empty( $stored['refresh_token'] ) ) {
			return new \WP_Error(
				'google_oauth_missing_refresh_token',
				__( 'Google authorization is missing or invalid. Please reconnect Google.', 'metform-pro' )
			);
		}

		$response = wp_remote_post(
			'https://oauth2.googleapis.com/token',
			array(
				'timeout' => self::TIMEOUT,
				'headers' => array( 'Content-Type' => 'application/x-www-form-urlencoded' ),
				'body'    => array(
					'grant_type'    => 'refresh_token',
					'refresh_token' => $stored['refresh_token'],
					'client_id'     => $this->client_id,
					'client_secret' => $this->client_secret,
				),
			)
		);

		$data = $this->decode_response( $response, 'google_oauth_refresh_failed' );

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

		if ( empty( $data['access_token'] ) ) {
			return new \WP_Error(
				'google_oauth_no_access_token',
				__( 'Google did not return an access token. Please reconnect Google.', 'metform-pro' )
			);
		}

		// Google omits refresh_token on refresh responses; keep the original.
		$data['refresh_token'] = $stored['refresh_token'];

		$expires_in = isset( $data['expires_in'] ) ? max( 1, (int) $data['expires_in'] ) : 3600;

		update_option( 'wf_google_access_token', wp_json_encode( $data ) );
		set_transient( 'mf_google_sheet_token', $data, max( 1, $expires_in - 60 ) );

		return $data;
	}

	/**
	 * Fires an authenticated request against a Google API endpoint.
	 *
	 * @param string $method HTTP method (GET, POST, PUT, ...).
	 * @param string $url    Full request URL.
	 * @param array  $body   Optional request body, JSON-encoded automatically.
	 * @param array  $query  Optional query args appended to the URL.
	 * @return array|\WP_Error Decoded JSON response, or WP_Error on failure.
	 */
	public function request( $method, $url, $body = array(), $query = array() ) {
		return $this->authenticated_request( $method, $url, $body, $query, true );
	}

	/**
	 * Internal request handler with single-retry-on-401 support.
	 *
	 * @param string $method      HTTP method.
	 * @param string $url         Full request URL.
	 * @param array  $body        Request body.
	 * @param array  $query       Query args.
	 * @param bool   $allow_retry Whether a 401 response should trigger one token refresh + retry.
	 * @return array|\WP_Error
	 */
	private function authenticated_request( $method, $url, $body, $query, $allow_retry ) {
		$token = $this->get_access_token();

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

		if ( ! empty( $query ) ) {
			$url = add_query_arg( $query, $url );
		}

		$args = array(
			'method'  => strtoupper( $method ),
			'timeout' => self::TIMEOUT,
			'headers' => array(
				'Authorization' => 'Bearer ' . $token,
				'Content-Type'  => 'application/json',
			),
		);

		if ( ! empty( $body ) ) {
			$args['body'] = wp_json_encode( $body );
		}

		$response = wp_remote_request( $url, $args );

		if ( ! is_wp_error( $response ) && 401 === (int) wp_remote_retrieve_response_code( $response ) && $allow_retry ) {
			$refreshed = $this->refresh_access_token();

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

			return $this->authenticated_request( $method, $url, $body, array(), false );
		}

		return $this->decode_response( $response, 'google_api_request_failed' );
	}

	/**
	 * Creates a new Google Spreadsheet.
	 *
	 * @param string $title Spreadsheet title.
	 * @return array|\WP_Error Response containing `spreadsheetId`, or WP_Error.
	 */
	public function create_spreadsheet( $title ) {
		if ( '' === trim( (string) $title ) ) {
			return new \WP_Error( 'google_invalid_title', __( 'The spreadsheet title is required.', 'metform-pro' ) );
		}

		return $this->request(
			'POST',
			'https://sheets.googleapis.com/v4/spreadsheets',
			array( 'properties' => array( 'title' => $title ) ),
			array( 'fields' => 'spreadsheetId' )
		);
	}

	/**
	 * Overwrites values in a specific range (e.g. header row).
	 *
	 * @param string $id     Spreadsheet ID.
	 * @param string $range  A1 notation range, e.g. `Sheet1!A1:Z1`.
	 * @param array  $values 2D array of row values.
	 * @return array|\WP_Error
	 */
	public function update_values( $id, $range, $values ) {
		$valid = $this->validate_sheet_request( $id, $range );

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

		$url = sprintf( 'https://sheets.googleapis.com/v4/spreadsheets/%s/values/%s', rawurlencode( $id ), rawurlencode( $range ) );

		return $this->request( 'PUT', $url, array( 'values' => $values ), array( 'valueInputOption' => 'USER_ENTERED' ) );
	}

	/**
	 * Appends values as new row(s) after the last row with data in the range.
	 *
	 * @param string $id     Spreadsheet ID.
	 * @param string $range  A1 notation range, e.g. `Sheet1!A2:Z2`.
	 * @param array  $values 2D array of row values.
	 * @return array|\WP_Error
	 */
	public function append_values( $id, $range, $values ) {
		$valid = $this->validate_sheet_request( $id, $range );

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

		$url = sprintf( 'https://sheets.googleapis.com/v4/spreadsheets/%s/values/%s:append', rawurlencode( $id ), rawurlencode( $range ) );

		return $this->request( 'POST', $url, array( 'values' => $values ), array( 'valueInputOption' => 'USER_ENTERED' ) );
	}

	/**
	 * Lists all spreadsheets accessible to the authenticated account.
	 *
	 * @return array|\WP_Error Drive `files.list` response.
	 */
	public function list_spreadsheets() {
		return $this->request(
			'GET',
			'https://www.googleapis.com/drive/v3/files',
			array(),
			array(
				'q'                         => "mimeType='application/vnd.google-apps.spreadsheet'",
				'pageSize'                  => 1000,
				'includeItemsFromAllDrives' => 'true',
				'supportsAllDrives'         => 'true',
				'orderBy'                   => 'name',
			)
		);
	}

	/**
	 * Fetches metadata (including sheet/tab list) for a spreadsheet.
	 *
	 * @param string $id Spreadsheet ID.
	 * @return array|\WP_Error
	 */
	public function get_spreadsheet( $id ) {
		if ( '' === trim( (string) $id ) ) {
			return new \WP_Error( 'google_missing_spreadsheet_id', __( 'The spreadsheet ID is required.', 'metform-pro' ) );
		}

		return $this->request( 'GET', 'https://sheets.googleapis.com/v4/spreadsheets/' . rawurlencode( $id ) );
	}

	/**
	 * Reads the persisted token option from the database.
	 *
	 * @return array|\WP_Error Token data array, or WP_Error if missing/invalid.
	 */
	private function stored_token() {
		$value = get_option( 'wf_google_access_token' );
		$token = is_string( $value ) ? json_decode( $value, true ) : (array) $value;

		return is_array( $token ) && ! empty( $token['access_token'] )
			? $token
			: new \WP_Error( 'google_oauth_missing_token', __( 'Google is not connected.', 'metform-pro' ) );
	}

	/**
	 * Validates a spreadsheet ID / range pair before firing a request.
	 *
	 * @param string $id    Spreadsheet ID.
	 * @param string $range A1 notation range.
	 * @return true|\WP_Error
	 */
	private function validate_sheet_request( $id, $range ) {
		if ( '' === trim( (string) $id ) ) {
			return new \WP_Error( 'google_missing_spreadsheet_id', __( 'The spreadsheet ID is required.', 'metform-pro' ) );
		}

		if ( '' === trim( (string) $range ) || preg_match( '/[\x00-\x1F\x7F]/', $range ) ) {
			return new \WP_Error( 'google_invalid_range', __( 'The worksheet range is invalid.', 'metform-pro' ) );
		}

		return true;
	}

	/**
	 * Decodes a `wp_remote_*()` response into an array, or a WP_Error on failure.
	 *
	 * @param array|\WP_Error $response Raw HTTP response.
	 * @param string          $code     Error code to use if the request failed.
	 * @return array|\WP_Error
	 */
	private function decode_response( $response, $code ) {
		if ( is_wp_error( $response ) ) {
			return new \WP_Error( $code, $response->get_error_message() );
		}

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

		if ( JSON_ERROR_NONE !== json_last_error() ) {
			return new \WP_Error( 'google_malformed_json', __( 'Google returned an invalid response.', 'metform-pro' ), array( 'status' => $status ) );
		}

		if ( $status < 200 || $status >= 300 ) {
			$message = isset( $data['error']['message'] )
				? sanitize_text_field( $data['error']['message'] )
				: sprintf( __( 'Google API request failed with HTTP status %d.', 'metform-pro' ), $status );

			return new \WP_Error( $code, $message, array( 'status' => $status ) );
		}

		return $data;
	}
}