<?php
/**
 * Square OAuth access-token refresh (lazy + cron).
 *
 * @package SHC4WC
 */

// phpcs:disable Squiz.Commenting
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

class SHC4WC_OAuth_Token {
	const CRON_HOOK                  = 'shc4wc_oauth_token_refresh_cron';
	const LOCK_TRANSIENT             = 'shc4wc_oauth_refresh_lock';
	const REFRESH_BUFFER_SECONDS     = 600;
	const CRON_REFRESH_WITHIN_SECONDS = 86400;
	const MAX_TOKEN_AGE_SECONDS      = 604800;

	/**
	 * Register cron callback.
	 */
	public static function register_hooks() {
		add_action( self::CRON_HOOK, array( __CLASS__, 'cron_refresh' ) );
	}

	/**
	 * Schedule daily cron if not already scheduled.
	 */
	public static function maybe_schedule_cron() {
		if ( ! wp_next_scheduled( self::CRON_HOOK ) ) {
			wp_schedule_event( time() + HOUR_IN_SECONDS, 'daily', self::CRON_HOOK );
		}
	}

	/**
	 * Clear scheduled cron (deactivation).
	 */
	public static function unschedule_cron() {
		$timestamp = wp_next_scheduled( self::CRON_HOOK );
		while ( $timestamp ) {
			wp_unschedule_event( $timestamp, self::CRON_HOOK );
			$timestamp = wp_next_scheduled( self::CRON_HOOK );
		}
	}

	/**
	 * Proactive refresh when token expires within 24 hours.
	 */
	public static function cron_refresh() {
		if ( ! self::should_refresh( self::CRON_REFRESH_WITHIN_SECONDS ) ) {
			return;
		}
		self::refresh_access_token( false );
	}

	/**
	 * Return a valid access token, refreshing when needed.
	 *
	 * @param bool $force_refresh Skip expiry checks and refresh now.
	 * @return string Access token or empty string.
	 */
	public static function get_access_token( $force_refresh = false ) {
		$oauth = SHC4WC_Plugin::get_oauth_data();
		$token = isset( $oauth['access_token'] ) ? sanitize_text_field( (string) $oauth['access_token'] ) : '';
		if ( '' === $token ) {
			return '';
		}

		if ( $force_refresh || self::should_refresh( self::REFRESH_BUFFER_SECONDS ) ) {
			$refreshed = self::refresh_access_token( $force_refresh );
			if ( is_wp_error( $refreshed ) ) {
				self::log( 'OAuth token refresh failed', array( 'error' => $refreshed->get_error_message() ), 'error' );
				return $token;
			}
			return $refreshed;
		}

		return $token;
	}

	/**
	 * Required plugin scopes that are missing from the saved/access-token scopes.
	 *
	 * @param string|null $env Optional environment; defaults to active.
	 * @return string[]
	 */
	public static function get_missing_scopes( $env = null ) {
		$required = SHC4WC_Plugin::get_required_oauth_scopes();
		$granted  = self::get_token_scopes( $env );
		if ( empty( $granted ) ) {
			return $required;
		}

		$granted_map = array_fill_keys( array_map( 'strtoupper', $granted ), true );
		$missing     = array();
		foreach ( $required as $scope ) {
			if ( empty( $granted_map[ strtoupper( $scope ) ] ) ) {
				$missing[] = $scope;
			}
		}

		return $missing;
	}

	/**
	 * Scopes currently associated with the stored access token.
	 *
	 * Uses POST /oauth2/token/status when cache is stale (Connect page load).
	 *
	 * @param string|null $env   Optional environment.
	 * @param bool        $force Force a live token-status request.
	 * @return string[]
	 */
	public static function get_token_scopes( $env = null, $force = false ) {
		if ( null === $env ) {
			$env = SHC4WC_Plugin::get_current_environment();
		}
		$env   = ( 'live' === $env ) ? 'live' : 'sandbox';
		$oauth = SHC4WC_Plugin::get_oauth_data_for_environment( $env );
		$token = isset( $oauth['access_token'] ) ? trim( (string) $oauth['access_token'] ) : '';
		if ( '' === $token ) {
			return array();
		}

		$cached     = isset( $oauth['scopes'] ) && is_array( $oauth['scopes'] ) ? $oauth['scopes'] : array();
		$checked_at = self::parse_timestamp( $oauth['scopes_checked_at'] ?? '' );
		$cache_ok   = ! empty( $cached ) && $checked_at > 0 && ( time() - $checked_at ) < HOUR_IN_SECONDS;

		if ( ! $force && $cache_ok ) {
			return array_values( array_unique( array_map( 'strtoupper', $cached ) ) );
		}

		$status = self::retrieve_token_status( $token, $env );
		if ( is_wp_error( $status ) ) {
			self::log(
				'Token status check failed',
				array(
					'env'   => $env,
					'error' => $status->get_error_message(),
				),
				'warning'
			);
			return array_values( array_unique( array_map( 'strtoupper', $cached ) ) );
		}

		$scopes = isset( $status['scopes'] ) && is_array( $status['scopes'] ) ? $status['scopes'] : array();
		$clean  = array();
		foreach ( $scopes as $scope ) {
			$scope = strtoupper( sanitize_text_field( (string) $scope ) );
			if ( '' !== $scope ) {
				$clean[] = $scope;
			}
		}
		$clean = array_values( array_unique( $clean ) );

		$save = array(
			'scopes'            => $clean,
			'scopes_checked_at' => gmdate( 'c' ),
		);
		if ( ! empty( $status['expires_at'] ) ) {
			$save['expires_at'] = sanitize_text_field( (string) $status['expires_at'] );
		}
		if ( ! empty( $status['merchant_id'] ) ) {
			$save['merchant_id'] = sanitize_text_field( (string) $status['merchant_id'] );
		}

		SHC4WC_Plugin::save_oauth_data( $save, $env );

		return $clean;
	}

	/**
	 * POST /oauth2/token/status — Square token introspection.
	 *
	 * @param string $access_token Access token.
	 * @param string $env          live|sandbox.
	 * @return array<string,mixed>|WP_Error
	 */
	public static function retrieve_token_status( $access_token, $env = 'live' ) {
		$access_token = trim( (string) $access_token );
		if ( '' === $access_token ) {
			return new WP_Error( 'shc4wc_no_access_token', 'Square access token missing.' );
		}

		$env  = ( 'live' === $env ) ? 'live' : 'sandbox';
		$base = ( 'live' === $env ) ? 'https://connect.squareup.com' : 'https://connect.squareupsandbox.com';
		$url  = trailingslashit( $base ) . 'oauth2/token/status';

		$response = wp_remote_post(
			$url,
			array(
				'headers' => array(
					'Authorization'  => 'Bearer ' . $access_token,
					'Content-Type'   => 'application/json',
					'Square-Version' => '2024-12-18',
				),
				'timeout' => 30,
				'body'    => '{}',
			)
		);

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

		$code = (int) wp_remote_retrieve_response_code( $response );
		$data = json_decode( wp_remote_retrieve_body( $response ), true );
		if ( ! is_array( $data ) ) {
			$data = array();
		}

		if ( $code < 200 || $code >= 300 ) {
			$message = 'Square token status request failed';
			if ( ! empty( $data['errors'][0]['detail'] ) ) {
				$message = sanitize_text_field( (string) $data['errors'][0]['detail'] );
			} elseif ( ! empty( $data['errors'][0]['code'] ) ) {
				$message = sanitize_text_field( (string) $data['errors'][0]['code'] );
			}

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

		return $data;
	}

	/**
	 * Whether the stored access token should be refreshed.
	 *
	 * @param int|null $buffer_seconds Refresh this many seconds before expires_at.
	 */
	public static function should_refresh( $buffer_seconds = null ) {
		if ( null === $buffer_seconds ) {
			$buffer_seconds = self::REFRESH_BUFFER_SECONDS;
		}

		$oauth = SHC4WC_Plugin::get_oauth_data();
		if ( empty( $oauth['access_token'] ) || empty( $oauth['refresh_token'] ) ) {
			return false;
		}

		$refreshed_at = self::parse_timestamp( $oauth['token_refreshed_at'] ?? '' );
		if ( $refreshed_at > 0 && ( time() - $refreshed_at ) >= self::MAX_TOKEN_AGE_SECONDS ) {
			return true;
		}

		$expires = self::parse_timestamp( $oauth['expires_at'] ?? '' );
		if ( $expires <= 0 ) {
			return $refreshed_at <= 0;
		}

		return time() >= ( $expires - max( 0, (int) $buffer_seconds ) );
	}

	/**
	 * Exchange refresh_token for a new access token.
	 *
	 * @param bool $force When false and token is still valid, returns current token.
	 * @return string|WP_Error Access token.
	 */
	public static function refresh_access_token( $force = false ) {
		$oauth = SHC4WC_Plugin::get_oauth_data();
		$token = isset( $oauth['access_token'] ) ? (string) $oauth['access_token'] : '';
		if ( '' === $token ) {
			return new WP_Error( 'shc4wc_no_access_token', 'Square access token missing.' );
		}

		if ( ! $force && ! self::should_refresh( self::REFRESH_BUFFER_SECONDS ) ) {
			return $token;
		}

		$refresh_token = isset( $oauth['refresh_token'] ) ? trim( (string) $oauth['refresh_token'] ) : '';
		if ( '' === $refresh_token ) {
			return new WP_Error( 'shc4wc_no_refresh_token', 'Square refresh token missing.' );
		}

		if ( ! self::acquire_lock() ) {
			$waited = self::wait_for_fresh_token();
			if ( is_wp_error( $waited ) ) {
				return $waited;
			}
			if ( '' !== $waited ) {
				return $waited;
			}
		}

		try {
			if ( SHC4WC_Plugin::uses_worker_oauth() ) {
				$data = self::request_worker_refresh( $refresh_token );
				if ( is_wp_error( $data ) ) {
					$error_data = $data->get_error_data();
					$status       = is_array( $error_data ) && isset( $error_data['status'] ) ? (int) $error_data['status'] : 0;
					$creds        = SHC4WC_Plugin::get_credentials();
					if ( in_array( $status, array( 404, 405 ), true ) && ! empty( $creds['client_secret'] ) ) {
						$data = self::request_square_refresh( $refresh_token );
					}
				}
			} else {
				$data = self::request_square_refresh( $refresh_token );
			}

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

			if ( empty( $data['access_token'] ) ) {
				return new WP_Error( 'shc4wc_token_refresh_empty', 'Square token refresh returned no access token.' );
			}

			self::save_token_response( $data );
			return sanitize_text_field( (string) $data['access_token'] );
		} finally {
			self::release_lock();
		}
	}

	/**
	 * Persist token fields from a Square / Worker token response.
	 *
	 * @param array $data Token response.
	 */
	public static function save_token_response( array $data ) {
		$oauth = SHC4WC_Plugin::get_oauth_data();
		if ( ! is_array( $oauth ) ) {
			$oauth = array();
		}

		$oauth['access_token'] = sanitize_text_field( (string) $data['access_token'] );
		if ( ! empty( $data['refresh_token'] ) ) {
			$oauth['refresh_token'] = sanitize_text_field( (string) $data['refresh_token'] );
		}
		if ( isset( $data['expires_at'] ) ) {
			$oauth['expires_at'] = sanitize_text_field( (string) $data['expires_at'] );
		}
		if ( ! empty( $data['merchant_id'] ) ) {
			$oauth['merchant_id'] = sanitize_text_field( (string) $data['merchant_id'] );
		}
		if ( isset( $data['refresh_token_expires_at'] ) ) {
			$oauth['refresh_token_expires_at'] = sanitize_text_field( (string) $data['refresh_token_expires_at'] );
		}
		$oauth['token_refreshed_at'] = gmdate( 'c' );

		SHC4WC_Plugin::save_oauth_data( $oauth );
	}

	/**
	 * @param string $value ISO-8601 datetime or unix timestamp string.
	 */
	private static function parse_timestamp( $value ) {
		$value = trim( (string) $value );
		if ( '' === $value ) {
			return 0;
		}
		if ( ctype_digit( $value ) ) {
			return (int) $value;
		}
		$ts = strtotime( $value );
		return $ts ? (int) $ts : 0;
	}

	/**
	 * @return bool True when this request owns the refresh lock.
	 */
	private static function acquire_lock() {
		if ( get_transient( self::LOCK_TRANSIENT ) ) {
			return false;
		}
		set_transient( self::LOCK_TRANSIENT, time(), 30 );
		return true;
	}

	private static function release_lock() {
		delete_transient( self::LOCK_TRANSIENT );
	}

	/**
	 * Another request is refreshing; wait briefly and read the updated token.
	 *
	 * @return string|WP_Error
	 */
	private static function wait_for_fresh_token() {
		for ( $i = 0; $i < 10; $i++ ) {
			usleep( 300000 );
			if ( ! get_transient( self::LOCK_TRANSIENT ) && ! self::should_refresh( self::REFRESH_BUFFER_SECONDS ) ) {
				$oauth = SHC4WC_Plugin::get_oauth_data();
				$token = isset( $oauth['access_token'] ) ? sanitize_text_field( (string) $oauth['access_token'] ) : '';
				if ( '' !== $token ) {
					return $token;
				}
			}
		}

		return new WP_Error( 'shc4wc_token_refresh_busy', 'Square token refresh is in progress.' );
	}

	/**
	 * @param string $refresh_token Stored refresh token.
	 * @return array|WP_Error
	 */
	private static function request_square_refresh( $refresh_token ) {
		$settings = SHC4WC_Plugin::get_settings();
		$creds    = SHC4WC_Plugin::get_credentials();
		if ( empty( $creds['client_id'] ) || empty( $creds['client_secret'] ) ) {
			return new WP_Error( 'shc4wc_missing_oauth_creds', 'Square OAuth credentials missing for token refresh.' );
		}

		$api_base = ( isset( $settings['environment'] ) && 'live' === $settings['environment'] )
			? 'https://connect.squareup.com'
			: 'https://connect.squareupsandbox.com';

		$response = wp_remote_post(
			$api_base . '/oauth2/token',
			array(
				'headers' => array(
					'Content-Type' => 'application/json',
				),
				'body'    => wp_json_encode(
					array(
						'client_id'     => $creds['client_id'],
						'client_secret' => $creds['client_secret'],
						'grant_type'    => 'refresh_token',
						'refresh_token' => $refresh_token,
					)
				),
				'timeout' => 60,
			)
		);

		return self::parse_token_http_response( $response, 'shc4wc_square_token_refresh_failed' );
	}

	/**
	 * @param string $refresh_token Stored refresh token.
	 * @return array|WP_Error
	 */
	private static function request_worker_refresh( $refresh_token ) {
		$base     = SHC4WC_Plugin::get_oauth_worker_base();
		$site_key = SHC4WC_Plugin::get_oauth_site_key();
		$settings = SHC4WC_Plugin::get_settings();
		$env      = ( isset( $settings['environment'] ) && 'live' === $settings['environment'] ) ? 'live' : 'sandbox';

		if ( '' === $base || '' === $site_key ) {
			return new WP_Error( 'shc4wc_worker_refresh_misconfigured', 'OAuth Worker is not configured for token refresh.' );
		}

		$ts  = time();
		$sig = hash_hmac( 'sha256', $refresh_token . "\n" . $env . "\n" . $ts, $site_key );

		$response = wp_remote_post(
			$base . '/refresh',
			array(
				'headers' => array(
					'Content-Type' => 'application/json; charset=utf-8',
				),
				'body'    => wp_json_encode(
					array(
						'site_key'      => $site_key,
						'refresh_token' => $refresh_token,
						'env'           => $env,
						'ts'            => $ts,
						'sig'           => $sig,
					)
				),
				'timeout' => 60,
			)
		);

		return self::parse_token_http_response( $response, 'shc4wc_worker_token_refresh_failed' );
	}

	/**
	 * @param array|WP_Error $response wp_remote_post result.
	 * @param string         $code     WP_Error code on failure.
	 * @return array|WP_Error
	 */
	private static function parse_token_http_response( $response, $code ) {
		if ( is_wp_error( $response ) ) {
			return $response;
		}

		$http_code = (int) wp_remote_retrieve_response_code( $response );
		$data      = json_decode( wp_remote_retrieve_body( $response ), true );
		if ( ! is_array( $data ) ) {
			$data = array();
		}

		if ( $http_code < 200 || $http_code >= 300 || empty( $data['access_token'] ) ) {
			$message = isset( $data['error'] ) ? sanitize_text_field( (string) $data['error'] ) : 'Square token refresh failed';
			return new WP_Error(
				$code,
				$message,
				array(
					'status' => $http_code,
					'data'   => $data,
				)
			);
		}

		return $data;
	}

	/**
	 * @param string               $message Log line.
	 * @param array<string, mixed> $context Context.
	 * @param string               $level   Log level.
	 */
	private static function log( $message, $context = array(), $level = 'info' ) {
		$settings = SHC4WC_Plugin::get_settings();
		if ( empty( $settings['debug'] ) || 'yes' !== $settings['debug'] ) {
			return;
		}
		$line = $message . ' ' . wp_json_encode( $context );
		if ( function_exists( 'wc_get_logger' ) ) {
			wc_get_logger()->log( $level, $line, array( 'source' => 'shc4wc' ) );
			return;
		}
		// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
		error_log( '[shc4wc] ' . $line );
	}
}
